Traduction du jeu Zero Escape 3 : Zero Time Dilemma

Traduction du jeu Zero Escape 3 : Zero Time Dilemma

Voilà un projet sur lequel j'ai travaillé il y a quelque temps: le projet de traduction du jeu Zero Escape 3 : Zero Time Dilemma. Si vous ne connaissez pas la série de Visual Novel, dépêchez-vous de rattraper votre retard, vous passez à côté d'un super jeu ! De plus, la série est entièrement disponible sur Steam maintenant, il n'y a donc pas d'excuse valable pour ne pas y jouer !

Dans ce projet, mon rôle a consisté à fournir les outils d'extraction et de recompilation des textes, les textes ont quant à eux été traduits principalement par "Jls" un professeur de mathématiques.

Cet article a pour but d’expliquer comment je m’y suis pris pour faire le Game Hacking de ce jeu. Je n'avais à ce moment-là aucune connaissance dans le domaine, et j'ai appris sur le tas. Mais j'ai finalement trouvé cela très amusant à faire. Il y a des chances pour qu'ils existent des procédés plus simples pour y arriver, mais je suis satisfait de ma méthode malgré tout. Bref, voilà toute ma réflexion sur ce Game Hack !

Lien : Patch FR pour le jeu ZE3: ZTD

Les fichiers du jeu

Il faut savoir que j’utilise la version Steam du jeu. Dans cette version, il y a principalement 3 fichiers :

  • 00000000.cfsi : Fichier qui contient quasiment tous les fichiers du jeu sauf les effets sonores 
  • bgm.cfsi : Fichier qui contient toutes les musiques du jeu
  • voice.cfsi : Fichier qui contient toutes les voix du jeu

Évidemment, le plus intéressant situe dans le fichier 00000000.cfsi, je me suis donc équipé d’un éditeur hexadécimal et c’était parti…

Le fichier 00000000.cfsi

L'Hexadécimal, un pur bonheur...

La structure du fichier est plus simple qu’il n’y parait au premier abord. Sur la Figure ci-dessus, on peut constater la liste de tous les fichiers du jeu. En faite, la première partie de ce fichier est une table d’indexage qui indique dans quel secteur on peut trouver le fichier en question.

Ce fichier commence donc par trois octets 0xFCE801 dont je n’ai pas trouvé de signification particulière.

Analysons ensuite l’octet suivant 0x00 (0), il n’y a rien de particulier à dire pour le moment. Ensuite, 0x01 (1) puis 0x0A (10) et enfin on a un nom de fichier « debug.json » et si l’on compte le nombre de lettres du fichier, il y en a 10. Donc 0x0A indique possiblement le nombre de caractères pour le nom du fichier.

Ensuite jusqu’au prochain nom « chr/010/ » on a « 00 00 00 00 F2 00 00 00 08 » qu’il faut analyser, comme on a vu pour « debug.json », l’octet précédent indique la taille du nom du fichier. Ici, c’est la même chose pour « chr/010/ » qui a 8 caractères ce qui correspond à 0x08 (8).

Je vais donc appeler « mystère » la chaîne « 00 00 00 00 F2 00 00 00 » vu qu’on ne sait pas encore ce que c’est.

Poursuivons… « 25 0B » suivis de « 010.std.orb », 0x0B (11) indique donc bien le nombre de caractères du nom de fichier. Il reste donc 0x25 (37) qu’on ne sait pas encore ce qu’il signifie. Après « 010.std.orb », on a encore huit octets comme pour la chaîne « mystère » : « 10 00 00 00 19 CC 0E 00 ».

Et ensuite, le même schéma continu au total 37 fois jusqu’au prochain nom de dossier. Mais du coup, on sait ce que signifie l’octet après un nom de dossier, il indique le nombre de fichiers du dossier.

