From 29d718de08c85803ebd6fe4211701cb8805a90ee Mon Sep 17 00:00:00 2001 From: giongto35 Date: Tue, 27 Aug 2019 07:26:33 +0800 Subject: [PATCH] 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 --- cmd/main.go | 5 ++- config/config.go | 6 ++++ go.mod | 2 +- overlord/gamelist/games.go | 23 ------------- overlord/handlers.go | 15 +++++--- static/js/controller.js | 6 ++-- static/js/ws.js | 11 +++--- util/gamelist/games.go | 70 ++++++++++++++++++++++++++++++++++++++ worker/handlers.go | 7 ++-- worker/overlord.go | 5 ++- worker/room/room.go | 66 ++++++++++++++--------------------- 11 files changed, 126 insertions(+), 90 deletions(-) delete mode 100644 overlord/gamelist/games.go create mode 100644 util/gamelist/games.go diff --git a/cmd/main.go b/cmd/main.go index a8ac70ab..2cf819cf 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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. diff --git a/config/config.go b/config/config.go index 4e5b32bc..3b0a5ca3 100644 --- a/config/config.go +++ b/config/config.go @@ -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", +} diff --git a/go.mod b/go.mod index d3fdadf8..1677cd96 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/overlord/gamelist/games.go b/overlord/gamelist/games.go deleted file mode 100644 index be8ad3b9..00000000 --- a/overlord/gamelist/games.go +++ /dev/null @@ -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 -} diff --git a/overlord/handlers.go b/overlord/handlers.go index 1270a4a9..95da71b2 100644 --- a/overlord/handlers.go +++ b/overlord/handlers.go @@ -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) } diff --git a/static/js/controller.js b/static/js/controller.js index bd467ac6..d8dfbc1b 100644 --- a/static/js/controller.js +++ b/static/js/controller.js @@ -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(``); + listbox.append(``); }); } @@ -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]}`); } diff --git a/static/js/ws.js b/static/js/ws.js index 75a234a2..3d171282 100644 --- a/static/js/ws.js +++ b/static/js/ws.js @@ -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(); diff --git a/util/gamelist/games.go b/util/gamelist/games.go new file mode 100644 index 00000000..495b6f38 --- /dev/null +++ b/util/gamelist/games.go @@ -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, + } +} diff --git a/worker/handlers.go b/worker/handlers.go index 838f4f50..867fa144 100644 --- a/worker/handlers.go +++ b/worker/handlers.go @@ -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 diff --git a/worker/overlord.go b/worker/overlord.go index d465afb3..e1f67377 100644 --- a/worker/overlord.go +++ b/worker/overlord.go @@ -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) diff --git a/worker/room/room.go b/worker/room/room.go index 634adb59..47481d6c 100644 --- a/worker/room/room.go +++ b/worker/room/room.go @@ -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 }