Aujourd'hui, je vais détailler le hack de l'écran titre du jeu. Cet article sera constitué de plusieurs parties, je ne dis pas combien car je n'en ai aucune idée. Mais l'objectif affiché est d'être suffisamment précis et clair pour permettre à toute personne disposant d'un petit bagage en romhack de comprendre ce qui se passe. Je m'attends à ce que vous ayez déjà joué avec le debugger de l'excellent FCEUXD-SP, et que des notions comme nametable ou encore PPU sonnent des vos esgourdes. De toute façon, nous reviendrons sur ces notions au fil des articles et des demandes s'il y en a...
Donc l'idée du topic, c'est de voir comment on peut passer de cette image à celle là.
Décodage
Pour ce faire (hommage vibrant à Albie's Hobbies), je me suis basé sur les techniques de hack NES présentées par Graou dans sa
doc très utile.
Rapidement, on peut déterminer que le code chargé de l'initialisation de la
nametable pour l'écran titre se trouve dans la bank
#03 et commence à l'adresse
$880D. Au moment de l'appel, l'Accumulateur contient l'indice du pointeur des données à charger dans la nametable. Le code commenté peut-être visualisé ci-dessous.
Code : Tout sélectionner
$880D 0A ASL ; double la valeur de l'indice du pointeur (car les pointeurs sont codés sur 2 octets)
$880E AA TAX ; placer cette valeur dans X
$880F BD E3 89 LDA $89E3,X ; lecture du pointeur à partir de l'adresse $89E3 indexée par X
$8812 85 00 STA $00 ; sauvegarde du pointeur en RAM à l'adresse $0000
$8814 BD E4 89 LDA $89E4,X ;
$8817 85 01 STA $01 ;
$8819 2C 02 20 BIT $2002 ; ?? si quelqu'un peut m'expliquer l'intérêt de cette instruction ??
$881C A0 00 LDY #$00 ; initialisation de l'index de lecture des données
$881E B1 00 LDA ($00,Y) ; lecture d'un octet à partir de l'adresse pointée sauvegardée en RAM à l'adresse $00
+-------- $8820 F0 52 BEQ $52 ; si l'octet lu == #$00 alors on saute à l'instruction pointée par la flèche
| $8822 C9 FE CMP #$FE ;
| +----- $8824 D0 25 BNE $25 ; sinon si l'octet lu != #$FE alors on saute à l'instruction pointée par la flèche
| | $8826 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| | $8829 B1 00 LDA ($00,Y) ; lecture d'un octet qui correspond au nombre de fois qu'on va écrire le même octet dans la PPU
| | $882B AA TAX ; sauvegarde de cet octet dans le registre X
| | $882C 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| | $882F B1 00 LDA ($00,Y) ; lecture du premier octet d'une adresse PPU
| | $8831 8D 06 20 STA $2006 ; début d'initialisation du pointeur d'écriture dans la PPU
| | $8834 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| | $8837 B1 00 LDA ($00,Y) ; lecture du second octet d'une adresse PPU
| | $8839 8D 06 20 STA $2006 ; fin d'initialisation du pointeur d'écriture dans la PPU
| | $883C C8 INY ; incrémentation de l'index de lecture des données
| | ;(ATTENTION: on remarque ici qu'on ne passe pas par la subroutine $886F)
| | $883D B1 00 LDA ($00,Y) ; lecture d'un octet à partir de l'adresse pointée sauvegardée en RAM à l'adresse $00
| | +-> $883F 8D 07 20 STA $2007 ; écriture de cet octet dans la PPU à l'adresse pointée
| | | $8842 CA DEX ; décrémentation du compteur du nombre d'écriture placé dans le registre X
| | +-- $8843 D0 FA BNE $FA ; tant que X contient une valeur > 0 alors on saute à l'instruction pointée par la flèche
| | $8845 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| | $8848 4C 1E 88 JMP $881E ; on saute à l'instruction $881E
| +----> $884B 85 03 STA $03 ; l'octet lu est sauvegardé en RAM à l'adresse $0003, il indique le nombre d'octets à lire
| $884D 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| $8850 B1 00 LDA ($00,Y) ; lecture du premier octet d'une adresse PPU
| $8852 8D 06 20 STA $2006 ; début d'initialisation du pointeur d'écriture dans la PPU
| $8855 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| $8858 B1 00 LDA ($00,Y) ; lecture du second octet d'une adresse PPU
| $885A 8D 06 20 STA $2006 ; fin d'initialisation du pointeur d'écriture dans la PPU
| +-> $885D 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| | $8860 B1 00 LDA ($00,Y) ; lecture d'un octet à partir de l'adresse pointée sauvegardée en RAM à l'adresse $00
| | $8862 8D 07 20 STA $2007 ; écriture de cet octet lu dans la PPU à l'adresse pointée
| | $8865 C6 03 DEC $03 ; décrémentation du nombre d'octets restant à lire conservé à l'adresse $0003
| +-- $8867 D0 F4 BNE $F4 ; si ce nombre > 0, alors on boucle en sautant à l'adresse pointée par la flèche
| $8869 20 6F 88 JSR $886F ; incrémentation de l'index de lecture des données
| $886C 4C 1E 88 JMP $881E ; on saute à l'instruction $881E
;-----------|---------------------------------------------------------------------------
; | subroutine: incrémentation de l'index Y et incrémentation du pointeur de données à $0000 si besoin
;-----------|---------------------------------------------------------------------------
| $886F C8 INY ;
| +-- $8870 D0 02 BNE $02 ;
| | $8872 E6 01 INC $01 ;
+-----+-> $8874 60 RTS ;
Localisation des données
Tout d'abord, on apprend donc que les données à écrire dans la nametable sont pointées par un pointeur qui est sauvegardé en RAM à l'adresse $0000. Ce pointeur est lui-même situé dans une table de pointeurs conservée à l'adresse
$89E3 (dans la même bank que le code ci-dessus). Si on ouvre un éditeur hexadecimal, il est possible de visualiser cette table de pointeurs (je n'expliquerai pas ici comment on transpose une adresse CPU en offset, cela fera l'objet d'un autre article si besoin).
Pour rappel, un pointeur est une adresse codée sur 2 octets. Dans la table présentée ci-dessus, on compte 17 pointeurs. Celui qui nous intéresse ici est le premier de la table. En effet, l'instruction à l'adresse
$880F est exécutée avec la valeur
#$00 dans le registre X. Ce sont donc les deux premiers octets de la table de pointeurs qui seront lus. Ces octets sont
#$05 et
#$8A, or comme vous le savez peut-être, les adresses CPU sont généralement écrites en Little-Endian sur NES. Cela veut dire que c'est l'octet de poids faible qui est écrit en premier. Par conséquent, pour reconstituer l'adresse des données de la nametable, il suffit d'inverser les deux octets lus, ce qui donne
$8A05. On peut noter cette fois encore que les données de la nametable sont donc situées dans la même bank que le code ci-dessus. Toujours depuis notre éditeur hexadécimal, on peut visualiser la "zone" de ces données. Mais il va falloir analyser encore un peu plus ces dernières pour bien les délimiter.
Déchiffrage des données
Pour déchiffrer et délimiter cette zone de données, il faut revenir au code désassemblé présenté plus haut. Ce qu'il dit, c'est que le CPU va lire des blocs de données. Chaque type de bloc caractérise une méthode de lecture ou d'écriture de données. On distingue 3 types de blocs:
- * les blocs de X octets à lire et à écrire dans la PPU;
* les blocs de 1 octet à lire et à écrire X fois dans la PPU;
* les blocs indiquant la fin des données de la nametable.
Blocs de X octets
Le premier octet de ce type de bloc doit avoir une valeur différente de
#$00 et de
#$FE. Il est composé de trois parties:
- * sur 1 octet: le nombre de données à lire et à écrire dans la PPU;
* sur 2 octets: une adresse pour initialiser le pointeur d'écriture des données dans la PPU;
* sur X octets: les données à écrire dans la PPU.
Bloc de 1 octet
Le premier octet de ce type de bloc doit avoir la valeur
#$FE. Il est lui aussi composé de trois parties:
- * sur 1 octet: le nombre de fois que l'octet à lire va être écrit dans la PPU;
* sur 2 octets: une adresse pour initialiser le pointeur d'écriture des données dans la PPU;
* sur 1 octet: la donnée à écrire dans la PPU.
Bloc de fin
Le premier et unique octet de ce type de bloc doit avoir la valeur
#$00.
Dans notre éditeur hexadécimal, on peut alors déterminer les différents blocs.
Conclusion
Ceci met fin à la première partie de cet article. Nous avons vu comment localiser le code de chargement de la nametable associée à l'écran titre, ainsi que comment localiser puis déchiffrer (en partie c'est vrai) les données relatives à cette nametable. Dans la prochaine partie, nous verrons ce que fait la PPU de ces données, je pense que je reviendrai sur les notions de nametable, d'
attribute table et de
palette.
Merci d'avoir pris le temps de lire cet article. J'espère jusqu'ici avoir été clair. N'hésitez pas à laisser vos commentaires ou questions!
EDIT -- Correction de quelques erreurs au niveau du code désassemblé.