# Dockers

# Games on Whales

# Installation Wolf (Multi-User Cloud Gaming)

**Objectif :** Faire tourner plusieurs instances de Steam indépendantes sur un ou plusieurs GPU, accessibles via Moonlight avec support manettes, en utilisant Wolf et la méthode Nvidia (Manual) sur Unraid.

---

### 1. Prérequis Système (Unraid)

#### A. Plugin Nvidia

Installer le plugin **"Nvidia Driver"** (par ich777) depuis les Community Applications.

#### B. Correctifs NVENC et NvFBC pour les pilotes Nvidia

Nvidia bloque le nombre de flux NVENC au-delà de 3. Pour lever cette limite, on applique un patch au chargement du driver.

Installer le plugin **"User Scripts"** depuis les Community Applications.  
Créer ensuite un script, par exemple nommé **"Nvidia NVENC Patch"** avec comme contenu :

```bash
#!/bin/bash
wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh -O /tmp/nvidia-patch.sh
chmod +x /tmp/nvidia-patch.sh
/tmp/nvidia-patch.sh
```

À lancer une première fois manuellement.  
Dans User Scripts, régler le script pour qu’il s’exécute **à chaque démarrage de l’Array**.

#### C. Activation du "Kernel Mode Setting" (KMS)

Wolf a besoin de contrôler l’affichage au niveau du noyau pour créer les sessions graphiques. Sans ça, on risque l’écran noir.

1. Aller dans **Main &gt; Flash &gt; Syslinux Configuration**.
2. Dans l’entrée `Unraid OS`, ajouter à la fin de la ligne : 
    
    ```
    nvidia-drm.modeset=1
    ```
3. **Redémarrer Unraid**.
4. Vérifier que le KMS est bien activé : 
    
    ```bash
    cat /sys/module/nvidia_drm/parameters/modeset
    ```
    
    Il doit renvoyer : `<strong>Y</strong>`

#### D. Persistance des Manettes (uinput / uhid)

Pour que Unraid recrée correctement les règles de manettes virtuelles à chaque démarrage (sinon elles disparaissent après reboot).

1. Ouvrir le terminal Unraid.
2. Éditer le fichier de boot : `nano /boot/config/go`
3. Ajouter ce bloc à la fin du fichier :

```bash
# --- Wolf : Support Manettes Virtuelles ---
# 1. Permissions uinput
chmod 666 /dev/uinput || true

# 2. Règles UDEV
cat << 'EOF' > /etc/udev/rules.d/85-wolf-virtual-inputs.rules
KERNEL=="uinput", SUBSYSTEM=="misc", MODE="0660", GROUP="input", OPTIONS+="static_node=uinput"
KERNEL=="uhid", TAG+="uaccess"
SUBSYSTEMS=="input", ATTRS{id/vendor}=="ab00", MODE="0660", GROUP="input", ENV{ID_SEAT}="seat9"
SUBSYSTEMS=="input", ATTRS{name}=="Wolf X-Box One (virtual) pad", MODE="0660", GROUP="input"
SUBSYSTEMS=="input", ATTRS{name}=="Wolf PS5 (virtual) pad", MODE="0660", GROUP="input"
SUBSYSTEMS=="input", ATTRS{name}=="Wolf gamepad (virtual) motion sensors", MODE="0660", GROUP="input"
SUBSYSTEMS=="input", ATTRS{name}=="Wolf Nintendo (virtual) pad", MODE="0660", GROUP="input"
EOF

# 3. Recharger les règles
udevadm control --reload-rules && udevadm trigger

```

---

### 2. Installation de Wolf (Méthode Nvidia Manual sur Unraid)

Avec la méthode Nvidia Manual, on va :

- Construire une image Docker qui contient les fichiers du driver Nvidia.
- Pré-remplir un volume Docker `nvidia-driver-vol` avec ces fichiers.
- Monter ce volume dans le conteneur Wolf via un template Unraid.

#### A. Construire l’image `gow/nvidia-driver:latest`

Dans le terminal Unraid :

```bash
cd /root

curl https://raw.githubusercontent.com/games-on-whales/gow/master/images/nvidia-driver/Dockerfile \
  | docker build -t gow/nvidia-driver:latest -f - --build-arg NV_VERSION=$(cat /sys/module/nvidia/version) .
```

Cela crée l’image locale `gow/nvidia-driver:latest` avec la version de driver actuellement chargée.

#### B. Créer / mettre à jour le volume Docker `nvidia-driver-vol`

On pré-popule un volume Docker avec les libs Nvidia :

```bash
docker create --rm \
  --mount source=nvidia-driver-vol,destination=/usr/nvidia \
  gow/nvidia-driver:latest sh

```

Vérifier qu’il existe :

```bash
docker volume ls | grep nvidia-driver
```

Il doit afficher :`<strong>nvidia-driver-vol</strong>`

#### C. Script User Scripts : reconstruire le volume après chaque reboot

**Important :** à chaque mise à jour du driver Nvidia, il faut reconstruire l’image et repopuler le volume, sinon Wolf utilise des libs qui ne correspondent plus au driver chargé.

Le plus simple sur Unraid : utiliser le plugin **User Scripts** avec un script lancé au démarrage de l’Array.

1. Dans User Scripts, créer un script nommé par exemple **"Wolf - Rebuild Nvidia driver volume"**.
2. Contenu du script :

```bash
#!/bin/bash
# Wolf - (Re)construction de l'image et du volume driver Nvidia + restart Wolf

WOLF_CONTAINER_NAME="wolf"
DRIVER_IMAGE="gow/nvidia-driver:latest"
DRIVER_VOLUME="nvidia-driver-vol"
NOTIFY="/usr/local/emhttp/webGui/scripts/notify"

notify_msg() {
  # $1 = niveau (normal|warning|alert)
  # $2 = sujet
  # $3 = description
  if [ -x "${NOTIFY}" ]; then
    "${NOTIFY}" -e "Wolf Nvidia" -s "$2" -d "$3" -i "$1"
  fi
}

echo ">>> [Wolf] $(date) - Début de la reconstruction Nvidia (image + volume)"
notify_msg "normal" "Début mise à jour Wolf Nvidia" "Reconstruction de l'image Nvidia et du volume ${DRIVER_VOLUME} en cours..."

# Vérif rapide de la version de driver en cours
if [ -r /sys/module/nvidia/version ]; then
  DRV_VER=$(cat /sys/module/nvidia/version)
  echo ">>> [Wolf] Driver Nvidia détecté : ${DRV_VER}"
else
  echo "!!! [Wolf] Impossible de lire /sys/module/nvidia/version (driver Nvidia non chargé ?)"
  notify_msg "warning" "Driver Nvidia introuvable" "Impossible de lire /sys/module/nvidia/version. Vérifiez que le driver est bien chargé avant de relancer le script."
fi

echo ">>> [Wolf] Reconstruction de l'image ${DRIVER_IMAGE}..."

curl -s https://raw.githubusercontent.com/games-on-whales/gow/master/images/nvidia-driver/Dockerfile \
  | docker build -t "${DRIVER_IMAGE}" -f - \
      --build-arg NV_VERSION=$(cat /sys/module/nvidia/version) \
      .

if [ $? -ne 0 ]; then
  echo "!!! [Wolf] Échec du build de l'image ${DRIVER_IMAGE}"
  notify_msg "alert" "Échec build image Nvidia" "Le build de l'image ${DRIVER_IMAGE} a échoué. Consultez les logs du script / de Docker pour plus de détails."
  exit 1
fi

echo ">>> [Wolf] (Re)population du volume ${DRIVER_VOLUME}..."

# On crée un conteneur temporaire pour initialiser le volume,
# comme dans la doc officielle Wolf, sans jamais l'exécuter.
CID=$(docker create \
  --mount source=${DRIVER_VOLUME},destination=/usr/nvidia \
  "${DRIVER_IMAGE}" sh 2>/dev/null)

if [ -z "${CID}" ]; then
  echo "!!! [Wolf] Échec de la création du conteneur temporaire pour le volume ${DRIVER_VOLUME}"
  notify_msg "alert" "Échec initialisation volume Nvidia" "Impossible de créer un conteneur temporaire pour le volume ${DRIVER_VOLUME}. Vérifiez Docker et le volume."
  exit 1
fi

echo ">>> [Wolf] Conteneur temporaire ${CID} créé pour initialiser le volume ${DRIVER_VOLUME}"

# On supprime tout de suite le conteneur (le volume, lui, reste)
docker rm "${CID}" >/dev/null 2>&1 || true
echo ">>> [Wolf] Conteneur temporaire ${CID} supprimé"

echo ">>> [Wolf] Redémarrage du conteneur ${WOLF_CONTAINER_NAME}..."

# On essaye de le stopper (au cas où il tourne déjà)
if docker stop "${WOLF_CONTAINER_NAME}" 2>/dev/null; then
  echo ">>> [Wolf] Conteneur ${WOLF_CONTAINER_NAME} stoppé."
else
  echo ">>> [Wolf] Conteneur ${WOLF_CONTAINER_NAME} n'était pas en cours d'exécution."
fi

# Puis on le démarre
if docker start "${WOLF_CONTAINER_NAME}" 2>/dev/null; then
  echo ">>> [Wolf] Conteneur ${WOLF_CONTAINER_NAME} démarré."
  notify_msg "normal" "Wolf Nvidia en place" "La mise en place des bibliothèques Nvidia et le redémarrage de Wolf se sont terminés avec succès."
else
  echo "!!! [Wolf] Impossible de démarrer le conteneur ${WOLF_CONTAINER_NAME} (à vérifier dans l'UI Docker)."
  notify_msg "alert" "Échec redémarrage Wolf" "Impossible de démarrer le conteneur ${WOLF_CONTAINER_NAME}. Vérifiez la configuration dans l'UI Docker."
  exit 1
fi

echo ">>> [Wolf] $(date) - Opération terminée."

```

3. Configurer le script pour s’exécuter à **chaque démarrage de l’Array**.

À chaque reboot (après une maj de driver par exemple), le volume sera automatiquement synchronisé avec la bonne version.

<p class="callout info">**Ce que fait le script :**  
- Vérifie la version du driver Nvidia actuellement chargée sur Unraid.  
- Rebuild l’image Docker gow/nvidia-driver:latest avec cette version de driver.  
- (Re)inititialise le volume Docker nvidia-driver-vol avec les librairies Nvidia de l’image.  
- Redémarre automatiquement le conteneur wolf pour qu’il utilise les librairies à jour.  
- Logue chaque étape pour faciliter le debug en cas de problème.</p>

#### D. Création automatique du template Unraid `my-wolf.xml`

On va créer directement le template Unraid via une commande dans le terminal, pour qu’il apparaisse dans **Docker &gt; Add Container**.

1. Dans le terminal Unraid, lancer :

