RomM — Manettes dans EmulatorJS
RomM — Manettes dans EmulatorJS : détection et mapping multijoueur
Dernière mise à jour : 2026-09-06
Cette page décrit deux correctifs appliqués à EmulatorJS, le moteur d'émulation que RomM embarque pour jouer directement dans le navigateur. Sans eux, certaines manettes sont vues par le navigateur mais déclarées absentes par l'émulateur, et seul le joueur 1 obtient une configuration de boutons.
Résultat obtenu après les deux correctifs :
Joueur 1 → détecté + touches configurées automatiquement
Joueur 2 → détecté + touches configurées automatiquement
Joueur 3 → détecté + touches configurées automatiquement
Joueur 4 → détecté + touches configurées automatiquement
⚠️ Ne pas confondre avec l'Emulator Streaming. RomM propose deux façons de jouer, et elles n'ont aucune pièce en commun :
- EmulatorJS — l'émulation tourne dans le navigateur du joueur, les manettes passent par la Gamepad API du navigateur. C'est le sujet de cette page.
- Emulator Streaming — Dolphin ou Eden tournent en conteneur sur le serveur et diffusent une image ; les manettes passent par Selkies. Voir les pages « Dolphin pour RomM », « Eden pour RomM » et « Accès distant, publication HTTPS et isolation réseau ».
Un problème de manette ne se diagnostique donc pas de la même manière selon le mode utilisé.
📌 Contexte de ces correctifs : EmulatorJS 4.2.3, tel qu'embarqué dans la version de RomM en service au moment du diagnostic. Serveur Unraid, conteneur
rommapp/romm. Ce sont des contournements liés à cette version — voir §10 avant toute mise à jour.
1. Ce que corrigent ces deux patchs
| # | Problème | Symptôme visible | Nature du correctif |
|---|---|---|---|
| 1 | Index de manette « à trous » mal interprété | Game Pad not connected alors que le Controller Debug de RomM fonctionne |
Chercher la manette par son index réel au lieu de sa position dans le tableau |
| 2 | Contrôles par défaut vides pour les joueurs 2 à 4 | Chaque joueur supplémentaire doit refaire son mapping à la main | Copier le mapping manette du joueur 1 vers les joueurs 2, 3 et 4 |
Les deux patchs vivent dans le même fichier, emulator.js, conservé dans l'appdata d'Unraid et monté dans le conteneur (§8).
2. Problème n°1 — la manette est vue, mais EmulatorJS la dit absente
2.1 Symptômes
Dans RomM, le diagnostic intégré répond correctement :
Controller Debug → OK
Tous les boutons et tous les axes réagissent. Mais dès qu'un jeu démarre dans EmulatorJS :
Game Pad not connected
Et dans la console du navigateur (F12) :
Uncaught TypeError: Cannot read properties of undefined (reading 'id')
at Object.connected (emulator.min.js:1:237337)
at GamepadHandler.dispatchEvent (emulator.min.js:1:206364)
at emulator.min.js:1:205943
at Array.forEach (<anonymous>)
at GamepadHandler.updateGamepadState (emulator.min.js:1:204942)
at GamepadHandler.loop (emulator.min.js:1:204688)
C'est la signature du bug : l'émulateur lit une case de tableau qui n'existe pas.
2.2 Diagnostic — ce que le navigateur expose vraiment
À exécuter dans la console du navigateur, manette branchée et un bouton pressé au moins une fois (la Gamepad API n'expose rien avant la première interaction) :
Array.from(navigator.getGamepads()).map((g, slot) =>
g ? {
slot,
index: g.index,
id: g.id,
mapping: g.mapping
} : null
)
Résultat observé dans notre installation :
0: null
1:
id: "Controller (STANDARD GAMEPAD)"
index: 1
mapping: "standard"
slot: 1
2: null
3: null
La manette est donc parfaitement fonctionnelle au niveau du navigateur. Le point qui compte : elle occupe le slot 1, avec index = 1, alors que le slot 0 est vide.
C'est ce qu'on appelle un index sparse — un emplacement libre avant une manette. Cela arrive dès qu'une manette a été débranchée, qu'un récepteur occupe un slot, ou simplement selon l'ordre de branchement.
2.3 La cause — un index qui n'est pas une position
EmulatorJS 4.2.3 confond deux choses différentes :
index navigateur ≠ position dans son tableau interne
[
{
id: "Controller (STANDARD GAMEPAD)",
index: 1
}
]
La manette est donc en position 0 de ce tableau, tout en portant index = 1. L'ancien code fait :
this.gamepad.gamepads[1].id
alors que this.gamepad.gamepads[1] vaut undefined — d'où l'erreur Cannot read properties of undefined (reading 'id'), et la manette déclarée non connectée.
💡 Tant que la manette occupe le slot 0, tout fonctionne :
index 0etposition 0coïncident par hasard. C'est ce qui rend le bug déroutant — il apparaît et disparaît selon l'ordre de branchement.
3. Correctif n°1 — chercher la manette par son index réel
Le principe : cesser de traiter gamepadIndex comme une position, et rechercher la manette dont l'index correspond.
Une fonction getGamepadSelectionValue(gamepadIndex) est ajoutée, bâtie autour de :
const gamepad = this.gamepad.gamepads.find(
candidate =>
candidate &&
candidate.index === gamepadIndex
);
Les emplacements du code qui lisaient directement le tableau appellent désormais cette fonction. Le cas problématique se résout alors correctement :
slot 0 → vide
slot 1 → manette index 1 ✓ trouvée
4. Problème n°2 — seul le joueur 1 est configuré
Une fois les manettes détectées, un second comportement apparaît. La première manette joue immédiatement, les autres non :
Player 1 → touches déjà configurées
Player 2 → touches à configurer manuellement
Player 3 → touches à configurer manuellement
Player 4 → touches à configurer manuellement
Ce n'est pas un bug, mais le comportement d'origine d'EmulatorJS : ses contrôles par défaut ne renseignent que le premier joueur.
0: {
// mapping complet du joueur 1
},
1: {},
2: {},
3: {}
5. Correctif n°2 — hériter le mapping manette du joueur 1
Les joueurs 2, 3 et 4 reprennent le mapping du joueur 1 :
Player 1
│
├── A / B / X / Y
├── Start / Select
├── D-Pad
├── sticks analogiques
├── L / R
└── L2 / R2
│
├────────► Player 2
├────────► Player 3
└────────► Player 4
⚠️ On ne copie que
value2, jamaisvalue. Dans EmulatorJS,value2porte le mapping manette etvaluele mapping clavier. Copiervaluedonnerait les mêmes touches clavier à plusieurs joueurs à la fois — une seule frappe piloterait quatre personnages.
Ce correctif suppose que les manettes sont exposées par le navigateur avec mapping: "standard" : EmulatorJS peut alors s'appuyer sur les positions normalisées de la Gamepad API. C'est le cas des manettes Xbox, des manettes PlayStation correctement reconnues, des 8BitDo et de beaucoup d'autres modèles. Une manette exotique remontée en mapping: "" devra toujours être configurée à la main.
6. Mise en place
6.1 Le dossier persistant
Sur Unraid :
mkdir -p /mnt/user/appdata/romm-ejs-gamepad-patch
Ce dossier contiendra trois fichiers :
| Fichier | Rôle |
|---|---|
emulator.js |
le fichier réellement servi à RomM (les deux correctifs) |
emulator.js.orig |
copie du fichier fourni par l'image Docker, avant toute modification |
emulator.js.bak-multiplayer |
état intermédiaire : correctif n°1 seul |
6.2 Retrouver le conteneur RomM
Son nom peut varier selon le template ; on le détecte par son image :
ROMM="$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /rommapp\/romm/ {print $1; exit}')"
echo "$ROMM"
Si la commande ne renvoie rien, le conteneur est arrêté ou son image diffère — vérifier avec docker ps -a.
6.3 Sauvegarder le fichier d'origine
docker cp \
"$ROMM:/var/www/html/assets/emulatorjs/data/src/emulator.js" \
"/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js.orig"
6.4 Appliquer le correctif n°1
⚠️ Ce correctif a été appliqué par édition directe du fichier, sans commande d'injection automatique — contrairement au correctif n°2 (§6.6). Il n'existe donc pas de recette pour le régénérer : c'est le fichier
emulator.jsde l'appdataqui fait office de référence.Conséquence pratique : ce dossier doit être sauvegardé. S'il était perdu, il faudrait refaire l'analyse du §2 et réécrire le patch à la main.
Le fichier de travail est :
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
Vérification de la présence du patch :
grep -n "getGamepadSelectionValue" \
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
Attendu dans notre installation — une définition et trois appels :
1142: const gamepadSelection = this.getGamepadSelectionValue(e.gamepadIndex);
1153: const gamepadSelection = this.getGamepadSelectionValue(e.gamepadIndex);
1194: getGamepadSelectionValue(gamepadIndex) {
3361: const gamepadSelection = this.getGamepadSelectionValue(e.gamepadIndex);
Les numéros de ligne sont indicatifs et bougeront à la moindre évolution du fichier ; c'est la présence des quatre occurrences qui compte.
6.5 Sauvegarder avant le correctif n°2
FILE="/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js"
cp "$FILE" "$FILE.bak-multiplayer"
Cette sauvegarde contient le correctif n°1 seul : c'est le point de retour si l'auto-mapping multijoueur pose problème (§11.1).
6.6 Injecter le correctif n°2
FILE="/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js"
grep -q "AUTO-MAP PLAYERS 2-4" "$FILE" || sed -i '/if (this\.config\.defaultControllers) this\.defaultControllers = this\.config\.defaultControllers;/a\
\
// AUTO-MAP PLAYERS 2-4: copy gamepad mappings from player 1\
for (let player = 1; player < 4; player++) {\
if (!this.defaultControllers[player] || Object.keys(this.defaultControllers[player]).length === 0) {\
this.defaultControllers[player] = {};\
for (const [button, mapping] of Object.entries(this.defaultControllers[0] || {})) {\
if (mapping.value2 !== undefined) {\
this.defaultControllers[player][button] = { value2: mapping.value2 };\
}\
}\
}\
}\
' "$FILE"
Le grep -q ... || en tête rend la commande rejouable sans risque : si le marqueur AUTO-MAP PLAYERS 2-4 est déjà là, le sed n'est pas exécuté et le bloc n'est pas injecté une seconde fois.
Vérification :
grep -n -A18 -B3 "AUTO-MAP PLAYERS 2-4" \
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
On doit lire :
if (this.config.defaultControllers)
this.defaultControllers = this.config.defaultControllers;
// AUTO-MAP PLAYERS 2-4: copy gamepad mappings from player 1
for (let player = 1; player < 4; player++) {
if (
!this.defaultControllers[player] ||
Object.keys(this.defaultControllers[player]).length === 0
) {
this.defaultControllers[player] = {};
for (const [button, mapping] of Object.entries(
this.defaultControllers[0] || {}
)) {
if (mapping.value2 !== undefined) {
this.defaultControllers[player][button] = {
value2: mapping.value2
};
}
}
}
}
💡 La boucle part de
player = 1: l'indice0est le joueur 1, déjà configuré. Elle ne touche qu'aux joueurs dont le mapping est vide — une configuration personnalisée déjà présente n'est jamais écrasée.
7. Faire servir les sources par RomM (debug: true)
Par défaut, EmulatorJS charge sa version minifiée :
emulator.min.js
Or nos correctifs portent sur le fichier source :
data/src/emulator.js
Sans réglage supplémentaire, ils ne seraient donc jamais chargés. Il faut demander à RomM de servir les sources, dans son config.yml (le même fichier que celui qui porte le bloc streaming des pages Dolphin et Eden) :
emulatorjs:
debug: true
Si une section emulatorjs: existe déjà, n'ajouter que la ligne debug: true en dessous.
⚠️ Ce réglage a un coût.
debug: truefait charger les fichiers EmulatorJS non minifiés : le démarrage d'un jeu est plus lent et le volume téléchargé plus important, surtout sur une connexion distante. C'est le prix à payer pour que les patchs s'appliquent — il n'y a pas d'autre moyen tant qu'on corrige la source plutôt que le fichier minifié.
Redémarrer RomM après modification.
8. Rendre le tout persistant (bind mount Unraid)
Un fichier modifié à l'intérieur d'un conteneur est perdu à la première mise à jour, recréation ou réinstallation. Le fichier corrigé doit donc venir de l'appdata.
Dans le template Docker RomM d'Unraid, ajouter un Path :
| Champ | Valeur |
|---|---|
| Host Path | /mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js |
| Container Path | /var/www/html/assets/emulatorjs/data/src/emulator.js |
| Access Mode | Read Only |
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
│
│ bind mount Unraid (ro)
▼
/var/www/html/assets/emulatorjs/data/src/emulator.js
Le fichier de l'appdata remplace alors celui de l'image, et devient celui réellement servi aux navigateurs.
C'est un montage de fichier, pas de dossier : le chemin hôte doit exister avant le démarrage du conteneur, sinon Docker crée un dossier vide à sa place et RomM ne trouve plus son
emulator.js.
Après une mise à jour de l'image RomM :
nouvelle image RomM
+
bind mount Unraid
=
notre emulator.js corrigé
Les deux correctifs restent donc actifs après un redémarrage, une recréation du conteneur ou une mise à jour Docker.
9. Redémarrer et valider
ROMM="$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /rommapp\/romm/ {print $1; exit}')"
docker restart "$ROMM"
Windows / Linux : Ctrl + Shift + R
macOS : Cmd + Shift + R
Points à vérifier :
-
Controller Debugde RomM répond ; - EmulatorJS annonce la manette connectée (plus de
Game Pad not connected) ; - la console (
F12) ne montre plusCannot read properties of undefined (reading 'id'); - le joueur 1 joue sans rien configurer ;
- les joueurs 2, 3 et 4 jouent également sans rien configurer ;
- les touches clavier ne pilotent qu'un seul joueur.
10. Après une mise à jour de RomM ou d'EmulatorJS
Ces correctifs sont des contournements taillés pour EmulatorJS 4.2.3. Une nouvelle version de RomM peut très bien corriger le bug d'index nativement, réorganiser la gestion des contrôleurs, ou changer la structure de emulator.js.
⚠️ Le piège, c'est justement la persistance. Le bind mount a priorité sur l'image : après une mise à jour, RomM continuera de servir notre ancien fichier à un runtime plus récent, même si l'image en fournit une version corrigée. Selon l'ampleur des changements, cela va du simple patch devenu inutile au plantage de l'émulateur.
Toute mise à jour majeure de RomM demande donc de relire ce point — ce n'est pas optionnel.
Vérification rapide que le fichier persistant contient toujours les deux patchs :
# correctif n°1 — détection
grep -n "getGamepadSelectionValue" \
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
# correctif n°2 — multijoueur
grep -n "AUTO-MAP PLAYERS 2-4" \
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
Si les deux commandes renvoient des lignes, le fichier est intact. Cela ne dit rien de sa compatibilité avec la nouvelle version de RomM : à tester en lançant un jeu, console ouverte.
Marche à suivre conseillée après une montée de version majeure :
- récupérer le nouveau
emulator.jsde l'image dans un fichier à part :docker cp "$ROMM:/var/www/html/assets/emulatorjs/data/src/emulator.js" /mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js.new; - y chercher
getGamepadSelectionValueou toute recherche parindex— si le bug est corrigé en amont, le correctif n°1 n'a plus lieu d'être ; - si seul le correctif multijoueur reste nécessaire, repartir du fichier neuf et lui appliquer le
seddu §6.6.
11. Revenir en arrière
11.1 Annuler le seul correctif multijoueur
Pour retrouver le fichier avec le correctif de détection mais sans l'auto-mapping :
cp \
"/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js.bak-multiplayer" \
"/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js"
ROMM="$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /rommapp\/romm/ {print $1; exit}')"
docker restart "$ROMM"
11.2 Retirer complètement les patchs
Le fichier d'origine est conservé dans emulator.js.orig, mais le restaurer ne suffit pas : il correspond à la version de RomM installée au moment de la copie, pas forcément à celle en service aujourd'hui. La méthode fiable :
- supprimer le Path dans le template Docker Unraid (celui du §8) ;
- recréer le conteneur RomM — il retrouve alors le
emulator.jsexact de son image ; - si le mode debug n'a plus d'utilité, remettre
debug: falsedansconfig.ymlou supprimer la ligne, pour retrouver les fichiers minifiés et un chargement plus rapide.
Le dossier de l'appdata peut être conservé : il ne gêne plus une fois le montage retiré, et garde la trace du travail fait.
12. Dépannage rapide
| Symptôme | Cause probable | Solution |
|---|---|---|
Game Pad not connected, alors que Controller Debug fonctionne |
Correctif n°1 absent, ou fichier minifié servi | §6.4 puis §7 |
Cannot read properties of undefined (reading 'id') dans la console |
Index sparse non géré — le correctif n°1 n'est pas chargé | Vérifier le grep du §10, puis le bind mount du §8 |
| Les patchs sont bien dans le fichier, mais rien ne change | debug: true manquant : RomM sert toujours emulator.min.js |
§7 |
| Rien ne change après modification du fichier | Cache du navigateur | Rechargement forcé (§9) |
| Joueurs 2 à 4 toujours vides | Correctif n°2 absent, ou mappings déjà non vides | §6.6 — la boucle ne touche que les mappings vides |
| Une même touche clavier pilote plusieurs joueurs | value a été copié en plus de value2 |
Restaurer emulator.js.bak-multiplayer (§11.1) et réinjecter §6.6 |
| Les patchs disparaissent après mise à jour Docker | Le bind mount n'est pas dans le template | §8 |
RomM ne démarre plus / plus de fichier emulator.js |
Docker a créé un dossier à la place du fichier hôte manquant | Vérifier que le Host Path existe bien comme fichier, puis recréer le conteneur |
| Une manette exotique reste non configurée | Le navigateur ne l'expose pas en mapping: "standard" |
Diagnostic du §2.2 ; mapping manuel à faire dans EmulatorJS |
| Jeu plus lent à démarrer qu'avant | Normal : debug: true charge les sources non minifiées |
§7 |
13. À compléter
- Le correctif n°1 n'a pas de recette rejouable (§6.4). Idéalement, produire un
diffentreemulator.js.origetemulator.jset le conserver ici — il permettrait de réappliquer le patch sur une version future :diff -u /mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js.orig /mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js - Le dossier
romm-ejs-gamepad-patchest-il couvert par la sauvegarde de l'appdata? À vérifier : c'est aujourd'hui le seul exemplaire du correctif n°1. - Bug remonté en amont ? Vérifier si le problème d'index sparse est déjà signalé au projet EmulatorJS — si oui, suivre le ticket permettrait de savoir quand retirer le patch.
- Version exacte de RomM au moment du diagnostic, à noter ici pour situer le correctif dans le temps.