
Un désassembleur et décompilateur expérimental pour les scripts TLOU2 DC.

dconstruct est un outil de rétro-ingénierie pour les fichiers DC-Script utilisés dans The Last of Us Part II. Il dispose d'un désassembleur et d'un décompilateur.
Il produit des fichiers .asm contenant les structures et bytecode désassemblés, ainsi que des fichiers .dcpl (DC Pseudo Language) contenant du pseudo-code proche du C.
Vous pouvez également modifier des fichiers via la ligne de commande, y compris remplacer des structures entières avec peu d'effort. Cela rend la création de mods qui changent simplement quelques valeurs dans les fichiers .bin extrêmement facile.
-e, créant de nouveaux fichiers utilisables pour les mods.Tout d'abord, il est recommandé de déplacer le répertoire dconstruct décompressé vers un emplacement sûr, par exemple C:\Program Files.
Pour faciliter au maximum l'utilisation de dconstruct, il est recommandé d'ajouter le répertoire .\bin (situé dans le dossier dconstruct) à votre PATH. Vous trouverez plus d'informations ici, ou suivez ces étapes rapides :

Assurez-vous que votre chemin se termine par "\bin" et NON par "\dconstruct".
Cliquez sur 'OK' dans toutes les boîtes de dialogue ouvertes.
Pour vérifier que cela a fonctionné, ouvrez une nouvelle invite de commande et tapez dconstruct --about. Vous devriez voir une sortie du programme et aucun message d'erreur.
Exécutez une commande comme celle-ci dans la ligne de commande pour générer votre premier fichier désassemblé :```shell dconstruct my_bin_file.bin
Cela produira ensuite un fichier nommé `my_bin_file.bin.asm` dans le même répertoire que votre fichier d'entrée. Vous pouvez ensuite ouvrir ce fichier à l'aide d'un éditeur de texte/code. Je recommande d'utiliser quelque chose comme VSCode qui offre des fonctionnalités de recherche avancées et est adapté aux fichiers volumineux. Le bloc-notes Windows standard n'est pas recommandé.
Pour décompiler un fichier, ajoutez le drapeau `--decompile` lors de l'exécution de la commande.
# Arguments de ligne de commande
- `-i` - fichier ou dossier d'entrée. Peut être omis si le chemin d'entrée est passé comme premier argument.
- `-o` - chemin de sortie. Si votre chemin d'entrée est un dossier, cela ne peut pas être un fichier. Si aucune sortie n'est spécifiée, le fichier .txt sera placé à côté du fichier d'entrée. Si l'entrée est un dossier et qu'aucune sortie n'est spécifiée, le programme créera un répertoire "output" dans le répertoire de travail actuel et y placera tous les fichiers.
- La base sid est chargée à partir de `sidbase.bin` situé à côté de l'exécutable.
- `--no_decompile` - ne pas émettre de code pseudo décompilé dans un fichier .dcpl. Le fichier sera placé à côté du fichier .asm. Ceci est faux par défaut.
- `--no_optimize` - ne pas optimiser et nettoyer le code dcpl. Cela implique l'inlining des appels de fonctions, la suppression des variables inutilisées, la transformation des boucles for compatibles en boucles foreach, et la conversion de certaines chaînes if-else en expressions match.
- `--pascal_case` - convertir les noms de fonctions du jeu en casse Pascal dans la sortie dcpl, par exemple get-boolean -> GetBoolean.
- `--graphs` - émettre des fichiers .svg contenant des graphes de flux de contrôle pour toutes les fonctions décompilées. Chaque fichier .bin obtient son propre dossier contenant tous ses graphes. Cela ralentit **considérablement** la vitesse de décompilation, il n'est donc pas recommandé lors de la décompilation d'un grand nombre de fichiers en même temps.
- `--emit_once` - interdit à la même structure d'être émise deux fois dans le désassemblage. Si une structure apparaît plusieurs fois, seule la première instance sera entièrement émise, et toutes les autres occurrences seront remplacées par une balise `ALREADY_EMITTED`. Cela peut réduire considérablement la taille du fichier.
- `-e` - effectuer une modification. Plus d'informations dans la section ci-dessous.
- `--edit_file` - fournir un fichier d'édition. Un fichier d'édition contient une modification par ligne. Il utilise la même syntaxe que le drapeau -e.
# Qu'est-ce qu'un désassembleur ?
Un [désassembleur](https://en.wikipedia.org/wiki/Disassembler) est un outil qui lit des instructions binaires (aussi appelées [bytecode](https://en.wikipedia.org/wiki/Bytecode) ou [code machine](https://en.wikipedia.org/wiki/Machine_code)) et traduit chacune en une version lisible par l'humain appelée [mnémotechnique](https://en.wikipedia.org/wiki/Assembly_language#Mnemonics). Les désassembleurs ne tentent généralement pas d'interpréter
beaucoup la signification de ces instructions et les transforment simplement 1-1 en leurs versions lisibles. Par exemple, les instructions :```arm
15 00 00 00
4A 01 01 00
43 31 01 00
1C 00 00 01
sont désassemblés dans les versions lisibles par l'homme suivantes :```arm LookupPointer r0, 0 LoadStaticU64Imm r1, 1 Move r49, r1 CallFf r0, r0, 1
Tous les nombres dans le bytecode sont écrits en [hexadécimal](https://en.wikipedia.org/wiki/hexadecimal). La première colonne de chaque ligne représente l'`opcode`, ou le type d'instruction à exécuter. La colonne suivante est le registre de destination, où le résultat de l'opération sera stocké. Les deux dernières colonnes sont les opérandes 1 et 2, qui sont soit des registres, soit des nombres littéraux sur lesquels l'opération sera effectuée. Toutes les instructions n'utilisent pas les 4 octets ; par exemple, la première instruction `LookupPointer` ne nécessite qu'un seul opérande.
Le désassembleur dconstruct ajoute également quelques informations supplémentaires destinées à faciliter la lecture des instructions. Il insère aussi des étiquettes (par ex. `L_0`) pour rendre les branchements dans le code plus faciles à suivre.```arm
15 00 00 00 LookupPointer r0, 0 r0 = ST[0] -> <is-player-abby?>
4A 01 01 00 LoadStaticU64Imm r1, 1 r1 = ST[1] -> <player>
43 31 01 00 Move r49, r1 r49 = player
1C 00 00 01 CallFf r0, r0, 1 r0 = is-player-abby?(player)
2F 0D 00 00 BranchIfNot r0, 0xD IF NOT r0 => L_0
Ceci est utile lorsque vous souhaitez examiner le contenu brut du fichier sans que le programme ne fasse trop de suppositions. Mais cela peut être difficile à lire pour de gros blocs de code, car il n'y a aucune structure. C'est là qu'intervient un décompilateur.
Un décompilateur est l'inverse d'un compilateur. Un compilateur est un programme qui prend du code écrit par un humain (comme C, Java, C++, ...) et produit des instructions machine. Dans le cas de TLOU2, et de nombreux autres jeux ND, le langage de script utilisé est appelé 'DC', qui est essentiellement une version du langage de programmation Racket>), et la « machine » est simplement le jeu lui-même qui exécute les instructions pendant que le jeu tourne. En gros, les programmeurs écrivent du DC et utilisent un compilateur pour transformer ce code en fichiers .bin livrés avec le jeu.
Un décompilateur prend le code désassemblé ci-dessus et produit ce qu'on appelle du pseudo-code. Le pseudo-code est une tentative de reconstruire le code source original qui a servi à générer les instructions brutes. Cela vise à rendre la compréhension du code beaucoup plus facile, cependant, le processus de génération de pseudo-code est assez complexe car il existe de nombreuses versions différentes de code source pouvant générer le byte code final, ainsi que des optimisations qui ont lieu lors de la compilation.
Actuellement, la sortie du décompilateur dconstruct n'est pas syntaxiquement similaire au DC original. DC (c'est-à-dire Racket) est un langage de programmation fonctionnel avec une syntaxe unique qui est f*cking illisible pour les programmeurs qui n'y sont pas habitués. Pour cette raison, j'ai choisi de faire ressembler le pseudo-code à du C pour l'instant, ce qui devrait être plus facile à lire pour la plupart des gens. Cependant, la création de plusieurs syntaxes incluant Racket ainsi qu'une version Python est prévue.
43 00 31 00 15 01 00 00 43 02 00 00 43 31 02 00 1B 01 01 01 43 02 00 00 40 03 01 00 24 02 02 03 2F 0B 02 00 40 02 02 00 2D 0C 00 00 40 02 03 00 43 03 02 00 15 04 04 00 43 05 01 00 43 31 05 00 1C 04 04 01 07 03 03 04 43 01 03 00 00 01 01 00
À partir de cela, il est virtuellement impossible de dire ce que fait le code.
## Code désassemblé avec étiquettes & table des symboles```arm
sqrt-sign = script-lambda [0x9A8D8] {
[1 args]
0000 0x09A928 43 00 31 00 Move r0, r49 r0 = arg_0
0001 0x09A930 15 01 00 00 LookupPointer r1, 0 r1 = ST[0] -> <absf>
0002 0x09A938 43 02 00 00 Move r2, r0 r2 = arg_0
0003 0x09A940 43 31 02 00 Move r49, r2 r49 = arg_0
0004 0x09A948 1B 01 01 01 Call r1, r1, 1 r1 = absf(arg_0)
0005 0x09A950 43 02 00 00 Move r2, r0 r2 = arg_0
0006 0x09A958 40 03 01 00 LoadStaticFloatImm r3, 1 r3 = ST[1] -> <0.000000>
0007 0x09A960 24 02 02 03 FGreaterThanEqual r2, r2, r3 r2 = r2 >= r3
0008 0x09A968 2F 0B 02 00 BranchIfNot r2, 0xB IF NOT r2 => L_0
0009 0x09A970 40 02 02 00 LoadStaticFloatImm r2, 2 r2 = ST[2] -> <1.000000>
000A 0x09A978 2D 0C 00 00 Branch 0xC GOTO => L_1
L_0:
000B 0x09A980 40 02 03 00 LoadStaticFloatImm r2, 3 r2 = ST[3] -> <-1.000000>
L_1:
000C 0x09A988 43 03 02 00 Move r3, r2 r3 = -1.000000
000D 0x09A990 15 04 04 00 LookupPointer r4, 4 r4 = ST[4] -> <sqrt>
000E 0x09A998 43 05 01 00 Move r5, r1 r5 = RET_absf
000F 0x09A9A0 43 31 05 00 Move r49, r5 r49 = RET_absf
0010 0x09A9A8 1C 04 04 01 CallFf r4, r4, 1 r4 = sqrt(RET_absf)
0011 0x09A9B0 07 03 03 04 FMul r3, r3, r4 -1.000000 = -1.000000 * RET_sqrt
0012 0x09A9B8 43 01 03 00 Move r1, r3 r1 = -1.000000
0013 0x09A9C0 00 01 01 00 Return r1 Return
SYMBOL TABLE:
0000 0x09A9C8 function: absf
0001 0x09A9D0 float: 0.000000
0002 0x09A9D8 float: 1.000000
0003 0x09A9E0 float: -1.000000
0004 0x09A9E8 function: sqrt
}
Le code est maintenant bien plus lisible, mais même la branche unique est ennuyeuse à lire si vous n'êtes pas habitué à lire l'assembleur.
Sans entrer dans trop de détails ici, un graphe de flot de contrôle (CFG) divise le code assembleur en « nœuds » le long des différentes instructions de branchement. C'est essentiel lorsque nous analysons le code pour déterminer où le « flot » du programme peut diverger en différents chemins, ce qui peut nous obliger à émettre des variables, des instructions if, des boucles for, etc. Ces graphes doivent être générés en arrière-plan, mais vous pouvez les imprimer sous forme d'images en utilisant le drapeau --graphs du programme.
u64? sqrt-sign(f32 arg_0) { f32 var_1; if (arg_0 >= 0.00) { var_1 = 1.00; } else { var_1 = -1.00; } return var_1 * sqrt(absf(arg_0)); }
Le but de la fonction est désormais très clair : nous prenons la valeur absolue de l'argument, prenons la racine carrée de cette valeur, puis la multiplions par le signe original de l'argument. Ainsi par exemple, `sqrt-sign(-9) = -3`.
## Passes d'optimisation
### Inlining d'appels de fonctions
#### Avant```c
u64? set-arrow-explosive-handle-rootvars() {
u64? var_0 = get-uint64(fx-handle, self);
u64? var_1 = get-float(kill, self);
set-effect-float(var_0, killradius, var_1);
u64? var_2 = get-uint64(fx-handle, self);
u64? var_3 = get-float(strong, self);
set-effect-float(var_2, strongradius, var_3);
u64? var_4 = get-uint64(fx-handle, self);
u64? var_5 = get-float(weak, self);
u64? var_6 = set-effect-float(var_4, weakradius, var_5);
return var_6;
}
u64? set-arrow-explosive-handle-rootvars() { set-effect-float(get-uint64(fx-handle, self), killradius, get-float(kill, self)); set-effect-float(get-uint64(fx-handle, self), strongradius, get-float(strong, self)); return set-effect-float(get-uint64(fx-handle, self), weakradius, get-float(weak, self)); }
### Boucles foreach
### Avant```c#
u64? bmm-deactivate-all(u64? arg_0) {
u64? var_0 = darray-count(arg_0);
begin-foreach();
for (u64 i = 0; i < var_0; i++) {
u64? var_1 = darray-at(arg_0, i);
u16 var_2;
if (var_1 && *(u16*)(var_1 + 12) == 7) {
var_2 = *(u64*)var_1;
} else if (var_1 && *(u16*)(var_1 + 12) == 5) {
var_2 = *(u64*)var_1;
} else if (var_1 && *(u16*)(var_1 + 12) == 4) {
var_2 = *(u64*)var_1;
} else {
var_2 = 0;
}
net-send-event-all(deactivate, var_2);
}
u64? var_3 = end-foreach();
return var_3;
}
u64? bmm-deactivate-all(u64? arg_0) { foreach (u64? var_1 : arg_0) { u16 var_2; if (var_1 && (u16)(var_1 + 12) == 7) { var_2 = (u64)var_1; } else if (var_1 && (u16)(var_1 + 12) == 5) { var_2 = (u64)var_1; } else if (var_1 && (u16)(var_1 + 12) == 4) { var_2 = (u64)var_1; } else { var_2 = 0; } net-send-event-all(deactivate, var_2); } }
### Expressions de correspondance
### Avant```scala
string #C57EE0A64537AE8F(u16 arg_0) {
string var_0;
if (arg_0 == 0) {
var_0 = "Militia";
} else if (arg_0 == 1) {
var_0 = "Scars";
} else if (arg_0 == 2) {
var_0 = "Rattlers";
} else if (arg_0 == 3) {
var_0 = "Infected";
} else if (arg_0 == 4) {
var_0 = "Max Num Factions";
} else {
var_0 = "Invalid";
}
return var_0;
}
string #C57EE0A64537AE8F(u16 arg_0) { return match (arg_0) { 0 -> "Militia" 1 -> "Scars" 2 -> "Rattlers" 3 -> "Infected" 4 -> "Max Num Factions" else -> "Invalid" }; }
## Exemple de structure désassemblée```c
*ellie-weapons* = symbol-array [0x00190] {
[0] int: 6
[1] int: 0
[2] array [0x198] {size: 6} {
[0] anonymous struct [0x780] {
[0] sid: pistol-beretta
}
[1] anonymous struct [0x788] {
[0] sid: pistol-revolver-taurus
}
[2] anonymous struct [0x790] {
[0] sid: rifle-remington-bolt
}
[3] anonymous struct [0x798] {
[0] sid: bow-ellie
}
[4] anonymous struct [0x7a0] {
[0] sid: shotgun-remington-pump
}
[5] anonymous struct [0x7a8] {
[0] sid: rifle-mpx5
}
}
}
Édition des fichiers DC à l'aide du drapeau -e
Vous pouvez utiliser le drapeau -e pour appliquer des modifications aux fichiers DC. Ces modifications sont enregistrées dans une nouvelle copie du fichier original, laissant l'original intact. Plusieurs drapeaux -e peuvent être spécifiés en même temps pour effectuer plusieurs modifications à la fois.
Alternativement, vous pouvez fournir au programme le chemin vers un fichier d'édition. Un fichier d'édition contient une modification par ligne. Il utilise la même syntaxe que le drapeau -e, mais devrait être un peu plus facile à utiliser si vous souhaitez appliquer plusieurs modifications à la fois.
L'édition a lieu avant le désassemblage et la décompilation, de sorte que la modification apparaîtra dans les fichiers générés.
Chaque modification suit cette syntaxe :```xml
Supposons que vous ayez une structure comme celle-ci :
[4] firearm-gameplay-def [0x11C28] {
[0] float 0.7 // might represent the rate of fire, so i want to lower it for my mod
...
}
```
Pour remplacer la première variable membre (index 0) par la valeur flottante 0.5, la commande d'édition serait :
`-e 0x11C28[0]=0.5`
La structure que nous voulons éditer est à `0x11C28`, et nous voulons la première variable membre (le 0 à gauche du flottant). Nous mettons ensuite la nouvelle valeur après le `=`, 0.5 dans ce cas. Si l'édition a réussi, le programme affichera un message indiquant que la valeur a été changée de `0.7->0.5`.
Pour un fichier d'édition, supprimez simplement le `-e` et mettez une édition par ligne :
### edit_file.txt
0x11C28[0]=0.5
0x11C28[1]=0.2
...
## Types de variables membres
Les structures peuvent avoir différents types de variables membres :
- `float` - Spécifiez des valeurs décimales avec un point (par exemple 0.5).
- `int` - Spécifiez des valeurs entières sans point (par exemple 42).
- `sid` (identifiant de chaîne) - (plus d'informations ci-dessous)
- `string` - actuellement non pris en charge pour le remplacement
- `structure` - en remplaçant un pointeur (plus d'informations ci-dessous)
### Remplacement de sid par recherche de nom :
`-e 0xABC[5]=ellie`
Cela recherche la valeur "ellie" dans la sidbase courante. Si elle n'existe pas, un avertissement est émis et aucune édition n'est appliquée. Si la valeur est trouvée, la valeur de hachage réelle (un grand nombre) remplacera la valeur actuelle à la variable membre.
### Remplacement de sid par surcharge manuelle directe du hachage :
`-e 0xABC[5]=#XXXXXXXXXXXXXXXX`
Le # indique une valeur de hachage brute, qui sera appliquée directement sans recherche.
### Remplacement de structures membres
Si une structure contient une autre structure comme membre, vous pouvez remplacer la structure membre entière en lui assignant l'adresse d'une autre structure.
Par exemple, supposons que vous ayez ce qui suit :```c++
[4] weapon-gameplay-def [0x0C523] {
...
[7] firearm-gameplay-def [0x11C28] {
...
}
}
```
Donc `weapon-gameplay-def` contient un `firearm-gameplay-def`.
Pour remplacer le `firearm-gameplay-def` à l'intérieur du `weapon-gameplay-def` par un autre `firearm-gameplay-def` situé à l'adresse `0x0ABC`, la modification sera :
`-e 0x11C28[7]=0x0ABC`
# Extension VS Code pour le langage DCPL
VS Code permet de créer des extensions personnalisées pour ajouter la coloration syntaxique à des langages personnalisés. dconstruct est livré avec un fichier .vsix qui ajoute cette prise en charge pour l'extension de fichier .dcpl. Parce que c'est un langage à usage unique qui n'est pas vraiment destiné à la programmation quotidienne, je ne télécharge pas l'extension sur le marketplace, et je la fournis plutôt sous forme de fichier .vsix brut. Pour installer cette extension dans votre VS Code, exécutez la commande suivante :```shell
code --install-extension <path/to/extension/dcpl-lint-0.0.1.vsix>
```
ou ouvrez la palette de commandes avec CTRL+SHIFT+P et tapez "Install extension via VSIX" et sélectionnez le fichier .vsix.
Après cela, votre code .dcpl devrait ressembler à ceci :

# Problèmes connus
Le décompilateur n'est actuellement pas 100% complet et est donc dans un état "expérimental". Si vous obtenez des avertissements lors de la décompilation, ne vous inquiétez pas car ces fonctions ne sont actuellement pas prises en charge, mais le seront espérons-le à l'avenir. En dehors de cela, il y a plusieurs problèmes connus pour le moment :
- les fonctions à expression unique avec court-circuitage intensif (en particulier celles dans les structures) ne sont pas encore implémentées. J'ai commencé à travailler sur l'algorithme pour celles-ci mais je ne sais pas combien de temps cela prendra, même si cela figure en tête de liste des priorités
- certains types sont incorrects, en particulier les types d'arguments
- les blocs if vides dans les instructions if/else peuvent causer une indentation étrange. Ce n'est pas toujours de ma faute, car certaines branches ne font en fait aucun travail réel, ce qui est difficile à détecter
En plus de ceux-ci, il y a quelques problèmes qui ne seront probablement pas corrigés :
- retourner des données inutiles lorsqu'on ne sait pas si une fonction est void ou non
- beaucoup de code redondant
# Fonctionnalités prévues
- un format de sortie complet pour Racket et Python
# Remerciements spéciaux
- **icemesh** – pour avoir fourni les [structures sous-jacentes des fichiers DC](https://github.com/icemesh/dc/tree/main/t2) et [son désassembleur](https://github.com/icemesh/t2-dc-disasm), qui ont largement servi d'inspiration.
- **Specilizer** – pour son DC-Tool, également une inspiration pour ce programme.
- **uxh** – pour ses connaissances en scripting.
- **bigdragon** & **Wedge** pour les tests bêta
- Toute la communauté de modding Discord – pour leur amabilité et leur aide.
## Soutien
Tous mes outils et mods seront toujours 100% gratuits, mais des programmes comme celui-ci demandent beaucoup de travail.
Si vous souhaitez me soutenir, vous pouvez visiter mon Ko-fi :
[](https://ko-fi.com/deepquantum)
## Licence
Les fichiers que vous créez avec ce mod sont entièrement vôtres et vous êtes libre d'en faire ce que vous voulez. Un crédit serait apprécié mais n'est pas strictement requis.
Le programme lui-même est sous licence [Creative Commons Attribution - Pas d'Utilisation Commerciale - Pas de Modification 4.0 International](https://creativecommons.org/licenses/by-nc-nd/4.0/).
Cela signifie que vous êtes autorisé à partager le programme avec d'autres si vous donnez du crédit, mais vous n'êtes actuellement pas autorisé à le modifier ni à le monétiser.