From 91ace06f8b6cce4ed79280e5014c7710b006be91 Mon Sep 17 00:00:00 2001 From: Sergey Stepanov Date: Tue, 5 Mar 2024 21:34:37 +0300 Subject: [PATCH] Replace the hasMultitap option with a more general solution The new hid option enables users to map a specific Libretro device (or multiple devices) to the input ports. For instance, this allows users to map a Multitap controller with the snes9x core. --- pkg/api/api.go | 3 - pkg/api/worker.go | 3 - pkg/config/config.yaml | 12 +++- pkg/config/emulator.go | 2 +- pkg/coordinator/user.go | 2 - pkg/coordinator/userhandlers.go | 2 - pkg/coordinator/workerapi.go | 4 -- pkg/worker/caged/libretro/frontend.go | 5 +- .../caged/libretro/nanoarch/nanoarch.go | 72 ++++++++----------- pkg/worker/coordinator.go | 6 -- pkg/worker/coordinatorhandlers.go | 9 --- web/index.html | 4 +- web/js/api/api.js | 2 - web/js/controller.js | 3 - web/js/input/keyboard.js | 1 - web/js/input/keys.js | 1 - web/js/settings/settings.js | 2 +- 17 files changed, 46 insertions(+), 87 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index f85daa8de..34353bb08 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -77,7 +77,6 @@ const ( SaveGame PT = 106 LoadGame PT = 107 ChangePlayer PT = 108 - ToggleMultitap PT = 109 RecordGame PT = 110 GetWorkerList PT = 111 ErrNoFreeSlots PT = 112 @@ -112,8 +111,6 @@ func (p PT) String() string { return "SaveGame" case LoadGame: return "LoadGame" - case ToggleMultitap: - return "ToggleMultitap" case RecordGame: return "RecordGame" case GetWorkerList: diff --git a/pkg/api/worker.go b/pkg/api/worker.go index 2b2fc4ddd..a4078f5e2 100644 --- a/pkg/api/worker.go +++ b/pkg/api/worker.go @@ -45,9 +45,6 @@ type ( TerminateSessionRequest[T Id] struct { Stateful[T] } - ToggleMultitapRequest[T Id] struct { - StatefulRoom[T] - } WebrtcAnswerRequest[T Id] struct { Stateful[T] Sdp string `json:"sdp"` diff --git a/pkg/config/config.yaml b/pkg/config/config.yaml index 27f1eaac2..39b695ea8 100644 --- a/pkg/config/config.yaml +++ b/pkg/config/config.yaml @@ -180,8 +180,12 @@ emulator: # - ratio (float) # - isGlAllowed (bool) # - usesLibCo (bool) - # - hasMultitap (bool) + # - hasMultitap (bool) -- (removed) # - coreAspectRatio (bool) -- correct the aspect ratio on the client with the info from the core. + # - hid (map[int][]int) + # A list of device IDs to bind to the input ports. + # Some cores allow binding multiple devices to a single port (DosBox), but typically, + # you should bind just one device to one port. # - vfr (bool) # (experimental) # Enable variable frame rate only for cores that can't produce a constant frame rate. @@ -226,7 +230,11 @@ emulator: snes: lib: snes9x_libretro roms: [ "smc", "sfc", "swc", "fig", "bs" ] - hasMultitap: true + hid: + # set the 2nd port to RETRO_DEVICE_JOYPAD_MULTITAP ((1<<8) | 1) as SNES9x requires it + # in order to support up to 5-player games + # see: https://nintendo.fandom.com/wiki/Super_Multitap + 1: 257 n64: lib: mupen64plus_next_libretro roms: [ "n64", "v64", "z64" ] diff --git a/pkg/config/emulator.go b/pkg/config/emulator.go index da8f5b2ea..49bf6713e 100644 --- a/pkg/config/emulator.go +++ b/pkg/config/emulator.go @@ -44,8 +44,8 @@ type LibretroCoreConfig struct { CoreAspectRatio bool Folder string Hacks []string - HasMultitap bool Height int + Hid map[int][]int IsGlAllowed bool Lib string Options map[string]string diff --git a/pkg/coordinator/user.go b/pkg/coordinator/user.go index e2a3d60b4..00479fda2 100644 --- a/pkg/coordinator/user.go +++ b/pkg/coordinator/user.go @@ -84,8 +84,6 @@ func (u *User) HandleRequests(info HasServerInfo, launcher games.Launcher, conf return api.ErrMalformed } u.HandleChangePlayer(*rq) - case api.ToggleMultitap: - u.HandleToggleMultitap() case api.RecordGame: if !conf.Recording.Enabled { return api.ErrForbidden diff --git a/pkg/coordinator/userhandlers.go b/pkg/coordinator/userhandlers.go index d754109d2..240f9dbe0 100644 --- a/pkg/coordinator/userhandlers.go +++ b/pkg/coordinator/userhandlers.go @@ -98,8 +98,6 @@ func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) { u.Notify(api.ChangePlayer, rq) } -func (u *User) HandleToggleMultitap() { u.w.ToggleMultitap(u.Id()) } - func (u *User) HandleRecordGame(rq api.RecordGameRequest[com.Uid]) { if u.w == nil { return diff --git a/pkg/coordinator/workerapi.go b/pkg/coordinator/workerapi.go index cc21bec37..000059060 100644 --- a/pkg/coordinator/workerapi.go +++ b/pkg/coordinator/workerapi.go @@ -49,10 +49,6 @@ func (w *Worker) ChangePlayer(id com.Uid, index int) (*api.ChangePlayerResponse, w.Send(api.ChangePlayer, api.ChangePlayerRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId), Index: index})) } -func (w *Worker) ToggleMultitap(id com.Uid) { - _, _ = w.Send(api.ToggleMultitap, api.ToggleMultitapRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId)}) -} - func (w *Worker) RecordGame(id com.Uid, rec bool, recUser string) (*api.RecordGameResponse, error) { return api.UnwrapChecked[api.RecordGameResponse]( w.Send(api.RecordGame, api.RecordGameRequest[com.Uid]{StatefulRoom: StateRoom(id, w.RoomId), Active: rec, User: recUser})) diff --git a/pkg/worker/caged/libretro/frontend.go b/pkg/worker/caged/libretro/frontend.go index ab6b8ed18..1b6638e33 100644 --- a/pkg/worker/caged/libretro/frontend.go +++ b/pkg/worker/caged/libretro/frontend.go @@ -43,8 +43,6 @@ type Emulator interface { HasSave() bool // Close will be called when the game is done Close() - // ToggleMultitap toggles multitap controller. - ToggleMultitap() // Input passes input to the emulator Input(player int, data []byte) // Scale returns set video scale factor @@ -159,8 +157,8 @@ func (f *Frontend) LoadCore(emu string) { meta := nanoarch.Metadata{ AutoGlContext: conf.AutoGlContext, Hacks: conf.Hacks, - HasMultitap: conf.HasMultitap, HasVFR: conf.VFR, + Hid: conf.Hid, IsGlAllowed: conf.IsGlAllowed, LibPath: conf.Lib, Options: conf.Options, @@ -309,7 +307,6 @@ func (f *Frontend) SetSessionId(name string) { f.storage.SetMainSaveName(na func (f *Frontend) SetDataCb(cb func([]byte)) { f.onData = cb } func (f *Frontend) SetVideoCb(ff func(app.Video)) { f.onVideo = ff } func (f *Frontend) Tick() { f.mu.Lock(); f.nano.Run(); f.mu.Unlock() } -func (f *Frontend) ToggleMultitap() { f.nano.ToggleMultitap() } func (f *Frontend) ViewportRecalculate() { f.mu.Lock(); f.vw, f.vh = f.ViewportCalc(); f.mu.Unlock() } func (f *Frontend) ViewportSize() (int, int) { return f.vw, f.vh } diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.go b/pkg/worker/caged/libretro/nanoarch/nanoarch.go index 86ca6bd92..0ae211751 100644 --- a/pkg/worker/caged/libretro/nanoarch/nanoarch.go +++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.go @@ -43,11 +43,7 @@ type Nanoarch struct { Handlers LastFrameTime int64 LibCo bool - multitap struct { - supported bool - enabled bool - value C.unsigned - } + meta Metadata options *map[string]string reserved chan struct{} // limits concurrent use Rot uint @@ -96,10 +92,10 @@ type Metadata struct { IsGlAllowed bool UsesLibCo bool AutoGlContext bool - HasMultitap bool HasVFR bool Options map[string]string Hacks []string + Hid map[int][]int CoreAspectRatio bool } @@ -159,6 +155,7 @@ func (n *Nanoarch) SetVideoDebounce(t time.Duration) { n.limiter = NewLimit(t) } func (n *Nanoarch) CoreLoad(meta Metadata) { var err error + n.meta = meta n.LibCo = meta.UsesLibCo n.vfr = meta.HasVFR n.Aspect = meta.CoreAspectRatio @@ -170,10 +167,6 @@ func (n *Nanoarch) CoreLoad(meta Metadata) { n.options = &meta.Options - n.multitap.supported = meta.HasMultitap - n.multitap.enabled = false - n.multitap.value = 0 - filePath := meta.LibPath if ar, err := arch.Guess(); err == nil { filePath = filePath + ar.LibExt @@ -298,34 +291,24 @@ func (n *Nanoarch) LoadGame(path string) error { } // set default controller types on all ports + // needed for nestopia for i := 0; i < MaxPort; i++ { C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, C.uint(i), C.RETRO_DEVICE_JOYPAD) } + // map custom devices to ports + for k, v := range n.meta.Hid { + for _, device := range v { + C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, C.uint(k), C.unsigned(device)) + n.log.Debug().Msgf("set custom port-device: %v:%v", k, device) + } + } + n.LastFrameTime = time.Now().UnixNano() return nil } -// ToggleMultitap toggles multitap controller for cores. -// -// 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. -func (n *Nanoarch) ToggleMultitap() { - if !n.multitap.supported || n.multitap.value == 0 { - return - } - mt := n.multitap.value - if n.multitap.enabled { - mt = C.RETRO_DEVICE_JOYPAD - } - C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, mt) - n.multitap.enabled = !n.multitap.enabled -} - func (n *Nanoarch) Shutdown() { if n.LibCo { thread.Main(func() { @@ -775,23 +758,30 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool { } return false case C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO: - // !to rewrite - if !Nan0.multitap.supported { + if Nan0.log.GetLevel() > logger.DebugLevel { return false } - 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" { - Nan0.multitap.value = types[j].id - return true + + info := (*[64]C.struct_retro_controller_info)(data) + for c, controller := range info { + tp := unsafe.Pointer(controller.types) + if tp == nil { + break + } + cInfo := strings.Builder{} + cInfo.WriteString(fmt.Sprintf("Controller [%v] ", c)) + cd := (*[32]C.struct_retro_controller_description)(tp) + delim := ", " + n := int(controller.num_types) + for i := 0; i < n; i++ { + if i == n-1 { + delim = "" } + cInfo.WriteString(fmt.Sprintf("%v: %v%s", cd[i].id, C.GoString(cd[i].desc), delim)) } + Nan0.log.Debug().Msgf("%v", cInfo.String()) } - return false + return true case C.RETRO_ENVIRONMENT_GET_CLEAR_ALL_THREAD_WAITS_CB: C.bridge_clear_all_thread_waits_cb(data) return true diff --git a/pkg/worker/coordinator.go b/pkg/worker/coordinator.go index 86ce62260..4902d753e 100644 --- a/pkg/worker/coordinator.go +++ b/pkg/worker/coordinator.go @@ -125,12 +125,6 @@ func (c *coordinator) HandleRequests(w *Worker) chan struct{} { } else { out = c.HandleChangePlayer(*dat, w) } - case api.ToggleMultitap: - if dat := api.Unwrap[api.ToggleMultitapRequest[com.Uid]](x.Payload); dat == nil { - err, out = api.ErrMalformed, api.EmptyPacket - } else { - c.HandleToggleMultitap(*dat, w) - } case api.RecordGame: if dat := api.Unwrap[api.RecordGameRequest[com.Uid]](x.Payload); dat == nil { err, out = api.ErrMalformed, api.EmptyPacket diff --git a/pkg/worker/coordinatorhandlers.go b/pkg/worker/coordinatorhandlers.go index 336af8110..3c65c32d3 100644 --- a/pkg/worker/coordinatorhandlers.go +++ b/pkg/worker/coordinatorhandlers.go @@ -234,15 +234,6 @@ func (c *coordinator) HandleChangePlayer(rq api.ChangePlayerRequest[com.Uid], w return api.Out{Payload: rq.Index} } -func (c *coordinator) HandleToggleMultitap(rq api.ToggleMultitapRequest[com.Uid], w *Worker) api.Out { - r := w.router.FindRoom(rq.Rid) - if r == nil { - return api.ErrPacket - } - room.WithEmulator(r.App()).ToggleMultitap() - return api.OkPacket -} - func (c *coordinator) HandleRecordGame(rq api.RecordGameRequest[com.Uid], w *Worker) api.Out { if !w.conf.Recording.Enabled { return api.ErrPacket diff --git a/web/index.html b/web/index.html index 757ccc03a..4169980b2 100644 --- a/web/index.html +++ b/web/index.html @@ -123,7 +123,7 @@

Options

- + @@ -136,7 +136,7 @@

Options

- + diff --git a/web/js/api/api.js b/web/js/api/api.js index 20de8312b..ad9c1322d 100644 --- a/web/js/api/api.js +++ b/web/js/api/api.js @@ -17,7 +17,6 @@ const api = (() => { GAME_SAVE: 106, GAME_LOAD: 107, GAME_SET_PLAYER_INDEX: 108, - GAME_TOGGLE_MULTITAP: 109, GAME_RECORDING: 110, GET_WORKER_LIST: 111, GAME_ERROR_NO_FREE_SLOTS: 112, @@ -58,7 +57,6 @@ const api = (() => { record: record, record_user: recordUser, }), - toggleMultitap: () => packet(endpoints.GAME_TOGGLE_MULTITAP), toggleRecording: (active = false, userName = '') => packet(endpoints.GAME_RECORDING, { active: active, diff --git a/web/js/controller.js b/web/js/controller.js index b84ff6b18..cd57844a3 100644 --- a/web/js/controller.js +++ b/web/js/controller.js @@ -393,9 +393,6 @@ case KEY.PAD4: updatePlayerIndex(3); break; - case KEY.MULTITAP: - api.game.toggleMultitap(); - break; case KEY.QUIT: input.poll.disable(); api.game.quit(room.getId()); diff --git a/web/js/input/keyboard.js b/web/js/input/keyboard.js index 3a09ba9ef..7b46c2cb0 100644 --- a/web/js/input/keyboard.js +++ b/web/js/input/keyboard.js @@ -35,7 +35,6 @@ const keyboard = (() => { KeyH: KEY.HELP, Backslash: KEY.STATS, Digit9: KEY.SETTINGS, - KeyM: KEY.MULTITAP, KeyT: KEY.DTOGGLE }); diff --git a/web/js/input/keys.js b/web/js/input/keys.js index 1f798b956..7b16777c8 100644 --- a/web/js/input/keys.js +++ b/web/js/input/keys.js @@ -29,7 +29,6 @@ const KEY = (() => { R2: 'r2', L3: 'l3', R3: 'r3', - MULTITAP: 'multitap', REC: 'rec', } })(); diff --git a/web/js/settings/settings.js b/web/js/settings/settings.js index 55dd89ddf..0e67fdd71 100644 --- a/web/js/settings/settings.js +++ b/web/js/settings/settings.js @@ -15,7 +15,7 @@ */ const settings = (() => { // internal structure version - const revision = 1.3; + const revision = 1.4; // default settings // keep them for revert to defaults option