
Дисассемблер и экспериментальный декомпилятор для скриптов TLOU2 DC.

dconstruct — это инструмент реверс-инжиниринга для файлов DC-Script, используемых в The Last of Us Part II. Он включает дизассемблер и декомпилятор.
Он выводит файлы .asm, содержащие дизассемблированные структуры и байт-код, а также файлы .dcpl (DC Pseudo Language) с C-подобным псевдокодом.
Вы также можете вносить изменения в файлы через командную строку, включая замену целых структур с минимальными усилиями. Это делает создание модов, которые просто изменяют пару значений внутри файлов .bin, чрезвычайно простым.
-e, создание новых файлов для использования в модах.Сначала рекомендуется переместить распакованную директорию dconstruct в безопасное место, например C:\Program Files.
Чтобы сделать dconstruct максимально удобным в использовании, рекомендуется добавить каталог .\bin внутри папки dconstruct в ваш PATH. Подробнее можно узнать здесь или выполнить следующие быстрые шаги:

Убедитесь, что ваш путь заканчивается на "\bin", а не на "\dconstruct".
Нажмите «ОК» во всех открытых диалогах.
Чтобы проверить, что всё сработало, откройте новую командную строку и введите dconstruct --about. Вы должны увидеть вывод программы и никаких сообщений об ошибках.
Выполните в командной строке команду, подобную этой, чтобы сгенерировать первый дизассемблированный файл:```shell dconstruct my_bin_file.bin
Затем это создаст файл с именем `my_bin_file.bin.asm` в той же директории, что и ваш входной файл. Вы можете открыть этот файл с помощью текстового/кодового редактора. Я рекомендую использовать что-то вроде VSCode, который предлагает расширенные возможности поиска и хорошо справляется с большими файлами. Стандартный блокнот Windows не рекомендуется.
Чтобы декомпилировать файл, добавьте флаг `--decompile` при выполнении команды.
# Аргументы командной строки
- `-i` - входной файл или папка. Может быть опущен, если передать путь к входным данным в качестве первого аргумента.
- `-o` - путь вывода. Если ваш входной путь - это папка, то этот параметр не может быть файлом. Если вывод не указан, файл .txt будет помещен рядом с входным файлом. Если входной файл является папкой и вывод не указан, программа создаст директорию 'output' в текущей рабочей директории и поместит туда все файлы.
- sidbase загружается из `sidbase.bin`, расположенного рядом с исполняемым файлом.
- `--no_decompile` - не создавать декомпилированный псевдокод в файл .dcpl. Файл будет помещен рядом с файлом .asm. По умолчанию этот параметр равен false.
- `--no_optimize` - не оптимизировать и не чистить код dcpl. Включает в себя встраивание вызовов функций, удаление неиспользуемых переменных, преобразование совместимых циклов for в циклы foreach и преобразование некоторых цепочек if-else в выражения match.
- `--pascal_case` - преобразовывать имена функций игры в Pascal Case в выводе dcpl, например, get-boolean -> GetBoolean.
- `--graphs` - создавать файлы .svg, содержащие графы потоков управления для всех декомпилированных функций. Каждый файл .bin получает свою собственную папку, содержащую все его графы. Это **значительно** замедляет скорость декомпиляции, поэтому не рекомендуется при декомпиляции большого количества файлов одновременно.
- `--emit_once` - запрещает повторное испускание одной и той же структуры в дизассемблированном коде. Если структура встречается несколько раз, только первый экземпляр будет полностью испущен, а все остальные вхождения будут заменены тегом `ALREADY_EMITTED`. Это может значительно уменьшить размер файла.
- `-e` - внести правку. Подробнее в разделе ниже.
- `--edit_file` - указать файл правок. Файл правок содержит одну правку на строку. Он использует тот же синтаксис, что и флаг -e.
# Что такое дизассемблер?
[Дизассемблер](https://en.wikipedia.org/wiki/Disassembler) — это инструмент, который читает бинарные инструкции (также известные как [байт-код](https://en.wikipedia.org/wiki/Bytecode) или [машинный код](https://en.wikipedia.org/wiki/Machine_code)) и переводит каждую из них в читаемую человеком версию, называемую [мнемоникой](https://en.wikipedia.org/wiki/Assembly_language#Mnemonics). Дизассемблеры обычно не пытаются интерпретировать смысл этих инструкций, а просто преобразуют их 1:1 в читаемые версии. Например, инструкции:```arm
15 00 00 00
4A 01 01 00
43 31 01 00
1C 00 00 01
разбираются на следующие удобочитаемые версии:```arm LookupPointer r0, 0 LoadStaticU64Imm r1, 1 Move r49, r1 CallFf r0, r0, 1
Все числа в байт-коде записаны в [шестнадцатеричной системе](https://en.wikipedia.org/wiki/hexadecimal). Первый столбец в каждой строке представляет `opcode`, или тип выполняемой инструкции. Следующий столбец — регистр назначения, куда будет сохранён результат операции. Последние два столбца — операнды 1 и 2, которые являются либо регистрами, либо литеральными числами, над которыми выполняется операция. Не все инструкции используют все 4 байта; например, первая инструкция `LookupPointer` требует только одного операнда.
Дисассемблер dconstruct также добавляет некоторую дополнительную информацию, призванную немного упростить чтение инструкций. Он также вставляет метки (например, `L_0`), чтобы облегчить отслеживание ветвлений в коде.```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
Это полезно, когда вы хотите просмотреть необработанное содержимое файла, не позволяя программе делать слишком много предположений. Но это может быть трудночитаемо для больших блоков кода, так как отсутствует какая-либо структура. Здесь на помощь приходит декомпилятор.
Декомпилятор — это обратная сторона компилятора. Компилятор — это программа, которая берет написанный человеком код (например, C, Java, C++, ...) и создает машинные инструкции. В случае TLOU2 и многих других игр ND используемый язык сценариев называется 'DC', который по сути является версией языка программирования Racket>), а "машиной" является сама игра, которая выполняет инструкции во время работы. По сути, программисты пишут на DC и используют компилятор для преобразования этого кода в файлы .bin, поставляемые с игрой.
Декомпилятор берет дизассемблированный код, полученный выше, и создает так называемый псевдокод. Псевдокод — это попытка восстановить исходный исходный код, который использовался для создания необработанных инструкций. Это призвано значительно упростить понимание кода, однако процесс генерации псевдокода довольно сложен, так как существует множество различных версий исходного кода, которые могут сгенерировать конечный байт-код, а также оптимизации, происходящие во время компиляции.
В настоящее время вывод декомпилятора dconstruct не синтаксически похож на оригинальный DC. DC (т.е. Racket) — это функциональный язык программирования с уникальным синтаксисом, который чертовски нечитаем для программистов, не привыкших к нему. По этой причине я решил сделать псевдокод более похожим на C, что должно быть проще для чтения большинству людей. Однако в планах создание большего количества синтаксисов, включая Racket, а также версию на Python.
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
Исходя из этого, практически невозможно понять, что делает код.
## Дизассемблированный код с метками и таблицей символов```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
}
Теперь код стал гораздо более читаемым, но даже одна ветвь раздражает при чтении, если вы не привыкли читать ассемблер.
Не вдаваясь в подробности, граф потока управления (CFG) разбивает ассемблерный код на «узлы» по различным инструкциям ветвления. Это важно при анализе кода, чтобы выяснить, где «поток» программы может расходиться на разные пути, что может потребовать от нас создания переменных, операторов if, циклов for и т.д. Эти графы необходимо генерировать в фоновом режиме, но вы можете вывести их в виде изображений с помощью флага программы --graphs.
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)); }
Теперь назначение функции совершенно ясно: мы берем абсолютное значение аргумента, извлекаем квадратный корень из этого значения, а затем умножаем его на исходный знак аргумента. Например, `sqrt-sign(-9) = -3`.
## Optimization passes
dconstruct автоматически применяет оптимизационные проходы к псевдокоду. Вот несколько примеров:
### Function call inlining
#### Before```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)); }
### Foreach loops
### До```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); } }
### Выражения соответствия
### Перед```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" }; }
## Пример дизассемблированной структуры```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
}
}
}
Редактирование DC-файлов с помощью флага -e
Вы можете использовать флаг -e для применения изменений к DC-файлам. Эти изменения сохраняются в новой копии исходного файла, оставляя оригинал нетронутым. Можно указать несколько флагов -e одновременно, чтобы внести несколько изменений за раз.
Кроме того, вы можете указать программе путь к файлу правок. Файл правок содержит по одной правке на строку. Он использует тот же синтаксис, что и флаг -e, но может быть немного удобнее, если вы хотите применить несколько правок одновременно.
Редактирование происходит до дизассемблирования и декомпиляции, поэтому правка будет отображаться в сгенерированных файлах.
Каждая правка следует следующему синтаксису:```xml
Предположим, у вас есть структура следующего вида:```c++ [4] firearm-gameplay-def [0x11C28] { [0] float 0.7 // might represent the rate of fire, so i want to lower it for my mod ... }
Чтобы заменить первую переменную-член (индекс 0) значением с плавающей точкой 0.5, команда редактирования будет такой:
`-e 0x11C28[0]=0.5`
Структура, которую мы хотим отредактировать, находится по адресу `0x11C28`, и нам нужна первая переменная-член (0 слева от числа с плавающей точкой). Затем мы указываем новое значение после `=`, в данном случае 0.5. Если редактирование прошло успешно, программа покажет сообщение о том, что значение изменено с `0.7->0.5`.
Для файла редактирования просто уберите `-e` и поместите по одному редактированию на строку:
### edit_file.txt
0x11C28[0]=0.5
0x11C28[1]=0.2
...
## Типы переменных-членов
Структуры могут содержать переменные-члены разных типов:
- `float` — указывайте десятичные значения с точкой (например, 0.5).
- `int` — указывайте целые значения без точки (например, 42).
- `sid` (строковый идентификатор) — (подробнее ниже)
- `string` — в настоящее время не поддерживается для замены
- `structure` — путём замены указателя (подробнее ниже)
### Замена sid по имени:
`-e 0xABC[5]=ellie`
Команда ищет значение "ellie" в текущей базе sidbase. Если его нет, выдаётся предупреждение, и редактирование не применяется. Если значение найдено, фактическое хеш-значение (большое число) заменит текущее значение переменной-члена.
### Замена sid прямым ручным переопределением хеша:
`-e 0xABC[5]=#XXXXXXXXXXXXXXXX`
Символ # указывает на необработанное хеш-значение, которое будет применено напрямую без поиска.
### Замена структур-членов
Если структура содержит другую структуру в качестве члена, можно заменить всю структуру-член, присвоив ей адрес другой структуры.
Например, предположим, у вас есть следующее:```c++
[4] weapon-gameplay-def [0x0C523] {
...
[7] firearm-gameplay-def [0x11C28] {
...
}
}
Итак, weapon-gameplay-def содержит firearm-gameplay-def.
Чтобы заменить firearm-gameplay-def внутри weapon-gameplay-def на другой firearm-gameplay-def, находящийся по адресу 0x0ABC, правка будет:
-e 0x11C28[7]=0x0ABC
VS Code поддерживает создание пользовательских расширений для добавления подсветки синтаксиса в пользовательские языки. dconstruct поставляется с файлом .vsix, который добавляет эту поддержку для расширения файлов .dcpl. Поскольку это одноразовый язык, не предназначенный для повседневного программирования, я не загружаю расширение в marketplace, а вместо этого поставляю его в виде сырого файла .vsix. Чтобы установить это расширение в вашем VS Code, выполните следующую команду:```shell code --install-extension <path/to/extension/dcpl-lint-0.0.1.vsix>
или откройте палитру команд с помощью CTRL+SHIFT+P и введите "Install extension via VSIX", затем выберите файл .vsix.
После этого ваш код .dcpl должен выглядеть примерно так:

# Известные проблемы
Декомпилятор в настоящее время не завершён на 100%, поэтому он находится в «экспериментальном» состоянии. Если во время декомпиляции вы получаете предупреждения, не беспокойтесь: эти функции пока не поддерживаются, но, надеюсь, будут поддерживаться в будущем. В дополнение к этому есть и другие известные на данный момент проблемы:
- функции с одним выражением и интенсивным использованием сокращённых вычислений (особенно внутри структур) пока не реализованы. Я начал работу над алгоритмом для них, но не знаю, сколько времени это займёт, хотя эта задача имеет высокий приоритет
- некоторые типы указаны неверно, особенно типы аргументов
- пустые блоки if в конструкциях if/else могут приводить к странным отступам. Это опять же не всегда моя вина, поскольку существуют ветви, которые на самом деле не выполняют никакой работы, и это сложно обнаружить
Вместе с этим есть несколько проблем, которые, вероятно, не будут исправлены:
- возврат мусора, когда неизвестно, является ли функция void или нет
- тонны избыточного кода
# Планируемые функции
- полный формат вывода Racket и Python
# Особая благодарность
- **icemesh** – за предоставление базовых [структур для DC-файлов](https://github.com/icemesh/dc/tree/main/t2) и [его дизассемблера](https://github.com/icemesh/t2-dc-disasm), которые во многом послужили вдохновением.
- **Specilizer** – за его DC-Tool, также послуживший вдохновением для этой программы.
- **uxh** – за знания в области скриптования.
- **bigdragon** и **Wedge** – за бета-тестирование
- Всё сообщество моддинга Discord – за дружелюбие и помощь.
## Поддержка
Все мои инструменты и моды всегда будут на 100% бесплатными, но такие программы требуют много работы.
Если вы хотите поддержать меня, вы можете посетить мой Ko-fi:
[](https://ko-fi.com/deepquantum)
## Лицензия
Файлы, созданные вами с помощью этого мода, полностью ваши, и вы можете делать с ними всё, что захотите. Упоминание авторства приветствуется, но не является обязательным.
Сама программа лицензирована в соответствии с
[Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/).
Это означает, что вы можете делиться программой с другими, указывая авторство, но в настоящее время вам не разрешается изменять её или использовать в коммерческих целях.