2010
12.24

Je viens de rentrer chez moi pour les vacances de noël (mes seules vacances de l’année \o/), du coup j’en profite bien évidemment pour squatter la PS3 familiale accompagnée de son ampli surround 5.1 qui embellit l’output HDMI (upscale, modification du contraste, etc.). Il se trouve que j’étais très frustré lorsque juste avant ma rentrée en septembre je me suis retrouvé bloqué devant l’avant dernier boss d’Eternal Sonata, du coup j’ai terminé ça tout de suite pour ne pas l’oublier 🙂 .

D’abord, qu’est-ce qu’Eternal Sonata ? C’est un RPG fait par les gens de tri-Crescendo (des anciens de chez Namco qui ont bossé sur Tales of Phantasia, Star Ocean, Baten Kaitos et quelques autres J-RPG) qui est sorti tout d’abord sur Xbox 360 puis sur PS3. Adoptant un graphisme très simple basé sur du cell shading, c’est un jeu que j’ai trouvé très agréable graphiquement (c’est un style que j’apprécie particulièrement, d’ailleurs les Tales of récents ont un style graphique très proche). C’est simplement joli et épuré tout en étant détaillé. Une galerie de screenshots est disponible sur jeuxvideo.com. Mais le graphisme n’est pas la seule chose élégante dans ce jeu : la musique est simplement sublime. Et bien heureusement pour un jeu comme celui ci, vous verrez pourquoi un peu plus loin si vous ne vous en doutez pas. Motoi Sakuraba (grand compositeur de musiques de J-RPG, qui s’occupe des Tales of, Star Ocean, Baten Kaitos, et plein d’autres jeux qui ont des musiques géniales) a simplement tout donné sur ce jeu, et c’est sans aucun doute à mes oreilles sa meilleure OST. Je vous laisse le soin de trouver des extraits en ligne (Youtube en a plein), ça en vaut la peine.

Parlons un peu plus en détail du scénario, tout en restant relativement spoiler-free. Première chose surprenante : le personnage principal du jeu est Frédéric Chopin, le compositeur français du 19ème siècle. Tout le jeu se déroule dans un de ses rèves qu’il a avant de mourir (ou pas ? haha, doutez !). Du coup, la musique “classique” (romantique plus précisément, mais accordons nous ce raccourci) a une grande part dans le jeu : la plupart des personnages et des lieux ont des noms issus du champ lexical de la musique (une liste loin d’être exhaustive : Beat, Allegretto, Staccato, Falsetto, Jazz, Legato, Polka, Mazurka et Crescendo sont des noms de personnages, le pays voisin s’appelle Baroque). C’est un choix comme les autres, au moins on voit autour de quoi ça tourne. Le scénario est aussi très proche de la vie du compositeur, et entre les différents chapitres (au nombre de 7 ou 8 il me semble) un interlude culturel nous présente à chaque fois une oeuvre de Chopin et le contexte dans lequel il l’a composé. Certains diront que c’est très bien, d’autres diront que ça casse l’immersion, à vrai dire je suis assez d’accord avec ces deux avis (bonne manière de ne pas trancher !).

Dans ce contexte musical, un scénario à la trame assez voire trop simple se déroule entre les différents personnages dont se composent le groupe. J’ai été assez déçu par le scénario, qui est vraiment sans trop de rebondissements (le grand méchant est nommé au début et il reste le grand méchant jusqu’à la fin du jeu, par exemple) et très classique (un pays dont le dirigeant est corrompu par le pouvoir, un groupe de rebelles qui jurent tout sur la justice). Pour plus d’informations et pour perdre votre vie, tvtropes répértorie pas mal de “clichés” (tropes are not clichés, je sais, mais je ne vois pas de meilleure traduction) qui se retrouvent dans ce jeu et notamment dans son scénario. C’est un peu triste et c’est surement le plus gros point négatif de ce jeu. Il y a quand même pas mal d’originalité autour qui sauve largement la mise.

