Team Zero

Team Zero

Voilà le premier projet que je vous présente et qui est actuellement au centre de mon attention, le projet de traduction du jeu Zero Escape 3 : Zero Time Dilemma.

Ce billet a pour but d’expliquer comment je m’y suis pris pour faire le Game Hacking du jeu.

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 : Contiens quasiment tous les fichiers du jeu sauf les sons
  • bgm.cfsi : contiens toutes les musiques du jeu
  • voice.cfsi : contiens 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

Fichier 00000000.cfsi

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 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 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 Wikipidia, car ce n’est pas mon but d’expliquer ce qu’est le little-endian).

Ainsi on sait que les 4 derniers octets des chaines 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 tous cela, 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 les secteurs. Dans ce fichier, les secteurs sont définis de taille 16. 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 le numéro de 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 en faite à 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 : 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);
}
 
Table d'indexage du fichier 00000000.cfsi

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

liste_fichiers

Cela parait compliqué, mais en fait pas tant que ça. Maintenant, il n’y a plus qu’a extraire les fichiers de textes situés dans locale. 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à, l’extraction des fichiers de textes ne devrait pas être plus compliquée !

Les fichiers de textes

texte

J’ai lu sur plusieurs forums que certains essayaient de modifier directement les fichiers de texte. Et que si l’on ajoutait des caractères plus rien ne marche. En faite, c’est assez simple à comprendre pourquoi avec le point précédent. 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. Il faut donc, lors de la réinsertion des fichiers dans l’archive, modifier la table d’indexage. Pour le patch automatique de la Team Zero, je ne réécris pas la table d’indexage, en fait, cela serait beaucoup trop long pour un patch intermédiaire, car il faudrait réécrire l’ensemble du fichier. Ce que je fais, 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 « perdu », 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. Un patch correct sera disponible une fois la traduction terminée.

Voilà pour le point sur la réinsertion des fichiers. Maintenant 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. Il s’agit de deux groupes de quatre octets, le premier pour 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) et ensuite on a 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 juste de modifier les quatre octets indiquant le nombre de caractères de la phrase traduite et c’est gagné 🙂 !

Un dernier point 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 :

usmo

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 :

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 encore 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.pic_c0052

 

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] » et pas un pixel de plus. Or notre traduction prend plus de pixels que prévu… C’est triste… Du coup, ça veut dire que les instructions pour découper l’image sont situées ailleurs…cinema_c_en

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

Ce fichier beaucoup plus petit que les autres n’est malheureusement pas plus faciles à décrypter, en tout cas pour moi, car il s’agit en faite d’un modèle 3D, compressé qui plus est, c’est-à-dire qu’on ne peut l’ouvre avec aucun éditeur 3D à 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 :

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 encore décrypté cette partie, qui je l’espère sera proche des fichiers. std classique.

Voilà pour le moment, à chaque fois que je me dis que j’approche de la fin, je découvre de nouvelles exceptions, mais là je pense que les fichiers. std.aux sont vraiment les derniers à analyser… En tout cas, si vous voulez aider à la traduction, n’hésiter pas à aller sur le site de la Team Zero tous les textes sont à disposition à la traduction pour le wiki.

Seuls le fichier us.mo et les images ne sont pas encore modifiables en ligne suite à leur caractère « particulier ». Je recherche activement des graphistes compétents pour la traduction des images ! J’aimerais garder quelque chose de très proche de l’original, c’est à dire sans aucune modification de la police.

Une dernière précision, c’est la première fois que je fais du Game Hacking, du coup je débute un peu en la matière, mais cela m’a permis d’apprendre plein de nouvelles choses intéressantes ^^. Alors si quelqu’un qui maîtrise le Game Hack et qu’il passe par ici, qu’il n’hésite pas a se manifester pour me donner des petits conseils :).

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *