
Um desmontador & decompilador experimental para scripts TLOU2 DC.
dconstruct é uma ferramenta de engenharia reversa para os arquivos DC-Script usados em The Last of Us Part II. Possui um desmontador e um descompilador.
Ele gera arquivos .asm contendo as estruturas desmontadas e bytecode, bem como arquivos .dcpl (DC Pseudo Language) contendo pseudo-código semelhante a C.
Você também pode fazer edições nos arquivos via linha de comando, incluindo a substituição de estruturas inteiras com pouco esforço. Isso torna extremamente fácil criar mods que simplesmente alteram alguns valores dentro dos arquivos .bin.
-e, criando novos arquivos que você pode usar para mods.Primeiro, é recomendado que você mova o diretório dconstruct descompactado para algum local seguro, como C:\Program Files.
Para tornar o dconstruct o mais fácil de usar possível, é recomendado que você adicione o diretório .\bin dentro da pasta dconstruct ao seu PATH. Você pode saber mais aqui, ou seguir estes passos rápidos:

dconstruct --about. Você deve ver alguma saída do programa e nenhuma mensagem de erro.Execute um comando como este na linha de comando para gerar seu primeiro arquivo desmontado:```shell dconstruct my_bin_file.bin
Isto irá então gerar um ficheiro chamado `my_bin_file.bin.asm` no mesmo diretório que o seu ficheiro de entrada. Pode então abrir esse ficheiro usando um editor de texto/código. Recomendo usar algo como VSCode que oferece funcionalidades avançadas de pesquisa e é bom a lidar com ficheiros grandes. O bloco de notas padrão do Windows não é recomendado.
Para descompilar um ficheiro, adicione a flag `--decompile` ao executar o comando.
# Argumentos da linha de comandos
- `-i` - ficheiro ou pasta de entrada. Pode ser omitido se for passado o caminho de entrada como primeiro argumento.
- `-o` - caminho de saída. Se o seu caminho de entrada for uma pasta, este não pode ser um ficheiro. Se não for especificada uma saída, o ficheiro .txt será colocado junto ao ficheiro de entrada. Se a entrada for uma pasta e não for especificada uma saída, o programa criará um diretório "output" no diretório de trabalho atual e colocará todos os ficheiros lá.
- A sidbase é carregada de `sidbase.bin` localizado junto ao executável.
- `--no_decompile` - não emite código pseudo decompilado para um ficheiro .dcpl. O ficheiro será colocado junto ao ficheiro .asm. Isto é falso por predefinição.
- `--no_optimize` - não otimiza e limpa o código dcpl. envolve a inlining de chamadas de funções, remoção de variáveis não utilizadas, transformação de loops for compatíveis em loops foreach e transformação de algumas cadeias if-else em expressões match.
- `--pascal_case` - converte os nomes das funções dos jogos para pascal case na saída dcpl, por exemplo, get-boolean -> GetBoolean.
- `--graphs` - emite ficheiros .svg contendo grafos de fluxo de controlo para todas as funções descompiladas. Cada ficheiro .bin tem a sua própria pasta contendo todos os seus grafos. Isto **significativamente** atrasa a velocidade de descompilação, por isso não é recomendado quando se descompila um grande número de ficheiros ao mesmo tempo.
- `--emit_once` - proíbe que a mesma estrutura seja emitida duas vezes na desmontagem. Se uma estrutura aparecer múltiplas vezes, apenas a primeira instância será totalmente emitida, e todas as outras ocasiões serão substituídas por uma tag `ALREADY_EMITTED`. Isto pode reduzir significativamente o tamanho do ficheiro.
- `-e` - faz uma edição. Mais informações na secção abaixo.
- `--edit_file` - fornece um ficheiro de edição. um ficheiro de edição contém uma edição por linha. usa a mesma sintaxe que a flag -e.
# O que é um desassemblador?
Um [desassemblador](https://en.wikipedia.org/wiki/Disassembler) é uma ferramenta que lê instruções binárias (também conhecido como [bytecode](https://en.wikipedia.org/wiki/Bytecode) ou [código de máquina](https://en.wikipedia.org/wiki/Machine_code)) e traduz cada uma numa versão legível por humanos chamada de [mnemónico](https://en.wikipedia.org/wiki/Assembly_language#Mnemonics). Os desassembladores geralmente não tentam interpretar muito sobre o significado destas instruções e apenas as transformam 1-1 nas suas versões legíveis. Por exemplo, as instruções:```arm
15 00 00 00
4A 01 01 00
43 31 01 00
1C 00 00 01
são desmontados nas seguintes versões legíveis por humanos:```arm LookupPointer r0, 0 LoadStaticU64Imm r1, 1 Move r49, r1 CallFf r0, r0, 1
Todos os números no bytecode são escritos em [hexadecimal](https://en.wikipedia.org/wiki/hexadecimal). A primeira coluna em cada linha representa o `opcode`, ou o tipo de instrução a ser executada. A próxima coluna é o registo de destino, onde o resultado da operação será armazenado. As duas últimas colunas são os operandos 1 e 2, que são registos ou números literais sobre os quais a operação será realizada. Nem todas as instruções usam todos os 4 bytes; por exemplo, a primeira instrução `LookupPointer` precisa apenas de um operando.
O desmontador dconstruct também adiciona algumas informações adicionais destinadas a tornar a leitura das instruções um pouco mais fácil. Ele também insere rótulos (e.g. `L_0`) para tornar mais fácil rastrear a ramificação no código.```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
Isso é útil quando você deseja visualizar o conteúdo bruto do arquivo sem que o programa faça muitas suposições. Mas pode ser difícil de ler para grandes blocos de código, pois não há estrutura alguma. É aí que entra um descompilador.
Um descompilador é o inverso de um compilador. Um compilador é um programa que recebe código escrito por humanos (como C, Java, C++, ...) e produz instruções de máquina. No caso de TLOU2 e muitos outros jogos da ND, a linguagem de script usada é chamada 'DC', que é basicamente uma versão da linguagem de programação Racket, e a "máquina" é o próprio jogo que executa as instruções enquanto o jogo está rodando. Essencialmente, os programadores escrevem DC e usam um compilador para transformar esse código nos arquivos .bin que são enviados com o jogo.
Um descompilador pega o código desmontado acima e produz o que é conhecido como pseudocódigo. Pseudocódigo é uma tentativa de reconstruir o código-fonte original que foi usado para gerar as instruções brutas. Isso tem como objetivo tornar a compreensão do código significativamente mais fácil, no entanto, o processo de geração de pseudocódigo é bastante complexo, pois existem muitas versões diferentes de código-fonte que podem gerar o bytecode final, além de otimizações que ocorrem durante a compilação.
Atualmente, a saída do descompilador dconstruct não é sintaticamente semelhante ao DC original. DC (ou seja, Racket) é uma linguagem de programação funcional com uma sintaxe única que é f*cking ilegível para programadores que não estão acostumados com ela. Por esse motivo, escolhi fazer o pseudocódigo se parecer mais com C por enquanto, o que deve ser mais fácil de ler para a maioria das pessoas. No entanto, está planejada a criação de mais sintaxes, incluindo Racket e uma versão em 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
A partir disso, é virtualmente impossível saber o que o código está fazendo.
## Código desmontado com rótulos e tabela de símbolos```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
}
O código agora é muito mais legível, mas mesmo o único branch é irritante de ler se você não está acostumado a ler assembly.
Sem entrar em muitos detalhes aqui, um Grafo de Fluxo de Controle (CFG) divide o código assembly em "nós" ao longo das várias instruções de branch. Isso é fundamental quando analisamos o código para descobrir onde o "fluxo" do programa pode divergir em diferentes caminhos, o que pode exigir que emitamos variáveis, instruções if, loops for, etc.. Esses gráficos precisam ser gerados em segundo plano, mas você pode imprimi-los em imagens usando a flag --graphs do programa.
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)); }
O propósito da função está agora muito claro: obtemos o valor absoluto do argumento, extraímos a raiz quadrada desse valor e depois multiplicamos pelo sinal original do argumento. Por exemplo, `sqrt-sign(-9) = -3`.
## Passos de otimização
O dconstruct aplica automaticamente passos de otimização ao pseudo-código. Aqui estão alguns exemplos:
### Inline de chamada de função
#### Antes```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)); }
### Laços foreach
### Antes```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); } }
### Expressões de correspondência
### Antes```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" }; }
## Exemplo de uma estrutura desmontada```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
}
}
}
Editando arquivos DC usando a flag -e
Você pode usar a flag -e para aplicar edições a arquivos DC. Essas edições são salvas em uma nova cópia do arquivo original, deixando o original intocado. Várias flags -e podem ser especificadas ao mesmo tempo para fazer várias edições de uma vez.
Alternativamente, você pode fornecer ao programa um caminho para um arquivo de edição. Um arquivo de edição contém uma edição por linha. Ele usa a mesma sintaxe da flag -e, mas deve ser um pouco mais fácil de usar se você quiser aplicar várias edições de uma vez.
A edição ocorre antes da desmontagem e descompilação, então a edição aparecerá nos arquivos gerados.
Cada edição segue esta sintaxe:```xml
[]= ``` - ``: O endereço de memória da estrutura que você deseja editar (em hexadecimal, começando com `0x`. É mais fácil copiar e colar de uma versão desmontada do arquivo que você deseja editar). - ``: O índice da variável membro dentro da estrutura. Equivalente ao número que você pode ver à esquerda do membro. - ``: O novo valor a ser atribuído a esse membro. Deve ter o mesmo tamanho. (ints e floats têm tamanho 4, sids/structs têm tamanho 8). O programa NÃO verificará se as estruturas são do mesmo tipo.Suponha que você tenha uma estrutura como esta:```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 ... }
Para substituir a primeira variável membro (índice 0) pelo valor float 0.5, o comando de edição seria:
`-e 0x11C28[0]=0.5`
A estrutura que queremos editar está em `0x11C28`, e queremos a primeira variável membro (o 0 à esquerda do float). Em seguida, colocamos o novo valor após o `=`, 0.5 neste caso. Se a edição for bem-sucedida, o programa mostrará uma mensagem indicando que o valor foi alterado de `0.7->0.5`.
Para um arquivo de edição, simplesmente remova o `-e` e coloque uma edição por linha:
### edit_file.txt
0x11C28[0]=0.5
0x11C28[1]=0.2
...
## Tipos de Variáveis Membro
As estruturas podem ter diferentes tipos de variáveis membro:
- `float` - Especifique valores decimais com um ponto (ex.: 0.5).
- `int` - Especifique valores inteiros sem ponto (ex.: 42).
- `sid` (identificador de string) - (mais informações abaixo)
- `string` - atualmente não suportado para substituição
- `structure` - substituindo um ponteiro (mais informações abaixo)
### Substituindo sid por pesquisa de nome:
`-e 0xABC[5]=ellie`
Isso procura o valor "ellie" no sidbase atual. Se não existir, um aviso é emitido e nenhuma edição é aplicada. Se o valor for encontrado, o valor hash real (um número grande) substituirá o valor atual na variável membro.
### Substituindo sid por substituição manual direta de hash:
`-e 0xABC[5]=#XXXXXXXXXXXXXXXX`
O # indica um valor hash bruto, que será aplicado diretamente sem pesquisa.
### Substituindo estruturas membro
Se uma estrutura contém outra estrutura como membro, você pode substituir toda a estrutura membro atribuindo a ela o endereço de outra estrutura.
Por exemplo, suponha que você tenha o seguinte:```c++
[4] weapon-gameplay-def [0x0C523] {
...
[7] firearm-gameplay-def [0x11C28] {
...
}
}
Portanto, weapon-gameplay-def contém um firearm-gameplay-def.
Para substituir o firearm-gameplay-def dentro do weapon-gameplay-def por um firearm-gameplay-def diferente localizado no endereço 0x0ABC, a edição será:
-e 0x11C28[7]=0x0ABC
O VS Code suporta a criação de extensões personalizadas para adicionar realce de sintaxe a linguagens personalizadas. O dconstruct vem com um arquivo .vsix que adiciona esse suporte para a extensão de arquivo .dcpl. Por ser uma linguagem de uso único não realmente destinada à programação do dia a dia, não estou enviando a extensão para o marketplace e, em vez disso, a estou distribuindo como um arquivo .vsix bruto. Para instalar esta extensão no seu VS Code, execute o seguinte comando:```shell code --install-extension <path/to/extension/dcpl-lint-0.0.1.vsix>
ou abra a paleta de comandos com CTRL+SHIFT+P e digite "Instalar extensão via VSIX" e selecione o arquivo .vsix.
Após isso, seu código .dcpl deve ficar algo assim:

# Problemas conhecidos
O descompilador atualmente não está 100% completo e, portanto, está em um estado "experimental". Se você receber avisos durante a descompilação, não se preocupe, pois essas funções não são suportadas atualmente, mas esperançosamente serão no futuro. Junto com estas, há mais problemas conhecidos no momento:
- funções de expressão única com curto-circuito pesado (particularmente aquelas dentro de structs) ainda não foram implementadas. Comecei a trabalhar no algoritmo para estas, mas não sei quanto tempo levará para terminar, embora isso esteja no topo da lista de prioridades.
- certos tipos estão incorretos, particularmente tipos de argumentos.
- blocos if vazios em instruções if/else podem causar indentação estranha. isso novamente não é sempre minha culpa, pois existem certos ramos que na verdade não fazem nenhum trabalho real, o que é difícil de detectar.
Junto com estes, há alguns problemas que provavelmente não serão corrigidos:
- retornar lixo quando não se sabe se uma função é void ou não
- toneladas de código redundante
# Funcionalidades planejadas
- um formato de saída completo em Racket e Python
# Agradecimentos especiais
- **icemesh** – por fornecer as [estruturas subjacentes para os arquivos DC](https://github.com/icemesh/dc/tree/main/t2) e [seu desmontador](https://github.com/icemesh/t2-dc-disasm), que serviu amplamente como inspiração.
- **Specilizer** – por sua DC-Tool, também uma inspiração para este programa.
- **uxh** – por conhecimento em scripting.
- **bigdragon** & **Wedge** por testes beta
- Toda a comunidade de modding no Discord – por serem amigáveis e prestativos.
## Suporte
Todas as minhas ferramentas e mods sempre serão 100% gratuitos, mas programas como este exigem muito trabalho.
Se você gostaria de me apoiar, pode visitar meu Ko-fi:
[](https://ko-fi.com/deepquantum)
## Licença
Os arquivos que você criar usando este mod são inteiramente seus e você é livre para fazer com eles o que quiser. Créditos seriam apreciados, mas não são estritamente exigidos.
O programa em si está licenciado sob a
[Licença Creative Commons Atribuição-NãoComercial-SemDerivações 4.0 Internacional](https://creativecommons.org/licenses/by-nc-nd/4.0/).
Isso significa que você tem permissão para compartilhar o programa com outras pessoas, desde que dê créditos, mas atualmente não tem permissão para modificá-lo nem monetizá-lo.