Journal d'un développeur Java
Un jour dans la peau d'un créateur de mods pour Minecraft
Edition Spéciale - Le cauchemar du modding java sur code obfusqué
Bonjour à tous et merci d'être venus nombreux à cette conférence sur le modding Minecraft.
Bien que je l'ai déjà dit ingame je le redis ici sur le forum - la dernière version du launcher est spéciale.
En effet il s'agit de la première version du launcher réalisée sans utliser le Minecraft Coder Pack.
Comme vous le savez, il était de plus en plus dur pour moi d'intégrer les mods du launcher au fur et à mesure des versions de Minecraft.
La raison ? Les délais de plus en plus long pour la sortie du MinecraftCoder Pack, et son incapacité croissante à gérer des versions un peu moddées du jeu,
pour que je puissse rendre mes mods compatibles avec Optifine. Il s'en découlait alors des versions simplifiées, sans capes et Shaders :/
A l'heure où j'écris ces lignes, le Minecraft Coder Pack en est encore à la version 1.8.0 du jeu, qui est vieille de plus d'un an... (1.8.8 à ce jour)
Mais qu'est-ce que ce fameux MCP dont tous les moddeurs parlent ?
Il s'agit d'un kit qui permet de réaliser facilement des mods pour Minecraft :
- Décompilation automatique du code du jeu
- Correction automatique de toutes les erreurs de compilation
- Renommage automatique de tout le code avec des noms de variables clairs et de la documentation
En gros il vous permet d'avoir sous les yeux le code source du jeu presque comme le verrait un employé de Mojang.
Il vous suffit alors de faire vos modifications, et vous pouvez tester ou exporter votre mod le tout en un clic.
En vrai, le code du jeu est obfusqué et illisible, comme ceci par exemple après simple décompilation :
Code : Tout sélectionner
//Une classe prise au hasard dans Minecraft 1.8.8
public class ahu extends afh
{
public static final amm<ahu.a> a = amm.a("facing", ahu.a.class);
public static final amk b = amk.a("powered");
// [...]
public alz a(adm ☃, cj ☃, cq ☃, float ☃, float ☃, float ☃, int ☃, pr ☃)
{
alz ☃ = Q().a(b, Boolean.valueOf(false));
if (a(☃, ☃, ☃.d())) {
return ☃.a(a, ahu.a.a(☃, ☃.aP()));
}
for (cq ☃ : cq.c.a) {
if ((☃ != ☃) && (a(☃, ☃, ☃.d()))) {
return ☃.a(a, ahu.a.a(☃, ☃.aP()));
}
}
if (adm.a(☃, ☃.b())) {
return ☃.a(a, ahu.a.a(cq.b, ☃.aP()));
}
return ☃;
}
- Décompiler le jeu : Ça encore ça va, c'est pas trop dur, il suffit de lancer la décompilation et d'attendre que ça se fasse ^^
- Retrouver quels sont les fichiers qu'on veut modifier : un peu plus dur, vu que tout dans le code a un nom aléatoire... sauf les chaînes de caractère qui sont alors d'une aide précieuse !
- Faire les modification en adaptant le code et en faisant un travail d'investigation : "alors, du coup, la référence à la classe Minecraft c'est azu ou cj dans cette version ? Ah ouais, ça ... donc ça... ok ! donc ça fait azh.c.d();, logique !"
- Corriger toutes les 142 $ùµ%@#§ erreurs de compilation ! C'est la partie la plus longue et fastidieuse.
- Recompiler, et hop ! On a le mod porté sur la dernière version du jeu.
S'il y a des développeurs dans la salle ... Ah, vous deux, au fond ?
Bon alors, déjà, merci d'être venus, donc, je vais détailler mon erreur préférée qui m'a fait perdre pas mal de cheveux :
Code : Tout sélectionner
// Dans un premier fichier
public class abc
{
public def a;
private int b;
// [...]
public enum a {
a,
b,
c;
}
}
// Et dans un autre fichier
public class ghi
{
private void a(abc.a ☃) {
if (☃ == abc.a.a) {
// [...]
}
}
}
On a deux classes abc et ghi, je veux modifier ghi, mais ghi ne compile pas. Pourquoi ?
Réponse, tenez vous bien, parce qu'en tentant d'accéder à l'élément a de l'énumération a de abc, le compilateur croit que l'on désigne le champ a : ben oui, ils ont le même nom.
Originellement, ils avaient un nom différent donc ça marchait, mais ensuite l'obfuscateur est passé et les deux se sont retrouvées avec le nom a. Cela marche, mais on ne peut plus le recompiler.
Et non, on ne peut pas les renommer, d'autres parties du code font référence au champ abc.a et à l'énumération abc.a : on est apparemment bloqués.
La solution est proprement immonde, mais c'est la seule que j'ai trouvé pour maintenir la compatibilité avec le reste du code donc sans renommer :
Code : Tout sélectionner
public class abc
{
public def a;
private int b;
// [...]
public abc.a a_a;
public abc.a a_b;
public abc.a a_c;
public enum a {
a,
b,
c;
static {
a_a = a;
a_b = b;
a_c = c;
}
}
}
// Et dans un autre fichier
public class ghi
{
private void a(abc.a ☃) {
if (☃ == abc.a_a) {
// [...]
}
}
}
En utilisant l'initialiseur statique dans abc.a, on force l'énumération à référencer d'elle-même le champ a_a sans essayer de référencer a.a
Ensuite on peut utiliser le champ abc.a_a sans que cela soulève d'ambiguïté pour le compilateur, et donc ça compile ! Miracle
... Génial non !?
tumbleweed.gif
Mais... Mais... Pourquoi la salle est-elle vide ?
Ah oui, une question d'un survivant, j'écoute ... Ce que je fais dans la vie ?
C'est très simple, le jour je programme, mais quand vient la nuit...
Comment ça, pas drôle ? Hé attendez ! Ne partez pas ! Je ne vous ai pas encore parlé du cauchemar des bonhommes de neige !
C'est quand tous les arguments d'une fonction ont le même nom ! ☃ ! Il faut alors retrouver qui va où est c'est super énervant !
... Ah, bon, la salle est maintenant complètement vide. Bon, je crois que c'est l'heure que j'aille dormir.
Bonne nuit feutre, bonne nuit tableau blanc. Merci de m'avoir accompagnés durant cette conférence qui a déchaîné les foules.
Et merci d'avoir lu, si vous en avez eu le courage, cet unique numéro du journal d'un moddeur incompris.
Cette version du launcher m'aura demandé trois jours de dur travail contre trois heures avec le MCP alors je compte sur vous pour en profiter !
C'était ORelio pour HelloMinecraft, à vous les studios !