
Dans l’article sur Metal Gear Solid, j’ai présenté la console portable que j’ai construite autour d’un XIAO ESP32S3 Sense. Castlevania: Symphony of the Night (SOTN) est l’autre jeu PlayStation qu’elle fait tourner, et celui qui est arrivé en premier : il tournait à 60 fps sur une carte de développement Waveshare ESP32-S3 avant même que la console portable n’existe, puis a été transféré dessus par la suite.
Cela a été possible pour la même raison que Metal Gear Solid. La décompilation communautaire de SOTN se compile en C portable, donc le jeu peut être compilé pour l’ESP32-S3 au lieu d’être émulé.
Gameplay sur la console portable : regardez-le sur YouTube.
Cet article couvre la structure du portage, comment le jeu tient dans la mémoire du SoC, comment le rastériseur logiciel a atteint 60 fps, les bugs que le matériel d’origine avait masqués, et une console portable que j’ai construite autour d’un Seeed XIAO ESP32S3 Sense.
Pourquoi un portage natif est possible
Émuler une PlayStation sur un ESP32-S3 est hors de portée. Un émulateur doit interpréter un CPU MIPS et un GPU instruction par instruction, ce qui nécessite plusieurs fois la performance dont dispose le SoC.
Le portage est différent. Le projet de décompilation de SOTN a reconstruit le jeu sous forme de code source C qui se compile en exécutable PC. La logique du jeu est du C ordinaire, et la seule chose qui le lie à la console est le SDK de Sony : les appels de bibliothèque qui dessinent les polygones, téléchargent les textures, lisent la manette et jouent les sons. La version PC remplace ce SDK par psyz, une réimplémentation au-dessus de SDL.
Le travail se décompose donc en trois parties :
- Compiler le jeu pour Xtensa.
- Remplacer le backend PC de psyz par un backend pour le SoC.
- Faire tenir le tout.
Le SoC cible est l’ESP32-S3 :
- Deux cœurs Xtensa LX7 à 240 MHz.
- 512 Ko de SRAM interne.
- 8 Mo de PSRAM octal (externe, beaucoup plus lente).
- 16 Mo de flash sur la première carte et 8 Mo sur la console portable.
- Pas de GPU.
Architecture
Les couches, du jeu jusqu’au matériel :
- Jeu : moteur décompilé, code du joueur et des armes, stages
- psyz : réimplémentation du SDK avec PSYZ_RENDERER=soft
- Couche plateforme (esp32/main) : VSync, compteurs racines, entrées, fichiers FAT, audio, scanout LCD
- Matériel ESP32-S3 : cœurs LX7, SRAM, PSRAM, flash, LCD SPI
- Jeu : le moteur décompilé (
src/dra), le code du joueur et des armes, et chaque zone du château (« stage »). Ces éléments sont inchangés, à l’exception des corrections de bugs. - psyz : la réimplémentation du SDK. J’y ai ajouté un troisième rastériseur,
PSYZ_RENDERER=soft. C’est un analyseur de paquets plus un cœur de rastériseur sans dépendance à SDL, donc le même fichier se compile sous Windows (pour tester contre la référence GPU) et sur le SoC. - Couche plateforme (
esp32/main) : cadencement VSync à 59,94 Hz, les compteurs racines qui pilotent le séquenceur sonore, les entrées de la manette, l’accès aux fichiers via FAT, le mixage audio sur le second cœur, et le scanout LCD.
Deux décisions de conception ont façonné tout le reste.
La VRAM reste le propre tampon du jeu. La PlayStation dispose de 1 Mo de mémoire vidéo, soit 1024×512 pixels en couleur 16 bits. La décompilation la modélise déjà comme un simple tableau. Le rastériseur dessine directement dedans (en PSRAM), et le LCD est alimenté depuis celle-ci. Il n’y a pas de copies fantômes ni de conversions de format, hormis la conversion finale vers le RGB565 de l’écran.
Les stages sont liés statiquement. Sur PC, chaque zone du château est une DLL chargée à la demande. L’ESP32-S3 n’a pas d’éditeur de liens dynamique, donc chaque zone est compilée dans le firmware, et une petite table associe un nom de stage à sa fonction d’initialisation. Cette décision a causé des problèmes à deux reprises, comme décrit dans « Les bugs que la PlayStation pardonne » et « Ajouter un stage » ci-dessous.
Faire tenir un jeu PlayStation dans 512 Ko

