

dconstruct は、The Last of Us Part II で使用されている DC-Script ファイルのためのリバースエンジニアリングツールです。逆アセンブラと逆コンパイラを備えています。
逆アセンブルされた構造体とバイトコードを含む.asmファイル、およびCライクな疑似コードを含む.dcpl(DC疑似言語)ファイルを出力します。
また、コマンドラインからファイルを編集することもでき、構造全体を簡単に置き換えることもできます。これにより、.binファイル内のいくつかの値を変更するだけのMODを非常に簡単に作成できます。
-e フラグを使用して編集を行い、MODに使用できる新しいファイルを作成まず、解凍した dconstruct ディレクトリを C:\Program Files などの安全な場所に移動することをお勧めします。
dconstruct をできるだけ使いやすくするために、dconstruct フォルダ内の .\bin ディレクトリを PATH に追加することをお勧めします。詳細はこちらをご覧いただくか、以下の簡単な手順に従ってください。

dconstruct --about と入力します。プログラムからの出力が表示され、エラーメッセージが表示されないはずです。以下のようなコマンドをコマンドラインで実行して、最初の逆アセンブルファイルを生成します:```shell dconstruct my_bin_file.bin
これにより、入力ファイルと同じディレクトリに `my_bin_file.bin.asm` というファイルが出力されます。そのファイルはテキスト/コードエディタで開くことができます。VSCodeのような高度な検索機能を備え、大きなファイルの処理に優れたエディタをお勧めします。標準のWindowsメモ帳は推奨しません。
ファイルを逆コンパイルするには、コマンド実行時に `--decompile` フラグを追加します。
# コマンドライン引数
- `-i` - 入力ファイルまたはフォルダ。最初の引数として入力パスを渡す場合は省略可能。
- `-o` - 出力パス。入力パスがフォルダの場合、これはファイルにすることはできません。出力が指定されない場合、.txtファイルは入力ファイルの隣に配置されます。入力がフォルダで出力が指定されない場合、プログラムはカレントワーキングディレクトリに "output" ディレクトリを作成し、すべてのファイルをそこに配置します。
- サイドベースは実行ファイルの隣にある `sidbase.bin` から読み込まれます。
- `--no_decompile` - 逆コンパイルされた疑似コードを .dcpl ファイルに出力しない。ファイルは .asm ファイルの隣に配置されます。デフォルトは false です。
- `--no_optimize` - dcpl コードの最適化とクリーンアップを行わない。関数呼び出しのインライン化、未使用変数の削除、互換性のある for ループの foreach ループへの変換、一部の if-else チェーンの match 式への変換などが含まれます。
- `--pascal_case` - ゲームの関数名を dcpl 出力でパスカルケースに変換する。例: get-boolean -> GetBoolean。
- `--graphs` - 逆コンパイルされたすべての関数の制御フローグラフを含む .svg ファイルを出力する。各 .bin ファイルには、そのグラフのすべてを含む独自のフォルダが作成されます。これにより逆コンパイル速度が **大幅に** 低下するため、多数のファイルを同時に逆コンパイルする場合には推奨されません。
- `--emit_once` - 同じ構造が逆アセンブル内で二度出力されることを防ぎます。構造が複数回出現する場合、最初のインスタンスのみ完全に出力され、その他の箇所は `ALREADY_EMITTED` タグに置き換えられます。これによりファイルサイズを大幅に削減できます。
- `-e` - 編集を行う。詳細は以下のセクションを参照。
- `--edit_file` - 編集ファイルを指定する。編集ファイルには1行に1つの編集が含まれます。`-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
All numbers in the bytecode are written in [hexadecimal](https://en.wikipedia.org/wiki/hexadecimal). The first column in each row represents the `opcode`, or the type of instruction to be executed. The next column is the destination register, where the result of the operation will be stored. The last two columns are operands 1 and 2, which are either registers or literal numbers on which the operation will be performed. Not all instructions use all 4 bytes, for example, the first `LookupPointer` instruction only needs one operand.
The dconstruct disassembler also adds some additional information meant to make reading the instructions a bit easier. It also inserts labels (e.g. `L_0`) to make branches in the code easier to trace.```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` となります。
## 最適化パス
dconstructは擬似コードに自動的に最適化パスを適用します。以下にいくつかの例を示します。
### 関数呼び出しのインライン展開
#### 適用前```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 ループ
### 以前```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
}
}
}
-e フラグを使用した DC ファイルの編集
-e フラグを使用して、DC ファイルに編集を適用できます。これらの編集は元のファイルの新しいコピーに保存され、元のファイルは変更されません。複数の -e フラグを同時に指定して、複数の編集を一度に行うこともできます。
または、プログラムに編集ファイルへのパスを指定することもできます。編集ファイルには、1行に1つの編集が含まれています。これは -e フラグと同じ構文を使用しますが、一度に複数の編集を適用したい場合には少し使いやすいはずです。
編集は 逆アセンブルと逆コンパイルの前に行われる ため、編集内容は生成されたファイルに反映されます。
各編集は次の構文に従います:```xml
[]= ``` - ``: 編集したい構造体のメモリアドレス(先頭に `0x` が付いた16進数)。編集したいファイルの逆アセンブル版からコピーペーストするのが最も簡単です。 - ``: 構造体内部のメンバ変数のインデックス。メンバの左側に表示されている数字と同じです。 - ``: そのメンバに割り当てる新しい値。同じサイズでなければなりません(int と float はサイズ4、sid/struct はサイズ8)。プログラムは構造体が同じ型かどうかをチェックしません。次のような構造体があるとします:```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`を省略し、1行に1つの編集を記述します:
### edit_file.txt
0x11C28[0]=0.5
0x11C28[1]=0.2
...
## メンバ変数の種類
構造体は異なる型のメンバ変数を持つことができます:
- `float` - 小数点を含む10進数値を指定します(例:0.5)。
- `int` - 小数点を含まない整数値を指定します(例:42)。
- `sid` (文字列識別子)- (詳細は以下)
- `string` - 現在、置換はサポートされていません
- `structure` - ポインタを置き換えることで実現(詳細は以下)
### 名前検索による sid の置換:
`-e 0xABC[5]=ellie`
現在の sidbase 内で値 "ellie" を検索します。存在しない場合は警告が発行され、編集は適用されません。値が見つかると、実際のハッシュ値(大きな数値)がメンバ変数の現在の値と置き換わります。
### 直接手動ハッシュ上書きによる sid の置換:
`-e 0xABC[5]=#XXXXXXXXXXXXXXXX`
`#`は生のハッシュ値を示し、検索なしで直接適用されます。
### メンバ構造体の置換
構造体が別の構造体をメンバとして含む場合、そのメンバ構造体全体を別の構造体のアドレスを割り当てることで置き換えることができます。
例えば、次のような場合を考えてみます:```c++
[4] weapon-gameplay-def [0x0C523] {
...
[7] firearm-gameplay-def [0x11C28] {
...
}
}
So weapon-gameplay-def には firearm-gameplay-def が含まれています。
weapon-gameplay-def 内の firearm-gameplay-def を、アドレス 0x0ABC にある別の firearm-gameplay-def に置き換える場合、編集は次のようになります。
-e 0x11C28[7]=0x0ABC
VS Code はカスタム拡張機能を作成して、カスタム言語にシンタックスハイライトを追加することをサポートしています。dconstruct には .vsix ファイルが同梱されており、これにより .dcpl ファイル拡張子に対するサポートが追加されます。この言語は日常的なプログラミングを目的としたものではなく、単一用途の言語であるため、拡張機能をマーケットプレイスにアップロードせず、代わりに生の .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/else文内の空のifブロックが奇妙なインデントを引き起こす可能性があります。これはまた、実際に何も仕事をしないブランチが存在するためであり、それを検出するのは難しいため、必ずしも私のせいではありません。
これらに加えて、おそらく修正されないであろう問題もあります。
- 関数が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コミュニティ全体 – 友好的で助けになることに対して。
## サポート
私のツールとMODは常に100%無料ですが、このようなプログラムには多大な労力がかかります。
もし私を支援したい場合は、Ko-fiをご覧ください:
[](https://ko-fi.com/deepquantum)
## ライセンス
このMODを使用して作成したファイルは完全にあなたのものであり、それらを自由に扱うことができます。クレジットをいただけるとありがたいですが、必須ではありません。
プログラム自体は、[クリエイティブ・コモンズ 表示 - 非営利 - 改変禁止 4.0 国際 ライセンス](https://creativecommons.org/licenses/by-nc-nd/4.0/)の下でライセンスされています。
これは、クレジットを表示すれば他の人とプログラムを共有することは許可されますが、現在のところ、改変や収益化は許可されていないことを意味します。