```bash
mkdir -p /boot/config/plugins/dockerMan/templates-user

cat << 'EOF' > /boot/config/plugins/dockerMan/templates-user/my-wolf.xml
<?xml version="1.0"?>
<Container version="2">
    <Name>wolf</Name>
    <Repository>ghcr.io/games-on-whales/wolf:stable</Repository>
    <Registry>https://ghcr.io/</Registry>
    <Network>host</Network>
    <MyIP/>
    <Shell>bash</Shell>
    <Privileged>true</Privileged>
    <Support>https://games-on-whales.github.io/wolf/stable/</Support>
    <Project>https://github.com/games-on-whales/wolf</Project>
    <Overview>Wolf est un serveur de streaming Moonlight permettant de partager un hôte pour plusieurs sessions de jeu et bureaux virtuels.</Overview>
    <Category>GameServers:</Category>
    <WebUI>http://[IP]:8080/</WebUI>
    <TemplateURL/>
    <Icon>https://games-on-whales.github.io/assets/favicon.png</Icon>
    <!-- Volume Docker Nvidia + règle cgroup -->
    <ExtraParams>-v nvidia-driver-vol:/usr/nvidia:rw --device-cgroup-rule="c 13:* rmw"</ExtraParams>
    <PostArgs/>
    <CPUset/>
    <DateInstalled>0</DateInstalled>
    <DonateText/>
    <DonateLink/>
    <Description>Wolf est un serveur de streaming Moonlight permettant de partager un hôte pour plusieurs sessions de jeu et bureaux virtuels.</Description>

    <Networking>
        <Mode>host</Mode>
        <Publish/>
    </Networking>

    <!-- Volumes "bruts" utilisés par le conteneur -->
    <Data>
        <Volume>
            <HostDir>/mnt/user/appdata/wolf</HostDir>
            <ContainerDir>/etc/wolf</ContainerDir>
            <Mode>rw</Mode>
        </Volume>
        <Volume>
            <HostDir>/var/run/docker.sock</HostDir>
            <ContainerDir>/var/run/docker.sock</ContainerDir>
            <Mode>rw</Mode>
        </Volume>
        <Volume>
            <HostDir>/dev</HostDir>
            <ContainerDir>/dev</ContainerDir>
            <Mode>rw</Mode>
        </Volume>
        <Volume>
            <HostDir>/run/udev</HostDir>
            <ContainerDir>/run/udev</ContainerDir>
            <Mode>rw</Mode>
        </Volume>
    </Data>

    <!-- Variables d'environnement réelles -->
    <Environment>
        <Variable>
            <Value>nvidia-driver-vol</Value>
            <Name>NVIDIA_DRIVER_VOLUME_NAME</Name>
            <Mode/>
        </Variable>
    </Environment>

    <Labels/>

    <!-- Config = ce qui apparaît dans l'UI Unraid -->

    <!-- Appdata Wolf -->
    <Config Name="Wolf Config" Target="/etc/wolf" Default="/mnt/user/appdata/wolf" Mode="rw"
            Description="Dossier de configuration de Wolf"
            Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/wolf</Config>

    <!-- Docker socket -->
    <Config Name="Docker socket" Target="/var/run/docker.sock" Default="/var/run/docker.sock" Mode="rw"
            Description="Socket Docker (nécessaire pour lancer les apps GOW)"
            Type="Path" Display="advanced" Required="true" Mask="false">/var/run/docker.sock</Config>

    <!-- /dev host -->
    <Config Name="Dev" Target="/dev" Default="/dev" Mode="rw"
            Description="Montage de /dev du host"
            Type="Path" Display="advanced" Required="true" Mask="false">/dev</Config>

    <!-- /run/udev host -->
    <Config Name="Udev" Target="/run/udev" Default="/run/udev" Mode="rw"
            Description="Montage de /run/udev du host"
            Type="Path" Display="advanced" Required="true" Mask="false">/run/udev</Config>

    <!-- Variable : nom du volume Nvidia -->
    <Config Name="NVIDIA_DRIVER_VOLUME_NAME" Target="NVIDIA_DRIVER_VOLUME_NAME" Default="nvidia-driver-vol" Mode=""
            Description="Nom du volume Docker contenant les bibliothèques Nvidia (méthode Manual)"
            Type="Variable" Display="advanced" Required="false" Mask="false">nvidia-driver-vol</Config>

    <!-- Devices GPU / input -->

    <Config Name="Device /dev/dri" Target="/dev/dri" Default="/dev/dri" Mode=""
            Description="DRM / GPU devices"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/dri</Config>

    <Config Name="Device /dev/nvidiactl" Target="/dev/nvidiactl" Default="/dev/nvidiactl" Mode=""
            Description="Nvidia control device"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/nvidiactl</Config>

    <Config Name="Device /dev/nvidia0" Target="/dev/nvidia0" Default="/dev/nvidia0" Mode=""
            Description="GPU Nvidia principal"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/nvidia0</Config>

    <Config Name="Device /dev/nvidia-uvm" Target="/dev/nvidia-uvm" Default="/dev/nvidia-uvm" Mode=""
            Description="Nvidia UVM"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/nvidia-uvm</Config>

    <Config Name="Device /dev/nvidia-uvm-tools" Target="/dev/nvidia-uvm-tools" Default="/dev/nvidia-uvm-tools" Mode=""
            Description="Nvidia UVM tools"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/nvidia-uvm-tools</Config>

    <Config Name="Device /dev/nvidia-cap1" Target="/dev/nvidia-caps/nvidia-cap1" Default="/dev/nvidia-caps/nvidia-cap1" Mode=""
            Description="Nvidia capability 1"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/nvidia-caps/nvidia-cap1</Config>

    <Config Name="Device /dev/nvidia-cap2" Target="/dev/nvidia-caps/nvidia-cap2" Default="/dev/nvidia-caps/nvidia-cap2" Mode=""
            Description="Nvidia capability 2"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/nvidia-caps/nvidia-cap2</Config>

    <Config Name="Device /dev/uinput" Target="/dev/uinput" Default="/dev/uinput" Mode=""
            Description="uinput (manettes virtuelles, clavier, etc.)"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/uinput</Config>

    <Config Name="Device /dev/uhid" Target="/dev/uhid" Default="/dev/uhid" Mode=""
            Description="UHID (entrée HID user space)"
            Type="Device" Display="advanced" Required="true" Mask="false">/dev/uhid</Config>

</Container>
EOF

```

2. Aller dans **Docker &gt; Add Container** et choisir le template **wolf** dans la liste déroulante. Normalement, tout est déjà correctement renseigné.
3. Cliquer sur **Apply** pour créer et lancer le conteneur Wolf.

<p class="callout info">**Note GPU :** ce template est prévu pour une configuration avec un seul GPU Nvidia : il expose explicitement le device `/dev/nvidia0`.  
Si tu ajoutes un second GPU, il suffira d’ajouter aussi le device `/dev/nvidia1`   
Via l’interface Unraid : Docker → Éditer le conteneur → “Add another device” pour le rendre visible dans le conteneur Wolf.  
  
J'explique plus loin le multi GPU. (en cours de rédaction)</p>

---

### 3. Configuration de Wolf (config.toml &amp; utilisateurs)

#### A. Préparation des permissions aux répertoires (méthode recommandée : ACL)

Par défaut, Unraid utilise l’utilisateur `nobody:users` (99:100) pour les partages.  
Plutôt que de faire des `chown/chmod -R` à chaque nouvel utilisateur ou nouveau jeu, on peut utiliser les ACL pour donner en plus tous les droits au groupe `1000` (celui utilisé par les containers Wolf/Steam).

Donner les droits **rwx** au groupe 1000 sur l’appdata Wolf

```bash
setfacl -R -m g:1000:rwx /mnt/user/appdata/wolf
setfacl -R -d -m g:1000:rwx /mnt/user/appdata/wolf
```

Donner les droits **rwx** au groupe 1000 sur le dossier de jeux

```bash
setfacl -R -m g:1000:rwx /mnt/user/Games
setfacl -R -d -m g:1000:rwx /mnt/user/Games

```

<p class="callout warning">Certains outils Unraid comme **“New Permissions”** ou des scripts maison peuvent **écraser les ACL** si tu les lances sur ces dossiers → dans ce cas, il faudra relancer les `setfacl`.  
  
Si tu déplaces des données entre disques via certains outils externes (rsync sans `-A`, cp, etc.), les ACL peuvent ne pas être copiées.</p>

#### Modifier le `config.toml`

**Fichier cible (côté host) :**

```bash
nano /mnt/user/appdata/wolf/cfg/config.toml

```

À titre perso, pour le moment, j’ai choisi d’épurer totalement l’affichage dans Moonlight. Mais il est possible de laisser l’app Wolf UI présente, qui permet de classer les apps par utilisateur.

Dans ce fichier, vous supprimez tout ce qu’il y a entre :

```bash
[[profiles]]
id = 'moonlight-profile-id'
```

et

```bash
[[profiles]]
id = 'user'
name = 'User'
```

Et vous collez à la place la configuration de votre premier utilisateur suivante :

<p class="callout info">Remplacez bien les quatre **&lt;NOM\_DU\_COMPTE&gt;**.  
Remplacez **&lt;url-image-png&gt;** par une image PNG. Dimension idéale width=112px height=150px (facilement reconnaissable dans Moonlight).</p>

```toml
# --- Utilisateur 1 : <NOM_DU_COMPTE> ---
    [[profiles.apps]]
    icon_png_path = '<url-image-png>'
    start_virtual_compositor = true
    title = '<NOM_DU_COMPTE>'

        [profiles.apps.runner]
        base_create_json = '''{
  "HostConfig": {
    "IpcMode": "host",
    "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"],
    "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"],
    "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}],
    "Privileged": false,
    "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]
  }
}
'''
        devices = []
        env = [
            'PROTON_LOG=1',
            'RUN_SWAY=true',
            'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*',
            'XKB_DEFAULT_LAYOUT=fr',
            'TZ=Europe/Paris'
        ]
        image = 'ghcr.io/games-on-whales/steam:edge'
        mounts = [
            '/mnt/user/Games:/home/retro/games-unraid:rw',
            '/mnt/user/appdata/wolf/<NOM_DU_COMPTE>:/home/retro/.steam:rw'
        ]
        name = '<NOM_DU_COMPTE>'
        ports = []
        type = 'docker'

```

<p class="callout info">**'XKB\_DEFAULT\_LAYOUT=fr'** → clavier AZERTY français.  
**'TZ=Europe/Paris'** → heure correcte dans l’app.</p>

<p class="callout info">**'/mnt/user/Games:/home/retro/games-unraid:rw'** → point de montage du répertoire d’installation des jeux.  
**'/mnt/user/appdata/wolf/&lt;NOM\_DU\_COMPTE&gt;:/home/retro/.steam:rw'** → rend l’installation de Steam persistante par utilisateur.</p>

### 3b. Variante des gestions d’utilisateurs

#### Wolf UI

Wolf UI permet d’avoir un répertoire par utilisateur.

- Ranger toutes les apps par utilisateur.
- Utiliser le mode `co-op` (deux utilisateurs sur la même session en coop locale).

<iframe allowfullscreen="allowfullscreen" height="314" src="https://www.youtube.com/embed/8an-SvnD4pk?si=IFsfXUJj87F0c-a3" width="560"></iframe>

#### Activation de Wolf UI

##### Faire apparaître le menu Wolf UI dans Moonlight

Pour activer Wolf UI, il faut modifier `config.toml`, repérer la partie :

```
[[profiles]]
id = 'moonlight-profile-id'

```

Directement dessous, collez :

```
[[profiles.apps]]
    icon_png_path = 'https://raw.githubusercontent.com/games-on-whales/wolf-ui/refs/heads/main/src/Icons/wolf_ui_icon.png'
    start_virtual_compositor = true
    title = 'Wolf UI'

        [profiles.apps.runner]
        base_create_json = '''{
      "HostConfig": {
        "IpcMode": "host",
        "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"],
        "Privileged": false,
        "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]
      }
    }'''
        devices = []
        env = [
            'GOW_REQUIRED_DEVICES=/dev/input/event* /dev/dri/* /dev/nvidia*',
            'WOLF_SOCKET_PATH=/var/run/wolf/wolf.sock',
            'WOLF_UI_AUTOUPDATE=False',
            'LOGLEVEL=INFO',
            'XKB_DEFAULT_LAYOUT=fr'
        ]
        image = 'ghcr.io/games-on-whales/wolf-ui:main'
        mounts = [ '/var/run/wolf/wolf.sock:/var/run/wolf/wolf.sock' ]
        name = 'Wolf-UI'
        ports = []
        type = 'docker'

```

##### Création des espaces utilisateur

De base, dans `config.toml`, un utilisateur `user` est créé avec toutes les apps dedans.

Vous pouvez créer un nouvel espace en ajoutant :

```
[[profiles]]
id = 'user'
name = '<NOM_DU_COMPTE>'
icon_png_path = 'url_de_votre_image.png'
pin = [ 3, 2, 1, 4 ]

```

<p class="callout info">L' `id` doit être unique, exemple : `user1`, `user2`, etc...  
Le `pin` est facultatif, il permet de mettre un password de 4 chiffres pour accéder à l'espace de l'utilisateur.</p>

<p class="callout info">Toutes les apps de cet utilisateur doivent être présentes sous ces lignes.</p>

---

### 4. Configuration Post-Installation

#### A. Appairage Moonlight

1. Lancer Moonlight sur le client (PC, TV, Tablette).
2. Noter le code PIN affiché.
3. Consulter les logs Docker : `docker logs -f wolf`
4. Cliquer sur le lien `http://IP_UNRAID:47989/pin/ID` visible dans les logs et entrer le code PIN à 4 chiffres affiché dans Moonlight.  
    ![image.png](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/scaled-1680-/image.png)
5. La page web vous demande de renseigner le PIN pour déverrouiller Moonlight.  
    ![image.png](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/scaled-1680-/bmTimage.png)

À la première ouverture de Steam, le dépôt s’installe, puis il faut vous connecter à votre compte avec Steam Guard.

#### B. Déclaration des jeux dans Steam

Par défaut, Steam ne connaît pas le disque d’installation des jeux configuré dans `config.toml`. Si vous laissez tel quel, il va installer les jeux dans le répertoire `appdata` (non partagé entre utilisateurs).

1. Fermer Big Picture.
2. Dans Steam, aller dans **Paramètres &gt; Stockage**.
3. Ajouter un lecteur &gt; remonter à `/Home` &gt; sélectionner le dossier `/games-unraid`.
4. Le définir comme lecteur **par défaut** (étoile).

<p class="callout info">À faire pour chaque compte Steam.</p>

#### C. MangoHud

L’overlay Steam ne fonctionne pas dans le conteneur Wolf. Pour voir les FPS et autres stats, on utilise **MangoHud**.

Il est déjà installé et activé par défaut pour tous les jeux Vulkan (y compris les jeux Proton).