La première édition de liens a échoué avec un dépassement de 3,34 Mo de RAM interne. La majeure partie de ce surplus n’était pas imputable au jeu :
g_TileDefDataPoolétait déclaré commeTileDefinition[0x40][4][0x1000]. Avec des pointeurs 32 bits, cela représente 16 à 32 Mo pour contenir environ 1 Mo de données. Le retyper en octets a réglé le poste le plus important.- La bibliothèque sonore allouait un tableau de 512 Ko sur la pile. Cela fonctionne sur PC ; sur un microcontrôleur, cela plante immédiatement.
- Les grands tampons rarement sollicités ont été déplacés vers la PSRAM avec
EXT_RAM_BSS_ATTR. - Les tables en lecture seule ont été marquées
const, ce qui les place en flash.
Ce dernier point comporte un piège : certaines données « en lecture seule » sont écrites. Le SDK met à jour les en-têtes de banques sonores sur place lorsqu’une banque est ouverte. Déclarées const, elles résident en flash, et écrire en flash via le cache provoque une faute matérielle (« Dbus write to cache »). Le schéma qui fonctionne consiste à conserver un maître const en flash, à le copier dans un tampon PSRAM au démarrage, et à laisser le jeu écrire dans la copie.
Après 28 cycles d’édition de liens, les chiffres étaient les suivants :
- 207 Ko de données initialisées en RAM interne.
- 43 Ko de données initialisées à zéro en RAM interne.
- 55 Ko de code en IRAM.
- 3 Mo de statiques en PSRAM.
- Environ 100 Ko de tas interne libre à l’exécution.
Le rastériseur logiciel, et comment atteindre 60 fps

SOTN est un jeu 2D, et son mélange de primitives est restreint : quads texturés et Gouraud, sprites 16×16 pour les couches de tuiles, rectangles et lignes. Il n’y a ni 3D ni moteur de transformation géométrique, ce qui a rendu un rastériseur logiciel réaliste.
Le rastériseur est exact au bit près par rapport à la version de référence GPU. J’ai comparé les sommes de contrôle des images entre la version PC et la carte après chaque optimisation.
La majeure partie du temps de frame était consacrée aux accès mémoire. Chaque pixel texturé lit un texel et une entrée de palette depuis la PSRAM et écrit un pixel dans la PSRAM. Les optimisations qui ont compté réduisent toutes ce trafic :
| Étape | Résultat |
|---|---|
| Pas de bord de triangle : divisions, puis accumulateurs 64 bits, puis 32 bits 16.16 | Le 64 bits était plus lent sur un cœur 32 bits ; le 16.16 a été retenu |
| Scanout déplacé sur le cœur 1, alimenté par une notification | Le jeu n’attend jamais le SPI |
| Cache de palette en RAM interne, boucles de rastérisation en IRAM | Warp Room à 31 fps |
| Chemin rapide pour les couches de tuiles : 4 texels par lecture, paires de pixels en écritures 32 bits | De 10,4 ms à 6,1 ms par frame |
| Scanout depuis le tampon que le jeu vient de finir de dessiner | Aucune copie, une frame de latence en moins |
| PSRAM et flash de 80 à 120 MHz | De 19,4 ms à 16,4 ms par frame : 60 fps |
J’ai aussi essayé un cache de lignes de texture dans le rastériseur de triangles, puis je l’ai retiré. Il n’apportait rien, car GCC avait déjà remonté les chargements.
Le changement consistant à effectuer le scanout depuis le tampon terminé mérite quelques explications. La source évidente pour l’affichage est la zone d’affichage courante du SDK, mais celle-ci a une frame de retard : au VSync, elle désigne le tampon dans lequel le jeu est sur le point de dessiner. Effectuer le scanout depuis celle-ci signifiait que le DMA et le rastériseur se disputaient la même moitié de la VRAM pendant toute la frame. Lire l’origine d’affichage depuis la propre structure de tampon du jeu fait travailler les deux sur des moitiés opposées par construction.
Les bugs que la PlayStation pardonne
La PlayStation n’a pas de protection mémoire. Une lecture errante renvoie ce qui s’y trouve, et une écriture errante atterrit dans une mémoire que souvent personne ne vérifie. La version PC masque aussi la plupart de ces bugs, car ses statiques sont vastes et indulgentes. L’ESP32-S3 dispose d’une MMU, d’une mémoire serrée et de voisins qui comptent, si bien que le portage s’est révélé être un très bon moyen de trouver des bugs latents.

