diff --git a/pkg/api/api.go b/pkg/api/api.go
index f85daa8d..34353bb0 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 2b2fc4dd..a4078f5e 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 27f1eaac..39b695ea 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 da8f5b2e..49bf6713 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 e2a3d60b..00479fda 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 d754109d..240f9dbe 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 cc21bec3..00005906 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 ab6b8ed1..1b6638e3 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 86ca6bd9..0ae21175 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 86ce6226..4902d753 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 336af811..3c65c32d 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 757ccc03..4169980b 100644
--- a/web/index.html
+++ b/web/index.html
@@ -123,7 +123,7 @@
-
+
@@ -136,7 +136,7 @@
-
+
diff --git a/web/js/api/api.js b/web/js/api/api.js
index 20de8312..ad9c1322 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 b84ff6b1..cd57844a 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 3a09ba9e..7b46c2cb 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 1f798b95..7b16777c 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 55dd89dd..0e67fdd7 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