Qui dit J-RPG dit combats (j’offre un #FollowFriday si un nerd me trouve un J-RPG sans combats). Et le système mis en place dans Eternal Sonata est génial. Entrainant, dynamique, demandant de la réflexion et de l’habilité, et qui change en plus régulièrement au fur et à mesure du jeu. Que demande le peuple. En très gros, chaque personnage (ami ou ennemi) participant au combat (maximum 3 dans votre groupe) a un temps d’action alloué à chaque tour. Ce temps se répartit en temps tactique (qui se termine dès que le personnage réalise une action, il permet juste de réflechir et d’observer) et temps d’action (qui lui inclut les déplacements, les attaques, les utilisations d’items et tout le reste). Les personnages sont disposés sur une map sur laquelle tout le monde peut se déplacer librement (pas de grille ni de position fixe, on se déplace vraiment comme on veut). Cette map a des zones de lumière et des zones d’ombre, qui influencent plusieurs éléments : le type de monstre (parfois certains monstres se comportent différemment a la lumière et à l’ombre) et les attaques spéciales réalisables par vos personnages (chaque personnage peut utiliser une attaque de lumière ou une attaque d’ombre selon là où il se trouve). La puissance des attaques spéciales dépend du nombre de coups consécutifs portés aux ennemis avant l’utilisation de l’attaque. Il est donc souvent intéressant de ne pas utiliser une attaque spéciale pour maximiser le compteur de coups consécutifs. Pour rendre tout ça très drôle, toutes les constantes et certaines règles sont modifiées à chaque chapitre du jeu. Par exemple, au dernier chapitre il n’y a pas de temps tactique et 4s de temps d’action, mais si un personnage utilise une attaque spéciale avec plus de 24 coups consécutifs avant, il est possible d’enchainer jusque 3 attaques. Ça peut parraitre complexe mais tous les concepts sont introduits petit à petit, et c’est très vite entrainant.

Quelques déceptions quand même en plus du scénario : déjà, la durée de vie est assez courte. Je l’ai terminé en 35h sachant que je suis loin d’être un hardcore gamer (je suis même plutôt mauvais). Ensuite, certains combats de boss sont très durs alors que les monstres qui se trouvent avant sont ridiculement simples (je pense notamment à l’avant dernier boss du jeu où je suis resté coincé un bon moment et qui demande pas mal de skill et de réflèxes pour le battre). Aussi, les différences entre la version Xbox 360 et la version PS3 sont assez énormes, et même si c’est dans ce cas à mon avantage c’est quelque chose que je ne cautionne pas trop. La fin du scénario est même complétement différente sur PS3, et il y a 2 ou 3 personnages jouables supplémentaires.

Pour résumer (TL;DR toussa), c’est un jeu que je conseille vraiment si vous aimez les JRPG. Même si son scénario reste classique, le pari de raconter la vie du compositeur Frédéric Chopin au travers d’un jeu vidéo est assez osé et plutôt bien réussi. Les graphismes sont attrayants, la musique est géniale, le système de combats est très entrainant et la difficulté n’est ni trop basse, ni trop haute à mon goût. Si vous avez une PS3, faites vous un joli cadeau de noël 🙂 .

2010
12.13

Dans le cadre d'un des projets sur lesquels je travaille, je dois récupérer les données de localisation d'un programme depuis des DLL de ressources. C'est une méthode assez usuelle sous Windows pour gérer la localisation d'un projet : toutes les chaines sont référencées dans le code par un ID, et elles sont stockées dans une DLL à part qui peut être différente selon la langue. C'est un peu ce qui est fait sous Linux avec gettext, mais en moins complexe et légèrement plus intégré.

Sous Windows, tous les fichiers exécutables (EXE, DLL, OCX, SYS, etc.) sont stockés au format PE (Portable Executable). Comme avec ELF sous Linux, ce format est composé de différentes sections. Celle qui va nous intéresser dans cet article est la section .rsrc, qui contient toutes les ressources contenues dans un exécutable. Une ressource, ça peut être tout et n'importe quoi. Windows en a quelques types prédéfinis qu'il sait gérer (icone, description d'une interface graphique, string table) mais il y a un type passe partout, BINARY, qui permet de stocker ce que l'on veut.

Notre but est ici d'accèder à une ressource de type String Table dans une DLL, afin dans un premier temps d'afficher toutes les chaines, puis d'en récupérer une à partir de son ID.

Dans un premier temps, on va créer une DLL contenant une string table avec MinGW pour pouvoir tester avec des données que l'on connait.

$ cat > foo.rc <<EOF
#define IDS_HELLO 1
#define IDS_WORLD 2

STRINGTABLE
{
IDS_HELLO, "Hello"
IDS_WORLD, "World"
}
EOF
$ mingw32-windres foo.rc -o foo.o
$ mingw32-gcc -shared -o foo.dll foo.o
$ strings -e l foo.dll
Hello
World

La prochaine étape, c'est de lire la documentation du format PE qui se trouve sur le site de Microsoft. En vrai, ça n'est pas important car je vais détailler les choses ici. Commençons donc le parsing de notre PE.

À la recherche de .rsrc

On va commencer par lire notre fichier DLL. En entier car je suis un bourrin et car je n'ai pas envie de me faire chier pour cet article, mais on pourrait très bien être plus fin.

>>> dll = open('foo.dll', 'rb').read()

Un exécutable au format PE commence par du code compilé qui affiche « This program cannot be run in DOS mode. », ceci pour la compatiblité MS-DOS. À l'offset 0x3C se trouve l'offset du header PE.

>>> import struct
>>> (pe_header_offset,) = struct.unpack("B", dll[0x3C:0x3C+1])
>>> hex(pe_header_offset)
'0x80'

À l'adresse que l'on vient de lire se trouvent 4 octets magiques permettant de s'assurer que l'on est bien en présence d'un fichier PE :

>>> data = dll[pe_header_offset:pe_header_offset+4]
>>> (pe_magic_dword,) = struct.unpack("4s", data)
>>> pe_magic_dword
b'PE\x00\x00'

Ensuite se trouve le header PE, mesurant 20 octets, et qui ne contient pas énormément de choses qui nous intéressent ici. Il est suivi d'un header « optionnel » (qui est présent dans 99.9% des fichiers PE vu qu'il est obligatoire sous Windows), dont la taille est variable en fonction de son type. Sa taille est indiquée dans un des champs du header PE : SizeOfOptionalHeader, de 2 octets, à l'offset 16.

>>> data = dll[pe_header_offset+4+16:pe_header_offset+4+18]
>>> (opt_hdr_size,) = struct.unpack("H", data)

Après le header PE et le header optionnel se trouve la table des sections. Il va falloir la lire intégralement puis la parcourir afin de trouver la section .rsrc, contenant les ressources inclues dans un fichier PE.

>>> def read_section(idx):
...     offset = pe_header_offset + 4 + 20 + opt_hdr_size
...     sect_offset = idx * 40
...     real_off = offset + sect_offset
...     data = dll[real_off:real_off+40]
...     tup = struct.unpack('8sIIII', data[:24])
...     return SectionHeader(*tup)
...
>>> read_section(0)
SectionHeader(name=b'.text\x00\x00\x00', virtual_size=2820,
              virtual_address=4096, size_of_data=3072,
              pointer_to_data=1024)
>>> read_section(1)
SectionHeader(name=b'.data\x00\x00\x00', virtual_size=8,
              virtual_address=8192, size_of_data=512,
              pointer_to_data=4096)
>>> read_section(2)
SectionHeader(name=b'.rdata\x00\x00', virtual_size=272,
              virtual_address=12288, size_of_data=512,
              pointer_to_data=4608)
>>> read_section(3)
SectionHeader(name=b'.bss\x00\x00\x00\x00', virtual_size=104,
              virtual_address=16384, size_of_data=0,
              pointer_to_data=0)

Le nombre total de sections est inclus dans le header PE, à l'offset 2, et mesure deux octets :

>>> data = dll[pe_header_offset+4+2:pe_header_offset+4+4]
>>> (num_of_sections,) = struct.unpack('H', data)

Lisons donc toutes les sections, ou plutôt tous les headers de sections :

>>> sect_headers = [read_section(i) for i in range(num_of_sections)]

On peut donc maintenant facilement trouver la section que l'on souhaite : .rsrc :

>>> for sh in sect_headers:
...     if sh.name.startswith(b'.rsrc'):
...             rsrc = sh
...
>>> rsrc
SectionHeader(name=b'.rsrc\x00\x00\x00', virtual_size=140,
              virtual_address=36864, size_of_data=512,
              pointer_to_data=7680)

Et on va maintenant lire tout son contenu, qui va de la position pointer_to_data à pointer_to_data + size_of_data.

>>> start_addr = rsrc.pointer_to_data
>>> end_addr = rsrc.pointer_to_data + rsrc.size_of_data
>>> rsrc_data = dll[start_addr:end_addr]

À l'intérieur de la section

La section .rsrc contient principalement une structure hiérarchique (un arbre quoi) qui permet d'y trouver facilement ce que l'on y cherche. Pour citer la documentation (en traduisant), « Le design général supporte 2**31 niveaux de hiérarchie. En pratique, Windows en utilise 3. ». Ces trois niveaux sont le type de ressource, son ID et la langue de la ressource. Pour les chaines contenues dans une string table, le type est la constante RT_STRING (RT = Resource Type), définie dans winuser.h comme valant 6. L'ID (deuxième niveau) est l'ID de la chaine recherchée, divisée par 16, plus un (string_id / 16 + 1). Enfin, la langue par défaut est Anglais US, dont l'ID peut être trouvé dans cette liste : 1033.

En feuille de l'arbre se trouve un bloc de 16 chaines précédées de leur taille sur 2 octets. Les chaines ne contiennent pas de terminateur nul (\0), on a de toute façon leur taille. Maintenant, parsing time ! On commence par définir les quelques structures qui vont nous servir :

>>> TreeNode = namedtuple(
...     'TreeNode',
...     'characteristics timestamp ver_major ver_minor '
...     'name_entries id_entries'
... )
>>> TreeEntry = namedtuple(
...     'TreeEntry',
...     'label address'
... )
>>> TreeLeaf = namedtuple(
...     'TreeLeaf',
...     'address size'
... )

Quelques fonctions pour parser ces noeuds :

>>> def read_node(offset):
...     data = rsrc_data[offset:offset+16]
...     return TreeNode(*struct.unpack('IIHHHH', data))
...
>>> def read_entry(offset):
...     data = rsrc_data[offset:offset + 8]
...     return TreeEntry(*struct.unpack('II', data))
... 
>>> def read_leaf(offset):
...     data = rsrc_data[offset:offset+8]
...     return TreeLeaf(*struct.unpack('II', data))
... 

Et enfin, des fonctions de plus haut niveau, qui lisent respectivement une feuille de l'arbre et son arborescence :

>>> def read_data(addr):
...     leaf = read_leaf(addr)
...     data_off = leaf.address - rsrc.virtual_address
...     return rsrc_data[data_off:data_off+leaf.size]
... 
>>> def read_tree(offset):
...     node = read_node(offset)
...     child_off = offset + 16
...     
...     # Skip the named childs
...     child_off += 8 * node.name_entries
...     
...     childs = {}
...     for i in range(node.id_entries):
...         entry = read_entry(child_off)
...         child_off += 8
...         
...         # If the MSB is 1, it is an internal node
...         if entry.address & 0x80000000:
...             child = read_tree(entry.address & ~0x80000000)
...         else:
...             child = read_data(entry.address)
...         childs[entry.label] = child
...     return childs

On confirme le bon fonctionnement sur notre DLL de test :

>>> read_tree(0)
{6: {1: {1033: b'...'}}}

Il reste donc les données en feuille de l'arbre à parser. C'est en fait très simple, comme dit plus haut : 16 chaines UTF-16 préfixées par leur taille.

>>> def read_table_chunk(chunk):
...     li = []
...     while chunk:
...         (size,) = struct.unpack('H', chunk[:2])
...         chunk = chunk[2:]
...         string = chunk[:2*size]
...         li.append(string.decode('utf-16'))
...         chunk = chunk[2*size:]
...     return li
... 
>>> read_table_chunk(read_tree(0)[6][1][1033])
['', 'Hello', 'World', '', '', '', '', '', '', '', '', '', '', '', '', '']

Avec tout ça c'est presque fini, plus qu'à faire une fonction qui récupère une string depuis son ID. Trivial !

>>> def resource_string(id):
...     tree = read_tree(0)
...     chunk_id = id // 16 + 1
...     if 6 not in tree:
...         raise ValueError("no string table found")
...     if chunk_id not in tree[6]:
...         raise IndexError("string chunk not found")
...     if 1033 not in tree[6][chunk_id]:
...         raise ValueError("wrong language for the string table")
...     chunk = read_table_chunk(tree[6][chunk_id][1033])
...     return chunk[id % 16]
... 
>>> resource_string(1)
'Hello'
>>> resource_string(2)
'World'

Et voilà ! Tout cela est bien entendu améliorable pour tenir en compte de pas mal de choses (les différentes langues par exemple), mais les bases sont là.