Comment marchent les secrets des Zelda Oracle
Dossier créé par Linkorange le July 27, 2024, 11:18 a.m.
Dernière modification par Linkorange le July 27, 2024, 11:18 a.m.
Ce dossier est le tout premier inédit à ce site 🥳 et pour fêter ça, on va décortiquer dans le moindre détail l'une des composantes essentielles de deux jeux de ma série vidéoludique favorite : les fameux Secrets de The Legend of Zelda - Oracle of Seasons et The Legend of Zelda - Oracle of Ages.
Attention !
Le but de ce dossier est de décortiquer un système très lié à la fin des deux jeux sus-cités. Par conséquent il va y
avoir des éléments de spoil bien vénère, du coup je vous recommande de ne pas continuer la lecture si vous souhaitez
découvrir les jeux par vous-mêmes. Cette page s'adresse principalement aux personnes connaissant déjà bien
l'aventure "Oracle" dans sa globalité.
Les Secrets, c'est quoi déjà ?
Parce qu'un petit rappel ne fait jamais de mal, ce que l'on appelle les "Secrets" est un système de mot de passe comme à l'ancienne qui permet d'échanger des données entre les deux jeux OOA et OOS. Ces mots de passe sont une série de caractères allant de 5 à 20 générés suivant certaines conditions dans l'un des deux jeux, par exemple, OOS, et il faut les rentrer, à une exception près, dans l'autre jeu (dans notre exemple ce serait dans OOA). Il existe 3 types de secrets que l'on va détailler ci-dessous.
Le secret pour démarrer une partie
On l'appellera aussi le secret des parties, c'est le secret que l'on obtient après le générique de fin. Il permet de démarrer une partie en jeu lié ou une partie en "mode héros", et fait 20 caractères. C'est le secret que l'on peut rentrer au moment de créer un nouveau fichier de sauvegarde dans chacun des jeux, et il transfère le nom de Link, le nom de l'enfant, l'animal (Ricky, Moosh ou Dimitri) qui vous accompagne, etc.
Dans les Oracle, il existe 4 types de parties :
| # | Description | Illustration |
|---|---|---|
| 1 |
La partie de base, celle que vous démarrez tout simplement sans entrer de secret.
|
|
| 2 |
La partie en jeu lié, celle que vous pouvez démarrer grâce au secret obtenu une fois la partie précédente terminée.
|
|
| 3 |
La partie en mode Héros, que vous pouvez démarrer grâce au "Secret Héros" obtenu après le générique de fin en jeu lié.
|
|
| 4 |
La partie en jeu lié en mode Héros, que vous pouvez démarrer grâce au secret obtenu en finissant une partie en mode Héros.
|
|
De manière générale, un secret obtenu dans OOA ne marchera que dans OOS et vice-versa, or je le mentionnais au-dessus, il y a UNE exception à cette règle : il s'agit du "Secret Héros", celui obtenu après avoir vaincu Ganon. En effet, c'est le seul secret que l'on peut entrer indépendamment dans les deux jeux, du coup si par exemple vous venez à bout de Ganon dans OOS et obtenez le "Secret Héros", eh bien vous pouvez rentrer ce même secret sans changer de cartouche et vous démarrerez une partie en mode Héros dans OOS tout en conservant votre nom. Mais si vous changez de cartouche, insérez OOA et entrez le même secret, alors vous démarrerez une partie en mode Héros dans OOA tout en conservant votre nom.
Le GameID
Il est intéressant de noter que si vous entrez exactement le même nom de fichier, le même nom pour l'enfant, que vous obtenez le même animal, etc. dans deux parties démarrées séparément, il est quasi certain que le secret généré à la fin de chacune de ces parties va différer. Pourquoi ? Eh bien parce qu'une composante aléatoire intervient à la génération du tout premier secret : c'est ce que l'on appelle le GameID.
Le GameID est un nombre compris entre 0 et 32767 instancié pseudo-aléatoirement lors de la génération du tout premier secret. Par la suite, il ne sera plus jamais changé, et plus encore, c'est l'une des informations transmises de jeu en jeu au gré des secrets, dont la valeur reste constante au sein des 4 types de parties énumérées plus haut : il est l'identifiant de la "lignée" de parties dans laquelle il a été généré.
En quoi nous intéresse-t-il ? Eh bien il est tout simplement au coeur du calcul de TOUS les secrets que l'on va croiser dans nos différentes parties, mais ça on y reviendra plus loin quand on détaillera en détail l'algorithme de la génération des secrets.
Le secret des anneaux
L'une des composantes de gameplay un peu annexes mais non moins chronophages (surtout si on vise le 100%), ce sont les anneaux. Il y a en tout 64 anneaux disponibles dans OOS et OOA, certains accessibles uniquement dans OOA, d'autres dans OOS, certains accessibles uniquement en jeu lié et un accessible uniquement à partir du mode Héros. Je ne vais pas détailler l'intégralité de ce que je sais sur ces anneaux, car d'une on n'en aura pas besoin pour expliquer le système des secrets, et de deux ce guide GameFAQs est la ressource la plus complète que j'ai pu voir à ce jour sur le sujet et le fera mieux que moi.
Lorsque l'on termine une partie (ou à n'importe quel moment en jeu lié / mode Héros), on peut aller parler au serpent rouge présent chez Vasu le joailler. On peut choisir de lui dire un secret ou alors d'écouter son secret. Le secret qu'il nous donne encapsule sur 15 caractères l'intégralité des données des anneaux actuellement en notre possession. Si l'on communique ce secret au serpent rouge de la partie de destination, alors les anneaux encodés dans ce secret se voient rajoutés à la collection de la partie de destination (si des anneaux sont en notre possession mais pas encodés par le secret alors pas de panique, ils ne sont pas enlevés ; communiquer un secret au serpent rouge ne peut pas supprimer d'anneau).
A savoir qu'un secret des anneaux ne fonctionne que sur une lignée de parties (donc avec même GameID), et ne fonctionnera dans aucun autre fichier de sauvegarde.
Le master cheat code
Il existe en vérité une exception à la règle ci-dessus qui permet de donner les anneaux que l'on souhaite à n'importe quel fichier de sauvegarde, peu importe son GameID. Par conséquent, on peut générer un secret qui donne les 64 anneaux et qui marche pour absolument toutes les parties possibles et inimaginables, et ce, que ce soit dans OOS ou OOA.
Comment ça marche ? Eh bien il existe dans le code du jeu une condition très spécifique lorsque la valeur du GameID vaut 0. Pour rappel, il y a 32768 valeurs possibles, de 0 à 32767. Or, le code du jeu qui génère le GameID prend bien soin de retourner une valeur différente de 0, donc de manière normale ce n'est pas possible. Mais en craquant le code (ce qui est exactement ce qu'on va faire ci-dessous), il est possible de générer "manuellement" un secret avec un GameID valant 0 😁.
La condition est explicite dans le code du jeu donc c'est volontaire de la part des développeurs. C'était probablement utilisé lors de phases de testing pendant le développement du jeu, et ça n'a pas été enlevé à la sortie. Grosso modo, lorsque vous donnez un secret au serpent rouge, celui-ci est décodé et passe par une phase de validation avec potentiel rejet du secret. Or, si la valeur du GameID vaut 0, cette phase de validation est skippée et les anneaux encodés dans le secret seront ajoutés à la collection.
Vous vous en foutez des paragraphes ci-dessus et vous voulez juste accéder à ce fameux master cheatcode ? Comme je suis clément, voici une page dans laquelle vous pourrez trouver le master cheat code qui vous donne les 64 anneaux du jeu, mais également un autre master cheat code qui vous donne uniquement les anneaux GBA (particulièrement utile si vous jouez sur une version genre 3DS ou Switch qui n'active pas le flag GBA). Comble du bonheur, les master cheat codes sont donnés aussi bien pour la version japonaise que pour les versions européenne/US. Enjoy !
Le secret qui donne des bonus
Quand vous jouez en jeu lié - disons par exemple OOA - des personnages qui ne seraient pas là en temps normal apparaissent à quelques endroits de la carte. Ces personnages feront référence à des lieux ou des personnages de l'autre jeu, et vous demanderont d'aller y transmettre un secret de 5 caractères - dans notre exemple, dans OOS. Une fois ce secret communiqué, on obtient un bonus (une amélioration de capacité, un nouvel item, un anneau, etc) et dans la quasi-totalité des cas on reçoit un autre secret à communiquer à Farore qui se trouve dans l'Arbre Bojo dans le jeu d'origine - donc OOA si on poursuit l'exemple. Une fois cela fait on pourra obtenir le même bonus que celui obtenu dans le jeu de destination.
Vous l'aurez compris, c'est de ce secret de 5 caractères dont il s'agit dans ce paragraphe. Il y a en tout 10 secrets à communiquer du jeu lié au jeu de base, et 8 secrets à communiquer en retour ; en effet, 2 de ces secrets donnent des anneaux, et ne génèrent pas de secret retour, car il suffit d'aller les faire évaluer chez Vasu et utiliser le secret donné par le serpent rouge pour les communiquer.
Bon, et comment ça marche du coup ?
Après avoir expliqué en détail les 3 types de secrets, on peut enfin rentrer dans le dur, mettre les mains dans le cambouis, et expliquer le fonctionnement derrière la génération des secrets. Parlons donc bits, opérations logiques, conversions hexadécimales et autres joyeusetés de ce genre !
Prérequis pour la suite
Par la suite, on va manipuler des concepts mathématiques tels que les bases arithmétiques et leurs conversions
(principalement entre base 2, 10 et 16). Ce post de blog ne visant pas à être un cours d'arithmétique (il est déjà
bien assez long comme ça 😅), si les termes "binaire", "hexadécimal", ou encore "bit de poids fort" vous
parlent, alors vous avez le bagage pour continuer la lecture. Sinon, je vous recommande d'abord d'aller vous
renseigner sur ces concepts, car autrement vous risquez de ne plus rien comprendre !
Il est intéressant de noter que la version du jeu (japonaise, américaine, ou européenne) ne change rien au fonctionnement interne de la génération des secrets : en effet, le plus gros du travail est effectué sur des bits, qui eux sont universels. Seules certaines conversions mettant en scène des caractères lisibles par le joueur changent, et s'adaptent en fonction de la version. On y reviendra bien plus en détail plus bas car il y a des choses intéressantes à dire sur le sujet.
Les secrets sont générés au moment où ils sont communiqués au joueur. Cela a surtout un impact sur le secret des anneaux, car en fonction de la collection du joueur, il ne sera pas le même. Chaque type de secret suit son propre algorithme, mais ils suivent tous les mêmes étapes à grande échelle, détaillons-les ci-dessous :
- Chaque secret lit dans la RAM du jeu les informations qui l'intéresse, et les encode dans un grand nombre binaire selon un ordre prédéfini
- Une fois le nombre binaire obtenu, il est découpé par paquets de 6 bits sur lesquels on va travailler un par un
- Une valeur de checksum codée sur 6 bits est calculée sur la base de tous les paquets précédents, et est ajoutée en tant que deriner paquet
- Chaque paquet de 6 bits subit une opération qui le transforme en un autre paquet de 6 bits
- Chaque nouveau paquet de 6 bits est converti, selon une table fixe définie dans la ROM du jeu, en caractères lisibles par le joueur
Le décodage suit grosso modo les opérations inverses que celles décrites précédemment. Ainsi, il transforme les caractères en données de jeu brut, et les écrit dans la RAM. C'est pourquoi on ne parlera que des premières étapes du décodage lorsqu'on va le traiter, tout le reste ayant déjà été vu plus haut !
A propos du gros nombre binaire
Le nombre binaire dont on parle peut faire jusqu'à 120 bits en fonction du secret manipulé. Or on va souvent y faire
référence par la suite, donc au lieu de l'appeler "gros nombre binaire" je vous propose de l'appeler
unencodedSecret : il va contenir les données du secret qui n'est pas encore encodé, d'où ce
nom.
L'algorithme des secrets des parties
Cet algorithme est le plus long et le plus complexe, mais également le plus intéressant. Je vais
intégrer les étapes génériques énumérées plus haut dans les étapes de cet algorithme, car on y trouve certaines
spécificités malgré tout. Commençons d'abord par énumérer chaque étape, avant de les détailler une par
une. A chaque ligne qui ajoute des bits à unencodedSecret, j'indiquerai le nombre de bits rajoutés à
cette étape entre parenthèses à la fin de la ligne.
- On calcule une valeur codée sur 3 bits à partir du gameID. On appellera cette valeur "cipherKey"
-
On inverse les bits du cipherKey obtenu à l'étape ci-dessus et on le stocke dans
unencodedSecret(3 bits) -
On concatène le nombre binaire
00àunencodedSecret(2 bits) -
On inverse les bits du GameID et on les concatène à
unencodedSecret(15 bits) -
On concatène à
unencodedSecretun bit donnant l'info de est-ce que le jeu de destination va être en mode Héros ou non (1 bit) -
On concatène à
unencodedSecretun bit donnant le jeu à partir duquel a été généré le secret (1 bit) -
On encode la lettre 0 du nom du héros sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On encode la lettre 0 du nom de l'enfant sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On encode la lettre 1 du nom du héros sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On encode la lettre 1 du nom de l'enfant sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On prend la valeur liée au comportement de l'enfant, on inverse ses bits et on concatène le
tout à
unencodedSecret(6 bits) -
On encode la lettre 2 du nom du héros sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On encode la lettre 2 du nom de l'enfant sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On concatène à
unencodedSecretun bit donnant l'info de si on a reçu le premier anneau de la part de Vasu ou non (1 bit) -
On encode la lettre 3 du nom du héros sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On prend la valeur liée au compagnon (Ricky, Moosh ou Dimitri), on inverse ses bits et on
concatène le tout à
unencodedSecret(4 bits) -
On encode la lettre 4 du nom du héros sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On encode la lettre 3 du nom de l'enfant sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On concatène à
unencodedSecretun bit indiquant si la partie qui va être créée par le secret va être en jeu lié ou non (1 bit) -
On encode la lettre 4 du nom de l'enfant sur 8 bits, on les inverse, et on concatène le
résultat à
unencodedSecret(8 bits) -
On divise le
unencodedSecretainsi obtenu (114 bits à cette étape) en 19 paquets de 6 bits -
On fait un calcul de checksum sur 6 bits en fonction des 19 valeurs obtenues à l'étape
précédente, et on l'ajoute en tant que 20e élément à
unencodedSecret - On applique une fonction de hachage sur chacun des 20 paquets obtenus ci-dessus à partir d'un array fixe codé dans la ROM
- On convertit les 20 valeurs hashées en caractères lisibles par le joueur en fonction d'un autre array fixe codé dans la ROM
A propos de l'algorithme ci-dessus
Il est possible que les étapes ci-dessus diffèrent légèrement de ce qui est fait dans le
jeu. Afin d'identifier ces étapes, j'ai étudié non pas le code du jeu (qui n'a pas leak
à l'heure où j'écris ces lignes 😇) mais
le code source de Zora Sharp,
qui implémente la logique de la plupart des générateurs de secrets existant actuellement pour OOS/OOA. Pour vous
prouver que ça marche, on va le tester avec les informations relatives à ma partie : à l'issue de chaque étape,
je donnerai la valeur calculée jusque là de unencodedSecret. Si à la fin après encodage du tout on
retombe sur mon secret, alors on est bons !
Résumons les données de ma partie que je vais encoder ci-dessous :
- Ma partie est sur la version japonaise
- Mon GameID vaut 27874 (en valeur décimale)
- Le jeu de destination n'est pas une partie en mode Héros
- Le secret va être généré à partir de Oracle of Ages
- Mon héros s'appelle リンコ
- J'ai nommé l'enfant マリオ
- Le comportement de l'enfant valait 20 à la fin de OOA
- J'ai reçu le premier anneau de la part de Vasu
- Mon compagnon était Dimitri
- La partie qui va être créée va être en jeu lié
Etape 1 - Création du cipherKey
On parlait précédemment du GameID, cette valeur comprise entre 0 et 32767. Bon, si vous êtes un peu nerd, le nombre 32767 devrait vous parler, car il s'agit de la valeur 215 - 1. En d'autres termes, c'est une valeur codée sur 15 bits. On y reviendra plus en détail dans la partie qui lui est concernée, pour l'heure retenez juste que le GameID est encodé sur 15 bits.
Le cipherKey, qui est la valeur qui nous intéresse dans ce paragraphe, est calculée sur la base du GameID et ramenée à 3 bits, selon la formule ci-dessous :
cipherKey = ( (GameID >> 8) + (GameID & $FF) ) & $O7
Afin d'expliquer la formule ci-dessus, explicitons les 4 opérations qui y sont effectuées :
(GameID >> 8)-
L'opération >> est une opération de shift de bits vers
la droite. Dans ce cas, les bits du GameID sont shiftés (= décalés) de 8 bits vers la droite.
En d'autres termes, les 8 bits de poids fort sont décalés vers la droite, on ajoute des 0 par la gauche, et les 8 bits de poids faibles se font dégager.
Dans mon cas, le GameID une fois converti en binaire est le suivant :110110011100010. L'opération(GameID >> 8)donne alors le résultat suivant :000000001101100. Les 7 bits de poids fort du GameID sont à la place des bits de poids faible du résultat. (GameID & $FF)-
L'opération & désigne une opération logique AND. Comme
je le disais en préambule de cette section, elle n'a pas vocation a être un cours d'algèbre de Boole donc je
suppose que vous savez de quoi il s'agit 😛.
Ce qui est intéressant ici c'est l'opérande de droite$FF, qui convertie en binaire donne11111111. Ce que ça signifie, c'est que le résultat de cette opération va tronquer le GameID et ne garder que ses 8 bits de poids faible. (GameID >> 8) + (GameID & $FF)-
Cette opération additionne les deux résultats expliqués ci-dessus. Or si l'on regarde bien,
ces deux résultats sont respectivement les 7 bits de poids fort du GameID et les 8 bits de poids faible du
GameID. Par conséquent, ce qu'on a fait jusqu'ici est de découper le GameID en deux, et d'additionner ces deux
parties.
Si on reprend l'exemple ci-dessus avec monGameID = 110110011100010, alors l'opération réalisée ici est la suivante :1101100 + 11100010 = 101001110. Le lecteur averti notera que le résultat de l'exemple comporte non pas 7 bits ni 8 bits, mais 9 bits 👀. & $07-
Cette opération est une nouvelle opération logique AND, et le but est le même que celle
réalisée au-dessus. La valeur hexadécimale
$07s'exprime en binaire111. On va donc tronquer les 3 derniers bits du résultat obtenu par l'addition précédente. Ceci est notre cipherKey.
Afin de terminer avec l'exemple précédent (GameID = 110110011100010), le résultat devient donc cipherKey = 110.
Etape 2 - Inversion du cipherKey et début de l'encodage
Il n'y a pas grand-chose à dire à cette étape : on ne fait qu'inverser le cipherKey obtenu à l'étape précédente. Par exemple si le cipherKey vaut 110, alors le résultat de cette opération sera 011.
La partie intéressante ici est le stockage de ce nombre dans le fameux
unencodedSecret qu'on a défini plus haut. Pour rappel, c'est un gros nombre binaire qui à la fin de la
phase d'encodage va peser 120 bits, et là on vient de déterminer ses 3 bits de poids le plus fort. Par la suite
chaque ajout à unencodedSecret rajoutera des bits de poids plus faibles que ceux déjà présents. Si ça
peut vous aider à comprendre, voyez ce nombre comme un array de bits*, dans lequel on vient de
remplir les 3 premières cases.
* En assembleur Gameboy la notion d'array est très abstraite (on va plutôt avoir tendance à écrire des bytes dans des adresses mémoire qui se suivent), par conséquent l'implémentation est sans doute différente dans le vrai code du jeu, mais dans l'idée c'est exactement la même chose, et un array parlera sans doute à beaucoup plus de monde.
A ce stade, unencodedSecret = 011
Etape 3 - Le type de secret
Cette étape ne fait qu'ajouter deux bits de valeur 00 à
unencodedSecret. Quand j'ai vu ça au début je me suis demandé quel était l'intérêt d'une telle
opération, mais en réalité ce qui est encodé dans ces deux bits est le type de secret. Comme on l'a vu
plus haut, il y a 3 types de secrets, et la valeur 00 ici définit le type
de secret qui sert à démarrer une nouvelle partie.
A ce stade, unencodedSecret = 011 00
Etape 4 - Le GameID
Le fameux, on y revient ! Je l'ai déjà introduit dans ce paragraphe plus haut donc je ne vais pas revenir sur sa définition, ici on va se concentrer sur l'aspect technique qui l'entoure. Sachez que j'ai analysé dans le niveau de détail le plus élevé possible la génération du GameID, et si vous avez toujours soif de plus de savoir, je vous propose de décortiquer ça dans cet annexe en bas de page.
On le disait précédemment, le GameID est encodé sur 15 bits. L'étape actuelle va simplement
inverser ces 15 bits et les ajouter à unencodedSecret. Ainsi, dans mon cas, comme GameID =
110110011100010, alors sa valeur inversée 010001110011011 va être ajoutée à la suite des bits
ajoutés dans les étapes précédentes.
A ce stade, unencodedSecret = 011 00 010001110011011
Etape 5 - Le mode Héros
Cette étape fait partie des plus simples car un seul bit va être rajouté à
unencodedSecret, en fonction de si la partie que va permettre de créer le secret est en mode Héros ou
non. Ca marche pour deux des 4 types de parties définis plus haut. Le tableau suivant
explicite la valeur :
| Jeu de destination en mode Héros ? | Valeur |
|---|---|
| Oui | 1 |
| Non | 0 |
A ce stade, unencodedSecret = 011 00 010001110011011 0
Etape 6 - Le jeu qui a généré le secret
Cette étape fait également partie des plus simples car un seul bit va être rajouté à
unencodedSecret, en fonction du jeu à partir duquel a été généré le secret, selon le tableau
suivant :
| Jeu source | Valeur |
|---|---|
| Oracle of Seasons | 0 |
| Oracle of Ages | 1 |
Dans le code source que j'ai analysé ce bit est défini par le "jeu de destination", et définit par conséquent les valeurs suivantes : OOA = 0 et OOS = 1. Or, on en a parlé quand on explicitait les 4 types de parties plus haut, le secret permettant de jouer en mode Héros non lié peut être entré aussi bien dans OOS que OOA, peu importe la jeu à partir duquel il a été généré, ce qui rend cette définition de "jeu de destination" caduque à mon sens. C'est pourquoi j'ai préféré définir la valeur ci-dessus par le jeu à partir duquel a été généré le secret.
A ce stade, unencodedSecret = 011 00 010001110011011 0 1
Etapes 7, 8, 9, 10, 12, 13, 15, 17, 18 et 20 - Les noms du héros et de l'enfant
On va faire d'une pierre 10 coups ici car les étapes énumérées ci-dessus sont identiques. Dans OOA et OOS, on peut nommer deux personnages :
- Link, via le nom du fichier
- En vérité, on ne peut nommer Link que dans la toute première partie, et son nom va être automatiquement configuré dans toutes les futures parties issues de la même lignée, justement grâce à l'encodage dont on va parler ici.
- L'enfant né de l'union de Bipin et Blossom
- Bipin et Blossom sont deux personnages qui habitent soit dans la Cité Horon soit dans la Cité Lynna en fonction du premier jeu que l'on fait, puis déménagent dans la cité de l'autre jeu lorsque Onox ou Véran est vaincu(e). La première fois qu'on parle à Blossom on a la possibilité de nommer son enfant, et ce nom va être gardé dans la partie liée qui suit. En mode Héros non lié cependant, on a la possibilité de nommer l'enfant de nouveau, mais à l'écran de nommage le nom utilisé dans les parties précédentes est déjà rentré par défaut.
Les deux noms peuvent contenir jusqu'à 5 caractères chacun. Techniquement parlant, chaque caractère est en réalité relié à un nombre qui est codé sur 8 bits, et l'encodage de chacun de ces caractères est stocké dans un tableau fixe dans la ROM du jeu.
Il existe deux versions de ce tableau en fonction de la région pour laquelle est défini le jeu : un tableau pour la version japonaise, et un autre tableau pour les autres versions. Le tableau de la version japonaise comporte 240 cases tandis que celui des versions européenne/US en contient 176.
Si vous tentez cependant de démarrer une nouvelle partie sur votre cartouche, vous n'allez avoir accès qu'à 120 caractères et ce quelque soit la version. En effet, ce tableau est utilisé non seulement pour les caractères des noms mais aussi pour l'affichage de tous les textes du jeu (dialogues de PNJ, panneaux, menus, etc), et peut donc afficher certains caractères supplémentaires comme des flèches, de la ponctuation, les icônes des boutons A et B, etc.
Dans tous les cas, c'est ce tableau qui est utilisé pour encoder chaque lettre des noms du héros et de l'enfant. Par souci d'exhaustivité, je vous propose dans le tableau ci-dessous la liste de tous ces caractères, et ce dans les deux versions du jeu (JP et EUR/US), avec leur nombre décimal associé. Il est intéressant de noter que l'index des caractères ne commence pas à 0 comme on pourrait s'y attendre mais par 16.
| Home | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| JP | \0 |
\0 |
\0 |
\0 |
♥ | ↑ | ↓ | ← | → | \0 |
\0 |
「 | 」 | \0 |
\0 |
。 |
| US/EUR | ● | ♣ | ♦ | ♠ | \0 |
↑ | ↓ | ← | → | \0 |
\0 |
「 | 」 | · | \0 |
。 |
| Home | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
| JP | ! | " | # | $ | % | & | ' | ( | ) | * | + | , | - | . | / | |
| US/EUR | ! | " | # | $ | % | & | ' | ( | ) | * | + | , | - | . | / | |
| Home | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
| JP | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
| US/EUR | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | : | ; | < | = | > | ? |
| Home | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
| JP | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
| US/EUR | @ | A | B | C | D | E | F | G | H | I | J | K | L | M | N | O |
| Home | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
| JP | P | Q | R | S | T | U | V | W | X | Y | Z | [ | ~ | ] | ^ | \0 |
| US/EUR | P | Q | R | S | T | U | V | W | X | Y | Z | [ | ~ | ] | ^ | \0 |
| Home | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 |
| JP | あ | い | う | え | お | か | き | く | け | こ | さ | し | す | せ | そ | た |
| US/EUR | \0 |
a | b | c | d | e | f | g | h | i | j | k | l | m | n | o |
| Home | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
| JP | ち | つ | て | と | な | に | ぬ | ね | の | は | ひ | ふ | へ | ほ | ま | み |
| US/EUR | p | q | r | s | t | u | v | w | x | y | z | { | ¥ | } | ▲ | ■ |
| Home | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
| JP | む | め | も | や | ゆ | よ | ら | り | る | れ | ろ | を | わ | ん | ぁ | ぃ |
| US/EUR | À |  | Ä | Æ | Ç | È | É | Ê | Ë | Î | Ï | Ñ | Ö | Œ | Ù | Û |
| Home | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 |
| JP | ぅ | ぇ | ぉ | っ | ゃ | ゅ | ょ | が | ぎ | ぐ | げ | ご | ざ | じ | ず | ぜ |
| US/EUR | Ü | \0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
| Home | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 |
| JP | ぞ | だ | ぢ | づ | で | ど | ば | び | ぶ | べ | ぼ | ぱ | ぴ | ぷ | ぺ | ぽ |
| US/EUR | à | â | ä | æ | ç | è | é | ê | ë | î | ï | ñ | ö | œ | ù | û |
| Home | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
| JP | ア | イ | ウ | エ | オ | カ | キ | ク | ケ | コ | サ | シ | ス | セ | ソ | タ |
| US/EUR | ü | \0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
\0 |
♥ | \0 |
\0 |
| Home | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 |
| JP | チ | ツ | テ | ト | ナ | ニ | ヌ | ネ | ノ | ハ | ヒ | フ | ヘ | ホ | マ | ミ |
| US/EUR | ||||||||||||||||
| Home | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 |
| JP | ム | メ | モ | ヤ | ユ | ヨ | ラ | リ | ル | レ | ロ | ワ | ヲ | ン | ァ | ィ |
| US/EUR | ||||||||||||||||
| Home | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 |
| JP | ゥ | ェ | ォ | ッ | ャ | ュ | ョ | ガ | ギ | グ | ゲ | ゴ | ザ | ジ | ズ | ゼ |
| US/EUR | ||||||||||||||||
| Home | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 |
| JP | ゾ | ダ | ヂ | ヅ | デ | ド | バ | ビ | ブ | ベ | ボ | パ | ピ | プ | ペ | ポ |
| US/EUR | ||||||||||||||||
Note : Le caractère \0 dans le tableau ci-dessus signifie "caractère
vide"
Pour conclure l'explication de ce genre d'étapes dans l'algorithme de génération du secret,
procédons par l'exemple : imaginons que le nom du héros est "Link" dans la version européenne du jeu. Alors
l'encodage de son nom va ressembler à ça : 76, 105, 110, 107, 0. A noter que le 0 à la fin encode le
dernier et 5e caractère, qui est un caractère vide. Bien que le caractère vide apparaisse plusieurs fois dans le
tableau, j'ai pu vérifier empiriquement que c'est par la valeur 0 qu'il est encodé.
Maintenant, imaginons que l'on est à l'encodage du caractère 0 du nom du héros. Il s'agit de la
lettre majuscule L, qui est donc encodée avec le nombre 76. En convertissant ce nombre sur 8 bits, on obtient
01001100. Maintenant, il s'agit seulement d'inverser ces bits, ce qui donne 00110010, puis
de l'ajouter à unencodedSecret.
A ce stade, dans mon exemple (version japonaise, héros nommé リンコ et enfant nommé マリオ),
unencodedSecret = 011 00 010001110011011 0 1 11101011 01110011 10111011 11101011 xxxxxx 10011101 00101101 x
00000000 xxxx 00000000 00000000 x 00000000 (les x seront établis dans les étapes
suivantes)
Etape 11 - Le comportement de l'enfant
Vous savez peut-être que l'enfant de Bipin et Blossom, celui que l'on nomme, grandit une fois une partie en jeu lié. Mais ce que vous ne savez peut-être pas, c'est qu'il y a plusieurs évolutions possibles pour ce gamin ! En effet, peut-être pour certains d'entre vous il sera devenu botaniste, peut-être que pour d'autres il sera devenu chanteur...
C'est ce qui mène à ce résultat que l'on nomme le "comportement de l'enfant". En réalité, c'est une valeur codée sur 6 bits (donc comprise entre 0 et 63) qui va déterminer les différentes étapes de l'évolution de l'enfant, et qui est modifiée à chaque action que l'on fait en rapport avec l'enfant. Vous vous souvenez peut-être de sa maman qui vous quémande des rubis pour l'aider à soigner son fils, ou de ce derneir qui vous demande qui de la poule ou l'oeuf est arrivé en premier, eh bien chaque réponse que vous donnez modifie cette valeur du "comportement" de l'enfant.
Comme l'enfant grandit sur l'ensemble des deux parties (jeu non lié puis jeu lié), il est nécessaire de faire passer cette information pour pouvoir continuer à la travailler dans la seconde partie, et c'est pour cette raison qu'elle est encodée dans le secret.
Pour rentrer plus dans le détail, l'enfant va passer par deux phases où son sprite et son comportement vont évoluer : il va d'abord passer de bébé à enfant, puis de enfant à adulte. A chaque évolution, la valeur de son comportement est checkée par le jeu et ça détermine sa nouvelle personnalité.
Le tableau ci-dessous donne les détails de chaque étape qui modifie la valeur du comportement de l'enfant :
| Etape | Valeur du statut |
|---|---|
| Entrer le nom de l'enfant |
Etablit une valeur entre 1 et 3 pour le statut (cf. cet annexe si vous voulez avoir le détail du calcul |
| Blossom nous demande de lui donner une somme d'argent |
|
| Question sur comment endormir le bébé |
|
| Question sur quel type d'enfant on était |
|
| Question de l'enfant (la poule ou l'oeuf, ou est-ce que tu es fort) |
|
La personnalité de l'enfant à l'issue de la phase bébé => enfant se décide uniquement via la valeur dont on décrit les étapes ci-dessus. En revanche, sa personnalité à l'issue de la phase enfant => adulte se décide non seulement via la valeur de son comportement, mais aussi sur la base de la première personnalité qu'il a acquise à la phase précédente. Par souci d'exhaustivité (encore et toujours, j'en viens à me fatiguer moi-même 😅) voici un tableau récapitulant les personnalités issues des deux phases d'évolution possibles pour l'enfant et leurs conditions :
| Personnalité à l'issue de bébé => enfant | Curieux | Timide | Hyperactif | ||||||
|---|---|---|---|---|---|---|---|---|---|
| Valeur de comportement nécessaire | 1-5 | 6-10 | 11+ | ||||||
| Personnalité à l'issue de enfant => adulte | Chanteur | Flemmard | Guerrier | Chanteur | Flemmard | Arboriste | Flemmard | Guerrier | Arboriste |
| Valeur de comportement nécessaire | 1-9 | 10-13 | 14+ | 1-14 | 15-18 | 19+ | 1-20 | 21-25 | 26+ |
Après tout ça, la valeur de comportement est convertie sur 6 bits et ces derniers sont inversés
puis ajoutés à unencodedSecret. Pour prendre un exemple, imaginons que l'enfant est devenu timide, avec
une valeur de comportement égale à 9. Dans ce cas, la valeur binaire sur 6 bits de ce comportement devient
001001. C'est l'inversion de ces bits, donc le paquet 100100, qui va être ajouté à
unencodedSecret.
A ce stade, pour moi l'enfant était bien hyperactif et son comportement valait 20, par
conséquent unencodedSecret = 011 00 010001110011011 0 1 11101011 01110011 10111011 11101011 001010 10011101
00101101 x 00000000 xxxx 00000000 00000000 x 00000000 (les x seront établis dans les étapes
suivantes)
Etape 14 - Le premier anneau de Vasu
Cette étape est également l'une des plus simples : un flag garde en mémoire si Vasu vous a donné l'Anneau Amitié, le premier anneau qu'il vous donne symbolisant votre rencontre et qui sert également de tutoriel au système d'évaluation d'anneaux. La raison est que si vous avez déjà eu ce tutoriel dans votre première partie, alors le jeu ne vous l'imposera pas en jeu lié (dans tous les cas vous le reverrez en mode Héros non lié que le flag soit activé ou non).
Ce flag tient bien sûr sur un bit qui est ajouté à unencodedSecret à cette étape.
Le tableau suivant donne la valeur du bit en fonction de si oui ou non on a reçu l'Anneau
Amitié :
| A-t-on reçu l'Anneau Amitié de la part de Vasu ? | Valeur |
|---|---|
| Oui | 1 |
| Non | 0 |
A ce stade, unencodedSecret = 011 00 010001110011011 0 1 11101011 01110011 10111011
11101011 001010 10011101 00101101 1 00000000 xxxx 00000000 00000000 x 00000000 (les x seront
établis dans les étapes suivantes)
Etape 16 - Le compagnon
En jeu non lié, à un certain stade de l'aventure, l'animal qui va accompagner Link - Ricky, Moosh, ou Dimitri - et changer une partie de la map du jeu va être décidé en fonction de si on possède une flûte ou non et si oui laquelle (achat au magasin ou obtention via un mini-jeu). En jeu lié, on retrouve l'animal qui nous accompagnait lors de notre première partie et on ne peut pas en avoir un autre. Cette information se transmet évidemment grâce au secret, et s'encode à l'étape à laquelle nous sommes.
Chose surprenante, bien qu'il n'y ait que trois animaux possibles pour accompagner Link, cette information est codée sur 4 bits (donc 16 valeurs différentes théoriquement possibles). Cependant, les seules trois valeurs qui ont du sens sont les suivantes :
| Compagnon | Valeur |
|---|---|
| Ricky | 11 |
| Dimitri | 12 |
| Moosh | 13 |
La valeur est encodée sur 4 bits, inversée, puis ajoutée à unencodedSecret. Si l'on
prend l'exemple de Ricky, alors sa valeur en binaire vaut 1011, par conséquent ce sont les bits
1101 qui seront rajoutés à unencodedSecret.
Et il se passe quoi pour les autres valeurs ?
Si une valeur autre que les trois prévues par le jeu est rentrée pour l'animal, alors non seulement le sprite de
l'animal en question va avoir un sprite d'un objet qui n'a rien à voir, mais la zone de Labrynna ou Holodrum qui
correspond à cet animal va être complètement foutue en l'air ! Un speedrunner du nom de Stewmath a hacké le jeu et
fait ces tests, et il en parle
dans son
blog, montrant le résultat dans Ages pour une valeur non prévue. C'est assez rigolo, je vous invite à aller voir
ça !
A ce stade, moi j'avais Dimitri, par conséquent unencodedSecret = 011 00 010001110011011
0 1 11101011 01110011 10111011 11101011 001010 10011101 00101101 1 00000000 0011 00000000 00000000 x 00000000
(plus qu'un x !)
Etape 19 - Le jeu lié
Encore une étape très simple : un bit donnant l'info de si le jeu de destination va être une
partie en jeu lié ou non est ajouté à unencodedSecret. Bien évidemment, si c'est oui, tous les
événements (cinématique d'intro, Zelda qui apparaît dans la région, etc), le contenu bonus, ou encore la vraie fin
vont être déclenchés dans la partie générée par ce secret. Ce bit est défini selon le tableau
suivant :
| Va-t-on générer une partie en jeu lié ? | Valeur |
|---|---|
| Oui | 1 |
| Non | 0 |
A ce stade, unencodedSecret = 011 00 010001110011011 0 1 11101011 01110011 10111011
11101011 001010 10011101 00101101 1 00000000 0011 00000000 00000000 1 00000000. Ca y est, on les a
tous !
Etape 21 - Division en paquets de 6 bits
Une fois enfin parvenus à cette étape, unencodedSecret comporte 114 bits. On va
alors le découper par paquets de 6 bits, ce qui donne 114/6 = 19 paquets. Dans l'exemple de notre
unencodedSecret on obtient un résultat comme ceci : 011000 100011 100110 110111 101011 011100
111011 101111 101011 001010 100111 010010 110110 000000 000110 000000 000000 000100
000000.
Etape 22 - Calcul du checksum
Ce sont les 19 paquets obtenus ci-dessus qui vont être encodés en caractères affichés par le jeu, or à la fin le secret comporte 20 caractères, non pas 19 ! C'est le but de cette étape : calculer le 20e et dernier paquet. Ces 6 bits que l'on va calculer sont le checksum du mot de passe. Ils servent non seulement à ajouter un 20e caractère, mais également lors du décodage, pour vérifier la cohérence du secret. En gros, si les informations contenues dans les 19 autres caractères ne sont pas cohérentes avec la valeur contenue dans le dernier caractère, le mot de passe sera refusé par le jeu. Si vous avez quelques connaissances en cryptographie, c'est le principe de base du checksum, c'est d'ailleurs la raison pour laquelle on le nomme ainsi.
L'opération de calcul de checksum est en réalité bien plus simple que ce qu'on pourrait le
penser. Déjà, les deux bits de poids le plus fort du checksum seront toujours 00. C'est une étape fixe
dans le code du jeu. Pour les 4 bits de poids faible restants, on utilise la formule
suivante :
checksum = ∑(paquets de 6 bits) & $0F
La somme utilisée ci-dessus est une somme arithmétique toute bête. Que la base soit 2, 10 ou 16
ça revient au même. A la fin, l'opération AND sur $0F permet de tronquer le résultat de la somme pour
n'en garder que les 4 derniers bits, qui seront les 4 bits de poids faible du checksum après les deux 0. A noter que
les bits qui composent le checksum sont les seuls à ne pas subir d'opération d'inversion avant d'être insérés dans
unencodedSecret, ils sont gardés tels quels.
Avec le unencodedSecret défini plus haut, le calcul du checksum devient le
suivant : (011000 + 100011 + 100110 + 110111 + 101011 + 011100 + 111011 + 101111 + 101011 + 001010 + 100111 +
010010 + 110110 + 000000 + 000110 + 000000 + 000000 + 000100 + 000000) & 001111
Le résultat de l'opération ci-dessus donne checksum = 000111
Etape 23 - Le hachage
Cette étape est ce qui va mettre le bordel dans le secret jusqu'ici et ce qui est censé complexifier son décodage. Elle va se dérouler en trois temps :
- Dans un premier temps on va calculer un indice de départ qui va servir pour la suite
- Dans un second temps on va transformer chaque paquet de 6 bits en fonction de l'indice calculé au préalable et d'un tableau fixé dans la ROM du jeu (c'est la phase de hachage)
- Enfin, on va opérer une transformation sur le premier paquet pour y insérer le cipherKey
1. L'indice de départ
Il est calculé à partir du cipherKey, ou plutôt l'inverse du cipherKey si on garde sa définition
telle que dans la première étape de l'algorithme. En d'autres termes, la valeur servant
de base pour ce calcul reprend le résultat obtenu à la deuxième étape de
l'algorithme, appelons-la base pour éviter la confusion. Avec ça, le calcul donnant l'indice de
départ (nommons celui-ci startIndex) est la suivante :
startIndex = base × 4
... oui, c'est tout. Une simple multiplication par 4. On rappelle que la base est
tout comme le cipherKey une valeur codée sur 3 bits, donc ayant une valeur de 7 maximum. Par conséquent, l'indice de
départ a une valeur de 28 maximum.
Au fait, pourquoi je nomme cette valeur "indice de départ" depuis tout à l'heure ? C'est ce qu'on va voir dans le second temps de cette étape.
2. La fonction de hachage
A partir de là, on va boucler sur chaque paquet de 6 bits et faire une opération XOR avec une valeur issue d'une table de hachage fixée dans la ROM qu'on va également parcourir incrémentalement et dont la première valeur est prise à l'indice défini par le résultat obtenu précédemment. Le résultat de cette opération donnera 20 nouveaux paquets, cette fois-ci hachés. On va faire une opération très spécifique décrite plus bas sur le premier de ces 20 paquets hachés, puis à la fin tous seront encodés en caractères lisibles pour le joueur ; mais ça c'est l'étape d'après.
La table de hachage mentionnée précédemment est, comme la table d'encodage des caractères définie plus haut, dépendante de la version du jeu, version japonaise ou version occidentale (européenne / américaine). Voici donc ci-dessous la table de hachage dans les deux versions représentant les valeurs et leurs index :
| Home | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| JP | 49 | 9 | 41 | 59 | 24 | 60 | 23 | 51 |
| EUR/US | 21 | 35 | 46 | 4 | 13 | 63 | 26 | 16 |
| Home | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| JP | 53 | 1 | 11 | 10 | 48 | 33 | 45 | 37 |
| EUR/US | 58 | 47 | 30 | 32 | 15 | 62 | 54 | 55 |
| Home | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| JP | 32 | 58 | 47 | 30 | 57 | 25 | 42 | 6 |
| EUR/US | 9 | 41 | 59 | 49 | 2 | 22 | 61 | 56 |
| Home | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
| JP | 4 | 21 | 35 | 46 | 50 | 40 | 19 | 52 |
| EUR/US | 40 | 19 | 52 | 50 | 1 | 11 | 10 | 53 |
| Home | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
| JP | 16 | 13 | 63 | 26 | 55 | 15 | 62 | 54 |
| EUR/US | 14 | 27 | 18 | 44 | 33 | 45 | 37 | 48 |
| Home | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
| JP | 56 | 2 | 22 | 61 | 44 | 14 | 27 | 18 |
| EUR/US | 25 | 42 | 6 | 57 | 60 | 23 | 51 | 24 |
Dans mon exemple la valeur de la base est 011 (3 en décimal). Dans ce
cas, la valeur de l'indice de départ startIndex vaut 3 × 4 = 12, ce qui signifie
qu'on va multiplier le premier paquet par la valeur à l'index 12 du tableau ci-dessus, puis le 2e paquet par la
valeur à l'index 13, et ainsi de suite jusqu'au 20e paquet.
Pour rappel, mon unencodedSecret vaut 011000 100011 100110 110111 101011
011100 111011 101111 101011 001010 100111 010010 110110 000000 000110 000000 000000 000100 000000 000111.
Afin de simplifier la représentation des calculs qu'on va mener on va d'abord convertir ces paquets de 6 bits en
décimal (même si la base dans laquelle sont écrits les nombres ne change strictement rien au calcul) : 24 35
38 55 43 28 59 47 43 10 39 18 54 0 6 0 0 4 0 7. Maintenant, considérant qu'on est sur une version japonaise,
alors le hachage va ressembler à ce qui suit :
24 XOR 48 = 4035 XOR 33 = 238 XOR 45 = 1155 XOR 37 = 1843 XOR 32 = 1128 XOR 58 = 3859 XOR 47 = 2047 XOR 30 = 4943 XOR 57 = 1810 XOR 25 = 1939 XOR 42 = 1318 XOR 6 = 2054 XOR 4 = 500 XOR 21 = 216 XOR 35 = 370 XOR 46 = 460 XOR 50 = 504 XOR 40 = 440 XOR 19 = 197 XOR 52 = 51
3. Incrustation du cipherKey dans le premier paquet
A cette étape, on a presque nos 20 valeurs prêtes à être encodées, mais il ne reste une dernière
opération à effectuer sur le tout premier paquet afin de le changer encore une fois. Cette opération servira lors de
l'étape du décodage du secret, car elle permettra de retrouver la valeur du cipherKey grâce à
laquelle va démarrer l'algorithme de décodage. En reprenant base comme défini au-dessus et en
définissant secret[0] le hash du premier paquet réalisé juste au-dessus, l'opération est la
suivante :
secret[0] = (secret[0] & 7) | (base << 3)
L'opération est ici simple à comprendre : on tronque les 6 bits du premier paquet obtenu pour
ne garder que les 3 bits de poids le plus faible et via l'opération OR et le shift de 3 bits vers la gauche, on
colle la base qui elle fait 3 bits au niveau des poids les plus forts, ce qui donne un paquet de 6 bits
avec l'inverse du cipherKey encodée dans ses 3 bits de poids fort. Ainsi quand on voudra retrouver cette valeur lors
du processus de décodage, il suffira de regarder les 3 premiers bits du secret.
Pour conclure, les 20 paquets ayant maintenant chacun leur hash, on obtient le résultat suivant
(en décimal) : 24 2 11 18 11 38 20 49 18 19 13 20 50 21 37 16 50 44 19 51.
Etape 24 - L'encodage
Dans cette dernière étape, on va transformer les 20 paquets que l'on vient de calculer en caractères afin de les afficher pour que le joueur puisse les lire et les noter. Cette conversion se fait sur la base d'une table (eh oui, encore une !) fixée dans la ROM du jeu, qui n'est pas la même que celle pour les noms.
Cette table comporte cette fois 64 caractères, et une fois de plus diffère selon si on est sur la ROM japonaise ou une ROM européenne / américaine. Ci-dessous la table représentant cet encodage dans les deux versions :
| Home | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| JP | え | か | く | 0 | け | つ | 1 | し |
| EUR/US | B | D | F | G | H | J | L | M |
| Home | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| JP | に | ね | そ | ぺ | 2 | た | せ | い |
| EUR/US | ♠ | ♥ | ♦ | ♣ | # | N | Q | R |
| Home | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| JP | て | み | ほ | す | う | お | ら | の |
| EUR/US | S | T | W | Y | ! | ● | ▲ | ■ |
| Home | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
| JP | 3 | ふ | さ | ざ | き | ひ | わ | や |
| EUR/US | + | - | b | d | f | g | h | j |
| Home | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
| JP | こ | は | ゆ | よ | へ | る | な | と |
| EUR/US | m | $ | * | / | : | ~ | n | q |
| Home | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
| JP | 5 | 6 | 7 | を | ぷ | も | め | り |
| EUR/US | r | s | t | w | y | ? | % | & |
| Home | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
| JP | ち | ま | あ | ん | ぞ | れ | 8 | ご |
| EUR/US | ( | = | ) | 2 | 3 | 4 | 5 | 6 |
| Home | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
| JP | ど | む | ぴ | 9 | 4 | ぼ | が | だ |
| EUR/US | 7 | 8 | 9 | ↑ | ↓ | ← | → | @ |
A l'étape précédente, nous nous sommes retrouvés avec les 20 paquets suivants : 24 2 11 18
11 38 20 49 18 19 13 20 50 17 37 42 51 44 27. Pour rappel, ils ont été obtenus sur la base de la version
japonaise, donc pour les convertir en caractères on va utiliser la ligne correspondante. Ceci donne les caractères
suivants : 3くぺほぺ なうまほす たうあおる めあぷすん.
L'algorithme des secrets des anneaux
Si vous avez lu l'entièreté de l'algorithme précédent, alors déjà GG à vous parce que c'était sans doute très long, et surtout vous allez voir que les deux prochains algorithmes qu'on va traiter sont infiniment plus simples !
Le principe du secret des anneaux étant déjà expliqué plus haut, on ne va pas détailler de nouveau comment ça fonctionne, mais ce qu'on peut dire c'est que la seule information qui nous intéresse pour générer ce secret est : quels anneaux sont en notre possession. Pour ceci, rien de plus simple : il existe dans la ROM du jeu un espace mémoire* de 64 bits qui va coder sur chacun d'entre eux si l'on possède ou non l'anneau à cet index.
* En assembleur Gameboy, une case mémoire a plutôt une unité en bytes qu'en bits, ce qui veut dire qu'on manipule les données 8 bits par 8 bits. Ainsi, il serait plus correct de dire que l'espace mémoire donc je parle fait 8 bytes, même si dans le fond ça ne change rien à la suite de l'algorithme ; en revanche ça peut aider à comprendre pourquoi on va découper les données comme on va le faire.
Dans cet espace mémoire, le bit correspondant à chacun des 64 anneaux est rangé du bit le plus faible au bit le plus fort dans le même ordre que celui affiché par le jeu. Ainsi, l'Anneau Amitié correspond au bit de poids 1, puis l'anneau Force N-1 correspond à au bit de poids 2, etc. jusqu'à l'Anneau Protection qui correspond au bit de poids 64. Chaque bit valant 1 si l'anneau est en notre possession, et 0 sinon (les anneaux non évalués ne comptent pas, il faut que l'anneau apparaisse dans la liste de Vasu).
Avec ceci en tête, on a donc 64 bits, nommons rings ce nombre. on va le découper en
8 paquets de 8 bits avec la définition suivante :
ring[i] = (rings >> i) & $FF, i ∈ [0,7]
Ainsi on obtient ring[0] qui va représenter les 8 bits de poids faible de
rings (et qui encode la possession des anneaux numérotés de 1 à 8), puis ring[1] les 8
bits de poids suivants (qui encode la possession des anneaux de 9 à 16), etc. jusqu'à ring[7] qui
désigne les 8 bits de poids le plus fort (qui encode la possession des anneaux de 57 à 64). On est fin prêts pour
l'algorithme.
- On calcule une valeur codée sur 3 bits à partir du gameID. On appellera cette valeur cipherKey
-
On inverse les bits du cipherKey obtenu à l'étape ci-dessus et on le stocke dans
unencodedSecret(3 bits) -
On concatène le nombre binaire
01àunencodedSecret(2 bits) -
On inverse les bits du GameID et on les concatène à
unencodedSecret(15 bits) -
On inverse les 8 bits composant
ring[1]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[5]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[7]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[3]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[0]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[4]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[2]et on les concatène àunencodedSecret(8 bits) -
On inverse les 8 bits composant
ring[6]et on les concatène àunencodedSecret(8 bits) -
On divise le
unencodedSecretainsi obtenu (84 bits à cette étape) en 14 paquets de 6 bits -
On fait un calcul de checksum sur 6 bits en fonction des 14 valeurs obtenues à l'étape
précédente, et on l'ajoute en tant que 15e élément à
unencodedSecret - On applique une fonction de hachage sur chacun des 15 paquets obtenus ci-dessus à partir d'un array fixe codé dans la ROM
- On convertit les 15 valeurs hashées en caractères lisibles par le joueur en fonction d'un autre array fixe codé dans la ROM
A propos de l'algorithme ci-dessus
Tout comme l'algorithme précédent, il est possible que les étapes ci-dessus diffèrent
légèrement de ce qui est fait dans le jeu. La source qui m'a permis d'identifier ces étapes est la même que
précédemment, à savoir le code
source de Zora Sharp, qui implémente la logique de la plupart des générateurs de secrets existant
actuellement pour OOS/OOA. Pour vous prouver que cet algorithme fonctionne, on va également le tester avec les
informations relatives à ma partie : à l'issue de chaque étape, je donnerai la valeur calculée jusque là de
unencodedSecret, et si à la fin après encodage du tout on retombe sur mon secret, ça devrait être
convaincant.
Résumons les données des anneaux que je vais encoder ci-dessous :
- Ma partie est sur la version japonaise
- Mon GameID vaut 27874 (en valeur décimale)
-
Je possède les anneaux suivants :
- 01 - Anneau Amitié
- 02 - Anneau Force N-1
- 03 - Anneau Force N-2
- 05 - Anneau Armure N-1
- 09 - Anneau Bleu
- 12 - Anneau Expert
- 16 - Anneau Maple
- 18 - Anneau Pégase
- 19 - Anneau Lancer
- 21 - Anneau Coeur N-2
- 24 - Anneau Lumière N-1
- 27 - Anneau Chance Vert
- 29 - Anneau Chance Or
- 30 - Anneau Chance Rouge
- 31 - Anneau Sacré Vert
- 32 - Anneau Sacré Bleu
- 33 - Anneau Sacré Rouge
- 35 - Anneau de Roc
- 41 - Anneau Découverte
- 45 - Anneau Like-Like
- 49 - Anneau Res. Bombes
- 53 - Anneau Tueur
- 60 - Anneau Paix
- 61 - Anneau Zora
- 62 - Anneau Poing
- 63 - Anneau Fantasque
Pour éviter la redondance
Beaucoup d'étapes ci-dessous sont exactement les mêmes que celles déjà décrites plus haut. Afin d'éviter de réécrire
les mêmes paragraphes, je me contenterai simplement d'un lien amenant vers les paragraphes
au-dessus.
Etape 1 - Création du cipherKey
Etape 2 - Inversion du cipherKey et début de l'encodage
Voir Inversion du cipherKey et début de l'encodage
A ce stade, unencodedSecret = 011
Etape 3 - Le type de secret
Cette étape ajoute les deux bits de valeur 01 à unencodedSecret. La
valeur 01 correspond au type de secret des anneaux.
A ce stade, unencodedSecret = 011 01
Etape 4 - Le GameID
A ce stade, unencodedSecret = 011 01 010001110011011
Etapes 5 à 12 - La possession des anneaux
C'est la partie intéressante de l'algorithme du secret des anneaux. Comme on l'a expliqué plus haut, chaque groupe de 8 anneaux est encodé sur 8 bits, chacun valant 1 si l'on possède l'anneau et 0 sinon. Dans mon cas, rappelons les anneaux que je possède :
- 01 - Anneau Amitié
- 02 - Anneau Force N-1
- 03 - Anneau Force N-2
- 05 - Anneau Armure N-1
- 09 - Anneau Bleu
- 12 - Anneau Expert
- 16 - Anneau Maple
- 18 - Anneau Pégase
- 19 - Anneau Lancer
- 21 - Anneau Coeur N-2
- 24 - Anneau Lumière N-1
- 27 - Anneau Chance Vert
- 29 - Anneau Chance Or
- 30 - Anneau Chance Rouge
- 31 - Anneau Sacré Vert
- 32 - Anneau Sacré Bleu
- 33 - Anneau Sacré Rouge
- 35 - Anneau de Roc
- 41 - Anneau Découverte
- 45 - Anneau Like-Like
- 49 - Anneau Res. Bombes
- 53 - Anneau Tueur
- 60 - Anneau Paix
- 61 - Anneau Zora
- 62 - Anneau Poing
- 63 - Anneau Fantasque
Ainsi, si l'on convertit ces informations dans les 64 bits dont je parlais précédemment on
obtient le nombre binaire suivant : 0111100000010001000100010000010111110100100101101000100100010111
(les premiers anneaux sont dans les bits de poids faible et les derniers dans les bits de poids fort). En découpant
dans les ring[i] définis plus haut, cela donne :
ring[0] = 00010111ring[1] = 10001001ring[2] = 10010110ring[3] = 11110100ring[4] = 00000101ring[5] = 00010001ring[6] = 00010001ring[7] = 01111000
Par la suite, chacun des ring[i] va être inversé puis rajouté à
unencodedSecret dans cet ordre : ring[1] > ring[5] > ring[7] > ring[3] > ring[0] > ring[4] >
ring[2] > ring[6].
A ce stade, unencodedSecret = 011 01 010001110011011 10010001 10001000 00011110 00101111
11101000 10100000 01101001 10001000
Etape 13 - Division en paquet de 6 bits
Voir Division en paquet de 6 bits
A ce stade, unencodedSecret = 011010 100011 100110 111001 000110 001000 000111 100010
111111 101000 101000 000110 100110 001000
Etape 14 - Calcul du checksum
A ce stade, unencodedSecret = 011010 100011 100110 111001 000110 001000 000111 100010
111111 101000 101000 000110 100110 001000 000110
Etape 15 - Le hachage
A ce stade, on a obtenu les hashes suivants : 26 2 11 28 38 50 40 60 6 49 2 0 34 29
37
Etape 16 - L'encodage
Via le tableau d'encodage défini précédemment, on obtient le secret suivant :
さくぺきな あ541ま くえゆひる
L'algorithme des secrets des bonus
Ce secret est encore plus simple que les précédents, car la quantité d'information à encoder est d'autant plus réduite. On le disait plus haut, il y a 10 secrets de cette sorte en tout dans une partie liée (et 8 secrets retour mais ils y sont fortement liés, on va voir comment ci-dessous), et chacun de ces secrets possède un identifiant entre 0 et 9 qu'on listera plus loin.
Vous vous en doutez donc, l'identifiant de chacun de ces secrets va être utilisé pour générer et décoder la chaîne de caractères. Comme pour les secrets précédents, le GameID va également être utilisé afin de ne rendre le secret utilisable que dans une seule lignée de parties. Ci-dessous l'algorithme d'encodage des secrets des bonus.
- On calcule une valeur codée sur 3 bits à partir du gameID et du secret actuel. On appellera cette valeur cipherKey
-
On inverse les bits du cipherKey obtenu à l'étape ci-dessus et on le stocke dans
unencodedSecret(3 bits) -
On concatène le nombre binaire
11àunencodedSecret(2 bits) -
On inverse les bits du GameID et on les concatène à
unencodedSecret(15 bits) -
On prend la valeur correspondant au secret, on inverse ses bits et on les concatène à
unencodedSecret(4 bits) -
On divise le
unencodedSecretainsi obtenu (24 bits à cette étape) en 4 paquets de 6 bits -
On fait un calcul de checksum sur 6 bits en fonction des 4 valeurs obtenues à l'étape
précédente, et on l'ajoute en tant que 5e élément à
unencodedSecret - On applique une fonction de hachage sur chacun des 5 paquets obtenus ci-dessus à partir d'un array fixe codé dans la ROM
- On convertit les 5 valeurs hashées en caractères lisibles par le joueur en fonction d'un autre array fixe codé dans la ROM
A propos de l'algorithme ci-dessus
Tout comme les algorithmes précédents, il est possible que les étapes ci-dessus diffèrent
légèrement de ce qui est fait dans le jeu. La source qui m'a permis d'identifier ces étapes est la même que
précédemment, à savoir le code
source de Zora Sharp, qui implémente la logique de la plupart des générateurs de secrets existant
actuellement pour OOS/OOA. Pour vous prouver que cet algorithme fonctionne, on va également le tester avec les
informations relatives à ma partie : à l'issue de chaque étape, je donnerai la valeur calculée jusque là de
unencodedSecret, et si à la fin après encodage du tout on retombe sur mon secret, a priori c'est
bon.
Résumons les données que je vais encoder ci-dessous :
- Ma partie est sur la version japonaise
- Mon GameID vaut 27874 (en valeur décimale)
- Le secret à encoder est celui que me donne Tingle dans OOA après lui avoir communiqué un secret reçu dans OOS
Etape 1 - Création du cipherKey
L'étape de création du cipherKey diffère par rapport à celle des autres types de secrets, car la valeur correspondant au secret actuel, l'origine de ce secret, et si c'est un secret retour ou non entrent dans son calcul.
Avant de calculer le cipherKey, on va calculer une autre valeur qu'on va appeler
cipherBase. Pour ce faire, on va d'abord établir une valeur en fonction de deux
critères : de quel jeu (OOS ou OOA) est généré le secret, et est-ce que c'est un secret retour ou non. Résumons les
différentes valeurs possibles dans le tableau ci-dessous :
| OOS | OOA | |
|---|---|---|
| Secret aller | 0 | 1 |
| Secret retour | 3 | 2 |
On obtient alors une valeur codée sur 2 bits, à laquelle on va ajouter un bit de poids fort
calculé par rapport à la valeur de l'identifiant du secret en cours. Plus précisément, on va garder le bit qui donne
la parité de cet identifiant : s'il est pair, alors le bit vaudra 0, et s'il est impair alors le bit vaudra 1.
Mathématiquement, en posant secretID l'identifiant du secret, cela se traduit par la formule
suivante :
cipherKey = cipherBase | ((secretID & $01) << 2)
Avec les informations que j'ai données plus haut, j'ai récupéré mon secret dans OOA et c'est un
secret retour, donc cipherBase = 2 (ce qui donne 10 en binaire). Par ailleurs, on va le
voir un peu plus bas, l'identifiant de ce secret est secretID = 7. Ainsi, on
trouve une valeur de cipherKey = 110
Etape 2 - Inversion du cipherKey et début de l'encodage
Voir Inversion du cipherKey et début de l'encodage
A ce stade, unencodedSecret = 110
Etape 3 - Le type de secret
Cette étape ajoute les deux bits de valeur 11 à unencodedSecret. La
valeur 11 correspond au type de secret des bonus.
A ce stade, unencodedSecret = 110 11
Etape 4 - Le GameID
A ce stade, unencodedSecret = 110 11 010001110011011
Etape 5 - L'identifiant du secret
Chacun des 10 secrets disponibles dans une partie en jeu lié possède un identifiant entre 0 et
9. Le secret retour correspondant à un secret possède le même identifiant. On a déjà utilisé l'identifiant lors du
calcul du cipherKey plus haut, mais c'est surtout à cette étape de l'algorithme qu'il va être intégré à
unencodedSecret.
Il y a donc 10 secrets (+ 8 retour) dans le sens OOS > OOA, et de la même manière 10 secrets (+ 8 retour) dans le sens OOA > OOS. Le tableau ci-dessous répertorie ces secrets dans les deux sens avec le personnage destinataire du secret aller (qui est à l'origine du secret retour quand il y en a un) :
| Identifiant | Sens OOA > OOS | Sens OOS > OOA | A un secret retour ? |
|---|---|---|---|
| 0 | Le vieux sous l'horlogerie | Roi Zora | ✓ |
| 1 | Ghini sous la tombe du cimetière | Fée égarée de la Forêt des Fées | ✓ |
| 2 | Subrosien sous les 3 volcans au nord de Subrosia | Troy (au niveau du mini-jeu Cible Chario à Rouleroche) | ✓ |
| 3 | Maître Plongeur à la Cité Engloutie | Maire Plenn | ✗ |
| 4 | Forgeron Subrosien | Vieux au fond de la Bibliothèque de l'Île Monocle | ✓ |
| 5 | Pirate resté dans la maison au sud de Subrosia | Tokay du Musée du Tokay Sauvage | ✓ |
| 6 | Grande Fée au fond du Temple des Saisons | Madame Yan au sud de la Cité Lynne | ✗ |
| 7 | Peste Mojo à l'ouest de la Cité Engloutie | Tingle | ✓ |
| 8 | Biggoron | Ancêtre Goron au Stand de Tir de Rouleroche | ✓ |
| 9 | Maire Ruul | Soeurs de la Maison Moyenne la Cité Symétrie | ✓ |
Par exemple, le secret que l'on doit donner à Tingle (dans OOA) nous est donné dans par une Grande Fée dans une grotte près du donjon 2 de OOS. Le secret qui nous est communiqué par cette dernière a pour identifiant 7 selon le tableau ci-dessus. Une fois ce secret communiqué à Tingle et l'augmentation de graines mystiques obtenue, il va nous donner un secret retour à aller communiquer à Farore (dans OOS) pour obtenir le même bonus. Ce nouveau secret a également 7 pour identifiant.
Cette valeur, codée sur 4 bits, les voit ensuite se faire inverser puis ajouter à
unencodedSecret.
A ce stade, unencodedSecret = 110 11 010001110011011 1110
Etape 6 - Division en paquet de 6 bits
Voir Division en paquet de 6 bits
A ce stade, unencodedSecret = 110110 100011 100110 111110
Etape 7 - Calcul du checksum
Le calcul du checksum pour les secrets des bonus est presque le même que pour les autres types de secret, à ceci près que la dernière étape change.
Dans un premier temps, on somme tous les paquets déjà calculés et on tronque le résultat pour ne garder que les 4 bits de poids faible. Cette partie est identique à tous les autres secrets.
Ce qui change, ce sont les deux bits de poids fort. Dans les autres types de secret, il était
laissé à 00, mais cette fois-ci, on prend la valeur issue de ce tableau (le jeu
d'origine du secret est-il OOA ou OOS ? Est-ce un secret aller ou un secrer retour ?), qui fait 2 bits maximum, et
on accole ces deux bits au checksum précédemment calculé. Dans notre exemple, le checksum vaut 0101. Le
secret étant un secret retour issu de OOA, alors la valeur des deux bits précédents le checksum est 01.
Finalement, on peut conclure que dans notre exemple, checksum = 010101.
A ce stade, unencodedSecret = 110110 100011 100110 111110
010101
Etape 8 - Le hachage
A ce stade, on a obtenu les hashes suivants : 50 54 5 16
31
Etape 9 - L'encodage
Via le tableau d'encodage défini précédemment, on obtient le secret suivant :
あ8つてや
Le décodage
Dans ce paragraphe je ne vais pas détailler tout l'algorithme de décodage car c'est grosso modo exactement les mêmes étapes que l'encodage des secrets mais en inversé. Ce qui mérite d'être expliqué, c'est comment cet algorithme est démarré et ça pourra potentiellement aider à comprendre pourquoi le secret est construit comme il l'est.
Il faut savoir que le démarrage de l'algorithme de décodage est le même quelque soit le type de secret, par conséquent l'explication qui suit est valide pour tout ce qu'on a vu au-dessus.
Revenons un instant sur la partie qui modifie le premier paquet de 6 bits lors de l'opération de hachage appliquée sur chacun des paquets composant le secret. Cette opération consiste à insérer le cipherKey* dans les 3 bits de poids fort de ce premier paquet.
* On expliquait plus haut que ces trois bits ne constituent pas exactement le cipherKey tel qu'on l'a défini, mais son inverse. Or à une opération inverse près, c'est le même nombre, par consqéuent par souci de simplicité dans les prochains paragraphes on ne va pas s'embêter et juste le nommer cipherKey.
Le fait qu'on ait la garantie que le cipherKey se retrouve dans les trois premiers bits est la clé du décodage : en effet, c'est grâce à lui qu'on va pouvoir calculer l'indice de départ, qui sera exactement le même que celui calculé lors de l'encodage. Chaque caractère du secret étant ré-encodé en nombre selon ce tableau, on va pouvoir procéder à la même opération que celle effectuée lors du hachage.
La raison pour laquelle on refait exactement la même opération que lors de l'encodage vient
d'une propriété de la fonction logique XOR : en effet, pour n'importe quels entiers
A, B et C, l'équivalence suivante est toujours vraie :
A XOR B = C ⇔ A XOR C = B ⇔ B XOR C = A
En d'autres termes, lors de l'encodage, disons que pour chaque bloc on avait A la
valeur brut du paquet de 6 bits avant le hachage, B la valeur prise de la table de hachage, et
C le résultat de l'opération A XOR B qui a permis de trouver l'index du caractère à
encoder dans le tableau d'encodage final. Dans le cas du décodage, grâce à l'indice de départ calculé via le
cipherKey qu'on a retrouvé, on a B, et C nous a été donné en ré-encodant le caractère
courant en nombre. Ainsi il devient facile de retrouver A et ce pour tous les paquets de 6 bits, ce qui
permet de décoder chacun d'entre eux. A partir de là, il suffit de retravailler le découpage des bits afin d'isoler
chaque information.
Pour procéder par l'exemple, reprenons le secret des parties que j'avais calculé au-dessus. Nous
avions les caractères suivants : 3くぺほぺ なうまほす たうあおる めあぷすん. Le premier caractère une fois
ré-encodé donne le nombre 24, soit 011000 en binaire. En isolant les 3 bits de poids fort, on obtient un cipherKey
valant 011 en binaire, donc 3 en décimal, ce qui donne un indice de base de 3 × 4 = 12. Ainsi, on va démarrer
à l'index 12 de la table de hachage, et on va ré-encoder le reste des caractères du
secret en nombre.
Prenons les 5 premiers caractères pour faire court : 24 2 11 18 11 après avoir
juste ré-encodé les caractères en nombres. Les 5 premières valeurs de la table de hachage en partant de l'index 12
sont les suivants : 48 33 45 37 32. On peut alors faire l'opération XOR dans ce
sens :
24 XOR 48 = 402 XOR 33 = 3511 XOR 45 = 3818 XOR 37 = 5511 XOR 32 = 43
Si vous remontez à la fin de ce paragraphe, alors vous verrez qu'à part la première valeur on retombe sur les valeurs qu'on avait à l'issue de notre hachage ! La première valeur est différente car une opération spéciale avait été effectuée sur elle, par conséquent il faut refaire l'opération inverse pour retrouver les 6 premiers bits d'origine :
firstValue = (firstValue & $07) | (cipherKey << 3)
En effet, à l'origine les 6 premiers bits sont composés des 3 bits du cipherKey, puis 3 bits de
poids faible. L'opération XOR au-dessus, bien qu'on l'a représentée effectuée sur chaque paquet de 6 bits, et en
réalité une opération bit par bit, et dans ce cas précis elle a reconstitué les 3 bits de poids faible qui
n'avaient, eux, pas été modifiés. Par conséquent, il s'agit simplement de remplacer les 3 bits de poids fort par le
cipherKey, et on revient sur la définition d'origine des premiers bits du unencodedSecret qu'on avait
défini lors des deux premières étapes d'encodage.
Dans notre exemple précédent, on avait comme première valeur 40, donc 101000 en binaire, donc
l'opération ci-dessus donne ceci : (101000 & 7) | (011 << 3) = 011000 et on retombe sur nos
pattes.
Avec ceci, on a décodé l'entièreté de notre secret et reconstitué les 120, 90, ou 30 bits qui
composaient unencodedSecret. L'étape suivante est la vérification du checksum : on vérifie que la
valeur donnée par les 6 derniers bits correspond bien à la somme de tous les autres paquets tronqués dans leurs 4
derniers bits, et si c'est le cas, le secret est valide et la séquence de décodage peut
continuer.
Annexes
Level up !!
Les sections ci-dessous vont vraiment jusqu'au bout du bout des choses, bien plus que les sections précédentes, car
je vais décortiquer des mécaniques encore plus profondes qui ne sont plus du ressort de la génération des secrets,
bien qu'elles y soient liées. Pour ce faire, j'ai cette fois-ci utilisé le code désassemblé des jeux Oracle, un
travail de longue haleine mené par le speedrunner Stewmath, dont vous pourrez trouver le repo
à cette adresse. Pour pouvoir
comprendre ce code, j'ai dû me former à l'assembleur Gameboy (un dérivé de l'assembleur pour les processeurs Z80),
car les instructions ne parlent pas toujours d'elles-mêmes. Rassurez-vous, je n'attends pas la même chose de votre
part et c'est pourquoi je vais faire mon possible pour vulgariser les morceaux de code étudiés afin de les rendre
compréhensibles pour peu que vous ayez un peu de bagage informatique.
Comment est créé le GameID ?
Le GameID est malheureusement une valeur que l'on ne peut pas deviner à moins de voir notre premier secret. Déjà parce qu'il est déterminé au moment de la génération du premier secret, mais également car, contrairement à certaines idées reçues, il ne dépend pas d'informations comme le nom du héros, le nom de l'enfant, ou d'autres données propres à une partie, mais uniquement via un compteur caché de temps de jeu.
Le code qui génère le GameID est trouvable à cet emplacement. Il se divise en 4 parties distinctes que je vais détailler ci-dessous.
1. Vérification du GameID
Cette étape se concentre sur le morceau de code suivant :
ld hl,wGameID
ldi a,(hl)
or (hl)
ret nz
Ce que ce code fait, c'est de charger la valeur du GameID stockée dans un espace mémoire* fixé dans la WRAM (= Working RAM ou Work RAM selon les usages), afin de les charger dans les registres de travail. En assembleur, on ne peut pas manipuler des données simplement, il faut d'abord les charger dans un registre et c'est ce qui est fait ici.
* Si ça vous intéresse, l'espace mémoire réservé au GameID est composé des deux adresses $C600 et $C601. Une adresse mémoire correspond à un byte (= 8 bits) de données, par conséquent le GameID est techniquement codé sur 16 bits, et non 15 comme je vous l'ai répété depuis le début 🤯. Pas d'inquiétude, je ne vous ai pas menti, et on va voir pourquoi dans la suite !
Une fois la valeur chargée, on vérifie simplement si elle vaut 0 ou non : si non, alors ça veut dire que le GameID est déjà déterminé donc on n'a pas besoin de le générer. Dans ce cas on termine la subroutine ici. S'il vaut 0, alors ça veut dire que ce n'est pas terminé et donc on avance à la suite.
2. Définition via le compteur de temps de jeu
Cette étape se concentre sur le morceau de code suivant :
ld l,<wPlaytimeCounter+1
ldd a,(hl)
and $7f
ld b,a
ld a,(hl)
jr nz,+
Avant de démarrer dans le dur de cette partie du code, intéressons-nous un instant au compteur de temps de jeu. C'est une information stockée dans la WRAM sur 4 bytes*, qui se voit incrémentée à chaque frame du jeu (la Gameboy tourne à environ 59.7 frames par seconde).
* 4 bytes, c'est en vérité énorme. On parle de de 4 × 8 = 32 bits de
données, en d'autres termes un nombre pouvant aller jusqu'à 4 294 967 295 (oui, QUATRE MILLIARDS). Je ne sais
pas si ce compteur est réinitialisé à chaque redémarrage de la console, mais en supposant que c'est le cas, et
en considérant qu'il est incrémenté à chaque frame, il faudrait laisser allumée la console pendant 2 ans et un
peu plus de 3 mois non stop pour la voir atteindre sa valeur maximale.
Autre détail pour le coup complètement superflu, il existe un autre compteur, nommé frameCounter dans le code
sur lequel je m'appuie, qui lui ne fait que 1 byte (valeur maximum 255), dans lequel est stocké à chaque frame
la valeur du byte le plus faible du compteur de temps de jeu après avoir été incrémentée. Pour les curieux, le
frameCounter est stockée dans l'adresse mémoire $CC0, tandis que le compteur de temps de jeu (nommé
playtimeCounter) est stocké dans les 4 adresses s'étendant de $C622 à $C625. La suite d'instructions qui
incrémente les deux compteurs peut se lire
à cet
endroit.
Les instructions au-dessus découpent les 2 bytes de poids faible du compteur de temps de jeu et
les stockent dans deux registres différents. Ce sont les deux candidats pour constituer les 15 bits de notre GameID.
Une opération AND est appliquée sur les 8 bits de poids fort de cet ensemble avec l'opérande $7F : on
l'a vu précédemment dans les algorithmes d'encodage des secrets, une telle opération est souvent faite pour tronquer
une valeur et ne garder que certains de ses bits, et c'est encore une fois le cas ici, car
7F(16) = 01111111(2), par conséquent les 7 bits de poids faible sont conservés et
le bit de poids fort est mis à 0.
Par la suite, ce qu'on va faire va changer en fonction de si le résultat de l'opération AND ci-dessus vaut 0 ou non. Si vous connaissez l'expression "code spaghetti", et que vous savez que ce n'est pas une bonne pratique chez les développeurs, sachez qu'en assembleur on n'a pas vraiment le choix vu que les conditions ou les boucles n'existent pas 😅. C'est pourquoi on définit des morceaux de codes vers lesquels on va "jump" en fonction de la valeur calculée précédemment.
3. Définition via le registre DIV
On ne va à cette étape que si le résultat du calcul précédent valait 0. Pour rappel, on avait isolé les deux bytes de poids faible du compteur de temps de jeu, et tronqué celui du poids le plus fort des deux pour n'en garder que les 7 derniers bits. C'est seulement s'ils valent tous 0 qu'on rentre dans le morceau de code suivant :
--
or a
jr nz,+
ld a,($ff00+R_DIV)
jr --
A ce stade, les 7 bits candidats pour être les 7 bits de poids fort du GameID sont nuls. Or vous le savez, on veut éviter que le GameID soit nul. C'est pourquoi on va dans un premier temps tester si les 8 bits candidats pour être les 8 bits de poids faible du GameID sont également nuls ou non. S'ils ne sont pas nuls, alors ça veut dire que le GameID ne sera pas nul, et on peut donc d'ores et déjà arrêter la présente suite d'instructions et se rendre directement à la prochaine car ce qui suit ne nous servira à rien.
Si en revanche ces 8 bits valent également 0, alors dans ce cas on va charger une valeur à partir d'un registre assez particulier qui n'est plus cette fois-ci tiré de la WRAM de la console, mais d'une adresse située dans une zone mémoire réservée à tout ce qui touche à l'entrée/sortie de la Gameboy, et plus précisément à l'horloge de son processeur.
Le registre en question s'appelle DIV pour "Divider register". C'est une valeur codée sur 8 bits qui a priori ne dépend pas du code du jeu, mais qui est purement matérielle, et qui est incrémentée à une fréquence de 16384Hz sur une Gameboy classique (ce nombre dépend du hardware de la console et les émulateurs tentent évidemment de s'approcher de cette valeur). En d'autres termes, cette valeur est modifiée environ toutes les 61 microsecondes (et donc vous vous doutez bien que pour un jeu qui tourne à 59.7 FPS pas toutes ses valeurs seront captées 😄).
C'est donc cette valeur qui va être stockée dans le registre de calcul en cours, et si par extrême malchance les 8 bits valent encore une fois tous 0, alors on recommence la procédure jusqu'à ce qu'il y en ait au moins 1 différent de 0. Une fois que c'est le cas, on peut passer à la suite.
4. Ecriture du GameID
Pour récapituler, on est arrivés à ce stade si le byte de poids fort candidat pour représenter les 7 bits de poids fort du GameID est non nul (indépendamment des 8 bits de poids faible candidats), ou alors s'il était nul mais en s'étant assuré que le byte représentant les 8 bits de poids faibles candidats, lui, était non nul. Dans tous les cas, on est fin prêts pour arriver sur les instructions suivantes :
+
ld l,<wGameID
ldi (hl),a
ld (hl),b
ret
Cette étape est en réalité extrêmement simple et il ne me faudra pas plus d'un paragraphe pour l'expliquer : on a désormais deux bytes candidats, dont l'un fait 7 bits, l'autre 8, et au moins l'un des deux n'est pas nul. Ce qu'on va simplement faire, c'est écrire ces deux bytes aux deux adresses mémoire de la WRAM dans lesquelles va être stocké le GameID, qui le restera tout le long de notre lignée de parties. Il fait bel et bien 15 bits (en réalité 16 mais avec le bit de poids fort mis à 0), et nous nous sommes assurés que sa valeur est non nulle.
Comment influe le nom de l'enfant sur son comportement ?
Dans le tableau décrit à cette étape, la première ligne mentionnait que le nom de l'enfant établissait une valeur de statut entre 1 et 3, mais sans expliciter comment ce calcul était fait. Ce paragraphe est là pour répondre à cette question, et pour ce faire on va une fois de plus regarder du côté du code assembleur, et plus précisément de cette fonction.
1. Initialisation de la somme
Cette étape se concentre sur le morceau de code suivant :
ld hl,wKidName
ld b,$00
C'est une étape très courte : elle charge l'adresse mémoire stockant le nom de
l'enfant*, et initialise à 0 le registre B. Je n'en ai pas parlé jusqu'à présent mais en
assembleur Gameboy, plusieurs registres sont disponibles, et les principaux avec lesquels on va travailler dans
cette partie du code sont le registre A et le registre B. Ici, dites-vous seulement que le registre B correspond à
une variable nommée sum qui pourrait être définie dans un langage de programmation plus haut niveau,
qui va contenir la somme de plusieurs valeurs.
Le nom de l'enfant est stocké dans un espace mémoire de 6 cases, s'étalant de l'adresse $C609 jusqu'à $C60E. Là vous pourrez probablement vous demander pourquoi 6 cases mémoire, vu que le nom ne peut contenir que 5 caractères ? Il s'agit en fait d'une astuce pour quand on va boucler sur les lettres de son nom pour savoir quand s'arrêter, car la 6e valeur est tout le temps censée valoir $00. Je pense que c'est également la raison pour laquelle le tableau des caractères commence à l'index 16 et non 0, ainsi il n'y a pas de confusion. A noter que le nom du héros suit exactement la même logique.
2. Calcul de la somme
Cette étape se concentre sur le morceau de code suivant :
@nextChar:
ldi a,(hl)
or a
jr z,@parsedName
and $0f
add b
ld b,a
jr @nextChar
Elle n'en a pas l'air, mais cette étape construit en réalité une boucle qui va parcourir chaque caractère du nom de l'enfant, avec la condition "tant que le caractère actuel n'est pas nul". En effet, s'il vaut 0, ça signifie qu'on a atteint la fin du nom et qu'on peut donc passer à l'étape suivante, mais dans le cas contraire on peut traiter sa valeur.
Ce que fait cette étape si le caractère n'est pas nul, c'est de d'abord lui appliquer une opération AND pour ne garder que ses 4 bits de poids faible, et de sommer ces 4 bits à la valeur présente dans le registre B. En d'autres termes, cette étape fait la somme des 4 bits de poids faible de chaque caractère du nom de l'enfant et garde le résultat en mémoire.
Dans mon exemple, j'avais appelé l'enfant "マリオ". Si l'on s'en réfère au tableau des noms,
alors on a les correspondances suivantes : マ = 206 = 11001110(2) ;
リ = 215 = 11010111(2) ; オ = 180 = 10110100(2). Ainsi, si on reprend notre notation
sum définie à l'étape précédente qui va garder la somme des 4 bits des valeurs ci-dessus, on a le
calcul suivant :
sum = 0 + (11001110 & 00001111) = 1110sum = 1110 + (11010111 & 00001111) = 1110 + 0111 = 10101sum = 10101 + (11001110 & 00001111) = 10101 + 1110 = 100011
A la fin, on obtient sum = 100011(2) = 35(10). Notons que
bien que les différents caractères se voient tronqués à 4 bits, la somme, elle, ne l'est pas.
3. Conversion en statut
A cette étape, on est passé par chaque caractère du nom de l'enfant et on a obtenu une certaine valeur stockée dans le registre B. Cette valeur a un maximum théorique de 15 × 5 = 75, ce qui est bien plus élevé que le statut initial entre 1 et 3 annoncé ! Le code suivant va justement aider à ramener la valeur actuelle dans cet intervalle :
@parsedName:
ld a,b
--
sub $03
jr nc,--
add $04
ld (wChildStatus),a
ret
Le code ci-dessus se divise en trois temps :
- On va d'abord décrémenter la valeur obtenue précédemment par pas de 3. En d'autres termes on va lui retirer 3 jusqu'à ce qu'elle devienne strictement négative* , et on s'arrête à ce moment-là.
- Ensuite on va simplement ajouter 4 à la valeur négative obtenue. C'est à ce moment que la valeur se cale dans l'intervalle 1-3, et ça va devenir notre valeur définitive pour le statut initial de l'enfant.
- On enregistre la valeur obtenue ci-dessus à l'adresse mémoire de la WRAM qui va stocker l'information du statut, qui sera modifiée par diverses interactions déjà décrites plus haut. Pour les curieux, l'adresse mémoire correspondante est $C60F.
* Quand on parle de bits, la valeur négative est également une notion
abstraite. Il y a plusieurs manières de gérer ça, mais dans notre cas, on va parler d'underflow, c'est-à-dire
qu'à l'issue de la dernière soustraction par 3 on va dépasser 0 par le bas. Concrètement, on revient sur la
valeur maximum du byte. Par exemple, si on enlève 3 au nombre 1, en décimal on obtiendrait -2, mais ici on va
passer par les étapes suivantes : 00000001 → 00000000 → 11111111 → 11111110.
La valeur 11111110 vaut en réalité 254, mais dans ce cas précis elle est interprétée par -2. Avoir un underflow
n'est pas très grave ici, car de toute manière l'incrémentation de 4 à l'étape d'après nous assure de faire un
overflow dans le sens inverse qui nous fait revenir sur des valeurs positives.
Pour conclure avec mon exemple ci-dessus, on avait une valeur de 35 à l'issue de l'étape précédente. Ainsi en décrémentant cette valeur par pas de 3 on va obtenir successivement les valeurs suivantes : 35, 32, 29, ... jusqu'à -2. A la suite de cette étape, on va rajouter 4 et on obtient un statut initial de valeur 2, que l'on va écrire dans la WRAM.
Voilà, grâce à cette explication vous pouvez désormais vous amuser à calculer le statut initial du gamin que vous avez nommé dans votre propre partie ! Ca ne sert pas à grand-chose, quoique ça pourrait éventuellement vous aider si vous visez une certaine évolution de sa personnalité (et encore, 1-3 n'est pas un intervalle très grand et on peut faire sans). Dans tous les cas c'était plutôt fun de se plonger dans ce code 🙂
Conclusion
L'écriture de cette page m'a pris tellement de temps que je me sens obligé de partager quelques dernières remarques 😅. A la base je voulais en faire le premier post du blog du site, mais avec le temps ce qui devait être une simple page est devenu un tel mastodonte que j'en ai créé une section du site dédiée à ce genre de gros dossiers un peu divers qui ne rentrent pas dans les autres sections.
L'écriture de cette page m'a fait me plonger dans du code C# que je n'ai plus croisé en quasi 10 ans, m'a fait découvrir le langage assembleur et ses concepts (et quand bien même les instructions varient en fonction du processeur la logique reste globalement la même, ce qui fait qu'aujourd'hui je peux un peu décrypter le code source de ALTTP ou de OOT, par exemple), et surtout ça m'a fait me plonger dans des concepts très avancés de Oracle of Ages et Oracle of Seasons.
Le système des secrets, le système des anneaux, comment sont gérés les noms ainsi que tous les textes du jeu, le fait qu'on puisse entrer un secret Héros indépendamment dans les deux jeux et que ça va marcher dans tous les cas, le comportement de l'enfant, toutes ses étapes et toutes ses évolutions possibles, la construction de la zone spécifique à notre compagnon qui en réalité change via la valeur qui lui est associée, etc. Tout ceci n'a plus aucun secret pour moi, et j'espère également pour vous désormais, si vous avez lu l'intégralité du dossier !
Si vous voulez aller plus loin, sachez que la méthode de chiffrement des secrets qu'on a décrite au-dessus à un nom : le cipher block, ou chiffrement par bloc en bon français, et plus précisément son mode d'opération Counter (CTR). Comme quoi, les développeurs ne sont pas partis de rien pour concevoir leur système 😁.
Pour conclure, si vous avez lu jusqu'ici, merci d'avoir lu ce dossier, que ce soit en partie ou en intégralité. J'espère que ça vous a intéressé autant que j'ai aimé l'écrire, et si vous avez soif de toujours plus de connaissances je vous laisse vous balader sur le site, il y a plein d'autres pages un peu du même genre 😇.
Sources
Afin d'écrire ce dossier je me suis aidé de multiples sources, en voici la liste exhaustive :
- Code source de Zora Sharp, moteur interne aux divers générateurs de secrets existant : https://github.com/kabili207/zora-sharp/tree/master
- Repo du code des Oracle désassemblé par Stewmath : https://github.com/Stewmath/oracles-disasm/tree/master
- Page du site zeldahacking.net qui décrit de manière très détaillée l'évolution du comportement de l'enfant : https://wiki.zeldahacking.net/oracle/Bipin_and_Blossom%27s_son
- Une autre page du site zeldahacking.net qui donne les secrets universels (avec GameID = 0) : https://wiki.zeldahacking.net/oracle/Universal_secrets
- Article de blog de Stewmath qui s'est un peu amusé avec les secrets (j'y ai tiré quelques informations de comment marche le système de compagnon) : https://stewmath.github.io/breaking-secrets-in-ages-seasons/
- Page détaillant les zones mémoires de la Gameboy, ce qui m'a surtout aidé à comprendre le registre DIV : https://gbdev.io/gb-asm-tutorial/part1/memory.html
- Fiche de référence sur les instructions de l'assembleur Gameboy, ce qui m'a énormément aidé à comprendre le code assembleur du disassembly : https://rgbds.gbdev.io/docs/v0.8.0/gbz80.7
- Tutoriel sur l'assembleur Gameboy qui m'a permis de mieux comprendre les concepts : https://gbdev.io/gb-asm-tutorial/index.html
- Livre au format PDF intitulé Game Boy Assembly Programming for the Modern Game Developer qui m'a aidé à pousser plus loin ma compréhension, notamment des registres, des flags, ou encore du stack pointer : https://github.com/ahrnbom/gbapfomgd
- Documentation Microsoft du langage C# qui m'a pas mal aidé à déchiffrer le code de Zora Sharp et comprendre les conversions en bytes/bits : https://learn.microsoft.com/en-us/dotnet/api/system.text.encoding.getbytes?view=net-8.0#system-text-encoding-getbytes(system-string)
- ZoraGen, l'un des générateurs de secrets existant se basant sur Zora Sharp, qui m'a notamment aidé à obtenir le GameID et la valeur du comportement de l'enfant de ma partie afin de pouvoir construire les exemples pour les algorithmes : https://github.com/kabili207/zoragen-wpf/releases/tag/v2.3.1
- Le ZeldaWiki francophone grâce à qui j'ai pu tirer les noms officiels des anneaux, de certains lieux, etc. : https://zelda.fandom.com/fr/wiki/Anneau_Magique
- Guide de GameFAQs sur les anneaux de OOS/OOA, une vraie mine d'or pour qui s'intéresse au système d'anneaux des jeux : https://gamefaqs.gamespot.com/gbc/198972-the-legend-of-zelda-oracle-of-seasons/faqs/67303
- Page Wikipédia sur la méthode de chiffrement block cipher : https://en.wikipedia.org/wiki/Block_cipher
- Page de The Spriters Resource de laquelle provient le background de cette page : https://www.spriters-resource.com/game_boy_gbc/thelegendofzeldaoracleofseasons/sheet/8964/