diff --git a/assets/emulator/libretro/cores/snes9x_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/snes9x_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..a0e87118 Binary files /dev/null and b/assets/emulator/libretro/cores/snes9x_libretro.armv7-neon-hf.so differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.dll b/assets/emulator/libretro/cores/snes9x_libretro.dll new file mode 100755 index 00000000..63e6fc91 Binary files /dev/null and b/assets/emulator/libretro/cores/snes9x_libretro.dll differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.dylib b/assets/emulator/libretro/cores/snes9x_libretro.dylib new file mode 100644 index 00000000..3a20f715 Binary files /dev/null and b/assets/emulator/libretro/cores/snes9x_libretro.dylib differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.so b/assets/emulator/libretro/cores/snes9x_libretro.so index f13bfbab..e32cdd89 100644 Binary files a/assets/emulator/libretro/cores/snes9x_libretro.so and b/assets/emulator/libretro/cores/snes9x_libretro.so differ diff --git a/pkg/config/config.go b/pkg/config/config.go index ce0381ec..921b10d3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -61,6 +61,7 @@ type EmulatorMeta struct { Rotation image.Rotate IsGlAllowed bool UsesLibCo bool + HasMultitap bool } var EmulatorConfig = map[string]EmulatorMeta{ @@ -80,9 +81,10 @@ var EmulatorConfig = map[string]EmulatorMeta{ Height: 240, }, "snes": { - Path: "assets/emulator/libretro/cores/mednafen_snes_libretro", + Path: "assets/emulator/libretro/cores/snes9x_libretro", Width: 256, Height: 224, + HasMultitap: true, }, "mame": { Path: "assets/emulator/libretro/cores/fbneo_libretro", diff --git a/pkg/coordinator/browser.go b/pkg/coordinator/browser.go index 7fb3064a..efcb4f2e 100644 --- a/pkg/coordinator/browser.go +++ b/pkg/coordinator/browser.go @@ -171,4 +171,19 @@ func (o *Server) RouteBrowser(client *BrowserClient) { return resp }) + + client.Receive("multitap", func(resp cws.WSPacket) (req cws.WSPacket) { + client.Println("Received multitap request from a browser -> relay to worker") + + // TODO: Async + resp.SessionID = client.SessionID + resp.RoomID = client.RoomID + wc, ok := o.workerClients[client.WorkerID] + if !ok { + return cws.EmptyPacket + } + resp = wc.SyncSend(resp) + + return resp + }) } diff --git a/pkg/emulator/libretro/nanoarch/cfuncs.go b/pkg/emulator/libretro/nanoarch/cfuncs.go index d71a250c..66433fb9 100644 --- a/pkg/emulator/libretro/nanoarch/cfuncs.go +++ b/pkg/emulator/libretro/nanoarch/cfuncs.go @@ -83,6 +83,10 @@ bool bridge_retro_unserialize(void *f, void *data, size_t size) { return ((bool (*)(void*, size_t))f)(data, size); } +void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) { + return ((void (*)(unsigned, unsigned))f)(port, device); +} + bool coreEnvironment_cgo(unsigned cmd, void *data) { bool coreEnvironment(unsigned, void*); return coreEnvironment(cmd, data); diff --git a/pkg/emulator/libretro/nanoarch/naemulator.go b/pkg/emulator/libretro/nanoarch/naemulator.go index 61001536..c32a068b 100644 --- a/pkg/emulator/libretro/nanoarch/naemulator.go +++ b/pkg/emulator/libretro/nanoarch/naemulator.go @@ -189,7 +189,7 @@ func (na *naEmulator) listenInput() { } func (na *naEmulator) LoadMeta(path string) config.EmulatorMeta { - coreLoad(na.meta.Path, na.meta.IsGlAllowed, na.meta.UsesLibCo, na.meta.Config) + coreLoad(na.meta) coreLoadGame(path) na.gamePath = path @@ -255,6 +255,14 @@ func (na *naEmulator) LoadGame() error { return nil } +func (na *naEmulator) ToggleMultitap() error { + if na.roomID != "" { + toggleMultitap() + } + + return nil +} + func (na *naEmulator) GetHashPath() string { return util.GetSavePath(na.roomID) } diff --git a/pkg/emulator/libretro/nanoarch/nanoarch.go b/pkg/emulator/libretro/nanoarch/nanoarch.go index 49ff0d76..0ab0a11e 100644 --- a/pkg/emulator/libretro/nanoarch/nanoarch.go +++ b/pkg/emulator/libretro/nanoarch/nanoarch.go @@ -49,6 +49,7 @@ void* bridge_retro_get_memory_data(void *f, unsigned id); bool bridge_retro_serialize(void *f, void *data, size_t size); bool bridge_retro_unserialize(void *f, void *data, size_t size); size_t bridge_retro_serialize_size(void *f); +void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device); bool coreEnvironment_cgo(unsigned cmd, void *data); void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch); @@ -100,6 +101,12 @@ var isGlAllowed bool var usesLibCo bool var coreConfig ConfigProperties +var multitap struct { + supported bool + enabled bool + value C.unsigned +} + var systemDirectory = C.CString("./pkg/emulator/libretro/system") var saveDirectory = C.CString(".") var currentUser *C.char @@ -131,6 +138,7 @@ type CloudEmulator interface { LoadGame() error GetHashPath() string Close() + ToggleMultitap() error } //export coreVideoRefresh @@ -328,6 +336,22 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool { return true } return false + case C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO: + if (multitap.supported) { + info := (*[100]C.struct_retro_controller_info)(data) + var i C.unsigned + for i = 0; unsafe.Pointer(info[i].types) != nil; i++ { + var j C.unsigned + types := (*[100]C.struct_retro_controller_description)(unsafe.Pointer(info[i].types)) + for j = 0; j < info[i].num_types; j++ { + if (C.GoString(types[j].desc) == "Multitap") { + multitap.value = types[j].id + return true + } + } + } + } + return false default: //fmt.Println("[Env]: command not implemented", cmd) return false @@ -478,6 +502,7 @@ var retroGetMemoryData unsafe.Pointer var retroSerializeSize unsafe.Pointer var retroSerialize unsafe.Pointer var retroUnserialize unsafe.Pointer +var retroSetControllerPortDevice unsafe.Pointer func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer { cs := C.CString(name) @@ -486,15 +511,19 @@ func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer { return pointer } -func coreLoad(pathNoExt string, isGlAllowedParam bool, usesLibCoParam bool, pathToCfg string) { - isGlAllowed = isGlAllowedParam - usesLibCo = usesLibCoParam - coreConfig = ScanConfigFile(pathToCfg) +func coreLoad(meta config.EmulatorMeta) { + isGlAllowed = meta.IsGlAllowed + usesLibCo = meta.UsesLibCo + coreConfig = ScanConfigFile(meta.Config) + + multitap.supported = meta.HasMultitap + multitap.enabled = false + multitap.value = 0 mu.Lock() // Different OS requires different library, bruteforce till it finish for _, ext := range config.EmulatorExtension { - pathWithExt := pathNoExt + ext + pathWithExt := meta.Path + ext cs := C.CString(pathWithExt) retroHandle = C.dlopen(cs, C.RTLD_LAZY) C.free(unsafe.Pointer(cs)) @@ -505,7 +534,7 @@ func coreLoad(pathNoExt string, isGlAllowedParam bool, usesLibCoParam bool, path if retroHandle == nil { err := C.dlerror() - log.Fatalf("error loading %s, err %+v", pathNoExt, *err) + log.Fatalf("error loading %s, err %+v", meta.Path, *err) } retroInit = loadFunction(retroHandle, "retro_init") @@ -525,6 +554,7 @@ func coreLoad(pathNoExt string, isGlAllowedParam bool, usesLibCoParam bool, path retroSerializeSize = loadFunction(retroHandle, "retro_serialize_size") retroSerialize = loadFunction(retroHandle, "retro_serialize") retroUnserialize = loadFunction(retroHandle, "retro_unserialize") + retroSetControllerPortDevice = loadFunction(retroHandle, "retro_set_controller_port_device") mu.Unlock() @@ -650,6 +680,21 @@ func coreLoadGame(filename string) { } } +func toggleMultitap() { + if (multitap.supported && multitap.value != 0) { + // Official SNES games only support a single multitap device + // Most require it to be plugged in player 2 port + // And Snes9X requires it to be "plugged" after the game is loaded + // Control this from the browser since player 2 will stop working in some games if multitap is "plugged" in + if (multitap.enabled) { + C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, C.RETRO_DEVICE_JOYPAD) + } else { + C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, multitap.value) + } + multitap.enabled = !multitap.enabled + } +} + // serializeSize returns the amount of data the implementation requires to serialize // internal state (save states). // Between calls to retro_load_game() and retro_unload_game(), the diff --git a/pkg/emulator/type.go b/pkg/emulator/type.go index b2ae62e3..b5a5cabe 100644 --- a/pkg/emulator/type.go +++ b/pkg/emulator/type.go @@ -19,4 +19,6 @@ type CloudEmulator interface { GetHashPath() string // Close will be called when the game is done Close() + + ToggleMultitap() error } diff --git a/pkg/worker/coordinator.go b/pkg/worker/coordinator.go index c7d6eaa6..052fa030 100644 --- a/pkg/worker/coordinator.go +++ b/pkg/worker/coordinator.go @@ -265,6 +265,26 @@ func (h *Handler) RouteCoordinator() { return req }) + oClient.Receive( + "multitap", + func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a multitap toggle from coordinator") + req.ID = "multitap" + req.Data = "ok" + if resp.RoomID != "" { + room := h.getRoom(resp.RoomID) + err := room.ToggleMultitap() + if err != nil { + log.Println("[!] Could not toggle multitap state: ", err) + req.Data = "error" + } + } else { + req.Data = "error" + } + + return req + }) + oClient.Receive( "terminateSession", func(resp cws.WSPacket) (req cws.WSPacket) { diff --git a/pkg/worker/room/room.go b/pkg/worker/room/room.go index ec738cea..10e83dfa 100644 --- a/pkg/worker/room/room.go +++ b/pkg/worker/room/room.go @@ -451,6 +451,12 @@ func (r *Room) LoadGame() error { return err } +func (r *Room) ToggleMultitap() error { + err := r.director.ToggleMultitap() + + return err +} + func (r *Room) EmptySessions() bool { return len(r.rtcSessions) == 0 } diff --git a/web/js/controller.js b/web/js/controller.js index 49778f5a..a61ba3b5 100644 --- a/web/js/controller.js +++ b/web/js/controller.js @@ -389,6 +389,12 @@ updatePlayerIndex(3); break; + // toggle multitap + case KEY.MULTITAP: + socket.toggleMultitap(); + break; + + // quit case KEY.QUIT: input.poll().disable(); diff --git a/web/js/input/joystick.js b/web/js/input/joystick.js index caab2f8b..c5ee2fdb 100644 --- a/web/js/input/joystick.js +++ b/web/js/input/joystick.js @@ -205,19 +205,40 @@ const joystick = (() => { } // https://bugs.chromium.org/p/chromium/issues/detail?id=1076272 - if (browser === 'chrome' && gamepad.id.includes('PLAYSTATION(R)3')) { - joystickMap = { - 0: KEY.A, - 1: KEY.B, - 2: KEY.Y, - 3: KEY.X, - 4: KEY.L, - 5: KEY.R, - 8: KEY.SELECT, - 9: KEY.START, - 10: KEY.DTOGGLE, - 11: KEY.R3, - }; + if (gamepad.id.includes('PLAYSTATION(R)3')) { + if (browser === 'chrome') { + joystickMap = { + 1: KEY.A, + 0: KEY.B, + 2: KEY.Y, + 3: KEY.X, + 4: KEY.L, + 5: KEY.R, + 8: KEY.SELECT, + 9: KEY.START, + 10: KEY.DTOGGLE, + 11: KEY.R3, + }; + } else { + joystickMap = { + 13: KEY.A, + 14: KEY.B, + 12: KEY.X, + 15: KEY.Y, + 3: KEY.START, + 0: KEY.SELECT, + 4: KEY.UP, + 6: KEY.DOWN, + 7: KEY.LEFT, + 5: KEY.RIGHT, + 10: KEY.L, + 11: KEY.R, + 8: KEY.L2, + 9: KEY.R2, + 1: KEY.DTOGGLE, + 2: KEY.R3, + }; + } } // reset state diff --git a/web/js/input/keyboard.js b/web/js/input/keyboard.js index 127c75bd..79b4ef4d 100644 --- a/web/js/input/keyboard.js +++ b/web/js/input/keyboard.js @@ -30,7 +30,8 @@ const keyboard = (() => { KeyF: KEY.FULL, KeyH: KEY.HELP, Backslash: KEY.STATS, - Digit9: KEY.SETTINGS + Digit9: KEY.SETTINGS, + KeyM: KEY.MULTITAP }); let keyMap = {}; diff --git a/web/js/input/keys.js b/web/js/input/keys.js index 73075493..724d370c 100644 --- a/web/js/input/keys.js +++ b/web/js/input/keys.js @@ -29,5 +29,6 @@ const KEY = (() => { R2: 'r2', L3: 'l3', R3: 'r3', + MULTITAP: 'multitap', } })(); diff --git a/web/js/network/socket.js b/web/js/network/socket.js index caf79a34..02800a87 100644 --- a/web/js/network/socket.js +++ b/web/js/network/socket.js @@ -114,6 +114,7 @@ const socket = (() => { "player_index": playerIndex }); const quitGame = (roomId) => send({"id": "quit", "data": "", "room_id": roomId}); + const toggleMultitap = () => send({"id": "multitap", "data": ""}); return { init: init, @@ -123,6 +124,7 @@ const socket = (() => { loadGame: loadGame, updatePlayerIndex: updatePlayerIndex, startGame: startGame, - quitGame: quitGame + quitGame: quitGame, + toggleMultitap: toggleMultitap, } })($, event, log);