mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-07-20 16:54:25 +00:00
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:
parent
a42aebec88
commit
83efc733a8
17 changed files with 521 additions and 151 deletions
10
go.mod
vendored
10
go.mod
vendored
|
|
@ -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
16
go.sum
vendored
|
|
@ -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=
|
||||
|
|
|
|||
41
pkg/api/coordinator_api.go
Normal file
41
pkg/api/coordinator_api.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
289
pkg/games/game_library.go
Normal 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
|
||||
}
|
||||
46
pkg/games/game_library_test.go
Normal file
46
pkg/games/game_library_test.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue