Move gamelist to become a utility library, this package can be shared between overlord and worker (#72)

* Return games based on type

* Add nes extension also

* Update game list logic

* Add more files to diff
This commit is contained in:
giongto35 2019-08-27 07:26:33 +08:00 committed by GitHub
parent 0790a6d5c3
commit 29d718de08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 90 deletions

View file

@ -20,8 +20,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const gamePath = "games"
// Time allowed to write a message to the peer.
var upgrader = websocket.Upgrader{}
@ -49,7 +47,7 @@ func initilizeOverlord() {
// initializeWorker setup a worker
func initializeWorker() {
worker := worker.NewHandler(*config.OverlordHost, gamePath)
worker := worker.NewHandler(*config.OverlordHost)
defer func() {
log.Println("Close worker")
@ -103,6 +101,7 @@ func main() {
if *config.IsMonitor {
go monitor()
}
// There are two server mode
// Overlord is coordinator. If the OvelordHost Param is `overlord`, we spawn a new host as Overlord.
// else we spawn new server as normal server connecting to OverlordHost.

View file

@ -24,3 +24,9 @@ var MatchWorkerRandom = false
var ProdEnv = "prod"
var Codec = CODEC_H264
var FileTypeToEmulator = map[string]string{
"gba": "gba",
"cue": "pcsx",
"zip": "mame",
"nes": "nes",
}

2
go.mod vendored
View file

@ -10,6 +10,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pion/webrtc/v2 v2.1.2
github.com/prometheus/client_golang v1.1.0 // indirect
github.com/prometheus/client_golang v1.1.0
gopkg.in/hraban/opus.v2 v2.0.0-20180426093920-0f2e0b4fc6cd
)

View file

@ -1,23 +0,0 @@
package gamelist
import (
"os"
"path/filepath"
)
// getGameList returns list of games stored in games
// TODO: change to class
func GetGameList(gamePath string) []string {
var games []string
filepath.Walk(gamePath, func(path string, info os.FileInfo, err error) error {
if info != nil && !info.IsDir() {
// Remove prefix to obtain file names
path = path[len(gamePath)+1:]
// Add to games list
games = append(games, path)
}
return nil
})
return games
}

View file

@ -13,8 +13,8 @@ import (
"github.com/giongto35/cloud-game/config"
"github.com/giongto35/cloud-game/cws"
"github.com/giongto35/cloud-game/overlord/gamelist"
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/util/gamelist"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
)
@ -25,6 +25,7 @@ const (
)
type Server struct {
// roomToServer map roomID to workerID
roomToServer map[string]string
// workerClients are the map serverID to worker Client
workerClients map[string]*WorkerClient
@ -161,7 +162,7 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
wssession.BrowserClient.Send(cws.WSPacket{
ID: "init",
Data: createInitPackage(o.workerClients[serverID].StunTurnServer, gamePath),
Data: createInitPackage(o.workerClients[serverID].StunTurnServer),
}, nil)
// If peerconnection is done (client.Done is signalled), we close peerconnection
@ -290,9 +291,13 @@ func (o *Server) cleanConnection(client *WorkerClient, serverID string) {
// createInitPackage returns serverhost + game list in encoded wspacket format
// This package will be sent to initialize
func createInitPackage(stunturn, gamePath string) string {
gameList := gamelist.GetGameList(gamePath)
initPackage := append([]string{stunturn}, gameList...)
func createInitPackage(stunturn string) string {
var gameName []string
for _, game := range gamelist.GameList {
gameName = append(gameName, game.Name)
}
initPackage := append([]string{stunturn}, gameName...)
encodedList, _ := json.Marshal(initPackage)
return string(encodedList)
}

View file

@ -37,14 +37,14 @@ function reloadGameMenu() {
// sort gameList first
gameList.sort(function (a, b) {
return a.name > b.name ? 1 : -1;
return a > b ? 1 : -1;
});
// generate html
var listbox = $("#menu-container");
listbox.html('');
gameList.forEach(function (game) {
listbox.append(`<div class="menu-item unselectable" unselectable="on"><div><span>${game.name}</span></div></div>`);
listbox.append(`<div class="menu-item unselectable" unselectable="on"><div><span>${game}</span></div></div>`);
});
}
@ -88,7 +88,7 @@ function pickGame(idx) {
$(`.menu-item:eq(${idx}) span`).addClass("pick");
gameIdx = idx;
log(`> [Pick] game ${gameIdx + 1}/${gameList.length} - ${gameList[gameIdx].name}`);
log(`> [Pick] game ${gameIdx + 1}/${gameList.length} - ${gameList[gameIdx]}`);
}

11
static/js/ws.js vendored
View file

@ -35,10 +35,8 @@ conn.onmessage = e => {
data.shift()
gameList = [];
data.forEach(file => {
var file = file
var name = file.substr(0, file.indexOf('.'));
gameList.push({file: file, name: name});
data.forEach(name => {
gameList.push(name);
});
log("Received game list");
@ -63,7 +61,7 @@ conn.onmessage = e => {
// TODO: Calc time
break;
case "start":
roomID = d["room_id"];
roomID = d["room_id"];
log(`Got start with room id: ${roomID}`);
popup("Started! You can share you game!")
saveRoomID(roomID);
@ -272,8 +270,7 @@ function startGame() {
log("Starting game screen");
screenState = "game";
// conn.send(JSON.stringify({"id": "start", "data": gameList[gameIdx].file, "room_id": $("#room-txt").val(), "player_index": parseInt(playerIndex.value, 10)}));
conn.send(JSON.stringify({"id": "start", "data": gameList[gameIdx].file, "room_id": roomID != null ? roomID : '', "player_index": 1}));
conn.send(JSON.stringify({"id": "start", "data": gameList[gameIdx], "room_id": roomID != null ? roomID : '', "player_index": 1}));
// clear menu screen
stopGameInputTimer();

70
util/gamelist/games.go Normal file
View file

@ -0,0 +1,70 @@
package gamelist
import (
"os"
"path/filepath"
"strings"
"github.com/giongto35/cloud-game/config"
)
type GameInfo struct {
Name string
Path string
Type string
}
const gamePath = "games"
var GameList []GameInfo
func init() {
GameList = getAllGames(gamePath)
}
// GetGameInfoFromName returns game info from a gameName
func GetGameInfoFromName(name string) GameInfo {
for _, game := range GameList {
if game.Name == name {
return game
}
}
return GameInfo{}
}
// getAllGames returns list of games stored in games. This call should be called when server start (package init)
// TODO: Maybe later we need to make realtime update without server restart
func getAllGames(gamePath string) []GameInfo {
var games []GameInfo
filepath.Walk(gamePath, func(path string, info os.FileInfo, err error) error {
if info != nil && !info.IsDir() && isValidGameType(path) {
// Add to games list
gameInfo := getGameInfo(path)
games = append(games, gameInfo)
}
return nil
})
return games
}
// isValidGameType check if a game is valid for cloud retro based on extension
func isValidGameType(gamePath string) bool {
ext := filepath.Ext(gamePath)[1:]
_, ok := config.FileTypeToEmulator[ext]
return ok
}
// getGameInfo returns game info from a path
func getGameInfo(path string) GameInfo {
// Remove prefix to obtain file names
fileName := path[len(gamePath)+1:]
ext := filepath.Ext(fileName)
return GameInfo{
Name: strings.TrimSuffix(fileName, ext),
Type: ext[1:],
Path: path,
}
}

View file

@ -27,8 +27,6 @@ type Handler struct {
rooms map[string]*room.Room
// ID of the current server globalwise
serverID string
// Path to game list
gamePath string
// onlineStorage is client accessing to online storage (GCP)
onlineStorage *storage.Client
// sessions handles all sessions server is handler (key is sessionID)
@ -36,14 +34,13 @@ type Handler struct {
}
// NewHandler returns a new server
func NewHandler(overlordHost string, gamePath string) *Handler {
func NewHandler(overlordHost string) *Handler {
onlineStorage := storage.NewInitClient()
return &Handler{
rooms: map[string]*room.Room{},
sessions: map[string]*Session{},
overlordHost: overlordHost,
gamePath: gamePath,
onlineStorage: onlineStorage,
}
}
@ -132,7 +129,7 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int)
// or the roomID doesn't have any running sessions (room was closed)
// we spawn a new room
if roomID == "" || !h.isRoomRunning(roomID) {
room := room.NewRoom(roomID, h.gamePath, gameName, h.onlineStorage)
room := room.NewRoom(roomID, gameName, h.onlineStorage)
// TODO: Might have race condition
h.rooms[room.ID] = room
return room

View file

@ -76,8 +76,7 @@ func (h *Handler) RouteOverlord() {
"start",
func(resp cws.WSPacket) (req cws.WSPacket) {
log.Println("Received a start request from overlord")
session, ok := h.sessions[resp.SessionID]
log.Println("Find ", resp.SessionID, session, ok)
session, _ := h.sessions[resp.SessionID]
peerconnection := session.peerconnection
room := h.startGameHandler(resp.Data, resp.RoomID, resp.PlayerIndex, peerconnection)
@ -195,7 +194,7 @@ func getServerIDOfRoom(oc *OverlordClient, roomID string) string {
}
func (h *Handler) startGameHandler(gameName, roomID string, playerIndex int, peerconnection *webrtc.WebRTC) *room.Room {
log.Println("Starting game")
log.Println("Starting game", gameName)
// If we are connecting to overlord, request corresponding serverID based on roomID
// TODO: check if roomID is in the current server
room := h.getRoom(roomID)

View file

@ -11,9 +11,11 @@ import (
"strings"
"sync"
"github.com/giongto35/cloud-game/config"
"github.com/giongto35/cloud-game/emulator"
"github.com/giongto35/cloud-game/libretro/nanoarch"
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/util/gamelist"
"github.com/giongto35/cloud-game/webrtc"
storage "github.com/giongto35/cloud-game/worker/cloud-storage"
)
@ -48,12 +50,17 @@ type Room struct {
}
// NewRoom creates a new room
func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *Room {
// if no roomID is given, generate it
func NewRoom(roomID string, gameName string, onlineStorage *storage.Client) *Room {
// If no roomID is given, generate it from gameName
// If the is roomID, get gameName from roomID
if roomID == "" {
roomID = generateRoomID(gameName)
} else {
gameName = getGameNameFromRoomID(roomID)
}
log.Println("Init new room", roomID, gameName)
gameInfo := gamelist.GetGameInfoFromName(gameName)
log.Println("Init new room: ", roomID, gameName, gameInfo)
imageChannel := make(chan *image.RGBA, 30)
audioChannel := make(chan float32, 30)
inputChannel := make(chan int, 100)
@ -73,7 +80,7 @@ func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *
}
// Check if room is on local storage, if not, pull from GCS to local storage
go func(gamePath, gameName, roomID string) {
go func(game gamelist.GameInfo, roomID string) {
// Check room is on local or fetch from server
savepath := util.GetSavePath(roomID)
log.Println("Check ", savepath, " on local : ", room.isGameOnLocal(savepath))
@ -85,16 +92,11 @@ func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *
}
}
if roomID != "" {
gameName = getGameNameFromRoomID(roomID)
}
log.Printf("Room %s started. GamePath: %s, GameName: %s", roomID, gamePath, gameName)
log.Printf("Room %s started. GamePath: %s, GameName: %s", roomID, game.Path, game.Name)
// Spawn new emulator based on gameName and plug-in all channels
room.director = getEmulator(gameName, roomID, imageChannel, audioChannel, inputChannel)
path := gamePath + "/" + gameName
meta := room.director.LoadMeta(path)
log.Printf("Load with Meta %+v", meta)
room.director = getEmulator(game.Type, roomID, imageChannel, audioChannel, inputChannel)
meta := room.director.LoadMeta(game.Path)
go room.startVideo(meta.Width, meta.Height)
go room.startAudio(meta.AudioSampleRate)
room.director.Start()
@ -103,32 +105,24 @@ func NewRoom(roomID, gamePath, gameName string, onlineStorage *storage.Client) *
// TODO: do we need GC, we can remove it
runtime.GC()
}(gamePath, gameName, roomID)
}(gameInfo, roomID)
return room
}
// create director
func getEmulator(gameName string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) emulator.CloudEmulator {
gameType := getGameType(gameName)
switch gameType {
case "nes":
func getEmulator(gameType string, roomID string, imageChannel chan<- *image.RGBA, audioChannel chan<- float32, inputChannel <-chan int) emulator.CloudEmulator {
if gameType == "nes" {
return emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel)
case "gba":
nanoarch.Init("gba", roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator
case "bin":
nanoarch.Init("pcsx", roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator
case "zip":
nanoarch.Init("mame", roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator
}
return nil
ename, ok := config.FileTypeToEmulator[gameType]
if !ok {
return nil
}
nanoarch.Init(ename, roomID, imageChannel, audioChannel, inputChannel)
return nanoarch.NAEmulator
}
// getGameNameFromRoomID parse roomID to get roomID and gameName
@ -140,19 +134,11 @@ func getGameNameFromRoomID(roomID string) string {
return parts[1]
}
func getGameType(gameName string) string {
parts := strings.Split(gameName, ".")
if len(parts) <= 1 {
return ""
}
return parts[len(parts)-1]
}
// generateRoomID generate a unique room ID containing 16 digits
func generateRoomID(gameName string) string {
// RoomID contains random number + gameName
// Next time when we only get roomID, we can launch game based on gameName
roomID := strconv.FormatInt(rand.Int63(), 16) + "|" + gameName
log.Println("Generate Room ID", roomID)
//roomID := uuid.Must(uuid.NewV4()).String()
return roomID
}