Comment je les ai trouvés
Une trace de panique nomme la victime, pas le coupable. L’outil qui a fonctionné était OpenOCD via l’USB-JTAG intégré au SoC, conjointement avec GDB :
- Réinitialiser et mettre en pause le SoC via OpenOCD.
- Placer un point d’arrêt sur le gestionnaire de panique.
- Quand il est atteint, décoder le vrai PC depuis la trame de panique.
- Toujours décoder par rapport à l’ELF exact qui a été flashé, car les adresses changent à chaque recompilation.
Animation de palette hors limites
Le Labo d’Alchimie demande l’animation de palette (tileset & 0xFF) + 0x7FFF | 0x4000, ce qui indexe l’entrée 2 de la table de palettes du stage. La table n’a qu’une entrée. La lecture hors limites a atterri sur la table de banques de sprites adjacente, qui a ensuite été enregistrée comme descripteur d’animation de palette et écrite à chaque image.
Le correctif rejette les descripteurs dont l’étendue dépasse le tampon de palette. Le même comportement indéfini existe en amont ; le PC l’absorbe dans un grand BSS.
Deux stages, un seul HitDetection
Avec les stages liés statiquement, 122 symboles globaux étaient définis dans plus d’un stage. Les en-têtes partagés implémentent des choses comme la collision et les mises à jour d’entités, et sur la PlayStation un seul stage était jamais chargé. Les archives statiques laissent l’éditeur de liens résoudre silencieusement chaque nom vers une seule définition. Le Labo d’Alchimie exécutait le HitDetection de la Salle de Téléportation contre ses propres tables d’entités, et la corruption apparaissait une couche plus tard.
Le correctif est une liste générée de chaque symbole partagé par deux stages, renommé par stage avec des définitions -D.
Des salles qui mutent leur propre carte
Les portes et les murs destructibles écrivent dans la carte de tuiles de la salle. Avec les cartes générées en flash, la première porte a fait planter le jeu. Le correctif est un point unique où la couche de premier plan de la salle active est copiée dans un tampon PSRAM inscriptible.
Le nom de l’ennemi

Avec la relique Parchemin de Fée, le jeu affiche le nom de l’ennemi que vous frappez. BottomCornerText parcourt la chaîne à la recherche du terminateur FF 00 de la PlayStation. Les chaînes PC sont de simples chaînes C, donc le parcours dépassait la fin et écrasait un tampon de pile de 64 octets. Le symptôme était « ça se fige dès que je touche un ennemi ». Le correctif borne le parcours.
Une course qui n’existait pas sur la PlayStation
L’audio tourne sur le second cœur, et le tirage audio faisait avancer les compteurs racines. Ces compteurs déclenchent le gestionnaire VSync du jeu, les téléversements de textures et les callbacks de la file GPU. Sur la PlayStation, ce sont des interruptions sur le même CPU. Ici, ils s’exécutaient concurremment avec le jeu sur l’autre cœur.
Le correctif accumule les ticks de compteur sur le cœur 1 et déclenche les gestionnaires sur le thread du jeu.
Une entrée qui n’arrivait jamais
Le rapport était « je ne peux pas sauter ». Trois bugs étaient empilés :
- Le port série USB rejette les octets entrants tant que DTR est bas, et mon script clavier le maintenait bas pour éviter de réinitialiser la carte.
- La console était configurée pour l’UART alors que le câble était sur l’USB natif.
- Une broche de bouton non câblée se lisait comme enfoncée en permanence, ce qui maintenait CROSS enfoncé indéfiniment.
La console portable : mise en route matérielle sur un XIAO ESP32S3 Sense



