Add the ability to set emulator ROM folder (#348)

Emulators now can load games with the same file extensions if you either place ROMs into the folder named as the emulator in the config file (libretro.core.list value) or any folder as you specify in the `libretro.core.list.{emulator}.folder` config param.
This commit is contained in:
sergystepanov 2021-08-30 19:50:35 +03:00 committed by GitHub
parent e7a2b6a33e
commit 47a9d70b74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 109 additions and 26 deletions

14
configs/config.yaml vendored
View file

@ -125,10 +125,19 @@ emulator:
compression: zip
# Libretro core configuration
#
# The emulator selection will happen in this order:
# - based on the folder name in the folder param
# - based on the folder name (core name) in the list (i.e. nes, snes)
# - based on the rom names in the roms param
#
# Available config params:
# - lib (string)
# - config (string)
# - roms ([]string)
# - folder (string)
# By default emulator selection is based on the folder named as cores
# in the list (i.e. nes, snes) but if you specify folder param,
# then it will try to load the ROM file from that folder first.
# - width (int) -- broken
# - height (int) -- broken
# - ratio (float)
@ -142,11 +151,16 @@ emulator:
pcsx:
lib: pcsx_rearmed_libretro
roms: [ "cue" ]
# example of folder override
folder: psx
# MAME core requires additional manual setup, please read:
# https://docs.libretro.com/library/fbneo/
mame:
lib: fbneo_libretro
roms: [ "zip" ]
mame2003:
lib: mame2003_libretro
roms: [ "zip" ]
nes:
lib: nestopia_libretro
roms: [ "nes" ]

View file

@ -3,6 +3,7 @@ package emulator
import (
"path"
"path/filepath"
"strings"
)
type Emulator struct {
@ -42,6 +43,7 @@ type LibretroCoreConfig struct {
Lib string
Config string
Roms []string
Folder string
Width int
Height int
Ratio float64
@ -64,17 +66,24 @@ func (e Emulator) GetLibretroCoreConfig(emulator string) LibretroCoreConfig {
return conf
}
// GetEmulatorByRom returns emulator name by its supported ROM name.
// !to cache into an optimized data structure
func (e Emulator) GetEmulatorByRom(rom string) string {
// GetEmulator tries to find a suitable emulator.
// !to remove quadratic complexity
func (e Emulator) GetEmulator(rom string, path string) string {
found := ""
for emu, core := range e.Libretro.Cores.List {
for _, romName := range core.Roms {
if rom == romName {
return emu
found = emu
if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
folder := p[0]
if (folder != "" && folder == core.Folder) || folder == emu {
return emu
}
}
}
}
}
return ""
return found
}
func (e Emulator) GetSupportedExtensions() []string {

View file

@ -0,0 +1,52 @@
package emulator
import "testing"
func TestGetEmulator(t *testing.T) {
tests := []struct {
rom string
path string
config map[string]LibretroCoreConfig
emulator string
}{
{
rom: "nes",
path: "test/game.nes",
config: map[string]LibretroCoreConfig{
"snes": {Roms: []string{"nes"}},
"nes": {Folder: "test", Roms: []string{"nes"}},
},
emulator: "nes",
},
{
rom: "nes",
path: "nes/game.nes",
config: map[string]LibretroCoreConfig{
"snes": {Roms: []string{"nes"}},
"nes": {Roms: []string{"nes"}},
},
emulator: "nes",
},
{
rom: "nes",
path: "test/game.nes",
config: map[string]LibretroCoreConfig{
"snes": {Roms: []string{"nes"}},
"nes": {Roms: []string{"nes"}},
},
emulator: "nes",
},
}
emu := Emulator{
Libretro: LibretroConfig{},
}
for _, test := range tests {
emu.Libretro.Cores.List = test.config
em := emu.GetEmulator(test.rom, test.path)
if test.emulator != em {
t.Errorf("expected result: %v, but was %v with: %v, %v", test.emulator, em, test.rom, test.path)
}
}
}

View file

@ -205,6 +205,7 @@ func newGameStartCall(roomId string, data string, library games.GameLibrary) (ap
return api.GameStartCall{
Name: gameInfo.Name,
Base: gameInfo.Base,
Path: gameInfo.Path,
Type: gameInfo.Type,
}, nil

View file

@ -30,6 +30,7 @@ func (packet *GameStartRequest) From(data string) error { return from(packet, da
type GameStartCall struct {
Name string `json:"name"`
Base string `json:"base"`
Path string `json:"path"`
Type string `json:"type"`
}

View file

@ -74,6 +74,7 @@ type GameMetadata struct {
Name string
// the game file extension (e.g. nes, n64)
Type string
Base string
// the game path relative to the library base path
Path string
}
@ -126,8 +127,8 @@ func (lib *library) GetAll() []GameMetadata {
func (lib *library) FindGameByName(name string) GameMetadata {
var game GameMetadata
if val, ok := lib.games[name]; ok {
val.Path = filepath.Join(lib.config.path, val.Path)
game = val
val.Base = lib.config.path
return val
}
return game
}

View file

@ -1,7 +1,6 @@
package games
import (
"reflect"
"testing"
)
@ -31,7 +30,19 @@ func TestLibraryScan(t *testing.T) {
return meta.Name
})
if !reflect.DeepEqual(test.expected, list) {
// ^2 complexity (;
all := true
for _, expect := range test.expected {
found := false
for _, game := range list {
if game == expect {
found = true
break
}
}
all = all && found
}
if !all {
t.Errorf("Test fail for dir %v with %v != %v", test.directory, list, test.expected)
}
}

View file

@ -110,28 +110,21 @@ func (h *Handler) handleGameStart() cws.PacketHandler {
log.Println("Received a start request from coordinator")
session := h.getSession(resp.SessionID)
if session == nil {
log.Printf("Error: No session for ID: %s\n", resp.SessionID)
log.Printf("error: no session with id: %s", resp.SessionID)
return cws.EmptyPacket
}
peerconnection := session.peerconnection
// TODO: Standardize for all types of packet. Make WSPacket generic
startPacket := api.GameStartCall{}
if err := startPacket.From(resp.Data); err != nil {
rom := api.GameStartCall{}
if err := rom.From(resp.Data); err != nil {
return cws.EmptyPacket
}
gameMeta := games.GameMetadata{
Name: startPacket.Name,
Type: startPacket.Type,
Path: startPacket.Path,
}
room := h.startGameHandler(gameMeta, resp.RoomID, resp.PlayerIndex, peerconnection)
game := games.GameMetadata{Name: rom.Name, Type: rom.Type, Base: rom.Base, Path: rom.Path}
room := h.startGameHandler(game, resp.RoomID, resp.PlayerIndex, session.peerconnection)
session.RoomID = room.ID
// TODO: can data race
// TODO: can data race (and it does)
h.rooms[room.ID] = room
return cws.WSPacket{ID: "start", RoomID: room.ID}
return cws.WSPacket{ID: api.GameStart, RoomID: room.ID}
}
}

View file

@ -10,6 +10,7 @@ import (
"math"
"net"
"os"
"path/filepath"
"sync"
"github.com/giongto35/cloud-game/v2/pkg/config/worker"
@ -157,8 +158,8 @@ func NewRoom(roomID string, game games.GameMetadata, onlineStorage *storage.Clie
// If not then load room or create room from local.
log.Printf("Room %s started. GameName: %s, WithGame: %t", roomID, game.Name, cfg.Encoder.WithoutGame)
// Spawn new emulator based on gameName and plug-in all channels
emuName := cfg.Emulator.GetEmulatorByRom(game.Type)
// Spawn new emulator and plug-in all channels
emuName := cfg.Emulator.GetEmulator(game.Type, game.Path)
libretroConfig := cfg.Emulator.GetLibretroCoreConfig(emuName)
if cfg.Encoder.WithoutGame {
@ -176,7 +177,7 @@ func NewRoom(roomID string, game games.GameMetadata, onlineStorage *storage.Clie
room.audioChannel = audioChannel
}
gameMeta := room.director.LoadMeta(game.Path)
gameMeta := room.director.LoadMeta(filepath.Join(game.Base, game.Path))
// nwidth, nheight are the WebRTC output size
var nwidth, nheight int