![MangoHud](https://games-on-whales.github.io/wolf/stable/_images/mangohud_example.gif)

##### Jeux OpenGL (Linux natif)

Pour les jeux OpenGL, vous pouvez l’activer jeu par jeu en ajoutant :

```bash
mangohud %command%

```

![image.png](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/scaled-1680-/8sUimage.png)

##### Activation et configuration

En jeu, vous pouvez appuyer sur **`Maj droite + F12`** pour afficher/masquer MangoHud.  
Et sur **`Maj droite + F11`** pour changer sa position à l’écran.

---

### 5. Accès à Distance (Ouverture de Ports)

Wolf ne gère pas l’UPnP. Pour jouer depuis l’extérieur, deux options :

#### Option A : VPN (Recommandé)

Utiliser **Tailscale** ou **Wireguard** (plugins Unraid). C’est plus sécurisé et ne nécessite aucune ouverture de port. (Plus chiant côté FireTV ou autre, mais c’est la vie 😄)

#### Option B : Ouverture de Ports (NAT)

Sans VPN, rediriger les ports suivants sur votre routeur vers l’IP du serveur Unraid :

- **TCP 47984** : HTTPS (sécurité)
- **TCP 47989** : HTTP (appairage Web)
- **TCP 48010** : RTSP (initialisation du stream)
- **UDP 47999** : Contrôle (manettes)
- **UDP 48100** : Flux vidéo (RTP - spécifique Wolf)
- **UDP 48200** : Flux audio (RTP - spécifique Wolf)

# Applications

Voici la liste des applications utilisables.

<p class="callout success"><span style="color: rgb(224, 62, 45);">**&lt;en cours de rédaction pour l'ajout des autres app&gt;**</span></p>

## Heroic Games Launcher

Un lanceur open source pour gérer et jouer aux jeux **Epic Games Store**, **GOG** et **Amazon Prime Games** sous Linux.

### Configuration par défaut

Voici le bloc de configuration par défaut qui sera utilisé dans Wolf.  
Vous pouvez le remplacer en éditant le fichier `config.toml`.

```
[[profiles.apps]]
    icon_png_path = 'https://games-on-whales.github.io/wildlife/apps/heroic-games-launcher/assets/icon.png'
    start_virtual_compositor = true
    title = 'Heroic'

        [profiles.apps.runner]
        base_create_json = '''{
  "HostConfig": {
      "IpcMode": "host",
      "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"],
      "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"],
      "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}],
      "Privileged": false,
      "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"]
    }
}'''
        devices = []
        env = [ 'RUN_SWAY=true', 'GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*' ]
        image = 'ghcr.io/games-on-whales/heroic-games-launcher:edge'
        mounts = []
        name = 'WolfHeroic'
        ports = []
        type = 'docker'
```

<p class="callout info">Pensez à ajouter dans les variables d'environnement : **'XKB\_DEFAULT\_LAYOUT=fr'** pour le clavier AZERTY</p>

<p class="callout info">Si vous avez suivi mon tutoriel pour Wolf. Il faudra ajouter les points de montage suivant  
**mounts = \[**  
            **'/mnt/cache/Games:/home/retro/games-unraid:rw',**  
            **'/mnt/user/appdata/wolf/&lt;NOM\_DU\_COMPTE&gt;:/home/retro/.heroic:rw'**  
        **\]**</p>

### Premier lancement

Voici l'interface au démarrage de **Heroic.**

[![image.png](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/scaled-1680-/NqOimage.png)](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/NqOimage.png)

#### Association des comptes

Connecter vos différents comptes EPIC / GOG / Amazon, afin de récupérer vos bibliothèques de jeux.

#### Ajout du répertoire d'installation des jeux

Nous allons maintenant faire en sorte que les jeux installés depuis Heroic se fassent dans le même répertoire commun.

1. Allez dans le menu **`Options`** puis **`Général`**.
2. Dans la section `<strong>Chemin d'installation par défaut</strong>`, mettez 
    
    ```
    /home/retro/games-unraid
    ```
    
    <p class="callout info">C'est le chemin du répertoire que nous avons utilisé dans le tuto d'installation de Wolf. Il faudra l'adapter si vous avez choisi un autre répertoire.</p>

#### Utilisation de GE-PROTON

Pour faire fonctionner les jeux Windows (exécutable en .exe). Nous allons utiliser PROTON.  
J'ai noté une meilleure compatibilité qu'avec `Wine` qui est utilisé par défaut dans Heroic.

1. Allez dans le menu `<strong>Gestion de Wine</strong>`
2. Choisir l'onglet **`GE-Proton`**
3. Cliquez sur la flèche de téléchargement pour la version `<strong>GE-Proton-Latest</strong>`

Utilisation de Proton par dédaut :

1. Allez dans le menu **`Options`** puis **`Paramètre de jeu par défaut`**.
2. Choisissez `Proton - GE - Proton - Latest` dans la version de Wine

### Ajout des jeux Heroic dans Steam

Dans le but de simplifier l'accès à notre bibliothèque de jeux. Nous allons ajouter les jeux téléchargés depuis les différentes plateformes, à Steam. Ce qui permettra de centraliser sur Steam l'ensemble de nos jeux.

1. Au lancement de Steam, quittez le mode Big Picture.
2. Allez dans le menu `<strong>Steam</strong>` puis **`Paramètres`.**
3. Dans les paramètres, allez dans le menu **`Bibliothèque`**, ensuite cliquez **`Ajouter un jeu non Steam`**
4. Cliquez sur `<strong>Parcourir...</strong>` Et naviguez dans `/home/retro/games-unraid` (le répertoire de notre tuto)
5. Sélectionner les `<strong>.exe</strong>` des jeux que vous voulez importer dans Steam.
6. Ne pas oublier de cliquer `<strong>Ajouter la sélection</strong>` une fois fait.

Les jeux seront désormais dans la section `NON STEAM` de votre bibliothèque.

##### Prérequis pour lancer les jeux

Si vos jeux sont des installations Windows (executable en .exe). Il faudra les forcer en compatibilité PROTON

1. Dans Steam, allez dans les `<strong>Propriétés...</strong>` de votre jeu.
2. Dans le menu `<strong>Compatibilité</strong>`, cochez `<strong>forcer l'utilisation d'un outil...</strong>` Et choisissez `<strong>Proton Expérimental</strong>`

[![image.png](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/scaled-1680-/60Oimage.png)](https://bookstack.thymon.fr/uploads/images/gallery/2025-11/60Oimage.png)

# Dolphin pour RomM

# RomM — Emulator Streaming avec Dolphin sur Unraid (GPU NVIDIA)

Ce tutoriel explique comment ajouter un conteneur **Dolphin** (GameCube/Wii) à une instance **RomM 5.1.0+** existante pour utiliser la fonctionnalité **Emulator Streaming** : lancer un jeu depuis l'interface RomM et y jouer dans le navigateur, avec rendu et encodage sur le GPU NVIDIA.

**Architecture :** RomM ne pilote pas Dolphin directement. Un *Docker Mod* (maintenu par LoneAngelFayt) injecte dans le conteneur Dolphin un **broker HTTP** (port 8000) que RomM contacte pour lancer/piloter les jeux. L'image et le son sont diffusés par **Selkies** (le bureau web intégré aux images LinuxServer), en H.264 NVENC.

> ⚠️ Testé avec : Unraid 7, RTX 3060, driver NVIDIA 610.x, `lscr.io/linuxserver/dolphin:latest` (base Selkies/Wayland), RomM 5.1.0.

---

## 1. Prérequis Unraid

### 1.1 Plugin Nvidia Driver

- Installer le plugin **Nvidia Driver** (Community Applications).
- Driver propriétaire **580 ou supérieur** requis. LinuxServer recommande la **branche Production** pour Unraid.
- Le GPU ne doit **pas** être réservé au VFIO (il doit être disponible pour Docker).
- Sur un serveur *headless* (sans écran), la doc LinuxServer indique qu'un **dummy plug HDMI/DP** branché sur la carte est requis pour que DRM s'initialise correctement.

### 1.2 Paramètres noyau (Syslinux)

Menu **Main → Flash → Syslinux Config** (vue *Raw*), ajouter à la fin de la ligne `append` de l'entrée « Unraid OS » (et « Unraid OS GUI Mode » si utilisée) :

```
nvidia-drm.modeset=1 nvidia_drm.fbdev=1
```

Redémarrer le serveur, puis vérifier :

```bash
cat /proc/cmdline                                    # les 2 paramètres doivent apparaître
cat /sys/module/nvidia_drm/parameters/modeset        # doit renvoyer Y
nvidia-smi -L                                        # liste le(s) GPU + UUID
```

Notez l'**UUID du GPU** (`GPU-xxxxxxxx-...`), il servira dans le template.

### 1.3 Limite inotify (optionnel mais recommandé)

Les images Selkies consomment beaucoup d'instances inotify. Pour éviter l'avertissement « Too many open files » :

```bash
sysctl -w fs.inotify.max_user_instances=1024
echo 'sysctl -w fs.inotify.max_user_instances=1024' >> /boot/config/go
```

---

## 2. ⚠️ Correctifs NVIDIA indispensables (Unraid)

Le runtime NVIDIA d'Unraid présente **deux trous d'injection** vers les conteneurs. Sans eux, l'émulateur tourne en rendu logiciel (très lent) ou affiche un écran noir. Les deux correctifs sont simples et persistants.

### 2.1 Composant egl-x11 (rendu OpenGL/EGL)

**C'est le point qui fait échouer la plupart des installations** (symptôme : interface Dolphin visible, mais **zone de jeu noire** avec le son, ou jeu qui rame énormément en rendu logiciel).

Le runtime NVIDIA d'Unraid injecte dans les conteneurs les plateformes EGL `wayland`, `wayland2` et `gbm`, **mais pas les deux plateformes X11** (`xlib`/`xcb`) — pourtant livrées par le driver et indispensables à Dolphin, qui crée son contexte OpenGL via **EGL sur X11**. Sans elles, l'EGL NVIDIA ne peut pas créer de surface et Dolphin retombe en rendu logiciel (ou écran noir via Zink : `MESA: error: zink: could not create swapchain`).

Vérifier que le driver de l'hôte fournit bien les fichiers :

```bash
ls /var/local/overlay/usr/lib64/ | grep egl-x
ls /var/local/overlay/usr/share/egl/egl_external_platform.d/ | grep -E "xlib|xcb"
```

Vous devez voir `libnvidia-egl-xlib.so.1.X.X`, `libnvidia-egl-xcb.so.1.X.X`, `20_nvidia_xlib.json` et `20_nvidia_xcb.json`. **Notez le numéro de version exact des `.so`** (ex. `1.0.5`) : il est utilisé dans les montages ci-dessous, et il **change à chaque mise à jour du driver** (voir §7).

Le correctif consiste en **4 montages en lecture seule** + **1 variable**, intégrés au template Dolphin de la section suivante.

### 2.2 Nœud `/dev/nvidia-modeset` (présentation Vulkan)

Le toolkit injecte `nvidia0`, `nvidiactl`, `nvidia-uvm`, `nvidia-uvm-tools` et `nvidia-caps`… **mais pas `/dev/nvidia-modeset`**, qui est nécessaire à la *présentation* Vulkan (l'énumération des GPU, elle, fonctionne sans lui — d'où des diagnostics trompeurs).

Symptômes sans ce nœud : `vulkaninfo` liste bien la carte mais `vkGetPhysicalDeviceSurfacePresentModesKHR` échoue en `ERROR_UNKNOWN` ; `vkcube` sélectionne le GPU puis plante à la création de la swapchain ; Dolphin en backend Vulkan ou via Zink affiche un **écran noir avec le son**.

Vérification :

```bash
ls /dev/nvidia-modeset                                  # sur l'hôte : présent
docker exec dolphin ls /dev/nvidia-modeset 2>&1         # dans le conteneur : absent
```

Correctif : ajouter `--device /dev/nvidia-modeset` aux Extra Parameters (voir §3.4).

---

## 3. Conteneur Dolphin

**Ajouter un conteneur** avec l'image `lscr.io/linuxserver/dolphin` et la configuration suivante.

### 3.1 Générer un secret partagé

Ce secret authentifie les échanges RomM ↔ broker. Générez-le une fois :

```bash
openssl rand -hex 32
```

### 3.2 Ports (host:container)

| Port hôte | Port conteneur | Rôle |
|---|---|---|
| 3000 | 3000 | Selkies HTTP (peu utile) |
| 3001 | 3001 | **Selkies HTTPS** (le flux de jeu) |
| 8000 | 8000 | **Broker RomM** |

Adaptez les ports hôte s'ils sont déjà pris par un autre conteneur (dans ce cas, reportez le port choisi dans le bloc `streaming` du §4.2).

### 3.3 Variables d'environnement

| Variable | Valeur | Remarque |
|---|---|---|
| `DOCKER_MODS` | `ghcr.io/loneangelfayt/dolphin-romm-integration-mod:latest` | installe le broker |
| `ROM_ROOT` | `/romm/library` | racine de la bibliothèque vue par le broker |
| `BROKER_SECRET` | *(votre secret)* | même valeur que côté RomM |
| `NVIDIA_VISIBLE_DEVICES` | *(UUID du GPU)* | ou `all` si un seul GPU |
| `NVIDIA_DRIVER_CAPABILITIES` | `all` | |
| `DRINODE` | `/dev/dri/renderD128` | GPU de **rendu** (vérifier avec `ls -l /dev/dri/by-path/` que c'est bien la carte NVIDIA) |
| `DRI_NODE` | `/dev/dri/renderD128` | GPU d'**encodage** — identique = mode Zero-Copy |
| `SELKIES_MANUAL_WIDTH` | `1920` | bride la résolution du flux |
| `SELKIES_MANUAL_HEIGHT` | `1080` | sinon elle suit la fenêtre du navigateur (coûteux) |
| `__EGL_VENDOR_LIBRARY_FILENAMES` | `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` | force l'EGL NVIDIA (fait partie du correctif §2) |
| `PUID` / `PGID` | `99` / `100` | standard Unraid |

### 3.4 Extra Parameters

```
--gpus all --runtime nvidia --shm-size=1gb --device /dev/nvidia-modeset --cpuset-cpus=0-5,12-17
```

- `--device /dev/nvidia-modeset` : correctif §2.2.
- `--cpuset-cpus` : épinglage CPU, **obligatoire sur tout Ryzen multi-CCD** — sans lui les performances sont fortement dégradées (voir §7). Adaptez impérativement la liste à votre processeur : la valeur ci-dessus correspond au premier CCD d'un 3900X et sera **contre-productive** sur une autre topologie. Sur un CPU mono-CCD ou Intel, retirez l'option.

### 3.5 Volumes

| Host Path | Container Path | Mode |
|---|---|---|
| *(votre dossier de ROMs — le même que celui monté dans RomM)* | `/romm/library` | **ro** |
| *(votre appdata, ex. `/mnt/user/appdata/dolphin`)* | `/config` | rw |
| `/var/local/overlay/usr/lib64/libnvidia-egl-xlib.so.1.X.X` | `/usr/lib64/libnvidia-egl-xlib.so.1` | **ro** |
| `/var/local/overlay/usr/lib64/libnvidia-egl-xcb.so.1.X.X` | `/usr/lib64/libnvidia-egl-xcb.so.1` | **ro** |
| `/var/local/overlay/usr/share/egl/egl_external_platform.d/20_nvidia_xlib.json` | `/usr/share/egl/egl_external_platform.d/20_nvidia_xlib.json` | **ro** |
| `/var/local/overlay/usr/share/egl/egl_external_platform.d/20_nvidia_xcb.json` | `/usr/share/egl/egl_external_platform.d/20_nvidia_xcb.json` | **ro** |

> Remplacez `1.X.X` par la version relevée au §2. Le Container Path des libs est volontairement le nom **court** `.so.1` : c'est celui que le loader EGL recherche (déclaré dans les JSON), ce qui évite d'avoir à créer des symlinks.

> 💡 **Important :** le chemin de la bibliothèque doit être **identique** dans RomM et dans Dolphin (`/romm/library` des deux côtés), sinon le broker ne retrouvera pas les fichiers que RomM lui demande de lancer.

### 3.6 Vérifications après démarrage

```bash
docker logs dolphin 2>&1 | grep -iE "broker|NVENC|Zero-Copy"
```

Attendu : `ROM broker listening on port 8000`, `Shared secret auth enabled`, `NVENC ... Initialized successfully`, `Decision: Zero-Copy path active`.

```bash
curl -s http://localhost:8000/health        # → {"status":"ok"}
docker exec dolphin sh -c 'ldconfig -p | grep egl-x'   # → les 2 libs xlib/xcb
```

---

## 4. Côté RomM

### 4.1 Variable d'environnement

Ajouter au conteneur RomM :

| Variable | Valeur |
|---|---|
| `STREAMING_BROKER_SECRET` | *(le même secret qu'au §3.1)* |

### 4.2 config.yml

Dans le `config.yml` de RomM (dossier config), ajouter le bloc `streaming` (adapter l'IP du serveur et les ports choisis) :

```yaml
streaming:
  enabled: true
  containers:
    - platform: ngc
      host: https://IP_DU_SERVEUR:3001
      broker_host: http://IP_DU_SERVEUR:8000
      label: Dolphin
      memory_card_sync: true
    - platform: wii
      host: https://IP_DU_SERVEUR:3001
      broker_host: http://IP_DU_SERVEUR:8000
      label: Dolphin
```

Redémarrer RomM après modification.

### 4.3 Structure de la bibliothèque

- Le dossier de plateforme doit correspondre au **fs_slug** de RomM : **`ngc`** pour la GameCube, **`wii`** pour la Wii (pas `gc`, pas `gamecube`) — sinon prévoir un mappage dans `config.yml`.
- Les ROMs doivent être des **fichiers directs** (`.rvz`, `.iso`) — **pas d'archives** `.zip`/`.7z`, Dolphin ne peut pas les lancer via le broker.
- ⚠️ RomM supporte deux structures (`library/roms/<plateforme>/` ou `library/<plateforme>/roms/`) mais **la simple présence d'un dossier `library/roms/` fait basculer toute la bibliothèque sur la première** : ne mélangez pas les deux, ou toutes vos autres plateformes deviendront invisibles au scan.

Rescanner la plateforme dans RomM après tout changement.

---

## 5. Premier lancement

1. Ouvrir une première fois `https://IP_DU_SERVEUR:3001` et **accepter le certificat auto-signé** (sinon le flux ne se chargera pas dans RomM).
2. Dans RomM, ouvrir la fiche d'un jeu GameCube → bouton **Play on Dolphin**.
3. Le jeu doit s'afficher, fluide, avec le son.

**Vérifier que le GPU est bien utilisé** (pendant qu'un jeu tourne) :

```bash
docker exec -u abc dolphin sh -c 'for p in /proc/[0-9]*; do ls -l $p/fd 2>/dev/null | grep -q nvidia && echo "$(basename $p) $(tr "\0" " " < $p/cmdline | cut -c1-60)"; done'
```

`dolphin-emu` **doit** apparaître dans la liste. S'il n'y a que `selkies`, `labwc` et `Xwayland`, Dolphin est en rendu logiciel → revoir le §2.

> Note : lancez bien ce test avec `-u abc` — en root, la lecture de `/proc/<pid>/fd` des processus de l'utilisateur `abc` échoue silencieusement.

---

## 6. Réglages Dolphin utiles

Le fichier de configuration est `<appdata>/.config/dolphin-emu/Dolphin.ini`. **Toujours éditer conteneur arrêté** (Dolphin réécrit ses fichiers en se fermant), et savoir que **le mod réimpose `GFXBackend = OpenGL` à chaque recréation du conteneur** — c'est le bon backend ici (c'est le chemin EGL corrigé au §2), ne pas chercher à le changer.

> ⚠️ **Après toute édition depuis l'hôte** (`sed -i`, `cat >`, redirection…), rétablissez le propriétaire : `chown -R 99:100 <appdata>`. Ces commandes peuvent recréer le fichier en `root:root`, et l'émulateur ne peut alors plus enregistrer sa configuration — silencieusement, à part une ligne d'erreur dans son log.

### 6.1 Performances

Réglage recommandé (section `[Core]`) :

```ini
CPUThread = True
```

C'est le mode « Dual Core » de Dolphin ; selon les versions il peut être désactivé par défaut, et son absence coûte très cher en performances.

### 6.2 Jeux en français

La langue de la **console émulée** (celle que les jeux GameCube utilisent) se règle dans `[Core]` :

```bash
docker stop dolphin
F=<appdata>/.config/dolphin-emu/Dolphin.ini

sed -i '/^SelectedLanguage *=/d' "$F"
sed -i '/^\[Core\]/a SelectedLanguage = 2' "$F"

chown -R 99:100 <appdata>
docker start dolphin
grep -n "^SelectedLanguage" "$F"
```

Valeurs : `0` anglais, `1` allemand, **`2` français**, `3` espagnol, `4` italien, `5` néerlandais.

> Cette clé survit aux redémarrages **et** aux recréations du conteneur : le mod ne la touche pas.

> ⚠️ **N'ajoutez pas `[Interface] LanguageCode`** pour traduire l'interface de Dolphin : dans cette image, la clé provoque une boîte de dialogue d'erreur de langue à chaque démarrage. Les jeux sont de toute façon en français grâce à `SelectedLanguage`, ce qui est l'essentiel — l'interface de l'émulateur n'est presque jamais visible pour les joueurs (le broker lance directement le jeu en plein écran).

> **Jeux Wii :** la langue ne vient pas de `Dolphin.ini` mais de la NAND Wii émulée. Elle se règle dans l'interface (Options → Configuration → Wii → Langue du système) et persiste dans l'appdata.

---

## 7. ⚠️ Épinglage CPU — obligatoire sur Ryzen multi-CCD

Sur les Ryzen à plusieurs CCD (3900X, 3950X, 5900X, 5950X, 7900X, 7950X…), les cœurs sont répartis en groupes ayant chacun **son propre cache L3**. Un émulateur dont les threads dialoguent en permanence perd beaucoup quand l'ordonnanceur les fait migrer d'un groupe à l'autre : les échanges passent alors par l'Infinity Fabric, bien plus lent.

**Ce n'est pas une optimisation facultative :** sans épinglage, comptez une perte de l'ordre d'un tiers des performances, suffisante pour rendre injouables les titres exigeants. Le symptôme est déroutant — **performances médiocres alors que rien ne sature** (CPU global bas, GPU à 10 %, aucun thread à 100 %) — parce que les threads n'attendent pas du calcul, mais la mémoire.

Identifiez vos groupes de cache :

```bash
lscpu -e=CPU,CORE,L3
```

Les CPU partageant le même numéro de L3 (ou deux L3 voisins d'un même CCD) forment un groupe. Exemple sur un 3900X : le premier CCD regroupe les CPU `0-5` et leurs hyperthreads `12-17`.

Épinglez ensuite le conteneur sur un seul CCD via `--cpuset-cpus=0-5,12-17` (Extra Parameters), ou l'onglet **CPU Pinning** du template Unraid.

> Le gain observé sur ce type de configuration va de quelques pourcents à **+50 %**. C'est aussi ce qui explique qu'une VM avec vCPU épinglés paraisse bien plus rapide que le même émulateur en conteneur non épinglé, sur la même machine.

> **Multi-conteneurs :** si Dolphin et Eden tournent simultanément, répartissez-les sur des groupes différents plutôt que de les faire cohabiter sur le même CCD.

## 8. Maintenance et limites connues

- **À chaque mise à jour du driver NVIDIA de l'hôte**, le nom des libs (`libnvidia-egl-*.so.1.X.X`) change : mettre à jour les 2 Host Path correspondants dans le template Dolphin, sinon écran noir / rendu logiciel au retour.
- **Une seule session à la fois** par conteneur : un conteneur = une instance Dolphin = un flux. Pour du multi-joueurs simultané, dupliquer le conteneur (autres ports, autre secret possible) et ajouter une entrée dans `streaming.containers`.
- **Sessions fantômes** : fermer l'onglet du navigateur ne tue pas Dolphin ; la session peut rester occupée. En attendant mieux : `docker exec dolphin pkill dolphin-emu`.
- Les paquets installés à la main dans le conteneur (`apt-get`) **ne survivent pas** à une recréation. Pour des outils de diagnostic permanents, chaîner le mod `universal-package-install` : `DOCKER_MODS=ghcr.io/loneangelfayt/dolphin-romm-integration-mod:latest|linuxserver/mods:universal-package-install` + `INSTALL_PACKAGES=vulkan-tools|mesa-utils|x11-utils`.

## 9. Dépannage rapide

| Symptôme | Cause probable | Solution |
|---|---|---|
| « ROMs not found » au scan | Nom de dossier ≠ fs_slug, ou structures mélangées | §4.3 |
| Pas de bouton « Play on Dolphin » | Bloc `streaming` absent/invalide, ou secret différent | §4.1–4.2 |
| Flux ne se charge pas dans RomM | Certificat auto-signé jamais accepté | §5.1 |
| Jeu très lent, `dolphin-emu` absent du test des fd | Rendu logiciel (egl-x11 manquant) | §2.1 |
| Zone de jeu **noire**, interface et son OK | Présentation Vulkan impossible (`/dev/nvidia-modeset` manquant) | §2.2 |
| Jeu lent alors que rien ne sature (CPU bas, GPU ~10 %) | Threads migrant entre CCD | §7 |
| Écran noir après une mise à jour du driver | Version des libs egl-x11 changée | §8 |
| RomM renvoie **502** au lancement | L'émulateur crashe au démarrage : le broker abandonne après 5 essais. Voir `<appdata>/dolphin.log` | ci-dessous |
| `qt.qpa.xcb: could not connect to display :0` dans `dolphin.log` | Sockets X périmées accumulées dans `/tmp/.X11-unix` (le broker détecte un affichage mort) | **Recréer** le conteneur — un simple redémarrage conserve `/tmp` |
| Boîte d'erreur de langue au démarrage | Clé `[Interface] LanguageCode` présente | La supprimer (§6.2) |
| « Failed to initialize video backend » | Variables graphiques parasites ou backend forcé à Vulkan | Revenir à la config §3, `GFXBackend = OpenGL` |

# Eden pour RomM

# RomM — Emulator Streaming avec Eden (Nintendo Switch) sur Unraid (GPU NVIDIA)

Ce tutoriel explique comment ajouter un conteneur **Eden** (émulateur Nintendo Switch, fork de Yuzu) à une instance **RomM 5.1.0+** pour lancer des jeux Switch depuis RomM et y jouer dans le navigateur, avec rendu GPU NVIDIA.

Le principe est identique à l'intégration Dolphin : un *Docker Mod* injecte un **broker HTTP** (port 8000 du conteneur) que RomM contacte, et le flux est diffusé par **Selkies** en H.264 NVENC.

> ✅ Testé et fonctionnel avec **RomM 5.1.0 stable** (la branche de développement n'est pas nécessaire), Unraid 7, Ryzen 9 3900X + RTX 3060, `lscr.io/linuxserver/eden`. Résultat : **Mario Kart 8 Deluxe à 60 FPS stables** en 1080p dans le navigateur.

> 📖 Ce guide suppose que la page **« Emulator Streaming avec Dolphin »** a déjà été suivie : les prérequis hôte sont les mêmes et ne sont pas re-détaillés ici — plugin Nvidia Driver (branche Production, ≥ 580), paramètres noyau `nvidia-drm.modeset=1 nvidia_drm.fbdev=1`, et surtout les **deux correctifs NVIDIA (§2 de la page Dolphin)** : composant **egl-x11** et nœud **`/dev/nvidia-modeset`**, indispensables ici aussi.

> 🌐 **Ce guide décrit une installation en réseau local.** Pour jouer depuis l'extérieur, pour l'isolation réseau des conteneurs, et pour le **portail nginx du mod Eden qui bloque le streaming avec RomM 5.1.0**, voir la page **« Accès distant, publication HTTPS et isolation réseau »**. Elle contient aussi l'avertissement de sécurité à lire avant toute publication.

---

## 1. Particularités d'Eden à connaître avant de commencer

1. **Contenu requis de votre propre console.** Eden ne lance aucun jeu commercial sans les **`prod.keys`** (clés de déchiffrement) et le **firmware** de la Switch — à extraire de votre propre console.
2. **Eden est Vulkan-first**, et son backend Vulkan a besoin du nœud `/dev/nvidia-modeset` (§2.2 de la page Dolphin). Sans lui : liste de GPU **vide** dans les menus, et au lancement d'un jeu l'erreur *« Eden has encountered an error while running the video core »* (log : `Invalid device index -1`, `VK_ERROR_INITIALIZATION_FAILED`).
3. **Les réglages faits dans l'interface ne sont jamais sauvegardés** quand Eden est lancé par le broker : celui-ci tue le processus avant qu'Eden n'écrive sa configuration. Tout réglage durable se fait **dans le fichier, conteneur arrêté** (§5).
4. **Deux réglages par défaut coûtent très cher en performances** : la précision GPU sur *High* et l'absence d'épinglage CPU sur les Ryzen multi-CCD (§5 et §6). Sans eux, comptez ~30 FPS au lieu de 60.

---

## 2. Conteneur Eden

Image : `lscr.io/linuxserver/eden`. Configuration identique à Dolphin, à trois différences près : le mod, les ports hôte (les deux conteneurs coexistent), et l'appdata dédié.

### 2.1 Ports (host:container)

Les services écoutent toujours sur **3000/3001/8000 côté conteneur** — seuls les ports hôte changent :

| Port hôte (exemple) | Port conteneur | Rôle |
|---|---|---|
| 3010 | 3000 | Selkies HTTP |
| 3011 | 3001 | **Selkies HTTPS** (flux de jeu) |
| 8001 | 8000 | **Broker RomM** |

### 2.2 Variables d'environnement

| Variable | Valeur |
|---|---|
| `DOCKER_MODS` | `ghcr.io/loneangelfayt/eden-romm-integration-mod:latest` |
| `ROM_ROOT` | `/romm/library` |
| `BROKER_SECRET` | **le même secret que vos autres conteneurs émulateurs** — RomM n'a qu'un `STREAMING_BROKER_SECRET` global |
| `NVIDIA_VISIBLE_DEVICES` | UUID du GPU (ou `all`) |
| `NVIDIA_DRIVER_CAPABILITIES` | `all` |
| `DRINODE` / `DRI_NODE` | `/dev/dri/renderD128` (adapter) |
| `SELKIES_MANUAL_WIDTH` / `HEIGHT` | `1920` / `1080` |
| `__EGL_VENDOR_LIBRARY_FILENAMES` | `/usr/share/glvnd/egl_vendor.d/10_nvidia.json` |
| `PUID` / `PGID` | `99` / `100` |

### 2.3 Extra Parameters

```
--gpus all --runtime nvidia --shm-size=1gb --device /dev/nvidia-modeset --cpuset-cpus=0-5,12-17
```

L'épinglage `--cpuset-cpus` est **obligatoire sur tout Ryzen multi-CCD** : sans lui, comptez environ un tiers de performances en moins (§6). Adaptez impérativement la liste à votre processeur — la valeur ci-dessus correspond au premier CCD d'un 3900X et serait **contre-productive** sur une autre topologie. Sur un CPU mono-CCD ou Intel, retirez l'option.

### 2.4 Volumes

| Host Path | Container Path | Mode |
|---|---|---|
| *(le même dossier de ROMs que RomM)* | `/romm/library` | **ro** |
| *(appdata dédié, ex. `/mnt/user/appdata/eden`)* | `/config` | rw |
| **+ les 4 montages du correctif egl-x11** (identiques à la page Dolphin : 2 libs + 2 JSON) | | **ro** |

> Rappel : le dossier de bibliothèque doit être monté **au même chemin conteneur** (`/romm/library`) dans RomM et dans Eden.

### 2.5 Vérification du broker

```bash
docker logs eden 2>&1 | grep -iE "broker|mod" | head -10
curl -s http://localhost:8001/health        # → {"status": "ok"}
```

---

## 3. Premier démarrage d'Eden

Ouvrir `https://IP_DU_SERVEUR:3011`, accepter le certificat auto-signé. Au premier lancement, Eden propose de forcer **X11** au lieu de Wayland : **accepter** (c'est le chemin réparé par le correctif egl-x11, et celui qu'utilise le broker).

---

## 4. Clés, firmware, jeux et mises à jour

**Ordre important : les clés d'abord, le firmware ensuite** (Eden a besoin des clés pour installer le firmware).

1. **`prod.keys`** : fichier décompressé, à poser dans l'appdata : `<appdata>/.local/share/eden/keys/prod.keys`. Redémarrer Eden ensuite.
2. **Firmware** : garder le ZIP tel quel, le poser dans l'appdata (visible sous `/config/` dans le conteneur), puis dans Eden : **Tools → Install Firmware** en pointant le ZIP.
3. **Jeux** : fichiers directs `.nsp` ou `.xci` (pas d'archives), dans le dossier de plateforme **`switch`** de la bibliothèque (fs_slug RomM : `switch`).
4. **Mises à jour et DLC** : Eden n'a **pas** de « dossier de patchs » — ils s'installent une fois pour toutes dans la NAND virtuelle : **File → Install Files to NAND**, sélectionner les `.nsp` de mise à jour. Vérification : clic droit sur le jeu → Properties → la version doit être celle de la mise à jour. Tout persiste dans l'appdata (`.local/share/eden/nand/`) et profite à tous les joueurs. Ne placez **pas** les mises à jour dans la bibliothèque RomM (elles seraient scannées comme une fausse plateforme).

---

## 5. ⚠️ Réglages Eden (à faire dans le fichier, conteneur arrêté)

L'interface ne sauvegarde rien quand Eden est lancé par le broker (§1.3). Éditez donc directement `<appdata>/.config/eden/qt-config.ini`, **conteneur arrêté**.

Chez Eden, chaque clé est accompagnée d'une ligne `clé\default=` : tant qu'elle vaut `true`, la valeur est ignorée au profit du défaut compilé. **Il faut donc toujours passer `\default` à `false`.**

> ⚠️ **Après toute édition depuis l'hôte** (`sed -i`, `cat >`, redirection…), rétablissez le propriétaire : `chown -R 99:100 <appdata>`. Ces commandes recréent le fichier et peuvent le laisser en `root:root` ; Eden ne peut alors plus enregistrer sa configuration, ce qui ne se voit que par un `Config file could not be saved!` dans son log.

### 5.1 Les deux réglages qui comptent

```bash
docker stop eden
F=<appdata>/.config/eden/qt-config.ini

# Backend graphique : Vulkan
sed -i 's/^backend\\default=.*/backend\\default=false/; s/^backend=.*/backend=1/' "$F"

# Précision GPU : Normal (le défaut compilé est High = beaucoup plus lent)
sed -i 's/^gpu_accuracy\\default=.*/gpu_accuracy\\default=false/; s/^gpu_accuracy=.*/gpu_accuracy=0/' "$F"

docker start eden
grep -nE "^backend|^gpu_accuracy" "$F"
```

**`gpu_accuracy` est le réglage le plus rentable de tout ce guide** : le défaut *High* force des chemins d'émulation beaucoup plus lents, pour une précision dont la quasi-totalité des jeux n'a pas besoin. Mesuré ici : **+33 % de FPS** en passant sur *Normal*.

### 5.2 Énumérations utiles

Depuis janvier 2026, Eden a fusionné le backend de rendu et le backend de shaders dans une seule clé `backend` :

| `backend` | Valeur |
|---|---|
| 0 | OpenGL (GLSL) |
| 1 | **Vulkan** (recommandé) |
| 2 | Null |
| 3 | OpenGL GLASM |
| 4 | OpenGL SPIR-V |

> La clé `shader_backend` est **obsolète** : si elle traîne dans votre fichier, elle est ignorée en silence. Supprimez-la.

| `gpu_accuracy` | Valeur |
|---|---|
| 0 | **Normal** (recommandé) |
| 1 | High (défaut compilé) |
| 2 | Extreme |

### 5.3 Réglages vérifiés, à laisser tels quels

`use_asynchronous_gpu_emulation=true` (indispensable), `use_multi_core=true`, `use_disk_shader_cache=true`, `speed_limit=100`.

`use_asynchronous_shaders` : laisser à **false** — testé ici, l'activer dégrade légèrement les performances.

### 5.4 Langue française et plein écran

Là encore, deux notions distinctes : la langue de **la console émulée** (celle que les jeux utilisent) et celle de **l'interface** d'Eden.

```bash
docker stop eden
F=<appdata>/.config/eden/qt-config.ini

# Langue de la console émulée (jeux) : 2 = français
sed -i 's/^language_index\\default=.*/language_index\\default=false/; s/^language_index=.*/language_index=2/' "$F"

# Plein écran persistant
sed -i 's/^fullscreen\\default=.*/fullscreen\\default=false/; s/^fullscreen=.*/fullscreen=true/' "$F"

docker start eden
grep -nE "^language_index|^fullscreen" "$F"
```

Valeurs de `language_index` : `0` japonais, `1` anglais US, **`2` français**, `3` allemand, `4` italien, `5` espagnol, `12` anglais UK, `13` français canadien.

**Interface d'Eden en français :** la clé `language` de la section `[UI]` stocke un code de locale dont le format varie selon les builds. Le plus fiable est de la faire écrire par Eden lui-même :

1. Lancer Eden **manuellement** (pas via RomM) depuis le bureau Selkies
2. Emulation → Configure → General → Interface language → *Français*
3. **Fermer Eden proprement** (File → Exit) — c'est ce qui déclenche l'écriture de la configuration
4. Vérifier : `grep -n "^language" <appdata>/.config/eden/qt-config.ini`

Si la ligne `language\default=true` subsiste, la passer à `false` conteneur arrêté.

> Rappel : le plein écran est de toute façon déclenché par le broker (il envoie F11 trois secondes après le lancement). Le réglage ci-dessus le rend cohérent aussi pour les lancements manuels. Pour en sortir ponctuellement — par exemple pour lire le compteur de FPS — appuyer sur **Échap**.

---

## 6. ⚠️ Épinglage CPU — obligatoire sur Ryzen multi-CCD

Voir la section correspondante de la page Dolphin pour l'explication détaillée. En résumé : sur un Ryzen à plusieurs CCD, les threads d'Eden migrant d'un groupe de cache à l'autre coûtent très cher, sans que rien ne paraisse saturer.

**Ce réglage n'est pas optionnel** : il représente ici la moitié du chemin entre un jeu injouable et 60 FPS stables.

```bash
lscpu -e=CPU,CORE,L3       # identifier les groupes de cache
```

Puis épingler le conteneur sur un seul CCD via `--cpuset-cpus` (Extra Parameters) ou l'onglet **CPU Pinning** du template.

> Mesuré sur un 3900X avec Mario Kart 8 Deluxe : **40 → 60 FPS** grâce à ce seul réglage. C'est aussi ce qui explique qu'une VM avec vCPU épinglés paraisse bien plus rapide que le même émulateur en conteneur non épinglé.

**Récapitulatif des gains mesurés** (Mario Kart 8 Deluxe, 1080p) :

| Étape | FPS |
|---|---|
| Configuration initiale | ~30 (voire écran noir sans les correctifs §2 de la page Dolphin) |
| + `gpu_accuracy=0` | ~40 |
| + épinglage CCD | **60 stables** |

---

## 7. Côté RomM

Ajouter l'entrée dans le bloc `streaming` du `config.yml` (adapter IP et ports) :

```yaml
    - platform: switch
      host: https://IP_DU_SERVEUR:3011
      broker_host: http://IP_DU_SERVEUR:8001
      label: Eden
```

Redémarrer RomM, rescanner la plateforme Switch.

---

## 8. Test final

1. Fermer proprement toute instance d'Eden ouverte dans le bureau Selkies (le broker lance la sienne).
2. Fiche d'un jeu Switch dans RomM → bouton **Play on Eden**.
3. Le jeu doit s'afficher, avec le son.

Vérifier l'accélération GPU pendant qu'un jeu tourne :

```bash
docker exec -u abc eden sh -c 'for p in /proc/[0-9]*; do ls -l $p/fd 2>/dev/null | grep -q nvidia && echo "$(basename $p) $(tr "\0" " " < $p/cmdline | cut -c1-60)"; done'
```

Le processus `eden` doit apparaître dans la liste.

> **Astuce mesure :** le broker met Eden en plein écran (il envoie F11 après 3 s), ce qui masque la barre d'état. Appuyez sur **Échap** pour en sortir et lire le FPS et le pourcentage de vitesse.

---

## 9. Dépannage rapide

| Symptôme | Cause | Solution |
|---|---|---|
| « error while running the video core », log : `Invalid device index -1` | Nœud `/dev/nvidia-modeset` absent → Vulkan sans présentation | §2.2 de la page Dolphin |
| Liste de GPU vide dans les menus Graphics | Idem | §2.2 de la page Dolphin |
| Réglages qui « ne tiennent pas » | Config jamais écrite (processus tué par le broker) | Éditer `qt-config.ini` conteneur arrêté (§5) |
| Réglage écrit mais sans effet | Ligne `clé\default=true` restée en place | Passer `\default` à `false` (§5) |
| ~30 FPS au lieu de 60, sans que rien ne sature | `gpu_accuracy` sur High et/ou pas d'épinglage CPU | §5.1 et §6 |
| Erreur de clés à l'installation du firmware ou d'un jeu | `prod.keys` absent ou mal placé | §4, puis redémarrer Eden |
| Jeu à la version 1.0.0 malgré la mise à jour | Mise à jour non installée en NAND | §4 — File → Install Files to NAND |
| Jeu très lent, `eden` absent du test des fd | Correctif egl-x11 manquant | §2.1 de la page Dolphin |
| Écran noir après une mise à jour du driver NVIDIA | Version des libs egl-x11 changée sur l'hôte | Mettre à jour les Host Path des montages |
| **403 Forbidden nginx** en ouvrant l'URL du flux | Portail du mod : normal sans session RomM ouverte | Page « Accès distant », §5 |
| Écran noir depuis RomM, alors que le lancement réussit côté serveur | Portail du mod : RomM 5.1.0 n'envoie pas le `stream_token` | Page « Accès distant », §5 |
| Le problème revient après chaque modification de template | Le conteneur est recréé, le portail est réinstallé | Script `custom-cont-init.d` — page « Accès distant », §5 |

# RomM — Accès distant, publication HTTPS et isolation réseau

Cette page complète les guides **Dolphin** et **Eden**. Elle explique comment rendre le streaming utilisable **en dehors du réseau local**, et comment limiter les risques que cela crée.

> ⚠️ **À lire en entier avant de publier quoi que ce soit.** Dans l'état actuel de RomM (5.1.0), rendre le flux accessible depuis Internet revient à exposer un bureau interactif sans authentification. La section 2 détaille exactement ce que cela implique.

---

## 1. Pourquoi ça ne fonctionne qu'en LAN par défaut

Le bloc `streaming` du `config.yml` contient deux adresses par conteneur, et elles ne sont **pas** utilisées par le même acteur :

```yaml
    - platform: switch
      host: https://192.168.10.100:3713      # ← chargé par le NAVIGATEUR du joueur
      broker_host: http://192.168.10.100:8011 # ← appelé par le BACKEND de RomM
```

- **`broker_host`** est un appel de serveur à serveur. RomM le contacte lui-même, en interne. Une adresse privée convient parfaitement.
- **`host`** est chargé **par le navigateur du joueur**, dans une iframe. RomM ne relaie rien : c'est le poste du joueur qui établit la connexion vidéo directement avec Selkies.

Conséquence : avec une IP privée dans `host`, un joueur hors du réseau obtient un délai d'attente. La page RomM se charge, la session est bien réclamée côté serveur, mais l'image n'arrive jamais.

**`host` doit donc être une adresse joignable depuis le poste du joueur.** Il n'existe que deux façons d'y arriver : amener le joueur dans le réseau (VPN), ou amener le flux jusqu'à lui (publication). Il n'y a pas de troisième voie.

---

## 2. ⚠️ Le trou de sécurité qu'il faut assumer

**Le point d'accès du flux n'a aucune authentification.** RomM n'attache aucun identifiant à l'URL de l'iframe, et `STREAMING_BROKER_SECRET` — qui existe pourtant — ne protège que le côté `broker_host`, jamais le côté `host`. Il est facile de croire qu'on est couvert alors qu'on ne l'est pas.

Ce que cela signifie concrètement une fois le sous-domaine publié :

- **N'importe qui connaissant l'URL obtient un bureau interactif** dans le conteneur, sans jamais toucher à RomM.
- **L'obscurité ne protège pas.** Chaque certificat Let's Encrypt est publié dans les journaux de *Certificate Transparency*, consultables publiquement. Un sous-domaine « non devinable » y apparaît en quelques minutes. Constaté ici : des robots scannaient le sous-domaine **moins de 24 h** après sa création.
- **La bibliothèque est montée** dans le conteneur. Même en lecture seule, elle reste intégralement copiable.
- **`/config` est en écriture** et contient les clés de console, le firmware et les sauvegardes.
- **Le GPU et la bande passante** sont utilisables par qui obtient le bureau.
- Le streaming **contourne aussi le modèle de permissions de RomM** : un compte en lecture seule peut réclamer une session et se retrouver sur ce bureau. Cela vaut même sur une instance purement locale.

### Ce que l'isolation réseau (section 4) apporte — et n'apporte pas

Le cloisonnement décrit plus bas **ferme le pire scénario** : le rebond depuis le conteneur vers le reste du réseau local et vers l'hôte. C'est la mesure la plus rentable, et elle ne coûte rien aux joueurs.

En revanche, elle **ne protège ni la bibliothèque, ni les clés, ni le GPU**. Elle réduit le rayon d'impact, elle ne referme pas la porte.

### Vos options, en toute honnêteté

| Option | Protection | Friction pour les joueurs |
|---|---|---|
| **LAN uniquement** | Totale | Personne ne joue à distance |
| **VPN** (WireGuard, Tailscale) | Totale | Chaque joueur installe un client |
| **Publication** (cette page) | Aucune sur le flux | Aucune |
| Publication + Access List NPM | Réelle | Identifiants à ressaisir à chaque session de navigateur |

L'Access List de NPM mérite d'être mentionnée mais déçoit à l'usage : Chrome bloque les invites d'authentification dans une iframe cross-origin, il faut donc visiter le sous-domaine une première fois pour saisir ses identifiants, et la mémorisation ne survit pas à la fermeture du navigateur. Le SSO (Authelia, Authentik…) ne fonctionne pas davantage dans ce contexte, pour les mêmes raisons de cookies `SameSite` et d'en-têtes `X-Frame-Options`.

### Une atténuation simple et gratuite

**Désactivez les Proxy Hosts entre les sessions.** Deux clics dans NPM, et le sous-domaine ne répond plus du tout le reste du temps. La fenêtre d'exposition passe de « permanente » à « quand quelqu'un joue », sans aucune contrainte pour les joueurs.

### C'est temporaire

Une PR est en cours côté RomM pour corriger exactement ce point : le broker émettra un jeton de session que RomM ajoutera à l'URL du flux, et un portail le validera avant d'autoriser la connexion. Suivre [rommapp/romm#4173](https://github.com/rommapp/romm/pull/4173) et [l'issue #3968](https://github.com/rommapp/romm/issues/3968). Cette page devra être révisée à ce moment-là.

---

## 3. Publier le flux derrière Nginx Proxy Manager

Un sous-domaine par émulateur. Exemple avec `eden.thymon.fr` :

**DNS** — un enregistrement CNAME `eden` vers votre domaine, exactement comme pour RomM. Vérifier la résolution avant de demander le certificat.

**Proxy Host, onglet Details :**

| Champ | Valeur |
|---|---|
| Domain Names | `eden.thymon.fr` |
| Scheme | **https** |
| Forward Hostname / IP | `eden` (nom du conteneur, voir §4) ou l'IP du serveur |
| Forward Port | **3001** |
| Block Common Exploits | activé |
| **Websockets Support** | **ACTIVÉ** |

> 🚨 **Visez impérativement le port HTTPS du conteneur (3001), jamais le port HTTP (3000).** Le portail de sécurité installé par le mod n'existe que dans le bloc serveur HTTPS. Proxyfier vers le port 3000 contourne toute protection — l'erreur a été commise ici, et l'interface s'est retrouvée servie sans aucun filtre. NPM ne vérifie pas le certificat en amont, l'auto-signé du conteneur ne pose donc aucun problème.

**Onglet SSL :** certificat Let's Encrypt, Force SSL, HTTP/2.

**Onglet Advanced** — le flux est une connexion WebSocket de longue durée, les délais par défaut la couperaient au bout d'une minute d'inactivité :

```
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
client_max_body_size 0;
```

**Puis dans `config.yml`**, remplacer uniquement `host:` :

```yaml
      host: https://eden.thymon.fr
```

`broker_host` ne change pas : c'est RomM qui l'appelle, en interne.

> Tant qu'un seul émulateur est migré, la page RomM restera signalée « HTTPS brisé » par Chrome : elle continue de charger du contenu actif depuis les autres origines en certificat auto-signé. L'avertissement ne disparaît qu'une fois **tous** les flux publiés. Si le message persiste ensuite, réinitialisez les autorisations du site dans Chrome — l'option « autoriser le contenu non sécurisé » reste mémorisée.

---

## 4. Réseau Docker dédié et isolation

C'est la partie qui réduit réellement les risques, et elle apporte aussi de la simplicité.

### 4.1 Créer le réseau

```bash
docker network create --subnet 172.30.0.0/24 romm-net
```

Placez-y **RomM et tous les conteneurs d'émulation**, avec une **IP fixe** pour chacun (le champ apparaît dans le template Unraid dès qu'un réseau personnalisé est sélectionné) :

```
romm → 172.30.0.5     eden → 172.30.0.10
dolphin → 172.30.0.11     rpcs3 → 172.30.0.12
```

### 4.2 Supprimer tous les ports publiés

Une fois les conteneurs sur le même réseau, ils se joignent **par leur nom**. Plus aucun port n'a besoin d'être publié sur l'hôte — ni les ports Selkies, ni surtout **les ports des brokers**, qui commandent le lancement des jeux et n'ont rien à faire sur le LAN.

Dans `config.yml`, utilisez les noms et les **ports internes** :

```yaml
      broker_host: http://eden:8000        # et non http://IP:8011
```

Et dans NPM, `Forward Hostname` = `eden`, port `3001`.

### 4.3 NPM doit rester sur les deux réseaux

NPM doit rejoindre `romm-net` pour résoudre les noms, **tout en gardant** son accès au LAN pour proxyfier vos autres services. On l'attache donc aux deux :

```bash
docker network connect romm-net NginxProxyManager
```

⚠️ Cette commande **ne survit pas à une recréation du conteneur** (mise à jour, modification de template). Pour la rendre permanente, ajoutez dans le champ **Post Arguments** du template NPM :

```
&& docker network connect romm-net NginxProxyManager
```

Unraid l'ajoute à la fin du `docker run`, donc le rattachement est rejoué automatiquement à chaque recréation — exactement le seul cas où il se perdait.

### 4.4 Couper les conteneurs du reste du réseau

Deux règles suffisent. Attention : la première ne couvre **pas** l'accès à l'hôte lui-même, car ce trafic passe par la chaîne `INPUT` et non `FORWARD` — d'où la seconde.

Créez un **User Script** planifié sur **At First Array Start** (les règles iptables ne survivent pas à un redémarrage) :

```bash
#!/bin/bash
SUBNET=172.30.0.0/24
LAN=192.168.10.0/24          # adapter

iptables -D DOCKER-USER -s "$SUBNET" -d "$LAN" -j DROP 2>/dev/null
iptables -I DOCKER-USER -s "$SUBNET" -d "$LAN" -j DROP

iptables -D INPUT -s "$SUBNET" -j DROP 2>/dev/null
iptables -I INPUT -s "$SUBNET" -j DROP
```

Les `-D` en amont évitent d'empiler des doublons si le script est relancé.

**Vérification :**

```bash
docker exec eden sh -c 'timeout 3 curl -sI http://IP_UNRAID >/dev/null && echo "PASSE (raté)" || echo "BLOQUE (bon)"'
docker exec eden sh -c 'timeout 3 curl -sI http://AUTRE_MACHINE_LAN >/dev/null && echo "PASSE (raté)" || echo "BLOQUE (bon)"'
```

Les deux doivent afficher **BLOQUE**, et le streaming doit continuer de fonctionner (RomM appelle le broker, jamais l'inverse).

> NPM garde son IP sur le réseau bridge pour joindre le LAN, il n'est donc pas concerné par ces règles. Si vous choisissez de le placer **uniquement** sur `romm-net`, il faudra ajouter des règles `ACCEPT` pour son IP **avant** les DROP.

---

## 5. Le portail nginx du mod Eden

Les versions récentes du mod `eden-romm-integration` installent un portail de sécurité : nginx interroge le broker (`auth_request` → `/verify`) et exige un `stream_token` avant de servir quoi que ce soit.

**C'est une bonne chose sur le principe — mais RomM 5.1.0 n'envoie pas encore ce jeton.** Le résultat est un **403 systématique**, qui se manifeste par un écran noir dans le lecteur. Le journal d'accès du conteneur est sans ambiguïté :

```
GET /?stream_token=<jeton>   → 200, puis GET /websockets → 101   (ouverture manuelle)
GET /                        → 403                              (iframe de RomM)
```

Le mod Dolphin, plus ancien, n'a pas ce portail — d'où une Switch qui ne fonctionne plus alors que la GameCube va bien.

### Neutraliser le portail automatiquement

Le portail est réinstallé **à chaque démarrage** du conteneur, donc un `sed` manuel est à refaire sans arrêt. Le point d'accroche `custom-cont-init.d` s'exécute malheureusement **avant** que le mod ne pose le portail : il faut donc y lancer une boucle de surveillance en arrière-plan.

```bash
mkdir -p /mnt/user/appdata/eden-init
cat > /mnt/user/appdata/eden-init/10-gate-off.sh << 'EOF'
#!/bin/bash
(
  while true; do
    if grep -qE '^[[:space:]]*auth_request /_stream_auth;' /etc/nginx/sites-available/default 2>/dev/null; then
      sed -i '/auth_request off/!s|^\( *auth_request .*\)|#\1|' /etc/nginx/sites-available/default
      nginx -s reload 2>/dev/null && echo "[gate-off] portail neutralise"
    fi
    sleep 30
  done
) &
EOF
chmod +x /mnt/user/appdata/eden-init/10-gate-off.sh
chown -R root:root /mnt/user/appdata/eden-init
```

Puis, dans le template Eden, ajouter un chemin :

| Champ | Valeur |
|---|---|
| Container Path | `/custom-cont-init.d` |
| Host Path | `/mnt/user/appdata/eden-init` |
| Access Mode | **Read Only** |

> Ce dossier est volontairement placé **hors de `/config`** : ces scripts s'exécutent en root, et `/config` est inscriptible par l'application. Sur un conteneur exposé, un répertoire d'exécution root modifiable de l'intérieur serait une porte ouverte. C'est précisément pour cette raison que LinuxServer a sorti ces dossiers de `/config`.

**Vérification** (compter 40 secondes après le démarrage) :

```bash
docker logs eden 2>&1 | grep gate-off
docker exec eden grep -n "auth_request /_stream_auth" /etc/nginx/sites-available/default
```

Vous devez voir « portail neutralise » et la ligne commentée.

> 🔻 **À supprimer le jour où la PR v2 arrive.** Une fois que RomM enverra le jeton, ce portail deviendra une vraie protection — et ce script la désactiverait silencieusement.

---

## 6. Pièges à connaître

**nginx met en cache la résolution des noms.** NPM résout `eden` au chargement de sa configuration et garde l'IP. Après une recréation de conteneur, il tape sur l'ancienne adresse → **502 Bad Gateway**, alors qu'un `curl` depuis NPM fonctionne (résolution fraîche). Remède : redémarrer NPM. Les IP fixes évitent le problème.

**Toute modification d'un template recrée le conteneur** : le portail revient, la couche inscriptible est effacée (paquets installés à la main et journaux nginx perdus), et les patchs du mod sont réappliqués. Un simple redémarrage, lui, conserve tout.

**Le journal d'accès nginx** est dans le conteneur, à `/var/log/nginx/access.log` — pas sous `/config`. C'est l'outil de diagnostic le plus fiable : il montre l'URI exacte reçue et son code de retour.

**Vérifier une exposition depuis l'extérieur** : utilisez le journal d'accès ou un téléphone en 4G, en navigation privée. Un navigateur qui possède déjà un cookie de session vous laissera passer alors que tout le monde est bloqué.

---

## 7. Récapitulatif de l'architecture cible

- Un sous-domaine par émulateur, en HTTPS Let's Encrypt, WebSockets activés, pointant vers le **port 3001** du conteneur
- Tous les conteneurs sur un réseau Docker dédié avec IP fixes, **aucun port publié**
- Brokers joignables uniquement depuis ce réseau, par nom de conteneur
- NPM attaché aux deux réseaux, rattachement rendu permanent par Post Arguments
- Conteneurs coupés du LAN et de l'hôte par deux règles iptables persistées
- Portail Eden neutralisé automatiquement, **en attendant la v2**
- Et l'acceptation assumée que **le flux reste accessible sans authentification** tant que la v2 n'est pas là

# RomM — Manettes dans EmulatorJS

> _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 :

```text
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é.

> 📎 **Il existe un second patch EmulatorJS sur cette instance**, décrit dans la page **« RomM — Forcer un core EmulatorJS par plateforme »**. Il impose un core pour la N64 et l'Arcade, et modifie un **fichier différent** :
>
> | Patch | Fichier modifié | Ce que c'est | Persistance |
> |---|---|---|---|
> | **Manettes** (cette page) | `assets/emulatorjs/data/src/emulator.js` | le **moteur** EmulatorJS | bind mount Unraid |
> | **Cores** (l'autre page) | `assets/EmulatorJS-*.js` | le **bundle frontend de RomM** | script relancé par *Post Arguments* |
>
> Les deux coexistent sans conflit, mais leurs noms se ressemblent assez pour qu'on les confonde en cherchant une panne. Le `debug: true` du §7 ne concerne **que** le patch manettes.

> 📌 **Contexte de ces correctifs** : EmulatorJS **4.2.3**, tel qu'embarqué dans **RomM 5.2.0**. Serveur Unraid, image `rommapp/romm`, conteneur `romm`. Ce sont des contournements liés à ces versions — 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 :

```text
Controller Debug → OK
```

Tous les boutons et tous les axes réagissent. Mais dès qu'un jeu démarre dans EmulatorJS :

```text
Game Pad not connected
```

Et dans la console du navigateur (`F12`) :

```text
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) :

```javascript
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 :

```text
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 :

```text
index navigateur   ≠   position dans son tableau interne
```

Le navigateur annonce `gamepadIndex = 1`. Mais le tableau interne d'EmulatorJS, lui, ne garde que les manettes réellement présentes :

```javascript
[
    {
        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 :

```javascript
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 0` et `position 0` coï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 :

```javascript
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 :

```text
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 :

```text
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.

```javascript
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 :

```text
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`, jamais `value`.** Dans EmulatorJS, `value2` porte le mapping **manette** et `value` le mapping **clavier**. Copier `value` donnerait 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 :

```bash
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 :

```bash
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

```bash
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.js` de l'`appdata` qui 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 :

```text
/mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
```

Vérification de la présence du patch :

```bash
grep -n "getGamepadSelectionValue" \
  /mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
```

Attendu dans notre installation — une définition et trois appels :

```text
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

```bash
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

```bash
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 :

```bash
grep -n -A18 -B3 "AUTO-MAP PLAYERS 2-4" \
  /mnt/user/appdata/romm-ejs-gamepad-patch/emulator.js
```

On doit lire :

```javascript
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'indice `0` est 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** :

```text
emulator.min.js
```

Or nos correctifs portent sur le fichier **source** :

```text
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) :

```yaml
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: true` fait 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** |

```text
/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 :

```text
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

```bash
ROMM="$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /rommapp\/romm/ {print $1; exit}')"

docker restart "$ROMM"
```

Puis, dans le navigateur, forcer le rechargement du cache — sans quoi l'ancien fichier reste servi :

```text
Windows / Linux : Ctrl + Shift + R
macOS           : Cmd + Shift + R
```

Points à vérifier :

- [ ] `Controller Debug` de RomM répond ;
- [ ] EmulatorJS annonce la manette **connectée** (plus de `Game Pad not connected`) ;
- [ ] la console (`F12`) ne montre plus `Cannot 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 :

```bash
# 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 :

1. récupérer le nouveau `emulator.js` de 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` ;
2. y chercher `getGamepadSelectionValue` ou toute recherche par `index` — si le bug est corrigé en amont, le correctif n°1 n'a plus lieu d'être ;
3. si seul le correctif multijoueur reste nécessaire, repartir du fichier neuf et lui appliquer le `sed` du §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 :

```bash
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 :

1. **supprimer le Path** dans le template Docker Unraid (celui du §8) ;
2. recréer le conteneur RomM — il retrouve alors le `emulator.js` exact de son image ;
3. si le mode debug n'a plus d'utilité, remettre `debug: false` dans `config.yml` ou 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 `diff` entre `emulator.js.orig` et `emulator.js` et 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-patch` est-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.
- **Reprendre la méthode du patch cores ?** Celui-ci **repatche** le fichier après chaque recréation du conteneur au lieu de le **remplacer** par bind mount — ce qui lui évite le défaut décrit au §10 (servir un vieux fichier à un runtime récent). La même approche serait transposable ici, au prix d'un script à écrire. Voir « RomM — Forcer un core EmulatorJS par plateforme », §7.

# RomM — Forcer un core EmulatorJS par plateforme

> _Dernière mise à jour : 2026-09-06_

EmulatorJS propose souvent **plusieurs cores** (moteurs d'émulation) pour une même plateforme, et ils ne se valent pas avec notre bibliothèque. RomM laisse normalement le choix à l'utilisateur et mémorise sa décision **dans son navigateur** — ce qui va mal avec une instance partagée entre plusieurs personnes et plusieurs appareils.

Cette page décrit le patch qui impose un core côté serveur, pour tout le monde :

```text
Nintendo 64 → parallel_n64
Arcade      → mame2003_plus
```

Toutes les autres plateformes gardent le comportement normal de RomM.

> ⚠️ **Il y a deux patchs EmulatorJS sur cette instance, et ils ne touchent pas le même fichier.** Les noms se ressemblent beaucoup, d'où une confusion facile :
>
> | Patch | Fichier modifié | Ce que c'est | Persistance |
> |---|---|---|---|
> | **Manettes** (page « RomM — Manettes dans EmulatorJS ») | `assets/emulatorjs/data/src/emulator.js` | le **moteur** EmulatorJS lui-même | bind mount Unraid |
> | **Cores** (cette page) | `assets/EmulatorJS-*.js` | le **bundle frontend de RomM** qui pilote le moteur | script relancé par *Post Arguments* |
>
> Ils sont indépendants et coexistent sans conflit. Le `emulatorjs: debug: true` du `config.yml`, exigé par le patch manettes, **n'a aucun effet** sur celui-ci.

> 📌 **Contexte** : RomM **5.2.0**, image `rommapp/romm:latest`, conteneur nommé `romm`, sur Unraid.

---

## 1. Le problème — un choix qui appartient au navigateur

RomM mémorise le core retenu dans le `localStorage` du navigateur, sous deux clés :

```text
player:<ROM_ID>:core          → le core choisi pour un jeu précis
player:<PLATFORM_SLUG>:core   → le core choisi pour toute une plateforme
```

Par exemple `player:n64:core` ou `player:arcade:core`.

Trois conséquences gênantes :

- la préférence ne suit ni l'utilisateur ni l'appareil — chaque navigateur repart de zéro ;
- un utilisateur peut sélectionner un core par mégarde, obtenir un jeu qui rame ou ne démarre pas, et **conclure que le jeu est cassé** alors que seul le core est en cause ;
- rien ne permet à l'administrateur de dire « sur cette plateforme, c'est ce core-là ».

## 2. Comment RomM choisit un core

Dans RomM 5.2.0, l'ordre de sélection est :

```text
1. Core mémorisé pour le jeu        (player:<ROM_ID>:core)
2. Core mémorisé pour la plateforme (player:<PLATFORM_SLUG>:core)
3. Premier core supporté par RomM
```

Le premier de la liste qui est à la fois renseigné **et** présent dans les cores supportés l'emporte.

## 3. Pourquoi un patch, et pas un réglage

RomM ne propose aujourd'hui **aucun paramètre serveur** pour imposer un core. Ce genre de configuration n'existe pas :

```yaml
n64:
  default_core: parallel_n64
```

La section `emulatorjs.settings` du `config.yml` permet de régler les **options** d'un core, mais pas de désigner celui qui doit être choisi. Il faut donc modifier la logique du frontend.

## 4. Ce que fait le patch

Un core « administrateur » est inséré **en tête** de la liste de candidats :

```text
1. Core forcé par l'administrateur   ← ajouté par le patch
2. Core mémorisé pour le jeu
3. Core mémorisé pour la plateforme
4. Premier core supporté
```

Concrètement, le patch injecte une table de correspondance dans le bundle :

```javascript
{n64:`parallel_n64`, arcade:`mame2003_plus`}[rom.value.platform_slug]
```

Deux garde-fous en découlent naturellement :

- **Plateforme absente de la table** → la valeur est `undefined`, elle est ignorée, et RomM retrouve son comportement normal. C'est le cas de toutes les plateformes autres que N64 et Arcade.
- **Core forcé absent des cores supportés** → il est également écarté, car la valeur injectée traverse le même filtre que les autres (`cores.includes(...)`). Si une future version de RomM retirait `parallel_n64`, le lancement ne se bloquerait pas : RomM reprendrait simplement sa sélection habituelle.

> 💡 Les préférences déjà enregistrées dans les navigateurs **ne sont pas effacées**. Elles restent en place et redeviendraient actives si le patch était retiré ; elles sont simplement ignorées pour les deux plateformes forcées, puisque le core administrateur passe avant.

---

## 5. Le script

### 5.1 Le dossier

```bash
mkdir -p /mnt/user/appdata/romm-core-patch
```

### 5.2 Créer le script

```bash
cat > /mnt/user/appdata/romm-core-patch/apply.sh <<'EOF'
#!/bin/bash
CONTAINER="romm"

# Attend que le conteneur RomM soit démarré
for i in $(seq 1 60); do
    if docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null | grep -q true; then
        break
    fi
    sleep 2
done

docker exec -i "$CONTAINER" python3 - <<'PY'
from pathlib import Path
import re
import sys

assets = Path("/var/www/html/assets")
files = list(assets.glob("EmulatorJS-*.js"))
marker = "/*ROMM_FORCE_CORE*/"

pattern = re.compile(
    r'(?P<sel>[A-Za-z_$][\w$]*)\.value=\['
    r'localStorage\.getItem\(`player:\$\{(?P<rom>[A-Za-z_$][\w$]*)\.value\.id\}:core`\),'
    r'localStorage\.getItem\(`player:\$\{(?P=rom)\.value\.platform_slug\}:core`\)'
    r'\]\.find\((?P<x>[A-Za-z_$][\w$]*)=>!!(?P=x)&&'
    r'(?P<cores>[A-Za-z_$][\w$]*)\.value\.includes\((?P=x)\)\)'
    r'\?\?(?P=cores)\.value\[0\]'
)

matches = []
for p in files:
    text = p.read_text(errors="replace")
    if marker in text:
        print(f"[OK] Patch déjà présent : {p.name}")
        sys.exit(0)
    m = pattern.search(text)
    if m:
        matches.append((p, text, m))

if len(matches) != 1:
    print(f"[ERREUR] {len(matches)} bundle(s) compatible(s) trouvé(s).")
    print("Aucune modification effectuée.")
    sys.exit(1)

p, text, m = matches[0]
sel = m.group("sel")
rom = m.group("rom")
old = m.group(0)
prefix = f"{sel}.value=["

forced = (
    f'{prefix}{marker}'
    f'{{n64:`parallel_n64`,arcade:`mame2003_plus`}}'
    f'[{rom}.value.platform_slug],'
)
new = old.replace(prefix, forced, 1)

backup = Path(str(p) + ".before-core-patch")
if not backup.exists():
    backup.write_text(text)

text = text[:m.start()] + new + text[m.end():]
p.write_text(text)

print(f"[OK] Patch appliqué : {p.name}")
print("[OK] N64    -> parallel_n64")
print("[OK] Arcade -> mame2003_plus")
PY
EOF
```

### 5.3 Le rendre exécutable

```bash
chmod +x /mnt/user/appdata/romm-core-patch/apply.sh
```

> ⚠️ Sans ce `chmod`, le lancement par *Post Arguments* (§7) échoue en silence à chaque recréation du conteneur.

### 5.4 Ce que fait le script, étape par étape

1. attend que le conteneur `romm` soit actif (60 tentatives, 2 secondes d'intervalle — soit 2 minutes au plus) ;
2. cherche le bundle `/var/www/html/assets/EmulatorJS-*.js`, dont le nom est **hashé et change à chaque version** ;
3. s'arrête immédiatement si le marqueur `/*ROMM_FORCE_CORE*/` est déjà présent ;
4. localise la logique de sélection du core par expression régulière ;
5. **exige exactement une correspondance** — ni zéro, ni plusieurs — sinon il ne touche à rien ;
6. sauvegarde le bundle d'origine en `.before-core-patch` ;
7. insère la table de correspondance et écrit le fichier.

Le script est donc **idempotent** : le relancer dix fois donne le même résultat qu'une fois.

---

## 6. Appliquer et vérifier

### 6.1 Lancement manuel

```bash
/mnt/user/appdata/romm-core-patch/apply.sh
```

Résultat attendu à la première exécution :

```text
[OK] Patch appliqué : EmulatorJS-XXXXXXXX.js
[OK] N64    -> parallel_n64
[OK] Arcade -> mame2003_plus
```

Et aux suivantes :

```text
[OK] Patch déjà présent : EmulatorJS-XXXXXXXX.js
```

### 6.2 Vérifier que le patch est en place

```bash
docker exec romm sh -c '
grep -RlF "/*ROMM_FORCE_CORE*/" /var/www/html/assets/EmulatorJS-*.js
'
```

Le chemin du bundle doit s'afficher :

```text
/var/www/html/assets/EmulatorJS-XXXXXXXX.js
```

**Une réponse vide signifie que le patch n'est pas appliqué.**

### 6.3 ⚠️ Vider le cache du navigateur

Le nom du bundle est hashé, ce qui pousse les navigateurs à le mettre en cache **très longtemps**. Or le patch change son **contenu sans changer son nom** : un navigateur qui l'a déjà chargé continuera de servir la version non patchée, parfois plusieurs jours.

Après chaque application du patch, forcer le rechargement :

```text
Windows / Linux : Ctrl + Shift + R
macOS           : Cmd + Shift + R
```

C'est la première chose à vérifier quand le patch est présent côté serveur mais que le core ne change pas côté joueur — et il faut le faire **sur chaque appareil** utilisé.

---

## 7. Persistance — pourquoi un script et pas un bind mount

Le patch manettes est rendu persistant par un montage de fichier. **Cette méthode ne convient pas ici**, pour une raison simple : le nom du bundle est hashé et change à chaque mise à jour.

```text
EmulatorJS-Dr7gVfBj.js   →   EmulatorJS-AbCd1234.js
```

Monter un fichier sur un nom hashé reviendrait à figer une version qui n'existera plus, et à masquer le vrai bundle de la nouvelle image. C'est pourquoi on **repatche** au lieu de **remplacer**.

> 💡 Cette approche a un avantage que le bind mount n'a pas : après une mise à jour de RomM, c'est bien le **nouveau** fichier qui est patché. Il n'y a aucun risque de servir un vieux fichier à un runtime récent.

### La solution : Post Arguments

Dans le template Docker RomM d'Unraid, passer en **Advanced View**, puis ajouter dans le champ **Post Arguments** :

```bash
&& /mnt/user/appdata/romm-core-patch/apply.sh
```

Unraid construit alors une commande de la forme :

```bash
docker run ... rommapp/romm:latest && /mnt/user/appdata/romm-core-patch/apply.sh
```

Le `docker run` rend la main dès que le conteneur démarre — d'où la boucle d'attente du script, qui laisse à RomM le temps d'être réellement actif avant de le patcher.

## 8. Ce qui se passe dans chaque situation

| Action | Conteneur recréé ? | Bundle réinitialisé ? | Patch après l'opération |
|---|---|---|---|
| `docker restart romm` | non | non | **toujours là** — rien à faire |
| Mise à jour de l'image RomM | oui | oui | réappliqué automatiquement |
| `Apply` du template Unraid | oui | oui | réappliqué automatiquement |
| Modification du mapping (§10) | non | non | à relancer à la main |

Aucune intervention manuelle n'est donc nécessaire après une mise à jour de RomM — **à condition que le patch s'applique toujours**, ce qui est le sujet de la section suivante.

---

## 9. ⚠️ Après une mise à jour de RomM

L'expression régulière du script vise du JavaScript **minifié et très précis**. Il suffit que RomM réécrive cette portion de code, ou que le minificateur produise autre chose, pour qu'elle ne corresponde plus.

Dans ce cas le script affiche :

```text
[ERREUR] 0 bundle(s) compatible(s) trouvé(s).
Aucune modification effectuée.
```

**Le comportement est volontairement prudent : il ne casse rien.** RomM continue de fonctionner normalement, avec sa sélection de core habituelle.

> ⚠️ **Mais l'échec est silencieux pour l'utilisateur.** Lancé par *Post Arguments*, le script écrit son message d'erreur là où personne ne le lit. Personne n'est prévenu : les cores cessent simplement d'être forcés, et le symptôme réapparaît sous la forme « le jeu N64 ne marche plus » plusieurs jours plus tard.
>
> **Après toute mise à jour de RomM, lancer la vérification du §6.2.** Une réponse vide veut dire que le patch n'est plus appliqué.

Pour comprendre pourquoi, relancer le script à la main (§6.1) et lire son message. Si l'expression régulière ne correspond plus, il faut la réécrire d'après le nouveau code du bundle — le fichier `.before-core-patch` de l'ancienne version, s'il est encore là, aide à repérer ce qui a changé.

> Le fichier `.before-core-patch` est créé **dans le conteneur**. Il disparaît à chaque recréation : ce n'est pas une sauvegarde durable, seulement un filet pour la session en cours. Ce n'est pas gênant — l'original revient toujours de l'image Docker.

---

## 10. Ajouter une plateforme

Le mapping se trouve dans cette ligne du script :

```python
    f'{{n64:`parallel_n64`,arcade:`mame2003_plus`}}'
```

> ⚠️ **Les accolades doubles sont obligatoires.** Cette ligne est une *f-string* Python : `{{` et `}}` y produisent une simple accolade dans le JavaScript final. En écrivant `{n64:...}` avec des accolades simples, Python interpréterait le contenu comme une variable et le script échouerait.

Pour ajouter une plateforme, allonger la liste **à l'intérieur** des accolades doubles :

```python
    f'{{n64:`parallel_n64`,arcade:`mame2003_plus`,gba:`mgba`}}'
```

Deux valeurs doivent être exactes, sans quoi la plateforme sera simplement ignorée :

- le **slug RomM** de la plateforme (celui qui apparaît dans l'URL de la plateforme, et dans la clé `player:<slug>:core`) ;
- le **nom du core EmulatorJS**, tel qu'EmulatorJS le désigne.

Après modification, relancer le script — il ne fera rien tant que l'ancien patch est en place, il faut donc **d'abord recréer le conteneur** (ou restaurer le bundle) pour repartir d'un fichier propre.

---

## 11. Revenir au comportement standard

Retirer des **Post Arguments** du template :

```bash
&& /mnt/user/appdata/romm-core-patch/apply.sh
```

puis faire **Apply**. Unraid recrée RomM depuis l'image officielle, et le bundle repart non patché.

Un simple `docker restart` ne suffit pas : le conteneur doit être **recréé** pour que le bundle d'origine revienne.

Le dossier `/mnt/user/appdata/romm-core-patch/` peut être conservé — il ne gêne plus, et garde le script sous la main.

> Les préférences de core enregistrées dans les navigateurs redeviennent alors actives. Un utilisateur qui avait choisi un core inadapté avant le patch le retrouvera.

---

## 12. Dépannage rapide

| Symptôme | Cause probable | Solution |
|---|---|---|
| Le core n'est pas forcé, patch pourtant présent côté serveur | Cache du navigateur : ancien bundle servi | Rechargement forcé, sur chaque appareil — §6.3 |
| `grep` du §6.2 ne renvoie rien après une mise à jour | L'expression régulière ne correspond plus au nouveau code | Relancer le script à la main et lire l'erreur — §9 |
| `[ERREUR] 0 bundle(s) compatible(s) trouvé(s).` | RomM a réécrit la logique de sélection du core | Réécrire l'expression régulière d'après le nouveau bundle — §9 |
| `[ERREUR] 2 bundle(s) compatible(s) trouvé(s).` | Plusieurs bundles présents dans `assets/` | Vérifier le contenu du dossier ; le script refuse volontairement de deviner |
| Rien ne se passe après une recréation du conteneur | Script non exécutable, ou *Post Arguments* absent | `chmod +x` (§5.3) et vérifier le template (§7) |
| Le patch disparaît après un `Apply` du template | *Post Arguments* effacé lors d'une édition du template | Le remettre — §7 |
| Une nouvelle plateforme n'est pas forcée | Slug ou nom de core erroné, ou accolades simples | §10 |
| Le core forcé n'est pas pris alors que le patch est là | Ce core n'est plus dans les cores supportés par RomM | Comportement voulu — vérifier le nom du core, §4 |
| Le conteneur a été renommé, le script ne fait plus rien | `CONTAINER="romm"` est écrit en dur dans le script | Corriger la variable en tête du script — §13 |

---

## 13. À compléter

- **Le nom du conteneur est en dur** (`CONTAINER="romm"`), alors que les autres pages RomM le détectent par son image :
  `ROMM="$(docker ps --format '{{.Names}} {{.Image}}' | awk '$2 ~ /rommapp\/romm/ {print $1; exit}')"`.
  Aligner le script sur cette méthode le rendrait insensible à un renommage.
- **Aucune trace n'est conservée des exécutions.** Rediriger la sortie vers un fichier (`>> /mnt/user/appdata/romm-core-patch/apply.log 2>&1` dans les *Post Arguments*) permettrait de savoir, après coup, si le patch a réussi lors de la dernière mise à jour — c'est le point faible identifié au §9.
- **Pourquoi ces cores plutôt que d'autres ?** Noter ici ce qui a été constaté avec les autres cores N64 et Arcade : cela évitera de refaire les essais, et aidera à choisir un remplaçant si l'un d'eux disparaît.
- **Demande en amont ?** Un réglage serveur de core par défaut serait la vraie solution. Vérifier si la fonctionnalité est déjà demandée au projet RomM, et suivre le ticket le cas échéant.