Revenons tout au début, on a laissé quelques octets sans aucune explication. 0x00 devrait donc indiquer le nombre de caractères du nom du dossier, mais il est égal à 0. On en déduit donc que le fichier ne se trouve pas dans un dossier, mais à la racine. Le 0x01 qui suit indique donc qu’il n’y a qu’un seul fichier à la racine, le fichier « debug.json ».

Maintenant, on arrive quasiment à lire toute la table d’indexage sauf les 8 octets qui suivent chaque fichier. Jusqu’à la fin de la table d’indexage qui se termine à l’adresse 0x23A65 (146 021) qui est suivie de plusieurs 0x00 jusqu’à l’adresse 0x023A70 (146 032). Ensuite, on a ainsi quelque chose qui ressemble très fortement à du JSON ({ … » flags » : { …. » already_read ». : 0,…) or le tout premier fichier était « debug.json » aurait-on notre premier fichier ?

Le fichier .json se termine par 0x7D0D0A qui ferme l’accolade ouvrante (0x7D = « { ») suivit de deux caractères spéciaux (espace ou retour à la ligne) ensuite c’est que des 0x00. Elle se situe à l’adresse 0x023B62 (146 274), avec un peu de math : 0x23B62 – 0x23A70 = 0xF2 (242).

F2 est dans la chaîne mystère « 00 00 00 00 F2 00 00 00 » dans cette chaîne, on a donc la taille du fichier. Le fichier .json est un petit fichier de 242 octets, c’est pour ça que sa taille tient que sur un seul octet. Mais en réalité, le type de la taille est « long » soit 4 octets. Ainsi l’indication de la taille du fichier est 0xF2000000 » qui se lit « à l’envers » donc 0x000000F2 (242), car c’est du little-endian (je vous renvoie vers Wikipédia, car ce n’est pas mon but d’expliquer ce qu’est le little-endian).

Ainsi on sait donc que les 4 derniers octets des chaînes mystères indiquent la taille du fichier. Il ne reste donc plus qu’à déterminer à quoi correspondent les 4 premiers. Regardons pour chaque fichier :

Fichier ? Taille du fichier
Debug.json 00 00 00 00 (0) F2 00 00 00 (242)
010.std.orb 10 00 00 00 (16) 19 CC 0E 00 (969 753)
010_eye_brows.std.orb DC EC 00 00 (60 636) 84 12 00 00 (4 740)
012.std.orb FB ED 00 00 (64 493) FB ED 00 00 (60 923)

Maintenant, il faut déterminer le lien entre toutes les informations, c’est là que ça devient un peu compliqué. À la fin de la table d’indexage, on a plusieurs 0x00 (0) jusqu’à l’adresse 0x23A70 (146 032), puis à la fin du fichier debug.json, on a de nouveau plusieurs 0x00 jusqu’à l’adresse 0x023B70 (146 288). La raison est assez simple à deviner, en faite les 0x00 servent à remplir des "secteurs". Dans ce fichier, les secteurs ont une taille de 16 octets. Ainsi tout fichier qui se termine en plein milieu d’un secteur est complété par des 0. Bien sûr, un fichier fait généralement plusieurs secteurs.

Et maintenant, tout devrait être clair. Les fichiers sont rangés par secteur, et ce qu’il nous manque c’est l'adresse du début du premier secteur !

Le fichier debug.json est le tout premier fichier, donc le secteur est 0x00000000 (0) et a une taille de 0xF2000000 (242) octet. Il lui faut donc 0x10 (16)  secteurs, car 0x09 * 0x10 = 0x90  (15 * 16 = 144) ce qui est plus petit que la taille du fichier, alors que 0x10 * 0x10 = 0x100 (16 * 16 = 256) ce qui est plus grand que la taille du fichier.

Et la bingo ! 010.std.orb commence au 0x10 (16) ème secteur ! La table d’indexage est donc maintenant entièrement décryptée. On a donc toutes les informations pour extraire tous les fichiers ! Attention cependant, le secteur 0 commence à l’adresse 0x023A70, car on a la table d’indexage, on appelle cette valeur « basePos ». Ainsi pour trouver l’adresse de début d’un fichier, il faut faire le petit calcul suivant : basePos + secteur * 0x10.

Le code C++ si dessous (simplifié) résume un peu toute la démarche. Le code n’est pas fonctionnel en l’état, mais permet de comprendre dans les grandes lignes comment ça marche.

// file = 00000000.cfsi 
vector<tuple<string, long, long>> index_table;  
do { 
  // Lit 1 octet  pour déterminer la taille de la chaine de caractère du chemin du dossier 
  char path_name_size = read_octet(file); 
  //Récupère le chemin du dossier 
  string path_name = read_word(file, path_name_size); 
  //Lit le nombre de fichier du dossier 
  char nb_file = read_octet(file, 1);   
  //Lit tous les fichiers du dossier 
  for(unsigned int i = 0; i < nb_file; i++) { 
    //Lit 1 octet pour la taille du nom de fichier 
    char file_name_size = read_octet(file);  
    //Lit le  nom de fichier 
    string file_name = read_word(file, file_name_size); 
    string name = path_name + name; 
    //Lit la position du fichier et la multiplie par  16 (10 en base 16 =  16 en base 10) 
    long offset = read_long(file) * 0x10; 
    //Lit la taille du fichier 
    long size = read_long(file);  
    //On sauvegarde les informations du fichier 
    index_table.push_back(tuple<string, long, long>(name, offset, size)); //On sauvegarde les informations du fichier 
  } 
} while ( nb_file > 0 ); //Si nb_file   = 0 c'est qu'il n'y a plus de fichier à lire 

//Enregistre la position après lecture de la table, puis complètre le secteur 
long basePos = sector(file.tell(), 0x10);  

for(unsigned int i = 0;  i < index_table.size(); i++) { 
  //Récupération du nom du fichier 
  long name = get<0>(index_table[i]); 
  //Récupération de l'addresse grace au numéro de secteur 
  long offset = get<1>(index_table[i]) + basePos; 
  //Récupération de la taille du fichier 
  long size = get<2>(index_table[i]); 
  //On écrit un fichier de nom "name",  commençant à l'adresse "offset" de taille "size" 
  write_file(file,  name, offset, size); 
} 

On lance tout ça, et voilà, on a extrait tous les fichiers du jeu !

Tous les fichiers du jeu

Cela parait compliqué, mais en fait pas tant que ça. Maintenant, il n’y a plus qu’à extraire les fichiers de textes qui sont situés dans le répertoire local. Car eux aussi sont archivés d’une manière un peu particulière, mais beaucoup plus simple que l’archive 00000000.cfsi, donc si vous avez compris jusque là comment extraire les fichiers, l’extraction des textes ne devrait pas être plus compliquée à comprendre.

Les fichiers de textes

Le tout premier fichier texte du jeu...

J’ai vu sur certains forums de discussion que certains se contentent de modifier directement les fichiers de texte à partir d'ici. Sauf qu'en réalité, en faisant ça, on est terriblement contrainte, car on ne peut pas ajouter de caractère. Si l’on ajoute des caractères, cela ne marchera plus... C’est assez simple à comprendre pourquoi avec la section précédente. La table d’indexage. Si vous modifiez le fichier et qu’il n’a pas la même taille, la réinsertion ne se fait plus correctement, il se peut que votre texte déborde sur le secteur du fichier suivant, et là c’est le drame, car plus aucune adresse ne correspond. Il faut donc, lors de la réinsertion des fichiers dans l’archive, modifier la table d’indexage. 

Pour le patch automatique que j'avais créé, je ne réécrivais pas la table d’indexage. En fait, cela serait beaucoup trop long pour un patch intermédiaire (surtout quand on doit faire de nombreux tests). La raison pour laquelle c'est long: il faut réécrire l’ensemble du fichier. La solution que j'ai trouvée, c’est que j’ajoute tout simplement les fichiers modifiés à la fin de l’archive, et je modifie juste l’adresse dans la table d’indexage. Ainsi j’évite le problème du décalage. L’inconvénient de cette méthode, c’est que les fichiers originaux sont toujours présents, mais « perdus », car la table d’indexage ne pointe plus vers ces fichiers, du coup, l’archive prend de la place pour rien. Mais bon, pour les tests de la traduction c’est amplement suffisant. Le patch final quant à lui, reconstruit une archive propre.

Voilà pour le point sur la réinsertion des fichiers. Maintenant que l’on sait que l'on peut modifier la taille des fichiers sans aucun problème, il reste quand même la question « comment modifier le texte ? », car oui ça ne se fait pas tout seul.

L’analyse du fichier est très simple. Une phrase commence par deux groupes de quatre octets, le premier est l’identifiant unique de la phrase, par exemple 0x01000000 est l’identifiant unique de la première phrase du jeu. Suivi ensuite du second groupe de quatre octets indiquant le nombre de caractères de la phrase, dans l’exemple ci-dessus 0x0D000000 (13). Le tout est suivi de la phrase « Hey! Open up! » de 13 caractères (ne pas oublier les espaces). Et ce schéma se répète jusqu’à la fin du fichier. Voilà, rien de plus à dire sur le texte, dans ce fichier il suffit de modifier les quatre octets indiquant le nombre de caractères de la phrase traduite et c’est gagné !

Remarque: le fichier us.mo qui est un peu particulier. Ce fichier est encore plus simple à modifier, car un simple logiciel suffit : PoEdit. PoEdit est un logiciel de traduction qui permet d’ouvrir des fichiers « . po » qui contiennent la VO avec la version traduite, cela se présente comme ça :

Le fichier us.mo

On voit donc la VO en japonais, et la version traduite en anglais. Ce fichier contient tous les éléments textuels (ou presque, j’y reviendrai après) autres que les dialogues. Et notamment les mots de passe ! C’est grâce à la modification de ce fichier que j’ai réussi ceci :

Le mot de passe original à ce moment du jeu est "me", le nouveau mot de passe est "moi" maintenant.

On a donc la possibilité de traduire quasiment tous les textes du jeu… Il ne manque plus que les images textuelles et là, ça devient beaucoup beaucoup plus compliqué… Pour moi encore plus compliqué que l’archive, car je n’ai pas réussi décrypté tous les secrets…

Les images textuelles

« Pour les images prend Photoshop, c’est simple, je ne vois pas où est le problème. » C’est ce qu’on pourrait me dire. Oui sauf qu’en réalité, c’est plus compliqué qu’il n’y parait. Déjà, les fichiers sont dans un format d’image un peu particulier le format DDS (DirectDraw Surface), mais là n’est pas le problème, il existe beaucoup de convertisseurs sur le web pour les obtenir en PNG. C’est plutôt le type d’image qui va être important. En effet, on a deux types d’images.

Le premier type, simple à modifier, Photoshop suffit amplement.

Une image simple à modifier

Le second par contre est plus compliqué, en effet, en gardant la même police il n’est pas possible de remplacer le texte par « [Carte du secteur C] » et la seconde ligne par « sauvegardée dans [FICHIER] ». Si vous faite cela, jeu vous aurez probablement « [Carte du secteur sauvegardée da » ce n’est pas très beau… Alors pourquoi ce problème ? Et bien en faite, l’image est découpée en deux morceaux au pixel près. Ainsi on a deux morceaux d’image « [Map of Ward C] » et « stored in [FILE] ». Les dimensions sont exactement celles de ce texte et ne possèdent pas un pixel de plus. Or notre traduction française prend plus de pixels que prévu… C’est triste… Au choix, on choisit une taille de police plus petite, ou on trouve où sont les instructions pour découper l’image, qui sont évidemment situées ailleurs…

cinema_c_en
Une image en deux morceaux avec une taille précise à respecter

L’image qui pose problème s’appelle « cinema_c_en.dds ». Or dans le même dossier, on a un fichier « cinema_c_en.std.orb ». On se doute donc que les deux fichiers sont liés d’une manière ou d’une autre. Sauf qu’à l’ouverture du fichier « cinema_c_en.std.orb », rien n’est compréhensible, c’est un format compressé. Heureusement très simple à décompresser, il suffit de retirer les quatre premiers octets qui donne la taille du fichier compressé, et ensuite de décompresser ce fichier avec GZIP. Et là, on obtient le fichier « cinema_c_en.std » qui est beaucoup plus clair !

fichier cinema
cinema_c_en.std, un modèle 3D...

Ce fichier beaucoup plus petit que les autres n’est malheureusement pas plus facile à décrypter, en tout cas avec mes connaissances actuelles, car il s’agit en faite d’un modèle 3D, compressé qui plus est, c’est-à-dire qu’on ne peut l’ouvrir avec aucun éditeur 3D directement à ma connaissance. Et je n’ai pas trouvé comment le décompresser. Néanmoins en fouillant dans les tréfonds de l’internet, j’ai réussi à tirer quelques informations.

Je ne vais pas expliquer ma démarche comme pour l’archive principale, car je n’ai pas encore tout compris. Mais dans ce fichier, il y a les coordonnées de tous les points.  Prenez l’image ci-dessous que j'ai eu avec Blender et un script de décompression du fichier .std:

3d
Position du texte dans un espace 3D

Le texte est situé dans les deux carrés roses, un pour chaque ligne, ce qui correspond au découpage en deux parties de l’image. Dans le fichier . std, il y a la position des 8 points (4 par rectangles) qui pour chaque point, est un vecteur de 3 éléments pour les situer dans l’espace. Mais ce n’est pas tous, car ces points là ne font que dire où appliquer l’image, mais pas comment découper l’image ! On a donc aussi 8 points qui sont un vecteur de 2 éléments pour indiquer où découper l’image.

J’arrive plus où moins à extraire ces vecteurs, ici les valeurs sont pour la première ligne de l’image :

  GAUCHE DROITE
x y z x y z
Haut Position 3D -1.7 -0.25 0 1.7 -0.25 0
Image 2D 0.007 0.031 0.67 0.031
Bas Position 3D -1.7 0.25 0 1.7 0.25 0
Image 2D 0.007 0.437 0.67 0.437

Pour ce qui est du découpage de l’image, je n’ai pas de problème, il s’agit d’une position en pourcentage. Il est donc facile de déterminer à quoi cela correspond. Par contre, la taille de l’image dans le modèle 3D j’ai du expérimenter un peu plus.

Ce qui nous intéresse ici, c’est la position suivant l’axe x, la hauteur elle, ne changera pas lors de la traduction. Ici, le texte fait 66 % de 512px soit 338px et la distance entre les points 3D est de 3,4 unités. On pourrait donc conclure que 10px = 0,1 unité. Pour la seconde ligne, le texte fait 63 % soit 322.5px et la distance entre les points 3D est de 3,3 ce qui valide notre hypothèse. Je n’ai pas fait de tests plus poussés pour vérifier si cela est vrai pour tous les fichiers. À voir avec l’expérimentation.

Les fichiers . std.aux

Cela aurait été tellement parfait s’il n’y avait pas un autre cas particulier ! Certains fichiers. std ont les coordonnées séparées dans plusieurs fichiers auxiliaires. Et pour le moment je n’ai pas réussi à décrypter cette partie... 

Et voilà pour toutes les informations que j'ai réussi à extraire des fichiers compressés du jeu, c’était la première fois que je fais du Game Hacking (et probablement la seule fois), cela m’a permis d’apprendre plein de nouvelles choses intéressantes. Et si quelqu’un qui maîtrise des techniques de Game Hack et qu’il passe par ici, qu’il n’hésite pas a me contacter s'il sait comment traduire les derniers fichiers .std ;).