L’objectif était une console portable. C’est le même matériel qui a ensuite fait tourner Metal Gear Solid : SOTN a été le premier jeu dessus. Les pièces :
- Seeed XIAO ESP32S3 Sense : le même SoC ESP32-S3 avec 8 Mo de PSRAM, 8 Mo de flash, et un slot microSD sur sa carte d’extension.
- Écran SPI ILI9341 320×240 : la résolution native de la PlayStation, donc aucune mise à l’échelle n’est nécessaire.
- Un stick analogique pris sur une manette de drone.
- Huit boutons.
Le XIAO dispose de onze broches utilisables. L’écran en nécessite cinq (SCK, MOSI, MISO, CS, DC) plus une ligne de reset, et le stick nécessite deux broches ADC. Il en reste trois pour huit boutons.
Six des boutons partagent une broche ADC via une échelle de résistances : une résistance de tirage de 10k et, par bouton, une résistance vers la masse (0 Ω, 2k2, 4k7, 10k, 22k, 47k). Chaque bouton produit une tension différente. La limitation est que deux boutons maintenus ensemble se lisent comme le plus bas. La paire que chaque jeu maintient ensemble (attaque et saut dans SOTN) reçoit les deux broches dédiées restantes.
Le schéma de câblage complet, la liste des pièces et la carte des boutons se trouvent dans le guide matériel du dépôt.
La mise en route a été sa propre liste de leçons. Avant de toucher au jeu, j’ai écrit un firmware de test séparé qui dessine des barres de couleur et affiche chaque bouton et le stick à l’écran :
- L’écran était blanc, et devenait plus sombre quand j’appuyais sur les boutons. Il n’avait pas de fil de masse, donc il s’alimentait à travers les diodes de protection de ses broches de données. Ajouter GND a corrigé ça instantanément.
- L’écran était un ILI9341 plutôt que l’ILI9488 que j’avais prévu. C’est un contrôleur différent avec un format de pixel différent (RGB565 sur SPI au lieu de RGB666). Son pilote n’est pas intégré à ESP-IDF et provient du registre de composants.
- Une vraie ligne de reset compte. Avec RESET tiré haut et seulement un reset logiciel, l’écran ne démarrait pas de manière fiable.
- Boutons à quatre pattes : deux pattes du même côté sont déjà connectées à l’intérieur. Si vous câblez celles-là, le bouton est toujours enfoncé. Utilisez les pattes diagonales.
- Le stick dérivait tout seul. Une nacelle de drone repose là où ses ressorts la placent (2317 sur 4095 sur la mienne, là où on pourrait supposer 2048), et l’ADC est bruité. Le correctif :
- Calibrer le centre au démarrage.
- Apprendre la course de chaque axe au fur et à mesure de son utilisation.
- Moyenner 8 échantillons.
- Appliquer une zone morte de 60 counts, soit quatre fois le bruit mesuré au repos.
- L’axe Y était inversé deux fois. Le firmware de test inversait Y pour que « haut déplace le point vers le haut » sur un écran dont le Y croît vers le bas. Le jeu prend des bits de direction, où haut est simplement haut. Transporter la convention de l’écran l’a inversé à nouveau.
La console portable — écran, entrées et un crash attrapé par un watchpoint
Tout sur la carte SD
8 Mo de flash ne peuvent pas contenir une application de 3 Mo à côté de 3,7 Mo de données de jeu, donc sur la console portable toutes les données viennent de la carte microSD. Cette carte partage le bus SPI avec l’écran, ce qui a plus tard provoqué son propre crash (voir « Le bus SPI partagé » ci-dessous).
Deux pièges propres à cette carte
GPIO43 est à la fois la broche data/command de l’écran et le TX de l’UART0. Le code Waveshare installait le pilote UART pour sa console série. Ce faisant, la broche repasse silencieusement à l’UART, et l’écran cesse de distinguer les commandes des pixels. La console portable n’utilise l’USB natif que pour sa console.
printf sur l’USB natif bloque quand personne ne lit. Avec un PC branché, on ne le remarque jamais. Débranché, le tampon d’émission se remplit en quelques secondes et le jeu s’arrête à l’intérieur d’un printf, sur un appareil censé fonctionner débranché. Toute la sortie, y compris la journalisation d’ESP-IDF elle-même, passe désormais par un wrapper qui écrit avec un délai d’attente nul et abandonne ce que personne ne lit.
Scintillement, puis rectangles glissants
La première compilation scintillait. J’ai supposé que l’horloge SPI était trop lente et je l’ai augmentée de 40 à 80 MHz. Cela a un peu aidé, mais en instrumentant le scanout, j’ai vu ce qui se passait réellement :
- Une image mettait environ 24 ms à être envoyée.
- Le jeu en produisait une toutes les 16,7 ms.
- Donc le jeu prenait de vitesse l’écran — il échangeait les tampons et commençait à redessiner celui que le DMA était encore en train de transmettre.
C’était un problème de cohérence des tampons. Le correctif était le chemin de copie que le portage Waveshare avait conservé comme filet de sécurité — copier l’image terminée, puis envoyer la copie. Ma première version de ce mécanisme avait deux bugs :
- Elle ne copiait que les lignes du rectangle de découpe courant du rastériseur, qu’un effet peut réduire à quelques lignes. L’écran se figeait pendant que le jeu continuait de tourner.
- Avec deux tampons de copie, elle pouvait écraser celui qui était encore en cours d’envoi. Sur une scène à défilement horizontal, cela se manifeste par des rectangles qui glissent latéralement.
Même après ces deux corrections, les rectangles persistaient. Ramener l’horloge SPI à 40 MHz les a fait disparaître — les fils de liaison ne tenaient pas les 80 MHz, et un décalage d’un octet décale toute une bande de 8 lignes. L’horloge SPI du S3 divise une source de 80 MHz, donc les seules options sont 80 et 40 MHz, sans rien entre les deux.
Les fils étant le goulot d’étranglement, l’option restante était d’envoyer moins de données. Chaque bande de 8 lignes est empreintée au moment de sa conversion en RGB565, et les bandes identiques à ce que l’écran affiche déjà sont ignorées. Les images abandonnées sont passées d’environ 40 % à 6 %. Cela donne à peu près 55 fps à l’écran quand la caméra est immobile, tandis que la logique du jeu reste à pleine vitesse.
Le crash attrapé par un watchpoint matériel
Le rapport suivant était « j’ai appuyé sur START et ça a redémarré ». Il y en avait eu un autre avant — « du texte est apparu, puis l’écran est devenu blanc, puis ça a redémarré ».
La panique se produisait dans ClearOTag, appelée depuis la boucle principale avec un pointeur d’ordering table de 0x4b8. La boucle principale avance avec g_CurrentBuffer = g_CurrentBuffer->next, donc en remontant, quelque chose avait écrit 0x44 dans le lien next de l’un des deux tampons GPU.
L’ESP32-S3 dispose de deux watchpoints matériels. Je les ai armés tous les deux, un sur le champ next de chaque tampon, deux secondes après le démarrage, bien après la seule écriture légitime. Appuyer sur START a arrêté le CPU sur le store du coupable :
Debug exception reason: Watchpoint 1 triggered
AddPrim ← MenuDrawImg ← MenuDrawChar ← MenuDrawStr ← MenuDrawStats ← MenuDraw
Le menu pause prend le prochain sprite libre avec &g_CurrentBuffer->sprite[g_GpuUsage.sp] et ne vérifie jamais la borne. sprite[] est le dernier champ d’un tampon GPU, et le second tampon commence juste après, en débutant par son lien next. Dessiner le texte des statistiques a dépassé les 512 sprites, et AddPrim a écrit un en-tête de primitive par-dessus le lien. Le même jeu vérifie g_GpuUsage.sp < MAX_SPRT_COUNT ailleurs, mais le menu ne le faisait pas. Deux lignes ont suffi à corriger cela.
Le bus SPI partagé
Juste après, un crash différent est apparu lors des chargements de niveau — une assertion dans le pilote SPI, running_cmd == 0. La carte démarrait une commande alors qu’un transfert DMA de l’écran était encore sur le fil. Sur la carte Waveshare, la carte ne diffusait que la musique optionnelle ; sur la console portable, elle transporte tout.
Le correctif est un mutex :
- L’écran le détient pendant une image et vide son DMA en cours avant de le relâcher.
- La carte le prend autour de chaque commande, en enveloppant le hook
do_transactiondu pilote.
Instrumenter sans se tromper soi-même
Deux leçons de cette période :
- Mon écouteur série redémarrait la carte. Ouvrir le port de la manière par défaut active DTR/RTS, qui sur l’USB natif du S3 correspondent au reset et au boot-select. Chaque fois que je me connectais pour inspecter un écran figé, je redémarrais la carte et je lisais un journal sain. Le correctif consiste à construire l’objet port, abaisser les deux lignes, puis l’ouvrir.
- Mesurer l’opération entière. Mon premier chronométrage du scanout séparait « attente » et « conversion » mais omettait l’appel de dessin lui-même, qui bloque quand la file SPI est pleine. Il rapportait 5 ms pour une image qui en prenait 24.
Exécuter chaque compilation sans flasher la carte
Après plusieurs heures d’essais et d’erreurs, flasher la carte après chaque changement était l’étape la plus lente de la boucle — écrire l’image en USB, attendre le redémarrage, reconnecter le port série sans redémarrer à nouveau la carte, lire le journal, recommencer. Donc, pour exécuter et vérifier chaque compilation, j’ai utilisé velxio-cli, le client en ligne de commande de Velxio. Il prend le même .bin que ma chaîne d’outils produisait, l’exécute sur l’ESP32-S3 simulé de Velxio pendant une durée simulée fixe et renvoie la sortie série, ce qui me permettait de lire les compteurs et les timings sans toucher au matériel. La carte ne recevait une nouvelle image que quand une compilation le méritait.
# velxio.toml, next to the build: board = "xiao-esp32-s3", firmware = the merged .bin
velxio-cli run --timeout 5000 --timeout-exit-code 0 --serial-log-file serial.log .
L’installer et faire une première exécution prend quelques minutes — voir le démarrage rapide de velxio-cli.
Ajouter un niveau — le budget mémoire, encore
En sortant du Laboratoire d’Alchimie, on arrive à l’Entrée du Château (NP3). L’ajouter a fait déborder la RAM interne de 160 Ko. Les graphismes compressés, les palettes, les tile maps et les tables de sprites du niveau étaient déclarés modifiables, donc ils étaient copiés en RAM au démarrage. Les marquer const les a déplacés en flash. Le portage PC amont avait déjà ajouté NP3, et ce commit s’est transposé proprement.
Deux autres découvertes :
- Les sprites de Richter (le personnage du prologue) représentaient 85 Ko de données modifiables en RAM, jamais utilisées en jeu normal. Les marquer
constà nouveau a fait que, après l’ajout de NP3, l’utilisation de la RAM interne était inférieure à avant. - L’éditeur de liens signalait 16 symboles en double entre NP3 et les autres niveaux. Il y en avait 31. Comparer les tables de symboles avec
nma trouvé les 15 autres, dontEntitySlograetEntityGaibon— du code de boss que NP3 aurait silencieusement emprunté au Laboratoire d’Alchimie.
Les fichiers générés sont régénérés à partir du disque de chaque utilisateur, donc les changements const vivent dans un script (esp32/const_stage_tables.py) plutôt que dans les sources générées.
Conseils pour un portage similaire
- Choisissez une décompilation qui compile déjà une cible portable. C’est ce qui rend un portage natif possible là où un émulateur ne l’est pas.
- Faites en sorte que la carte vous dise ce qui ne va pas. Utilisez des compteurs, des timings décomposés en leurs parties, et des sommes de contrôle d’images. La plupart de mes hypothèses erronées ont été réfutées par un chiffre.
- Une pile d’appels nomme la victime. Pour une corruption mémoire, un watchpoint matériel sur l’adresse corrompue nomme le coupable.
- La mémoire est le budget de performance. Sur ce SoC, moins d’accès à la PSRAM battent toujours une arithmétique astucieuse.
- La tolérance du matériel d’origine masque des bugs. Attendez-vous à en trouver.
État actuel et prochaines étapes

- Carte Waveshare — Laboratoire d’Alchimie à 60 fps.
- Console portable — logique de jeu à pleine vitesse, écran autour de 55 fps lorsque la caméra est immobile.
- Niveaux reliés — Labo Alchimie, Salle de téléportation, et le menu titre/sauvegarde. L’Entrée du Château est reliée et attend son premier test sur la console portable.
- Son — les effets fonctionnent, et la musique XA se joue lorsque l’image du disque est sur la carte.
- Ensuite — davantage de niveaux (le budget mémoire a maintenant de la marge), et un harnais soudé pour la console portable afin de retenter le SPI à 80 MHz.
Le code est sur GitHub à davidmonterocrespo24/sotn-decomp (branche esp32-port). Le schéma de câblage et la liste des composants se trouvent dans le guide matériel. Vous avez besoin de votre propre copie du jeu ; le dépôt ne contient aucune donnée de jeu.
Merci aux contributeurs de la décompilation de SOTN, et à Xeeynamo pour psyz. Ce projet est bâti sur leur travail.