Add game library module (#232)

* Add game library

* Add missing local game lib files

* Add missing return statement

* Use v2 suffix

* Bump the dependencies
This commit is contained in:
sergystepanov 2020-10-10 16:42:06 +03:00 committed by GitHub
parent a42aebec88
commit 83efc733a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 521 additions and 151 deletions

10
go.mod vendored
View file

@ -7,6 +7,7 @@ require (
cloud.google.com/go/storage v1.12.0
github.com/disintegration/imaging v1.6.2
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3
github.com/fsnotify/fsnotify v1.4.9
github.com/gen2brain/x264-go v0.0.0-20200605131102-0523307cbe23
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
github.com/gofrs/uuid v3.3.0+incompatible
@ -25,11 +26,10 @@ require (
github.com/prometheus/procfs v0.2.0 // indirect
github.com/spf13/pflag v1.0.5
github.com/veandco/go-sdl2 v0.4.4
golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 // indirect
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f // indirect
golang.org/x/tools v0.0.0-20200930213115-e57f6d466a48 // indirect
google.golang.org/genproto v0.0.0-20200930140634-01fc692af84b // indirect
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d // indirect
google.golang.org/genproto v0.0.0-20201002142447-3860012362da // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20200710132758-e28f8214483b
)

16
go.sum vendored
View file

@ -568,8 +568,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae h1:duLSQW+DZ5MsXKX7kc4rXlq6/mmxz4G6ewJuBPlhRe0=
golang.org/x/crypto v0.0.0-20200930160638-afb6bcd081ae/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -652,8 +652,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10 h1:YfxMZzv3PjGonQYNUaeU2+DhAdqOxerQ30JFB6WgAXo=
golang.org/x/net v0.0.0-20200930145003-4acb6c075d10/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c h1:dk0ukUIHmGHqASjP0iue2261isepFCC6XRCSd1nHgDw=
golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -788,8 +788,8 @@ golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4X
golang.org/x/tools v0.0.0-20200918232735-d647fc253266 h1:k7tVuG0g1JwmD3Jh8oAl1vQ1C3jb4Hi/dUl1wWDBJpQ=
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20200930213115-e57f6d466a48 h1:/N7U6gDrclaaWCzrdxtKeyEcUx2goazt5PelznylQNM=
golang.org/x/tools v0.0.0-20200930213115-e57f6d466a48/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
@ -869,8 +869,8 @@ google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200930140634-01fc692af84b h1:GJQgk4o/yzqYEu9Y6V4gRCI54KRlkOE0xAZeEGUUJjw=
google.golang.org/genproto v0.0.0-20200930140634-01fc692af84b/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201002142447-3860012362da h1:DTQYk4u7nICKkkVZsBv0/0po0ChISxAJ5CTAfUhO0PQ=
google.golang.org/genproto v0.0.0-20201002142447-3860012362da/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=

View file

@ -0,0 +1,41 @@
package api
import (
"encoding/json"
)
// This list of postfixes is used in the API:
// - *Request postfix denotes clients calls (i.e. from a browser to the HTTP-server).
// - *Call postfix denotes IPC calls (from the coordinator to a worker).
type GameStartRequest struct {
GameName string `json:"game_name"`
IsMobile bool `json:"is_mobile"`
}
func (packet *GameStartRequest) From(data string) error { return from(packet, data) }
type GameStartCall struct {
Name string `json:"name"`
Path string `json:"path"`
Type string `json:"type"`
}
func (packet *GameStartCall) From(data string) error { return from(packet, data) }
func (packet *GameStartCall) To() (string, error) { return to(packet) }
func from(source interface{}, data string) error {
err := json.Unmarshal([]byte(data), source)
if err != nil {
return err
}
return nil
}
func to(target interface{}) (string, error) {
b, err := json.Marshal(target)
if err != nil {
return "", err
}
return string(b), nil
}

View file

@ -47,6 +47,8 @@ var FileTypeToEmulator = map[string]string{
"z64": "n64",
}
var SupportedRomExtensions = listSupportedRomExtensions()
// There is no good way to determine main width and height of the emulator.
// When game run, frame width and height can scale abnormally.
type EmulatorMeta struct {
@ -103,3 +105,11 @@ var EmulatorConfig = map[string]EmulatorMeta{
}
var EmulatorExtension = []string{".so", ".armv7-neon-hf.so", ".dylib", ".dll"}
func listSupportedRomExtensions() []string {
m := make([]string, 0, len(FileTypeToEmulator))
for k := range FileTypeToEmulator {
m = append(m, k)
}
return m
}

View file

@ -1,10 +1,14 @@
package coordinator
import (
"errors"
"fmt"
"log"
"github.com/giongto35/cloud-game/v2/pkg/api"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/worker/room"
"github.com/gorilla/websocket"
)
@ -118,6 +122,17 @@ func (o *Server) RouteBrowser(client *BrowserClient) {
if !ok {
return cws.EmptyPacket
}
// +injects game data into the original game request
gameStartCall, err := newGameStartCall(resp.RoomID, resp.Data, o.library)
if err != nil {
return cws.EmptyPacket
}
if packet, err := gameStartCall.To(); err != nil {
return cws.EmptyPacket
} else {
resp.Data = packet
}
workerResp := wc.SyncSend(resp)
// Response from worker contains initialized roomID. Set roomID to the session
@ -187,3 +202,34 @@ func (o *Server) RouteBrowser(client *BrowserClient) {
return resp
})
}
// newGameStartCall gathers data for a new game start call of the worker
func newGameStartCall(roomId string, data string, library games.GameLibrary) (api.GameStartCall, error) {
request := api.GameStartRequest{}
if err := request.From(data); err != nil {
return api.GameStartCall{}, errors.New("invalid request")
}
// the name of the game either in the `room id` field or
// it's in the initial request
game := request.GameName
if roomId != "" {
// ! should be moved into coordinator
name := room.GetGameNameFromRoomID(roomId)
if name == "" {
return api.GameStartCall{}, errors.New("couldn't decode game name from the room id")
}
game = name
}
gameInfo := library.FindGameByName(game)
if gameInfo.Path == "" {
return api.GameStartCall{}, fmt.Errorf("couldn't find game info for the game %v", game)
}
return api.GameStartCall{
Name: gameInfo.Name,
Path: gameInfo.Path,
Type: gameInfo.Type,
}, nil
}

View file

@ -6,20 +6,22 @@ import (
)
type Config struct {
Port int
PublicDomain string
PingServer string
URLPrefix string
DebugHost string
Port int
PublicDomain string
PingServer string
URLPrefix string
DebugHost string
LibraryMonitoring bool
MonitoringConfig monitoring.ServerMonitoringConfig
}
func NewDefaultConfig() Config {
return Config{
Port: 8800,
PublicDomain: "http://localhost:8000",
PingServer: "",
Port: 8800,
PublicDomain: "http://localhost:8000",
PingServer: "",
LibraryMonitoring: false,
MonitoringConfig: monitoring.ServerMonitoringConfig{
Port: 6601,
@ -40,6 +42,7 @@ func (c *Config) AddFlags(fs *pflag.FlagSet) *Config {
fs.StringVarP(&c.DebugHost, "debughost", "d", "", "Specify the server want to connect directly to debug")
fs.StringVarP(&c.PublicDomain, "domain", "n", c.PublicDomain, "Specify the public domain of the coordinator")
fs.StringVarP(&c.PingServer, "pingServer", "", c.PingServer, "Specify the worker address that the client can ping (with protocol and port)")
fs.BoolVarP(&c.LibraryMonitoring, "libMonitor", "", c.LibraryMonitoring, "Enable ROM library monitoring")
return c
}

View file

@ -9,10 +9,10 @@ import (
"time"
"github.com/giongto35/cloud-game/v2/pkg/config"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/monitoring"
"github.com/golang/glog"
"github.com/gorilla/mux"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
@ -95,7 +95,17 @@ func makeHTTPToHTTPSRedirectServer(server *Server) *http.Server {
// initializeCoordinator setup an coordinator server
func (o *Coordinator) initializeCoordinator() {
coordinator := NewServer(o.cfg)
// init games library
lib := games.NewLibrary(games.Config{
BasePath: "assets/games",
Supported: config.SupportedRomExtensions,
Ignored: []string{"neogeo", "pgm"},
Verbose: true,
WatchMode: o.cfg.LibraryMonitoring,
})
lib.Scan()
coordinator := NewServer(o.cfg, lib)
var certManager *autocert.Manager
var httpsSrv *http.Server

View file

@ -12,8 +12,8 @@ import (
"github.com/giongto35/cloud-game/v2/pkg/config"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/util"
"github.com/giongto35/cloud-game/v2/pkg/util/gamelist"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
)
@ -24,6 +24,8 @@ const (
type Server struct {
cfg Config
// games library
library games.GameLibrary
// roomToWorker map roomID to workerID
roomToWorker map[string]string
// workerClients are the map workerID to worker Client
@ -38,9 +40,10 @@ const devPingServer = "http://localhost:9000/echo"
var upgrader = websocket.Upgrader{}
var errNotFound = errors.New("Not found")
func NewServer(cfg Config) *Server {
func NewServer(cfg Config, library games.GameLibrary) *Server {
return &Server{
cfg: cfg,
cfg: cfg,
library: library,
// Mapping roomID to server
roomToWorker: map[string]string{},
// Mapping workerID to worker
@ -245,7 +248,7 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
bc.Send(cws.WSPacket{
ID: "init",
Data: createInitPackage(wc.StunTurnServer),
Data: createInitPackage(wc.StunTurnServer, o.library.GetAll()),
}, nil)
// If peerconnection is done (client.Done is signalled), we close peerconnection
@ -404,9 +407,9 @@ func (o *Server) cleanWorker(wc *WorkerClient, workerID string) {
// createInitPackage returns serverhost + game list in encoded wspacket format
// This package will be sent to initialize
func createInitPackage(stunturn string) string {
func createInitPackage(stunturn string, games []games.GameMetadata) string {
var gameName []string
for _, game := range gamelist.GameList {
for _, game := range games {
gameName = append(gameName, game.Name)
}

View file

@ -131,6 +131,23 @@ func (c *Client) SyncSend(request WSPacket) (response WSPacket) {
return <-res
}
// SendAwait sends some packet while waiting for a tile-limited response
//func (c *Client) SendAwait(packet WSPacket) WSPacket {
// ch := make(chan WSPacket)
// defer close(ch)
// c.Send(packet, func(response WSPacket) { ch <- response })
//
// for {
// select {
// case packet := <-ch:
// return packet
// case <-time.After(config.WsIpcTimeout):
// log.Printf("Packet receive timeout!")
// return EmptyPacket
// }
// }
//}
// Heartbeat maintains connection to server
func (c *Client) Heartbeat() {
// send heartbeat every 1s

289
pkg/games/game_library.go Normal file
View file

@ -0,0 +1,289 @@
package games
import (
"crypto/md5"
"fmt"
"io"
"log"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
)
// Config is an external configuration
type Config struct {
// some directory which is gonna be
// the root folder for the library
BasePath string
// a list of supported file extensions
Supported []string
// a list of ignored words in the files
Ignored []string
// print some additional info
Verbose bool
// enable directory changes watch
WatchMode bool
}
// libConf is an optimized internal library configuration
type libConf struct {
path string
supported map[string]bool
ignored map[string]bool
verbose bool
watchMode bool
}
type library struct {
config libConf
// indicates repo source existence
hasSource bool
// scan time
lastScanDuration time.Duration
// library entries
// !should be a tree-based structure
// game name -> game meta
// games with duplicate names are merged
games map[string]GameMetadata
// to restrict parallel execution
// or throttling
// !CAS would be better
mu sync.Mutex
isScanning bool
isScanningDelayed bool
}
type GameLibrary interface {
GetAll() []GameMetadata
FindGameByName(name string) GameMetadata
Scan()
}
type GameMetadata struct {
uid string
// the display name of the game
Name string
// the game file extension (e.g. nes, n64)
Type string
// the game path relative to the library base path
Path string
}
func NewLibrary(conf Config) GameLibrary {
hasSource := true
dir, err := filepath.Abs(conf.BasePath)
if err != nil {
hasSource = false
log.Printf("[lib] invalid source: %v (%v)\n", conf.BasePath, err)
}
library := &library{
config: libConf{
path: dir,
supported: toMap(conf.Supported),
ignored: toMap(conf.Ignored),
verbose: conf.Verbose,
watchMode: conf.WatchMode,
},
mu: sync.Mutex{},
games: map[string]GameMetadata{},
hasSource: hasSource,
}
if conf.WatchMode && hasSource {
go library.watch()
}
return library
}
func (lib *library) GetAll() []GameMetadata {
var res []GameMetadata
for _, value := range lib.games {
res = append(res, value)
}
return res
}
// FindGameByName returns some game info with its full filepath
func (lib *library) FindGameByName(name string) GameMetadata {
var game GameMetadata
if val, ok := lib.games[name]; ok {
val.Path = path.Join(lib.config.path, val.Path)
game = val
}
return game
}
func (lib *library) Scan() {
if !lib.hasSource {
log.Printf("[lib] scan... skipped (no source)\n")
return
}
// scan throttling
lib.mu.Lock()
if lib.isScanning {
defer lib.mu.Unlock()
lib.isScanningDelayed = true
log.Printf("[lib] scan... delayed\n")
return
}
lib.isScanning = true
lib.mu.Unlock()
log.Printf("[lib] scan... started\n")
start := time.Now()
var games []GameMetadata
dir := lib.config.path
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info != nil && !info.IsDir() && lib.isFileExtensionSupported(path) {
meta := getMetadata(path, dir)
meta.uid = hash(path)
if !lib.config.ignored[meta.Name] {
games = append(games, meta)
}
}
return nil
})
if err != nil {
log.Printf("[lib] scan error with %q: %v\n", dir, err)
}
if len(games) > 0 {
lib.set(games)
}
lib.lastScanDuration = time.Since(start)
if lib.config.verbose {
lib.dumpLibrary()
}
// run scan again if delayed
lib.mu.Lock()
defer lib.mu.Unlock()
lib.isScanning = false
if lib.isScanningDelayed {
lib.isScanningDelayed = false
go lib.Scan()
}
log.Printf("[lib] scan... completed\n")
}
// watch adds the ability to rescan the entire library
// during filesystem changes in a watched directory.
// !to add incremental library change
func (lib *library) watch() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Printf("[lib] watcher has failed: %v", err)
return
}
done := make(chan bool)
go func(repo *library) {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op == fsnotify.Create || event.Op == fsnotify.Remove {
// !to try to add the proper file/dir add/remove scan logic
// which is tricky
repo.Scan()
}
case _, ok := <-watcher.Errors:
if !ok {
return
}
}
}
}(lib)
if err = watcher.Add(lib.config.path); err != nil {
log.Printf("[lib] watch error %v", err)
}
<-done
_ = watcher.Close()
log.Printf("[lib] the watch has ended\n")
}
func (lib *library) set(games []GameMetadata) {
res := make(map[string]GameMetadata)
for _, value := range games {
res[value.Name] = value
}
lib.games = res
}
func (lib *library) isFileExtensionSupported(path string) bool {
ext := filepath.Ext(path)
if ext == "" {
return false
}
return lib.config.supported[ext[1:]]
}
// getMetadata returns game info from a path
func getMetadata(path string, basePath string) GameMetadata {
name := filepath.Base(path)
ext := filepath.Ext(name)
relPath, _ := filepath.Rel(basePath, path)
return GameMetadata{
Name: strings.TrimSuffix(name, ext),
Type: ext[1:],
Path: relPath,
}
}
// dumpLibrary printouts the current library snapshot of games
func (lib *library) dumpLibrary() {
var gameList strings.Builder
for _, game := range lib.games {
gameList.WriteString(" " + game.Name + " (" + game.Path + ")" + "\n")
}
log.Printf("\n"+
"--------------------------------------------\n"+
"--- The Library of ROMs ---\n"+
"--------------------------------------------\n"+
"%v"+
"--------------------------------------------\n"+
"--- ROMs: %03d %26s ---\n"+
"--------------------------------------------\n",
gameList.String(), len(lib.games), lib.lastScanDuration)
}
// hash makes an MD5 hash of the string
func hash(str string) string {
h := md5.New()
_, err := io.WriteString(h, str)
if err != nil {
return ""
}
return fmt.Sprintf("%x", h.Sum(nil))
}
func toMap(list []string) map[string]bool {
res := make(map[string]bool)
for _, s := range list {
res[s] = true
}
return res
}

View file

@ -0,0 +1,46 @@
package games
import (
"reflect"
"testing"
)
func TestLibraryScan(t *testing.T) {
tests := []struct {
directory string
expected []string
}{
{
directory: "../../assets/games",
expected: []string{
"Super Mario Bros", "Sushi The Cat", "anguna",
},
},
}
for _, test := range tests {
library := NewLibrary(Config{
BasePath: test.directory,
Supported: []string{"gba", "zip", "nes"},
Ignored: []string{"neogeo", "pgm"},
})
library.Scan()
games := library.GetAll()
list := _map(games, func(meta GameMetadata) string {
return meta.Name
})
if !reflect.DeepEqual(test.expected, list) {
t.Errorf("Test fail for dir %v with %v != %v", test.directory, list, test.expected)
}
}
}
func _map(vs []GameMetadata, f func(info GameMetadata) string) []string {
vsm := make([]string, len(vs))
for i, v := range vs {
vsm[i] = f(v)
}
return vsm
}

View file

@ -6,13 +6,3 @@ func MinInt(x int, y int) int {
}
return y
}
func ContainsString(sslice []string, s string) bool {
for _, ss := range sslice {
if ss == s {
return true
}
}
return false
}

View file

@ -1,78 +0,0 @@
package gamelist
import (
"os"
"path/filepath"
"strings"
"github.com/giongto35/cloud-game/v2/pkg/config"
"github.com/giongto35/cloud-game/v2/pkg/util"
)
type GameInfo struct {
Name string
Path string
Type string
}
const gamePath = "assets/games"
var excludeFiles = []string{"neogeo", "pgm"}
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)
// Skip file that is in excludeFiles (bios files)
if util.ContainsString(excludeFiles, gameInfo.Name) {
return nil
}
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 := filepath.Base(path)
ext := filepath.Ext(fileName)
return GameInfo{
Name: strings.TrimSuffix(fileName, ext),
Type: ext[1:],
Path: path,
}
}

View file

@ -5,7 +5,9 @@ import (
"log"
"strconv"
"github.com/giongto35/cloud-game/v2/pkg/api"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/util"
"github.com/giongto35/cloud-game/v2/pkg/webrtc"
"github.com/giongto35/cloud-game/v2/pkg/worker/room"
@ -156,18 +158,17 @@ func (h *Handler) RouteCoordinator() {
peerconnection := session.peerconnection
// TODO: Standardize for all types of packet. Make WSPacket generic
var startPacket struct {
GameName string `json:"game_name"`
IsMobile bool `json:"is_mobile"`
}
err := json.Unmarshal([]byte(resp.Data), &startPacket)
if err != nil {
log.Println("Error: Cannot decode json:", err)
startPacket := api.GameStartCall{}
if err := startPacket.From(resp.Data); err != nil {
return cws.EmptyPacket
}
gameMeta := games.GameMetadata{
Name: startPacket.Name,
Type: startPacket.Type,
Path: startPacket.Path,
}
room := h.startGameHandler(startPacket.GameName, resp.RoomID, resp.PlayerIndex, peerconnection, util.GetVideoEncoder(startPacket.IsMobile))
room := h.startGameHandler(gameMeta, resp.RoomID, resp.PlayerIndex, peerconnection, util.GetVideoEncoder(false))
session.RoomID = room.ID
// TODO: can data race
h.rooms[room.ID] = room
@ -317,8 +318,8 @@ func getServerIDOfRoom(oc *CoordinatorClient, roomID string) string {
}
// startGameHandler starts a game if roomID is given, if not create new room
func (h *Handler) startGameHandler(gameName, existedRoomID string, playerIndex int, peerconnection *webrtc.WebRTC, videoEncoderType string) *room.Room {
log.Println("Starting game", gameName)
func (h *Handler) startGameHandler(game games.GameMetadata, existedRoomID string, playerIndex int, peerconnection *webrtc.WebRTC, videoEncoderType string) *room.Room {
log.Printf("Loading game: %v\n", game.Name)
// If we are connecting to coordinator, request corresponding serverID based on roomID
// TODO: check if existedRoomID is in the current server
room := h.getRoom(existedRoomID)
@ -326,7 +327,7 @@ func (h *Handler) startGameHandler(gameName, existedRoomID string, playerIndex i
if room == nil {
log.Println("Got Room from local ", room, " ID: ", existedRoomID)
// Create new room and update player index
room = h.createNewRoom(gameName, existedRoomID, videoEncoderType)
room = h.createNewRoom(game, existedRoomID, videoEncoderType)
room.UpdatePlayerIndex(peerconnection, playerIndex)
// Wait for done signal from room

View file

@ -10,7 +10,7 @@ import (
"github.com/giongto35/cloud-game/v2/pkg/config"
"github.com/giongto35/cloud-game/v2/pkg/config/worker"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/util"
"github.com/giongto35/cloud-game/v2/pkg/webrtc"
storage "github.com/giongto35/cloud-game/v2/pkg/worker/cloud-storage"
@ -170,12 +170,12 @@ func (h *Handler) detachRoom(roomID string) {
// createNewRoom creates a new room
// Return nil in case of room is existed
func (h *Handler) createNewRoom(gameName string, roomID string, videoEncoderType string) *room.Room {
func (h *Handler) createNewRoom(game games.GameMetadata, roomID string, videoEncoderType string) *room.Room {
// If the roomID is empty,
// 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, gameName, videoEncoderType, h.onlineStorage, h.cfg)
room := room.NewRoom(roomID, game, videoEncoderType, h.onlineStorage, h.cfg)
// TODO: Might have race condition
h.rooms[room.ID] = room
return room

View file

@ -16,13 +16,12 @@ import (
"strings"
"sync"
"github.com/giongto35/cloud-game/v2/pkg/config/worker"
"github.com/giongto35/cloud-game/v2/pkg/config"
"github.com/giongto35/cloud-game/v2/pkg/config/worker"
"github.com/giongto35/cloud-game/v2/pkg/emulator"
"github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/nanoarch"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/util"
"github.com/giongto35/cloud-game/v2/pkg/util/gamelist"
"github.com/giongto35/cloud-game/v2/pkg/webrtc"
storage "github.com/giongto35/cloud-game/v2/pkg/worker/cloud-storage"
)
@ -128,18 +127,12 @@ func NewVideoImporter(roomID string) chan nanoarch.GameFrame {
}
// NewRoom creates a new room
func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStorage *storage.Client, cfg worker.Config) *Room {
// If no roomID is given, generate it from gameName
// If the is roomID, get gameName from roomID
func NewRoom(roomID string, game games.GameMetadata, videoEncoderType string, onlineStorage *storage.Client, cfg worker.Config) *Room {
if roomID == "" {
roomID = generateRoomID(gameName)
} else {
gameName = getGameNameFromRoomID(roomID)
log.Println("Get Gamename from RoomID", gameName)
roomID = generateRoomID(game.Name)
}
gameInfo := gamelist.GetGameInfoFromName(gameName)
log.Println("Init new room: ", roomID, gameName, gameInfo)
log.Println("Init new room: ", roomID, game)
inputChannel := make(chan nanoarch.InputEvent, 100)
room := &Room{
@ -158,7 +151,7 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor
}
// Check if room is on local storage, if not, pull from GCS to local storage
go func(game gamelist.GameInfo, roomID string) {
go func(game games.GameMetadata, roomID string) {
// Check room is on local or fetch from server
savepath := util.GetSavePath(roomID)
log.Println("Check ", savepath, " on online storage : ", room.isGameOnLocal(savepath))
@ -167,7 +160,7 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor
}
// If not then load room or create room from local.
log.Printf("Room %s started. GamePath: %s, GameName: %s, WithGame: %t", roomID, game.Path, game.Name, cfg.WithoutGame)
log.Printf("Room %s started. GameName: %s, WithGame: %t", roomID, game.Name, cfg.WithoutGame)
// Spawn new emulator based on gameName and plug-in all channels
emuName, _ := config.FileTypeToEmulator[game.Type]
@ -227,7 +220,7 @@ func NewRoom(roomID string, gameName string, videoEncoderType string, onlineStor
// TODO: do we need GC, we can remove it
runtime.GC()
}(gameInfo, roomID)
}(game, roomID)
return room
}
@ -250,7 +243,7 @@ func getEmulator(emuName string, roomID string, imageChannel chan<- nanoarch.Gam
}
// getGameNameFromRoomID parse roomID to get roomID and gameName
func getGameNameFromRoomID(roomID string) string {
func GetGameNameFromRoomID(roomID string) string {
parts := strings.Split(roomID, separator)
if len(parts) > 1 {
return parts[1]

View file

@ -12,11 +12,10 @@ import (
"github.com/giongto35/cloud-game/v2/pkg/config"
"github.com/giongto35/cloud-game/v2/pkg/config/worker"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"github.com/giongto35/cloud-game/v2/pkg/monitoring"
"github.com/golang/glog"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
type Worker struct {