Add 'annotate-usecode' command to import USECODE IR JSON annotations
- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors. - Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option. - Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra. - Enhanced JSON schema to include response structure for the new command.
This commit is contained in:
parent
4d3c8cd81b
commit
daa363c3d2
39 changed files with 41450 additions and 871 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -8,6 +8,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../misc/scummvm"
|
"path": "../../misc/scummvm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../misc/pentagram"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|
|
||||||
|
|
@ -443,3 +443,5 @@ ASCII: `........................................................................
|
||||||
- `vm_mask_ladder.tsv` records the current `entity_vm_context_try_create_masked_for_entity` wrapper ladder in machine-readable form so gameplay mask lanes can be compared against descriptor-side families without reopening the notes.
|
- `vm_mask_ladder.tsv` records the current `entity_vm_context_try_create_masked_for_entity` wrapper ladder in machine-readable form so gameplay mask lanes can be compared against descriptor-side families without reopening the notes.
|
||||||
- `readable_script_ir.md` and `readable_script_ir.tsv` join descriptor neighborhoods, the verified VM IR, the runtime owner/source path, and the current mask-family hints into one conservative script-facing bridge artifact.
|
- `readable_script_ir.md` and `readable_script_ir.tsv` join descriptor neighborhoods, the verified VM IR, the runtime owner/source path, and the current mask-family hints into one conservative script-facing bridge artifact.
|
||||||
- `runtime_descriptor_family_rankings.md` and `runtime_descriptor_family_rankings.tsv` rank descriptor families against the verified runtime lanes so the current human-readable script bridge is searchable by family fit rather than only by neighborhood dumps.
|
- `runtime_descriptor_family_rankings.md` and `runtime_descriptor_family_rankings.tsv` rank descriptor families against the verified runtime lanes so the current human-readable script bridge is searchable by family fit rather than only by neighborhood dumps.
|
||||||
|
- `immortality_target_body_scan.md` and `immortality_target_body_scan.tsv` now scan the strongest current immortality candidates (`EVENT`, `NPCTRIG`, `_BOOT`, `SFXTRIG`, `SPECIAL`, `TRIGPAD`) for inline `0x410` literals and record the tightest remaining active-event template frontier.
|
||||||
|
- `immortality_npctrig_clauses.md` and `immortality_npctrig_clauses.tsv` now split the compact `NPCTRIG` slot `0x0A` / `0x20` bodies into prefix, clause, and tail regions so the event-bearing ladder can be compared against the typed/setup companion body without reopening raw hex.
|
||||||
|
|
|
||||||
75
USECODE/EUSECODE_extracted/immortality_body_structure.md
Normal file
75
USECODE/EUSECODE_extracted/immortality_body_structure.md
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Immortality Body Structure
|
||||||
|
|
||||||
|
This report decodes one layer deeper than the literal scan for the surviving EVENT and NPCTRIG frontier.
|
||||||
|
It is still heuristic: the output is limited to repeatable byte grammar, subheader boundaries, field-tag trailers, and motif offsets that can be cross-checked against the 000d slot-backed runtime lane.
|
||||||
|
|
||||||
|
## EVENT slot `0x0A`
|
||||||
|
|
||||||
|
- Body length: `8150` bytes.
|
||||||
|
- Open header: `0x5A 0x2F 0x5C 0x1EF5` -> `EVENT` with embedded event-code byte `0x11`.
|
||||||
|
- Clause terminators (`0x7A`): `3`; local labels (`0x5B`): `383`.
|
||||||
|
- Internal labeled subheaders (`0x53 0x5C <u16> EVENT`): `90` -> `0x0088->0x1E6E`, `0x00F8->0x1DFE`, `0x0123->0x1DD3`, `0x0149->0x1DAD`, `0x0174->0x1D82`, `0x01CC->0x1D2A`, `0x0210->0x1CE6`, `0x0236->0x1CC0`, `0x0261->0x1C95`, `0x028E->0x1C68`, `0x02B9->0x1C3D`, `0x0361->0x1B95`.
|
||||||
|
- Tail field tags: `69:0000->referent`, `69:000A->event`, `24:02FE->item`, `69:6574->m`, `24:02FC->source`, `24:02FA->dest`, `24:02F8->door`, `24:05F3->wp`, `69:00F1->counter`, `69:00EF->counter2`, `24:02ED->n`, `69:00EB->link`, `69:00E9->cx`, `69:00E7->cy`, `69:00E5->ex`, `69:00E3->ey`, `69:00E1->time`, `69:00DF->op`, `69:00DD->opp`, `24:02DB->post1`, `24:02D9->post2`, `24:02D7->floor`, `69:00D5->dir`, `69:00D3->qHi`, `24:02D1->flicMan`, `69:4D63->an`.
|
||||||
|
|
||||||
|
| Motif | Count | First Offsets |
|
||||||
|
|---|---:|---|
|
||||||
|
| `call_40_06_4c_02` | 47 | `0x0011,0x010F,0x0160,0x024D,0x02A5,0x034D,0x0396,0x03F2,0x0422,0x047C,...` |
|
||||||
|
| `call_40_06_0f_04` | 50 | `0x001A,0x003C,0x0045,0x00C6,0x00D4,0x01EB,0x01F4,0x032D,0x0374,0x03D2,...` |
|
||||||
|
| `subheader_53_5c` | 90 | `0x0088,0x00F8,0x0123,0x0149,0x0174,0x01CC,0x0210,0x0236,0x0261,0x028E,...` |
|
||||||
|
| `writeback_57_02` | 44 | `0x007A,0x00EA,0x013B,0x01BE,0x0228,0x068F,0x0708,0x0799,0x08EA,0x0949,...` |
|
||||||
|
| `branch_59_0a` | 61 | `0x0072,0x00E2,0x0108,0x0133,0x0159,0x01B6,0x0220,0x0246,0x029E,0x0346,...` |
|
||||||
|
| `branch_3f_0a` | 39 | `0x0025,0x0057,0x019B,0x0271,0x02D4,0x02EE,0x0308,0x0322,0x03C7,0x045E,...` |
|
||||||
|
| `field_4b_fe_0f` | 23 | `0x0408,0x0412,0x0439,0x044D,0x0493,0x04A3,0x04EF,0x04FF,0x055E,0x0587,...` |
|
||||||
|
| `field_4b_fc_0f` | 3 | `0x0569,0x0575,0x057E` |
|
||||||
|
| `push_24_51` | 51 | `0x0029,0x005B,0x019F,0x0275,0x02D8,0x02F2,0x030C,0x0326,0x033F,0x03CB,...` |
|
||||||
|
| `event_field_69_0a_00` | 1 | `0x1F09` |
|
||||||
|
|
||||||
|
## NPCTRIG slot `0x0A`
|
||||||
|
|
||||||
|
- Body length: `373` bytes.
|
||||||
|
- Open header: `0x5A 0x06 0x5C 0x013E` -> `NPCTRIG` with embedded event-code byte `0x11`.
|
||||||
|
- Clause terminators (`0x7A`): `1`; local labels (`0x5B`): `12`.
|
||||||
|
- Internal labeled subheaders (`0x53 0x5C <u16> NPCTRIG`): `5` -> `0x0064->0x00DB`, `0x0093->0x00AC`, `0x00C2->0x007D`, `0x00F1->0x004E`, `0x0120->0x001F`.
|
||||||
|
- Tail field tags: `69:0000->referent`, `69:000A->event`, `24:02FE->item`, `69:6574->m`, `24:02FC->item2`, `69:6574->m2`, `24:02FA->n`.
|
||||||
|
|
||||||
|
| Motif | Count | First Offsets |
|
||||||
|
|---|---:|---|
|
||||||
|
| `call_40_06_4c_02` | 1 | `0x0011` |
|
||||||
|
| `call_40_06_0f_04` | 1 | `0x001D` |
|
||||||
|
| `subheader_53_5c` | 5 | `0x0064,0x0093,0x00C2,0x00F1,0x0120` |
|
||||||
|
| `writeback_57_02` | 5 | `0x0056,0x0085,0x00B4,0x00E3,0x0112` |
|
||||||
|
| `branch_59_0a` | 0 | `-` |
|
||||||
|
| `branch_3f_0a` | 5 | `0x0045,0x0074,0x00A3,0x00D2,0x0101` |
|
||||||
|
| `field_4b_fe_0f` | 0 | `-` |
|
||||||
|
| `field_4b_fc_0f` | 0 | `-` |
|
||||||
|
| `push_24_51` | 5 | `0x0049,0x0078,0x00A7,0x00D6,0x0105` |
|
||||||
|
| `event_field_69_0a_00` | 1 | `0x0152` |
|
||||||
|
|
||||||
|
## NPCTRIG slot `0x20`
|
||||||
|
|
||||||
|
- Body length: `345` bytes.
|
||||||
|
- Open header: `0x5A 0x06 0x5C 0x0120` -> `NPCTRIG` with embedded event-code byte `0x01`.
|
||||||
|
- Clause terminators (`0x7A`): `1`; local labels (`0x5B`): `19`.
|
||||||
|
- Internal labeled subheaders (`0x53 0x5C <u16> NPCTRIG`): `1` -> `0x00BA->0x0067`.
|
||||||
|
- Tail field tags: `69:0000->referent`, `69:000A->typeNpc`, `24:02FE->n`, `24:02FC->item`, `69:6574->m`, `24:02FA->item2`, `69:6574->m2`.
|
||||||
|
|
||||||
|
| Motif | Count | First Offsets |
|
||||||
|
|---|---:|---|
|
||||||
|
| `call_40_06_4c_02` | 2 | `0x0011,0x002C` |
|
||||||
|
| `call_40_06_0f_04` | 3 | `0x0085,0x008E,0x0097` |
|
||||||
|
| `subheader_53_5c` | 1 | `0x00BA` |
|
||||||
|
| `writeback_57_02` | 0 | `-` |
|
||||||
|
| `branch_59_0a` | 0 | `-` |
|
||||||
|
| `branch_3f_0a` | 1 | `0x0051` |
|
||||||
|
| `field_4b_fe_0f` | 10 | `0x0053,0x0060,0x006D,0x007A,0x00A0,0x00CC,0x00D9,0x00E6,0x0107,0x0119` |
|
||||||
|
| `field_4b_fc_0f` | 0 | `-` |
|
||||||
|
| `push_24_51` | 0 | `-` |
|
||||||
|
| `event_field_69_0a_00` | 1 | `0x0134` |
|
||||||
|
|
||||||
|
## Current Read
|
||||||
|
|
||||||
|
- `EVENT 0x0A` is the generic hub-shaped body: it has `90` internal labeled subheaders and the widest field trailer (`69:0000->referent, 69:000A->event, 24:02FE->item, 69:6574->m, 24:02FC->source, 24:02FA->dest, 24:02F8->door, 24:05F3->wp, 69:00F1->counter, 69:00EF->counter2, 24:02ED->n, 69:00EB->link, 69:00E9->cx, 69:00E7->cy, 69:00E5->ex, 69:00E3->ey, 69:00E1->time, 69:00DF->op, 69:00DD->opp, 24:02DB->post1, 24:02D9->post2, 24:02D7->floor, 69:00D5->dir, 69:00D3->qHi, 24:02D1->flicMan, 69:4D63->an`).
|
||||||
|
- `NPCTRIG 0x0A` is the compact player-trigger candidate: it reuses the same class-labelled open header and subheader grammar, but it stays constrained to `69:0000->referent, 69:000A->event, 24:02FE->item, 69:6574->m, 24:02FC->item2, 69:6574->m2, 24:02FA->n` instead of the wider EVENT field set.
|
||||||
|
- `NPCTRIG 0x20` keeps the same constrained field set as `NPCTRIG 0x0A` and changes only the embedded prolog event-code byte (`0x01` vs `0x11`), which fits a variant trigger/setup lane better than a separate generic hub.
|
||||||
|
- The repeated `0x53 0x5C <u16> LABEL` subheaders and dense `0x5B <u16>` local labels make these bodies look like inline clause streams rather than single flat payloads, which is consistent with the `000d:21ed -> 000d:22bc` runtime lane that copies variable-length inline bytes first and only then consumes compact metadata bytes plus streamed words.
|
||||||
|
- The surviving slot focus is still `0x0A`: both EVENT and NPCTRIG expose non-zero slot-`0x0A` bodies, and the runtime side has an exact offset-specialized masked wrapper for slot `0x0A` at `0005:2c35` (`entity_vm_context_try_create_mask_0400_slot0a_with_offset`).
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
entry_index class_name slot event_name_hint body_length header_open_arg header_target header_label header_event_code clause_terminator_count local_label_count subheader_count subheader_targets tail_fields all_fields motif_counts motif_offsets
|
||||||
|
190 EVENT 0x0A equip 8150 0x2F 0x1EF5 EVENT 0x11 3 383 90 0x0088->0x1E6E,0x00F8->0x1DFE,0x0123->0x1DD3,0x0149->0x1DAD,0x0174->0x1D82,0x01CC->0x1D2A,0x0210->0x1CE6,0x0236->0x1CC0,0x0261->0x1C95,0x028E->0x1C68,0x02B9->0x1C3D,0x0361->0x1B95,0x03AA->0x1B4C,0x069D->0x1859,0x0716->0x17E0,0x073C->0x17BA,0x07A7->0x174F,0x07CD->0x1729,0x07F3->0x1703,0x0819->0x16DD,0x0849->0x16AD,0x089E->0x1658,0x08F8->0x15FE,0x0926->0x15D0,0x0957->0x159F,0x09BD->0x1539,0x09E4->0x1512,0x0AD0->0x1426,0x0AF6->0x1400,0x0B21->0x13D5,0x0B47->0x13AF,0x0B70->0x1386,0x0C6F->0x1287,0x0C95->0x1261,0x0CC0->0x1236,0x0CE6->0x1210,0x0D0F->0x11E7,0x0DA0->0x1156,0x0DC9->0x112D,0x0DF4->0x1102,0x0E1D->0x10D9,0x0E99->0x105D,0x0EBF->0x1037,0x0EEA->0x100C,0x0F44->0x0FB2,0x0F6A->0x0F8C,0x0FF1->0x0F05,0x1017->0x0EDF,0x1071->0x0E85,0x1097->0x0E5F,0x113F->0x0DB7,0x1165->0x0D91,0x1190->0x0D66,0x11EA->0x0D0C,0x1210->0x0CE6,0x127D->0x0C79,0x136C->0x0B8A,0x13FA->0x0AFC,0x146D->0x0A89,0x14A0->0x0A56,0x14EE->0x0A08,0x1515->0x09E1,0x155C->0x099A,0x1589->0x096D,0x1654->0x08A2,0x16B5->0x0841,0x17F5->0x0701,0x181B->0x06DB,0x1846->0x06B0,0x186C->0x068A,0x18F7->0x05FF,0x19E9->0x050D,0x1A0F->0x04E7,0x1A3A->0x04BC,0x1A60->0x0496,0x1A89->0x046D,0x1AD6->0x0420,0x1B09->0x03ED,0x1B7E->0x0378,0x1C59->0x029D,0x1C80->0x0276,0x1CCF->0x0227,0x1CF5->0x0201,0x1D21->0x01D5,0x1D97->0x015F,0x1DD2->0x0124,0x1E00->0x00F6,0x1E44->0x00B2,0x1E72->0x0084,0x1EE6->0x0010 69:0000->referent,69:000A->event,24:02FE->item,69:6574->m,24:02FC->source,24:02FA->dest,24:02F8->door,24:05F3->wp,69:00F1->counter,69:00EF->counter2,24:02ED->n,69:00EB->link,69:00E9->cx,69:00E7->cy,69:00E5->ex,69:00E3->ey,69:00E1->time,69:00DF->op,69:00DD->opp,24:02DB->post1,24:02D9->post2,24:02D7->floor,69:00D5->dir,69:00D3->qHi,24:02D1->flicMan,69:4D63->an 24:4501->VENT,69:0000->referent,69:000A->event,24:02FE->item,69:6574->m,24:02FC->source,24:02FA->dest,24:02F8->door,24:05F3->wp,69:00F1->counter,69:00EF->counter2,24:02ED->n,69:00EB->link,69:00E9->cx,69:00E7->cy,69:00E5->ex,69:00E3->ey,69:00E1->time,69:00DF->op,69:00DD->opp,24:02DB->post1,24:02D9->post2,24:02D7->floor,69:00D5->dir,69:00D3->qHi,24:02D1->flicMan,69:4D63->an call_40_06_4c_02:47,call_40_06_0f_04:50,subheader_53_5c:90,writeback_57_02:44,branch_59_0a:61,branch_3f_0a:39,field_4b_fe_0f:23,field_4b_fc_0f:3,push_24_51:51,event_field_69_0a_00:1 call_40_06_4c_02=0x0011,0x010F,0x0160,0x024D,0x02A5,0x034D,0x0396,0x03F2,0x0422,0x047C,...,call_40_06_0f_04=0x001A,0x003C,0x0045,0x00C6,0x00D4,0x01EB,0x01F4,0x032D,0x0374,0x03D2,...,subheader_53_5c=0x0088,0x00F8,0x0123,0x0149,0x0174,0x01CC,0x0210,0x0236,0x0261,0x028E,...,writeback_57_02=0x007A,0x00EA,0x013B,0x01BE,0x0228,0x068F,0x0708,0x0799,0x08EA,0x0949,...,branch_59_0a=0x0072,0x00E2,0x0108,0x0133,0x0159,0x01B6,0x0220,0x0246,0x029E,0x0346,...,branch_3f_0a=0x0025,0x0057,0x019B,0x0271,0x02D4,0x02EE,0x0308,0x0322,0x03C7,0x045E,...,field_4b_fe_0f=0x0408,0x0412,0x0439,0x044D,0x0493,0x04A3,0x04EF,0x04FF,0x055E,0x0587,...,field_4b_fc_0f=0x0569,0x0575,0x057E,push_24_51=0x0029,0x005B,0x019F,0x0275,0x02D8,0x02F2,0x030C,0x0326,0x033F,0x03CB,...,event_field_69_0a_00=0x1F09
|
||||||
|
191 NPCTRIG 0x0A equip 373 0x06 0x013E NPCTRIG 0x11 1 12 5 0x0064->0x00DB,0x0093->0x00AC,0x00C2->0x007D,0x00F1->0x004E,0x0120->0x001F 69:0000->referent,69:000A->event,24:02FE->item,69:6574->m,24:02FC->item2,69:6574->m2,24:02FA->n 69:0000->referent,69:000A->event,24:02FE->item,69:6574->m,24:02FC->item2,69:6574->m2,24:02FA->n call_40_06_4c_02:1,call_40_06_0f_04:1,subheader_53_5c:5,writeback_57_02:5,branch_59_0a:0,branch_3f_0a:5,field_4b_fe_0f:0,field_4b_fc_0f:0,push_24_51:5,event_field_69_0a_00:1 call_40_06_4c_02=0x0011,call_40_06_0f_04=0x001D,subheader_53_5c=0x0064,0x0093,0x00C2,0x00F1,0x0120,writeback_57_02=0x0056,0x0085,0x00B4,0x00E3,0x0112,branch_3f_0a=0x0045,0x0074,0x00A3,0x00D2,0x0101,push_24_51=0x0049,0x0078,0x00A7,0x00D6,0x0105,event_field_69_0a_00=0x0152
|
||||||
|
191 NPCTRIG 0x20 345 0x06 0x0120 NPCTRIG 0x01 1 19 1 0x00BA->0x0067 69:0000->referent,69:000A->typeNpc,24:02FE->n,24:02FC->item,69:6574->m,24:02FA->item2,69:6574->m2 24:3D74->t@[S,69:0000->referent,69:000A->typeNpc,24:02FE->n,24:02FC->item,69:6574->m,24:02FA->item2,69:6574->m2 call_40_06_4c_02:2,call_40_06_0f_04:3,subheader_53_5c:1,writeback_57_02:0,branch_59_0a:0,branch_3f_0a:1,field_4b_fe_0f:10,field_4b_fc_0f:0,push_24_51:0,event_field_69_0a_00:1 call_40_06_4c_02=0x0011,0x002C,call_40_06_0f_04=0x0085,0x008E,0x0097,subheader_53_5c=0x00BA,branch_3f_0a=0x0051,field_4b_fe_0f=0x0053,0x0060,0x006D,0x007A,0x00A0,0x00CC,0x00D9,0x00E6,0x0107,0x0119,event_field_69_0a_00=0x0134
|
||||||
|
118
USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md
Normal file
118
USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Immortality NPCTRIG Clauses
|
||||||
|
|
||||||
|
This report focuses on the surviving compact NPCTRIG frontier and splits the extracted slot bodies into prefix, clause, and tail regions.
|
||||||
|
It is intended to make the slot `0x0A` versus slot `0x20` difference explicit enough to compare against the runtime-side slot-`0x0A` consumer path.
|
||||||
|
|
||||||
|
## NPCTRIG slot `0x0A`
|
||||||
|
|
||||||
|
- Event hint: `equip`.
|
||||||
|
- Open header: `0x5A 0x06 0x5C 0x013E` -> `NPCTRIG` with event-code byte `0x11`.
|
||||||
|
- First tail-field offset: `0x0145`.
|
||||||
|
- Subheader offsets: `0x0064`, `0x0093`, `0x00C2`, `0x00F1`, `0x0120`.
|
||||||
|
- Subheader targets: `0x00DB`, `0x00AC`, `0x007D`, `0x004E`, `0x001F`.
|
||||||
|
- Subheader offset deltas: `0x2F`, `0x2F`, `0x2F`, `0x2F`.
|
||||||
|
- Subheader target deltas: `0xFFD1`, `0xFFD1`, `0xFFD1`, `0xFFD1`.
|
||||||
|
- Runtime-shape motifs: `writeback_57_02=yes`, `push_24_51=yes`, `field_4b_fe_0f=0`.
|
||||||
|
|
||||||
|
| Segment | Range | Len | Local Labels | Subheaders | Branch 3F 0A | Writeback 57 02 | Push 24 51 | Field 4B FE 0F | Motif Offsets | Prefix | Suffix |
|
||||||
|
|---|---|---:|---|---:|---:|---:|---:|---:|---|---|---|
|
||||||
|
| prefix | `0x0000..0x0064` | 100 | `0x0017,0x001A,0x0026,0x0030,0x0036,0x004D` | 0 | 1 | 1 | 1 | 0 | `branch_3f_0a=+0x45; writeback_57_02=+0x56; push_24_51=+0x49` | `5a065c3e014e50435452494700000b11` | `02630320006efe5e54010112` |
|
||||||
|
| clause_1 | `0x0064..0x0093` | 47 | `0x007C` | 1 | 1 | 1 | 1 | 0 | `subheader_53_5c=+0x00; branch_3f_0a=+0x10; writeback_57_02=+0x21; push_24_51=+0x14` | `535cdb004e504354524947000052bc00` | `02630320006efe5e54010112` |
|
||||||
|
| clause_2 | `0x0093..0x00C2` | 47 | `0x00AB` | 1 | 1 | 1 | 1 | 0 | `subheader_53_5c=+0x00; branch_3f_0a=+0x10; writeback_57_02=+0x21; push_24_51=+0x14` | `535cac004e5043545249470000528d00` | `02630320006efe5e54010112` |
|
||||||
|
| clause_3 | `0x00C2..0x00F1` | 47 | `0x00DA` | 1 | 1 | 1 | 1 | 0 | `subheader_53_5c=+0x00; branch_3f_0a=+0x10; writeback_57_02=+0x21; push_24_51=+0x14` | `535c7d004e5043545249470000525e00` | `02630320006efe5e54010112` |
|
||||||
|
| clause_4 | `0x00F1..0x0120` | 47 | `0x0109` | 1 | 1 | 1 | 1 | 0 | `subheader_53_5c=+0x00; branch_3f_0a=+0x10; writeback_57_02=+0x21; push_24_51=+0x14` | `535c4e004e5043545249470000522f00` | `02630320006efe5e54010112` |
|
||||||
|
| clause_5 | `0x0120..0x0145` | 37 | `0x0130,0x013F` | 1 | 0 | 0 | 0 | 0 | `subheader_53_5c=+0x00` | `535c1f004e5043545249470000520000` | `1e0a24006efa5b4700500501` |
|
||||||
|
| tail | `0x0145..0x0175` | 48 | `-` | 0 | 0 | 0 | 0 | 0 | `-` | `6900007265666572656e740000690a00` | `74656d32000024fa026e007a` |
|
||||||
|
|
||||||
|
Repeated windows (8-byte):
|
||||||
|
|
||||||
|
- `4E 50 43 54 52 49 47 00` at `0x0005`, `0x0068`, `0x0097`, `0x00C6`, `0x00F5`, `0x0124`
|
||||||
|
- `50 43 54 52 49 47 00 00` at `0x0006`, `0x0069`, `0x0098`, `0x00C7`, `0x00F6`, `0x0125`
|
||||||
|
- `40 06 57 02 02 63 03 20` at `0x0054`, `0x0083`, `0x00B2`, `0x00E1`, `0x0110`
|
||||||
|
- `06 57 02 02 63 03 20 00` at `0x0055`, `0x0084`, `0x00B3`, `0x00E2`, `0x0111`
|
||||||
|
- `57 02 02 63 03 20 00 6E` at `0x0056`, `0x0085`, `0x00B4`, `0x00E3`, `0x0112`
|
||||||
|
- `02 02 63 03 20 00 6E FE` at `0x0057`, `0x0086`, `0x00B5`, `0x00E4`, `0x0113`
|
||||||
|
|
||||||
|
Repeated windows (6-byte):
|
||||||
|
|
||||||
|
- `4E 50 43 54 52 49` at `0x0005`, `0x0068`, `0x0097`, `0x00C6`, `0x00F5`, `0x0124`
|
||||||
|
- `50 43 54 52 49 47` at `0x0006`, `0x0069`, `0x0098`, `0x00C7`, `0x00F6`, `0x0125`
|
||||||
|
- `43 54 52 49 47 00` at `0x0007`, `0x006A`, `0x0099`, `0x00C8`, `0x00F7`, `0x0126`
|
||||||
|
- `54 52 49 47 00 00` at `0x0008`, `0x006B`, `0x009A`, `0x00C9`, `0x00F8`, `0x0127`
|
||||||
|
- `40 06 57 02 02 63` at `0x0054`, `0x0083`, `0x00B2`, `0x00E1`, `0x0110`
|
||||||
|
- `06 57 02 02 63 03` at `0x0055`, `0x0084`, `0x00B3`, `0x00E2`, `0x0111`
|
||||||
|
|
||||||
|
Runtime-fit candidates:
|
||||||
|
|
||||||
|
- Candidate clause selector starts: `0x0064`, `0x0093`, `0x00C2`, `0x00F1`, `0x0120`.
|
||||||
|
- Candidate clause selector targets: `0x00DB`, `0x00AC`, `0x007D`, `0x004E`, `0x001F`.
|
||||||
|
- Uniform selector stride: `0x2F`; full clauses carrying both `push_24_51` and `writeback_57_02`: `4`.
|
||||||
|
- Runtime side anchor: `000d:5572` proves the wrapper extra word is additive (`entity_vm_slot_load_value(...) + offset`), while `000d:21ed -> 000d:2433` copies one inline blob, reads two signed metadata bytes, then consumes a word matrix where byte A controls the lead-word row count and byte B controls the shared target-list width.
|
||||||
|
|
||||||
|
Tail field offsets:
|
||||||
|
|
||||||
|
- `0x0145` -> `69:0000->referent`
|
||||||
|
- `0x0152` -> `69:000A->event`
|
||||||
|
- `0x015C` -> `24:02FE->item`
|
||||||
|
- `0x015F` -> `69:6574->m`
|
||||||
|
- `0x0165` -> `24:02FC->item2`
|
||||||
|
- `0x0168` -> `69:6574->m2`
|
||||||
|
- `0x016F` -> `24:02FA->n`
|
||||||
|
|
||||||
|
## NPCTRIG slot `0x20`
|
||||||
|
|
||||||
|
- Event hint: `-`.
|
||||||
|
- Open header: `0x5A 0x06 0x5C 0x0120` -> `NPCTRIG` with event-code byte `0x01`.
|
||||||
|
- First tail-field offset: `0x0127`.
|
||||||
|
- Subheader offsets: `0x00BA`.
|
||||||
|
- Subheader targets: `0x0067`.
|
||||||
|
- Subheader offset deltas: `-`.
|
||||||
|
- Subheader target deltas: `-`.
|
||||||
|
- Runtime-shape motifs: `writeback_57_02=no`, `push_24_51=no`, `field_4b_fe_0f=10`.
|
||||||
|
|
||||||
|
| Segment | Range | Len | Local Labels | Subheaders | Branch 3F 0A | Writeback 57 02 | Push 24 51 | Field 4B FE 0F | Motif Offsets | Prefix | Suffix |
|
||||||
|
|---|---|---:|---|---:|---:|---:|---:|---:|---|---|---|
|
||||||
|
| prefix | `0x0000..0x00BA` | 186 | `0x001C,0x0029,0x0037,0x0044,0x004C,0x004D,0x005B,0x0068` | 0 | 1 | 0 | 0 | 5 | `branch_3f_0a=+0x51; field_4b_fe_0f=+0x53,+0x60,+0x6D,+0x7A,+0xA0` | `5a065c20014e50435452494700000b01` | `570002110a23005e54010112` |
|
||||||
|
| clause_1 | `0x00BA..0x0127` | 109 | `0x00C7,0x00D4,0x00E1,0x00EE,0x00F1,0x00FF,0x0112,0x0121` | 1 | 0 | 0 | 0 | 5 | `subheader_53_5c=+0x00; field_4b_fe_0f=+0x12,+0x1F,+0x2C,+0x4D,+0x5F` | `535c67004e50435452494700005b6200` | `0f06e5006efa5b6f00500501` |
|
||||||
|
| tail | `0x0127..0x0159` | 50 | `-` | 0 | 0 | 0 | 0 | 0 | `-` | `6900007265666572656e740000690a00` | `000024fa026974656d32007a` |
|
||||||
|
|
||||||
|
Repeated windows (8-byte):
|
||||||
|
|
||||||
|
- `4E 50 43 54 52 49 47 00` at `0x0005`, `0x00BE`
|
||||||
|
- `50 43 54 52 49 47 00 00` at `0x0006`, `0x00BF`
|
||||||
|
- `4B FE 0F 06 52 00 6E FA` at `0x0060`, `0x00CC`
|
||||||
|
- `FE 0F 06 52 00 6E FA 5B` at `0x0061`, `0x00CD`
|
||||||
|
- `4B FE 0F 06 53 00 6E FA` at `0x006D`, `0x00D9`
|
||||||
|
- `FE 0F 06 53 00 6E FA 5B` at `0x006E`, `0x00DA`
|
||||||
|
|
||||||
|
Repeated windows (6-byte):
|
||||||
|
|
||||||
|
- `00 0A 00 4B FE 0F` at `0x005D`, `0x006A`, `0x0077`
|
||||||
|
- `0A 00 4B FE 0F 06` at `0x005E`, `0x006B`, `0x0078`
|
||||||
|
- `00 0A 05 4B FE 0F` at `0x00C9`, `0x00D6`, `0x00E3`
|
||||||
|
- `0A 05 4B FE 0F 06` at `0x00CA`, `0x00D7`, `0x00E4`
|
||||||
|
- `4E 50 43 54 52 49` at `0x0005`, `0x00BE`
|
||||||
|
- `50 43 54 52 49 47` at `0x0006`, `0x00BF`
|
||||||
|
|
||||||
|
Runtime-fit candidates:
|
||||||
|
|
||||||
|
- Candidate clause selector starts: `0x00BA`.
|
||||||
|
- Candidate clause selector targets: `0x0067`.
|
||||||
|
- Uniform selector stride: `-`; full clauses carrying both `push_24_51` and `writeback_57_02`: `0`.
|
||||||
|
- Runtime side anchor: `000d:5572` proves the wrapper extra word is additive (`entity_vm_slot_load_value(...) + offset`), while `000d:21ed -> 000d:2433` copies one inline blob, reads two signed metadata bytes, then consumes a word matrix where byte A controls the lead-word row count and byte B controls the shared target-list width.
|
||||||
|
|
||||||
|
Tail field offsets:
|
||||||
|
|
||||||
|
- `0x0127` -> `69:0000->referent`
|
||||||
|
- `0x0134` -> `69:000A->typeNpc`
|
||||||
|
- `0x0140` -> `24:02FE->n`
|
||||||
|
- `0x0146` -> `24:02FC->item`
|
||||||
|
- `0x0149` -> `69:6574->m`
|
||||||
|
- `0x014F` -> `24:02FA->item2`
|
||||||
|
- `0x0152` -> `69:6574->m2`
|
||||||
|
|
||||||
|
## Current Read
|
||||||
|
|
||||||
|
- Slot `0x0A` now reads as a repeated clause ladder, not a monolithic blob: `5` subheaders sit on a uniform `0x2F, 0x2F, 0x2F, 0x2F` byte stride, and their targets walk backward by `0xFFD1, 0xFFD1, 0xFFD1, 0xFFD1`. Each clause block carries one `branch_3f_0a`, one `push_24_51`, and one `writeback_57_02`, which fits an event-bearing clause stream better than a pure type filter.
|
||||||
|
- Slot `0x20` is structurally different even before the tail fields: its open event-code byte is `0x01` instead of `0x11`, it has only one class-labelled subheader, no `writeback_57_02`, no `push_24_51`, and `10` `field_4b_fe_0f` hits concentrated around repeated `0x0A 00/05 4B FE 0F ...` windows. That is a materially better fit for a typed gate or setup/attachment body than for the live event-emission ladder.
|
||||||
|
- This split matches the current runtime-side bridge better than the previous undifferentiated frontier. The verified slot-`0x0A` wrapper `0005:2c35` seeds mask `0x0400`, slot `0x0A`, and one additive word that `000d:5572` applies directly to the loaded slot value before `000d:21ed` consumes the result. The exact `000d:21ed -> 000d:22bc` contract is now narrower too: after copying the inline blob it reads two signed bytes, uses byte A as the lead-word row count, uses byte B as the shared target-list width, performs `A x B` `entity_link` calls, and pushes back only non-`0x0400` words. `NPCTRIG slot 0x0A` is the only surviving compact body here with a natural five-row selector family (`5` evenly spaced clause starts at stride `0x2F`), while slot `0x20` offers only one clause and no matching writeback/push motif.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
slot event_name_hint body_length header_target header_event_code subheader_offsets subheader_targets subheader_offset_deltas subheader_target_deltas uniform_stride full_clause_count tail_start has_writeback has_push_2451 field_4b_fe_0f_count repeated_windows_8 repeated_windows_6
|
||||||
|
0x0A equip 373 0x013E 0x11 0x0064,0x0093,0x00C2,0x00F1,0x0120 0x00DB,0x00AC,0x007D,0x004E,0x001F 0x2F,0x2F,0x2F,0x2F 0xFFD1,0xFFD1,0xFFD1,0xFFD1 0x2F 4 0x0145 yes yes 0 4e50435452494700@0x0005,0x0068,0x0097,0x00C6,0x00F5,0x0124;5043545249470000@0x0006,0x0069,0x0098,0x00C7,0x00F6,0x0125;4006570202630320@0x0054,0x0083,0x00B2,0x00E1,0x0110;0657020263032000@0x0055,0x0084,0x00B3,0x00E2,0x0111;570202630320006e@0x0056,0x0085,0x00B4,0x00E3,0x0112;0202630320006efe@0x0057,0x0086,0x00B5,0x00E4,0x0113 4e5043545249@0x0005,0x0068,0x0097,0x00C6,0x00F5,0x0124;504354524947@0x0006,0x0069,0x0098,0x00C7,0x00F6,0x0125;435452494700@0x0007,0x006A,0x0099,0x00C8,0x00F7,0x0126;545249470000@0x0008,0x006B,0x009A,0x00C9,0x00F8,0x0127;400657020263@0x0054,0x0083,0x00B2,0x00E1,0x0110;065702026303@0x0055,0x0084,0x00B3,0x00E2,0x0111
|
||||||
|
0x20 345 0x0120 0x01 0x00BA 0x0067 0 0x0127 no no 10 4e50435452494700@0x0005,0x00BE;5043545249470000@0x0006,0x00BF;4bfe0f0652006efa@0x0060,0x00CC;fe0f0652006efa5b@0x0061,0x00CD;4bfe0f0653006efa@0x006D,0x00D9;fe0f0653006efa5b@0x006E,0x00DA 000a004bfe0f@0x005D,0x006A,0x0077;0a004bfe0f06@0x005E,0x006B,0x0078;000a054bfe0f@0x00C9,0x00D6,0x00E3;0a054bfe0f06@0x00CA,0x00D7,0x00E4;4e5043545249@0x0005,0x00BE;504354524947@0x0006,0x00BF
|
||||||
|
50
USECODE/EUSECODE_extracted/immortality_target_body_scan.md
Normal file
50
USECODE/EUSECODE_extracted/immortality_target_body_scan.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Immortality Target Body Scan
|
||||||
|
|
||||||
|
This report is a focused follow-up on the player-trigger immortality lane.
|
||||||
|
It scans the current highest-value EUSECODE candidates for inline `0x410` literals and compares the strongest active-event template bodies.
|
||||||
|
|
||||||
|
- No scanned target body contains inline little-endian `0x0410`, inline dword `0x00000410`, or byte-swapped `0x1004` literals.
|
||||||
|
- `EVENT` remains the widest unresolved active-event frontier because it still exposes one monolithic slot-`0x0A` body (`8150` bytes) with no finer body split yet.
|
||||||
|
- `NPCTRIG` remains the strongest compact player-trigger frontier because it is event-bearing and has two non-zero bodies (`0x0A`, `0x20`) but still no inline `0x410` literal.
|
||||||
|
- `_BOOT` event cores (`COR_BOOT`, `REE_BOOT`) remain near-template event families rather than special immortality emitters: their best pairings share only short common prefixes plus shared suffix-heavy tails.
|
||||||
|
- `SPECIAL` and `TRIGPAD` stay negative controls here: callable bodies exist, but the new literal scan still shows no inline `0x410` evidence.
|
||||||
|
|
||||||
|
## Body Rows
|
||||||
|
|
||||||
|
| Class | Slot | Hint | Body Range | Len | `0x0410` hits | `0x00000410` hits | `0x1004` hits | Prefix | Suffix |
|
||||||
|
|---|---:|---|---|---:|---|---|---|---|---|
|
||||||
|
| EVENT | `0x0A` | equip | `0x00D4..0x20AA` | 8150 | 0:- | 0:- | 0:- | `5a2f5cf51e4556454e54000000000b11` | `4869000024d102666c69634d616e007a` |
|
||||||
|
| NPCTRIG | `0x0A` | equip | `0x00DA..0x024F` | 373 | 0:- | 0:- | 0:- | `5a065c3e014e50435452494700000b11` | `24fc026974656d32000024fa026e007a` |
|
||||||
|
| NPCTRIG | `0x20` | - | `0x024F..0x03A8` | 345 | 0:- | 0:- | 0:- | `5a065c20014e50435452494700000b01` | `6974656d000024fa026974656d32007a` |
|
||||||
|
| COR_BOOT | `0x0A` | equip | `0x00D4..0x02FB` | 551 | 0:- | 0:- | 0:- | `5a025cfd01434f525f424f4f54000b11` | `6e74000069fe00636f756e746572007a` |
|
||||||
|
| COR_BOOT | `0x0F` | enterFastArea | `0x02FB..0x052F` | 564 | 0:- | 0:- | 0:- | `5a045c0b02434f525f424f4f54000b1b` | `656d000069fc00636f756e746572007a` |
|
||||||
|
| COR_BOOT | `0x10` | leaveFastArea | `0x052F..0x056A` | 59 | 0:- | 0:- | 0:- | `5a005c2700434f525f424f4f54000b1c` | `5001016900007265666572656e74007a` |
|
||||||
|
| REE_BOOT | `0x0A` | equip | `0x00D4..0x041F` | 843 | 0:- | 0:- | 0:- | `5a025c21035245455f424f4f54000b11` | `6e74000069fe00636f756e746572007a` |
|
||||||
|
| REE_BOOT | `0x0F` | enterFastArea | `0x041F..0x067B` | 604 | 0:- | 0:- | 0:- | `5a045c33025245455f424f4f54000b1b` | `656d000069fc00636f756e746572007a` |
|
||||||
|
| REE_BOOT | `0x10` | leaveFastArea | `0x067B..0x06B6` | 59 | 0:- | 0:- | 0:- | `5a005c27005245455f424f4f54000b1c` | `5001016900007265666572656e74007a` |
|
||||||
|
| SFXTRIG | `0x0A` | equip | `0x00D4..0x018C` | 184 | 0:- | 0:- | 0:- | `5a005c9a005346585452494700000b11` | `72656e740000690a006576656e74007a` |
|
||||||
|
| SPECIAL | `0x0A` | equip | `0x00E0..0x0146` | 102 | 0:- | 0:- | 0:- | `5a005c4a005350454349414c00000b11` | `666572656e740000690a00766172007a` |
|
||||||
|
| SPECIAL | `0x0F` | enterFastArea | `0x0616..0x075B` | 325 | 0:- | 0:- | 0:- | `5a045c1b015350454349414c00000b1b` | `4e756d000069fc006e70634e756d007a` |
|
||||||
|
| SPECIAL | `0x10` | leaveFastArea | `0x075B..0x080E` | 179 | 0:- | 0:- | 0:- | `5a005c9f005350454349414c00000b1c` | `5001016900007265666572656e74007a` |
|
||||||
|
| SPECIAL | `0x20` | - | `0x0146..0x04DC` | 918 | 0:- | 0:- | 0:- | `5a065c5c035350454349414c00005b26` | `000024fc026e7063000069fa0071007a` |
|
||||||
|
| SPECIAL | `0x21` | - | `0x04DC..0x0616` | 314 | 0:- | 0:- | 0:- | `5a085cfb005350454349414c00005b82` | `736531000069f800706861736532007a` |
|
||||||
|
| TRIGPAD | `0x06` | gotHit | `0x00D4..0x035C` | 648 | 0:- | 0:- | 0:- | `5a065c4c025452494750414400000b0c` | `026974656d000024fa02656c6576007a` |
|
||||||
|
|
||||||
|
## Strongest Template Pairings
|
||||||
|
|
||||||
|
These comparisons are limited to `COR_BOOT`, `REE_BOOT`, `NPCTRIG`, and `SFXTRIG` because they are the current highest-value active-event families near the immortality frontier.
|
||||||
|
|
||||||
|
| Left | Right | Prefix | Suffix | Total |
|
||||||
|
|---|---|---:|---:|---:|
|
||||||
|
| COR_BOOT `0x0A` (`551`) | REE_BOOT `0x0A` (`843`) | 3 | 39 | 42 |
|
||||||
|
| COR_BOOT `0x0F` (`564`) | REE_BOOT `0x0F` (`604`) | 3 | 38 | 41 |
|
||||||
|
| COR_BOOT `0x10` (`59`) | REE_BOOT `0x10` (`59`) | 5 | 17 | 22 |
|
||||||
|
| REE_BOOT `0x0A` (`843`) | REE_BOOT `0x0F` (`604`) | 1 | 10 | 11 |
|
||||||
|
| COR_BOOT `0x0F` (`564`) | REE_BOOT `0x0A` (`843`) | 1 | 10 | 11 |
|
||||||
|
| COR_BOOT `0x0A` (`551`) | REE_BOOT `0x0F` (`604`) | 1 | 10 | 11 |
|
||||||
|
| COR_BOOT `0x0A` (`551`) | COR_BOOT `0x0F` (`564`) | 1 | 10 | 11 |
|
||||||
|
| REE_BOOT `0x10` (`59`) | SFXTRIG `0x0A` (`184`) | 3 | 5 | 8 |
|
||||||
|
| COR_BOOT `0x10` (`59`) | SFXTRIG `0x0A` (`184`) | 3 | 5 | 8 |
|
||||||
|
| NPCTRIG `0x0A` (`373`) | NPCTRIG `0x20` (`345`) | 3 | 2 | 5 |
|
||||||
|
| REE_BOOT `0x0F` (`604`) | SFXTRIG `0x0A` (`184`) | 1 | 2 | 3 |
|
||||||
|
| REE_BOOT `0x0F` (`604`) | REE_BOOT `0x10` (`59`) | 1 | 2 | 3 |
|
||||||
17
USECODE/EUSECODE_extracted/immortality_target_body_scan.tsv
Normal file
17
USECODE/EUSECODE_extracted/immortality_target_body_scan.tsv
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
entry_index class_name slot event_name_hint body_start body_end body_length le16_0410_count le16_0410_offsets le32_00000410_count le32_00000410_offsets le16_1004_count le16_1004_offsets body_prefix_hex body_suffix_hex
|
||||||
|
190 EVENT 0x0A equip 0x00D4 0x20AA 8150 0 0 0 5a2f5cf51e4556454e54000000000b11 4869000024d102666c69634d616e007a
|
||||||
|
191 NPCTRIG 0x0A equip 0x00DA 0x024F 373 0 0 0 5a065c3e014e50435452494700000b11 24fc026974656d32000024fa026e007a
|
||||||
|
191 NPCTRIG 0x20 0x024F 0x03A8 345 0 0 0 5a065c20014e50435452494700000b01 6974656d000024fa026974656d32007a
|
||||||
|
189 COR_BOOT 0x0A equip 0x00D4 0x02FB 551 0 0 0 5a025cfd01434f525f424f4f54000b11 6e74000069fe00636f756e746572007a
|
||||||
|
189 COR_BOOT 0x0F enterFastArea 0x02FB 0x052F 564 0 0 0 5a045c0b02434f525f424f4f54000b1b 656d000069fc00636f756e746572007a
|
||||||
|
189 COR_BOOT 0x10 leaveFastArea 0x052F 0x056A 59 0 0 0 5a005c2700434f525f424f4f54000b1c 5001016900007265666572656e74007a
|
||||||
|
283 REE_BOOT 0x0A equip 0x00D4 0x041F 843 0 0 0 5a025c21035245455f424f4f54000b11 6e74000069fe00636f756e746572007a
|
||||||
|
283 REE_BOOT 0x0F enterFastArea 0x041F 0x067B 604 0 0 0 5a045c33025245455f424f4f54000b1b 656d000069fc00636f756e746572007a
|
||||||
|
283 REE_BOOT 0x10 leaveFastArea 0x067B 0x06B6 59 0 0 0 5a005c27005245455f424f4f54000b1c 5001016900007265666572656e74007a
|
||||||
|
285 SFXTRIG 0x0A equip 0x00D4 0x018C 184 0 0 0 5a005c9a005346585452494700000b11 72656e740000690a006576656e74007a
|
||||||
|
272 SPECIAL 0x0A equip 0x00E0 0x0146 102 0 0 0 5a005c4a005350454349414c00000b11 666572656e740000690a00766172007a
|
||||||
|
272 SPECIAL 0x0F enterFastArea 0x0616 0x075B 325 0 0 0 5a045c1b015350454349414c00000b1b 4e756d000069fc006e70634e756d007a
|
||||||
|
272 SPECIAL 0x10 leaveFastArea 0x075B 0x080E 179 0 0 0 5a005c9f005350454349414c00000b1c 5001016900007265666572656e74007a
|
||||||
|
272 SPECIAL 0x20 0x0146 0x04DC 918 0 0 0 5a065c5c035350454349414c00005b26 000024fc026e7063000069fa0071007a
|
||||||
|
272 SPECIAL 0x21 0x04DC 0x0616 314 0 0 0 5a085cfb005350454349414c00005b82 736531000069f800706861736532007a
|
||||||
|
273 TRIGPAD 0x06 gotHit 0x00D4 0x035C 648 0 0 0 5a065c4c025452494750414400000b0c 026974656d000024fa02656c6576007a
|
||||||
|
43
USECODE/EUSECODE_extracted/immortality_target_body_scan.txt
Normal file
43
USECODE/EUSECODE_extracted/immortality_target_body_scan.txt
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
ENTRY 189 COR_BOOT FILE chunk_189_table_1B90_off_01D610_len_00056A.bin
|
||||||
|
BODY class=COR_BOOT slot=0x0A hint=equip start=0x00D4 end=0x02FB len=551 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a025cfd01434f525f424f4f54000b11 last16=6e74000069fe00636f756e746572007a
|
||||||
|
BODY class=COR_BOOT slot=0x0F hint=enterFastArea start=0x02FB end=0x052F len=564 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a045c0b02434f525f424f4f54000b1b last16=656d000069fc00636f756e746572007a
|
||||||
|
BODY class=COR_BOOT slot=0x10 hint=leaveFastArea start=0x052F end=0x056A len=59 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a005c2700434f525f424f4f54000b1c last16=5001016900007265666572656e74007a
|
||||||
|
|
||||||
|
ENTRY 190 EVENT FILE chunk_190_table_1B98_off_02F49E_len_0020AA.bin
|
||||||
|
BODY class=EVENT slot=0x0A hint=equip start=0x00D4 end=0x20AA len=8150 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a2f5cf51e4556454e54000000000b11 last16=4869000024d102666c69634d616e007a
|
||||||
|
|
||||||
|
ENTRY 191 NPCTRIG FILE chunk_191_table_1BA8_off_04C347_len_0003A8.bin
|
||||||
|
BODY class=NPCTRIG slot=0x0A hint=equip start=0x00DA end=0x024F len=373 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a065c3e014e50435452494700000b11 last16=24fc026974656d32000024fa026e007a
|
||||||
|
BODY class=NPCTRIG slot=0x20 hint=- start=0x024F end=0x03A8 len=345 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a065c20014e50435452494700000b01 last16=6974656d000024fa026974656d32007a
|
||||||
|
|
||||||
|
ENTRY 272 SPECIAL FILE chunk_272_table_26E0_off_05B625_len_00080E.bin
|
||||||
|
BODY class=SPECIAL slot=0x0A hint=equip start=0x00E0 end=0x0146 len=102 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a005c4a005350454349414c00000b11 last16=666572656e740000690a00766172007a
|
||||||
|
BODY class=SPECIAL slot=0x20 hint=- start=0x0146 end=0x04DC len=918 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a065c5c035350454349414c00005b26 last16=000024fc026e7063000069fa0071007a
|
||||||
|
BODY class=SPECIAL slot=0x21 hint=- start=0x04DC end=0x0616 len=314 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a085cfb005350454349414c00005b82 last16=736531000069f800706861736532007a
|
||||||
|
BODY class=SPECIAL slot=0x0F hint=enterFastArea start=0x0616 end=0x075B len=325 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a045c1b015350454349414c00000b1b last16=4e756d000069fc006e70634e756d007a
|
||||||
|
BODY class=SPECIAL slot=0x10 hint=leaveFastArea start=0x075B end=0x080E len=179 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a005c9f005350454349414c00000b1c last16=5001016900007265666572656e74007a
|
||||||
|
|
||||||
|
ENTRY 273 TRIGPAD FILE chunk_273_table_26F8_off_068FF3_len_00035C.bin
|
||||||
|
BODY class=TRIGPAD slot=0x06 hint=gotHit start=0x00D4 end=0x035C len=648 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a065c4c025452494750414400000b0c last16=026974656d000024fa02656c6576007a
|
||||||
|
|
||||||
|
ENTRY 283 REE_BOOT FILE chunk_283_table_2768_off_052B90_len_0006B6.bin
|
||||||
|
BODY class=REE_BOOT slot=0x0A hint=equip start=0x00D4 end=0x041F len=843 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a025c21035245455f424f4f54000b11 last16=6e74000069fe00636f756e746572007a
|
||||||
|
BODY class=REE_BOOT slot=0x0F hint=enterFastArea start=0x041F end=0x067B len=604 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a045c33025245455f424f4f54000b1b last16=656d000069fc00636f756e746572007a
|
||||||
|
BODY class=REE_BOOT slot=0x10 hint=leaveFastArea start=0x067B end=0x06B6 len=59 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a005c27005245455f424f4f54000b1c last16=5001016900007265666572656e74007a
|
||||||
|
|
||||||
|
ENTRY 285 SFXTRIG FILE chunk_285_table_27A0_off_05926C_len_00018C.bin
|
||||||
|
BODY class=SFXTRIG slot=0x0A hint=equip start=0x00D4 end=0x018C len=184 le16_0410=0:- le32_00000410=0:- le16_1004=0:- first16=5a005c9a005346585452494700000b11 last16=72656e740000690a006576656e74007a
|
||||||
|
|
||||||
|
TOP_STRUCTURAL_PAIRS
|
||||||
|
PAIR COR_BOOT:0x0A len=551 <-> REE_BOOT:0x0A len=843 prefix=3 suffix=39 total=42
|
||||||
|
PAIR COR_BOOT:0x0F len=564 <-> REE_BOOT:0x0F len=604 prefix=3 suffix=38 total=41
|
||||||
|
PAIR COR_BOOT:0x10 len=59 <-> REE_BOOT:0x10 len=59 prefix=5 suffix=17 total=22
|
||||||
|
PAIR REE_BOOT:0x0A len=843 <-> REE_BOOT:0x0F len=604 prefix=1 suffix=10 total=11
|
||||||
|
PAIR COR_BOOT:0x0F len=564 <-> REE_BOOT:0x0A len=843 prefix=1 suffix=10 total=11
|
||||||
|
PAIR COR_BOOT:0x0A len=551 <-> REE_BOOT:0x0F len=604 prefix=1 suffix=10 total=11
|
||||||
|
PAIR COR_BOOT:0x0A len=551 <-> COR_BOOT:0x0F len=564 prefix=1 suffix=10 total=11
|
||||||
|
PAIR REE_BOOT:0x10 len=59 <-> SFXTRIG:0x0A len=184 prefix=3 suffix=5 total=8
|
||||||
|
PAIR COR_BOOT:0x10 len=59 <-> SFXTRIG:0x0A len=184 prefix=3 suffix=5 total=8
|
||||||
|
PAIR NPCTRIG:0x0A len=373 <-> NPCTRIG:0x20 len=345 prefix=3 suffix=2 total=5
|
||||||
|
PAIR REE_BOOT:0x0F len=604 <-> SFXTRIG:0x0A len=184 prefix=1 suffix=2 total=3
|
||||||
|
PAIR REE_BOOT:0x0F len=604 <-> REE_BOOT:0x10 len=59 prefix=1 suffix=2 total=3
|
||||||
File diff suppressed because it is too large
Load diff
19
_tmp_targets.py
Normal file
19
_tmp_targets.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import csv, pathlib
|
||||||
|
p = pathlib.Path('USECODE/EUSECODE_extracted/class_event_index.tsv')
|
||||||
|
targets = {189,190,191,272,273,283,285}
|
||||||
|
rows = [r for r in csv.DictReader(p.open('r', encoding='utf-8'), delimiter='\t') if int(r['entry_index'], 0) in targets]
|
||||||
|
for eid in sorted(targets):
|
||||||
|
print(f'ENTRY {eid}')
|
||||||
|
for r in rows:
|
||||||
|
if int(r['entry_index'], 0) != eid:
|
||||||
|
continue
|
||||||
|
length = (r.get('derived_body_length') or '').strip()
|
||||||
|
if not length:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
n = int(length, 0)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if n == 0:
|
||||||
|
continue
|
||||||
|
print(f" slot {r['slot']} {r['event_name_hint']}: start={r['derived_body_start']} end={r['derived_body_end']} len={r['derived_body_length']} raw={r['raw_event_entry_word']} template={r.get('repeated_template_status','')}")
|
||||||
|
|
@ -16,4 +16,6 @@ This file is an index. Detailed notes have been split into the `docs/` folder by
|
||||||
| [docs/raw-000a-000d.md](docs/raw-000a-000d.md) | 000d proximity/visibility buckets, 000a tracked handles, cache manager, init/shutdown, seg082 allocator, seg137/138 palette helpers, seg004/005 startup, 0x4588 object-role evidence, 000d VM owner/resource loader follow-up |
|
| [docs/raw-000a-000d.md](docs/raw-000a-000d.md) | 000d proximity/visibility buckets, 000a tracked handles, cache manager, init/shutdown, seg082 allocator, seg137/138 palette helpers, seg004/005 startup, 0x4588 object-role evidence, 000d VM owner/resource loader follow-up |
|
||||||
| [docs/far-call-targets.md](docs/far-call-targets.md) | Top-104 most-called far-call targets (Tiers 1-5, ranks 1-104), supporting functions discovered, analysis gaps and seg043 reconciliation |
|
| [docs/far-call-targets.md](docs/far-call-targets.md) | Top-104 most-called far-call targets (Tiers 1-5, ranks 1-104), supporting functions discovered, analysis gaps and seg043 reconciliation |
|
||||||
| [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md) | ScummVM Ultima8/Pentagram Crusader integration survey: USECODE/event tables, FLEX/resource formats, world/map loaders, HUD/media, and RE follow-up priorities |
|
| [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md) | ScummVM Ultima8/Pentagram Crusader integration survey: USECODE/event tables, FLEX/resource formats, world/map loaders, HUD/media, and RE follow-up priorities |
|
||||||
|
| [docs/pentagram-crusader-reference.md](docs/pentagram-crusader-reference.md) | Pentagram-source Crusader/U8 reference: direct Crusader USECODE parser and VM evidence, U8 usecode docs, runtime-confidence limits, and cross-checks against the ScummVM note |
|
||||||
| [docs/usecode-roundtrip-ir.md](docs/usecode-roundtrip-ir.md) | ScummVM-to-binary USECODE cross-walk, owner-loaded class-layout and header/event-count reconciliation, conservative IR v0 plan, and the generated class-event/body-window outputs that now ground reversible `_BOOT`, `SURCAM*`, and environmental family decompile artifacts plus repeated-family regression checks |
|
| [docs/usecode-roundtrip-ir.md](docs/usecode-roundtrip-ir.md) | ScummVM-to-binary USECODE cross-walk, owner-loaded class-layout and header/event-count reconciliation, conservative IR v0 plan, and the generated class-event/body-window outputs that now ground reversible `_BOOT`, `SURCAM*`, and environmental family decompile artifacts plus repeated-family regression checks |
|
||||||
|
| [docs/usecode-pentagram-ghidra-path.md](docs/usecode-pentagram-ghidra-path.md) | Pentagram-derived Crusader USECODE parser plan, proof-of-concept workflow, canonical IR v1 goals, and the Ghidra-side annotation import path |
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"3","code","0x40400","0x55A","None","","","","crusader_ne_segments.csv"
|
"3","code","0x40400","0x55A","None","","","","crusader_ne_segments.csv"
|
||||||
"4","code","0x40A00","0x10B1","Foothold","Reset/cache entry path","runtime_cache_reset_sequence","ASYLUM.24 and downstream reset callers still need tighter classification","crusader_decompilation_notes.md; plan-mid.md"
|
"4","code","0x40A00","0x10B1","Foothold","Reset/cache entry path","runtime_cache_reset_sequence","ASYLUM.24 and downstream reset callers still need tighter classification","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
"5","code","0x41E00","0x8D7","Partial","Startup/display transition prepare/driver lane","startup_display_transition_prepare; startup_display_transition_driver","The two main seg005 bodies are now named and tied to caller-side validation through vtable +0x0c, the seg108 0x4f38 sprite/object helper lane, the shared active-dispatch hold byte at 0x6828, the seg049 watch/controller lane at 0x2bd8, and the seg126 follow-up path; the exact higher-level state label is still unresolved","crusader_decompilation_notes.md; plan-mid.md"
|
"5","code","0x41E00","0x8D7","Partial","Startup/display transition prepare/driver lane","startup_display_transition_prepare; startup_display_transition_driver","The two main seg005 bodies are now named and tied to caller-side validation through vtable +0x0c, the seg108 0x4f38 sprite/object helper lane, the shared active-dispatch hold byte at 0x6828, the seg049 watch/controller lane at 0x2bd8, and the seg126 follow-up path; the exact higher-level state label is still unresolved","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
"6","code","0x42C00","0x75E","None","","","","crusader_ne_segments.csv"
|
"6","code","0x42C00","0x75E","Foothold","Gameplay-side masked materializer and local state/value selector lane","entity_vm_context_try_create_mask_0008_slot30_with_offset; entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready","Outer callers for the renamed seg006 helpers are still unresolved, and the higher-level gameplay subsystem owning the local state-selector and adjacent class-linked value family still needs caller-side recovery","plan-mid.md; docs/raw-0008-000c.md"
|
||||||
"7","code","0x43600","0x484","None","","","","crusader_ne_segments.csv"
|
"7","code","0x43600","0x484","None","","","","crusader_ne_segments.csv"
|
||||||
"8","code","0x43C00","0x1386","None","","","","crusader_ne_segments.csv"
|
"8","code","0x43C00","0x1386","None","","","","crusader_ne_segments.csv"
|
||||||
"9","code","0x45400","0x495","None","","","","crusader_ne_segments.csv"
|
"9","code","0x45400","0x495","None","","","","crusader_ne_segments.csv"
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
"46","code","0x7A200","0x7DC","None","","","","crusader_ne_segments.csv"
|
"46","code","0x7A200","0x7DC","None","","","","crusader_ne_segments.csv"
|
||||||
"47","code","0x7AC00","0x9B4","None","","","","crusader_ne_segments.csv"
|
"47","code","0x7AC00","0x9B4","None","","","","crusader_ne_segments.csv"
|
||||||
"48","code","0x7B800","0x63","None","","","","crusader_ne_segments.csv"
|
"48","code","0x7B800","0x63","None","","","","crusader_ne_segments.csv"
|
||||||
"49","code","0x7BA00","0x1E3F","Foothold","Watch/camera controller object lane","watch_entity_controller_create_global; watch_entity_controller_create; watch_entity_controller_dispatch_if_present; entity_set_watch_ptr","Exact controller-vs-watched-entity ownership is still open, but startup_display_transition_driver now gives caller-side confirmation that the shared active-dispatch hold byte is raised before the 0x2bd8 vtable +0x2c dispatch and cleared again immediately after the same watch/controller phase","crusader_decompilation_notes.md; plan-mid.md"
|
"49","code","0x7BA00","0x1E3F","Partial","Watch/camera controller object lane","watch_entity_controller_create_global; watch_entity_controller_create; watch_entity_controller_dispatch_if_present; entity_set_watch_ptr","The 0x2bd8 lane is now a real shared watch/controller object with verified vtable +0x2c/+0x30 dispatch, and the startup/display handoff bodies consistently raise or clear the borrowed active-dispatch hold byte around that controller phase; the exact controller-vs-watched-entity ownership label is still open","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
"50","code","0x7DE00","0x9C8","None","","","","crusader_ne_segments.csv"
|
"50","code","0x7DE00","0x9C8","None","","","","crusader_ne_segments.csv"
|
||||||
"51","code","0x7EA00","0x1D02","None","","","","crusader_ne_segments.csv"
|
"51","code","0x7EA00","0x1D02","None","","","","crusader_ne_segments.csv"
|
||||||
"52","code","0x80A00","0x1D65","None","","","","crusader_ne_segments.csv"
|
"52","code","0x80A00","0x1D65","None","","","","crusader_ne_segments.csv"
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
"67","code","0x8FE00","0x839","None","","","","crusader_ne_segments.csv"
|
"67","code","0x8FE00","0x839","None","","","","crusader_ne_segments.csv"
|
||||||
"68","code","0x90800","0xB4A","None","","","","crusader_ne_segments.csv"
|
"68","code","0x90800","0xB4A","None","","","","crusader_ne_segments.csv"
|
||||||
"69","code","0x91800","0x2A0","None","","","","crusader_ne_segments.csv"
|
"69","code","0x91800","0x2A0","None","","","","crusader_ne_segments.csv"
|
||||||
"70","code","0x91C00","0xF24","Foothold","File-handle allocation/open wrappers","file_handle_alloc_init_and_open; file_handle_open_with_mode","Exact DOS open/create flags and mode semantics still need caller-side argument decoding","crusader_decompilation_notes.md"
|
"70","code","0x91C00","0xF24","Partial","DOS file-handle lifecycle and owner-resource twin file-family loaders","file_handle_alloc_init_and_open; file_handle_open_with_mode","Exact DOS open/create flags and the per-family record schema behind the twin `0009:67b6` / `0009:6916` loader passes still need caller-side decoding","crusader_decompilation_notes.md; docs/raw-000a-000d.md; docs/raw-0008-000c.md"
|
||||||
"71","code","0x92E00","0x6C2","None","","","","crusader_ne_segments.csv"
|
"71","code","0x92E00","0x6C2","None","","","","crusader_ne_segments.csv"
|
||||||
"72","code","0x93600","0xCA1","None","","","","crusader_ne_segments.csv"
|
"72","code","0x93600","0xCA1","None","","","","crusader_ne_segments.csv"
|
||||||
"73","code","0x94600","0x9AA","None","","","","crusader_ne_segments.csv"
|
"73","code","0x94600","0x9AA","None","","","","crusader_ne_segments.csv"
|
||||||
|
|
@ -106,7 +106,7 @@
|
||||||
"105","code","0xAEC00","0x9F6","None","","","","crusader_ne_segments.csv"
|
"105","code","0xAEC00","0x9F6","None","","","","crusader_ne_segments.csv"
|
||||||
"106","code","0xAF800","0x1795","None","","","","crusader_ne_segments.csv"
|
"106","code","0xAF800","0x1795","None","","","","crusader_ne_segments.csv"
|
||||||
"107","code","0xB1400","0x40C","None","","","","crusader_ne_segments.csv"
|
"107","code","0xB1400","0x40C","None","","","","crusader_ne_segments.csv"
|
||||||
"108","code","0xB1A00","0x113F","Foothold","Active sprite/object state lane","sprite_object_clear_flag40_if_present; sprite_object_set_flag40_if_present","startup_display_transition_prepare now confirms repeated seg108 helper use around the shared active-dispatch creation, and the same window shows a bounded local counter/stack at object +0x196/+0x186 rather than reuse of the caller object validated through vtable +0x0c; the local bit 0x40 contract at 0x4f38+0x32 is now separated from the shared active-dispatch owner byte at 0x6828+0x40, but the higher-level meaning of the sprite/object lane and its relation to 0x4588 is still unresolved","crusader_decompilation_notes.md; plan-mid.md"
|
"108","code","0xB1A00","0x113F","Partial","Active sprite/object state lane","sprite_object_clear_flag40_if_present; sprite_object_set_flag40_if_present; sprite_object_push_state_word; sprite_object_pop_state_word","The 0x4f38 lane is now a verified bounded sprite/object state-word stack at +0x186/+0x196 with a separate local bit-0x40 contract at +0x32. It is reused across startup/display and later UI flows, and it is now clearly separated from both the validated seg005 caller object and the shared active-dispatch hold byte at 0x6828+0x40; the higher-level sprite/object meaning and any link to 0x4588 remain unresolved","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
"109","code","0xB2E00","0x1424","None","","","High-value gap around 000b:2e00 still unresolved","crusader_ne_segments.csv; crusader_decomp_progress.md"
|
"109","code","0xB2E00","0x1424","None","","","High-value gap around 000b:2e00 still unresolved","crusader_ne_segments.csv; crusader_decomp_progress.md"
|
||||||
"110","code","0xB4400","0x4C4","None","","","","crusader_ne_segments.csv"
|
"110","code","0xB4400","0x4C4","None","","","","crusader_ne_segments.csv"
|
||||||
"111","code","0xB4A00","0x489","None","","","","crusader_ne_segments.csv"
|
"111","code","0xB4A00","0x489","None","","","","crusader_ne_segments.csv"
|
||||||
|
|
@ -131,9 +131,9 @@
|
||||||
"130","code","0xCEA00","0x47D","None","","","","crusader_ne_segments.csv"
|
"130","code","0xCEA00","0x47D","None","","","","crusader_ne_segments.csv"
|
||||||
"131","code","0xCF000","0x44D","None","","","","crusader_ne_segments.csv"
|
"131","code","0xCF000","0x44D","None","","","","crusader_ne_segments.csv"
|
||||||
"132","code","0xCF600","0x3EB8","None","","","","crusader_ne_segments.csv"
|
"132","code","0xCF600","0x3EB8","None","","","","crusader_ne_segments.csv"
|
||||||
"133","code","0xD3800","0x215A","None","","","","crusader_ne_segments.csv"
|
"133","code","0xD3800","0x215A","Partial","VM masked-context creation, context save/load, and slot-value reload lane","entity_vm_context_try_create_masked_for_entity; entity_vm_context_create_from_slot_index; entity_vm_context_save; entity_vm_context_load; entity_vm_slot_load_value_plus_offset","The outer selector into entity_vm_opcode_sequence_run and the direct caller roles for the `0x0400/0x000a` and `0x0800/0x000b` offset-specialized wrappers remain unresolved, but the generic masked-create hub and persisted slot-plus-offset lane are now stable","plan-mid.md; docs/raw-0008-000c.md; docs/raw-000a-000d.md"
|
||||||
"134","code","0xD6000","0xEF0","Foothold","VM runtime bootstrap and post-init seeding","entity_vm_runtime_init_from_path_if_configured; entity_vm_referent_registry_init; entity_vm_runtime_release_slots; entity_vm_runtime_init_slots","Configured path/global at 0x65a and the exact external file format behind the 0x6611 runtime owner table still need tighter classification","plan-mid.md; docs/raw-0008-000c.md"
|
"134","code","0xD6000","0xEF0","Partial","VM runtime bootstrap, context seeding, and opcode sequencer support","entity_vm_runtime_init_from_path_if_configured; entity_vm_referent_registry_init; entity_vm_runtime_release_slots; entity_vm_runtime_init_slots; entity_vm_opcode_sequence_run","The upstream selector path into entity_vm_opcode_sequence_run and the exact configured owner-file naming at 0x65a still need caller-side recovery, but the runtime bootstrap, persisted slot-plus-offset lane, and sequencer entry/exit contract are now stable enough for partial coverage","plan-mid.md; docs/raw-0008-000c.md; docs/raw-000a-000d.md"
|
||||||
"135","code","0xD7000","0x3B7","Foothold","VM runtime owner-resource helper","entity_vm_runtime_owner_resource_create; entity_vm_runtime_owner_resource_destroy","Embedded file-backed helper class and 0x0d-stride slot-table population semantics still need callee-side recovery","plan-mid.md; docs/raw-0008-000c.md"
|
"135","code","0xD7000","0x3B7","Partial","VM runtime owner-resource helper and paired external file-family loader","entity_vm_runtime_owner_resource_create; entity_vm_runtime_owner_resource_destroy","The helper now has two parallel file-family loops at 0009:67b6 and 0009:6916 feeding separate buffers, but the exact per-family record schema and higher-level resource names are still unresolved","plan-mid.md; docs/raw-0008-000c.md"
|
||||||
"136","code","0xD7600","0x5BD","Partial","Shared active dispatch-entry owner and hold-state controller","active_dispatch_entry_mark_enabled; active_dispatch_entry_mark_disabled; active_dispatch_entry_create_default","The shared active entry is now tied to the seg126 DS:0x6341 transition-animation path and to the shared 0x31a2 break/hold depth; current evidence also separates its borrowed +0x40 presentation hold token from the seg108-local 0x4f38 bit-0x40 lane, but the exact higher-level transition/callback subsystem name is still unresolved","crusader_decompilation_notes.md; plan-mid.md"
|
"136","code","0xD7600","0x5BD","Partial","Shared active dispatch-entry owner and hold-state controller","active_dispatch_entry_mark_enabled; active_dispatch_entry_mark_disabled; active_dispatch_entry_create_default","The shared active entry is now tied to the seg126 DS:0x6341 transition-animation path and to the shared 0x31a2 break/hold depth; current evidence also separates its borrowed +0x40 presentation hold token from the seg108-local 0x4f38 bit-0x40 lane, but the exact higher-level transition/callback subsystem name is still unresolved","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
"137","code","0xD7E00","0xFBB","Partial","Palette and dispatch-entry emission helper family","entity_dispatch_entry_init_runtime_state; entity_dispatch_entry_release_runtime_state; vga_palette_set_all_black; vga_palette_set_all_white; vga_palette_set_all_rgb; dispatch_entry_create_black_palette_state_active; dispatch_entry_create_grayscale_palette_state_active; dispatch_entry_create_solid_palette_state_active","Higher-level event/script meaning is still unresolved, especially the paired 0x68bf object and the exact role of the 0004:5ad4-5b6e caller sequence","crusader_decompilation_notes.md; plan-mid.md"
|
"137","code","0xD7E00","0xFBB","Partial","Palette and dispatch-entry emission helper family","entity_dispatch_entry_init_runtime_state; entity_dispatch_entry_release_runtime_state; vga_palette_set_all_black; vga_palette_set_all_white; vga_palette_set_all_rgb; dispatch_entry_create_black_palette_state_active; dispatch_entry_create_grayscale_palette_state_active; dispatch_entry_create_solid_palette_state_active","Higher-level event/script meaning is still unresolved, especially the paired 0x68bf object and the exact role of the 0004:5ad4-5b6e caller sequence","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
"138","code","0xD9200","0x32E4","Partial","Entity cleanup/finalize with callback, watch-controller release, and dispatch-entry palette emission","entity_cleanup_resources_and_dispatch; sprite_redraw_global_if_active; FUN_000d_938c","Concrete callback-object subsystem naming is still unresolved, but this lane now has verified caller-side control of watch/controller state at 0x2bd8, uses the shared active-dispatch byte +0x40 as a borrowed presentation hold token rather than a local owner install, and emits two distinct 0x4588 payload pairs (entity +0x12d/+0x12f and +0x74f/+0x751) in addition to the palette-emission helpers","crusader_decompilation_notes.md; plan-mid.md"
|
"138","code","0xD9200","0x32E4","Partial","Entity cleanup/finalize with callback, watch-controller release, and dispatch-entry palette emission","entity_cleanup_resources_and_dispatch; sprite_redraw_global_if_active; FUN_000d_938c","Concrete callback-object subsystem naming is still unresolved, but this lane now has verified caller-side control of watch/controller state at 0x2bd8, uses the shared active-dispatch byte +0x40 as a borrowed presentation hold token rather than a local owner install, and emits two distinct 0x4588 payload pairs (entity +0x12d/+0x12f and +0x74f/+0x751) in addition to the palette-emission helpers","crusader_decompilation_notes.md; plan-mid.md"
|
||||||
|
|
|
||||||
|
|
|
@ -366,20 +366,113 @@ The 000c event handler at `000c:9703` is entered via the large cheat-event dispa
|
||||||
|
|
||||||
Key negative result: no function in the compiled C code directly pushes the value `0x410` into the game's event broadcast path. All three occurrences of the immediate `0x410` in the disassembly are: (a) the `CMP BX,0x410` comparison inside the 000c switch, (b) a multi-event subscription list at `000b:b5cb` (registering to receive the event), and (c) an abort-function error code at `000d:5290` unrelated to the cheat.
|
Key negative result: no function in the compiled C code directly pushes the value `0x410` into the game's event broadcast path. All three occurrences of the immediate `0x410` in the disassembly are: (a) the `CMP BX,0x410` comparison inside the 000c switch, (b) a multi-event subscription list at `000b:b5cb` (registering to receive the event), and (c) an abort-function error code at `000d:5290` unrelated to the cheat.
|
||||||
|
|
||||||
Conclusion: event 0x410 is generated exclusively by the **interpreted USECODE lane** (centered on `EUSECODE.FLX`), not by any static keyboard-level scan-code path in the compiled binary. The F10 keyboard branch in `seg001_input_keyboard_handler` is a separate `0x44` path gated by `0x6045`, not by `0x410`. Separate follow-up work on the imported `ASYLUM.DLL` shows that DLL exports `ASS_*` audio routines, so it should not be conflated with the immortality toggle path. The in-game trigger is still best modeled as a USECODE item or controller script, consistent with the surrounding string evidence (`000e:6337 "CruHealer"`, `000e:6341 "BatteryCharger"`, `000e:6445 "Controller"`, `000e:64ab "AutoFirer"` — these are USECODE process class names bracketing the Immortality string).
|
The strongest new compiled-side recovery in this pass is the seg109 listener object behind that subscription site. `cheat_event_listener_create` at `000b:b3b1` allocates one listener object and registers the shared cheat/control event bundle (`0x13d`, `0x1b`, `0x443`, `0x142`, `0x141`, `0x143`, `0x23f`, `0x43e`, `0x41f`, `0x417`, `0x431`, `0x411`, `0x410`, `0x441`, `0x421`, `0x22d`) through the seg109 registration helper at `000b:3d2a`. Its paired `cheat_event_listener_handle_event` body at `000b:b62c` is subscriber-side only: for event `0x410` it rewrites the event object's field `+0x6` to local state `0x0e` and falls into the shared `FUN_000b_b7f3` state-processing tail. That listener does not produce event `0x410`; it only reacts after the event has already been emitted elsewhere.
|
||||||
|
|
||||||
|
The generic compiled dispatch path is one step tighter now too. The larger `000c:8a62` wrapper first peels off local gated cases, then falls into the generic cheat/control event dispatcher at `000c:8c56`, which reads `event_object->code` from field `+0x6` and switches over values like `0x141`, `0x142`, `0x143`, `0x23f`, `0x410`, `0x431`, `0x441`, and `0x443`. That makes the shared event-object contract explicit: `000c:8c56` consumes the original emitted event id from `+0x6`, while `cheat_event_listener_handle_event` reuses the same `+0x6` field as a local state/subcommand code before entering `FUN_000b_b7f3`.
|
||||||
|
|
||||||
|
One extraction-side false lead is now closed too: the `TELEPAD` row in `USECODE/EUSECODE_extracted/class_event_index.tsv` with `raw_code_offset = 0x00000410` is a class-body offset for slot `0x20`, not direct evidence that `TELEPAD` emits gameplay event `0x410`.
|
||||||
|
|
||||||
|
The requested USECODE family sweep also tightened the player-trigger side without closing it. Inside `class_event_index.tsv`, `NPCTRIG` is the only requested family that is both explicitly event-bearing at the descriptor level and also has non-empty callable bodies in the current event-slot extraction (`equip` / slot `0x0a` at raw offset `0x0175`, plus one anonymous slot `0x20` body at raw offset `0x0159`). `SPECIAL`, `TRIGPAD`, and `REB_PAD` all have non-empty callable bodies too, but they remain referent/state neighbors rather than direct event carriers: `SPECIAL` shows bodies for `equip`, `enterFastArea`, `leaveFastArea`, and anonymous slots `0x20/0x21`; `TRIGPAD` shows `gotHit`; `REB_PAD` shows `gotHit` and anonymous slots `0x20/0x21`. None of those extracted bodies currently expose a verified `0x410` immediate or decoded event payload.
|
||||||
|
|
||||||
|
Conclusion: event 0x410 is generated exclusively by the **interpreted USECODE lane** (centered on `EUSECODE.FLX`), not by any static keyboard-level scan-code path in the compiled binary. The F10 keyboard branch in `seg001_input_keyboard_handler` is a separate `0x44` path gated by `0x6045`, not by `0x410`. Separate follow-up work on the imported `ASYLUM.DLL` shows that DLL exports `ASS_*` audio routines, so it should not be conflated with the immortality toggle path. The in-game trigger is still best modeled as a USECODE item or controller script, consistent with the surrounding string evidence (`000e:6337 "CruHealer"`, `000e:6341 "BatteryCharger"`, `000e:6445 "Controller"`, `000e:64ab "AutoFirer"` — these are USECODE process class names bracketing the Immortality string). The new extractor-side report `USECODE/EUSECODE_extracted/immortality_target_body_scan.md` now scans the strongest current bodies in `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `SPECIAL`, and `TRIGPAD` and finds no inline little-endian `0x0410`, no dword `0x00000410`, and no byte-swapped `0x1004` in any of them. That closes the immediate-emitter hypothesis for those currently exposed bodies and narrows the remaining frontier to data-driven decoding of the monolithic `EVENT` slot `0x0a` body and the compact `NPCTRIG` slot `0x0a` / `0x20` bodies, not to `TRIGPAD`, `SPECIAL`, `REB_PAD`, or `TELEPAD`.
|
||||||
|
|
||||||
|
The next extractor pass now pushes that one layer deeper. `USECODE/EUSECODE_extracted/immortality_body_structure.md` shows that `EVENT` slot `0x0a` is structurally a wide generic hub body, not a compact trigger leaf: it carries `90` internal `0x53 0x5c <u16> EVENT` subheaders, `383` local `0x5b` labels, and one wide tail-field set covering `event`, `item`, `source`, `dest`, `door`, `counter`, `counter2`, `link`, `time`, `post1`, `post2`, `floor`, and `flicMan`. By contrast, `NPCTRIG` stays compact and trigger-shaped. Slot `0x0a` has only `5` class-labelled subheaders and a narrow tail-field set (`referent`, `event`, `item`, `item2`), while slot `0x20` has only `1` such subheader and swaps the tail `event` field for `typeNpc` while keeping the same compact `item` / `item2` neighborhood. That is the strongest current player-trigger result: `EVENT` now reads as the generic event hub body, while the likeliest player-facing path is the `NPCTRIG` pair with slot `0x0a` as the compact event-bearing trigger body and slot `0x20` as its nearby typed/setup companion.
|
||||||
|
|
||||||
|
The next focused decode pass sharpens that split enough to treat the two `NPCTRIG` bodies differently instead of as one unresolved pair. New report `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` fixes the open-header parse and shows that slot `0x0a` starts with `0x5A 0x06 0x5C 0x013E NPCTRIG ... 0x0B 0x11`, then falls into a five-step clause ladder with subheaders at `0x0064/0x0093/0x00c2/0x00f1/0x0120`. Those subheaders sit on a uniform `0x2f` stride, their targets walk backward by the same amount, and each full-width clause carries one `branch_3f_0a`, one `push_24_51`, and one `writeback_57_02`. Slot `0x20` is structurally different: its prolog ends with event-code byte `0x01`, it has only one class-labelled subheader, no `writeback_57_02`, no `push_24_51`, and ten `field_4b_fe_0f` hits clustered around repeated `0x0a 00/05 4b fe 0f ...` windows before the tail field `69:000a -> typeNpc`. That is the strongest current descriptor-side reduction of the search space: slot `0x0a` now reads like the live event-bearing clause ladder, while slot `0x20` reads more like a typed gate or setup/attachment companion body than like a second emitter.
|
||||||
|
|
||||||
|
The runtime-side bridge is tighter too. The binary already had one exact offset-specialized masked wrapper for slot `0x0a`, `entity_vm_context_try_create_mask_0400_slot0a_with_offset` at `0005:2c35`, and the `000d:21ed -> 000d:22bc` lane is still verified as a slot-backed inline-payload consumer that copies a variable-length byte stream first and then consumes compact metadata bytes plus streamed words. The new body-structure report is consistent with that runtime contract: the surviving `EVENT` / `NPCTRIG` bodies are clause streams with repeated internal subheaders and local labels, not flat literal blobs. That still does **not** prove that `NPCTRIG 0x0a` emits `0x410` directly, but it narrows the best remaining emitter frontier from `EVENT or NPCTRIG` down to `NPCTRIG slot 0x0a` with `NPCTRIG slot 0x20` as the strongest adjacent support body.
|
||||||
|
|
||||||
|
The clause report makes that runtime comparison more concrete too. `0005:2c35` is no longer just an abstract "with offset" wrapper: `entity_vm_slot_load_value_plus_offset` at `000d:5572` now proves the extra word is applied additively to the loaded slot value before `000d:21ed` consumes the result. The internal consumer at `000d:21ed -> 000d:22bc` is tighter as well: after copying the inline blob into the context it reads two signed metadata bytes, uses byte A as the lead-word row count, uses byte B as the shared target-list width, performs `A x B` `entity_link` calls, and pushes back only non-`0x0400` words. That makes `NPCTRIG 0x0a` the only surviving compact body with a natural selector family for this lane: it has `5` evenly spaced clause starts at stride `0x2f`, while slot `0x20` has only one clause and no matching writeback/push motif. So the best current working model is no longer "EVENT or NPCTRIG" or even "NPCTRIG 0x0a plus 0x20 as co-equal bodies"; it is specifically "NPCTRIG slot `0x0a` event-bearing clause ladder, with slot `0x20` as a typed companion/setup body feeding or constraining the same family."
|
||||||
|
|
||||||
**Secondary handler (000b:b62c):**
|
**Secondary handler (000b:b62c):**
|
||||||
|
|
||||||
`000b:b62c` subscribes to event 0x410 via the registration at `000b:b5cb`. When event 0x410 is received by this handler, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to `000b:b7f3` for processing. This is a parallel state-machine path that runs alongside the 000c toggle; likely it drives an associated USECODE process or animation object into state 14.
|
`cheat_event_listener_handle_event` (`000b:b62c`) receives event 0x410 through the registration installed by `cheat_event_listener_create` at `000b:b3b1`. When event 0x410 arrives, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to `000b:b7f3` for processing. This is a parallel state-machine path that runs alongside the 000c toggle; likely it drives an associated USECODE process or animation object into state 14.
|
||||||
|
|
||||||
| Address | Symbol | Role |
|
| Address | Symbol | Role |
|
||||||
|-------------|-------------------------------|------|
|
|-------------|-------------------------------|------|
|
||||||
| `0004:c055` | `player_receive_damage_and_dispatch_effects` | Renamed. Contains the `0x604f` immortality gate at `0004:c205`. |
|
| `0004:c055` | `player_receive_damage_and_dispatch_effects` | Renamed. Contains the `0x604f` immortality gate at `0004:c205`. |
|
||||||
|
| `000b:b3b1` | `cheat_event_listener_create` | Allocates one seg109 listener object and subscribes it to the shared cheat/control event bundle that includes `0x410`. |
|
||||||
|
| `000b:b62c` | `cheat_event_listener_handle_event` | Subscriber-side event mapper: rewrites incoming `0x410` to local state `0x0e` before entering the shared listener state machine. |
|
||||||
| `DS:0x604f` | Immortality flag | Set/cleared by event `0x410`. Read only at `0004:c205`. |
|
| `DS:0x604f` | Immortality flag | Set/cleared by event `0x410`. Read only at `0004:c205`. |
|
||||||
| `DS:0x60d2` | Immortality-on notification ptr | Near pointer in DS; resolves to far ptr → "Immortality enabled." display. |
|
| `DS:0x60d2` | Immortality-on notification ptr | Near pointer in DS; resolves to far ptr → "Immortality enabled." display. |
|
||||||
| `DS:0x60ee` | Immortality-off notification ptr | Near pointer in DS; resolves to far ptr → "Immortality disabled." display. |
|
| `DS:0x60ee` | Immortality-off notification ptr | Near pointer in DS; resolves to far ptr → "Immortality disabled." display. |
|
||||||
| `000a:b988` | `video_bios_state_snapshot` | Called after notification display in the 0x410 toggle to refresh screen state. |
|
| `000a:b988` | `video_bios_state_snapshot` | Called after notification display in the 0x410 toggle to refresh screen state. |
|
||||||
|
|
||||||
|
### Hidden cheat menu investigation (seg109 UI lane)
|
||||||
|
|
||||||
|
New compiled-side evidence shows a real but likely dormant cheat-menu UI path:
|
||||||
|
|
||||||
|
| Address | Symbol | Role |
|
||||||
|
|-------------|-------------------------------------|------|
|
||||||
|
| `000b:9a86` | `cheat_menu_open_from_current_slot` | Builds a `cheat_event_listener` object, preloads selection from current slot state (`0x659c/0x659e`), pushes it through the sprite tree, and runs a modal draw/update loop. |
|
||||||
|
| `000b:9c0d` | `cheat_menu_open_modal` | Smaller modal wrapper that directly constructs `cheat_event_listener_create(...)`, traverses it, and returns. |
|
||||||
|
| `000b:b3b1` | `cheat_event_listener_create` | Constructor for the listener object. Registers event bundle including `0x23f`, `0x410`, `0x411`, `0x441`, etc. |
|
||||||
|
| `000b:b62c` | `cheat_event_listener_handle_event` | Listener event mapper; event `0x23f` toggles armed/visible state byte `+0x47`; event `0x410` remaps to local state `0x0e` then enters `FUN_000b_b7f3`. |
|
||||||
|
|
||||||
|
#### Reachability status in retail binary
|
||||||
|
|
||||||
|
- Static constructor callsites for `cheat_event_listener_create` are exactly two locations: `000b:9a9b` and `000b:9c56`.
|
||||||
|
- Static inbound xrefs to the wrapper entries `000b:9a86` and `000b:9c0d` are currently empty in the recovered code graph.
|
||||||
|
- The cheat-code matcher `cheat_code_check` (`0007:0d0a`) toggles `0x844/0x6045` and emits event `0x103`; it does **not** call these menu wrappers directly.
|
||||||
|
- The 000c handler for `0x103` (`000c:99dd`) executes a status/refresh lane and notification path; no direct call to `cheat_event_listener_create` appears there.
|
||||||
|
|
||||||
|
Current best read: this menu path is compiled and functional at object level, but likely orphaned/hidden in final gameplay flow (possibly debug/dev-only trigger removed, or only reachable through non-recovered data-driven callback wiring).
|
||||||
|
|
||||||
|
#### Retail patch-targeting trail
|
||||||
|
|
||||||
|
The practical patch work ended up being mostly about **finding a call site whose runtime context matches the hidden menu wrappers**, not just finding any place that reaches `000a:5276`.
|
||||||
|
|
||||||
|
Verified retail anchor points:
|
||||||
|
|
||||||
|
| File off | Ghidra | Meaning | Notes |
|
||||||
|
|----------|--------|---------|-------|
|
||||||
|
| `0x70d75` | `0007:0d75` | cheat matcher emits event `0x103` | retail bytes = `68 03 01 9A FF FF 00 00 83 C4 02`; NE fixup source = `0007:0d79` -> `seg092:0476` |
|
||||||
|
| `0x71d68` | fixup entry for `0007:0d79` | seg039 relocation record | exact retail entry: addr_type `0x03`, rel_type `0x00`, chain_off `0x2b79`, target `seg092:0476` |
|
||||||
|
| `0xc99dd` | `000c:99dd` | later controller-side handler that also executes `push 0x103 / call 000a:5276` | retail fixup source = `000c:99e1` -> `seg092:0476`; this is the first materially safer deferred hook candidate after the direct matcher path failed |
|
||||||
|
| `0xb9a8d` | `000b:9a8d` | arg setup inside `cheat_menu_open_from_current_slot` | original wrapper uses caller stack words `[BP+8]` and `[BP+6]` plus local armed flag `1` |
|
||||||
|
| `0xb9c48` | `000b:9c48` | arg setup inside `cheat_menu_open_modal` | original wrapper still feeds caller stack words `[BP+8]` and `[BP+6]` into `cheat_event_listener_create`, but starts with local byte `+0x47 = 0` |
|
||||||
|
|
||||||
|
What failed and why:
|
||||||
|
|
||||||
|
- Direct retarget of `0007:0d79` to `000b:9a86` crashed at startup when the NE relocation table was patched incorrectly as a raw far pointer. That was a file-format problem, not a semantic proof.
|
||||||
|
- After the patcher was made NE-fixup-aware, direct retarget to `000b:9a86` no longer broke startup, but the game hung when the cheat actually fired. Disassembly shows why: `cheat_menu_open_from_current_slot` consumes caller-supplied words at `[BP+8]` and `[BP+6]`, so the cheat matcher context is the wrong stack shape.
|
||||||
|
- Retargeting the same early cheat-matcher call to `000b:9c0d` got farther: the mouse pointer appeared, proving the hidden menu/display path was being entered. But it still hung with looping music, which points to **timing/context**, not a bad target address. The modal path appears unsafe when entered directly from the keyboard matcher even after the constructor args are forced to zero.
|
||||||
|
|
||||||
|
Current best patch rationale:
|
||||||
|
|
||||||
|
- `0007:0d75` is still the right place to intercept the cheat sequence itself because it is the verified success emission site.
|
||||||
|
- `000c:99dd` is the better candidate for the **actual menu-open call** because it is a later controller/event context, not the raw keyboard matcher frame.
|
||||||
|
- `000b:9c48` is the right argument-fix companion because it is the constructor-argument site for `cheat_menu_open_modal`, and the direct disassembly shows that this is where the wrapper still pulls caller-dependent words.
|
||||||
|
|
||||||
|
Rejected follow-up patch design:
|
||||||
|
|
||||||
|
- Site 1 tried changing `0007:0d75` from `push 0x103` to `push 0x42f`, keeping the original event-dispatch helper call intact.
|
||||||
|
- Site 2 retargeted the `000c:99e1` relocation so the `0x42f` handler's internal `push 0x103 / call 000a:5276` sequence called `cheat_menu_open_modal` instead.
|
||||||
|
- Site 3 patched `000b:9c48` from `6A 00 FF 76 08 FF 76 06` to `6A 00 6A 00 6A 00 90 90`.
|
||||||
|
|
||||||
|
Observed result on retail test build:
|
||||||
|
|
||||||
|
- The game no longer failed at startup, and the mouse pointer appeared when the cheat fired, confirming that the hidden modal UI path was being entered.
|
||||||
|
- But the game then halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit/teardown path (`"No pity. No mercy. No remorse."`).
|
||||||
|
- That is strong evidence that event `0x42f` is the wrong deferred hook context for this experiment even though the retargeted address itself was valid enough to enter the UI path.
|
||||||
|
|
||||||
|
Current patch candidate under test:
|
||||||
|
|
||||||
|
- Site 1: keep the original `0007:0d75` bytes and retarget only its existing far-call fixup from `seg092:0476` to `000b:9a86` (`cheat_menu_open_from_current_slot`).
|
||||||
|
- Site 2: patch `000b:9a8d` from `6A 01 FF 76 08 FF 76 06` to `6A 01 6A 00 6A 00 90 90`.
|
||||||
|
|
||||||
|
Rationale for the revised wrapper patch:
|
||||||
|
|
||||||
|
- Earlier direct-hook attempts proved that inheriting the two caller-frame words at `000b:9a8f/9a92` is unsafe from the cheat matcher context.
|
||||||
|
- But later decompilation of `cheat_event_listener_create` showed that the leading `push 0x1` at `000b:9a8d` is a distinct mode byte used by the constructor path, so zeroing all three pushed values was too aggressive.
|
||||||
|
- The current patch therefore preserves the leading `1` and only forces the two ambiguous 16-bit parameters to zero.
|
||||||
|
|
||||||
|
Risk notes:
|
||||||
|
|
||||||
|
- These remain behavioral exploration hacks, not correctness fixes.
|
||||||
|
- The evidence now strongly suggests the hard part is runtime context and event timing, not discovering the retail file offsets.
|
||||||
|
- If the revised direct `0007:0d79 -> 000b:9a86` path with the narrower `000b:9a8d` wrapper patch still fails, the next step should be a queue/defer design or a trampoline/cave patch rather than another blind event substitution.
|
||||||
|
|
||||||
### Conservative folklore verification
|
### Conservative folklore verification
|
||||||
|
|
||||||
- "Cheats can be enabled with `-laurie`" is **directly verified**.
|
- "Cheats can be enabled with `-laurie`" is **directly verified**.
|
||||||
|
|
@ -389,4 +482,6 @@ Conclusion: event 0x410 is generated exclusively by the **interpreted USECODE la
|
||||||
- "H enables hack mover" is **real at runtime** (strings confirmed), but not found in the static low-level byte dispatch; the activation comes from the USECODE scripting layer.
|
- "H enables hack mover" is **real at runtime** (strings confirmed), but not found in the static low-level byte dispatch; the activation comes from the USECODE scripting layer.
|
||||||
- "Immortality makes the player invincible" is **partially verified**: damage is divided by 262,144, making HP loss negligible; the hit stagger still plays. There is no bypass of the HP system entirely.
|
- "Immortality makes the player invincible" is **partially verified**: damage is divided by 262,144, making HP loss negligible; the hit stagger still plays. There is no bypass of the HP system entirely.
|
||||||
- "Immortality is toggled with a keyboard combo" is **not supported in compiled C code**: event 0x410 has no static keyboard dispatch path. It is USECODE-triggered.
|
- "Immortality is toggled with a keyboard combo" is **not supported in compiled C code**: event 0x410 has no static keyboard dispatch path. It is USECODE-triggered.
|
||||||
|
- `TELEPAD` slot `0x20` in `class_event_index.tsv` is **not** direct `0x410` event evidence; its `0x00000410` value is the extracted class-body offset for that slot.
|
||||||
|
- Among the requested USECODE families, `NPCTRIG` is the strongest remaining player-trigger candidate because it is explicitly event-bearing and also has extracted callable bodies, while `TRIGPAD`, `SPECIAL`, and `REB_PAD` currently read as neighboring referent/state/controller bodies rather than direct event carriers.
|
||||||
- The hidden five-byte matcher compares bytes from live code at `0007:2833`, and the ordinary keyboard ISR producer does not naturally emit byte values `0x80` and `0xfd` into record byte `+1`.
|
- The hidden five-byte matcher compares bytes from live code at `0007:2833`, and the ordinary keyboard ISR producer does not naturally emit byte values `0x80` and `0xfd` into record byte `+1`.
|
||||||
|
|
|
||||||
235
docs/pentagram-crusader-reference.md
Normal file
235
docs/pentagram-crusader-reference.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Pentagram Crusader Reference
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note mines Pentagram's Ultima 8 / Crusader code and bundled docs for evidence that is useful to current Crusader reverse-engineering, especially the USECODE / VM lane.
|
||||||
|
|
||||||
|
It complements [docs/scummvm-crusader-reference.md](docs/scummvm-crusader-reference.md). Where Pentagram and ScummVM agree, that usually strengthens provenance, but not always confidence: several of the relevant ScummVM Ultima8 components appear to descend from the same Pentagram-era implementation ideas, so matching behavior between the two should not be treated as fully independent confirmation.
|
||||||
|
|
||||||
|
## Highest-Value Findings
|
||||||
|
|
||||||
|
1. Pentagram contains direct Crusader USECODE parser and VM support, not just generic U8 notes.
|
||||||
|
Files: `convert/crusader/ConvertUsecodeCrusader.h`, `usecode/UsecodeFlex.cpp`, `usecode/Usecode.cpp`, `usecode/UCMachine.cpp`, `usecode/remorseintrinsics.h`, `kernel/GUIApp.cpp`.
|
||||||
|
|
||||||
|
2. Pentagram's older U8 USECODE documentation is still useful as contrast material because it shows which parts of the object/event model stayed stable and which parts changed in Crusader.
|
||||||
|
File: `docs/u8usecode.txt`.
|
||||||
|
|
||||||
|
3. Pentagram preserves one practical caution that ScummVM does not show as clearly: its Crusader runtime support is incomplete.
|
||||||
|
Files: `FAQ`, `world/Item.cpp`, `games/RemorseGame.cpp`.
|
||||||
|
|
||||||
|
4. Pentagram also records a few engine-format deltas that are useful outside USECODE, including Crusader map coordinate scaling, larger map chunks, and a wider Crusader `typeflag.dat` record.
|
||||||
|
Files: `world/Map.cpp`, `world/CurrentMap.cpp`, `graphics/TypeFlags.cpp`.
|
||||||
|
|
||||||
|
## Direct Pentagram Crusader Evidence
|
||||||
|
|
||||||
|
### USECODE class layout and event lookup
|
||||||
|
|
||||||
|
`usecode/UsecodeFlex.cpp` matches the broad Crusader model already noted from ScummVM:
|
||||||
|
|
||||||
|
- class body object = `classid + 2`
|
||||||
|
- class names come from object `1` at `name_object + 4 + 13 * classid`
|
||||||
|
- Crusader class base offset is read from bytes `8..11` of the class object and decremented by `1`
|
||||||
|
- Crusader event count is computed as `(get_class_base_offset(classid) + 19) / 6`
|
||||||
|
|
||||||
|
`usecode/Usecode.cpp` then resolves Crusader event offsets from class data at `20 + 6 * eventid`, using bytes `+2..+5` of each 6-byte row as the code offset.
|
||||||
|
|
||||||
|
Implication for current RE:
|
||||||
|
|
||||||
|
- Pentagram independently preserves the same `classid + 2` and 6-byte event-row reading used in the ScummVM note.
|
||||||
|
- The shared `(base + 19) / 6` event-count rule should still be treated carefully in current owner-loaded/raw EUSECODE work, because local binary validation already showed that this shared Pentagram/ScummVM rule is not a clean fit for sampled raw class records.
|
||||||
|
- In other words, Pentagram is strong provenance for the implementation lineage, but not a reason to override validated binary-side arithmetic.
|
||||||
|
|
||||||
|
### Crusader event-name table
|
||||||
|
|
||||||
|
`convert/crusader/ConvertUsecodeCrusader.h` provides a named Crusader event table for `0x00..0x1f`:
|
||||||
|
|
||||||
|
- clear names: `look`, `use`, `anim`, `setActivity`, `cachein`, `hit`, `gotHit`, `hatch`, `schedule`, `release`, `combine`, `calledFromAnim`, `enterFastArea`, `leaveFastArea`, `justMoved`, `AvatarStoleSomething`, `animGetHit`
|
||||||
|
- weak placeholders remain for `0x0a`, `0x0b`, `0x0d`, `0x11`, and `0x15..0x1f`
|
||||||
|
|
||||||
|
This is slightly rougher than the current ScummVM note in naming quality, but it is still useful because it shows which ordinals were already considered understood in the older Pentagram work and which ones remained unresolved.
|
||||||
|
|
||||||
|
### Crusader call opcode semantics inside the VM
|
||||||
|
|
||||||
|
`usecode/UCMachine.cpp` contains one especially useful comment-backed distinction:
|
||||||
|
|
||||||
|
- U8 opcode `0x11` calls a function at an explicit class/code offset
|
||||||
|
- Crusader opcode `0x11` calls function number `yy yy` of class `xx xx`, then translates that number through `get_class_event()`
|
||||||
|
|
||||||
|
That matters for current USECODE analysis because it reinforces the reading that Crusader bytecode is event-ordinal-driven in places where U8 was direct-offset-driven.
|
||||||
|
|
||||||
|
### Remorse intrinsic runtime table exists, but it is partial and sparse
|
||||||
|
|
||||||
|
`kernel/GUIApp.cpp` creates `UCMachine(RemorseIntrinsics, 308)` for Remorse, and `usecode/remorseintrinsics.h` holds that live runtime table.
|
||||||
|
|
||||||
|
What is useful:
|
||||||
|
|
||||||
|
- it confirms a real Remorse-specific runtime intrinsic table with at least `308` entries
|
||||||
|
- some entries are already mapped to concrete engine hooks such as frame/shape/status/quality accessors, item creation, movement helpers, egg helpers, and timer-tick access
|
||||||
|
|
||||||
|
What is not useful enough yet:
|
||||||
|
|
||||||
|
- the table is far sparser and rougher than ScummVM's later Remorse/Regret intrinsic descriptions
|
||||||
|
- many entries are still `0` or placeholder comments
|
||||||
|
|
||||||
|
Practical use:
|
||||||
|
|
||||||
|
- treat Pentagram intrinsics as secondary hints or provenance for older naming work
|
||||||
|
- prefer ScummVM for higher-coverage intrinsic labeling
|
||||||
|
- prefer raw binary behavior over either table for actual renames
|
||||||
|
|
||||||
|
### Version-sensitive global evidence
|
||||||
|
|
||||||
|
Pentagram's scratch notes add one useful wrinkle to the global-slot story:
|
||||||
|
|
||||||
|
- `docs/scratch/globals/remorse1.01.txt` starts with `global_address 003D`
|
||||||
|
- `docs/scratch/globals/regret1.01.txt` starts with `global_address 001E`
|
||||||
|
|
||||||
|
Cross-reference with ScummVM:
|
||||||
|
|
||||||
|
- the existing ScummVM note records Remorse global `0x003c` and Regret global `0x001e`
|
||||||
|
|
||||||
|
Safest read:
|
||||||
|
|
||||||
|
- Regret lines up cleanly at `0x001e`
|
||||||
|
- Remorse appears version-sensitive or notation-sensitive between Pentagram artifacts and later ScummVM code (`0x003d` in the Pentagram scratch output for Remorse 1.01 versus `0x003c` in the ScummVM runtime initialization path)
|
||||||
|
|
||||||
|
Implication for RE:
|
||||||
|
|
||||||
|
- keep Remorse global-slot claims version-tagged when possible
|
||||||
|
- do not collapse `0x003c` and `0x003d` into one unqualified global statement without checking game/version context
|
||||||
|
|
||||||
|
## U8-Specific Documentation That Still Helps
|
||||||
|
|
||||||
|
### `docs/u8usecode.txt`
|
||||||
|
|
||||||
|
This file is U8-specific, not direct Crusader evidence, but it is still useful in three ways.
|
||||||
|
|
||||||
|
First, it documents the older U8 class/object indexing model:
|
||||||
|
|
||||||
|
- object `0` = global flag names
|
||||||
|
- object `1` = usecode function names
|
||||||
|
- object `2 + shape` = shape-linked usecode body
|
||||||
|
- object `1026 + npc` = NPC-linked usecode body
|
||||||
|
|
||||||
|
Second, it records the classic U8 per-class layout:
|
||||||
|
|
||||||
|
- 12-byte header prefix
|
||||||
|
- 32 event pointers
|
||||||
|
- code body after that table
|
||||||
|
|
||||||
|
Third, it preserves an older event-meaning list for ordinals `0x00..0x1f`.
|
||||||
|
|
||||||
|
Why it still matters for Crusader:
|
||||||
|
|
||||||
|
- many semantic event labels survive into the Crusader table: `look`, `use`, `anim`, `cachein`, `hit`, `gotHit`, `hatch`, `schedule`, `release`, `combine`, `enterFastArea`, `leaveFastArea`, `AvatarStoleSomething`
|
||||||
|
- the document makes the Crusader deltas clearer: Crusader moved away from a fixed 32 x 4-byte event-pointer table and instead uses a 6-byte-per-event structure with event-number lookup in the VM
|
||||||
|
|
||||||
|
Recommended use:
|
||||||
|
|
||||||
|
- use `u8usecode.txt` as a contrast document for inherited VM concepts and event semantics
|
||||||
|
- do not use it as direct proof of Crusader container layout or opcode contracts
|
||||||
|
|
||||||
|
## Cross-Reference Against The Existing ScummVM Note
|
||||||
|
|
||||||
|
### Where Pentagram and ScummVM clearly agree
|
||||||
|
|
||||||
|
Both references point to the same core Crusader USECODE model:
|
||||||
|
|
||||||
|
- `classid + 2` class lookup
|
||||||
|
- class names in object `1`
|
||||||
|
- bytes `8..11` as the class header field used for Crusader code/event addressing
|
||||||
|
- 6-byte Crusader event rows
|
||||||
|
- named event ordinals `0x00..0x1f`
|
||||||
|
- a Crusader-specific VM/global path rather than a straight U8 reuse
|
||||||
|
|
||||||
|
This agreement is useful because it shows the model is not a one-off local interpretation.
|
||||||
|
|
||||||
|
### Where Pentagram adds something materially useful
|
||||||
|
|
||||||
|
Pentagram contributes a few things the ScummVM note did not emphasize as strongly:
|
||||||
|
|
||||||
|
- older U8 documentation that makes Crusader structural deltas easier to isolate
|
||||||
|
- explicit confirmation in `UCMachine.cpp` that Crusader opcode `0x11` is event-number dispatch, not raw offset dispatch
|
||||||
|
- scratch global dumps that expose version-sensitive Remorse versus Regret behavior
|
||||||
|
- explicit incompleteness warnings in the project itself, which help calibrate how much authority to assign to runtime behavior
|
||||||
|
|
||||||
|
### Where Pentagram should not increase confidence much
|
||||||
|
|
||||||
|
For the current header/count dispute in owner-loaded/raw EUSECODE parsing, Pentagram and ScummVM agreeing with each other does not settle the question.
|
||||||
|
|
||||||
|
Reason:
|
||||||
|
|
||||||
|
- the relevant Pentagram and ScummVM Crusader USECODE code paths are very close in structure
|
||||||
|
- that makes them best treated as one implementation lineage, not two independent external confirmations
|
||||||
|
|
||||||
|
Current rule for RE remains:
|
||||||
|
|
||||||
|
- use Pentagram/ScummVM to anchor object indexing, row size, event labels, and VM intent
|
||||||
|
- keep the local binary-validated class-header arithmetic as the authority when the shared engine code disagrees with sampled Crusader records
|
||||||
|
|
||||||
|
## Non-USECODE Engine Findings Worth Keeping
|
||||||
|
|
||||||
|
These are lower priority than the USECODE sections, but still useful for future binary-side work.
|
||||||
|
|
||||||
|
### Map loading
|
||||||
|
|
||||||
|
`world/Map.cpp` shows that Crusader on-disk map records are still read as 16-byte records, but Pentagram doubles `x` and `y` after loading when `GAME_IS_CRUSADER`.
|
||||||
|
|
||||||
|
Implication:
|
||||||
|
|
||||||
|
- if a raw loader appears to scale map coordinates or if current external-map tooling sees a factor-of-two mismatch, Pentagram provides a concrete engine-side reason to test that path
|
||||||
|
|
||||||
|
### Current map chunking
|
||||||
|
|
||||||
|
`world/CurrentMap.cpp` sets `mapChunkSize = 1024` for Crusader versus `512` for U8.
|
||||||
|
|
||||||
|
Implication:
|
||||||
|
|
||||||
|
- this matches the broader cross-project pattern that Crusader is not just U8 data with renamed files; some world/grid assumptions are materially different
|
||||||
|
|
||||||
|
### Crusader `typeflag.dat`
|
||||||
|
|
||||||
|
`graphics/TypeFlags.cpp` switches Crusader to 9-byte records instead of U8's 8-byte records, with extended family-bit handling and multiple Crusader-only flag placeholders.
|
||||||
|
|
||||||
|
Implication:
|
||||||
|
|
||||||
|
- Crusader `typeflag.dat` should continue to be treated as its own format family
|
||||||
|
- any local parser or reverse-engineered structure should not inherit the U8 8-byte layout blindly
|
||||||
|
|
||||||
|
## Confidence Limits
|
||||||
|
|
||||||
|
Pentagram is valuable, but only in bounded ways.
|
||||||
|
|
||||||
|
Direct reasons for caution:
|
||||||
|
|
||||||
|
- `FAQ` says Crusader support was a future goal, not a completed feature
|
||||||
|
- `games/RemorseGame.cpp` is clearly incomplete compared with the ScummVM Crusader startup path
|
||||||
|
- `world/Item.cpp` explicitly disables all Crusader usecode events except `use()`
|
||||||
|
|
||||||
|
So for current Crusader RE, the best weighting is:
|
||||||
|
|
||||||
|
- high confidence: parser/disassembler layout clues, event ordinals, VM intent, container/indexing models, file-format deltas
|
||||||
|
- medium confidence: sparse Remorse intrinsic names and scratch global artifacts
|
||||||
|
- low confidence: full runtime behavior, startup semantics, and any absence-based conclusion from Pentagram's Crusader execution path
|
||||||
|
|
||||||
|
## Most Useful Pentagram Files
|
||||||
|
|
||||||
|
- `convert/crusader/ConvertUsecodeCrusader.h`
|
||||||
|
- `usecode/UsecodeFlex.cpp`
|
||||||
|
- `usecode/Usecode.cpp`
|
||||||
|
- `usecode/UCMachine.cpp`
|
||||||
|
- `docs/u8usecode.txt`
|
||||||
|
- `docs/scratch/globals/remorse1.01.txt`
|
||||||
|
- `world/Item.cpp`
|
||||||
|
- `graphics/TypeFlags.cpp`
|
||||||
|
- `world/Map.cpp`
|
||||||
|
- `world/CurrentMap.cpp`
|
||||||
|
|
||||||
|
## Practical RE Follow-Ups
|
||||||
|
|
||||||
|
1. Keep using Pentagram and ScummVM event names as slot-label hints only, especially for `0x0a`, `0x0b`, `0x11`, and the still-placeholder high ordinals.
|
||||||
|
2. When documenting Crusader USECODE VM behavior, cite Pentagram's `opcode 0x11 = class/event dispatch` distinction alongside the existing ScummVM reference.
|
||||||
|
3. Keep local owner-loaded/raw EUSECODE arithmetic authoritative over the shared Pentagram/ScummVM `(base + 19) / 6` rule until a direct main USECODE sample proves otherwise.
|
||||||
|
4. Tag Remorse global-slot references with version context when using Pentagram scratch outputs.
|
||||||
|
5. Reuse Pentagram's map/typeflag deltas when a future binary pass returns to world loaders or shape/type metadata.
|
||||||
|
6. Treat missing behavior in Pentagram's Crusader runtime as non-evidence unless ScummVM or raw binary analysis supports the same absence.
|
||||||
|
|
@ -201,9 +201,9 @@ Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/
|
||||||
| `000c:84c3` | `entity_state_set_byte40_at_global_ptr` | Sets byte `[g_active_dispatch_entry_farptr + 0x40] = 1` then calls thunk unconditionally; current evidence treats this as raising the shared active-entry transition/display hold byte rather than toggling an unrelated global |
|
| `000c:84c3` | `entity_state_set_byte40_at_global_ptr` | Sets byte `[g_active_dispatch_entry_farptr + 0x40] = 1` then calls thunk unconditionally; current evidence treats this as raising the shared active-entry transition/display hold byte rather than toggling an unrelated global |
|
||||||
| `000c:ac55` | `entity_state_fire_if_handle_valid` | Guard: fires thunk dispatch only when `[0x6054] != -1`; no-op otherwise |
|
| `000c:ac55` | `entity_state_fire_if_handle_valid` | Guard: fires thunk dispatch only when `[0x6054] != -1`; no-op otherwise |
|
||||||
| `000c:ac6d` | `entity_state_fire_with_args_if_handle_valid` | 3-arg variant: pushes `[BP+0xe]` (byte), `[BP+0xc]`, `[BP+0xa]`, handle `[0x6054]`, then `CALLF 0000:ffff` |
|
| `000c:ac6d` | `entity_state_fire_with_args_if_handle_valid` | 3-arg variant: pushes `[BP+0xe]` (byte), `[BP+0xc]`, `[BP+0xa]`, handle `[0x6054]`, then `CALLF 0000:ffff` |
|
||||||
| `000c:afa5` | `entity_state_check_field49_and_call_vfunc3c` | Checks field `[ptr+0x49]`: −1→reset to 0 return 1; 2→call `vtable[0x3c]` return 0; else thunk dispatch |
|
| `000c:afa5` | `transition_file_family_select_and_refresh` | Local startup/display selector: `field49==-1` normalizes to `0`; `field49==2` dispatches `vtable[0x3c]`; `field49==0/1/4` composes one of three sibling filenames from inherited base `0x6aa:0x6ac` plus stem/suffix buffers `0x621c/0x6223`, `0x621c/0x622d`, or `0x621c/0x6237`, loads the result into object `+0x520`, then runs the shared redraw/palette/input refresh path |
|
||||||
| `000c:b153` | `entity_state_animation_done_tick` | Checks `[param_2+0x14+0xa]` animation-complete flag; if zero increments `field49` and calls `entity_state_check_field49_and_call_vfunc3c`; if set calls `vtable[0x3c]` |
|
| `000c:b153` | `transition_file_family_advance_on_anim_tick` | Polls `[param_2+0x14+0xa]`; when clear increments `field49` and re-enters `transition_file_family_select_and_refresh`, otherwise exits through `vtable[0x3c]` |
|
||||||
| `000c:b199` | `entity_state_input_key_handler` | Full input dispatcher: ESC/x/X → `vtable[0x3c]` (cancel); Left/Right arrows `0x14b/0x148` → prev state; n/N/`0x14d/0x150` → next state; e/E → set `field47=1`; `-` with counter → trigger at 4. Manages `field47` and `field49` |
|
| `000c:b199` | `transition_file_family_input_key_handler` | Local selector key handler: ESC/x/X → `vtable[0x3c]`; Left/Right arrows `0x14b/0x148` → previous file-family state; n/N/`0x14d/0x150` → next state; e/E arms `field47`; `-` after arming counts up to forced state `4`; selector moves drain the event queue and clear `0x8a94/0x8a96/0x8a98` |
|
||||||
| `000c:b2c3` | `stub_noop_000c_b2c3` | Empty stub; returns immediately |
|
| `000c:b2c3` | `stub_noop_000c_b2c3` | Empty stub; returns immediately |
|
||||||
| `000c:b2c8` | `entity_state_dispatch_if_field49_eq4` | Fires thunk only when `[ptr+0x49]==4` |
|
| `000c:b2c8` | `entity_state_dispatch_if_field49_eq4` | Fires thunk only when `[ptr+0x49]==4` |
|
||||||
| `000c:b349` | `entity_state_dispatch_if_far_ptr_nonzero_a` | Fires thunk if far-pointer args non-zero |
|
| `000c:b349` | `entity_state_dispatch_if_far_ptr_nonzero_a` | Fires thunk if far-pointer args non-zero |
|
||||||
|
|
@ -211,8 +211,8 @@ Second sweep through `000c` adjacent helpers — gated thunk wrappers and input/
|
||||||
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
|
| `000c:b3d8` | `entity_state_dispatch_if_far_ptr_nonzero_b` | Same null-guard pattern as `b349`, variant b |
|
||||||
|
|
||||||
**Patterns confirmed:**
|
**Patterns confirmed:**
|
||||||
- `field49` = state-sequence index; 0=reset, 2=vtable callback, 4=triggered end
|
- `field49` = local transition file-family selector state in this startup/display family; `0/1/4` choose sibling filenames under shared base `0x6aa:0x6ac` plus stem `0x621c`, `2` dispatches `vtable[0x3c]`, and `-1` normalizes back to `0`
|
||||||
- `field47` = keystroke-combo counter
|
- `field47` = keystroke arm/counter for the local `e/E` then `-` path into selector state `4`
|
||||||
- `field3f` = linked data pointer (event/record reference)
|
- `field3f` = linked data pointer (event/record reference)
|
||||||
- `[0x6054]` = current entity handle; `[0x6828]` = `g_active_dispatch_entry_farptr`, the shared active-dispatch entry owner whose byte `+0x40` is reused across the startup/display lane as a hold/busy token
|
- `[0x6054]` = current entity handle; `[0x6828]` = `g_active_dispatch_entry_farptr`, the shared active-dispatch entry owner whose byte `+0x40` is reused across the startup/display lane as a hold/busy token
|
||||||
- Bits in `[ptr+0x5b]`: `0x1=init`, `0x2=active/event`, `0x40=pending dispatch`, `0x100=flag100`, `0x180=skip-all mask`
|
- Bits in `[ptr+0x5b]`: `0x1=init`, `0x2=active/event`, `0x40=pending dispatch`, `0x100=flag100`, `0x180=skip-all mask`
|
||||||
|
|
@ -259,15 +259,17 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
|
||||||
- `entity_vm_slot_index_from_entity` (`000d:45c5`) computes one slot index from a gameplay entity by branching on seg021 class/type helpers and then adding one of the current runtime base offsets `0x8c7c/0x8c7e/0x8c80`
|
- `entity_vm_slot_index_from_entity` (`000d:45c5`) computes one slot index from a gameplay entity by branching on seg021 class/type helpers and then adding one of the current runtime base offsets `0x8c7c/0x8c7e/0x8c80`
|
||||||
- `entity_vm_context_try_create_masked_for_entity` (`000d:463a`) uses that slot index to test one owner-side mask entry before it creates a context, which is the strongest current bridge from gameplay entities into this VM lane
|
- `entity_vm_context_try_create_masked_for_entity` (`000d:463a`) uses that slot index to test one owner-side mask entry before it creates a context, which is the strongest current bridge from gameplay entities into this VM lane
|
||||||
- `entity_vm_context_create_from_slot_index` (`000d:46ec`) allocates one `0x6714` context object, seeds its `+0xd6/+0xd8` lane through `entity_vm_slot_load_value_plus_offset`, initializes the local mini-VM state, and can prepend caller data into the backward-growing buffer at `+0x102`
|
- `entity_vm_context_create_from_slot_index` (`000d:46ec`) allocates one `0x6714` context object, seeds its `+0xd6/+0xd8` lane through `entity_vm_slot_load_value_plus_offset`, initializes the local mini-VM state, and can prepend caller data into the backward-growing buffer at `+0x102`
|
||||||
|
- `entity_vm_opcode_sequence_run` (`000d:ebe3`) is now named conservatively in Ghidra: it seeds the stage chain from object `+0xfe`, runs `000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104`, then finishes with tracked-handle cleanup plus the `0008:ebe7` gate on object `+0xc0` and byte `+0x4b`
|
||||||
- `entity_vm_context_sync_global_value_and_dispatch` (`000d:48da`) is the current context-side runner/sync point: it marks the context busy at `+0x123`, calls `entity_vm_set_field_da_to_global`, optionally writes the current value through `+0x11b/+0x11d`, and dispatches through the context vtable on success
|
- `entity_vm_context_sync_global_value_and_dispatch` (`000d:48da`) is the current context-side runner/sync point: it marks the context busy at `+0x123`, calls `entity_vm_set_field_da_to_global`, optionally writes the current value through `+0x11b/+0x11d`, and dispatches through the context vtable on success
|
||||||
- `entity_vm_context_save` / `entity_vm_context_load` / `entity_vm_context_destroy` / `entity_vm_context_free_buffer` (`000d:498f`, `000d:4a78`, `000d:4962`, `000d:48b6`) now pin down the lifecycle of this object family rather than leaving the whole `000d:45xx..4exx` island anonymous
|
- `entity_vm_context_save` / `entity_vm_context_load` / `entity_vm_context_destroy` / `entity_vm_context_free_buffer` (`000d:498f`, `000d:4a78`, `000d:4962`, `000d:48b6`) now pin down the lifecycle of this object family rather than leaving the whole `000d:45xx..4exx` island anonymous
|
||||||
- `entity_vm_context_try_create_masked_for_entity` is now better constrained at the return-value level too: after the runtime-disable check at `0x6610` and the owner-side slot-mask test succeed, it reports two distinct success shapes. Immediate-flagged contexts (`+0x16 & 0x0008`) clear the caller output word, while object-backed contexts return the created object's low word. That makes the helper a typed bridge from gameplay entities into VM-backed object results, not only a yes/no mask probe.
|
- `entity_vm_context_try_create_masked_for_entity` is now better constrained at the return-value level too: after the runtime-disable check at `0x6610` and the owner-side slot-mask test succeed, it reports two distinct success shapes. Immediate-flagged contexts (`+0x16 & 0x0008`) clear the caller output word, while object-backed contexts return the created object's low word. That makes the helper a typed bridge from gameplay entities into VM-backed object results, not only a yes/no mask probe.
|
||||||
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) is now one step tighter too: the embedded seg069/070 helper is file-backed rather than abstract. Construction starts with `dos_file_handle_init` (`0009:1c00`), then uses helper vtable slot `+0x04` as the size query that drives the child `+0x10/+0x12` allocation and helper vtable slot `+0x0c` as the table-population callback for the `0x0d`-stride owner table.
|
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) is now one step tighter too: the embedded seg069/070 helper is file-backed rather than abstract. Construction starts with `dos_file_handle_init` (`0009:1c00`), then uses helper vtable slot `+0x04` as the size query that drives the child `+0x10/+0x12` allocation and helper vtable slot `+0x0c` as the table-population callback for the `0x0d`-stride owner table.
|
||||||
- That file-backed helper is now tighter one step deeper as well. The seg070 loops rooted at raw windows `0009:67b6` and `0009:6916` walk helper-owned record arrays at object `+0x10/+0x18`, format per-entry paths through the seg001 string helpers (`0003:e4d3` / `0003:e590`), then open, read, and close each file through `file_handle_alloc_init_and_open` (`0009:1c3a`), `dos_file_seek` (`0009:2034`), and `dos_file_close` (`0009:1e61`). The paired `+0x18` entries are consumed as 16-bit ids passed into those path-format loops beside the far-pointer path table at `+0x10`; no object-1 or `classid + 2` arithmetic appears there, so the safest current read is slot-local file ids rather than exposed original class/object indices. That is strong evidence that `000d:7000` seeds the owner table from an indexed external file set rather than by copying one monolithic in-memory descriptor blob.
|
- That file-backed helper is now tighter one step deeper as well. The seg070 loops rooted at raw windows `0009:67b6` and `0009:6916` walk helper-owned record arrays at object `+0x10/+0x18`, format per-entry paths through the seg001 string helpers (`0003:e4d3` / `0003:e590`), then open, read, and close each file through `file_handle_alloc_init_and_open` (`0009:1c3a`), `dos_file_seek` (`0009:2034`), and `dos_file_close` (`0009:1e61`). The paired `+0x18` entries are consumed as 16-bit ids passed into those path-format loops beside the far-pointer path table at `+0x10`; no object-1 or `classid + 2` arithmetic appears there, so the safest current read is slot-local file ids rather than exposed original class/object indices. That is strong evidence that `000d:7000` seeds the owner table from an indexed external file set rather than by copying one monolithic in-memory descriptor blob.
|
||||||
- A final loader-side tightening from the current pass is that `0009:67b6` and `0009:6916` now read as twin entry walkers rather than one isolated path-format callback. Both windows iterate the helper-owned count at `+0x14`, index the far-pointer path table at `+0x10` and paired 16-bit id table at `+0x18`, check the source path through `0003:e669`, build formatted paths with distinct local format strings (`DS:3f2d` vs `DS:3f40`), and then reach the same file open/read/close lane. The remaining open question is not whether they are file-backed, but whether they represent two file families, two record templates, or two load phases inside the same helper class.
|
- A final loader-side tightening from the current pass is that `0009:67b6` and `0009:6916` now read as paired file-family walkers rather than one isolated path-format callback. Both windows iterate the helper-owned count at `+0x14`, index the far-pointer path table at `+0x10` and paired 16-bit id table at `+0x18`, check the source path through `0003:e669`, build formatted paths with distinct local format strings (`DS:3f2d` vs `DS:3f40`), and then reach the same file open/read/close lane. Each loop also writes into its own independently allocated output far buffer before the shared trailer runs, so the best current reading is two parallel file families or record banks loaded by the same helper rather than two phases over one shared buffer. The remaining open question is the exact per-family record schema and higher-level resource role, not whether the helper is file-backed.
|
||||||
- The caller-side bootstrap for that helper is now anchored too: `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) first checks the configured byte/string global at `0x65a`, builds a path through seg072 helper `0009:3600` using globals `0x6d6:0x6d8` plus `0x65a`, validates that path through `000a:500a`, then calls `entity_vm_runtime_create(0,0,path)`. This is the first verified source-argument path for `entity_vm_runtime_owner_resource_create`, and it strongly suggests the owner/resource table is loaded from an external configured file rather than from a purely in-memory descriptor blob.
|
- The caller-side bootstrap for that helper is now anchored too: `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) first checks the configured byte/string global at `0x65a`, builds a path through seg072 helper `0009:3600` using globals `0x6d6:0x6d8` plus `0x65a`, validates that path through `000a:500a`, then calls `entity_vm_runtime_create(0,0,path)`. This is the first verified source-argument path for `entity_vm_runtime_owner_resource_create`, and it strongly suggests the owner/resource table is loaded from an external configured file rather than from a purely in-memory descriptor blob.
|
||||||
- Seg072 helper `0009:3600` is now classified more tightly as a rotating slash-aware path composer rather than a generic buffer advance helper. Its prologue cycles through five `0x50`-byte temp buffers, and its inner cases append optional string parts while inserting `\` only when adjacent path components need a separator. That narrows the two globals used by `000d:44df`: `0x65a` behaves as the configured relative runtime-owner filename/path component, while `0x6d6:0x6d8` behaves as the mutable base/resource-root path buffer that gets joined with `0x65a` before `000a:500a` validation.
|
- Seg072 helper `0009:3600` is now classified more tightly as a rotating slash-aware path composer rather than a generic buffer advance helper. Its prologue cycles through five `0x50`-byte temp buffers, and its inner cases append optional string parts while inserting `\` only when adjacent path components need a separator. That narrows the two globals used by `000d:44df`: `0x65a` behaves as the configured relative runtime-owner filename/path component, while `0x6d6:0x6d8` behaves as the mutable base/resource-root path buffer that gets joined with `0x65a` before `000a:500a` validation.
|
||||||
- The two still-xref-dark wrappers `0005:2c35` and `0005:2c68` are also narrower now. Their signed extra word does not participate in owner-mask selection inside `entity_vm_context_try_create_masked_for_entity`; it is forwarded into `entity_vm_context_create_from_slot_index`, stored in context field `+0x34`, and passed on to `entity_vm_slot_load_value_plus_offset`. The best current reading is therefore `offset-specialized masked context creation`, not a separate direct selector lane.
|
- The two still-xref-dark wrappers `0005:2c35` and `0005:2c68` are also narrower now. Their signed extra word does not participate in owner-mask selection inside `entity_vm_context_try_create_masked_for_entity`; it is forwarded into `entity_vm_context_create_from_slot_index`, stored in context field `+0x34`, and passed on to `entity_vm_slot_load_value_plus_offset`. The best current reading is therefore `offset-specialized masked context creation`, not a separate direct selector lane.
|
||||||
|
- Ghidra now records that signed-offset contract directly in the wrapper names too: `0005:2c35` = `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `0005:2c68` = `entity_vm_context_try_create_mask_0800_slot0b_with_offset`. That still stops short of real caller-role recovery, but it removes the last ambiguity about whether the extra stack word is semantically live.
|
||||||
- The first opcode-level behavior split inside that runtime is now visible in the `000d:0988` family:
|
- The first opcode-level behavior split inside that runtime is now visible in the `000d:0988` family:
|
||||||
- one branch calls `entity_vm_referent_chain_append_unique_from`, which looks like an attach/union operation on the current referent payload chain
|
- one branch calls `entity_vm_referent_chain_append_unique_from`, which looks like an attach/union operation on the current referent payload chain
|
||||||
- the `0x1a/0x1b` branch instead calls `entity_vm_referent_chain_remove_matching_from`, which looks like the inverse operation and makes the opcode family materially closer to a graph-editing script VM than a flat event list
|
- the `0x1a/0x1b` branch instead calls `entity_vm_referent_chain_remove_matching_from`, which looks like the inverse operation and makes the opcode family materially closer to a graph-editing script VM than a flat event list
|
||||||
|
|
@ -307,11 +309,14 @@ Globals used: `[0x6312]`=start index, `[0x6314]`=count, `[0x630e]`=palette src p
|
||||||
|
|
||||||
Conservative interpretation after this pass:
|
Conservative interpretation after this pass:
|
||||||
|
|
||||||
- The `000d:21ed -> 000d:22bc` lane is strongly supported as a slot-backed payload to entity-link closure path, where two byte-sized metadata fields shape the matrix walk and word entries are link/entity ids.
|
- The `000d:21ed -> 000d:22bc` lane is strongly supported as a slot-backed payload to entity-link closure path, where two signed byte-sized metadata fields shape an exact `A x B` matrix walk: byte A is the lead-word row count, byte B is the shared target-list width, and the word entries passed to `entity_link` are runtime link/entity ids rather than descriptor selectors.
|
||||||
- Descriptor-family alignment is therefore stronger with generic active event ecosystems (`EVENT`/`NPCTRIG`/`*_BOOT`/`SFXTRIG`) than with `SURCAM*` callback holders, because no direct `eventTrigger`-specific discriminator is read in this lane.
|
- Descriptor-family alignment is therefore stronger with generic active event ecosystems (`EVENT`/`NPCTRIG`/`*_BOOT`/`SFXTRIG`) than with `SURCAM*` callback holders, because no direct `eventTrigger`-specific discriminator is read in this lane.
|
||||||
- Direct descriptor-id attribution is still rejected for now: no code evidence ties the consumed bytes/words here to explicit EUSECODE class indices or to a hard `JELYHACK`/`SURCAM*` switch.
|
- Direct descriptor-id attribution is still rejected for now: no code evidence ties the consumed bytes/words here to explicit EUSECODE class indices or to a hard `JELYHACK`/`SURCAM*` switch.
|
||||||
|
- The new extractor-side structure pass tightens the descriptor-side fit inside that generic active-event ecosystem. `USECODE/EUSECODE_extracted/immortality_body_structure.md` shows `EVENT` slot `0x0a` as a broad hub clause stream with `90` internal `0x53 0x5c <u16> EVENT` subheaders and the widest field trailer, while `NPCTRIG` slot `0x0a` stays compact at `5` subheaders and a narrow `referent/event/item/item2` tail. That does not prove a direct class-id bridge into `000d:21ed -> 000d:22bc`, but it does make `NPCTRIG slot 0x0a` the strongest remaining compact descriptor-side candidate for the offset-specialized slot-`0x0a` runtime wrapper `entity_vm_context_try_create_mask_0400_slot0a_with_offset` (`0005:2c35`) instead of the older undifferentiated `EVENT or NPCTRIG` frontier.
|
||||||
|
- The next focused extractor pass sharpens that fit again. `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` now shows `NPCTRIG` slot `0x0a` as a fixed-width five-clause ladder: subheaders at `0x0064/0x0093/0x00c2/0x00f1/0x0120`, uniform `0x2f` stride, backward-walking targets, and one `branch_3f_0a` + `push_24_51` + `writeback_57_02` triple in each full clause. The new runtime-fit section also matters: `000d:5572` proves the extra word from `0005:2c35` is additive (`entity_vm_slot_load_value(...) + offset`), so slot `0x0a` now exposes the only surviving compact five-row selector family that plausibly matches byte A in `000d:21ed`, while slot `0x20` remains a one-clause typeNpc-heavy body with no comparable writeback/push motif or stride family.
|
||||||
|
- The downstream-use follow-up weakens that direct selector fit. Instruction windows at `000d:47ef..47f3` show `entity_vm_context_create_from_slot_index` storing slot index `SI` at `+0x32` and the dynamic additive word `DI` at `+0x34`, but the live sequencer lane `000d:21ed -> 000d:22bc` never rereads either field: after the create call it only touches the copied blob at `+0x102`, the seeded byte lane at `+0xd6/+0xd8`, and the caller stream at `+0xcc/+0xce`. The persistent uses of `+0x34` are instead the object save/load path: `000d:49e9..4a27` serializes `+0x10c` then `+0x34`, and `000d:4c2d..4c4d` reloads `(+0x32,+0x34)` through `entity_vm_slot_load_value_plus_offset` before storing the returned pair at `+0x10c/+0x10e`. The safest current read is therefore `persisted source offset feeding a later slot-value reload`, not `direct clause selector consumed by the matrix stage`, which weakens the `NPCTRIG slot 0x0a` alignment unless the derived reload value itself can still be tied back to that ladder.
|
||||||
|
|
||||||
### FUN_000d_ebe3 opcode-to-payload-shape matrix (sequencer-local)
|
### entity_vm_opcode_sequence_run opcode-to-payload-shape matrix (sequencer-local)
|
||||||
|
|
||||||
| Sequencer stage | Code anchors | Opcode / lane status | Payload shape class | Verified behavior |
|
| Sequencer stage | Code anchors | Opcode / lane status | Payload shape class | Verified behavior |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
|
|
@ -327,8 +332,9 @@ Conservative interpretation after this pass:
|
||||||
What is now hard evidence in code:
|
What is now hard evidence in code:
|
||||||
|
|
||||||
- `000d:0988` compares one opcode-local word at `[BP-0x32]` against concrete values `0x19`, `0x1a`, and `0x1b` (`000d:099b`, `000d:09a1`, `000d:0a07`, `000d:0a0d`).
|
- `000d:0988` compares one opcode-local word at `[BP-0x32]` against concrete values `0x19`, `0x1a`, and `0x1b` (`000d:099b`, `000d:09a1`, `000d:0a07`, `000d:0a0d`).
|
||||||
- `FUN_000d_ebe3` calls `000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104` (`000d:ebf5`, `000d:ec09`, `000d:ec1d`, `000d:ec31`, `000d:ec48`, `000d:ec54`).
|
- `entity_vm_opcode_sequence_run` (`000d:ebe3`) calls `000d:177c -> 000d:1acb -> 000d:0988 -> 000d:22bc -> optional 000d:1d4a -> 000d:2104` (`000d:ebf5`, `000d:ec09`, `000d:ec1d`, `000d:ec31`, `000d:ec48`, `000d:ec54`).
|
||||||
- `000d:177c`, `000d:1acb`, and `000d:2104` do not contain their own opcode compares in recovered body ranges; they behave as wrapper stages around the opcode-local family tested in `000d:0988`.
|
- `000d:177c`, `000d:1acb`, and `000d:2104` do not contain their own opcode compares in recovered body ranges; they behave as wrapper stages around the opcode-local family tested in `000d:0988`.
|
||||||
|
- The entry/exit contract is one step tighter too. `000d:ebe9` seeds the first stage from object field `+0xfe`, while the success tail at `000d:ec62..ec79` runs `tracked_entity_handle_mark_remove_all_if_enabled` and then gates `FUN_0008_ebe7` on object field `+0xc0` plus byte `+0x4b`. So the sequencer is not just an isolated opcode cluster; it also participates in outer runtime cleanup and follow-up dispatch state.
|
||||||
|
|
||||||
Conservative case identity mapping from this pass:
|
Conservative case identity mapping from this pass:
|
||||||
|
|
||||||
|
|
@ -339,9 +345,9 @@ Conservative case identity mapping from this pass:
|
||||||
|
|
||||||
Still unresolved after this pass:
|
Still unresolved after this pass:
|
||||||
|
|
||||||
- The animation constructor near calls at `000e:283e`, `000e:2931`, and `000e:29e4` land on a separate mis-split `000e:ebe3` region, not on `FUN_000d_ebe3`. They therefore no longer count as direct xref evidence for the `000d` dispatcher.
|
- The animation constructor near calls at `000e:283e`, `000e:2931`, and `000e:29e4` land on a separate mis-split `000e:ebe3` region, not on `entity_vm_opcode_sequence_run`. They therefore no longer count as direct xref evidence for the `000d` dispatcher.
|
||||||
- The true upstream selector/write path for `[BP-0x32]` in `FUN_000d_ebe3` is still unresolved, and no additional opcode id can yet be assigned uniquely beyond the internal `0x19/0x1a/0x1b` family already proven inside `000d:0988`.
|
- The true upstream selector/write path for `[BP-0x32]` in `entity_vm_opcode_sequence_run` is still unresolved, and no additional opcode id can yet be assigned uniquely beyond the internal `0x19/0x1a/0x1b` family already proven inside `000d:0988`.
|
||||||
- Repeated MCP-visible instruction and data-use searches still do not produce a real direct caller edge for `FUN_000d_ebe3`, `0005:2c35`, or `0005:2c68`. For now that makes the next defensible route `caller-frame / shared-consumer recovery`, not more recycled raw call searches or the retired `000a:44fd` and `000e:ebe3` hypotheses.
|
- Repeated MCP-visible instruction and data-use searches still do not produce a real direct caller edge for `entity_vm_opcode_sequence_run`, `0005:2c35`, or `0005:2c68`. For now that makes the next defensible route `caller-frame / shared-consumer recovery`, not more recycled raw call searches or the retired `000a:44fd` and `000e:ebe3` hypotheses.
|
||||||
|
|
||||||
### First readable VM IR sketch (verified-only)
|
### First readable VM IR sketch (verified-only)
|
||||||
|
|
||||||
|
|
@ -466,9 +472,52 @@ The next gameplay-side wrapper pass now extends well past the three earlier seed
|
||||||
- Direct callsites are now pinned for the simpler wrappers: `0005:0292 -> 0005:2c06`, `0005:0fee -> 0005:2cd2`, `0005:5946/59e9 -> 0005:2c9b`, and `0007:814e/822e -> 0005:2d01`.
|
- Direct callsites are now pinned for the simpler wrappers: `0005:0292 -> 0005:2c06`, `0005:0fee -> 0005:2cd2`, `0005:5946/59e9 -> 0005:2c9b`, and `0007:814e/822e -> 0005:2d01`.
|
||||||
- The two direct `0005:2d30` callers are now role-shaped as well: `0005:5370` reaches slot `0x0f` only after `entity_class_has_flag2000` succeeds and class-word bit `0x8000` is clear, while `0005:6f47` reaches the same gate from the complementary branch where class-word bit `0x2000` is still clear before the caller continues into its larger state/update flow.
|
- The two direct `0005:2d30` callers are now role-shaped as well: `0005:5370` reaches slot `0x0f` only after `entity_class_has_flag2000` succeeds and class-word bit `0x8000` is clear, while `0005:6f47` reaches the same gate from the complementary branch where class-word bit `0x2000` is still clear before the caller continues into its larger state/update flow.
|
||||||
- `0005:2c68` is no longer usable as indirect selector evidence. The `0007:e521` and `0007:e73c` instruction windows do push `0x2c68` immediately before `CALLF 000a:44fd`, but decompile now shows that value is the caller-local data pointer `DAT_0000_2c68` passed into a fatal-report helper, not an indirect call to wrapper `0005:2c68`.
|
- `0005:2c68` is no longer usable as indirect selector evidence. The `0007:e521` and `0007:e73c` instruction windows do push `0x2c68` immediately before `CALLF 000a:44fd`, but decompile now shows that value is the caller-local data pointer `DAT_0000_2c68` passed into a fatal-report helper, not an indirect call to wrapper `0005:2c68`.
|
||||||
- `0005:2c35` and `0005:2c68` therefore both remain unresolved in direct caller/xref evidence, and the real selector work stays centered on the still-xref-dark upstream edge into `FUN_000d_ebe3` rather than the disproven `000a:44fd` hypothesis.
|
- `0005:2c35` and `0005:2c68` therefore both remain unresolved in direct caller/xref evidence, and the real selector work stays centered on the still-xref-dark upstream edge into `entity_vm_opcode_sequence_run` rather than the disproven `000a:44fd` hypothesis.
|
||||||
- Net effect: the active-event ecosystem fit is reinforced by direct caller behavior and payload shapes, but final slot-to-descriptor ownership still requires real caller-role recovery for the remaining xref-dark entry points.
|
- Net effect: the active-event ecosystem fit is reinforced by direct caller behavior and payload shapes, but final slot-to-descriptor ownership still requires real caller-role recovery for the remaining xref-dark entry points.
|
||||||
|
|
||||||
|
#### Current batch: masked-context hub and sequencer-internal consumer recovery
|
||||||
|
|
||||||
|
- The generic masked VM-context hub is now instruction-verified at `000d:463a`. That body maps the incoming entity through `entity_vm_slot_index_from_entity`, rejects the path when runtime global `0x6610` is active or the owner/resource table at `0x6611 + 0x1315/+0x1317` is absent, tests the per-slot `0x0d`-stride owner mask pair against the caller-supplied high/low mask words, and only then falls into `entity_vm_context_create_from_slot_index` (`000d:46ec`).
|
||||||
|
- `search_instructions` on `000d:463a` now confirms this hub is not isolated to the `0005` wrapper island. In addition to the known seg021 wrappers, live direct callers now include `0004:f047` (mask `0x8000:0x0007`), `0004:f076` (mask `0x2000:0x0015`), and larger callers at `0006:0bbc` / `0006:10e7`. That is new caller-side evidence for the wider owner-slot taxonomy even though the offset-specialized wrappers `0005:2c35` and `0005:2c68` themselves still have no direct caller edges.
|
||||||
|
- The xref-dark offset wrappers are now tighter structurally too. Disassembly of `0005:2c35` and `0005:2c68` confirms they do nothing beyond sign-extending one extra word, passing mask pairs `0x0400:0x000a` and `0x0800:0x000b`, forwarding the entity pointer to `000d:463a`, and returning the out-word on success. That keeps their best current reading at `offset-specialized masked context creation`, not a separate selector lane.
|
||||||
|
- The offset word is now behaviorally tighter too. `entity_vm_slot_load_value_plus_offset` (`000d:5572`) is a straight `entity_vm_slot_load_value(...) + offset` wrapper, so the extra word passed by `0005:2c35` is not a second mask or opaque cookie; it is an additive selector/value adjustment that can plausibly choose one of the evenly spaced slot-`0x0a` clause starts once a real caller is recovered.
|
||||||
|
- The next caller-path pass tightens why `0005:2c35` stays dark. MCP xrefs now show only three entries into `entity_vm_context_create_from_slot_index` (`000d:46ac` from the generic masked hub, plus direct internal sequencer islands `000d:208b` and `000d:21ed`), while `0005:2c35` itself still has no recovered code or data xrefs. Stack setup at `000d:208b` hardcodes the `000d:5572` additive slot-load parameter to `0`, which does not match the `NPCTRIG` slot-`0x0a` clause starts (`0x0064/0x0093/0x00c2/0x00f1/0x0120`) or backward targets (`0x00db/0x00ac/0x007d/0x004e/0x001f`). The remaining live selector frontier is therefore the still-overlapped `000d:21ed` caller frame, not a normal visible caller of `0005:2c35`.
|
||||||
|
- The sequencer lane also gained two concrete internal consumer shapes. `000d:208b` is now the instruction-verified `create one slot-backed context and materialize or forward its result` path: it builds a `0x6714` context from the caller stream state, writes immediate-flagged results straight to the out pointer, and otherwise forwards the created object through `entity_vm_opcode_finish`. `000d:21ed` is the matching `prepend inline payload and build entity-link matrix` path: it creates a context, prepends caller-owned bytes into `+0x102`, consumes the seeded `+0xd6/+0xd8` bytes as shape/count metadata, and builds repeated `entity_link` closures from the following streamed ids before the same finish path.
|
||||||
|
- A new downstream-use pass narrows the extra-word role further. The stored offset field at context `+0x34` is now confirmed as durable object state rather than an immediate sequencer input: `000d:21ed -> 000d:22bc` does not reread it at all, `000d:498f`/`000d:4a78` serialize and reload it, and `000d:4c2d..4c4d` recomputes a slot-backed value from `(+0x32,+0x34)` into `+0x10c/+0x10e`. That shifts the remaining immortality question one step downstream: if `NPCTRIG slot 0x0a` still fits this runtime lane, it is more likely through the value reloaded from the slot-plus-offset pair than through `+0x34` as a direct clause selector.
|
||||||
|
- The hidden pre-call span in the `000d:21ed` lane is now recovered from direct program-memory bytes as well. Window `000d:2131..21ed` reads the seeded `+0xd6/+0xd8` stream as three successive words followed by two signed bytes: word0 becomes the slot index pushed at `000d:21d4`, word1 and word2 are added at `000d:21d0` before being pushed as the dynamic additive arg at `000d:21d3`, byte3 is forwarded as the setup-data length byte, and byte4 becomes the inline-blob length used for the later prepend copy. That makes the source classification explicit: context `+0x34` is not loaded from the owner table or from the caller object at `+0xd4`; it is a computed sum of two consecutive words inside the seeded stream itself.
|
||||||
|
- The same recovered window also tightens the upstream source layout feeding `entity_vm_context_setup`. The current caller frame base is `caller + [caller+0xd4]`, where `+0xd4` matches the saved frame offset written by `entity_vm_stack_push_frame` (`000c:f7c7`) rather than a descriptor-local field. From that frame base, `000d:21db..21e0` pushes `[frame+0x0a/+0x0c]` as a far pointer passed into `entity_vm_context_setup`, and `000d:21bd..21c8` separately derives `[frame+0x0e]` as the inline payload tail copied after context creation. So this consumer is now better modeled as one generic VM frame-record shape with two payload sources: a frame-stored far pointer plus byte-sized setup length for the initial `+0xcc` stack seed, followed by an adjacent inline tail blob with its own byte-sized length.
|
||||||
|
- The next frame-producer pass recovers the closest non-overlapped writer feeding that lane too. Raw bytes at `000c:fbf7..fc47` (`caseD_0`) show a generic frame-record producer reading one signed placement byte from the same seeded `+0xd6/+0xd8` stream, popping a far-pointer dword from the caller stream at `[caller+0xcc/+0xce]`, computing `frame_base = caller + [caller+0xd4]`, and storing the dword at `[frame_base + placement + 0x4/+0x6]`. That means the immediate source far pointer consumed later by `000d:21ed` is already stream-backed rather than owner-row-backed; if the `000d:21ed` record uses this exact producer family for its `[frame+0x0a/+0x0c]` lane, the relevant placement byte is `0x0006`, which is the only value that lands the written dword at `+0x0a/+0x0c` and leaves the inline tail starting at `+0x0e`.
|
||||||
|
- That stronger runtime shape weakens any claim that `000d:21ed` is already reading a descriptor-family-specific record. `NPCTRIG` slot `0x0a` still remains the best surviving descriptor-side candidate because its five-clause ladder is the only compact body that fits the row-count frontier, but the code evidence now shows the immediate input to `000d:21ed` is a generic frame-local record containing a source far pointer, a seeded slot/additive pair, and an inline tail. The remaining descriptor-side question is therefore one level earlier again: where the caller frame receives its `[frame+0x0a/+0x0c]` far pointer and whether the summed `add_a + add_b` still corresponds to a clause-base/delta pair inside `NPCTRIG` slot `0x0a` rather than to a more generic descriptor-relative offset.
|
||||||
|
- That changes the `NPCTRIG` cross-check in one important way. `NPCTRIG` slot `0x0a` remains the strongest surviving descriptor-side hypothesis only as an upstream source for a predecoded caller-stream record, because the recovered writer consumes a caller-stream dword plus a seeded placement byte instead of indexing owner rows or descriptor tables directly. `NPCTRIG` slot `0x20` still reads as the typed/setup companion body, but neither slot is now a good fit for the immediate write into `[frame+0x0a/+0x0c]` itself.
|
||||||
|
- One more layer of the producer path is now instruction-verified too. The setup call at `000d:4788 -> 000c:f844 -> 000c:f6e8` does not seed the new context's `+0xcc/+0xce` caller stream directly from the owner table row. Instead `entity_vm_context_setup` first allocates or reuses the object-local stream buffer at `context+0x36+0xcc`, then copies a caller-supplied setup blob from the parent frame using the far pointer/length arguments passed through `000d:46ec`. The slot/additive record returned by `entity_vm_slot_load_value_plus_offset` becomes the separate seeded `+0xd6/+0xd8` stream, while the owner-table row at `(+0x10/+0x12) + 0x0d*slot + 4` is mirrored to `0x39ca[slot]` and preserved separately in the context state.
|
||||||
|
- The closest sibling template to `caseD_0` also sharpens the placement-byte reading. `000c:ff9f..000d:000d` reads one signed placement byte and one length byte from the same seeded `+0xd6/+0xd8` stream, then copies `len` bytes from `[frame_base + placement + 0x4]` back onto the caller stream. Together with the recovered `000d:21ed` consumer layout (`[frame+0x0a/+0x0c]` far ptr, `[frame+0x0e..]` inline tail), that makes the strongest current fit a fixed two-slot family for this record shape: `caseD_0` uses placement `0x0006` for the far-pointer dword, and the sibling blob-copier uses placement `0x000a` for the inline tail starting at `frame+0x0e`.
|
||||||
|
- The producer side of that same record family is now tighter too. Linear raw-byte recovery across `000c:f98b..000d:000d` shows `000c:fc4b..fcbb` as the forward blob producer matching the reverse `000c:ff9f..000d:000d` case: it reads placement and length from the seeded `+0xd6/+0xd8` lane, computes `frame_base = caller + [caller+0xd4]`, and copies `len` bytes from the caller stream at `[caller+0xcc/+0xce]` into `[frame_base + placement + 0x4]`. For the `000d:21ed` record shape, that makes placement `0x000a` the best fit for the inline tail now consumed from `[frame+0x0e..]`.
|
||||||
|
- The dword lane now has a matching reverse case as well. Raw bytes at `000c:ff1f..ff83` show the same recursive family in the opposite direction: it reads one signed placement byte from the seeded `+0xd6/+0xd8` lane, computes `frame_base = caller + [caller+0xd4]`, loads a dword from `[frame_base + placement + 0x4/+0x6]`, subtracts `4` from `[caller+0xcc]`, and writes that dword back onto the caller stream. In other words, the immediate upstream producer for the `000c:fbf7..fc47` far-pointer write can already be another frame-record copier, not a direct owner-row or descriptor-table lookup.
|
||||||
|
- That narrows the remaining source classification again. The setup far pointer consumed by `000d:21ed` is now best modeled as a recursively propagated pointer into another VM-side byte buffer or predecoded descriptor workspace, not as the owner/resource row source mirrored separately through `0x39ca`. The owner row still matters for slot-backed state reloads, but the `entity_vm_context_setup` blob pointer itself is traveling through the frame-record family independently of that owner-row mirror.
|
||||||
|
- That also weakens the full-tuple `NPCTRIG` fit one more notch without killing it. The surviving tuple is now better read as `(slot, add_a, add_b, setup_len, inline_len, placement=0x0006/0x000a)` feeding a generic recursive frame-record contract. `NPCTRIG` slot `0x0a` remains the strongest descriptor-side candidate only as an earlier decoder that could have produced this predecoded record family, while slot `0x20` still reads as the typed/setup companion body. No recovered instruction in the immediate `000c:f98b..000d:000d` family yet ties the setup far pointer directly back to either slot.
|
||||||
|
- Net effect on source classification: the `000d:21ed`-relevant frame record is still not best modeled as generic VM scratch. Its immediate setup bytes are recursively copied from a parent frame record, and the wider context-build path is still anchored in descriptor-derived VM state (`+0xd6/+0xd8` from `entity_vm_slot_load_value_plus_offset`, owner-row source mirrored via `0x39ca`). What remains open is not whether this lane is scratch-backed, but which earlier decoder materializes the parent-frame far pointer before `000c:fbf7` consumes the next dword.
|
||||||
|
- After the new reverse-case recovery, that blocker can be stated more tightly: the missing piece is no longer a generic parent-frame materializer somewhere above `000c:fbf7`, but the first non-recursive decoder that originates the far pointer before the `ff1f/ff9f -> fbf7/fc4b -> 000d:21ed` propagation chain repeats it.
|
||||||
|
- The next pass closes that specific source-classification gap inside the same hidden interpreter body. Raw bytes at `000c:fa2f..fa5b` recover an inner opcode dispatcher that reads one opcode byte from the seeded `+0xd6/+0xd8` lane, bounds-checks it against `0x79`, and jumps through `CS:[0x3d9f + opcode * 2]`. That matters because the same local case family now exposes both the recursive frame-record replay stages and a separate set of direct caller-stream seed cases.
|
||||||
|
- Those non-recursive seed cases are now concrete. `000c:fd51` writes one inline byte from the `+0xd6/+0xd8` control stream onto the caller stream after decrementing `[caller+0xcc]` by `1`, `000c:fd91` and `000c:fdd1` do the same for inline words, and `000c:fe11..fe59` does it for an inline dword. In the dword case the interpreter advances through four literal bytes in the control stream, subtracts `4` from `[caller+0xcc]`, and writes the literal dword directly onto the caller stream before any frame replay logic runs.
|
||||||
|
- That makes `000c:fe11` the strongest current first non-recursive origin for the far-pointer lane later consumed by `000c:fbf7..fc47` and then by `000d:21ed`. The immediate setup far pointer is therefore no longer best modeled as coming from the owner/resource row, the mirrored `0x39ca` lane, or a generic VM scratch buffer. Its immediate compiled-side source is an inline dword literal embedded in the interpreter/control stream itself; `000c:ff1f..ff83` and `000c:fbf7..fc47` are replay stages layered on top of that literal-seeding path.
|
||||||
|
- That retunes the `NPCTRIG` cross-check again without killing it. `NPCTRIG` slot `0x0a` still remains the best upstream descriptor-side candidate because it is still the only compact active-event body that fits the surviving slot/additive shape, and slot `0x20` still reads as the typed/setup companion. But any direct immortality mapping now has to explain how the upstream decoder turns that descriptor family into a literal-bearing VM control stream before `000c:fe11`, not how `000d:21ed` or `000c:fbf7` index descriptor rows directly.
|
||||||
|
- One more pass tightens the creator/consumer split enough to rule out the owner row as the immediate control-stream builder. Direct instruction recovery at `000d:46ec` shows `entity_vm_context_create_from_slot_index` using the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` only for the separate `0x39ca[slot]` mirror, while the live `+0xd6/+0xd8` lane passed into `entity_vm_context_setup` still comes from `entity_vm_slot_load_value_plus_offset`. In the recovered `000d:21ed` pre-call span, that seeded lane is consumed as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, with `add_a + add_b` forwarded as the dynamic word stored at context `+0x34`.
|
||||||
|
- The same pass also clarifies the setup-payload contract that feeds the later link-matrix stage. `000d:21ed` passes `[frame+0x0a/+0x0c]` as the setup far pointer into `entity_vm_context_setup`, copies `[frame+0x0e..]` as a separate inline tail, and then `000d:22bc` consumes two signed metadata bytes plus a streamed word matrix to drive repeated `entity_link` calls. The immediate source is therefore `decoded per-slot VM stream + frame replay`, not `owner-row lookup + direct descriptor row`.
|
||||||
|
- That changes the opcode-family reading around `000c:fa2f` in a useful way even though the exact opcode indices remain unresolved in the current overlapped table view. The hidden dispatcher now has a verified immediate-literal family: `000c:fd51` pushes one inline byte to the caller stream, `000c:fd91` pushes a sign-extended byte as a word, `000c:fdd1` pushes an inline word, and `000c:fe11` pushes an inline dword. Together with the recursive replay cases `000c:ff1f` and `000c:ff9f`, that is enough to classify the upstream builder as a generic literal-bearing interpreter/control stream rather than a direct `NPCTRIG` clause reader.
|
||||||
|
- The descriptor-side fit therefore weakens from `specific direct NPCTRIG selector` to `broader descriptor-derived VM workspace` while staying narrow enough to keep `NPCTRIG` slot `0x0a` alive as the best upstream candidate. Slot `0x0a` still matches the event-bearing compact body and its five-clause ladder remains the only surviving compact source family with a plausible row-count/additive shape, but slot `0x20` still looks like the typed/setup companion and neither slot is now a good fit for the immediate control-stream seeding logic itself.
|
||||||
|
- The slot-load miss path now closes the workspace-materialization side of that question. Inside `entity_vm_slot_load_value` (`000d:51fd`), a cache miss triggers `000d:5066`, which first reads a slot header and then a `count * 6 + 0xc0` subentry table through the owner-resource wrapper `000d:714c`. When one subentry is still unloaded, `000d:5305..53d4` allocates a value object through `000d:3800`, then calls `000d:714c` again with the subentry source range and the new object's buffer at `+0x0a/+0x0c`; the function returns that same buffer pointer as the final `DX:AX` result. The immediate `+0xd6/+0xd8` workspace is therefore first materialized as a file-backed slot-value buffer during the slot-load miss path itself, not synthesized later from the owner-row mirror or from generic scratch state.
|
||||||
|
- The inline-tail source is not as tightly closed yet. The same hidden case family contains several immediate scalar caller-stream seed cases, so the `000d:21ed` tail at `[frame+0x0e..]` can now plausibly be assembled from control-stream literals or from another nearby non-recursive payload case rather than from a direct owner-row read. No instruction recovered in `000c:f98b..000d:000d` performs a matching direct descriptor-row lookup for that tail.
|
||||||
|
- Net effect from this pass: the missing outer selector into `entity_vm_opcode_sequence_run` is still unresolved, but the lane is no longer just one opaque dispatcher plus dark wrappers. It now has a verified generic masked-context creation hub, wider caller-family anchors for that hub, and two internally differentiated sequencer consumer blocks built directly on `entity_vm_context_create_from_slot_index`.
|
||||||
|
|
||||||
|
#### Follow-up: four newly surfaced direct `000d:463a` callers
|
||||||
|
|
||||||
|
- `0004:f033` (`0x8000:0x0007`) now reads as a generic gameplay-side materialization lane rather than a state-transition helper. When the local seg021 class-nibble query returns `8`, the wrapper bypasses the VM path and returns object word `+0x02` directly from the locally produced object. Otherwise it forwards through `entity_vm_context_try_create_masked_for_entity` and returns the created object's word `+0x02` on success.
|
||||||
|
- `0004:f05c` (`0x2000:0x0015`) stays on the gameplay-state side too, but with a stronger caller role. The only current direct caller window at `0004:f2b3` reaches it after overlap/proximity tests and entity byte `+0x32` toggling, so the safest reading is still `stateful gameplay materialization lane`, not `descriptor selector`.
|
||||||
|
- `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) adds the first strong non-`0005` extra-payload lane. It passes mask `0x0008:0x0030` plus one caller word into `000d:463a`; on failure it drops into `0006:0cfa`, which copies class-detail word `+0x02` to `+0x04`, derives a replacement selector from class-detail words `+0x06/+0x08/+0x0a` or the caller value, may clear flag `0x08` through `entity_class_clear_flag8_and_dispatch`, and then continues into the local state-transition/dispatch table. That is concrete evidence that at least one extra-word masked lane is feeding class-state transition materialization rather than a free-standing VM selector root.
|
||||||
|
- `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) provides the second strong extra-payload lane. It passes mask `0x0010:0x0008` plus one caller word into `000d:463a`, but only after local readiness gates through `0006:ffed` plus the seg021 availability/flag8-clear path. Unlike the earlier looser reading, the helper itself does not fall back to `0006:13b0` or `0006:13e4`; on miss it simply returns `0`. That makes the function a guarded masked-materialization attempt, while the neighboring `0006:13b0/13e4 -> 0006:07c0` class-linked lookups remain adjacent family evidence rather than a direct local fallback inside `0006:108c`.
|
||||||
|
- Taken together, the new seg004 and seg006 callers strengthen the existing read of the still-dark wrappers `0005:2c35` (`0x0400:0x000a`) and `0005:2c68` (`0x0800:0x000b`). Those wrappers still have no direct caller evidence, but they now sit inside a larger verified subfamily of `extra-word masked materializers` whose known members feed state selectors, class-linked values, or other gameplay-side payload resolution instead of acting as the real upstream selector into `entity_vm_opcode_sequence_run`.
|
||||||
|
- MCP-native function xrefs now reinforce that stopping point rather than changing it: `entity_vm_context_try_create_masked_for_entity` reports the expected direct callers through `0004:f047`, `0004:f076`, the named `0005` wrapper island, and the two seg006 callsites `0006:0bbc` / `0006:10e7`, while `entity_vm_opcode_sequence_run` plus the dark `0x0400/0x000a` and `0x0800/0x000b` wrappers still surface no direct function-xref callers in the current database. The best next path therefore remains caller-frame recovery or nearby unnamed-function repair, not another generic masked-hub sweep.
|
||||||
|
|
||||||
| `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params |
|
| `000c:f844` | `entity_vm_context_setup` | Calls `entity_vm_stack_init_with_data`, then sets `+0xd6..+0xe3` with position/dimension/state params |
|
||||||
| `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full |
|
| `000c:f600` | `entity_vm_pair_stack_push` | Push (word_a, word_b) onto 31-entry array at `[ptr+0x80]` (count); error if full |
|
||||||
| `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty |
|
| `000c:f63c` | `entity_vm_pair_stack_pop` | Pop and return word from pair stack; error if empty |
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,9 @@ Current verified caller-side detail:
|
||||||
- The seg127 fade-controller ownership is also one step tighter in the same lane. `transition_preentry_setup_resources` resets `0x630a` at `000c:c855`, `transition_preentry_step_script` now has a verified early gate at `000c:ca25` that yields to the fade controller whenever `0x630a` is active, and `transition_palette_fade_begin` at `000c:cdca` explicitly installs palette source/range/step state into `0x630e..0x6316`, asserts `0x630a`, and kicks one immediate fade tick.
|
- The seg127 fade-controller ownership is also one step tighter in the same lane. `transition_preentry_setup_resources` resets `0x630a` at `000c:c855`, `transition_preentry_step_script` now has a verified early gate at `000c:ca25` that yields to the fade controller whenever `0x630a` is active, and `transition_palette_fade_begin` at `000c:cdca` explicitly installs palette source/range/step state into `0x630e..0x6316`, asserts `0x630a`, and kicks one immediate fade tick.
|
||||||
- Fade direction is now pinned to seg126 script-control bytes rather than the outer seg005 wrappers. Inside `transition_preentry_step_script`, control byte `0x5e` reaches `palette_fade_begin_full_down` at `000c:cb06`, while control byte `0x26` reaches `palette_fade_begin_full_up` at `000c:cd1a`; control byte `0x2a` shares the same post-fade bookkeeping path after the full-up call.
|
- Fade direction is now pinned to seg126 script-control bytes rather than the outer seg005 wrappers. Inside `transition_preentry_step_script`, control byte `0x5e` reaches `palette_fade_begin_full_down` at `000c:cb06`, while control byte `0x26` reaches `palette_fade_begin_full_up` at `000c:cd1a`; control byte `0x2a` shares the same post-fade bookkeeping path after the full-up call.
|
||||||
- The upstream producer path for the remaining seg126 control bytes is now tighter too. `transition_preentry_setup_resources` composes one path from the mutable base at `0x6aa:0x6ac` plus local name buffers (`0x631c`, `0x6335`) through the seg072 slash-aware path helper `0009:3600`, opens that file through `file_handle_alloc_init_and_open`, allocates a buffer of the returned size, reads the full payload into `0x6301:0x6303`, and seeds `0x62fa/0x62fc/0x62ff/0x6305/0x630a/0x6318` before the loop starts. Current best reading is therefore `file-backed transition script/control buffer`, not locally synthesized opcodes.
|
- The upstream producer path for the remaining seg126 control bytes is now tighter too. `transition_preentry_setup_resources` composes one path from the mutable base at `0x6aa:0x6ac` plus local name buffers (`0x631c`, `0x6335`) through the seg072 slash-aware path helper `0009:3600`, opens that file through `file_handle_alloc_init_and_open`, allocates a buffer of the returned size, reads the full payload into `0x6301:0x6303`, and seeds `0x62fa/0x62fc/0x62ff/0x6305/0x630a/0x6318` before the loop starts. Current best reading is therefore `file-backed transition script/control buffer`, not locally synthesized opcodes.
|
||||||
|
- The adjacent seg126 selector lane is now classified tightly enough for conservative renames. `transition_file_family_select_and_refresh` (`000c:afa5`) keys object field `+0x49` through values `0`, `1`, and `4`, composes three sibling filenames from the inherited base `0x6aa:0x6ac` plus shared stem `0x621c` with suffix buffers `0x6223`, `0x622d`, and `0x6237`, loads the chosen file into object `+0x520`, and then runs the same redraw/palette/input refresh path. The same helper uses `field49==2` as a direct `vtable[0x3c]` callback branch and `field49==-1` as a normalize-back-to-zero state.
|
||||||
|
- The local wrappers around that selector now sharpen the caller model without forcing a stronger UI label. `transition_file_family_advance_on_anim_tick` (`000c:b153`) increments `+0x49` when the polled byte at `[param_2+0x14+0xa]` is clear and then re-enters the selector, while `transition_file_family_input_key_handler` (`000c:b199`) maps Left/Right and `n/N` into previous/next selector steps, uses `e/E` plus repeated `-` to force selector state `4`, and otherwise exits through `vtable[0x3c]`.
|
||||||
|
- This closes the narrow `+0x49` question as a local three-way file-family selector lane, but it still does not justify a stronger UI label for the paired `0x8c5c/0x8c60` renderer presets or the sibling seg127 fade inputs.
|
||||||
- The remaining `transition_preentry_step_script` opcodes now have stable local mechanics even though the higher-level text semantics are still open. Control byte `0x21` consumes the next script word into `SI` and advances `0x62ff` by two, which makes it the current baseline/start-position loader for later text draws. Control byte `0x40` renders one null-terminated entry from the same script buffer through renderer object `0x8c5c:0x8c5e`, while control byte `0x24` mirrors that behavior through `0x8c60:0x8c62`; both paths measure width through the renderer vtable, draw through seg088 `000a:30d7`, blit through seg080 `0009:943a`, advance `SI` by rendered width plus four, and then scan forward to the next opcode byte. Control byte `0x23` sets local completion byte `0x62fe = 1` and returns, so the outer shell exits on the next loop test instead of iterating further.
|
- The remaining `transition_preentry_step_script` opcodes now have stable local mechanics even though the higher-level text semantics are still open. Control byte `0x21` consumes the next script word into `SI` and advances `0x62ff` by two, which makes it the current baseline/start-position loader for later text draws. Control byte `0x40` renders one null-terminated entry from the same script buffer through renderer object `0x8c5c:0x8c5e`, while control byte `0x24` mirrors that behavior through `0x8c60:0x8c62`; both paths measure width through the renderer vtable, draw through seg088 `000a:30d7`, blit through seg080 `0009:943a`, advance `SI` by rendered width plus four, and then scan forward to the next opcode byte. Control byte `0x23` sets local completion byte `0x62fe = 1` and returns, so the outer shell exits on the next loop test instead of iterating further.
|
||||||
- Secondary renderer-factory sampling keeps the `0x8c5c` / `0x8c60` split conservative. Other sampled `000a:9748` xrefs use different adjacent preset pairs such as `0x0d/0x0c` at `0007:df30/df3f` and `0x0c/0x0f` at `0008:47c9/4851`, while no sampled caller reproduced the exact `0x10/0x11` startup pair outside `transition_preentry_setup_resources`. That supports keeping these as paired preset text renderers without forcing a title/body or normal/highlight label.
|
- Secondary renderer-factory sampling keeps the `0x8c5c` / `0x8c60` split conservative. Other sampled `000a:9748` xrefs use different adjacent preset pairs such as `0x0d/0x0c` at `0007:df30/df3f` and `0x0c/0x0f` at `0008:47c9/4851`, while no sampled caller reproduced the exact `0x10/0x11` startup pair outside `transition_preentry_setup_resources`. That supports keeping these as paired preset text renderers without forcing a title/body or normal/highlight label.
|
||||||
- The missing seg126 step body at `000c:ca1d` still cannot be split out safely because `create_function_by_address` collides with the existing oversized overlap namespace, so this pass preserved the recovery as a decompiler comment instead of forcing a destructive boundary repair. Current best reading is still that `000c:ca1d..cd34` is the real `transition_preentry_step_script` body and that `000c:cd35` starts the fade-tick helper.
|
- The missing seg126 step body at `000c:ca1d` still cannot be split out safely because `create_function_by_address` collides with the existing oversized overlap namespace, so this pass preserved the recovery as a decompiler comment instead of forcing a destructive boundary repair. Current best reading is still that `000c:ca1d..cd34` is the real `transition_preentry_step_script` body and that `000c:cd35` starts the fade-tick helper.
|
||||||
|
|
@ -317,6 +320,14 @@ Current best neutral conclusion from this pass: the shared `g_active_dispatch_en
|
||||||
- The in-scope `0x31a2` readers are now classed cleanly by role. `0004:c24d` and `000c:e4d8` are edge waits; `000c:ca11` is the seg126 modal-break exit; `000c:e546`, `000c:e5c6`, and `000d:c0ee` are cleanup-abort exits; `000d:9304` and `000d:b6b1` are deferred dispatch/state-advance gates.
|
- The in-scope `0x31a2` readers are now classed cleanly by role. `0004:c24d` and `000c:e4d8` are edge waits; `000c:ca11` is the seg126 modal-break exit; `000c:e546`, `000c:e5c6`, and `000d:c0ee` are cleanup-abort exits; `000d:9304` and `000d:b6b1` are deferred dispatch/state-advance gates.
|
||||||
- Two remaining `0x31a2` reads stay outside that presentation classification set. `0005:453d` is only a plain getter wrapper for the shared depth word, and `0008:5149` is a seg008 internal/accounting-side read that adds the current depth to another local count before tripping a `>= 0x10` capacity flag.
|
- Two remaining `0x31a2` reads stay outside that presentation classification set. `0005:453d` is only a plain getter wrapper for the shared depth word, and `0008:5149` is a seg008 internal/accounting-side read that adds the current depth to another local count before tripping a `>= 0x10` capacity flag.
|
||||||
|
|
||||||
|
### Current batch: renderer preset contract and seg127 fade-input closure
|
||||||
|
|
||||||
|
- `transition_preentry_setup_resources` is now exact on the paired renderer setup path. Instruction window `000c:c659..c6ab` shows that `FUN_000a_9748` is called only with preset ids `0x10` and `0x11`, storing the resulting temporary renderer objects at `0x8c5c:0x8c5e` and `0x8c60:0x8c62`, then immediately drawing the same seed text buffer `DS:0x631a` at `(0x0a,0x0a)` through both. This closes the structural question as `paired preset text lanes` inside one temporary transition presentation path, but still does not justify a stronger title/body or highlight/shadow label.
|
||||||
|
- The recovered `transition_preentry_step_script` body is also slightly tighter on the two text opcodes. `0x40` and `0x24` both measure their string through renderer vtable slot `+0x0c`, center it inside a `0x280`-wide lane, fetch rendered width through slot `+0x08`, draw through seg088 `000a:30d7`, blit through seg080 `0009:943a`, and advance `SI` by `rendered_width + 4`; only the selected preset lane differs (`0x8c5c` for `0x40`, `0x8c60` for `0x24`).
|
||||||
|
- The seg127 fade-controller inputs are now exact rather than only role-level. `transition_palette_fade_begin` stores palette source at `0x630e:0x6310`, start index at `0x6312`, count at `0x6314`, step at `0x6316`, brightness at `0x630d`, active flag at `0x630a`, and direction/state at `0x630b`, then immediately ticks the local fade controller. `transition_palette_fade_tick` dispatches `0x630b==1` to `transition_palette_fade_out_step` and `0x630b==2` to `transition_palette_fade_in_step`.
|
||||||
|
- The two default script-selected fade wrappers are now instruction-verified too. `palette_fade_begin_full_down` at `000c:c616` pushes direction `1`, step `4`, count `0x80`, start `0`, and palette buffer `DS:0x8c64`; `palette_fade_begin_full_up` at `000c:c600` is the same wrapper with direction `2`. Combined with the `0x5e`, `0x26`, and `0x2a` script-byte sites in `transition_preentry_step_script`, this closes the neighboring seg127 fade-input contract for the startup/display lane.
|
||||||
|
- The late presentation-handoff family is now direct-decompile confirmed rather than only caller-window inferred. `FUN_000d_938c` creates up to two temporary runtime-state palette entries (`kind 0x3c`, then `kind 0x14`), waits for them to clear, redraws, clears `g_active_dispatch_entry_farptr[+0x40]`, and only then dispatches caller vtable `+0x08`; `entity_cleanup_resources_and_dispatch` shows the same late shared-hold clear on the `entity +0x737` branch immediately before the shared `0x2bd8` controller dispatch. That is enough to treat the startup/display major section as materially complete, with only low-impact residual ambiguity around the exact UI label of preset pair `0x10/0x11` and the optional overlap hygiene at `000c:db68`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Follow-up: `0x4588` Object-Role Evidence
|
## Follow-up: `0x4588` Object-Role Evidence
|
||||||
|
|
@ -330,6 +341,10 @@ The `0x4588` FAR object is a runtime-installed callback/dispatch object that par
|
||||||
- **Teardown:** `000a:4a56` checks a once-flag at `0x4595`, clears `0x4588` when non-null, optionally performs a vtable `+0x0c` callback when `0x4590 != 0x458c`, then calls vtable slot `+0x04` followed by `FUN_0009_0d30()`.
|
- **Teardown:** `000a:4a56` checks a once-flag at `0x4595`, clears `0x4588` when non-null, optionally performs a vtable `+0x0c` callback when `0x4590 != 0x458c`, then calls vtable slot `+0x04` followed by `FUN_0009_0d30()`.
|
||||||
- **Callbacks:** `000a:b9e5`, `000a:ba66`, `000d:9d5e`, and `000d:a3b7` all push a two-word value pair followed by the `0x4588` FAR pointer and call vtable slot `+0x0c`. `entity_conditional_render_dispatch` calls the same vtable slot with a single literal `0x0101` argument.
|
- **Callbacks:** `000a:b9e5`, `000a:ba66`, `000d:9d5e`, and `000d:a3b7` all push a two-word value pair followed by the `0x4588` FAR pointer and call vtable slot `+0x0c`. `entity_conditional_render_dispatch` calls the same vtable slot with a single literal `0x0101` argument.
|
||||||
|
|
||||||
|
Current batch note:
|
||||||
|
|
||||||
|
- `runtime_callback_object_init_once`, `runtime_callback_object_teardown_once`, and `entity_conditional_render_dispatch` now line up even more strongly as a video or presentation-state callback lane rather than a generic allocator client. The object is installed only after BIOS video-state snapshot, teardown emits a final callback only when recorded mode/state changed, and one live caller uses the literal mode-like pair `0x0101` through the same vtable `+0x0c` slot. That is enough to keep pushing the role toward `presentation/video-state callback broker`, but still not enough for a fully behavioral subsystem rename.
|
||||||
|
|
||||||
### Payload pairs from payload sync callsites
|
### Payload pairs from payload sync callsites
|
||||||
|
|
||||||
- `000d:9d5e` → vtable `+0x0c` payload from object fields `+0x12d/+0x12f`
|
- `000d:9d5e` → vtable `+0x0c` payload from object fields `+0x12d/+0x12f`
|
||||||
|
|
@ -362,11 +377,14 @@ The next ScummVM-guided validation step now confirms that the sampled owner-load
|
||||||
|
|
||||||
### Header and event-table shape
|
### Header and event-table shape
|
||||||
|
|
||||||
|
- The loader-side count field is now tighter too. The first dword in the sampled owner-loaded class header is not the total slot count; `000d:5066` uses it as the extra-slot count beyond a fixed `0x20` base table, which is why the cached table allocation is `extra_count * 6 + 0xc0` and the refcount array is `extra_count * 2 + 0x40`.
|
||||||
|
- That reading matches the extracted class-family shapes exactly: `EVENT` keeps first dword `0x00000000`, `NPCTRIG` moves to `0x00000001`, and `ROLL_NS` to `0x00000002`, while the already-validated owner-loaded event counts remain `0x20`, `0x21`, and `0x23` respectively.
|
||||||
- The sampled class records do contain a stable 4-byte header field at bytes `8..11`.
|
- The sampled class records do contain a stable 4-byte header field at bytes `8..11`.
|
||||||
- The observed values are small boundaries: `0x00d4`, `0x00da`, and `0x00e6` in the current sample set.
|
- The observed values are small boundaries: `0x00d4`, `0x00da`, and `0x00e6` in the current sample set.
|
||||||
- Treating that dword directly as the first post-event-table offset makes the layout line up cleanly: `(dword_at_8 - 20) / 6` yields valid tables of 32, 33, or 35 slots before inline payload/name data begins.
|
- Treating that dword directly as the first post-event-table offset makes the layout line up cleanly: `(dword_at_8 - 20) / 6` yields valid tables of 32, 33, or 35 slots before inline payload/name data begins.
|
||||||
- The region at `class + 0x14` is therefore now directly confirmed as repeated 6-byte slots with `u16 unknown_word + u32 code_or_payload_field` layout.
|
- The region at `class + 0x14` is therefore now directly confirmed as repeated 6-byte slots with `u16 unknown_word + u32 code_or_payload_field` layout.
|
||||||
- Representative low-slot examples are `JELYHACK` slot `1` = `{word=0x002a, dword=0x00000001}`, `SURCAMNS` slot `1` = `{word=0x0051, dword=0x000000d2}`, `SURCAMEW` slot `1` = `{word=0x00f7, dword=0x000000d2}`, `EVENT` slot `10` = `{word=0x1fd6, dword=0x00000001}`, and `REE_BOOT` slots `10/15/16` = `{0x034b,1}`, `{0x025c,0x034c}`, `{0x003b,0x05a8}`.
|
- Representative low-slot examples are `JELYHACK` slot `1` = `{word=0x002a, dword=0x00000001}`, `SURCAMNS` slot `1` = `{word=0x0051, dword=0x000000d2}`, `SURCAMEW` slot `1` = `{word=0x00f7, dword=0x000000d2}`, `EVENT` slot `10` = `{word=0x1fd6, dword=0x00000001}`, and `REE_BOOT` slots `10/15/16` = `{0x034b,1}`, `{0x025c,0x034c}`, `{0x003b,0x05a8}`.
|
||||||
|
- The runtime-side selector arithmetic is now exact as well: the owner-resource callbacks operate on `class_id + 2`, which matches the extracted `object_index` column directly. `EVENT` therefore lands on child `0x363` from class id `0x361`, and `NPCTRIG` on child `0x365` from class id `0x363`.
|
||||||
- The leading event word is still not decoded semantically.
|
- The leading event word is still not decoded semantically.
|
||||||
|
|
||||||
### What remains open
|
### What remains open
|
||||||
|
|
@ -374,11 +392,50 @@ The next ScummVM-guided validation step now confirms that the sampled owner-load
|
||||||
- Scanning with the previously noted ScummVM-style `(base_offset + 19) / 6` interpretation overruns into inline payload/name bytes on these owner-loaded records, so the local sample set does not support that exact event-count formula as written.
|
- Scanning with the previously noted ScummVM-style `(base_offset + 19) / 6` interpretation overruns into inline payload/name bytes on these owner-loaded records, so the local sample set does not support that exact event-count formula as written.
|
||||||
- The best current arithmetic fit is now tighter: ScummVM's decremented `base_offset` is also used as the live code-stream base in `uc_machine.cpp`, so the local owner-loaded records fit best if bytes `8..11` are the first code-byte offset and event-count derivation is `(base_offset - 19) / 6`, which is exactly equivalent here to `(raw_u32_at_8_11 - 20) / 6`.
|
- The best current arithmetic fit is now tighter: ScummVM's decremented `base_offset` is also used as the live code-stream base in `uc_machine.cpp`, so the local owner-loaded records fit best if bytes `8..11` are the first code-byte offset and event-count derivation is `(base_offset - 19) / 6`, which is exactly equivalent here to `(raw_u32_at_8_11 - 20) / 6`.
|
||||||
- Current `000d` loader evidence does not point to a header rewrite before VM consumption. `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) only builds the external path and creates the runtime, `entity_vm_runtime_create` (`000d:4c99`) only installs the helper returned by `000d:7000`, `entity_vm_runtime_owner_resource_create` (`000d:7000`) only allocates the child owner table and fills it through helper vtable `+0x0c`, and `entity_vm_context_create_from_slot_index` (`000d:46ec`) directly reads slot-backed source data from that owner table. No local step is yet verified as rewriting the sampled class headers.
|
- Current `000d` loader evidence does not point to a header rewrite before VM consumption. `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) only builds the external path and creates the runtime, `entity_vm_runtime_create` (`000d:4c99`) only installs the helper returned by `000d:7000`, `entity_vm_runtime_owner_resource_create` (`000d:7000`) only allocates the child owner table and fills it through helper vtable `+0x0c`, and `entity_vm_context_create_from_slot_index` (`000d:46ec`) directly reads slot-backed source data from that owner table. No local step is yet verified as rewriting the sampled class headers.
|
||||||
|
- The slot-value miss path is now exact enough to align against the extractor rather than only against motifs. `entity_vm_slot_load_value` (`000d:51fd`) does not build the returned workspace out of owner-row fields or late interpreter scratch: on a miss it uses `000d:5066` plus the same owner-resource wrapper `000d:714c` to read a `0x14`-byte class header, then a cached `6 * (0x20 + extra_count)` subentry table, and finally the selected subentry's byte range straight into a newly allocated value-object buffer at `+0x0a/+0x0c`.
|
||||||
|
- The final body read at `000d:53b4` now matches the extracted row arithmetic exactly. The 6-byte row contributes `word body_len` plus `dword raw_code_offset`, the class header contributes `dword code_base`, and the reader fetches `body_len` bytes from `code_base + raw_code_offset - 1` through `code_base + raw_code_offset + body_len - 2`.
|
||||||
|
- That gives a direct owner-loaded fit for the two surviving `NPCTRIG` bodies. For class `NPCTRIG` (`class_id = 0x363`, `object_index = 0x365`), slot `0x0a` uses `{len = 0x0175, raw_code_offset = 0x00000001, code_base = 0x00da}` and therefore materializes range `0x00da..0x024e` (`373` bytes), while slot `0x20` uses `{len = 0x0159, raw_code_offset = 0x00000176, code_base = 0x00da}` and therefore materializes range `0x024f..0x03a7` (`345` bytes). `EVENT` slot `0x0a` fits the same runtime arithmetic with `{len = 0x1fd6, raw_code_offset = 0x00000001, code_base = 0x00d4}` -> `0x00d4..0x20a9`.
|
||||||
|
- Because `000d:5066/51fd/53b4` now line up with the extracted class headers and event rows byte-for-byte, the remaining immortality blocker is no longer header math or slot-number translation. The open step is upstream class selection into this now-verified loader path: whether the live slot `0x0a` request really names `NPCTRIG`, `EVENT`, or another descriptor family sharing the same owner-loaded format.
|
||||||
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) still does not expose a direct binary-side class-name lookup or explicit `classid + 2` arithmetic. What it does expose is an indexed file-set loader contract: helper-owned count at `+0x14`, far-pointer table at `+0x10`, paired per-entry word table at `+0x18`, vtable `+0x04` size query, and vtable `+0x0c` materialization of the `0x0d`-stride owner records later consumed by `entity_vm_context_create_from_slot_index`. The current pass also makes the helper shape slightly more concrete: the two raw seg070 windows at `0009:67b6` and `0009:6916` are twin per-entry path/read loops with distinct format strings (`DS:3f2d` and `DS:3f40`) but the same `+0x10/+0x18` indexing and file open/read/close lane, which is better evidence for a multi-table or multi-phase external loader than for direct in-memory descriptor iteration.
|
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) still does not expose a direct binary-side class-name lookup or explicit `classid + 2` arithmetic. What it does expose is an indexed file-set loader contract: helper-owned count at `+0x14`, far-pointer table at `+0x10`, paired per-entry word table at `+0x18`, vtable `+0x04` size query, and vtable `+0x0c` materialization of the `0x0d`-stride owner records later consumed by `entity_vm_context_create_from_slot_index`. The current pass also makes the helper shape slightly more concrete: the two raw seg070 windows at `0009:67b6` and `0009:6916` are twin per-entry path/read loops with distinct format strings (`DS:3f2d` and `DS:3f40`) but the same `+0x10/+0x18` indexing and file open/read/close lane, which is better evidence for a multi-table or multi-phase external loader than for direct in-memory descriptor iteration.
|
||||||
- The signed slot-offset lane used by the still-xref-dark wrappers `0005:2c35` / `0005:2c68` is also no longer confined to `entity_vm_context_create_from_slot_index` (`000d:46ec`). Inside `entity_vm_runtime_create`, the pre-entry body at `000d:4c25..4c90` reloads object fields `+0x32/+0x34` through `entity_vm_slot_load_value_plus_offset` (`000d:5572`), stores that returned pair into object fields `+0x10c/+0x10e`, and also caches the owner-source far pointer at `+0x117/+0x119`. The paired save path at `000d:49ec` then serializes `+0x10c` through seg070 `0009:2034`, which makes the slot-plus-offset pair a persisted runtime/dispatch state lane rather than a transient wrapper-only argument.
|
- The signed slot-offset lane used by the still-xref-dark wrappers `0005:2c35` / `0005:2c68` is also no longer confined to `entity_vm_context_create_from_slot_index` (`000d:46ec`). Ghidra now reflects that contract in the conservative wrapper names `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset`. Inside `entity_vm_runtime_create`, the pre-entry body at `000d:4c25..4c90` reloads object fields `+0x32/+0x34` through `entity_vm_slot_load_value_plus_offset` (`000d:5572`), stores the reconstructed `DX:AX` pair into object fields `+0x10c/+0x10e`, and also caches the owner-source far pointer at `+0x117/+0x119`. The paired save path at `000d:49ec` is narrower than it first looked: it serializes only the low word at `+0x10c` through seg070 `0009:2034`, while the high word is recomputed on load from the fresh `entity_vm_slot_load_value()` result plus the saved additive word.
|
||||||
|
- Current disassembly closes the exact low-slot wrapper contracts too. `0005:2c35` sign-extends caller word `[BP+0x0a]`, then calls `entity_vm_context_try_create_masked_for_entity` with slot `0x0a` and packed mask `0x00000400`; `0005:2c68` is the same signed-additive shim for slot `0x0b` and packed mask `0x00000800`. Neither wrapper has a recovered outward code/data xref yet, so the best current provenance remains `extra-word masked materializer family member`, not a gameplay event label.
|
||||||
|
- The newly recovered post-load consumers of `+0x10c/+0x10e` are weak and do not behave like a recovered event-dispatch selector. Predicate `FUN_0001_a772` returns true only when the pair is exactly `0000:0001`, while normalization block `FUN_0002_1860` checks `segment == 0` and clamps `offset < 0x0080` up to `0x0080`. No recovered downstream comparison or dispatch branch matches the five verified `NPCTRIG` slot `0x0a` clause starts (`0x0064/0x0093/0x00c2/0x00f1/0x0120`) or backward targets (`0x001f/0x004e/0x007d/0x00ac/0x00db`); if anything, the `0x0080` floor cuts across that family instead of confirming it.
|
||||||
|
- The masked-create hub in front of that lane is now explicit too. Window `000d:463a..46e8` maps one gameplay entity through `entity_vm_slot_index_from_entity`, tests the owner/resource table row mask at `0x6611 -> +0x1315/+0x1317 -> (+0x10/+0x12) + 0x0d*slot`, and only then calls `entity_vm_context_create_from_slot_index`. That matters because the offset-specialized wrappers `0005:2c35` / `0005:2c68` are now instruction-verified as nothing more than sign-extended extra-word shims over this generic masked-context hub, rather than separate selector logic.
|
||||||
|
- The upstream slot selector is now exact enough to rule out one remaining binary-side shortcut. `entity_vm_slot_index_from_entity` (`000d:45c5`) does not expose a class-family choice like `NPCTRIG` versus `EVENT`; it only chooses one of three generic category spans before the owner row is consulted: `(a)` entity ids `1..255` with class-word bit `0x0002` clear map to `entity_id + base_0x8c7e`, `(b)` class-nibble `4` objects map to `class_byte_0x7e05 + base_0x8c80`, and `(c)` everything else maps to `type_word_0x7df9 + base_0x8c7c`.
|
||||||
|
- The runtime init path now shows where those bases come from too. After `entity_vm_runtime_create` succeeds, `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) seeds `0x8c7c/0x8c7e/0x8c80/0x8c82` as cumulative category bases by looping over four word counts at `0x6608..0x660e`. Because the compiled side only sees those category-base spans and the later owner-row mask words, it still does not reveal a direct descriptor-class discriminator before the slot body is loaded.
|
||||||
|
- One direct non-hub consumer reinforces that read. `FUN_0005_295f` is the only currently recovered caller of `entity_vm_slot_index_from_entity` outside the masked hub; it recomputes the same slot index, directly tests owner-row bit `0x0040`, and then branches into gameplay handling before optionally calling `entity_vm_context_try_create_masked_for_entity` with mask `0x0040:0x0006`. Together with the still-empty xref results for `0005:2c35` and the stable `0005:2c35..2c57` function boundary, the safest current interpretation is that these owner-row words are generic capability masks, not explicit `NPCTRIG` / `EVENT` family tags.
|
||||||
|
- The next immortality pass separates that owner-row path from the live control-stream path even more sharply. Inside `entity_vm_context_create_from_slot_index` (`000d:46ec`), the owner-table row still feeds only the preserved `0x39ca[slot]` mirror, while the actual `+0xd6/+0xd8` control stream handed to `entity_vm_context_setup` comes from `entity_vm_slot_load_value_plus_offset` and the caller-supplied setup/tail pointers come from the current VM frame record. That makes the immediate builder for the `000d:21ed` lane `slot-backed decoded stream plus frame-local replay`, not `owner-row decode`.
|
||||||
|
- That is the current hard wall for the immortality frontier. The strongest verified answer remains that `NPCTRIG` slot `0x0a` is the best upstream descriptor-side fit and `EVENT` slot `0x0a` remains the generic-hub baseline, but the binary selector path now bottoms out at category spans plus row-capability bits rather than at a provable class-family discriminator.
|
||||||
|
- The open descriptor question therefore moves one step earlier again. Current `000d` loader/runtime evidence still supports a descriptor-derived upstream workspace, but not a direct owner-row-to-opcode path for the immortality trigger. The closest verified compiled-side seeding now happens later inside the hidden dispatcher at `000c:fa2f`, where immediate literal cases can push byte/word/dword payloads straight onto the caller stream before the frame replay family re-materializes them into the child frame.
|
||||||
|
- The seg070 twin-file-family helper is now tighter at the buffer/schema level as well. The paired loops at `0009:67b6` and `0009:6916` do not reuse one ambiguous scratch object: each loop performs its own size query/allocation sequence, builds paths from the same `+0x10/+0x18/+0x14` table trio with its own format string (`DS:3f2d` versus `DS:3f40`), feeds a dedicated temporary far buffer through the shared `file_handle_alloc_init_and_open` / `dos_file_seek` / `dos_file_close` trailer, and then frees that loop-local buffer before returning. Current safest read is therefore `two distinct temporary file-family materialization passes inside one owner-resource helper`, not one callback shard reused for both families.
|
||||||
- Additional `0x39ca` consumers are now classified more cleanly. Beyond the already-known static seeds at `000d:7299 -> DS:67f2` and `000d:761c -> DS:6872`, the constructor-like windows at `000d:929a` and `000d:963c` seed rows `DS:68ec` and `DS:68f5` respectively before enabling local timer/dispatch behavior. Those writes behave like dispatch-entry-local static seed rows, not owner-table mirrors. Separately, `FUN_000d_938c` reads temporary dispatch-entry fields `+0x32/+0x34` at `000d:9449..9468` and `000d:9547..9566` only as a wait/poll condition on the scratch-palette (`kind 0x3c`) and current-palette (`kind 0x14`) entries it creates, which further separates active dispatch-entry state from the owner-backed `0x39ca[slot] = {source_off, source_seg}` rows written by `000d:46ec`.
|
- Additional `0x39ca` consumers are now classified more cleanly. Beyond the already-known static seeds at `000d:7299 -> DS:67f2` and `000d:761c -> DS:6872`, the constructor-like windows at `000d:929a` and `000d:963c` seed rows `DS:68ec` and `DS:68f5` respectively before enabling local timer/dispatch behavior. Those writes behave like dispatch-entry-local static seed rows, not owner-table mirrors. Separately, `FUN_000d_938c` reads temporary dispatch-entry fields `+0x32/+0x34` at `000d:9449..9468` and `000d:9547..9566` only as a wait/poll condition on the scratch-palette (`kind 0x3c`) and current-palette (`kind 0x14`) entries it creates, which further separates active dispatch-entry state from the owner-backed `0x39ca[slot] = {source_off, source_seg}` rows written by `000d:46ec`.
|
||||||
- Safe event-label correlation remains intentionally narrow after this pass. The sampled low slot ids are now concrete, but none of them yet have a verified binary-side behavior match strong enough to promote a ScummVM label like `look`, `use`, or `cachein`.
|
- Safe event-label correlation remains intentionally narrow after this pass. The sampled low slot ids are now concrete, but none of them yet have a verified binary-side behavior match strong enough to promote a ScummVM label like `look`, `use`, or `cachein`.
|
||||||
|
|
||||||
|
### Current batch: higher-slot masked wrapper ladder (`0x10..0x14`)
|
||||||
|
|
||||||
|
- The gameplay-side masked-wrapper island now extends one verified step past the older `0x0f` frontier. Raw call setup around `0005:3115..322d` shows five higher-slot entries feeding `entity_vm_context_try_create_masked_for_entity` with slot ids `0x10`, `0x11`, `0x12`, `0x13`, and `0x14`.
|
||||||
|
- The slot `0x10` lane is not yet a clean standalone function object, but the containing body at `0005:3115..3129` is exact enough to classify its call shape: it pushes zero extra word, slot `0x10`, packed mask `0x00010000`, and the live entity pointer before the far call to `000d:463a`. The preceding guard at `0005:30f2..3113` restricts that path to one class-nibble-`4` lane.
|
||||||
|
- Four neighboring helpers are now renamed directly in Ghidra from stable function objects:
|
||||||
|
- `0005:313e` = `entity_vm_context_try_create_mask_00020000_slot11_with_offset`
|
||||||
|
- `0005:3171` = `entity_vm_context_try_create_mask_00040000_slot12`
|
||||||
|
- `0005:31da` = `entity_vm_context_try_create_mask_00080000_slot13_with_offset_if_valid_entity`
|
||||||
|
- `0005:31a0` = `entity_vm_context_try_create_mask_00100000_slot14_with_offset`
|
||||||
|
- Their payload shapes are now exact from disassembly, not only inferred from decompile:
|
||||||
|
- slot `0x11` pushes one caller-supplied extra word (`MOVZX EAX,[BP+0xa] ; PUSH EAX`)
|
||||||
|
- slot `0x12` pushes a fixed zero extra word
|
||||||
|
- slot `0x13` pushes one sign-extended caller word after the same `0005:2686` / `0005:ffed` entity-validity gate used by the older slot-`0x01` helper
|
||||||
|
- slot `0x14` pushes one caller-supplied extra word
|
||||||
|
- This widens the verified owner-slot taxonomy in a USECODE-relevant way: the binary is no longer only distinguishing compact low-slot wrappers like `0x0a`/`0x0b`; it also separates a higher-slot family with mixed `no extra word` versus `signed extra word` call contracts.
|
||||||
|
- The first outward callers in this higher-slot family are now explicit too. `entity_vm_context_try_create_mask_00040000_slot12` (`0005:3171`) is called at `0005:1776` and `0005:1945`; both callsites are currently trapped in non-function windows, but they are real direct edges into the slot-`0x12` zero-extra-word lane. By contrast, current MCP xrefs still show no direct outward callers for the slot `0x11`, `0x13`, or `0x14` wrappers and still none for the dark slot `0x0a` / `0x0b` pair.
|
||||||
|
- The persisted-context side of the same lane is now tighter at the field level. `entity_vm_context_save` (`000d:498f`) serializes `+0x11f`, `+0x121`, the derived low word at `+0x10c`, the additive word at `+0x34`, and the `0x80`-byte local buffer at `+0x36/+0x38`; `entity_vm_context_load` (`000d:4a78`) rebuilds the frame pointers, reloads the saved low word as the additive argument to `entity_vm_slot_load_value_plus_offset`, restores `+0x10c/+0x10e`, and refreshes the owner-linked source pair at `+0x117/+0x119`. That strengthens the current read that persistence preserves `(slot, additive_word, derived_low_word)` after selector choice, not the upstream class-family selector itself.
|
||||||
|
- The external event-name correlation can now be tightened slightly but still stays hint-level only:
|
||||||
|
- slot `0x12` having no extra word is compatible with the external `justMoved()` zero-argument event label
|
||||||
|
- slot `0x13` carrying one extra word is compatible with Pentagram's `AvatarStoleSomething(uword)` signature
|
||||||
|
- slot `0x11` carrying one extra word is compatible with Pentagram's placeholder `func11(sint16)` signature and with ScummVM's unresolved `cast`-side slot only at the broad `one scalar argument` level
|
||||||
|
- slot `0x14` currently does **not** fit Pentagram's older zero-argument `animGetHit()` signature, so that ordinal should remain slot-numbered on the binary side for now
|
||||||
|
- Operational consequence for the current VM lane: there is now stronger binary evidence that the masked-context family is organized around slot ordinals with distinct payload shapes, not only around one low-slot trigger subset. That helps the current round-trip IR because it justifies keeping higher ordinals as slot-stable records with payload-shape metadata even when their event labels remain external hints.
|
||||||
|
- The sequencer-side consumer model is also now preserved directly in Ghidra. Address `000d:22bc` carries a decompiler comment recording it as a sequencer-internal matrix stage: it reads two signed metadata bytes from `+0xd6/+0xd8`, consumes caller-stream words as entity/link ids, repeatedly calls `0008:7d27`, and only pushes back words without bit `0x0400` before jumping to `entity_vm_opcode_finish`.
|
||||||
|
|
||||||
### Conservative parser rule from this batch
|
### Conservative parser rule from this batch
|
||||||
|
|
||||||
- For current owner-loaded/raw EUSECODE work, keep bytes `8..11` raw and derive event count only with `(raw_u32_at_8_11 - 20) / 6` when divisibility and object-size bounds checks succeed.
|
- For current owner-loaded/raw EUSECODE work, keep bytes `8..11` raw and derive event count only with `(raw_u32_at_8_11 - 20) / 6` when divisibility and object-size bounds checks succeed.
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,13 @@ A small helper cluster in the raw `000e:` area implements a fixed-size CRLF reco
|
||||||
- the trigger/object namespace now clearly includes `JELYHACK`, `NPCTRIG`, `CRUZTRIG`, and `TRIGPAD`
|
- the trigger/object namespace now clearly includes `JELYHACK`, `NPCTRIG`, `CRUZTRIG`, and `TRIGPAD`
|
||||||
- `JELYHACK` / `JELYH2` sit in a local extraction neighborhood beside `SPECIAL`, `TRIGPAD`, `DATALINK`, `HOFFMAN`, `REE_BOOT`, `SURCAMEW`, and `SFXTRIG`, which looks more like a map/object grouping than random table order
|
- `JELYHACK` / `JELYH2` sit in a local extraction neighborhood beside `SPECIAL`, `TRIGPAD`, `DATALINK`, `HOFFMAN`, `REE_BOOT`, `SURCAMEW`, and `SFXTRIG`, which looks more like a map/object grouping than random table order
|
||||||
- that neighborhood does not make `JELYHACK` itself event-bearing, but it does place it immediately beside multiple event-capable or trigger-adjacent classes (`REE_BOOT`, `SFXTRIG`, `SURCAMEW.eventTrigger`)
|
- that neighborhood does not make `JELYHACK` itself event-bearing, but it does place it immediately beside multiple event-capable or trigger-adjacent classes (`REE_BOOT`, `SFXTRIG`, `SURCAMEW.eventTrigger`)
|
||||||
- no extracted chunk has yet been tied directly to event `0x410`
|
- the requested descriptor-family sweep now sharpens the nearby callable-body picture too: `NPCTRIG` is the only requested family here that is both explicitly event-bearing and non-empty in `class_event_index.tsv` (`equip` at slot `0x0a`, plus anonymous slot `0x20`), while `SPECIAL`, `TRIGPAD`, and `REB_PAD` have callable bodies but still look like state/controller or referent-neighbor records rather than direct event carriers
|
||||||
|
- the new generated `immortality_target_body_scan.md` / `.tsv` report now scans `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `SPECIAL`, and `TRIGPAD` body windows directly for inline little-endian `0x0410`, dword `0x00000410`, and byte-swapped `0x1004`
|
||||||
|
- that scan found zero literal hits in every currently targeted body, so no extracted target body is yet tied directly to event `0x410` by immediate-value evidence
|
||||||
|
- the `TELEPAD` slot-`0x20` row with `raw_code_offset = 0x00000410` in `class_event_index.tsv` is now closed as an offset collision, not proof that `TELEPAD` emits gameplay event `0x410`
|
||||||
|
- the new body scan also narrows the frontier structurally: `EVENT` remains one monolithic slot-`0x0a` body (`8150` bytes), `NPCTRIG` remains the strongest compact trigger frontier with slot `0x0a` (`373` bytes) plus slot `0x20` (`345` bytes), and `_BOOT` slot pairs (`COR_BOOT`/`REE_BOOT`) stay near-template bodies rather than unique immortality emitters
|
||||||
|
- `SPECIAL` and `TRIGPAD` are now stronger negative controls too: both still have callable bodies, but the new literal scan found no inline `0x410` evidence there either
|
||||||
|
- the practical blocker is now narrower: the extractor no longer stops at body offsets only, but it still does not decode emitted payload values or bytecode operands inside the surviving `EVENT` slot-`0x0a` and `NPCTRIG` slot-`0x0a` / `0x20` frontier bodies
|
||||||
- one exact `0x410` collision in compiled code is now explained away: `000e:0953` pushes `0x410` into imported `ASYLUM.27` from the animation audio-subframe path immediately after setting the local audio-completion byte at `+0xef1`. Since `ASYLUM.DLL` is the `ASS_*` audio/media library, treat this as a media ordinal/value collision rather than a second gameplay or USECODE event source.
|
- one exact `0x410` collision in compiled code is now explained away: `000e:0953` pushes `0x410` into imported `ASYLUM.27` from the animation audio-subframe path immediately after setting the local audio-completion byte at `+0xef1`. Since `ASYLUM.DLL` is the `ASS_*` audio/media library, treat this as a media ordinal/value collision rather than a second gameplay or USECODE event source.
|
||||||
- the present best reading is that `0x410` is likely carried by data relationships between generic event-capable descriptors (`EVENT`, `NPCTRIG`, `SFXTRIG`, etc.) and map/object references rather than by a plain-text script line
|
- the present best reading is that `0x410` is likely carried by data relationships between generic event-capable descriptors (`EVENT`, `NPCTRIG`, `SFXTRIG`, etc.) and map/object references rather than by a plain-text script line
|
||||||
- The `000e:` record parser helpers still matter, but they now appear to cover only the text-oriented subset rather than the entire FLX payload. The strongest concrete caller so far is the raw window at `000e:1b9f..1d49`, where `record_table_parse_buffer` is invoked after setup of fields that match the known animation object layout (`+0x117/+0x11b/+0x11f/+0x123`, `+0xeaf/+0xeb1`, `+0x10f/+0x111`). That makes the currently verified `000e:3639` consumer part of the animation-object lane, not a clean standalone EUSECODE loader.
|
- The `000e:` record parser helpers still matter, but they now appear to cover only the text-oriented subset rather than the entire FLX payload. The strongest concrete caller so far is the raw window at `000e:1b9f..1d49`, where `record_table_parse_buffer` is invoked after setup of fields that match the known animation object layout (`+0x117/+0x11b/+0x11f/+0x123`, `+0xeaf/+0xeb1`, `+0x10f/+0x111`). That makes the currently verified `000e:3639` consumer part of the animation-object lane, not a clean standalone EUSECODE loader.
|
||||||
|
|
@ -198,6 +204,10 @@ The game uses standard RIFF/IFF:
|
||||||
### Unresolved callee
|
### Unresolved callee
|
||||||
|
|
||||||
- `000e:ffb0` remains unresolved (decompiles garbled due to overlapping instructions at `000f:0085/000f:0086`). Current evidence from the `animation_start` loop suggests this path is the video-side subframe loader paired with `anim_load_audio_frame`.
|
- `000e:ffb0` remains unresolved (decompiles garbled due to overlapping instructions at `000f:0085/000f:0086`). Current evidence from the `animation_start` loop suggests this path is the video-side subframe loader paired with `anim_load_audio_frame`.
|
||||||
|
- The caller-side proof is now explicit enough to preserve that note in Ghidra too: `animation_start` invokes `anim_load_video_frame_wrapper` once per active subframe immediately after `anim_load_audio_frame`, and `anim_load_video_frame_wrapper` is only a thin forwarder to `000e:ffb0`. Until the overlap is repaired, the safest label remains `unresolved video-side subframe loader paired with the resolved audio-frame path`.
|
||||||
|
- A second caller pass tightens the local model without forcing a repair. `search_instructions` now shows `anim_load_video_frame_wrapper` is also called at `000e:11af` and `000e:1245`, not only from the startup prime loop at `000e:220c`. In both of those additional windows the return value is checked as a success/failure result, which makes `000e:ffb0` look like an active chunk-consume/decode step rather than a passive notifier.
|
||||||
|
- The strongest new evidence is the neighboring tag gate at `000e:121d..1234`: after `anim_load_audio_frame` runs, the same lane checks the current RIFF chunk tag against `0x62643030` / `0x63643030` (`"00db"` / `"00dc"`) before clearing the local busy flag and continuing. That is the first concrete caller-side clue that `000e:ffb0` is consuming AVI video-frame chunk types rather than some unrelated animation-side bookkeeping path.
|
||||||
|
- Boundary analysis still reports one overlapped function object `FUN_000e_ffb0 @ 000e:ffb0 body 000e:ffb0 - 000f:00e0`, so the function remains comment-only for now. The useful gain is semantic: the unresolved body is now best described as `video-side subframe loader/decoder for the 00db/00dc chunk lane, paired with anim_load_audio_frame`.
|
||||||
|
|
||||||
### Constructor pattern
|
### Constructor pattern
|
||||||
|
|
||||||
|
|
|
||||||
205
docs/usecode-pentagram-ghidra-path.md
Normal file
205
docs/usecode-pentagram-ghidra-path.md
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
# Pentagram-Derived USECODE Parser And Ghidra Path
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This note turns the earlier feasibility assessment into a concrete workflow.
|
||||||
|
|
||||||
|
The goal is not to make Ghidra decompile Crusader USECODE as if it were x86 immediately. The goal is to build one trustworthy bridge layer first:
|
||||||
|
|
||||||
|
- reuse Pentagram's Crusader opcode decoding where it is still valid
|
||||||
|
- replace Pentagram's older Crusader container/header assumptions with the owner-loaded class and slot model already verified in the binary and extractor
|
||||||
|
- emit a lossless IR that can drive both human-readable USECODE output and future Ghidra annotations
|
||||||
|
|
||||||
|
## What To Reuse From Pentagram
|
||||||
|
|
||||||
|
Useful directly:
|
||||||
|
|
||||||
|
- the opcode tokenization model from `convert/Convert.h`
|
||||||
|
- the disassembly-oriented mnemonic layout from `tools/disasm/Disasm.cpp`
|
||||||
|
- the Crusader event ordinal table from `convert/crusader/ConvertUsecodeCrusader.h`
|
||||||
|
|
||||||
|
Useful only as hints:
|
||||||
|
|
||||||
|
- intrinsic names and signatures
|
||||||
|
- old event-name labels for still-unresolved higher ordinals
|
||||||
|
|
||||||
|
Not safe to reuse unchanged:
|
||||||
|
|
||||||
|
- Pentagram's Crusader header reader
|
||||||
|
- any assumption that its old `maxOffset` / `externTable` / `fixupTable` structure matches the owner-loaded EUSECODE class bodies now validated in the extractor and DOS binary
|
||||||
|
- the partial Node-based decompiler path as if it were a general Crusader decompiler
|
||||||
|
|
||||||
|
## Verified Local Model To Use Instead
|
||||||
|
|
||||||
|
The proof-of-concept parser should be grounded in the existing local artifacts, not in Pentagram's old header logic.
|
||||||
|
|
||||||
|
Current authoritative inputs:
|
||||||
|
|
||||||
|
- `USECODE/EUSECODE_extracted/class_layout_index.tsv`
|
||||||
|
- `USECODE/EUSECODE_extracted/class_event_index.tsv`
|
||||||
|
- `USECODE/EUSECODE_extracted/chunks/`
|
||||||
|
|
||||||
|
Current authoritative facts:
|
||||||
|
|
||||||
|
- owner-loaded class object index is `class_id + 2`
|
||||||
|
- class bytes `8..11` provide the code-base anchor already carried in `class_layout_index.tsv`
|
||||||
|
- slot rows are 6-byte records: `u16 raw_event_entry_word + u32 raw_code_offset`
|
||||||
|
- slot body windows are already emitted conservatively as `derived_body_start`, `derived_body_end`, and `derived_body_length`
|
||||||
|
|
||||||
|
## End-To-End Process
|
||||||
|
|
||||||
|
### 1. Start from extracted owner-loaded artifacts
|
||||||
|
|
||||||
|
The parser should not reopen `EUSECODE.FLX` directly for the proof of concept. The extractor has already normalized the class and slot selection step.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
- one row from `class_layout_index.tsv`
|
||||||
|
- one row from `class_event_index.tsv`
|
||||||
|
- the corresponding chunk file under `USECODE/EUSECODE_extracted/chunks/`
|
||||||
|
|
||||||
|
### 2. Select one body window conservatively
|
||||||
|
|
||||||
|
For a chosen class and slot:
|
||||||
|
|
||||||
|
- locate `entry_index`
|
||||||
|
- confirm `derived_body_start` and `derived_body_end`
|
||||||
|
- slice the chunk-local body bytes exactly from that range
|
||||||
|
|
||||||
|
### 3. Decode opcodes with Pentagram-derived operand formats
|
||||||
|
|
||||||
|
Use Pentagram's operand-width model as the first parser source of truth.
|
||||||
|
|
||||||
|
For the proof of concept, keep decoding conservative:
|
||||||
|
|
||||||
|
- parse the op exactly when the operand format is understood
|
||||||
|
- keep the raw bytes for every parsed op
|
||||||
|
- stop cleanly on an unknown opcode and preserve the remaining tail bytes
|
||||||
|
|
||||||
|
### 4. Emit canonical IR v1
|
||||||
|
|
||||||
|
The parser output should be one machine-friendly object that includes:
|
||||||
|
|
||||||
|
- source artifact metadata
|
||||||
|
- class metadata
|
||||||
|
- slot/event metadata
|
||||||
|
- exact op list with raw bytes
|
||||||
|
- annotation hints for compiled-side VM anchors
|
||||||
|
|
||||||
|
### 5. Feed Ghidra with annotations, not with fake code yet
|
||||||
|
|
||||||
|
The first Ghidra-side use should be comments, bookmarks, and cross-reference notes on the compiled VM functions.
|
||||||
|
|
||||||
|
Do not try to map the bytecode into a full processor module first.
|
||||||
|
|
||||||
|
## Proof-Of-Concept Parser
|
||||||
|
|
||||||
|
Tool path:
|
||||||
|
|
||||||
|
- `tools/poc_crusader_usecode_parser.py`
|
||||||
|
|
||||||
|
Current scope:
|
||||||
|
|
||||||
|
- uses the extracted TSV and chunk artifacts already in the repo
|
||||||
|
- disassembles one selected class/slot body at a time
|
||||||
|
- emits canonical IR JSON
|
||||||
|
- optionally emits a readable text listing beside the JSON
|
||||||
|
|
||||||
|
Current deliberate limits:
|
||||||
|
|
||||||
|
- no full intrinsic name table yet
|
||||||
|
- no synthetic control-flow graph yet
|
||||||
|
- no recompilation path yet
|
||||||
|
- no Ghidra importer yet
|
||||||
|
|
||||||
|
That keeps the parser useful without pretending the VM is fully solved.
|
||||||
|
|
||||||
|
## Canonical Ghidra Annotation Import Path
|
||||||
|
|
||||||
|
The first importer should consume the parser IR and create only three kinds of output.
|
||||||
|
|
||||||
|
### 1. Bookmarks
|
||||||
|
|
||||||
|
Use bookmarks for class/slot-level evidence that should not be hidden inside comments.
|
||||||
|
|
||||||
|
Good first bookmark payloads:
|
||||||
|
|
||||||
|
- `NPCTRIG slot 0x0A body parsed by POC tool`
|
||||||
|
- `EVENT slot 0x0A body parsed by POC tool`
|
||||||
|
- `slot 0x13 payload-shape hint = signed_word`
|
||||||
|
|
||||||
|
### 2. Plate or decompiler comments on compiled anchors
|
||||||
|
|
||||||
|
Use comments on the compiled runtime functions that already consume or materialize the USECODE bodies.
|
||||||
|
|
||||||
|
Best current anchors:
|
||||||
|
|
||||||
|
- `000d:51fd` = slot value load path
|
||||||
|
- `000d:5572` = slot value plus additive word
|
||||||
|
- `000d:46ec` = context create from slot index
|
||||||
|
- `000d:22bc` = decoded matrix/pushback consumer
|
||||||
|
- `000d:ebe3` = opcode sequence runner
|
||||||
|
|
||||||
|
Comment payload should stay short and evidence-heavy, for example:
|
||||||
|
|
||||||
|
`POC USECODE body anchor: NPCTRIG slot 0x0A -> body 0x00DA..0x024F, raw word 0x013E, payload shape unresolved, parsed via tools/poc_crusader_usecode_parser.py`
|
||||||
|
|
||||||
|
### 3. Optional comment bundles per runtime family
|
||||||
|
|
||||||
|
If a later importer wants to annotate more than one function at once, keep it grouped by runtime family instead of by class name.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `slot-backed-owner-loaded-body`
|
||||||
|
- `slot-plus-offset-value-reload`
|
||||||
|
- `sequencer-matrix-consumer`
|
||||||
|
- `literal-replay-interpreter-upstream`
|
||||||
|
|
||||||
|
## Why Not A Ghidra Processor Yet
|
||||||
|
|
||||||
|
The missing pieces are still too important:
|
||||||
|
|
||||||
|
- full opcode semantics are incomplete
|
||||||
|
- stack and return discipline are incomplete
|
||||||
|
- the relation between owner-loaded body bytes and the later `000c:fa2f` literal/replay lane is still not closed end-to-end
|
||||||
|
- the upstream selector into `entity_vm_opcode_sequence_run` is still unresolved
|
||||||
|
|
||||||
|
So the right order is:
|
||||||
|
|
||||||
|
1. parser
|
||||||
|
2. IR
|
||||||
|
3. annotation import
|
||||||
|
4. only then reconsider a language module
|
||||||
|
|
||||||
|
## User Workflow
|
||||||
|
|
||||||
|
Run the proof-of-concept parser from the repo root.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
c:/Users/Maddo/.PYENV/PYENV-WIN/versions/3.14.3/python.exe tools/poc_crusader_usecode_parser.py --class NPCTRIG --slot 0x0A --emit-text
|
||||||
|
```
|
||||||
|
|
||||||
|
Recommended first targets:
|
||||||
|
|
||||||
|
1. `NPCTRIG` slot `0x0A`
|
||||||
|
2. `NPCTRIG` slot `0x20`
|
||||||
|
3. `EVENT` slot `0x0A`
|
||||||
|
4. one `_BOOT` slot `0x10` body as a short repeated-template control sample
|
||||||
|
|
||||||
|
What to look for in the output:
|
||||||
|
|
||||||
|
- exact raw body window
|
||||||
|
- whether the body terminates cleanly at opcode `0x7A`
|
||||||
|
- body-local call targets and global-address ops
|
||||||
|
- repeated structural motifs that can be carried back into the VM notes
|
||||||
|
- anchor hints for the compiled runtime functions
|
||||||
|
|
||||||
|
## Next Extensions
|
||||||
|
|
||||||
|
1. Add the full Crusader intrinsic-name table from Pentagram as hint-only metadata.
|
||||||
|
2. Emit repeated-body family diffs directly from the parser instead of only from the extractor reports.
|
||||||
|
3. Add a small importer that converts `annotation_hints` into Ghidra comments and bookmarks.
|
||||||
|
4. Extend the IR with control-flow edges only after branch/jump confidence is high enough.
|
||||||
|
5. Tie parser output back to the current slot/additive runtime tuples used in the compiled VM lane.
|
||||||
|
|
@ -359,6 +359,135 @@ class:
|
||||||
confidence: authoritative-bytes, hinted-label
|
confidence: authoritative-bytes, hinted-label
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## IR v1 Parser Schema
|
||||||
|
|
||||||
|
The next tooling step changes the role of this document slightly. IR v0 was a note-level target for reversible human-readable output. IR v1 is the canonical machine-facing schema for the Pentagram-derived proof-of-concept parser and any future Ghidra annotation bridge.
|
||||||
|
|
||||||
|
The design constraints are now explicit:
|
||||||
|
|
||||||
|
- keep every authoritative owner-loaded byte visible
|
||||||
|
- keep slot identity separate from semantic name hints
|
||||||
|
- keep runtime-facing metadata visible even when the body decompiler cannot yet explain it
|
||||||
|
- preserve enough structure to emit Ghidra comments and bookmarks later without reparsing prose notes
|
||||||
|
|
||||||
|
### Top-level IR object
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
schema_version: crusader-usecode-ir-v1-poc
|
||||||
|
source:
|
||||||
|
flex_path: USECODE/EUSECODE.FLX
|
||||||
|
extracted_root: USECODE/EUSECODE_extracted
|
||||||
|
chunk_file: USECODE/EUSECODE_extracted/chunks/chunk_191_table_1BA8_off_04C347_len_0003A8.bin
|
||||||
|
class:
|
||||||
|
entry_index: 191
|
||||||
|
object_index: 0x365
|
||||||
|
class_id: 0x363
|
||||||
|
class_name: NPCTRIG
|
||||||
|
raw_code_base_u32: 0x00da
|
||||||
|
code_base_minus_one: 0x00d9
|
||||||
|
conservative_event_count: 0x21
|
||||||
|
event:
|
||||||
|
slot: 0x0a
|
||||||
|
event_name_hint: equip
|
||||||
|
raw_event_entry_word: 0x013e
|
||||||
|
raw_code_offset: 0x00000001
|
||||||
|
derived_body_start: 0x00da
|
||||||
|
derived_body_end: 0x024f
|
||||||
|
derived_body_length: 373
|
||||||
|
repeated_template_status: ""
|
||||||
|
body:
|
||||||
|
end_reason: end_opcode
|
||||||
|
raw_body_sha1: <digest>
|
||||||
|
unknown_trailing_bytes: ""
|
||||||
|
ops:
|
||||||
|
- offset: 0x0000
|
||||||
|
absolute_body_offset: 0x00da
|
||||||
|
opcode: 0x5a
|
||||||
|
mnemonic: init
|
||||||
|
raw_bytes: 5a06
|
||||||
|
operands:
|
||||||
|
local_bytes: 0x06
|
||||||
|
- offset: 0x0011
|
||||||
|
absolute_body_offset: 0x00eb
|
||||||
|
opcode: 0x40
|
||||||
|
mnemonic: push_local_dword
|
||||||
|
raw_bytes: 40064c02
|
||||||
|
operands:
|
||||||
|
bp_offset: 0x06
|
||||||
|
annotation_hints:
|
||||||
|
runtime_family: slot-backed-owner-loaded-body
|
||||||
|
compiled_anchors:
|
||||||
|
- 000d:51fd
|
||||||
|
- 000d:5572
|
||||||
|
- 000d:46ec
|
||||||
|
- 000d:ebe3
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required fields
|
||||||
|
|
||||||
|
`source` keeps the specific extracted artifact path so the parser output can always be checked against the raw chunk bytes.
|
||||||
|
|
||||||
|
`class` keeps the owner-loaded identity and header math already validated in the binary.
|
||||||
|
|
||||||
|
`event` keeps the exact six-byte row meaningfully split into authoritative fields plus the derived body window.
|
||||||
|
|
||||||
|
`body` records how far the parser got and whether any bytes remain undecoded or trailing.
|
||||||
|
|
||||||
|
`ops` is intentionally lossless. Each decoded op keeps:
|
||||||
|
|
||||||
|
- body-relative offset
|
||||||
|
- absolute chunk-local offset
|
||||||
|
- raw opcode byte
|
||||||
|
- mnemonic
|
||||||
|
- exact raw bytes for the whole op
|
||||||
|
- parsed operands as typed fields
|
||||||
|
|
||||||
|
`annotation_hints` is the bridge to Ghidra. It is not a source-language feature. It exists so a later importer can attach the right comments and bookmarks to the compiled VM/runtime addresses without trying to infer them from free text.
|
||||||
|
|
||||||
|
### Opcode result policy
|
||||||
|
|
||||||
|
The parser should use four result classes only:
|
||||||
|
|
||||||
|
- `decoded_op`: normal parsed opcode with structured operands
|
||||||
|
- `unknown_opcode`: one-byte opcode not yet modeled; stop or fall back conservatively
|
||||||
|
- `raw_tail`: remaining undecoded bytes after a stop condition
|
||||||
|
- `debug_blob`: symbol/debug tail such as `0x5c`-anchored metadata
|
||||||
|
|
||||||
|
That keeps the IR trustworthy even before the whole Crusader VM is modeled.
|
||||||
|
|
||||||
|
### Call-site hint policy
|
||||||
|
|
||||||
|
For `call` and `spawn`-family ops, the parser may attach:
|
||||||
|
|
||||||
|
- `target_class_id`
|
||||||
|
- `target_event_slot`
|
||||||
|
- `target_event_name_hint`
|
||||||
|
|
||||||
|
It should not attach a stronger semantic claim than that. The body parser is class/event aware, but not yet authoritative about gameplay meaning.
|
||||||
|
|
||||||
|
### Annotation-hint schema
|
||||||
|
|
||||||
|
The Ghidra bridge should consume only small, stable items:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
annotation_hints:
|
||||||
|
runtime_family: slot-backed-owner-loaded-body
|
||||||
|
payload_shape_hint: signed_word
|
||||||
|
compiled_anchors:
|
||||||
|
- address: 000d:51fd
|
||||||
|
role: slot_value_loader
|
||||||
|
- address: 000d:5572
|
||||||
|
role: slot_value_plus_offset
|
||||||
|
- address: 000d:46ec
|
||||||
|
role: context_create_from_slot
|
||||||
|
- address: 000d:ebe3
|
||||||
|
role: opcode_sequence_run
|
||||||
|
- address: 000d:22bc
|
||||||
|
role: matrix_pushback_stage
|
||||||
|
```
|
||||||
|
|
||||||
|
This is deliberately smaller than a full import format. It keeps the parser reusable even if the first Ghidra-side importer is only a comment/bookmark script.
|
||||||
|
|
||||||
That is already a real decompilation output. It keeps the exact slot id, the exact six-byte row contents, and the exact class-header facts, while refusing to pretend that `use` is already a proven semantic name for this class.
|
That is already a real decompilation output. It keeps the exact slot id, the exact six-byte row contents, and the exact class-header facts, while refusing to pretend that `use` is already a proven semantic name for this class.
|
||||||
|
|
||||||
Here is the same style for one active event-bearing attachment class in the same island:
|
Here is the same style for one active event-bearing attachment class in the same island:
|
||||||
|
|
@ -543,6 +672,43 @@ vm_effect_possible:
|
||||||
|
|
||||||
That operator block is authoritative as a recovered VM vocabulary, but only ecosystem-level when attached to one specific descriptor family.
|
That operator block is authoritative as a recovered VM vocabulary, but only ecosystem-level when attached to one specific descriptor family.
|
||||||
|
|
||||||
|
### Binary-side slot and payload-shape evidence to preserve in IR
|
||||||
|
|
||||||
|
The current VM pass also adds one useful binary-side rule for the higher event ordinals: the compiled wrapper family distinguishes slot identity from payload shape, and that distinction should survive in any round-trip IR even when the human label stays unresolved.
|
||||||
|
|
||||||
|
Verified current ladder around `0005:3115..31da`:
|
||||||
|
|
||||||
|
- slot `0x10`: guarded callsite only, zero extra word, packed mask `0x00010000`
|
||||||
|
- slot `0x11`: named wrapper `entity_vm_context_try_create_mask_00020000_slot11_with_offset`, one caller-supplied extra word
|
||||||
|
- slot `0x12`: named wrapper `entity_vm_context_try_create_mask_00040000_slot12`, zero extra word
|
||||||
|
- slot `0x13`: named wrapper `entity_vm_context_try_create_mask_00080000_slot13_with_offset_if_valid_entity`, one sign-extended extra word after an entity-validity gate
|
||||||
|
- slot `0x14`: named wrapper `entity_vm_context_try_create_mask_00100000_slot14_with_offset`, one caller-supplied extra word
|
||||||
|
|
||||||
|
Why this matters for the IR:
|
||||||
|
|
||||||
|
- It is direct binary evidence that some higher Crusader slot ordinals are already grouped by argument shape before any descriptor-family mapping is proven.
|
||||||
|
- That means the IR should preserve `slot_id` plus `payload_shape` independently instead of collapsing everything into one guessed event-name table.
|
||||||
|
- It also gives a bounded way to cross-check external event signatures without over-trusting them: slot `0x12` fits a zero-arg event shape, slot `0x13` fits a one-word event shape, and slot `0x14` currently conflicts with Pentagram's older zero-arg `animGetHit()` note.
|
||||||
|
|
||||||
|
Practical annotation rule to adopt now:
|
||||||
|
|
||||||
|
- keep higher-slot labels binary-stable as `slot 0x10` .. `slot 0x14` unless local behavior closes the label
|
||||||
|
- attach external event names only as hints
|
||||||
|
- attach one small `payload_shape_hint` field such as `none`, `word`, or `signed_word`
|
||||||
|
|
||||||
|
Minimal hinted example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
slot_record:
|
||||||
|
slot_id: 0x13
|
||||||
|
event_name_hint: avatarStoleSomething
|
||||||
|
payload_shape_hint: signed_word
|
||||||
|
binary_anchor: 0005:31da
|
||||||
|
wrapper_name: entity_vm_context_try_create_mask_00080000_slot13_with_offset_if_valid_entity
|
||||||
|
```
|
||||||
|
|
||||||
|
The same pass also hardens one existing IR operator boundary: the `000d:22bc` stage is now comment-backed in Ghidra as a matrix/pushback consumer over decoded workspace bytes, not a direct descriptor-row reader. The current safe attachment point is therefore still `decoded VM workspace -> link-matrix stage`, not `NPCTRIG row -> direct entity-link emission`.
|
||||||
|
|
||||||
## Conservative Parser Rule To Adopt Now
|
## Conservative Parser Rule To Adopt Now
|
||||||
|
|
||||||
For the current owner-loaded EUSECODE and round-trip IR work, the safest reversible rule is:
|
For the current owner-loaded EUSECODE and round-trip IR work, the safest reversible rule is:
|
||||||
|
|
|
||||||
592
family_diff_NPCTRIG_0A.txt
Normal file
592
family_diff_NPCTRIG_0A.txt
Normal file
File diff suppressed because one or more lines are too long
30278
out_EVENT_0A.json
Normal file
30278
out_EVENT_0A.json
Normal file
File diff suppressed because it is too large
Load diff
2944
out_EVENT_0A.txt
Normal file
2944
out_EVENT_0A.txt
Normal file
File diff suppressed because it is too large
Load diff
1201
out_NPCTRIG_0A.json
Normal file
1201
out_NPCTRIG_0A.json
Normal file
File diff suppressed because it is too large
Load diff
117
out_NPCTRIG_0A.txt
Normal file
117
out_NPCTRIG_0A.txt
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
Class NPCTRIG entry=191 class_id=0x363
|
||||||
|
Slot 0x0A hint=equip body=0x00DA..0x024F
|
||||||
|
End reason: unknown_opcode ops=111 sha1=98524ea452eae2723f4b27e630c33a920c16def7
|
||||||
|
|
||||||
|
00DA: 5A init local_bytes=0x6 raw=5a06
|
||||||
|
00DC: 5C symbol_info symbol_offset=0x143 symbol=NPCTRIG trailing_zero=0x0 raw=5c3e014e5043545249470000
|
||||||
|
00E8: 0B push_word_immediate value_u16=0x211 raw=0b1102
|
||||||
|
00EB: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
00ED: 4C push_indirect size=0x2 raw=4c02
|
||||||
|
00EF: 77 set_info raw=77
|
||||||
|
00F0: 78 process_exclude raw=78
|
||||||
|
00F1: 5B line_number line_number=0x20 raw=5b2000
|
||||||
|
00F4: 5B line_number line_number=0x1F raw=5b1f00
|
||||||
|
00F7: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
00F9: 0F call_intrinsic arg_bytes=0x4 intrinsic_ordinal=0x4 intrinsic_name_hint=Item::getStatus(void) raw=0f040400
|
||||||
|
00FD: 6E add_sp value_u8=0xFC raw=6efc
|
||||||
|
00FF: 5E push_retval_word raw=5e
|
||||||
|
0100: 5B line_number line_number=0x20 raw=5b2000
|
||||||
|
0103: 0B push_word_immediate value_u16=0x1000 raw=0b0010
|
||||||
|
0106: 39 bit_and raw=39
|
||||||
|
0107: 51 jne relative_u16=0x6 relative_signed=0x6 target_offset=0x36 raw=510600
|
||||||
|
010A: 5B line_number line_number=0x21 raw=5b2100
|
||||||
|
010D: 52 jmp relative_u16=0x109 relative_signed=0x109 target_offset=0x13F raw=520901
|
||||||
|
0110: 5B line_number line_number=0x24 raw=5b2400
|
||||||
|
0113: 0B push_word_immediate value_u16=0x1000 raw=0b0010
|
||||||
|
0116: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
0118: 11 call_class_event target_class_id=0xA1E target_event_slot=0x23 target_event_name_hint=None raw=111e0a2300
|
||||||
|
011D: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
011F: 3F push_local_word bp_offset=0xA target=[BP+0Ah] raw=3f0a
|
||||||
|
0121: 0A push_byte_immediate value_u8=0x1 value_signed=0x1 raw=0a01
|
||||||
|
0123: 24 cmp raw=24
|
||||||
|
0124: 51 jne relative_u16=0x27 relative_signed=0x27 target_offset=0x74 raw=512700
|
||||||
|
0127: 5B line_number line_number=0x2C raw=5b2c00
|
||||||
|
012A: 59 push_pid raw=59
|
||||||
|
012B: 0B push_word_immediate value_u16=0x2FD raw=0bfd02
|
||||||
|
012E: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
0130: 57 spawn arg_bytes=0x2 this_size=0x2 target_class_id=0x363 target_event_slot=0x20 target_event_name_hint=None raw=57020263032000
|
||||||
|
0137: 6E add_sp value_u8=0xFE raw=6efe
|
||||||
|
0139: 5E push_retval_word raw=5e
|
||||||
|
013A: 54 implies arg0=0x1 arg1=0x1 raw=540101
|
||||||
|
013D: 12 pop_temp raw=12
|
||||||
|
013E: 53 suspend raw=53
|
||||||
|
013F: 5C symbol_info symbol_offset=0x143 symbol=NPCTRIG trailing_zero=0x0 raw=5cdb004e5043545249470000
|
||||||
|
014B: 52 jmp relative_u16=0xBC relative_signed=0xBC target_offset=0x130 raw=52bc00
|
||||||
|
014E: 3F push_local_word bp_offset=0xA target=[BP+0Ah] raw=3f0a
|
||||||
|
0150: 0A push_byte_immediate value_u8=0x2 value_signed=0x2 raw=0a02
|
||||||
|
0152: 24 cmp raw=24
|
||||||
|
0153: 51 jne relative_u16=0x27 relative_signed=0x27 target_offset=0xA3 raw=512700
|
||||||
|
0156: 5B line_number line_number=0x31 raw=5b3100
|
||||||
|
0159: 59 push_pid raw=59
|
||||||
|
015A: 0B push_word_immediate value_u16=0x384 raw=0b8403
|
||||||
|
015D: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
015F: 57 spawn arg_bytes=0x2 this_size=0x2 target_class_id=0x363 target_event_slot=0x20 target_event_name_hint=None raw=57020263032000
|
||||||
|
0166: 6E add_sp value_u8=0xFE raw=6efe
|
||||||
|
0168: 5E push_retval_word raw=5e
|
||||||
|
0169: 54 implies arg0=0x1 arg1=0x1 raw=540101
|
||||||
|
016C: 12 pop_temp raw=12
|
||||||
|
016D: 53 suspend raw=53
|
||||||
|
016E: 5C symbol_info symbol_offset=0x143 symbol=NPCTRIG trailing_zero=0x0 raw=5cac004e5043545249470000
|
||||||
|
017A: 52 jmp relative_u16=0x8D relative_signed=0x8D target_offset=0x130 raw=528d00
|
||||||
|
017D: 3F push_local_word bp_offset=0xA target=[BP+0Ah] raw=3f0a
|
||||||
|
017F: 0A push_byte_immediate value_u8=0x3 value_signed=0x3 raw=0a03
|
||||||
|
0181: 24 cmp raw=24
|
||||||
|
0182: 51 jne relative_u16=0x27 relative_signed=0x27 target_offset=0xD2 raw=512700
|
||||||
|
0185: 5B line_number line_number=0x36 raw=5b3600
|
||||||
|
0188: 59 push_pid raw=59
|
||||||
|
0189: 0B push_word_immediate value_u16=0x371 raw=0b7103
|
||||||
|
018C: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
018E: 57 spawn arg_bytes=0x2 this_size=0x2 target_class_id=0x363 target_event_slot=0x20 target_event_name_hint=None raw=57020263032000
|
||||||
|
0195: 6E add_sp value_u8=0xFE raw=6efe
|
||||||
|
0197: 5E push_retval_word raw=5e
|
||||||
|
0198: 54 implies arg0=0x1 arg1=0x1 raw=540101
|
||||||
|
019B: 12 pop_temp raw=12
|
||||||
|
019C: 53 suspend raw=53
|
||||||
|
019D: 5C symbol_info symbol_offset=0x143 symbol=NPCTRIG trailing_zero=0x0 raw=5c7d004e5043545249470000
|
||||||
|
01A9: 52 jmp relative_u16=0x5E relative_signed=0x5E target_offset=0x130 raw=525e00
|
||||||
|
01AC: 3F push_local_word bp_offset=0xA target=[BP+0Ah] raw=3f0a
|
||||||
|
01AE: 0A push_byte_immediate value_u8=0x4 value_signed=0x4 raw=0a04
|
||||||
|
01B0: 24 cmp raw=24
|
||||||
|
01B1: 51 jne relative_u16=0x27 relative_signed=0x27 target_offset=0x101 raw=512700
|
||||||
|
01B4: 5B line_number line_number=0x3B raw=5b3b00
|
||||||
|
01B7: 59 push_pid raw=59
|
||||||
|
01B8: 0B push_word_immediate value_u16=0x4D1 raw=0bd104
|
||||||
|
01BB: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
01BD: 57 spawn arg_bytes=0x2 this_size=0x2 target_class_id=0x363 target_event_slot=0x20 target_event_name_hint=None raw=57020263032000
|
||||||
|
01C4: 6E add_sp value_u8=0xFE raw=6efe
|
||||||
|
01C6: 5E push_retval_word raw=5e
|
||||||
|
01C7: 54 implies arg0=0x1 arg1=0x1 raw=540101
|
||||||
|
01CA: 12 pop_temp raw=12
|
||||||
|
01CB: 53 suspend raw=53
|
||||||
|
01CC: 5C symbol_info symbol_offset=0x143 symbol=NPCTRIG trailing_zero=0x0 raw=5c4e004e5043545249470000
|
||||||
|
01D8: 52 jmp relative_u16=0x2F relative_signed=0x2F target_offset=0x130 raw=522f00
|
||||||
|
01DB: 3F push_local_word bp_offset=0xA target=[BP+0Ah] raw=3f0a
|
||||||
|
01DD: 0A push_byte_immediate value_u8=0x5 value_signed=0x5 raw=0a05
|
||||||
|
01DF: 24 cmp raw=24
|
||||||
|
01E0: 51 jne relative_u16=0x27 relative_signed=0x27 target_offset=0x130 raw=512700
|
||||||
|
01E3: 5B line_number line_number=0x40 raw=5b4000
|
||||||
|
01E6: 59 push_pid raw=59
|
||||||
|
01E7: 0B push_word_immediate value_u16=0x1B4 raw=0bb401
|
||||||
|
01EA: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
01EC: 57 spawn arg_bytes=0x2 this_size=0x2 target_class_id=0x363 target_event_slot=0x20 target_event_name_hint=None raw=57020263032000
|
||||||
|
01F3: 6E add_sp value_u8=0xFE raw=6efe
|
||||||
|
01F5: 5E push_retval_word raw=5e
|
||||||
|
01F6: 54 implies arg0=0x1 arg1=0x1 raw=540101
|
||||||
|
01F9: 12 pop_temp raw=12
|
||||||
|
01FA: 53 suspend raw=53
|
||||||
|
01FB: 5C symbol_info symbol_offset=0x143 symbol=NPCTRIG trailing_zero=0x0 raw=5c1f004e5043545249470000
|
||||||
|
0207: 52 jmp relative_u16=0x0 relative_signed=0x0 target_offset=0x130 raw=520000
|
||||||
|
020A: 5B line_number line_number=0x45 raw=5b4500
|
||||||
|
020D: 0B push_word_immediate value_u16=0x1000 raw=0b0010
|
||||||
|
0210: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
0212: 11 call_class_event target_class_id=0xA1E target_event_slot=0x24 target_event_name_hint=None raw=111e0a2400
|
||||||
|
0217: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
0219: 5B line_number line_number=0x47 raw=5b4700
|
||||||
|
021C: 50 ret raw=50
|
||||||
|
|
||||||
|
unknown_trailing_bytes=05016900007265666572656e740000690a006576656e74000024fe026974656d000024fc026974656d32000024fa026e007a
|
||||||
1229
out_NPCTRIG_20.json
Normal file
1229
out_NPCTRIG_20.json
Normal file
File diff suppressed because it is too large
Load diff
117
out_NPCTRIG_20.txt
Normal file
117
out_NPCTRIG_20.txt
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
Class NPCTRIG entry=191 class_id=0x363
|
||||||
|
Slot 0x20 hint=None body=0x024F..0x03A8
|
||||||
|
End reason: unknown_opcode ops=111 sha1=2e696a6562c2a44adbd645948a442c9a55a63ba0
|
||||||
|
|
||||||
|
024F: 5A init local_bytes=0x6 raw=5a06
|
||||||
|
0251: 5C symbol_info symbol_offset=0x125 symbol=NPCTRIG trailing_zero=0x0 raw=5c20014e5043545249470000
|
||||||
|
025D: 0B push_word_immediate value_u16=0x1 raw=0b0100
|
||||||
|
0260: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
0262: 4C push_indirect size=0x2 raw=4c02
|
||||||
|
0264: 77 set_info raw=77
|
||||||
|
0265: 74 loopscr value_u8=0x24 raw=7424
|
||||||
|
0267: 74 loopscr value_u8=0x3D raw=743d
|
||||||
|
0269: 74 loopscr value_u8=0x40 raw=7440
|
||||||
|
026B: 5B line_number line_number=0x53 raw=5b5300
|
||||||
|
026E: 0B push_word_immediate value_u16=0x1DB raw=0bdb01
|
||||||
|
0271: 74 loopscr value_u8=0x25 raw=7425
|
||||||
|
0273: 0A push_byte_immediate value_u8=0x20 value_signed=0x20 raw=0a20
|
||||||
|
0275: 0A push_byte_immediate value_u8=0x20 value_signed=0x20 raw=0a20
|
||||||
|
0277: 1E mul raw=1e
|
||||||
|
0278: 5B line_number line_number=0x54 raw=5b5400
|
||||||
|
027B: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
027D: 4C push_indirect size=0x2 raw=4c02
|
||||||
|
027F: 70 loop current_var=0xFC string_bytes=0x6 loop_type=0x2 raw=70fc0602
|
||||||
|
0283: 51 jne relative_u16=0xB relative_signed=0xB target_offset=0x42 raw=510b00
|
||||||
|
0286: 5B line_number line_number=0x55 raw=5b5500
|
||||||
|
0289: 3F push_local_word bp_offset=0xFC target=[BP-04h] raw=3ffc
|
||||||
|
028B: 01 pop_local_word bp_offset=0xFA target=[BP-06h] raw=01fa
|
||||||
|
028D: 73 loopnext raw=73
|
||||||
|
028E: 52 jmp relative_u16=0xFFF2 relative_signed=0x-E target_offset=0x34 raw=52f2ff
|
||||||
|
0291: 6E add_sp value_u8=0xC6 raw=6ec6
|
||||||
|
0293: 5B line_number line_number=0x5A raw=5b5a00
|
||||||
|
0296: 3F push_local_word bp_offset=0xFA target=[BP-06h] raw=3ffa
|
||||||
|
0298: 51 jne relative_u16=0xD5 relative_signed=0xD5 target_offset=0x121 raw=51d500
|
||||||
|
029B: 5B line_number line_number=0x5B raw=5b5b00
|
||||||
|
029E: 0A push_byte_immediate value_u8=0x8 value_signed=0x8 raw=0a08
|
||||||
|
02A0: 3F push_local_word bp_offset=0xA target=[BP+0Ah] raw=3f0a
|
||||||
|
02A2: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
02A4: 0F call_intrinsic arg_bytes=0x8 intrinsic_ordinal=0x61 intrinsic_name_hint=Intrinsic0061() raw=0f086100
|
||||||
|
02A8: 6E add_sp value_u8=0xF8 raw=6ef8
|
||||||
|
02AA: 5B line_number line_number=0x5C raw=5b5c00
|
||||||
|
02AD: 0A push_byte_immediate value_u8=0x0 value_signed=0x0 raw=0a00
|
||||||
|
02AF: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
02B1: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0x52 intrinsic_name_hint=Intrinsic0052() raw=0f065200
|
||||||
|
02B5: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
02B7: 5B line_number line_number=0x5D raw=5b5d00
|
||||||
|
02BA: 0A push_byte_immediate value_u8=0x0 value_signed=0x0 raw=0a00
|
||||||
|
02BC: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
02BE: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0x53 intrinsic_name_hint=Intrinsic00BD() raw=0f065300
|
||||||
|
02C2: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
02C4: 5B line_number line_number=0x5E raw=5b5e00
|
||||||
|
02C7: 0A push_byte_immediate value_u8=0x0 value_signed=0x0 raw=0a00
|
||||||
|
02C9: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
02CB: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0x54 intrinsic_name_hint=Intrinsic0054() raw=0f065400
|
||||||
|
02CF: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
02D1: 5B line_number line_number=0x5F raw=5b5f00
|
||||||
|
02D4: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
02D6: 0F call_intrinsic arg_bytes=0x4 intrinsic_ordinal=0x9 intrinsic_name_hint=Item::getZ(void) raw=0f040900
|
||||||
|
02DA: 6E add_sp value_u8=0xFC raw=6efc
|
||||||
|
02DC: 5D push_retval_byte raw=5d
|
||||||
|
02DD: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
02DF: 0F call_intrinsic arg_bytes=0x4 intrinsic_ordinal=0x14 intrinsic_name_hint=Item::legal_create(uint16,uint16,uint16,uint16,uint16) raw=0f041400
|
||||||
|
02E3: 6E add_sp value_u8=0xFC raw=6efc
|
||||||
|
02E5: 5E push_retval_word raw=5e
|
||||||
|
02E6: 40 push_local_dword bp_offset=0x6 target=[BP+06h] raw=4006
|
||||||
|
02E8: 0F call_intrinsic arg_bytes=0x4 intrinsic_ordinal=0x13 intrinsic_name_hint=Intrinsic0013() raw=0f041300
|
||||||
|
02EC: 6E add_sp value_u8=0xFC raw=6efc
|
||||||
|
02EE: 5E push_retval_word raw=5e
|
||||||
|
02EF: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
02F1: 0F call_intrinsic arg_bytes=0xA intrinsic_ordinal=0x20 intrinsic_name_hint=Item::setQLo(sint16) raw=0f0a2000
|
||||||
|
02F5: 6E add_sp value_u8=0xF6 raw=6ef6
|
||||||
|
02F7: 5B line_number line_number=0x61 raw=5b6100
|
||||||
|
02FA: 59 push_pid raw=59
|
||||||
|
02FB: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
02FD: 57 spawn arg_bytes=0x0 this_size=0x2 target_class_id=0xA11 target_event_slot=0x23 target_event_name_hint=None raw=570002110a2300
|
||||||
|
0304: 5E push_retval_word raw=5e
|
||||||
|
0305: 54 implies arg0=0x1 arg1=0x1 raw=540101
|
||||||
|
0308: 12 pop_temp raw=12
|
||||||
|
0309: 53 suspend raw=53
|
||||||
|
030A: 5C symbol_info symbol_offset=0x125 symbol=NPCTRIG trailing_zero=0x0 raw=5c67004e5043545249470000
|
||||||
|
0316: 5B line_number line_number=0x62 raw=5b6200
|
||||||
|
0319: 0A push_byte_immediate value_u8=0x5 value_signed=0x5 raw=0a05
|
||||||
|
031B: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
031D: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0x52 intrinsic_name_hint=Intrinsic0052() raw=0f065200
|
||||||
|
0321: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
0323: 5B line_number line_number=0x63 raw=5b6300
|
||||||
|
0326: 0A push_byte_immediate value_u8=0x5 value_signed=0x5 raw=0a05
|
||||||
|
0328: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
032A: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0x53 intrinsic_name_hint=Intrinsic00BD() raw=0f065300
|
||||||
|
032E: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
0330: 5B line_number line_number=0x64 raw=5b6400
|
||||||
|
0333: 0A push_byte_immediate value_u8=0x5 value_signed=0x5 raw=0a05
|
||||||
|
0335: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
0337: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0x54 intrinsic_name_hint=Intrinsic0054() raw=0f065400
|
||||||
|
033B: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
033D: 5B line_number line_number=0x67 raw=5b6700
|
||||||
|
0340: 5B line_number line_number=0x66 raw=5b6600
|
||||||
|
0343: 0F call_intrinsic arg_bytes=0x0 intrinsic_ordinal=0x1B intrinsic_name_hint=Item::pop(uint16,uint16,uint8) raw=0f001b00
|
||||||
|
0347: 5E push_retval_word raw=5e
|
||||||
|
0348: 0A push_byte_immediate value_u8=0x0 value_signed=0x0 raw=0a00
|
||||||
|
034A: 36 ne raw=36
|
||||||
|
034B: 51 jne relative_u16=0x13 relative_signed=0x13 target_offset=0x112 raw=511300
|
||||||
|
034E: 5B line_number line_number=0x68 raw=5b6800
|
||||||
|
0351: 0F call_intrinsic arg_bytes=0x0 intrinsic_ordinal=0x1B intrinsic_name_hint=Item::pop(uint16,uint16,uint8) raw=0f001b00
|
||||||
|
0355: 5E push_retval_word raw=5e
|
||||||
|
0356: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
0358: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0xE5 intrinsic_name_hint=Item::hurl(sint16,sint16,sint16,sint16) raw=0f06e500
|
||||||
|
035C: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
035E: 52 jmp relative_u16=0xF relative_signed=0xF target_offset=0x121 raw=520f00
|
||||||
|
0361: 5B line_number line_number=0x6C raw=5b6c00
|
||||||
|
0364: 4E push_global global_id=0x3C size=0x2 raw=4e3c0002
|
||||||
|
0368: 4B push_local_addr bp_offset=0xFE target=[BP-02h] raw=4bfe
|
||||||
|
036A: 0F call_intrinsic arg_bytes=0x6 intrinsic_ordinal=0xE5 intrinsic_name_hint=Item::hurl(sint16,sint16,sint16,sint16) raw=0f06e500
|
||||||
|
036E: 6E add_sp value_u8=0xFA raw=6efa
|
||||||
|
0370: 5B line_number line_number=0x6F raw=5b6f00
|
||||||
|
0373: 50 ret raw=50
|
||||||
|
|
||||||
|
unknown_trailing_bytes=05016900007265666572656e740000690a00747970654e7063000024fe026e000024fc026974656d000024fa026974656d32007a
|
||||||
715
patch_crusader_cheat_menu.ps1
Normal file
715
patch_crusader_cheat_menu.ps1
Normal file
|
|
@ -0,0 +1,715 @@
|
||||||
|
param(
|
||||||
|
[ValidateSet('1', '2', '3', '4')]
|
||||||
|
[string]$Choice
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$exePath = Join-Path $PSScriptRoot 'CRUSADER.EXE'
|
||||||
|
$statePath = Join-Path $PSScriptRoot 'patch_crusader_cheat_menu.state.json'
|
||||||
|
|
||||||
|
$sites = @{
|
||||||
|
Hook = @{
|
||||||
|
Label = 'Hidden menu direct hook site'
|
||||||
|
Offset = 0x70D75
|
||||||
|
Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||||
|
Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||||
|
LegacyBadPatched = [byte[]](0x9A, 0x86, 0x9A, 0x0B, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90)
|
||||||
|
OriginalPatterns = @(
|
||||||
|
@('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02')
|
||||||
|
)
|
||||||
|
Fixup = @{
|
||||||
|
OperandOffset = 0x70D79
|
||||||
|
OriginalTargetSeg = 92
|
||||||
|
OriginalTargetOffset = 0x0476
|
||||||
|
PatchedTargetSeg = 117
|
||||||
|
PatchedTargetOffset = 0x0086
|
||||||
|
RetailSourceSegmentIndex = 39
|
||||||
|
RetailChainOffset = 0x2B79
|
||||||
|
RetailEntryOffset = 0x71D68
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Wrapper = @{
|
||||||
|
Label = 'Current-slot wrapper arg site'
|
||||||
|
Offset = 0xB9A8D
|
||||||
|
Original = [byte[]](0x6A, 0x01, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
|
||||||
|
Patched = [byte[]](0x6A, 0x01, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
|
||||||
|
OriginalPatterns = @(
|
||||||
|
@('6A', '01', 'FF', '76', '08', 'FF', '76', '06')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LegacyDeferredHook = @{
|
||||||
|
Label = 'Rejected deferred-event hook site'
|
||||||
|
Offset = 0xC99DD
|
||||||
|
Original = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||||
|
Patched = [byte[]](0x68, 0x03, 0x01, 0x9A, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xC4, 0x02)
|
||||||
|
OriginalPatterns = @(
|
||||||
|
@('68', '03', '01', '9A', 'FF', 'FF', '00', '00', '83', 'C4', '02')
|
||||||
|
)
|
||||||
|
Fixup = @{
|
||||||
|
OperandOffset = 0xC99E1
|
||||||
|
OriginalTargetSeg = 92
|
||||||
|
OriginalTargetOffset = 0x0476
|
||||||
|
PatchedTargetSeg = 117
|
||||||
|
PatchedTargetOffset = 0x020D
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LegacyDeferredWrapper = @{
|
||||||
|
Label = 'Rejected modal wrapper arg site'
|
||||||
|
Offset = 0xB9C48
|
||||||
|
Original = [byte[]](0x6A, 0x00, 0xFF, 0x76, 0x08, 0xFF, 0x76, 0x06)
|
||||||
|
Patched = [byte[]](0x6A, 0x00, 0x6A, 0x00, 0x6A, 0x00, 0x90, 0x90)
|
||||||
|
OriginalPatterns = @(
|
||||||
|
@('6A', '00', 'FF', '76', '08', 'FF', '76', '06')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-HexBytes {
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
|
||||||
|
return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ByteSlice {
|
||||||
|
param(
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[int]$Offset,
|
||||||
|
[int]$Count
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Offset -lt 0 -or ($Offset + $Count) -gt $Bytes.Length) {
|
||||||
|
throw "Requested byte range 0x{0:X}-0x{1:X} is outside the file." -f $Offset, ($Offset + $Count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
$slice = New-Object byte[] $Count
|
||||||
|
[Array]::Copy($Bytes, $Offset, $slice, 0, $Count)
|
||||||
|
return $slice
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-U16Le {
|
||||||
|
param(
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[int]$Offset
|
||||||
|
)
|
||||||
|
|
||||||
|
return [BitConverter]::ToUInt16($Bytes, $Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-U32Le {
|
||||||
|
param(
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[int]$Offset
|
||||||
|
)
|
||||||
|
|
||||||
|
return [BitConverter]::ToUInt32($Bytes, $Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-U16Le {
|
||||||
|
param(
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[int]$Offset,
|
||||||
|
[int]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
$raw = [BitConverter]::GetBytes([UInt16]$Value)
|
||||||
|
[Array]::Copy($raw, 0, $Bytes, $Offset, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-ByteArrayEqual {
|
||||||
|
param(
|
||||||
|
[byte[]]$Left,
|
||||||
|
[byte[]]$Right
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Left.Length -ne $Right.Length) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $Left.Length; $i++) {
|
||||||
|
if ($Left[$i] -ne $Right[$i]) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-BytePatternMatch {
|
||||||
|
param(
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[object[]]$Pattern
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($Bytes.Length -ne $Pattern.Length) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt $Bytes.Length; $i++) {
|
||||||
|
$expected = [string]$Pattern[$i]
|
||||||
|
if ($expected -eq '??') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Bytes[$i] -ne [Convert]::ToByte($expected, 16)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-BytesToStoredHex {
|
||||||
|
param([byte[]]$Bytes)
|
||||||
|
|
||||||
|
return (($Bytes | ForEach-Object { $_.ToString('X2') }) -join '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Convert-StoredHexToBytes {
|
||||||
|
param([string]$Hex)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Hex)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $Hex.Replace(' ', '')
|
||||||
|
if (($normalized.Length % 2) -ne 0) {
|
||||||
|
throw "Invalid stored hex string length: $Hex"
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = New-Object byte[] ($normalized.Length / 2)
|
||||||
|
for ($i = 0; $i -lt $result.Length; $i++) {
|
||||||
|
$result[$i] = [Convert]::ToByte($normalized.Substring($i * 2, 2), 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HookFixupInfo {
|
||||||
|
param(
|
||||||
|
[byte[]]$FileBytes,
|
||||||
|
[hashtable]$Site
|
||||||
|
)
|
||||||
|
|
||||||
|
$neHeaderOffset = [int](Get-U32Le -Bytes $FileBytes -Offset 0x3C)
|
||||||
|
$neSignature = Get-ByteSlice -Bytes $FileBytes -Offset $neHeaderOffset -Count 2
|
||||||
|
if ($neSignature[0] -ne 0x4E -or $neSignature[1] -ne 0x45) {
|
||||||
|
throw 'Executable does not contain a valid NE header at e_lfanew.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$numSegments = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x1C))
|
||||||
|
$segmentTableOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x22)) + $neHeaderOffset
|
||||||
|
$alignmentShift = [int](Get-U16Le -Bytes $FileBytes -Offset ($neHeaderOffset + 0x32))
|
||||||
|
|
||||||
|
$operandFileOffset = [int]$Site.Fixup.OperandOffset
|
||||||
|
$segmentInfoByIndex = @{}
|
||||||
|
$sourceSegment = $null
|
||||||
|
for ($index = 1; $index -le $numSegments; $index++) {
|
||||||
|
$segmentEntryOffset = $segmentTableOffset + (($index - 1) * 8)
|
||||||
|
$sectorOffset = [int](Get-U16Le -Bytes $FileBytes -Offset $segmentEntryOffset)
|
||||||
|
if ($sectorOffset -eq 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$segmentLength = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 2))
|
||||||
|
if ($segmentLength -eq 0) {
|
||||||
|
$segmentLength = 0x10000
|
||||||
|
}
|
||||||
|
|
||||||
|
$segmentFlags = [int](Get-U16Le -Bytes $FileBytes -Offset ($segmentEntryOffset + 4))
|
||||||
|
$segmentFileOffset = [int]($sectorOffset -shl $alignmentShift)
|
||||||
|
$segmentInfo = @{
|
||||||
|
SegmentIndex = $index
|
||||||
|
SegmentEntryOffset = $segmentEntryOffset
|
||||||
|
SegmentFileOffset = $segmentFileOffset
|
||||||
|
SegmentLength = $segmentLength
|
||||||
|
SegmentFlags = $segmentFlags
|
||||||
|
HasReloc = (($segmentFlags -band 0x0100) -ne 0)
|
||||||
|
}
|
||||||
|
$segmentInfoByIndex[$index] = $segmentInfo
|
||||||
|
|
||||||
|
if ($segmentFileOffset -le $operandFileOffset -and $operandFileOffset -lt ($segmentFileOffset + $segmentLength)) {
|
||||||
|
$sourceSegment = $segmentInfo
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $sourceSegment) {
|
||||||
|
$fallbackSegmentIndex = [int]$Site.Fixup.RetailSourceSegmentIndex
|
||||||
|
$fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset
|
||||||
|
if ($segmentInfoByIndex.ContainsKey($fallbackSegmentIndex)) {
|
||||||
|
$fallbackSegment = $segmentInfoByIndex[$fallbackSegmentIndex]
|
||||||
|
if (($fallbackSegment.SegmentFileOffset + $fallbackChainOffset) -eq $operandFileOffset) {
|
||||||
|
$sourceSegment = $fallbackSegment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($null -eq $sourceSegment) {
|
||||||
|
throw ("Could not locate the hook operand file offset 0x{0:X} in any NE code segment." -f $operandFileOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $sourceSegment.HasReloc) {
|
||||||
|
throw 'Hook source segment does not advertise a relocation table.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$segmentIndex = [int]$sourceSegment.SegmentIndex
|
||||||
|
$segmentFileOffset = [int]$sourceSegment.SegmentFileOffset
|
||||||
|
$segmentLength = [int]$sourceSegment.SegmentLength
|
||||||
|
$relocTableOffset = $segmentFileOffset + $segmentLength
|
||||||
|
$relocCount = [int](Get-U16Le -Bytes $FileBytes -Offset $relocTableOffset)
|
||||||
|
$entryOffset = $relocTableOffset + 2
|
||||||
|
$chainOffset = $operandFileOffset - $segmentFileOffset
|
||||||
|
|
||||||
|
for ($index = 0; $index -lt $relocCount; $index++) {
|
||||||
|
$addrType = $FileBytes[$entryOffset]
|
||||||
|
$relType = $FileBytes[$entryOffset + 1]
|
||||||
|
$entryChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 2)
|
||||||
|
if ($entryChainOffset -eq $chainOffset) {
|
||||||
|
return @{
|
||||||
|
SegmentIndex = $segmentIndex
|
||||||
|
SegmentFileOffset = $segmentFileOffset
|
||||||
|
RelocTableOffset = $relocTableOffset
|
||||||
|
EntryOffset = $entryOffset
|
||||||
|
EntryIndex = $index
|
||||||
|
AddrType = $addrType
|
||||||
|
RelType = $relType
|
||||||
|
ChainOffset = $entryChainOffset
|
||||||
|
TargetSeg = $FileBytes[$entryOffset + 4]
|
||||||
|
Reserved = $FileBytes[$entryOffset + 5]
|
||||||
|
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($addrType -eq 3 -and (($relType -band 0x03) -eq 0)) {
|
||||||
|
$visited = New-Object 'System.Collections.Generic.HashSet[int]'
|
||||||
|
$currentChainOffset = $entryChainOffset
|
||||||
|
while ($currentChainOffset -ne 0xFFFF -and $currentChainOffset -lt $segmentLength) {
|
||||||
|
if (-not $visited.Add($currentChainOffset)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentChainOffset -eq $chainOffset) {
|
||||||
|
return @{
|
||||||
|
SegmentIndex = $segmentIndex
|
||||||
|
SegmentFileOffset = $segmentFileOffset
|
||||||
|
RelocTableOffset = $relocTableOffset
|
||||||
|
EntryOffset = $entryOffset
|
||||||
|
EntryIndex = $index
|
||||||
|
AddrType = $addrType
|
||||||
|
RelType = $relType
|
||||||
|
ChainOffset = $entryChainOffset
|
||||||
|
TargetSeg = $FileBytes[$entryOffset + 4]
|
||||||
|
Reserved = $FileBytes[$entryOffset + 5]
|
||||||
|
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($entryOffset + 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentChainOffset = Get-U16Le -Bytes $FileBytes -Offset ($segmentFileOffset + $currentChainOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entryOffset += 8
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallbackEntryOffset = [int]$Site.Fixup.RetailEntryOffset
|
||||||
|
$fallbackChainOffset = [int]$Site.Fixup.RetailChainOffset
|
||||||
|
if ($fallbackEntryOffset -ge 0 -and ($fallbackEntryOffset + 7) -lt $FileBytes.Length) {
|
||||||
|
$addrType = $FileBytes[$fallbackEntryOffset]
|
||||||
|
$relType = $FileBytes[$fallbackEntryOffset + 1]
|
||||||
|
$entryChainOffset = [int](Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 2))
|
||||||
|
if (
|
||||||
|
$sourceSegment.SegmentIndex -eq [int]$Site.Fixup.RetailSourceSegmentIndex -and
|
||||||
|
$chainOffset -eq $fallbackChainOffset -and
|
||||||
|
$addrType -eq 3 -and
|
||||||
|
(($relType -band 0x03) -eq 0) -and
|
||||||
|
$entryChainOffset -eq $fallbackChainOffset
|
||||||
|
) {
|
||||||
|
return @{
|
||||||
|
SegmentIndex = $segmentIndex
|
||||||
|
SegmentFileOffset = $segmentFileOffset
|
||||||
|
RelocTableOffset = $relocTableOffset
|
||||||
|
EntryOffset = $fallbackEntryOffset
|
||||||
|
EntryIndex = -1
|
||||||
|
AddrType = $addrType
|
||||||
|
RelType = $relType
|
||||||
|
ChainOffset = $entryChainOffset
|
||||||
|
TargetSeg = $FileBytes[$fallbackEntryOffset + 4]
|
||||||
|
Reserved = $FileBytes[$fallbackEntryOffset + 5]
|
||||||
|
TargetOffset = Get-U16Le -Bytes $FileBytes -Offset ($fallbackEntryOffset + 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ("Could not find hook relocation entry for segment {0} chain offset 0x{1:X}." -f $segmentIndex, $chainOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-StateData {
|
||||||
|
if (-not (Test-Path -LiteralPath $statePath)) {
|
||||||
|
return @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = Get-Content -LiteralPath $statePath -Raw
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||||
|
return @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = ConvertFrom-Json -InputObject $raw -AsHashtable
|
||||||
|
if ($null -eq $parsed) {
|
||||||
|
return @{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-StateData {
|
||||||
|
param([hashtable]$StateData)
|
||||||
|
|
||||||
|
$json = ConvertTo-Json -InputObject $StateData -Depth 4
|
||||||
|
Set-Content -LiteralPath $statePath -Value $json -Encoding ASCII
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SavedOriginalBytes {
|
||||||
|
param(
|
||||||
|
[hashtable]$StateData,
|
||||||
|
[string]$SiteKey,
|
||||||
|
[int]$SiteOffset
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not $StateData.ContainsKey($SiteKey)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteState = $StateData[$SiteKey]
|
||||||
|
if ($siteState -isnot [hashtable]) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $siteState.ContainsKey('OriginalBytes')) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($siteState.ContainsKey('Offset') -and ([int]$siteState.Offset -ne $SiteOffset)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Convert-StoredHexToBytes -Hex ([string]$siteState.OriginalBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Save-OriginalBytesIfMissing {
|
||||||
|
param(
|
||||||
|
[hashtable]$StateData,
|
||||||
|
[string]$SiteKey,
|
||||||
|
[int]$SiteOffset,
|
||||||
|
[byte[]]$Bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($StateData.ContainsKey($SiteKey) -and ($StateData[$SiteKey] -is [hashtable])) {
|
||||||
|
$existing = $StateData[$SiteKey]
|
||||||
|
if ($existing.ContainsKey('OriginalBytes') -and $existing.ContainsKey('Offset') -and ([int]$existing.Offset -eq $SiteOffset)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$StateData[$SiteKey] = @{
|
||||||
|
Offset = $SiteOffset
|
||||||
|
OriginalBytes = Convert-BytesToStoredHex -Bytes $Bytes
|
||||||
|
}
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SiteState {
|
||||||
|
param(
|
||||||
|
[byte[]]$FileBytes,
|
||||||
|
[hashtable]$Site
|
||||||
|
)
|
||||||
|
|
||||||
|
$current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length
|
||||||
|
if ($Site.ContainsKey('Fixup')) {
|
||||||
|
$fixupInfo = Get-HookFixupInfo -FileBytes $FileBytes -Site $Site
|
||||||
|
$isFixupOriginal = ($fixupInfo.TargetSeg -eq $Site.Fixup.OriginalTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.OriginalTargetOffset)
|
||||||
|
$isFixupPatched = ($fixupInfo.TargetSeg -eq $Site.Fixup.PatchedTargetSeg) -and ($fixupInfo.TargetOffset -eq $Site.Fixup.PatchedTargetOffset)
|
||||||
|
|
||||||
|
if ($Site.ContainsKey('LegacyBadPatched') -and (Test-ByteArrayEqual -Left $current -Right $Site.LegacyBadPatched) -and $isFixupOriginal) {
|
||||||
|
return 'LegacyBadPatch'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-ByteArrayEqual -Left $current -Right $Site.Patched) -and $isFixupPatched) {
|
||||||
|
return 'Patched'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-ByteArrayEqual -Left $current -Right $Site.Original) -and $isFixupOriginal) {
|
||||||
|
return 'Original'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pattern in $Site.OriginalPatterns) {
|
||||||
|
if ((Test-BytePatternMatch -Bytes $current -Pattern $pattern) -and $isFixupOriginal) {
|
||||||
|
return 'Original'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-ByteArrayEqual -Left $current -Right $Site.Patched) {
|
||||||
|
return 'Patched'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-ByteArrayEqual -Left $current -Right $Site.Original) {
|
||||||
|
return 'Original'
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($pattern in $Site.OriginalPatterns) {
|
||||||
|
if (Test-BytePatternMatch -Bytes $current -Pattern $pattern) {
|
||||||
|
return 'Original'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-SiteStateKnown {
|
||||||
|
param(
|
||||||
|
[byte[]]$FileBytes,
|
||||||
|
[hashtable]$Site
|
||||||
|
)
|
||||||
|
|
||||||
|
$state = Get-SiteState -FileBytes $FileBytes -Site $Site
|
||||||
|
if ($state -eq 'Unknown') {
|
||||||
|
$current = Get-ByteSlice -Bytes $FileBytes -Offset $Site.Offset -Count $Site.Original.Length
|
||||||
|
throw (
|
||||||
|
"{0} at file offset 0x{1:X} does not match either expected byte sequence.`nCurrent : {2}`nOriginal: {3}`nPatched : {4}`n`nRefusing to modify an unknown executable state." -f
|
||||||
|
$Site.Label,
|
||||||
|
$Site.Offset,
|
||||||
|
(Format-HexBytes -Bytes $current),
|
||||||
|
(Format-HexBytes -Bytes $Site.Original),
|
||||||
|
(Format-HexBytes -Bytes $Site.Patched)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-ByteSlice {
|
||||||
|
param(
|
||||||
|
[byte[]]$Bytes,
|
||||||
|
[int]$Offset,
|
||||||
|
[byte[]]$Value
|
||||||
|
)
|
||||||
|
|
||||||
|
[Array]::Copy($Value, 0, $Bytes, $Offset, $Value.Length)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Show-Status {
|
||||||
|
param([byte[]]$FileBytes)
|
||||||
|
|
||||||
|
$hookState = Get-SiteState -FileBytes $FileBytes -Site $sites.Hook
|
||||||
|
$wrapperState = Get-SiteState -FileBytes $FileBytes -Site $sites.Wrapper
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'CRUSADER.EXE patch status'
|
||||||
|
Write-Host '------------------------'
|
||||||
|
Write-Host ("EXE: {0}" -f $exePath)
|
||||||
|
Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||||
|
Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host '1. Apply supported hidden-menu patch (aliases to Experiment B)'
|
||||||
|
Write-Host '2. Apply Experiment B (retarget + modal arg fix)'
|
||||||
|
Write-Host '3. Restore original bytes'
|
||||||
|
Write-Host '4. Exit'
|
||||||
|
Write-Host ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-DesiredState {
|
||||||
|
param(
|
||||||
|
[bool]$HookPatched,
|
||||||
|
[bool]$WrapperPatched,
|
||||||
|
[string]$Label
|
||||||
|
)
|
||||||
|
|
||||||
|
$fileBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||||
|
$stateData = Get-StateData
|
||||||
|
|
||||||
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Hook
|
||||||
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.Wrapper
|
||||||
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
||||||
|
Assert-SiteStateKnown -FileBytes $fileBytes -Site $sites.LegacyDeferredWrapper
|
||||||
|
|
||||||
|
$hookCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Count $sites.Hook.Original.Length
|
||||||
|
$hookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.Hook
|
||||||
|
$wrapperCurrent = Get-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Count $sites.Wrapper.Original.Length
|
||||||
|
$legacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $fileBytes -Site $sites.LegacyDeferredHook
|
||||||
|
$hookCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Hook
|
||||||
|
$wrapperCurrentState = Get-SiteState -FileBytes $fileBytes -Site $sites.Wrapper
|
||||||
|
|
||||||
|
$stateChanged = $false
|
||||||
|
if ($hookCurrentState -eq 'Original') {
|
||||||
|
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset -Bytes $hookCurrent) -or $stateChanged
|
||||||
|
if (-not $stateData.ContainsKey('HookFixup')) {
|
||||||
|
$stateData['HookFixup'] = @{
|
||||||
|
TargetSeg = $hookFixupInfo.TargetSeg
|
||||||
|
TargetOffset = $hookFixupInfo.TargetOffset
|
||||||
|
Reserved = $hookFixupInfo.Reserved
|
||||||
|
}
|
||||||
|
$stateChanged = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($wrapperCurrentState -eq 'Original') {
|
||||||
|
$stateChanged = (Save-OriginalBytesIfMissing -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset -Bytes $wrapperCurrent) -or $stateChanged
|
||||||
|
}
|
||||||
|
if ($stateChanged) {
|
||||||
|
Save-StateData -StateData $stateData
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($HookPatched) {
|
||||||
|
$hookBytes = $sites.Hook.Patched
|
||||||
|
$hookTargetSeg = [int]$sites.Hook.Fixup.PatchedTargetSeg
|
||||||
|
$hookTargetOffset = [int]$sites.Hook.Fixup.PatchedTargetOffset
|
||||||
|
$hookReserved = 0
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$hookBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Hook' -SiteOffset $sites.Hook.Offset
|
||||||
|
if ($null -eq $hookBytes) {
|
||||||
|
if ($hookCurrentState -eq 'Original' -or $hookCurrentState -eq 'LegacyBadPatch') {
|
||||||
|
$hookBytes = $hookCurrent
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw 'No saved original bytes are available for the Experiment A hook site. Restore requires either a prior patch run with this script or your full executable backup.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stateData.ContainsKey('HookFixup')) {
|
||||||
|
$hookTargetSeg = [int]$stateData['HookFixup'].TargetSeg
|
||||||
|
$hookTargetOffset = [int]$stateData['HookFixup'].TargetOffset
|
||||||
|
$hookReserved = [int]$stateData['HookFixup'].Reserved
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$hookTargetSeg = [int]$sites.Hook.Fixup.OriginalTargetSeg
|
||||||
|
$hookTargetOffset = [int]$sites.Hook.Fixup.OriginalTargetOffset
|
||||||
|
$hookReserved = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($WrapperPatched) {
|
||||||
|
$wrapperBytes = $sites.Wrapper.Patched
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$wrapperBytes = Get-SavedOriginalBytes -StateData $stateData -SiteKey 'Wrapper' -SiteOffset $sites.Wrapper.Offset
|
||||||
|
if ($null -eq $wrapperBytes) {
|
||||||
|
if ($wrapperCurrentState -eq 'Original') {
|
||||||
|
$wrapperBytes = $wrapperCurrent
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw 'No saved original bytes are available for the Experiment B wrapper site. Restore requires either a prior patch run with this script or your full executable backup.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$legacyDeferredHookBytes = $sites.LegacyDeferredHook.Original
|
||||||
|
$legacyDeferredHookTargetSeg = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetSeg
|
||||||
|
$legacyDeferredHookTargetOffset = [int]$sites.LegacyDeferredHook.Fixup.OriginalTargetOffset
|
||||||
|
$legacyDeferredHookReserved = 0
|
||||||
|
$legacyDeferredWrapperBytes = $sites.LegacyDeferredWrapper.Original
|
||||||
|
|
||||||
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Hook.Offset -Value $hookBytes
|
||||||
|
$fileBytes[$hookFixupInfo.EntryOffset + 4] = [byte]$hookTargetSeg
|
||||||
|
$fileBytes[$hookFixupInfo.EntryOffset + 5] = [byte]$hookReserved
|
||||||
|
Set-U16Le -Bytes $fileBytes -Offset ($hookFixupInfo.EntryOffset + 6) -Value $hookTargetOffset
|
||||||
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.Wrapper.Offset -Value $wrapperBytes
|
||||||
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredHook.Offset -Value $legacyDeferredHookBytes
|
||||||
|
$fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 4] = [byte]$legacyDeferredHookTargetSeg
|
||||||
|
$fileBytes[$legacyDeferredHookFixupInfo.EntryOffset + 5] = [byte]$legacyDeferredHookReserved
|
||||||
|
Set-U16Le -Bytes $fileBytes -Offset ($legacyDeferredHookFixupInfo.EntryOffset + 6) -Value $legacyDeferredHookTargetOffset
|
||||||
|
Set-ByteSlice -Bytes $fileBytes -Offset $sites.LegacyDeferredWrapper.Offset -Value $legacyDeferredWrapperBytes
|
||||||
|
|
||||||
|
[System.IO.File]::WriteAllBytes($exePath, $fileBytes)
|
||||||
|
|
||||||
|
$verifyBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||||
|
$hookState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Hook
|
||||||
|
$wrapperState = Get-SiteState -FileBytes $verifyBytes -Site $sites.Wrapper
|
||||||
|
$verifiedHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.Hook
|
||||||
|
$verifiedLegacyDeferredHookFixupInfo = Get-HookFixupInfo -FileBytes $verifyBytes -Site $sites.LegacyDeferredHook
|
||||||
|
$verifiedHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Hook.Offset -Count $hookBytes.Length
|
||||||
|
$verifiedWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.Wrapper.Offset -Count $wrapperBytes.Length
|
||||||
|
$verifiedLegacyDeferredHookBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredHook.Offset -Count $legacyDeferredHookBytes.Length
|
||||||
|
$verifiedLegacyDeferredWrapperBytes = Get-ByteSlice -Bytes $verifyBytes -Offset $sites.LegacyDeferredWrapper.Offset -Count $legacyDeferredWrapperBytes.Length
|
||||||
|
|
||||||
|
if (-not (Test-ByteArrayEqual -Left $verifiedHookBytes -Right $hookBytes)) {
|
||||||
|
throw 'Hook-site verification failed after write.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($verifiedHookFixupInfo.TargetSeg -ne $hookTargetSeg -or $verifiedHookFixupInfo.TargetOffset -ne $hookTargetOffset) {
|
||||||
|
throw 'Hook relocation verification failed after write.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ByteArrayEqual -Left $verifiedWrapperBytes -Right $wrapperBytes)) {
|
||||||
|
throw 'Wrapper-site verification failed after write.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredHookBytes -Right $legacyDeferredHookBytes)) {
|
||||||
|
throw 'Rejected deferred-event hook cleanup verification failed after write.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($verifiedLegacyDeferredHookFixupInfo.TargetSeg -ne $legacyDeferredHookTargetSeg -or $verifiedLegacyDeferredHookFixupInfo.TargetOffset -ne $legacyDeferredHookTargetOffset) {
|
||||||
|
throw 'Rejected deferred-event relocation cleanup verification failed after write.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ByteArrayEqual -Left $verifiedLegacyDeferredWrapperBytes -Right $legacyDeferredWrapperBytes)) {
|
||||||
|
throw 'Rejected modal-wrapper cleanup verification failed after write.'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host ("Applied: {0}" -f $Label)
|
||||||
|
Write-Host ("A hook @ 0x{0:X}: {1}" -f $sites.Hook.Offset, $hookState)
|
||||||
|
Write-Host ("B wrapper @ 0x{0:X}: {1}" -f $sites.Wrapper.Offset, $wrapperState)
|
||||||
|
Write-Host ''
|
||||||
|
Write-Host 'What this means:'
|
||||||
|
Write-Host '- Experiment A retargets the existing cheat-success far call into cheat_menu_open_from_current_slot while keeping the original event-dispatch framing.'
|
||||||
|
Write-Host '- Experiment B preserves the wrapper mode byte `1` but forces the two ambiguous 16-bit constructor parameters to zero instead of inheriting arbitrary caller-frame values.'
|
||||||
|
Write-Host '- Restore also cleans up the rejected deferred-event patch sites if they were left behind by earlier attempts.'
|
||||||
|
Write-Host ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-MenuChoice {
|
||||||
|
param([string]$SelectedChoice)
|
||||||
|
|
||||||
|
switch ($SelectedChoice.Trim()) {
|
||||||
|
'1' {
|
||||||
|
Write-Warning 'Experiment A alone is not supported on the cheat-code path. Applying the safer Experiment B patch instead.'
|
||||||
|
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (via menu option 1 alias)'
|
||||||
|
}
|
||||||
|
'2' {
|
||||||
|
Set-DesiredState -HookPatched $true -WrapperPatched $true -Label 'Experiment B (A + B)'
|
||||||
|
}
|
||||||
|
'3' {
|
||||||
|
Set-DesiredState -HookPatched $false -WrapperPatched $false -Label 'Restore original bytes'
|
||||||
|
}
|
||||||
|
'4' {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Warning 'Invalid selection.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $exePath)) {
|
||||||
|
throw "CRUSADER.EXE was not found next to the script. Put this .ps1 file in the same folder as CRUSADER.EXE."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($PSBoundParameters.ContainsKey('Choice')) {
|
||||||
|
[void](Invoke-MenuChoice -SelectedChoice $Choice)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
:mainloop while ($true) {
|
||||||
|
$currentBytes = [System.IO.File]::ReadAllBytes($exePath)
|
||||||
|
Show-Status -FileBytes $currentBytes
|
||||||
|
$choice = Read-Host 'Select 1, 2, 3, or 4'
|
||||||
|
if ([string]::IsNullOrEmpty($choice)) { break mainloop }
|
||||||
|
|
||||||
|
if (-not (Invoke-MenuChoice -SelectedChoice $choice)) {
|
||||||
|
break mainloop
|
||||||
|
}
|
||||||
|
}
|
||||||
103
plan-mid.md
103
plan-mid.md
|
|
@ -15,18 +15,21 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
|
||||||
|
|
||||||
## Progress Snapshot
|
## Progress Snapshot
|
||||||
|
|
||||||
- Overall useful decompilation progress: about 42%
|
- Overall useful decompilation progress: about 49%
|
||||||
- Reasonable uncertainty band: about 36% to 45%
|
- Reasonable uncertainty band: about 44% to 52%
|
||||||
- Top 100 far-call target coverage: about 80%
|
- Top 100 far-call target coverage: about 80%
|
||||||
- Segment spread with meaningful analysis: about 22% to 28%
|
- Segment spread with meaningful analysis: about 26% to 32%
|
||||||
- Tooling maturity for continued work: about 75%
|
- Tooling maturity for continued work: about 77%
|
||||||
|
|
||||||
### Why The Estimate Stays Here
|
### Why The Estimate Moves Slightly
|
||||||
|
|
||||||
- Recent work materially improved semantic confidence inside the startup/display, cache/allocator, callback-object, and USECODE/VM lanes.
|
- Recent work materially improved semantic confidence inside the startup/display, cache/allocator, callback-object, and USECODE/VM lanes.
|
||||||
- The startup/display lane now has a verified owner split: `g_active_dispatch_entry_farptr[+0x40]` is a borrowed shared presentation hold token, while the seg108 `0x4f38` bit-`0x40` lane stays local to the sprite/object stack.
|
- The startup/display lane is now materially complete as an active major section: the shared `g_active_dispatch_entry_farptr[+0x40]` hold token is separated from the seg108-local `0x4f38` bit-`0x40` lane, the seg126 control stream is confirmed as file-backed, the paired `0x8c5c/0x8c60` renderer objects are narrowed to two script-selected preset text lanes, and the neighboring seg127 fade controller now has an exact local contract at `0x630a..0x6316`.
|
||||||
- The seg126 control stream is now tighter at the producer side too: the traced setup path still supports a shared-base-path file selector feeding a full external script/control buffer, the `0x6aa:0x6ac` base now reads as an inherited external/default path buffer rather than a stronger in-code producer, and the in-scope `0x31a2` transition/presentation readers are now classified by role.
|
- The current VM/loader batch also justified a small bump: `000d:ebe3` is now a named ordered opcode sequencer with a tighter entry/exit contract, the masked-create hub at `000d:463a` is now a verified owner-table gate rather than an inferred wrapper sink, and the seg070 twin loops under `entity_vm_runtime_owner_resource_create` now read as paired file-family loaders writing into separate temporary buffers rather than one ambiguous callback shard.
|
||||||
- That work reduced ambiguity inside already-active clusters more than it expanded whole-program breadth, so the headline only moves modestly.
|
- The latest USECODE pass justified another small VM-lane bump: the gameplay-side wrapper ladder now extends through slots `0x10..0x14` with verified mixed payload shapes (`none` vs extra signed word), the new slot-only Ghidra names keep that taxonomy visible without overpromoting event labels, and the `000d:22bc` stage is now comment-backed as a sequencer-internal link-matrix/pushback consumer over decoded workspace bytes rather than a direct descriptor-row reader.
|
||||||
|
- The immortality follow-up justified another small tooling-and-confidence bump: the extractor now emits a dedicated target-body scan, the strongest current USECODE candidates show no inline `0x410` / `0x00000410` literal, and the remaining frontier is narrowed to data-driven decoding of `EVENT` slot `0x0a` plus `NPCTRIG` slots `0x0a` / `0x20` rather than the older wider trigger family set.
|
||||||
|
- The latest owner-loaded range pass justified another small confidence bump too: the owner-resource child selector now matches extracted `class_id + 2` exactly, the class header/subentry math at `000d:5066/51fd/53b4` is closed against the extractor's raw headers and event rows, and the surviving immortality uncertainty has moved from `can the loader fit NPCTRIG arithmetic at all?` to the narrower `which class family is actually selected upstream?` question.
|
||||||
|
- That closes one live top-priority section and justifies a small headline increase even though the remaining work is still breadth-heavy.
|
||||||
|
|
||||||
## Current Verified State
|
## Current Verified State
|
||||||
|
|
||||||
|
|
@ -43,44 +46,71 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
|
||||||
- 0008 dispatch-entry helpers and 000c state-machine helpers have broad partial coverage.
|
- 0008 dispatch-entry helpers and 000c state-machine helpers have broad partial coverage.
|
||||||
- 000a/000d tracked-handle, cache, allocator, dispatch-entry, and startup/display support lanes now have a coherent partial map.
|
- 000a/000d tracked-handle, cache, allocator, dispatch-entry, and startup/display support lanes now have a coherent partial map.
|
||||||
- 000e parser and animation subsystems have a real partial map.
|
- 000e parser and animation subsystems have a real partial map.
|
||||||
- The USECODE/VM owner/resource/runtime lane now has a workable partial model plus supporting extraction/reporting tooling.
|
- The USECODE/VM owner/resource/runtime lane now has a workable partial model, a named sequencer entry, paired external file-family loader evidence, and supporting extraction/reporting tooling.
|
||||||
|
- The USECODE/VM tooling lane now also has a concrete near-term implementation path: a Pentagram-derived proof-of-concept parser can reuse opcode decoding while swapping in the locally verified owner-loaded class and slot arithmetic, with a hybrid Ghidra comment/bookmark import path instead of a premature custom processor module.
|
||||||
|
- The USECODE/VM lane now also has a verified generic masked-context creation hub (`000d:463a`) plus two concrete sequencer-internal consumer blocks (`000d:208b`, `000d:21ed`) built directly on `entity_vm_context_create_from_slot_index`.
|
||||||
|
- The USECODE/VM lane now also has first caller-role evidence outside the older seg021 wrapper island: the new seg004 callers keep masks `0x8000:0x0007` and `0x2000:0x0015` in gameplay-side materialization lanes, while the newly named seg006 helpers now separate one extra-word masked lane with a real local class-state transition fallback (`0x0008:0x0030`) from a guarded `0x0010:0x0008` materializer that simply returns `0` on miss after readiness checks.
|
||||||
|
- The USECODE/VM lane now also has a wider verified higher-slot wrapper ladder: the `0005` island reaches slot ordinals `0x10..0x14`, slot `0x12` is a zero-extra-word lane, slots `0x11/0x13/0x14` carry extra-word payloads, and the current safest read is `slot-stable payload-shape taxonomy` rather than direct event-name promotion.
|
||||||
|
- The same higher-slot batch now has its first outward binary anchors: slot `0x12` wrapper `0005:3171` is directly called at `0005:1776` and `0005:1945`, the slot `0x10` guarded lane at `0005:3115..3129` is still fenced by the `0005:30f2..3113` class-nibble-`4` check, and the dark slot `0x0a` / `0x0b` wrappers are now instruction-verified as exact signed-additive shims over masks `0x00000400` / `0x00000800` even though their outward callers remain unrecovered.
|
||||||
|
- The compiled-side immortality lane is slightly tighter too: `000b:b3b1` / `000b:b62c` are now a cheat-event listener constructor/handler pair for the shared cheat/control bundle rather than a hidden `0x410` producer, and the extractor-side `TELEPAD` slot-`0x20` `raw_code_offset = 0x00000410` hit is closed as an offset collision rather than direct immortality evidence.
|
||||||
|
- The compiled-side immortality lane is tighter again after the follow-up pass: `000c:8a62 -> 000c:8c56` is now a verified generic event-object dispatcher reading the emitted event id from field `+0x6`, seg109 helper `000b:3d2a` is now comment-backed as generic listener-registration infrastructure rather than an emitter, and the strongest remaining player-trigger family is the event-bearing `NPCTRIG` / `EVENT` neighborhood rather than `TRIGPAD`, `SPECIAL`, `REB_PAD`, or `TELEPAD`.
|
||||||
|
- The immortality lane is tighter again after the extractor extension: generated report `USECODE/EUSECODE_extracted/immortality_target_body_scan.md` now proves that `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `SPECIAL`, and `TRIGPAD` bodies contain no inline little-endian `0x0410`, no dword `0x00000410`, and no byte-swapped `0x1004`; the best surviving frontier is now the monolithic `EVENT` slot `0x0a` body plus compact `NPCTRIG` slots `0x0a` / `0x20`.
|
||||||
|
- The immortality lane is tighter again after the structure pass: new report `USECODE/EUSECODE_extracted/immortality_body_structure.md` now shows `EVENT` slot `0x0a` as a broad hub clause stream (`90` internal `0x53 0x5c <u16> EVENT` subheaders, `383` local labels, wide `event/item/source/dest/door/counter/counter2/link/time/post1/post2/floor/flicMan` tail), while `NPCTRIG` stays compact (`5` subheaders for slot `0x0a`, `1` for slot `0x20`, with narrow `referent/event/item/item2` vs `referent/typeNpc/item/item2` tails). Current best surviving emitter frontier is therefore `NPCTRIG` slot `0x0a` with `NPCTRIG` slot `0x20` as its nearest typed/setup companion, while `EVENT` now reads more like the generic hub body behind the same active-event lane.
|
||||||
|
- The immortality lane is tighter again after the clause pass: new report `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` now fixes the open-header decode (`NPCTRIG 0x0a` event-code byte `0x11`, `NPCTRIG 0x20` event-code byte `0x01`) and shows slot `0x0a` as a five-step fixed-width clause ladder (`0x2f` subheader stride, backward-walking `0x2f` targets, per-clause `branch_3f_0a` + `push_24_51` + `writeback_57_02` motifs) while slot `0x20` stays typeNpc-heavy (`10` `field_4b_fe_0f` hits, no `push_24_51`, no `writeback_57_02`). The best remaining descriptor-side frontier is therefore no longer the `NPCTRIG` pair symmetrically; it is specifically `NPCTRIG` slot `0x0a` as the live event-bearing ladder, with slot `0x20` as a typed/setup companion body.
|
||||||
|
- The immortality lane is tighter again after the runtime-fit follow-up: the regenerated clause report now records the per-clause motif offsets and the selector-family fit against `000d:21ed -> 000d:22bc`. `000d:5572` proves the extra word carried by `0005:2c35` is additive (`slot_value + offset`), `000d:21ed` now has an exact `A x B` matrix contract (byte A = lead-word row count, byte B = shared target-list width), and `NPCTRIG` slot `0x0a` is the only surviving compact body that exposes a natural five-row additive selector family (`0x0064/0x0093/0x00c2/0x00f1/0x0120`, uniform stride `0x2f`) instead of a one-clause typeNpc gate.
|
||||||
|
- The immortality caller-path follow-up tightened the runtime bridge again: MCP xrefs now show only three entries into `entity_vm_context_create_from_slot_index` (`000d:46ac`, `000d:208b`, `000d:21ed`), while `0005:2c35` itself still has no recovered code or data xrefs. Stack setup at `000d:208b` hardcodes the `000d:5572` additive word to `0`, which does not match the `NPCTRIG` slot `0x0a` clause-start or target families. The remaining live selector frontier is therefore the still-overlapped `000d:21ed` caller frame rather than a normal caller of `0005:2c35`.
|
||||||
|
- The immortality downstream-use follow-up weakens the remaining direct-selector hypothesis again: `000d:46ec` stores the dynamic word from the `000d:21ed` lane into context field `+0x34`, but `000d:21ed -> 000d:22bc` never rereads `+0x34` or `+0x32` after creation. The durable uses are the object save/load path instead: `000d:498f` serializes only the derived low word at `+0x10c`, `000d:4a78` reloads that saved word as the additive argument to `000d:5572`, and `000d:4c2d..4c4d` rebuilds `+0x10c/+0x10e` from the live slot value plus that saved offset. The only recovered post-load consumers are a tiny sentinel predicate (`FUN_0001_a772` checks for exactly `0000:0001`) and a normalization block (`FUN_0002_1860` clamps `0000:xxxx` values below `0x0080` up to `0x0080`). No recovered compare or dispatch branch matches the `NPCTRIG` slot `0x0a` clause-start or target families, so the direct derived-value fit is weaker again.
|
||||||
|
- The persisted-context contract is tighter again after the latest pass: `entity_vm_context_save` (`000d:498f`) serializes `+0x11f`, `+0x121`, `+0x10c`, `+0x34`, and the `0x80`-byte local buffer, while `entity_vm_context_load` (`000d:4a78`) rebuilds the frame pointers, replays `entity_vm_slot_load_value_plus_offset` from saved `(slot, additive_word)`, restores `+0x10c/+0x10e`, and refreshes owner-source pair `+0x117/+0x119`. That is stronger evidence for `post-selector persistence of derived value state` than for any hidden upstream class discriminator.
|
||||||
|
- The immortality upstream-source follow-up removes most of the caller-frame ambiguity. Direct program-memory bytes for `000d:2131..21ed` now show the hidden pre-call layout explicitly: the seeded `+0xd6/+0xd8` stream is consumed as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, and `000d:21d0` pushes `add_a + add_b` as the dynamic word later stored at context `+0x34`. The same window now proves the caller-side frame shape too: frame base is `caller + [caller+0xd4]`, `[frame+0x0a/+0x0c]` is the far pointer passed into `entity_vm_context_setup`, and `[frame+0x0e..]` is a separate inline tail blob copied after creation. That rules out runtime owner-table fields or raw caller-object fields as the immediate source of `+0x34` and reframes the open question one level earlier: where that frame-local far pointer is seeded from, and whether the summed stream pair still maps to `NPCTRIG` slot `0x0a` clause-base/delta structure or only to a more generic descriptor-relative offset pair.
|
||||||
|
- The immortality frame-producer follow-up narrows the upstream writer one step further. Raw bytes at `000c:fbf7..fc47` (`caseD_0`) now show the nearest non-overlapped producer reading one signed placement byte from the seeded `+0xd6/+0xd8` stream, popping a far-pointer dword from the caller stream at `[caller+0xcc/+0xce]`, computing `frame_base = caller + [caller+0xd4]`, and storing that dword at `[frame_base + placement + 0x4/+0x6]`. That means the `000d:21ed` source lane is immediately caller-stream-backed rather than owner-row-backed; if its consumed `[frame+0x0a/+0x0c]` pair comes from this family, the relevant placement byte is `0x0006`, and any surviving `NPCTRIG` linkage must already have been predecoded into the generic caller stream before the frame record is materialized.
|
||||||
|
- The next producer-path pass tightens that split again. `000d:46ec -> 000c:f844 -> 000c:f6e8` now shows that a new context's `+0xcc/+0xce` stream is seeded by copying a caller-supplied setup blob into the object-local buffer, while the slot/additive record from `entity_vm_slot_load_value_plus_offset` seeds the separate `+0xd6/+0xd8` lane and the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` is mirrored separately through `0x39ca`. Linear raw-byte recovery across `000c:f98b..000d:000d` also closes the forward/reverse frame-record family around that lane: `000c:fc4b..fcbb` is the caller-stream -> frame blob producer that best matches inline-tail placement `0x000a`, while `000c:ff1f..ff83` is the frame -> caller-stream dword copier matching the `000c:fbf7..fc47` far-pointer writer at placement `0x0006`. The surviving open question is therefore narrower again: not which generic parent-frame materializer exists, but where the first non-recursive decoder originates the setup far pointer before this `ff1f/ff9f -> fbf7/fc4b -> 000d:21ed` propagation chain repeats it, and whether that origin still maps specifically to `NPCTRIG` slot `0x0a` or to a broader predecoded VM workspace.
|
||||||
|
- The next immortality pass closes the immediate far-pointer source classification too. Hidden raw bytes at `000c:fa2f..fa5b` recover an inner opcode dispatcher on the seeded `+0xd6/+0xd8` lane, and the same case family now exposes non-recursive caller-stream seeders at `000c:fd51`, `000c:fd91`, `000c:fdd1`, and `000c:fe11`. The dword case at `000c:fe11..fe59` reads an inline dword literal from that control stream, subtracts `4` from `[caller+0xcc]`, and writes the literal dword onto the caller stream before the recursive `ff1f/fbf7` replay family touches it. That means the immediate compiled-side source for the `000d:21ed` setup far pointer is now an inline VM control-stream literal, not an owner-row lookup or generic scratch buffer; any surviving `NPCTRIG` tie has to explain how slot `0x0a` is decoded into that literal-bearing stream upstream, while slot `0x20` still reads as the typed/setup companion body.
|
||||||
|
- The next immortality pass separates that literal-bearing stream from the owner-row path cleanly enough to retune the working model. Instruction recovery at `000d:46ec` now shows the owner-table row `(+0x10/+0x12) + 0x0d*slot + 4` feeding only the separate `0x39ca[slot]` mirror, while the live `+0xd6/+0xd8` control stream passed into `entity_vm_context_setup` continues to come from `entity_vm_slot_load_value_plus_offset`. The hidden `000d:21ed` pre-call span is now explicit as `word slot_index`, `word add_a`, `word add_b`, `byte setup_len`, `byte inline_len`, and the `000c:fa2f` case family now separates immediate literal seeders (`000c:fd51` byte, `000c:fd91` sign-extended byte->word, `000c:fdd1` word, `000c:fe11` dword) from the recursive replay stages (`000c:ff1f`, `000c:ff9f`). Current best read is therefore `decoded per-slot VM workspace plus frame replay`, not `direct NPCTRIG clause stream`, even though `NPCTRIG` slot `0x0a` remains the strongest surviving upstream descriptor family and slot `0x20` still reads as the typed/setup companion.
|
||||||
|
- The next immortality pass closes the workspace-materialization side of that boundary too. `entity_vm_slot_load_value` (`000d:51fd`) is now instruction-verified as the first concrete writer of the later `+0xd6/+0xd8` buffer on a cache miss: `000d:5066` loads a slot header plus cached `6`-byte subentry table through the owner-resource wrapper `000d:714c`, and `000d:5305..53d4` then reads the selected subentry's byte range directly into a newly allocated value-object buffer at `+0x0a/+0x0c`, which `000d:51fd` returns as the live far pair. That means the immediate workspace is file-backed owner-loaded slot data copied into memory before `000c:fa2f` interprets it. The remaining open question is no longer who first materializes the buffer at all, but whether the loaded slot family can be tied specifically to `NPCTRIG` slot `0x0a` or only to the broader owner-loaded descriptor workspace, with slot `0x20` still the best typed/setup companion.
|
||||||
|
- The next immortality pass closes the header/range-arithmetic blocker itself. The owner-resource callbacks operate on `class_id + 2`, which matches extracted `object_index` exactly; the first class-header dword is now constrained as the extra-slot count beyond a fixed `0x20` base table; bytes `8..11` remain the first code-byte offset; and `000d:53b4` reads body windows using the same `(word len, dword raw_code_offset, code_base)` arithmetic emitted by the extractor. `NPCTRIG` therefore now has exact owner-loaded body windows in the live runtime format: slot `0x0a` = `0x00da..0x024e` (`373` bytes) and slot `0x20` = `0x024f..0x03a7` (`345` bytes), while `EVENT` slot `0x0a` likewise fits `0x00d4..0x20a9`. The remaining immortality uncertainty is no longer range translation but upstream class selection into that now-verified loader path.
|
||||||
|
- The selector-side follow-up tightens that last uncertainty without closing it. `entity_vm_slot_index_from_entity` (`000d:45c5`) is now instruction-verified as a three-way category mapper only: `(1)` entity-id lane `1..255` with class bit `0x0002` clear -> `entity_id + 0x8c7e`, `(2)` class-nibble `4` lane -> `class_byte_0x7e05 + 0x8c80`, `(3)` fallback type lane -> `type_word_0x7df9 + 0x8c7c`. `entity_vm_runtime_init_from_path_if_configured` seeds those bases cumulatively from `0x6608..0x660e`, and direct caller `0005:295f` independently reuses the same slot index to test owner-row bit `0x0040`. That strengthens the read that the compiled side sees category spans plus generic row-capability masks, not a hard `NPCTRIG` / `EVENT` class-family discriminator, before the owner-loaded slot body is decoded.
|
||||||
|
- The compiled immortality lane is now concretely resolved on the cheat/toggle side with the correct flag split. `cheat_code_check` (`0007:0d0a`) is still the sole cheat-sequence matcher (5-byte table via `DS:0x2833`, index `DS:0x283d`), and it toggles `DS:0x844` (`cheats_enabled`) plus mirror `DS:0x6045`, then emits event `0x103`. The actual user-visible immortality toggle is event `0x410` at `000c:9703`, which boolean-toggles `DS:0x604f` and posts the on/off notifications (gate = `DS:0x844`). The older `DS:0x6050` lane at `immortality_activate` (`000c:8231`) remains a secondary entity/process path, not the primary player immortality toggle. Hidden seg109 menu wrappers `cheat_menu_open_from_current_slot` (`000b:9a86`) and `cheat_menu_open_modal` (`000b:9c0d`) are now named and verified to construct `cheat_event_listener_create`, but still have no static inbound xrefs in the recovered retail call graph (likely dormant/debug trigger path). Renamed in this area: `FUN_000c_8231` -> `immortality_activate`, `FUN_000c_834a` -> `immortality_conditional_activate`, `FUN_000c_8486` -> `immortality_activate_and_reset`, `FUN_000c_743f` -> `immortality_entity_process_create`, `FUN_000b_9a86` -> `cheat_menu_open_from_current_slot`, `FUN_000b_9c0d` -> `cheat_menu_open_modal`.
|
||||||
|
- Retail hidden-menu patching remains open, but the failed branches are now better separated from the still-live candidate. Verified file/fixup anchors are `0007:0d75` / `0007:0d79` (file `0x70d75` / relocation entry `0x71d68`) and `000c:99dd` / `000c:99e1` (file `0xc99dd`, seg126 chain `0x25e1`). The deferred `0x42f -> 000c:99dd -> 000b:9c0d` design is now explicitly rejected: it no longer broke startup, and it visibly entered the hidden UI path (mouse pointer appeared), but it halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit line, so `0x42f` is the wrong deferred context even though the address retarget itself was valid. The current live candidate is back on the direct `0007:0d79 -> 000b:9a86` retarget, but with a narrower wrapper patch at `000b:9a8d` that preserves the leading mode byte `1` and only zeros the two ambiguous 16-bit parameters.
|
||||||
|
|
||||||
### Recently Closed Or No Longer Live
|
### Recently Closed Or No Longer Live
|
||||||
|
|
||||||
- `ASYLUM.24` is resolved as `_ASS_StopAllSFX`; it is no longer an open plan item.
|
- `ASYLUM.24` is resolved as `_ASS_StopAllSFX`; it is no longer an open plan item.
|
||||||
- The cheat/input side lane is complete enough to leave the live queue.
|
- The cheat/input side lane is complete enough to leave the live queue.
|
||||||
- The segment coverage ledger is no longer a missing artifact; only refinement remains.
|
- The segment coverage ledger is no longer a missing artifact; only refinement remains.
|
||||||
- The startup/display lane now has named outer shells (`startup_display_transition_prepare`, `startup_display_transition_driver`) plus named seg126/127/136/137/138 helper families. The remaining work is higher-level ownership and state semantics, not basic structural recovery.
|
- The startup/display lane is now materially complete as a major section: the outer seg005 shells, seg126 setup/script/fade path, seg127 fade controller, seg136 owner split, seg137 palette-emission helpers, and seg138 late cleanup/handoff bodies all have stable structural roles.
|
||||||
- The top startup/display ownership question is now narrowed: `active_dispatch_entry_create_default` owns `g_active_dispatch_entry_farptr`, while seg049/seg126/seg138 helpers only borrow or clear the shared byte `+0x40`; the seg108 `0x4f38` lane is separate local sprite/object state.
|
- The top startup/display ownership question is closed tightly enough for planning: `active_dispatch_entry_create_default` owns `g_active_dispatch_entry_farptr`, while seg049/seg126/seg138 helpers only borrow or clear the shared byte `+0x40`; the seg108 `0x4f38` lane is separate local sprite/object state.
|
||||||
- The shared seg126 base-path question is effectively closed: literal-address search still shows no store into `0x6aa:0x6ac`, seg004 only mutates the pointed buffer while separately assigning sibling root `0x6ae:0x6b0`, and the startup/display family continues to treat `0x6aa:0x6ac` as an inherited mutable external/default base path.
|
- The shared seg126 base-path question is effectively closed: literal-address search still shows no store into `0x6aa:0x6ac`, seg004 only mutates the pointed buffer while separately assigning sibling root `0x6ae:0x6b0`, and the startup/display family continues to treat `0x6aa:0x6ac` as an inherited mutable external/default base path.
|
||||||
- The in-scope `0x31a2` transition/presentation reader pass is complete: the remaining reads in this lane now split into edge wait, modal break, deferred dispatch/state advance, and cleanup-abort roles.
|
- The in-scope `0x31a2` transition/presentation reader pass is complete: the remaining reads in this lane now split into edge wait, modal break, deferred dispatch/state advance, and cleanup-abort roles.
|
||||||
|
- The remaining startup/display residuals are now low-impact: the exact higher-level UI label of preset pair `0x10/0x11` is still open, and the `000c:db68` overlap still blocks clean function hygiene for `transition_preentry_step_script` even though it no longer blocks semantic recovery.
|
||||||
|
|
||||||
## Live Blockers
|
## Live Blockers
|
||||||
|
|
||||||
1. The startup/display transition lane still lacks exact higher-level owner/state labels across seg005, seg049, seg108, seg126, seg127, seg136, seg137, and seg138, even though the shared `g_active_dispatch_entry_farptr[+0x40]` hold token is now separated from the seg108-local `0x4f38` bit-`0x40` lane.
|
1. The oversized overlap rooted at `000c:db68` still blocks clean recovery of the real `transition_preentry_step_script` function object, even though it no longer blocks startup/display semantics.
|
||||||
2. The oversized overlap rooted at `000c:db68` still blocks safe recovery of the real `transition_preentry_step_script` body.
|
2. The `0x4588` callback object is better constrained and now leans toward a video/presentation-state broker, but it still is not behaviorally classified enough for a confident subsystem rename.
|
||||||
3. The `0x4588` callback object is better constrained but still not behaviorally classified enough for a confident subsystem rename.
|
3. The USECODE/VM sequencer still lacks the real upstream selector/caller path into `entity_vm_opcode_sequence_run`, and wrappers `entity_vm_context_try_create_mask_0400_slot0a_with_offset` / `entity_vm_context_try_create_mask_0800_slot0b_with_offset` remain outward-caller-dark even though their exact signed-additive `(slot, mask)` contracts are now closed, the generic masked hub at `000d:463a` is verified, and slot-`0x12` now has two concrete caller anchors at `0005:1776` / `0005:1945`.
|
||||||
4. The USECODE/VM sequencer still lacks the real upstream selector/caller path into `FUN_000d_ebe3`, and wrappers `0005:2c35` / `0005:2c68` remain caller-dark.
|
4. High-value missing or weak function objects still exist in hot ranges such as `000b:2e00`, `0007:5a00`, and `000e:ffb0`; `000e:ffb0` is now caller-side constrained to the overlapped video-frame chunk lane (`00db` / `00dc`) paired with `anim_load_audio_frame`, but the overlap still blocks clean recovery.
|
||||||
5. High-value missing or weak function objects still exist in hot ranges such as `000b:2e00`, `0007:5a00`, and `000e:ffb0`.
|
5. Non-CALLF far-pointer relocations and weakly covered resource/data loaders remain real second-pass blockers, even though they are not the first thing to attack.
|
||||||
6. Non-CALLF far-pointer relocations and weakly covered resource/data loaders remain real second-pass blockers, even though they are not the first thing to attack.
|
6. The immortality/`0x410` lane still lacks a verified USECODE emitter body, and the current blocker is now sharper. The owner-loaded format no longer blocks comparison: the class selector is now known to be `class_id + 2`, the header/subentry arithmetic at `000d:5066/51fd/53b4` matches extracted class headers and event rows exactly, and `NPCTRIG` slot `0x0a` / `0x20` now have concrete owner-loaded body ranges instead of only motif-level fits. But the compiled selector path is now also constrained enough to show what it does not provide: `000d:45c5` only maps entities into three generic category spans, `000d:44df` seeds those spans from `0x6608..0x660e`, `0005:295f` reuses the same slot index to test owner-row bit `0x0040`, and `0005:2c35` still has no caller/xref recovery. The remaining unresolved step is therefore a real upstream class-selector or caller-provenance recovery that can prove which class family is chosen before the slot body is decoded into the later `+0xd6/+0xd8` control stream and then into the `000c:fa2f` literal/replay lane.
|
||||||
|
|
||||||
## Current Focus
|
## Current Focus
|
||||||
|
|
||||||
1. Finish the startup/display transition lane while it is still producing direct executable coverage.
|
1. Continue the USECODE/VM lane where the verified masked-create hub (`000d:463a`), the internal consumer blocks (`000d:208b`, `000d:21ed`), or the newly separated `extra-word masked materializer` subfamily can still yield concrete caller, selector, or record-shape evidence rather than repeated direct-xref dead ends.
|
||||||
2. Continue the USECODE/VM lane only where it yields concrete caller, selector, or loader evidence rather than repeated direct-xref dead ends.
|
2. Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
|
||||||
3. Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
|
3. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again.
|
||||||
4. Use boundary repair only on active blockers with clear payoff.
|
4. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming.
|
||||||
|
|
||||||
## Next Resume Point
|
## Next Resume Point
|
||||||
|
|
||||||
1. Continue the adjacent seg126 startup/display clarification from the local three-way file-family selector at `000c:afa5..b152` and nearby seg049 `0x2bd8` dispatch sites, but only where it sharpens the validated presentation-handoff model without speculative renames.
|
1. Recover the real upstream caller/selector path into `entity_vm_opcode_sequence_run`, most likely by finding the first non-recursive `0x6714` context-method caller or vtable dispatch site rather than by repeating raw xref queries that still return no direct edges.
|
||||||
2. Repair the `000c:db68` overlap only if needed to split `transition_preentry_step_script` into its own clean function object and preserve the already-verified `000c:ca1d..cd4f` body in Ghidra.
|
2. Recover real caller roles for `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset` by treating them as the remaining dark members of the now-verified signed-additive masked-materializer subfamily and comparing them against the newly anchored slot-`0x12` caller pattern.
|
||||||
3. Classify the exact UI role of the paired `0x8c5c` / `0x8c60` renderer presets, the `+0x49` selector states, and the neighboring seg127 fade inputs only where the caller evidence stays inside the same startup/display family.
|
3. Tighten the newly surfaced higher-slot wrapper ladder around `0005:3115..31da`, especially the two slot-`0x12` caller sites at `0005:1776` / `0005:1945` and the slot-`0x10` guarded callsite, so any future promotion to `leaveFastArea` / `func11|cast` / `justMoved` / `AvatarStoleSomething` / `animGetHit` is driven by binary caller behavior rather than by external tables alone.
|
||||||
4. Recover the real upstream caller/selector path into `FUN_000d_ebe3` from persisted context/save/load or shared-consumer paths instead of repeating exhausted direct xref hunts.
|
4. Tighten the outward caller chains around the renamed seg006 masked helpers `entity_vm_context_try_create_mask_0008_slot30_with_offset` (`0006:0ba4`) and `entity_vm_context_try_create_mask_0010_slot08_with_offset_if_ready` (`0006:108c`) so the local state-selector lane and the adjacent class-linked value family can be tied back to concrete gameplay subsystems rather than only to class-detail fields.
|
||||||
5. Recover real caller roles for `0005:2c35` and `0005:2c68`, now that both are narrowed to signed slot-offset wrappers feeding the VM context lane.
|
5. Tighten the paired-file-family reading of the seg070 twin loops at `0009:67b6` and `0009:6916` by recovering which temporary buffer and record schema each family populates behind `entity_vm_runtime_owner_resource_create`.
|
||||||
6. Clarify whether the seg070 twin loops at `0009:67b6` and `0009:6916` represent two file families, two table formats, or two loader phases of the same helper behind `entity_vm_runtime_owner_resource_create`.
|
6. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`.
|
||||||
7. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`.
|
7. Revisit `0x4588` only if the video/presentation-state callback reading can be advanced into a behavioral name from caller-side evidence rather than from more lifecycle-only passes.
|
||||||
8. Revisit `000e:ffb0` and other high-value overlap targets only after the current startup/display and VM lanes stop yielding near-term wins.
|
8. If the VM lane stalls again, revisit `000e:ffb0` from the now-verified `00db/00dc` caller windows and try to recover an adjacent non-overlapped helper before attempting any boundary repair.
|
||||||
|
9. If the immortality lane is revisited, stay focused on `NPCTRIG` slot `0x0a` first, with slot `0x20` still treated as the typed/setup companion and `EVENT` only as the generic hub baseline; the next defensible step is no longer header/range arithmetic, slot-number translation, caller-frame recovery, first-origin recovery, owner-row tracing, or basic workspace materialization, but recovering the first producer that turns the three selector categories from `000d:45c5` into a concrete owner-loaded class choice and then comparing the surviving runtime tuple `(slot, add_a, add_b, setup_len, inline_len, placement)` against the now-exact owner-loaded `NPCTRIG` and `EVENT` body windows.
|
||||||
|
10. Use the new Pentagram-derived parser proof of concept as the first tooling bridge for raw class/slot bodies: extend opcode coverage conservatively, emit IR v1 artifacts, and only then prototype a Ghidra-side annotation importer against compiled anchors like `000d:51fd`, `000d:5572`, `000d:46ec`, `000d:22bc`, and `000d:ebe3`.
|
||||||
|
|
||||||
## Remaining Work To Reach A Reasonably Complete Decompilation State
|
## Remaining Work To Reach A Reasonably Complete Decompilation State
|
||||||
|
|
||||||
|
|
@ -94,18 +124,15 @@ Detailed completed analysis belongs in the files under `docs/`, not in this plan
|
||||||
|
|
||||||
### 2. Startup/Display And Presentation Lane
|
### 2. Startup/Display And Presentation Lane
|
||||||
|
|
||||||
- Finish semantic ownership across seg005, seg049, seg108, seg126, seg127, seg136, seg137, and seg138.
|
- Keep the startup/display lane closed unless new caller evidence materially changes its current model.
|
||||||
- Resolve the remaining role of the shared active-dispatch hold token versus local per-entry hold bytes.
|
- Classify the exact higher-level UI label of preset pair `0x10/0x11` only if stronger caller or string evidence appears.
|
||||||
- Recover the higher-level meaning of the file-backed seg126 control stream without speculating beyond verified byte behaviors.
|
- Revisit the remaining seg049/seg108/seg138 naming ambiguity only when it supports a defensible behavioral rename rather than another structural pass.
|
||||||
- Classify the exact UI role of the paired `0x8c5c` / `0x8c60` text-renderer lane if stronger caller evidence appears.
|
- Repair `000c:db68` only when a clean `transition_preentry_step_script` function object or adjacent active work makes the overlap fix worth the risk.
|
||||||
- Finish the fade-controller producer path so seg127 fade inputs are tied to higher-level transition states, not only local opcodes.
|
|
||||||
- Classify `FUN_000d_938c`, `transition_preentry_release_resources`, and `entity_cleanup_resources_and_dispatch` by role once their shared-hold semantics are fully separated.
|
|
||||||
- Remove the remaining overlap blockers in this lane, with `000c:db68` first.
|
|
||||||
|
|
||||||
### 3. VM / USECODE / Scripting Lane
|
### 3. VM / USECODE / Scripting Lane
|
||||||
|
|
||||||
- Recover the upstream selector into `FUN_000d_ebe3` and map payload-shape handlers to real opcode dispatch.
|
- Recover the upstream selector into `entity_vm_opcode_sequence_run` and map payload-shape handlers to real opcode dispatch.
|
||||||
- Recover real caller roles for the dark mask wrappers `0005:2c35` and `0005:2c68`.
|
- Recover real caller roles for the dark mask wrappers `entity_vm_context_try_create_mask_0400_slot0a_with_offset` and `entity_vm_context_try_create_mask_0800_slot0b_with_offset`.
|
||||||
- Keep separating owner-table-backed `0x39ca` rows from static dispatch-entry seed rows.
|
- Keep separating owner-table-backed `0x39ca` rows from static dispatch-entry seed rows.
|
||||||
- Finish classifying the seg069/070 helper behind `entity_vm_runtime_owner_resource_create`.
|
- Finish classifying the seg069/070 helper behind `entity_vm_runtime_owner_resource_create`.
|
||||||
- Broaden owner-loaded class/event validation beyond the first strong sample families.
|
- Broaden owner-loaded class/event validation beyond the first strong sample families.
|
||||||
|
|
|
||||||
5
reloc_report.txt
Normal file
5
reloc_report.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
segment=39 file_base=0x6E200 length=0x3416 reloc_table=0x71616 entries=0x145
|
||||||
|
exact_matches=1
|
||||||
|
EXACT index=234 entry_file_off=0x71D68 addr_type=0x03 rel_type=0x00 chain_off=0x2B79 target_seg=92 target_off=0x0476
|
||||||
|
nearby_matches=1
|
||||||
|
NEAR index=234 entry_file_off=0x71D68 addr_type=0x03 rel_type=0x00 chain_off=0x2B79 target_seg=92 target_off=0x0476
|
||||||
113
tmp_immortality_scan.py
Normal file
113
tmp_immortality_scan.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import csv
|
||||||
|
import pathlib
|
||||||
|
import struct
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(r"K:/ghidra/Crusader_Decomp")
|
||||||
|
TARGETS = {189, 190, 191, 272, 273, 283, 285}
|
||||||
|
TARGET_COMPARE_CLASSES = {"NPCTRIG", "COR_BOOT", "REE_BOOT", "SFXTRIG"}
|
||||||
|
|
||||||
|
|
||||||
|
def find_all(haystack: bytes, needle: bytes) -> list[int]:
|
||||||
|
offsets: list[int] = []
|
||||||
|
start = 0
|
||||||
|
while True:
|
||||||
|
found = haystack.find(needle, start)
|
||||||
|
if found < 0:
|
||||||
|
return offsets
|
||||||
|
offsets.append(found)
|
||||||
|
start = found + 1
|
||||||
|
|
||||||
|
def lcp(left: bytes, right: bytes) -> int:
|
||||||
|
count = 0
|
||||||
|
limit = min(len(left), len(right))
|
||||||
|
while count < limit and left[count] == right[count]:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def lcs(left: bytes, right: bytes) -> int:
|
||||||
|
count = 0
|
||||||
|
limit = min(len(left), len(right))
|
||||||
|
while count < limit and left[-1 - count] == right[-1 - count]:
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
rows = list(
|
||||||
|
csv.DictReader(
|
||||||
|
(ROOT / "USECODE/EUSECODE_extracted/class_event_index.tsv").open("r", encoding="utf-8"),
|
||||||
|
delimiter="\t",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows_by_entry: dict[int, list[dict[str, object]]] = {}
|
||||||
|
for row in rows:
|
||||||
|
entry_index = int(row["entry_index"])
|
||||||
|
if entry_index not in TARGETS:
|
||||||
|
continue
|
||||||
|
if not row["derived_body_start"]:
|
||||||
|
continue
|
||||||
|
rows_by_entry.setdefault(entry_index, []).append(
|
||||||
|
{
|
||||||
|
"class_name": row["class_name_hint"],
|
||||||
|
"slot": int(row["slot"], 0),
|
||||||
|
"event_name_hint": row["event_name_hint"],
|
||||||
|
"body_start": int(row["derived_body_start"], 0),
|
||||||
|
"body_end": int(row["derived_body_end"], 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_files: dict[int, pathlib.Path] = {}
|
||||||
|
for chunk_path in (ROOT / "USECODE/EUSECODE_extracted/chunks").glob("chunk_*.bin"):
|
||||||
|
entry_index = int(chunk_path.name.split("_")[1])
|
||||||
|
if entry_index in rows_by_entry:
|
||||||
|
chunk_files[entry_index] = chunk_path
|
||||||
|
|
||||||
|
bodies: dict[tuple[str, int], bytes] = {}
|
||||||
|
for entry_index in sorted(rows_by_entry):
|
||||||
|
chunk_path = chunk_files[entry_index]
|
||||||
|
data = chunk_path.read_bytes()
|
||||||
|
class_name = str(rows_by_entry[entry_index][0]["class_name"])
|
||||||
|
print(f"ENTRY {entry_index} {class_name} FILE {chunk_path.name}")
|
||||||
|
for row in sorted(rows_by_entry[entry_index], key=lambda item: int(item["body_start"])):
|
||||||
|
body = data[int(row["body_start"]):int(row["body_end"])]
|
||||||
|
class_name = str(row["class_name"])
|
||||||
|
slot = int(row["slot"])
|
||||||
|
bodies[(class_name, slot)] = body
|
||||||
|
hits_0410_16 = find_all(body, struct.pack("<H", 0x0410))
|
||||||
|
hits_0410_32 = find_all(body, struct.pack("<I", 0x00000410))
|
||||||
|
hits_1004_16 = find_all(body, struct.pack("<H", 0x1004))
|
||||||
|
print(
|
||||||
|
"BODY class={class_name} slot=0x{slot:02X} hint={hint} start=0x{start:04X} end=0x{end:04X} len={length} le16_0410={count16}:{offs16} le32_00000410={count32}:{offs32} le16_1004={count1004}:{offs1004} first16={first16} last16={last16}".format(
|
||||||
|
class_name=class_name,
|
||||||
|
slot=slot,
|
||||||
|
hint=str(row["event_name_hint"] or "-"),
|
||||||
|
start=int(row["body_start"]),
|
||||||
|
end=int(row["body_end"]),
|
||||||
|
length=len(body),
|
||||||
|
count16=len(hits_0410_16),
|
||||||
|
offs16=",".join(f"0x{offset:04X}" for offset in hits_0410_16[:16]) or "-",
|
||||||
|
count32=len(hits_0410_32),
|
||||||
|
offs32=",".join(f"0x{offset:04X}" for offset in hits_0410_32[:16]) or "-",
|
||||||
|
count1004=len(hits_1004_16),
|
||||||
|
offs1004=",".join(f"0x{offset:04X}" for offset in hits_1004_16[:16]) or "-",
|
||||||
|
first16=body[:16].hex(),
|
||||||
|
last16=body[-16:].hex(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("TOP_STRUCTURAL_PAIRS")
|
||||||
|
comparisons: list[tuple[int, int, int, tuple[str, int], tuple[str, int], int, int]] = []
|
||||||
|
compare_keys = [key for key in bodies if key[0] in TARGET_COMPARE_CLASSES]
|
||||||
|
for left_index, left_key in enumerate(compare_keys):
|
||||||
|
for right_key in compare_keys[left_index + 1:]:
|
||||||
|
left_body = bodies[left_key]
|
||||||
|
right_body = bodies[right_key]
|
||||||
|
prefix = lcp(left_body, right_body)
|
||||||
|
suffix = lcs(left_body, right_body)
|
||||||
|
comparisons.append((prefix + suffix, prefix, suffix, left_key, right_key, len(left_body), len(right_body)))
|
||||||
|
comparisons.sort(reverse=True)
|
||||||
|
for total, prefix, suffix, left_key, right_key, left_len, right_len in comparisons[:12]:
|
||||||
|
print(
|
||||||
|
f"PAIR {left_key[0]}:0x{left_key[1]:02X} len={left_len} <-> {right_key[0]}:0x{right_key[1]:02X} len={right_len} prefix={prefix} suffix={suffix} total={total}"
|
||||||
|
)
|
||||||
113
tmp_interpreter_filtered.txtGet-Location
Normal file
113
tmp_interpreter_filtered.txtGet-Location
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
000CFA04: sub word ptr es:[bx + 0xcc], 4
|
||||||
|
000CFA0C: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFA15: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFA32: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFA37: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFA3C: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFA5F: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFA64: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFA69: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFA86: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFA8B: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFA90: add word ptr es:[bx + 0xd6], 2
|
||||||
|
000CFAC6: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFACB: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFAD0: add word ptr es:[bx + 0xd6], 2
|
||||||
|
000CFAEE: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFAFD: mov ax, word ptr es:[bx + 0xd6]
|
||||||
|
000CFB05: push word ptr es:[bx + 0xd8]
|
||||||
|
000CFB0B: push word ptr es:[bx + 0xd8]
|
||||||
|
000CFB10: push word ptr es:[bx + 0xd6]
|
||||||
|
000CFB28: add word ptr es:[bx + 0xd6], 9
|
||||||
|
000CFB34: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFB39: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFB3E: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFB52: les bx, ptr es:[bx + 0xcc]
|
||||||
|
000CFB60: add word ptr es:[bx + 0xcc], 2
|
||||||
|
000CFB73: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFB8E: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFB93: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFB98: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFBAC: les bx, ptr es:[bx + 0xcc]
|
||||||
|
000CFBBA: add word ptr es:[bx + 0xcc], 2
|
||||||
|
000CFBCA: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFBE5: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFBEA: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFBEF: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFC03: les bx, ptr es:[bx + 0xcc]
|
||||||
|
000CFC18: add word ptr es:[bx + 0xcc], 4
|
||||||
|
000CFC2B: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFC4D: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFC52: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFC57: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFC68: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFC6D: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFC72: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFC8A: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFC9B: mov cx, word ptr es:[bx + 0xce]
|
||||||
|
000CFCA0: mov si, word ptr es:[bx + 0xcc]
|
||||||
|
000CFCC0: les bx, ptr es:[bx + 0xcc]
|
||||||
|
000CFCD5: add word ptr es:[bx + 0xcc], 4
|
||||||
|
000CFCEB: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFCF0: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFCF5: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFD06: les bx, ptr es:[bx + 0xcc]
|
||||||
|
000CFD1B: add word ptr es:[bx + 0xcc], 4
|
||||||
|
000CFD27: mov ax, word ptr es:[bx + 0xce]
|
||||||
|
000CFD2C: mov si, word ptr es:[bx + 0xcc]
|
||||||
|
000CFD4E: sub word ptr es:[bx + 0xcc], ax
|
||||||
|
000CFD59: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFD5E: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFD63: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFD73: dec word ptr es:[bx + 0xcc]
|
||||||
|
000CFD7A: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFD83: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFD96: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFD9B: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFDA0: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFDB1: sub word ptr es:[bx + 0xcc], 2
|
||||||
|
000CFDB9: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFDC2: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFDD5: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFDDA: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFDDF: add word ptr es:[bx + 0xd6], 2
|
||||||
|
000CFDF0: sub word ptr es:[bx + 0xcc], 2
|
||||||
|
000CFDF8: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFE01: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFE14: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFE19: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFE1E: add word ptr es:[bx + 0xd6], 4
|
||||||
|
000CFE36: sub word ptr es:[bx + 0xcc], 4
|
||||||
|
000CFE3E: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFE47: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFE61: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFE66: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFE6B: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFE83: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFE9D: sub word ptr es:[bx + 0xcc], 2
|
||||||
|
000CFEA5: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFEAE: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFEC1: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFEC6: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFECB: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFEE3: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFEFC: sub word ptr es:[bx + 0xcc], 2
|
||||||
|
000CFF04: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFF0D: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFF20: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFF25: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFF2A: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFF42: add ax, word ptr es:[bx + 0xd4]
|
||||||
|
000CFF62: sub word ptr es:[bx + 0xcc], 4
|
||||||
|
000CFF6A: mov es, word ptr es:[bx + 0xce]
|
||||||
|
000CFF73: mov bx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFF8D: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFF92: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFF97: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFFA8: mov ax, word ptr es:[bx + 0xd8]
|
||||||
|
000CFFAD: mov si, word ptr es:[bx + 0xd6]
|
||||||
|
000CFFB2: inc word ptr es:[bx + 0xd6]
|
||||||
|
000CFFC3: mov ax, word ptr es:[bx + 0xce]
|
||||||
|
000CFFC8: mov dx, word ptr es:[bx + 0xcc]
|
||||||
|
000CFFD9: sub word ptr es:[bx + 0xcc], ax
|
||||||
|
000CFFEE: add dx, word ptr es:[bx + 0xd4]
|
||||||
8
tmp_scan.py
Normal file
8
tmp_scan.py
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -516,6 +516,39 @@ FAMILY_ARTIFACT_SPECS: tuple[FamilyArtifactSpec, ...] = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
IMMORTALITY_TARGET_LABELS: tuple[str, ...] = (
|
||||||
|
"EVENT",
|
||||||
|
"NPCTRIG",
|
||||||
|
"COR_BOOT",
|
||||||
|
"REE_BOOT",
|
||||||
|
"SFXTRIG",
|
||||||
|
"SPECIAL",
|
||||||
|
"TRIGPAD",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
IMMORTALITY_TEMPLATE_COMPARE_LABELS: frozenset[str] = frozenset(
|
||||||
|
{"NPCTRIG", "COR_BOOT", "REE_BOOT", "SFXTRIG"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
IMMORTALITY_STRUCTURAL_TARGET_LABELS: frozenset[str] = frozenset({"EVENT", "NPCTRIG"})
|
||||||
|
|
||||||
|
|
||||||
|
IMMORTALITY_BODY_MOTIFS: tuple[tuple[str, bytes], ...] = (
|
||||||
|
("call_40_06_4c_02", bytes.fromhex("40 06 4c 02")),
|
||||||
|
("call_40_06_0f_04", bytes.fromhex("40 06 0f 04")),
|
||||||
|
("subheader_53_5c", bytes.fromhex("53 5c")),
|
||||||
|
("writeback_57_02", bytes.fromhex("57 02")),
|
||||||
|
("branch_59_0a", bytes.fromhex("59 0a")),
|
||||||
|
("branch_3f_0a", bytes.fromhex("3f 0a")),
|
||||||
|
("field_4b_fe_0f", bytes.fromhex("4b fe 0f")),
|
||||||
|
("field_4b_fc_0f", bytes.fromhex("4b fc 0f")),
|
||||||
|
("push_24_51", bytes.fromhex("24 51")),
|
||||||
|
("event_field_69_0a_00", bytes.fromhex("69 0a 00")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
VERIFIED_REPEATED_FAMILY_ROW_EXPECTATIONS: tuple[RepeatedFamilyRowExpectation, ...] = (
|
VERIFIED_REPEATED_FAMILY_ROW_EXPECTATIONS: tuple[RepeatedFamilyRowExpectation, ...] = (
|
||||||
RepeatedFamilyRowExpectation("JELYHACK", 0x01, 0x002A, 0x00000001, 0x00D4, 0x00FE, 42, "referent-anchor-twin/shared-slot-0x01/same-length-template"),
|
RepeatedFamilyRowExpectation("JELYHACK", 0x01, 0x002A, 0x00000001, 0x00D4, 0x00FE, 42, "referent-anchor-twin/shared-slot-0x01/same-length-template"),
|
||||||
RepeatedFamilyRowExpectation("JELYH2", 0x01, 0x002A, 0x00000001, 0x00D4, 0x00FE, 42, "referent-anchor-twin/shared-slot-0x01/same-length-template"),
|
RepeatedFamilyRowExpectation("JELYH2", 0x01, 0x002A, 0x00000001, 0x00D4, 0x00FE, 42, "referent-anchor-twin/shared-slot-0x01/same-length-template"),
|
||||||
|
|
@ -776,6 +809,33 @@ def hex_tail(data: bytes, width: int = 8) -> str:
|
||||||
return data[-width:].hex()
|
return data[-width:].hex()
|
||||||
|
|
||||||
|
|
||||||
|
def find_all_offsets(haystack: bytes, needle: bytes) -> list[int]:
|
||||||
|
offsets: list[int] = []
|
||||||
|
start = 0
|
||||||
|
while True:
|
||||||
|
found = haystack.find(needle, start)
|
||||||
|
if found < 0:
|
||||||
|
return offsets
|
||||||
|
offsets.append(found)
|
||||||
|
start = found + 1
|
||||||
|
|
||||||
|
|
||||||
|
def common_prefix_len(left: bytes, right: bytes) -> int:
|
||||||
|
limit = min(len(left), len(right))
|
||||||
|
offset = 0
|
||||||
|
while offset < limit and left[offset] == right[offset]:
|
||||||
|
offset += 1
|
||||||
|
return offset
|
||||||
|
|
||||||
|
|
||||||
|
def common_suffix_len(left: bytes, right: bytes) -> int:
|
||||||
|
limit = min(len(left), len(right))
|
||||||
|
offset = 0
|
||||||
|
while offset < limit and left[-1 - offset] == right[-1 - offset]:
|
||||||
|
offset += 1
|
||||||
|
return offset
|
||||||
|
|
||||||
|
|
||||||
def write_family_decompile_artifact(
|
def write_family_decompile_artifact(
|
||||||
out_dir: pathlib.Path,
|
out_dir: pathlib.Path,
|
||||||
parsed_class_chunks: list[ExtractedChunk],
|
parsed_class_chunks: list[ExtractedChunk],
|
||||||
|
|
@ -996,6 +1056,683 @@ def validate_verified_repeated_family_regressions(
|
||||||
return report_lines
|
return report_lines
|
||||||
|
|
||||||
|
|
||||||
|
def write_immortality_target_body_scan(
|
||||||
|
out_dir: pathlib.Path,
|
||||||
|
parsed_class_chunks: list[ExtractedChunk],
|
||||||
|
rows_by_entry: dict[int, list[ClassEventRow]],
|
||||||
|
raw_data_by_entry: dict[int, bytes],
|
||||||
|
) -> None:
|
||||||
|
chunk_by_label = {
|
||||||
|
chunk.primary_label: chunk
|
||||||
|
for chunk in parsed_class_chunks
|
||||||
|
if chunk.primary_label
|
||||||
|
}
|
||||||
|
scan_patterns = (
|
||||||
|
("le16_0410", struct.pack("<H", 0x0410)),
|
||||||
|
("le32_00000410", struct.pack("<I", 0x00000410)),
|
||||||
|
("le16_1004", struct.pack("<H", 0x1004)),
|
||||||
|
)
|
||||||
|
|
||||||
|
body_records: list[dict[str, object]] = []
|
||||||
|
comparison_records: list[tuple[int, int, int, str, int, int, str, int, int]] = []
|
||||||
|
compare_bodies: list[tuple[str, int, bytes]] = []
|
||||||
|
|
||||||
|
for label in IMMORTALITY_TARGET_LABELS:
|
||||||
|
chunk = chunk_by_label.get(label)
|
||||||
|
if chunk is None:
|
||||||
|
continue
|
||||||
|
raw_data = raw_data_by_entry.get(chunk.index)
|
||||||
|
if raw_data is None:
|
||||||
|
continue
|
||||||
|
for row in rows_by_entry.get(chunk.index, []):
|
||||||
|
if row.raw_code_offset == 0:
|
||||||
|
continue
|
||||||
|
if row.derived_body_start is None or row.derived_body_end is None:
|
||||||
|
continue
|
||||||
|
body = raw_data[row.derived_body_start:row.derived_body_end]
|
||||||
|
pattern_hits = {
|
||||||
|
pattern_name: find_all_offsets(body, needle)
|
||||||
|
for pattern_name, needle in scan_patterns
|
||||||
|
}
|
||||||
|
body_records.append(
|
||||||
|
{
|
||||||
|
"entry_index": row.entry_index,
|
||||||
|
"class_name": label,
|
||||||
|
"slot": row.slot,
|
||||||
|
"event_name_hint": row.event_name_hint or "",
|
||||||
|
"body_start": row.derived_body_start,
|
||||||
|
"body_end": row.derived_body_end,
|
||||||
|
"body_length": row.derived_body_length or len(body),
|
||||||
|
"first16": body[:16].hex(),
|
||||||
|
"last16": body[-16:].hex(),
|
||||||
|
"hits": pattern_hits,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if label in IMMORTALITY_TEMPLATE_COMPARE_LABELS:
|
||||||
|
compare_bodies.append((label, row.slot, body))
|
||||||
|
|
||||||
|
for left_index, left in enumerate(compare_bodies):
|
||||||
|
left_label, left_slot, left_body = left
|
||||||
|
for right in compare_bodies[left_index + 1:]:
|
||||||
|
right_label, right_slot, right_body = right
|
||||||
|
prefix = common_prefix_len(left_body, right_body)
|
||||||
|
suffix = common_suffix_len(left_body, right_body)
|
||||||
|
comparison_records.append(
|
||||||
|
(
|
||||||
|
prefix + suffix,
|
||||||
|
prefix,
|
||||||
|
suffix,
|
||||||
|
left_label,
|
||||||
|
left_slot,
|
||||||
|
len(left_body),
|
||||||
|
right_label,
|
||||||
|
right_slot,
|
||||||
|
len(right_body),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
comparison_records.sort(reverse=True)
|
||||||
|
|
||||||
|
tsv_lines = [
|
||||||
|
"entry_index\tclass_name\tslot\tevent_name_hint\tbody_start\tbody_end\tbody_length\tle16_0410_count\tle16_0410_offsets\tle32_00000410_count\tle32_00000410_offsets\tle16_1004_count\tle16_1004_offsets\tbody_prefix_hex\tbody_suffix_hex"
|
||||||
|
]
|
||||||
|
for record in body_records:
|
||||||
|
hits = record["hits"]
|
||||||
|
tsv_lines.append(
|
||||||
|
"{entry_index}\t{class_name}\t0x{slot:02X}\t{event_name_hint}\t0x{body_start:04X}\t0x{body_end:04X}\t{body_length}\t{le16_count}\t{le16_offsets}\t{le32_count}\t{le32_offsets}\t{be16_count}\t{be16_offsets}\t{first16}\t{last16}".format(
|
||||||
|
entry_index=record["entry_index"],
|
||||||
|
class_name=record["class_name"],
|
||||||
|
slot=record["slot"],
|
||||||
|
event_name_hint=record["event_name_hint"],
|
||||||
|
body_start=record["body_start"],
|
||||||
|
body_end=record["body_end"],
|
||||||
|
body_length=record["body_length"],
|
||||||
|
le16_count=len(hits["le16_0410"]),
|
||||||
|
le16_offsets=",".join(f"0x{offset:04X}" for offset in hits["le16_0410"]),
|
||||||
|
le32_count=len(hits["le32_00000410"]),
|
||||||
|
le32_offsets=",".join(f"0x{offset:04X}" for offset in hits["le32_00000410"]),
|
||||||
|
be16_count=len(hits["le16_1004"]),
|
||||||
|
be16_offsets=",".join(f"0x{offset:04X}" for offset in hits["le16_1004"]),
|
||||||
|
first16=record["first16"],
|
||||||
|
last16=record["last16"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(out_dir / "immortality_target_body_scan.tsv").write_text("\n".join(tsv_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
md_lines = [
|
||||||
|
"# Immortality Target Body Scan",
|
||||||
|
"",
|
||||||
|
"This report is a focused follow-up on the player-trigger immortality lane.",
|
||||||
|
"It scans the current highest-value EUSECODE candidates for inline `0x410` literals and compares the strongest active-event template bodies.",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
any_literal_hits = any(
|
||||||
|
record["hits"]["le16_0410"] or record["hits"]["le32_00000410"] or record["hits"]["le16_1004"]
|
||||||
|
for record in body_records
|
||||||
|
)
|
||||||
|
if any_literal_hits:
|
||||||
|
md_lines.append("- At least one target body contains an inline `0x410`-adjacent literal; inspect the TSV rows with non-zero hit counts.")
|
||||||
|
else:
|
||||||
|
md_lines.append("- No scanned target body contains inline little-endian `0x0410`, inline dword `0x00000410`, or byte-swapped `0x1004` literals.")
|
||||||
|
md_lines.append("- `EVENT` remains the widest unresolved active-event frontier because it still exposes one monolithic slot-`0x0A` body (`8150` bytes) with no finer body split yet.")
|
||||||
|
md_lines.append("- `NPCTRIG` remains the strongest compact player-trigger frontier because it is event-bearing and has two non-zero bodies (`0x0A`, `0x20`) but still no inline `0x410` literal.")
|
||||||
|
md_lines.append("- `_BOOT` event cores (`COR_BOOT`, `REE_BOOT`) remain near-template event families rather than special immortality emitters: their best pairings share only short common prefixes plus shared suffix-heavy tails.")
|
||||||
|
md_lines.append("- `SPECIAL` and `TRIGPAD` stay negative controls here: callable bodies exist, but the new literal scan still shows no inline `0x410` evidence.")
|
||||||
|
md_lines.append("")
|
||||||
|
md_lines.append("## Body Rows")
|
||||||
|
md_lines.append("")
|
||||||
|
md_lines.append("| Class | Slot | Hint | Body Range | Len | `0x0410` hits | `0x00000410` hits | `0x1004` hits | Prefix | Suffix |")
|
||||||
|
md_lines.append("|---|---:|---|---|---:|---|---|---|---|---|")
|
||||||
|
for record in body_records:
|
||||||
|
hits = record["hits"]
|
||||||
|
md_lines.append(
|
||||||
|
"| {class_name} | `0x{slot:02X}` | {event_name_hint} | `0x{body_start:04X}..0x{body_end:04X}` | {body_length} | {le16_count}:{le16_offsets} | {le32_count}:{le32_offsets} | {be16_count}:{be16_offsets} | `{first16}` | `{last16}` |".format(
|
||||||
|
class_name=record["class_name"],
|
||||||
|
slot=record["slot"],
|
||||||
|
event_name_hint=record["event_name_hint"] or "-",
|
||||||
|
body_start=record["body_start"],
|
||||||
|
body_end=record["body_end"],
|
||||||
|
body_length=record["body_length"],
|
||||||
|
le16_count=len(hits["le16_0410"]),
|
||||||
|
le16_offsets=",".join(f"0x{offset:04X}" for offset in hits["le16_0410"]) or "-",
|
||||||
|
le32_count=len(hits["le32_00000410"]),
|
||||||
|
le32_offsets=",".join(f"0x{offset:04X}" for offset in hits["le32_00000410"]) or "-",
|
||||||
|
be16_count=len(hits["le16_1004"]),
|
||||||
|
be16_offsets=",".join(f"0x{offset:04X}" for offset in hits["le16_1004"]) or "-",
|
||||||
|
first16=record["first16"],
|
||||||
|
last16=record["last16"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
md_lines.extend([
|
||||||
|
"",
|
||||||
|
"## Strongest Template Pairings",
|
||||||
|
"",
|
||||||
|
"These comparisons are limited to `COR_BOOT`, `REE_BOOT`, `NPCTRIG`, and `SFXTRIG` because they are the current highest-value active-event families near the immortality frontier.",
|
||||||
|
"",
|
||||||
|
"| Left | Right | Prefix | Suffix | Total |",
|
||||||
|
"|---|---|---:|---:|---:|",
|
||||||
|
])
|
||||||
|
for total, prefix, suffix, left_label, left_slot, left_len, right_label, right_slot, right_len in comparison_records[:12]:
|
||||||
|
md_lines.append(
|
||||||
|
f"| {left_label} `0x{left_slot:02X}` (`{left_len}`) | {right_label} `0x{right_slot:02X}` (`{right_len}`) | {prefix} | {suffix} | {total} |"
|
||||||
|
)
|
||||||
|
(out_dir / "immortality_target_body_scan.md").write_text("\n".join(md_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def read_ascii_cstring(data: bytes, start: int, max_len: int = 48) -> tuple[str, int] | None:
|
||||||
|
end = start
|
||||||
|
limit = min(len(data), start + max_len)
|
||||||
|
while end < limit and data[end] != 0:
|
||||||
|
byte = data[end]
|
||||||
|
if not (0x20 <= byte <= 0x7E):
|
||||||
|
return None
|
||||||
|
end += 1
|
||||||
|
if end >= len(data) or end == start or data[end] != 0:
|
||||||
|
return None
|
||||||
|
return data[start:end].decode("latin-1"), end + 1
|
||||||
|
|
||||||
|
|
||||||
|
def parse_body_open_header(body: bytes) -> dict[str, object] | None:
|
||||||
|
if len(body) < 7:
|
||||||
|
return None
|
||||||
|
if body[0] == 0x5A and body[2] == 0x5C:
|
||||||
|
open_arg = body[1]
|
||||||
|
target_offset = 3
|
||||||
|
label_offset = 5
|
||||||
|
elif body[1] == 0x5C:
|
||||||
|
open_arg = body[0]
|
||||||
|
target_offset = 1
|
||||||
|
label_offset = 3
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
label_result = read_ascii_cstring(body, label_offset)
|
||||||
|
if label_result is None:
|
||||||
|
return None
|
||||||
|
label, offset = label_result
|
||||||
|
while offset < len(body) and body[offset] == 0:
|
||||||
|
offset += 1
|
||||||
|
event_code = body[offset + 1] if offset + 1 < len(body) and body[offset] == 0x0B else None
|
||||||
|
return {
|
||||||
|
"open_arg": open_arg,
|
||||||
|
"target": read_u16_le(body, target_offset),
|
||||||
|
"label": label,
|
||||||
|
"event_code": event_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def find_labeled_subheaders(body: bytes, label: str) -> list[tuple[int, int]]:
|
||||||
|
offsets: list[tuple[int, int]] = []
|
||||||
|
label_bytes = label.encode("latin-1")
|
||||||
|
marker = b"\x53\x5c"
|
||||||
|
search_start = 0
|
||||||
|
while True:
|
||||||
|
found = body.find(marker, search_start)
|
||||||
|
if found < 0 or found + 4 >= len(body):
|
||||||
|
return offsets
|
||||||
|
if body[found + 4:found + 4 + len(label_bytes)] == label_bytes:
|
||||||
|
offsets.append((found, read_u16_le(body, found + 2)))
|
||||||
|
search_start = found + 1
|
||||||
|
|
||||||
|
|
||||||
|
def scan_body_field_tokens(body: bytes, tail_window: int | None = None) -> list[str]:
|
||||||
|
tokens: list[str] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
start = max(0, len(body) - tail_window) if tail_window is not None else 0
|
||||||
|
for offset in range(start, len(body) - 4):
|
||||||
|
if body[offset] not in {0x24, 0x69}:
|
||||||
|
continue
|
||||||
|
field_result = read_ascii_cstring(body, offset + 3)
|
||||||
|
if field_result is None:
|
||||||
|
continue
|
||||||
|
field_name, _ = field_result
|
||||||
|
token = f"{body[offset]:02X}:{read_u16_le(body, offset + 1):04X}->{field_name}"
|
||||||
|
if token not in seen:
|
||||||
|
seen.add(token)
|
||||||
|
tokens.append(token)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def format_offset_list(offsets: list[int], limit: int = 10) -> str:
|
||||||
|
if not offsets:
|
||||||
|
return ""
|
||||||
|
rendered = ",".join(f"0x{offset:04X}" for offset in offsets[:limit])
|
||||||
|
if len(offsets) > limit:
|
||||||
|
rendered += ",..."
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def scan_body_field_offsets(body: bytes, tail_window: int | None = None) -> list[tuple[int, str]]:
|
||||||
|
tokens: list[tuple[int, str]] = []
|
||||||
|
seen: set[tuple[int, str]] = set()
|
||||||
|
start = max(0, len(body) - tail_window) if tail_window is not None else 0
|
||||||
|
for offset in range(start, len(body) - 4):
|
||||||
|
if body[offset] not in {0x24, 0x69}:
|
||||||
|
continue
|
||||||
|
field_result = read_ascii_cstring(body, offset + 3)
|
||||||
|
if field_result is None:
|
||||||
|
continue
|
||||||
|
field_name, _ = field_result
|
||||||
|
token = f"{body[offset]:02X}:{read_u16_le(body, offset + 1):04X}->{field_name}"
|
||||||
|
entry = (offset, token)
|
||||||
|
if entry in seen:
|
||||||
|
continue
|
||||||
|
seen.add(entry)
|
||||||
|
tokens.append(entry)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
def count_offsets_in_range(offsets: list[int], start: int, end: int) -> int:
|
||||||
|
return sum(1 for offset in offsets if start <= offset < end)
|
||||||
|
|
||||||
|
|
||||||
|
def relative_offsets_in_range(offsets: list[int], start: int, end: int) -> list[int]:
|
||||||
|
return [offset - start for offset in offsets if start <= offset < end]
|
||||||
|
|
||||||
|
|
||||||
|
def format_relative_offsets(offsets: list[int], limit: int = 8) -> str:
|
||||||
|
if not offsets:
|
||||||
|
return "-"
|
||||||
|
rendered = ",".join(f"+0x{offset:02X}" for offset in offsets[:limit])
|
||||||
|
if len(offsets) > limit:
|
||||||
|
rendered += ",..."
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
|
||||||
|
def find_repeated_windows(body: bytes, size: int, min_count: int = 2, max_results: int = 6) -> list[tuple[bytes, list[int]]]:
|
||||||
|
if size <= 0 or len(body) < size:
|
||||||
|
return []
|
||||||
|
offsets_by_window: dict[bytes, list[int]] = {}
|
||||||
|
for offset in range(0, len(body) - size + 1):
|
||||||
|
window = body[offset:offset + size]
|
||||||
|
if window.count(0) == len(window):
|
||||||
|
continue
|
||||||
|
offsets_by_window.setdefault(window, []).append(offset)
|
||||||
|
repeated = [
|
||||||
|
(window, offsets)
|
||||||
|
for window, offsets in offsets_by_window.items()
|
||||||
|
if len(offsets) >= min_count
|
||||||
|
]
|
||||||
|
repeated.sort(key=lambda item: (-len(item[1]), item[1][0], item[0]))
|
||||||
|
return repeated[:max_results]
|
||||||
|
|
||||||
|
|
||||||
|
def format_hex_window(window: bytes) -> str:
|
||||||
|
return " ".join(f"{byte:02X}" for byte in window)
|
||||||
|
|
||||||
|
|
||||||
|
def build_npctrig_clause_segments(
|
||||||
|
body: bytes,
|
||||||
|
subheaders: list[tuple[int, int]],
|
||||||
|
) -> tuple[list[tuple[str, int, int]], int]:
|
||||||
|
first_subheader = subheaders[0][0] if subheaders else 0
|
||||||
|
tail_fields = scan_body_field_offsets(body, tail_window=min(len(body), 192))
|
||||||
|
tail_start = tail_fields[0][0] if tail_fields else len(body)
|
||||||
|
if tail_start <= first_subheader:
|
||||||
|
tail_start = len(body)
|
||||||
|
|
||||||
|
segments: list[tuple[str, int, int]] = []
|
||||||
|
if first_subheader > 0:
|
||||||
|
segments.append(("prefix", 0, first_subheader))
|
||||||
|
for index, (start, _) in enumerate(subheaders):
|
||||||
|
next_start = subheaders[index + 1][0] if index + 1 < len(subheaders) else tail_start
|
||||||
|
segments.append((f"clause_{index + 1}", start, next_start))
|
||||||
|
if tail_start < len(body):
|
||||||
|
segments.append(("tail", tail_start, len(body)))
|
||||||
|
return segments, tail_start
|
||||||
|
|
||||||
|
|
||||||
|
def write_npctrig_clause_report(
|
||||||
|
out_dir: pathlib.Path,
|
||||||
|
parsed_class_chunks: list[ExtractedChunk],
|
||||||
|
rows_by_entry: dict[int, list[ClassEventRow]],
|
||||||
|
raw_data_by_entry: dict[int, bytes],
|
||||||
|
) -> None:
|
||||||
|
chunk = next((candidate for candidate in parsed_class_chunks if candidate.primary_label == "NPCTRIG"), None)
|
||||||
|
if chunk is None:
|
||||||
|
return
|
||||||
|
raw_data = raw_data_by_entry.get(chunk.index)
|
||||||
|
if raw_data is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
records: list[dict[str, object]] = []
|
||||||
|
clause_motif_names = ["subheader_53_5c", "branch_3f_0a", "writeback_57_02", "push_24_51", "field_4b_fe_0f"]
|
||||||
|
|
||||||
|
for row in rows_by_entry.get(chunk.index, []):
|
||||||
|
if row.raw_code_offset == 0 or row.derived_body_start is None or row.derived_body_end is None:
|
||||||
|
continue
|
||||||
|
body = raw_data[row.derived_body_start:row.derived_body_end]
|
||||||
|
header = parse_body_open_header(body)
|
||||||
|
subheaders = find_labeled_subheaders(body, "NPCTRIG")
|
||||||
|
segments, tail_start = build_npctrig_clause_segments(body, subheaders)
|
||||||
|
motif_hits = {
|
||||||
|
motif_name: find_all_offsets(body, motif_bytes)
|
||||||
|
for motif_name, motif_bytes in IMMORTALITY_BODY_MOTIFS
|
||||||
|
}
|
||||||
|
repeated_windows_8 = find_repeated_windows(body, 8)
|
||||||
|
repeated_windows_6 = find_repeated_windows(body, 6)
|
||||||
|
tail_fields = scan_body_field_offsets(body, tail_window=min(len(body), 192))
|
||||||
|
segment_rows: list[dict[str, object]] = []
|
||||||
|
for segment_name, start, end in segments:
|
||||||
|
segment_body = body[start:end]
|
||||||
|
labels = [offset for offset in find_all_offsets(segment_body, bytes.fromhex("5B"))]
|
||||||
|
motif_offsets = {
|
||||||
|
motif_name: relative_offsets_in_range(motif_hits[motif_name], start, end)
|
||||||
|
for motif_name in clause_motif_names
|
||||||
|
}
|
||||||
|
segment_rows.append(
|
||||||
|
{
|
||||||
|
"segment": segment_name,
|
||||||
|
"start": start,
|
||||||
|
"end": end,
|
||||||
|
"length": end - start,
|
||||||
|
"prefix_hex": hex_edge(segment_body, width=16),
|
||||||
|
"suffix_hex": hex_tail(segment_body, width=12),
|
||||||
|
"local_labels": [start + offset for offset in labels[:8]],
|
||||||
|
"motif_counts": {
|
||||||
|
motif_name: count_offsets_in_range(motif_hits[motif_name], start, end)
|
||||||
|
for motif_name in clause_motif_names
|
||||||
|
},
|
||||||
|
"motif_offsets": motif_offsets,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
subheader_offset_deltas = [subheaders[index + 1][0] - subheaders[index][0] for index in range(len(subheaders) - 1)]
|
||||||
|
subheader_target_deltas = [subheaders[index + 1][1] - subheaders[index][1] for index in range(len(subheaders) - 1)]
|
||||||
|
uniform_stride = subheader_offset_deltas[0] if subheader_offset_deltas and len(set(subheader_offset_deltas)) == 1 else None
|
||||||
|
full_clause_segments = [
|
||||||
|
segment
|
||||||
|
for segment in segment_rows
|
||||||
|
if segment["segment"].startswith("clause_")
|
||||||
|
and segment["motif_counts"]["push_24_51"]
|
||||||
|
and segment["motif_counts"]["writeback_57_02"]
|
||||||
|
]
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"slot": row.slot,
|
||||||
|
"event_name_hint": row.event_name_hint or "",
|
||||||
|
"body_length": len(body),
|
||||||
|
"header": header,
|
||||||
|
"subheaders": subheaders,
|
||||||
|
"subheader_offset_deltas": subheader_offset_deltas,
|
||||||
|
"subheader_target_deltas": subheader_target_deltas,
|
||||||
|
"segments": segment_rows,
|
||||||
|
"tail_start": tail_start,
|
||||||
|
"tail_fields": tail_fields,
|
||||||
|
"repeated_windows_8": repeated_windows_8,
|
||||||
|
"repeated_windows_6": repeated_windows_6,
|
||||||
|
"has_writeback": bool(motif_hits["writeback_57_02"]),
|
||||||
|
"has_push_2451": bool(motif_hits["push_24_51"]),
|
||||||
|
"field_4b_fe_0f_count": len(motif_hits["field_4b_fe_0f"]),
|
||||||
|
"uniform_stride": uniform_stride,
|
||||||
|
"full_clause_count": len(full_clause_segments),
|
||||||
|
"selector_offsets": [offset for offset, _ in subheaders],
|
||||||
|
"selector_targets": [target for _, target in subheaders],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not records:
|
||||||
|
return
|
||||||
|
|
||||||
|
tsv_lines = [
|
||||||
|
"slot\tevent_name_hint\tbody_length\theader_target\theader_event_code\tsubheader_offsets\tsubheader_targets\tsubheader_offset_deltas\tsubheader_target_deltas\tuniform_stride\tfull_clause_count\ttail_start\thas_writeback\thas_push_2451\tfield_4b_fe_0f_count\trepeated_windows_8\trepeated_windows_6"
|
||||||
|
]
|
||||||
|
for record in records:
|
||||||
|
header = record["header"] or {}
|
||||||
|
tsv_lines.append(
|
||||||
|
"0x{slot:02X}\t{event_name_hint}\t{body_length}\t{header_target}\t{header_event_code}\t{subheader_offsets}\t{subheader_targets}\t{subheader_offset_deltas}\t{subheader_target_deltas}\t{uniform_stride}\t{full_clause_count}\t0x{tail_start:04X}\t{has_writeback}\t{has_push_2451}\t{field_4b_fe_0f_count}\t{repeated_windows_8}\t{repeated_windows_6}".format(
|
||||||
|
slot=record["slot"],
|
||||||
|
event_name_hint=record["event_name_hint"],
|
||||||
|
body_length=record["body_length"],
|
||||||
|
header_target=(f"0x{header['target']:04X}" if header else ""),
|
||||||
|
header_event_code=(f"0x{header['event_code']:02X}" if header and header.get("event_code") is not None else ""),
|
||||||
|
subheader_offsets=",".join(f"0x{offset:04X}" for offset, _ in record["subheaders"]),
|
||||||
|
subheader_targets=",".join(f"0x{target:04X}" for _, target in record["subheaders"]),
|
||||||
|
subheader_offset_deltas=",".join(f"0x{delta:02X}" for delta in record["subheader_offset_deltas"]),
|
||||||
|
subheader_target_deltas=",".join(f"0x{delta & 0xFFFF:04X}" for delta in record["subheader_target_deltas"]),
|
||||||
|
uniform_stride=(f"0x{record['uniform_stride']:02X}" if record["uniform_stride"] is not None else ""),
|
||||||
|
full_clause_count=record["full_clause_count"],
|
||||||
|
tail_start=record["tail_start"],
|
||||||
|
has_writeback="yes" if record["has_writeback"] else "no",
|
||||||
|
has_push_2451="yes" if record["has_push_2451"] else "no",
|
||||||
|
field_4b_fe_0f_count=record["field_4b_fe_0f_count"],
|
||||||
|
repeated_windows_8=";".join(
|
||||||
|
f"{window.hex()}@{','.join(f'0x{offset:04X}' for offset in offsets)}"
|
||||||
|
for window, offsets in record["repeated_windows_8"]
|
||||||
|
),
|
||||||
|
repeated_windows_6=";".join(
|
||||||
|
f"{window.hex()}@{','.join(f'0x{offset:04X}' for offset in offsets)}"
|
||||||
|
for window, offsets in record["repeated_windows_6"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(out_dir / "immortality_npctrig_clauses.tsv").write_text("\n".join(tsv_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
md_lines = [
|
||||||
|
"# Immortality NPCTRIG Clauses",
|
||||||
|
"",
|
||||||
|
"This report focuses on the surviving compact NPCTRIG frontier and splits the extracted slot bodies into prefix, clause, and tail regions.",
|
||||||
|
"It is intended to make the slot `0x0A` versus slot `0x20` difference explicit enough to compare against the runtime-side slot-`0x0A` consumer path.",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for record in records:
|
||||||
|
header = record["header"] or {}
|
||||||
|
md_lines.extend([
|
||||||
|
f"## NPCTRIG slot `0x{record['slot']:02X}`",
|
||||||
|
"",
|
||||||
|
f"- Event hint: `{record['event_name_hint'] or '-'}`.",
|
||||||
|
f"- Open header: `0x5A 0x{header['open_arg']:02X} 0x5C 0x{header['target']:04X}` -> `NPCTRIG` with event-code byte `{f'0x{header['event_code']:02X}' if header.get('event_code') is not None else '-'}`." if header else "- Open header: not recognized.",
|
||||||
|
f"- First tail-field offset: `0x{record['tail_start']:04X}`.",
|
||||||
|
f"- Subheader offsets: {', '.join(f'`0x{offset:04X}`' for offset, _ in record['subheaders']) or '`-`'}.",
|
||||||
|
f"- Subheader targets: {', '.join(f'`0x{target:04X}`' for _, target in record['subheaders']) or '`-`'}.",
|
||||||
|
f"- Subheader offset deltas: {', '.join(f'`0x{delta:02X}`' for delta in record['subheader_offset_deltas']) or '`-`'}.",
|
||||||
|
f"- Subheader target deltas: {', '.join(f'`0x{delta & 0xFFFF:04X}`' for delta in record['subheader_target_deltas']) or '`-`'}.",
|
||||||
|
f"- Runtime-shape motifs: `writeback_57_02={'yes' if record['has_writeback'] else 'no'}`, `push_24_51={'yes' if record['has_push_2451'] else 'no'}`, `field_4b_fe_0f={record['field_4b_fe_0f_count']}`.",
|
||||||
|
"",
|
||||||
|
"| Segment | Range | Len | Local Labels | Subheaders | Branch 3F 0A | Writeback 57 02 | Push 24 51 | Field 4B FE 0F | Motif Offsets | Prefix | Suffix |",
|
||||||
|
"|---|---|---:|---|---:|---:|---:|---:|---:|---|---|---|",
|
||||||
|
])
|
||||||
|
for segment in record["segments"]:
|
||||||
|
motif_counts = segment["motif_counts"]
|
||||||
|
motif_offsets = segment["motif_offsets"]
|
||||||
|
motif_offset_render = "; ".join(
|
||||||
|
f"{motif_name}={format_relative_offsets(offsets)}"
|
||||||
|
for motif_name, offsets in motif_offsets.items()
|
||||||
|
if offsets
|
||||||
|
) or "-"
|
||||||
|
label_render = ",".join(f"0x{offset:04X}" for offset in segment["local_labels"]) or "-"
|
||||||
|
md_lines.append(
|
||||||
|
"| {segment} | `0x{start:04X}..0x{end:04X}` | {length} | `{labels}` | {subheaders} | {branch} | {writeback} | {push_2451} | {field_4b_fe_0f} | `{motif_offsets}` | `{prefix}` | `{suffix}` |".format(
|
||||||
|
segment=segment["segment"],
|
||||||
|
start=segment["start"],
|
||||||
|
end=segment["end"],
|
||||||
|
length=segment["length"],
|
||||||
|
labels=label_render,
|
||||||
|
subheaders=motif_counts["subheader_53_5c"],
|
||||||
|
branch=motif_counts["branch_3f_0a"],
|
||||||
|
writeback=motif_counts["writeback_57_02"],
|
||||||
|
push_2451=motif_counts["push_24_51"],
|
||||||
|
field_4b_fe_0f=motif_counts["field_4b_fe_0f"],
|
||||||
|
motif_offsets=motif_offset_render,
|
||||||
|
prefix=segment["prefix_hex"],
|
||||||
|
suffix=segment["suffix_hex"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
md_lines.extend([
|
||||||
|
"",
|
||||||
|
"Repeated windows (8-byte):",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
for window, offsets in record["repeated_windows_8"]:
|
||||||
|
md_lines.append(
|
||||||
|
f"- `{format_hex_window(window)}` at {', '.join(f'`0x{offset:04X}`' for offset in offsets)}"
|
||||||
|
)
|
||||||
|
md_lines.extend([
|
||||||
|
"",
|
||||||
|
"Repeated windows (6-byte):",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
for window, offsets in record["repeated_windows_6"]:
|
||||||
|
md_lines.append(
|
||||||
|
f"- `{format_hex_window(window)}` at {', '.join(f'`0x{offset:04X}`' for offset in offsets)}"
|
||||||
|
)
|
||||||
|
md_lines.extend([
|
||||||
|
"",
|
||||||
|
"Runtime-fit candidates:",
|
||||||
|
"",
|
||||||
|
f"- Candidate clause selector starts: {', '.join(f'`0x{offset:04X}`' for offset in record['selector_offsets']) or '`-`'}.",
|
||||||
|
f"- Candidate clause selector targets: {', '.join(f'`0x{target:04X}`' for target in record['selector_targets']) or '`-`'}.",
|
||||||
|
f"- Uniform selector stride: `{f'0x{record['uniform_stride']:02X}' if record['uniform_stride'] is not None else '-'}`; full clauses carrying both `push_24_51` and `writeback_57_02`: `{record['full_clause_count']}`.",
|
||||||
|
"- Runtime side anchor: `000d:5572` proves the wrapper extra word is additive (`entity_vm_slot_load_value(...) + offset`), while `000d:21ed -> 000d:2433` copies one inline blob, reads two signed metadata bytes, then consumes a word matrix where byte A controls the lead-word row count and byte B controls the shared target-list width.",
|
||||||
|
"",
|
||||||
|
"Tail field offsets:",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
for offset, token in record["tail_fields"]:
|
||||||
|
md_lines.append(f"- `0x{offset:04X}` -> `{token}`")
|
||||||
|
md_lines.append("")
|
||||||
|
|
||||||
|
slot_0a = next((record for record in records if record["slot"] == 0x0A), None)
|
||||||
|
slot_20 = next((record for record in records if record["slot"] == 0x20), None)
|
||||||
|
if slot_0a and slot_20:
|
||||||
|
slot_0a_header = slot_0a["header"] or {}
|
||||||
|
slot_20_header = slot_20["header"] or {}
|
||||||
|
md_lines.extend([
|
||||||
|
"## Current Read",
|
||||||
|
"",
|
||||||
|
f"- Slot `0x0A` now reads as a repeated clause ladder, not a monolithic blob: `{len(slot_0a['subheaders'])}` subheaders sit on a uniform `{', '.join(f'0x{delta:02X}' for delta in slot_0a['subheader_offset_deltas']) or '-'}` byte stride, and their targets walk backward by `{', '.join(f'0x{delta & 0xFFFF:04X}' for delta in slot_0a['subheader_target_deltas']) or '-'}`. Each clause block carries one `branch_3f_0a`, one `push_24_51`, and one `writeback_57_02`, which fits an event-bearing clause stream better than a pure type filter.",
|
||||||
|
f"- Slot `0x20` is structurally different even before the tail fields: its open event-code byte is `{f'0x{slot_20_header['event_code']:02X}' if slot_20_header.get('event_code') is not None else '-'}` instead of `{f'0x{slot_0a_header['event_code']:02X}' if slot_0a_header.get('event_code') is not None else '-'}`, it has only one class-labelled subheader, no `writeback_57_02`, no `push_24_51`, and `{slot_20['field_4b_fe_0f_count']}` `field_4b_fe_0f` hits concentrated around repeated `0x0A 00/05 4B FE 0F ...` windows. That is a materially better fit for a typed gate or setup/attachment body than for the live event-emission ladder.",
|
||||||
|
"- This split matches the current runtime-side bridge better than the previous undifferentiated frontier. The verified slot-`0x0A` wrapper `0005:2c35` seeds mask `0x0400`, slot `0x0A`, and one additive word that `000d:5572` applies directly to the loaded slot value before `000d:21ed` consumes the result. The exact `000d:21ed -> 000d:22bc` contract is now narrower too: after copying the inline blob it reads two signed bytes, uses byte A as the lead-word row count, uses byte B as the shared target-list width, performs `A x B` `entity_link` calls, and pushes back only non-`0x0400` words. `NPCTRIG slot 0x0A` is the only surviving compact body here with a natural five-row selector family (`5` evenly spaced clause starts at stride `0x2F`), while slot `0x20` offers only one clause and no matching writeback/push motif.",
|
||||||
|
])
|
||||||
|
(out_dir / "immortality_npctrig_clauses.md").write_text("\n".join(md_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def write_immortality_body_structure_report(
|
||||||
|
out_dir: pathlib.Path,
|
||||||
|
parsed_class_chunks: list[ExtractedChunk],
|
||||||
|
rows_by_entry: dict[int, list[ClassEventRow]],
|
||||||
|
raw_data_by_entry: dict[int, bytes],
|
||||||
|
) -> None:
|
||||||
|
chunk_by_label = {
|
||||||
|
chunk.primary_label: chunk
|
||||||
|
for chunk in parsed_class_chunks
|
||||||
|
if chunk.primary_label in IMMORTALITY_STRUCTURAL_TARGET_LABELS
|
||||||
|
}
|
||||||
|
records: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
for label in sorted(IMMORTALITY_STRUCTURAL_TARGET_LABELS):
|
||||||
|
chunk = chunk_by_label.get(label)
|
||||||
|
if chunk is None:
|
||||||
|
continue
|
||||||
|
raw_data = raw_data_by_entry.get(chunk.index)
|
||||||
|
if raw_data is None:
|
||||||
|
continue
|
||||||
|
for row in rows_by_entry.get(chunk.index, []):
|
||||||
|
if row.raw_code_offset == 0 or row.derived_body_start is None or row.derived_body_end is None:
|
||||||
|
continue
|
||||||
|
body = raw_data[row.derived_body_start:row.derived_body_end]
|
||||||
|
header = parse_body_open_header(body)
|
||||||
|
subheaders = find_labeled_subheaders(body, label)
|
||||||
|
motif_hits = {
|
||||||
|
motif_name: find_all_offsets(body, motif_bytes)
|
||||||
|
for motif_name, motif_bytes in IMMORTALITY_BODY_MOTIFS
|
||||||
|
}
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"entry_index": row.entry_index,
|
||||||
|
"class_name": label,
|
||||||
|
"slot": row.slot,
|
||||||
|
"event_name_hint": row.event_name_hint or "",
|
||||||
|
"body_length": len(body),
|
||||||
|
"header": header,
|
||||||
|
"clause_terminators": body.count(0x7A),
|
||||||
|
"local_labels": body.count(0x5B),
|
||||||
|
"subheaders": subheaders,
|
||||||
|
"tail_fields": scan_body_field_tokens(body, tail_window=256),
|
||||||
|
"all_fields": scan_body_field_tokens(body),
|
||||||
|
"motif_hits": motif_hits,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
tsv_lines = [
|
||||||
|
"entry_index\tclass_name\tslot\tevent_name_hint\tbody_length\theader_open_arg\theader_target\theader_label\theader_event_code\tclause_terminator_count\tlocal_label_count\tsubheader_count\tsubheader_targets\ttail_fields\tall_fields\tmotif_counts\tmotif_offsets"
|
||||||
|
]
|
||||||
|
for record in records:
|
||||||
|
header = record["header"] or {}
|
||||||
|
motif_hits = record["motif_hits"]
|
||||||
|
tsv_lines.append(
|
||||||
|
"{entry_index}\t{class_name}\t0x{slot:02X}\t{event_name_hint}\t{body_length}\t{header_open_arg}\t{header_target}\t{header_label}\t{header_event_code}\t{clause_terminators}\t{local_labels}\t{subheader_count}\t{subheader_targets}\t{tail_fields}\t{all_fields}\t{motif_counts}\t{motif_offsets}".format(
|
||||||
|
entry_index=record["entry_index"],
|
||||||
|
class_name=record["class_name"],
|
||||||
|
slot=record["slot"],
|
||||||
|
event_name_hint=record["event_name_hint"],
|
||||||
|
body_length=record["body_length"],
|
||||||
|
header_open_arg=(f"0x{header['open_arg']:02X}" if header else ""),
|
||||||
|
header_target=(f"0x{header['target']:04X}" if header else ""),
|
||||||
|
header_label=(header.get("label", "") if header else ""),
|
||||||
|
header_event_code=(f"0x{header['event_code']:02X}" if header and header.get("event_code") is not None else ""),
|
||||||
|
clause_terminators=record["clause_terminators"],
|
||||||
|
local_labels=record["local_labels"],
|
||||||
|
subheader_count=len(record["subheaders"]),
|
||||||
|
subheader_targets=",".join(
|
||||||
|
f"0x{offset:04X}->0x{target:04X}" for offset, target in record["subheaders"]
|
||||||
|
),
|
||||||
|
tail_fields=",".join(record["tail_fields"]),
|
||||||
|
all_fields=",".join(record["all_fields"]),
|
||||||
|
motif_counts=",".join(
|
||||||
|
f"{motif_name}:{len(motif_hits[motif_name])}" for motif_name, _ in IMMORTALITY_BODY_MOTIFS
|
||||||
|
),
|
||||||
|
motif_offsets=",".join(
|
||||||
|
f"{motif_name}={format_offset_list(motif_hits[motif_name])}" for motif_name, _ in IMMORTALITY_BODY_MOTIFS if motif_hits[motif_name]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(out_dir / "immortality_body_structure.tsv").write_text("\n".join(tsv_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
md_lines = [
|
||||||
|
"# Immortality Body Structure",
|
||||||
|
"",
|
||||||
|
"This report decodes one layer deeper than the literal scan for the surviving EVENT and NPCTRIG frontier.",
|
||||||
|
"It is still heuristic: the output is limited to repeatable byte grammar, subheader boundaries, field-tag trailers, and motif offsets that can be cross-checked against the 000d slot-backed runtime lane.",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
for record in records:
|
||||||
|
header = record["header"] or {}
|
||||||
|
motif_hits = record["motif_hits"]
|
||||||
|
md_lines.extend([
|
||||||
|
f"## {record['class_name']} slot `0x{record['slot']:02X}`",
|
||||||
|
"",
|
||||||
|
f"- Body length: `{record['body_length']}` bytes.",
|
||||||
|
f"- Open header: `0x5A 0x{header['open_arg']:02X} 0x5C 0x{header['target']:04X}` -> `{header.get('label', '')}` with embedded event-code byte `{f'0x{header['event_code']:02X}' if header.get('event_code') is not None else '-'}`." if header else "- Open header: not recognized by the current heuristic.",
|
||||||
|
f"- Clause terminators (`0x7A`): `{record['clause_terminators']}`; local labels (`0x5B`): `{record['local_labels']}`.",
|
||||||
|
f"- Internal labeled subheaders (`0x53 0x5C <u16> {record['class_name']}`): `{len(record['subheaders'])}` -> {', '.join(f'`0x{offset:04X}->0x{target:04X}`' for offset, target in record['subheaders'][:12]) or '`-`'}." ,
|
||||||
|
f"- Tail field tags: {', '.join(f'`{value}`' for value in record['tail_fields']) or '`-`' }.",
|
||||||
|
"",
|
||||||
|
"| Motif | Count | First Offsets |",
|
||||||
|
"|---|---:|---|",
|
||||||
|
])
|
||||||
|
for motif_name, _ in IMMORTALITY_BODY_MOTIFS:
|
||||||
|
offsets = motif_hits[motif_name]
|
||||||
|
md_lines.append(
|
||||||
|
f"| `{motif_name}` | {len(offsets)} | `{format_offset_list(offsets) or '-'}` |"
|
||||||
|
)
|
||||||
|
md_lines.append("")
|
||||||
|
|
||||||
|
event_slot_0a = next((record for record in records if record["class_name"] == "EVENT" and record["slot"] == 0x0A), None)
|
||||||
|
npctrig_slot_0a = next((record for record in records if record["class_name"] == "NPCTRIG" and record["slot"] == 0x0A), None)
|
||||||
|
npctrig_slot_20 = next((record for record in records if record["class_name"] == "NPCTRIG" and record["slot"] == 0x20), None)
|
||||||
|
if event_slot_0a and npctrig_slot_0a and npctrig_slot_20:
|
||||||
|
npctrig_slot_0a_header = npctrig_slot_0a.get("header") or {}
|
||||||
|
npctrig_slot_20_header = npctrig_slot_20.get("header") or {}
|
||||||
|
md_lines.extend([
|
||||||
|
"## Current Read",
|
||||||
|
"",
|
||||||
|
f"- `EVENT 0x0A` is the generic hub-shaped body: it has `{len(event_slot_0a['subheaders'])}` internal labeled subheaders and the widest field trailer (`{', '.join(event_slot_0a['tail_fields'])}`).",
|
||||||
|
f"- `NPCTRIG 0x0A` is the compact player-trigger candidate: it reuses the same class-labelled open header and subheader grammar, but it stays constrained to `{', '.join(npctrig_slot_0a['tail_fields'])}` instead of the wider EVENT field set.",
|
||||||
|
f"- `NPCTRIG 0x20` keeps the same constrained field set as `NPCTRIG 0x0A` and changes only the embedded prolog event-code byte (`{f'0x{npctrig_slot_20_header['event_code']:02X}' if npctrig_slot_20_header.get('event_code') is not None else '-'}` vs `{f'0x{npctrig_slot_0a_header['event_code']:02X}' if npctrig_slot_0a_header.get('event_code') is not None else '-'}`), which fits a variant trigger/setup lane better than a separate generic hub.",
|
||||||
|
"- The repeated `0x53 0x5C <u16> LABEL` subheaders and dense `0x5B <u16>` local labels make these bodies look like inline clause streams rather than single flat payloads, which is consistent with the `000d:21ed -> 000d:22bc` runtime lane that copies variable-length inline bytes first and only then consumes compact metadata bytes plus streamed words.",
|
||||||
|
"- The surviving slot focus is still `0x0A`: both EVENT and NPCTRIG expose non-zero slot-`0x0A` bodies, and the runtime side has an exact offset-specialized masked wrapper for slot `0x0A` at `0005:2c35` (`entity_vm_context_try_create_mask_0400_slot0a_with_offset`).",
|
||||||
|
])
|
||||||
|
(out_dir / "immortality_body_structure.md").write_text("\n".join(md_lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def readable_neighbor_chunks(
|
def readable_neighbor_chunks(
|
||||||
center: ExtractedChunk,
|
center: ExtractedChunk,
|
||||||
chunk_by_index: dict[int, ExtractedChunk],
|
chunk_by_index: dict[int, ExtractedChunk],
|
||||||
|
|
@ -2211,6 +2948,9 @@ def write_summary(out_dir: pathlib.Path, input_path: pathlib.Path, data: bytes,
|
||||||
write_readable_template_reports(out_dir, descriptor_chunks, chunk_by_index, len(chunks))
|
write_readable_template_reports(out_dir, descriptor_chunks, chunk_by_index, len(chunks))
|
||||||
write_runtime_bridge_reports(out_dir, descriptor_chunks, chunk_by_index, len(chunks))
|
write_runtime_bridge_reports(out_dir, descriptor_chunks, chunk_by_index, len(chunks))
|
||||||
write_runtime_family_bridge_reports(out_dir, descriptor_chunks)
|
write_runtime_family_bridge_reports(out_dir, descriptor_chunks)
|
||||||
|
write_immortality_target_body_scan(out_dir, parsed_class_chunks, rows_by_entry, raw_data_by_entry)
|
||||||
|
write_immortality_body_structure_report(out_dir, parsed_class_chunks, rows_by_entry, raw_data_by_entry)
|
||||||
|
write_npctrig_clause_report(out_dir, parsed_class_chunks, rows_by_entry, raw_data_by_entry)
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
lines.append("# EUSECODE.FLX First-Pass Extraction")
|
lines.append("# EUSECODE.FLX First-Pass Extraction")
|
||||||
|
|
@ -2269,6 +3009,8 @@ def write_summary(out_dir: pathlib.Path, input_path: pathlib.Path, data: bytes,
|
||||||
lines.append("- `vm_mask_ladder.tsv` records the current `entity_vm_context_try_create_masked_for_entity` wrapper ladder in machine-readable form so gameplay mask lanes can be compared against descriptor-side families without reopening the notes.")
|
lines.append("- `vm_mask_ladder.tsv` records the current `entity_vm_context_try_create_masked_for_entity` wrapper ladder in machine-readable form so gameplay mask lanes can be compared against descriptor-side families without reopening the notes.")
|
||||||
lines.append("- `readable_script_ir.md` and `readable_script_ir.tsv` join descriptor neighborhoods, the verified VM IR, the runtime owner/source path, and the current mask-family hints into one conservative script-facing bridge artifact.")
|
lines.append("- `readable_script_ir.md` and `readable_script_ir.tsv` join descriptor neighborhoods, the verified VM IR, the runtime owner/source path, and the current mask-family hints into one conservative script-facing bridge artifact.")
|
||||||
lines.append("- `runtime_descriptor_family_rankings.md` and `runtime_descriptor_family_rankings.tsv` rank descriptor families against the verified runtime lanes so the current human-readable script bridge is searchable by family fit rather than only by neighborhood dumps.")
|
lines.append("- `runtime_descriptor_family_rankings.md` and `runtime_descriptor_family_rankings.tsv` rank descriptor families against the verified runtime lanes so the current human-readable script bridge is searchable by family fit rather than only by neighborhood dumps.")
|
||||||
|
lines.append("- `immortality_target_body_scan.md` and `immortality_target_body_scan.tsv` now scan the strongest current immortality candidates (`EVENT`, `NPCTRIG`, `_BOOT`, `SFXTRIG`, `SPECIAL`, `TRIGPAD`) for inline `0x410` literals and record the tightest remaining active-event template frontier.")
|
||||||
|
lines.append("- `immortality_npctrig_clauses.md` and `immortality_npctrig_clauses.tsv` now split the compact `NPCTRIG` slot `0x0A` / `0x20` bodies into prefix, clause, and tail regions so the event-bearing ladder can be compared against the typed/setup companion body without reopening raw hex.")
|
||||||
(out_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
(out_dir / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
all_strings = iter_printable_runs(data)
|
all_strings = iter_printable_runs(data)
|
||||||
|
|
|
||||||
1129
tools/poc_crusader_usecode_parser.py
Normal file
1129
tools/poc_crusader_usecode_parser.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -214,6 +214,15 @@ JSON_SCHEMAS = {
|
||||||
"list-classes": {"type": "array", "items": CLASS_SCHEMA},
|
"list-classes": {"type": "array", "items": CLASS_SCHEMA},
|
||||||
"run-script": STATUS_SCHEMA,
|
"run-script": STATUS_SCHEMA,
|
||||||
"apply-plan": STATUS_SCHEMA,
|
"apply-plan": STATUS_SCHEMA,
|
||||||
|
"annotate-usecode": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["annotated", "skipped", "files"],
|
||||||
|
"properties": {
|
||||||
|
"annotated": {"type": "integer"},
|
||||||
|
"skipped": {"type": "integer"},
|
||||||
|
"files": {"type": "integer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -523,6 +532,31 @@ def build_parser() -> argparse.ArgumentParser:
|
||||||
help="Validate and print the plan without modifying the project.",
|
help="Validate and print the plan without modifying the project.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
annotate_usecode_parser = subparsers.add_parser(
|
||||||
|
"annotate-usecode",
|
||||||
|
aliases=["annotate_usecode"],
|
||||||
|
help="Import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors.",
|
||||||
|
)
|
||||||
|
annotate_usecode_parser.add_argument(
|
||||||
|
"--ir-file",
|
||||||
|
dest="ir_files",
|
||||||
|
metavar="IR_FILE",
|
||||||
|
action="append",
|
||||||
|
required=True,
|
||||||
|
help="Path to an IR JSON file produced by poc_crusader_usecode_parser.py. May be given multiple times.",
|
||||||
|
)
|
||||||
|
annotate_usecode_parser.add_argument(
|
||||||
|
"--comment-type",
|
||||||
|
choices=["pre", "plate", "eol", "repeatable", "post"],
|
||||||
|
default="plate",
|
||||||
|
help="Ghidra comment type to use for anchor annotations (default: plate).",
|
||||||
|
)
|
||||||
|
annotate_usecode_parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Print what would be annotated without modifying the project.",
|
||||||
|
)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -947,6 +981,67 @@ def _print_plan(plan: dict) -> None:
|
||||||
print(json.dumps(plan, indent=2, sort_keys=True))
|
print(json.dumps(plan, indent=2, sort_keys=True))
|
||||||
|
|
||||||
|
|
||||||
|
def command_annotate_usecode(config: ProjectConfig, args: argparse.Namespace) -> int:
|
||||||
|
"""Import USECODE IR JSON files and set Ghidra comments on compiled anchor addresses."""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
# Collect all annotation records from every IR file.
|
||||||
|
pending: list[tuple[str, str]] = [] # (address, comment_text)
|
||||||
|
for ir_path_str in args.ir_files:
|
||||||
|
ir_path = Path(ir_path_str).resolve()
|
||||||
|
if not ir_path.is_file():
|
||||||
|
raise RuntimeError(f"IR file not found: {ir_path}")
|
||||||
|
ir = _json.loads(ir_path.read_text(encoding="utf-8"))
|
||||||
|
cls = ir.get("class", {})
|
||||||
|
evt = ir.get("event", {})
|
||||||
|
hints = ir.get("annotation_hints", {})
|
||||||
|
class_name = cls.get("class_name", "?")
|
||||||
|
slot = evt.get("slot", 0)
|
||||||
|
event_name_hint = evt.get("event_name_hint") or f"slot_0x{slot:02X}"
|
||||||
|
body_start = evt.get("derived_body_start", 0)
|
||||||
|
body_end = evt.get("derived_body_end", 0)
|
||||||
|
raw_word = evt.get("raw_event_entry_word", 0)
|
||||||
|
for anchor in hints.get("compiled_anchors", []):
|
||||||
|
address = anchor.get("address", "")
|
||||||
|
role = anchor.get("role", "")
|
||||||
|
if not address:
|
||||||
|
continue
|
||||||
|
comment = (
|
||||||
|
f"POC USECODE: {class_name} slot=0x{slot:02X} [{event_name_hint}]"
|
||||||
|
f" body=0x{body_start:04X}..0x{body_end:04X}"
|
||||||
|
f" raw_word=0x{raw_word:04X} | {role}"
|
||||||
|
)
|
||||||
|
pending.append((address, comment))
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
for address, comment in pending:
|
||||||
|
print(f"DRY-RUN {address} {comment}")
|
||||||
|
return _emit(
|
||||||
|
args,
|
||||||
|
{"annotated": 0, "skipped": len(pending), "files": len(args.ir_files)},
|
||||||
|
f"dry-run: {len(pending)} annotations would be written from {len(args.ir_files)} file(s)",
|
||||||
|
)
|
||||||
|
|
||||||
|
annotated = 0
|
||||||
|
skipped = 0
|
||||||
|
with open_program(config, read_only=False) as (project, program):
|
||||||
|
with transaction(program, "USECODE annotation import"):
|
||||||
|
for address, comment in pending:
|
||||||
|
try:
|
||||||
|
set_comment(program, address, comment, args.comment_type)
|
||||||
|
annotated += 1
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"SKIP {address}: {exc}")
|
||||||
|
skipped += 1
|
||||||
|
save_program(project, program)
|
||||||
|
|
||||||
|
return _emit(
|
||||||
|
args,
|
||||||
|
{"annotated": annotated, "skipped": skipped, "files": len(args.ir_files)},
|
||||||
|
f"annotated {annotated} anchors ({skipped} skipped) from {len(args.ir_files)} file(s)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int:
|
def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int:
|
||||||
plan = _load_plan(args.plan)
|
plan = _load_plan(args.plan)
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
|
@ -1037,6 +1132,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
"get-function-xrefs": command_get_function_xrefs,
|
"get-function-xrefs": command_get_function_xrefs,
|
||||||
"run-script": command_run_script,
|
"run-script": command_run_script,
|
||||||
"apply-plan": command_apply_plan,
|
"apply-plan": command_apply_plan,
|
||||||
|
"annotate-usecode": command_annotate_usecode,
|
||||||
}
|
}
|
||||||
return command_map[args.command](config, args)
|
return command_map[args.command](config, args)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue