Crusader_Decomp/plan-mid.md
MaddoScientisto daa363c3d2 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.
2026-03-24 18:14:20 +01:00

35 KiB

Crusader Decompilation Mid-Project Plan

Purpose

This file is the live mid-project tracker for the Crusader decompilation effort.

Keep it focused on:

  1. current verified state,
  2. active blockers,
  3. next resume work,
  4. and the remaining path to a reasonably complete decompilation.

Detailed completed analysis belongs in the files under docs/, not in this plan.

Progress Snapshot

  • Overall useful decompilation progress: about 49%
  • Reasonable uncertainty band: about 44% to 52%
  • Top 100 far-call target coverage: about 80%
  • Segment spread with meaningful analysis: about 26% to 32%
  • Tooling maturity for continued work: about 77%

Why The Estimate Moves Slightly

  • Recent work materially improved semantic confidence inside the startup/display, cache/allocator, callback-object, and USECODE/VM lanes.
  • 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 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.
  • 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

Primary Tracking Assets

  • crusader_segment_coverage_ledger.csv now exists for all 145 NE segments and should remain the primary coverage tracker.
  • crusader_decompilation_notes.md is now only an index; detailed evidence lives in docs/.
  • The raw full-EXE porting workflow is stable for the verified seg001 and seg021 mappings.

Strong Or Stable Areas

  • seg001 gameplay/input/projectile work is deep enough to support verified raw-name ports.
  • raw 0007 rendering/camera/tile-visibility work is structurally strong.
  • 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.
  • 000e parser and animation subsystems have a real partial map.
  • 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

  • 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 segment coverage ledger is no longer a missing artifact; only refinement remains.
  • 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 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 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

  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 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 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. 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. 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

  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. 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. Revisit the 0x4588 callback object only when caller-side evidence is strong enough to support behavioral naming.

Next Resume Point

  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. 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. 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. 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. 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. 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. 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

1. Coverage And Tracker Completion

  • Promote the existing 145-row ledger from a seeded first pass into a trustworthy executable-wide coverage dashboard.
  • Sweep untouched segments cluster-by-cluster instead of one-off function hunting, using adjacency and call relationships.
  • Convert more segments from None to Foothold / Partial where current notes already support it.
  • Close the largest remaining hot-target gaps so the far-call ranking list stays representative of real coverage.
  • Keep the plan, docs, and ledger synchronized after each verified batch.

2. Startup/Display And Presentation Lane

  • Keep the startup/display lane closed unless new caller evidence materially changes its current model.
  • Classify the exact higher-level UI label of preset pair 0x10/0x11 only if stronger caller or string evidence appears.
  • Revisit the remaining seg049/seg108/seg138 naming ambiguity only when it supports a defensible behavioral rename rather than another structural pass.
  • Repair 000c:db68 only when a clean transition_preentry_step_script function object or adjacent active work makes the overlap fix worth the risk.

3. VM / USECODE / Scripting Lane

  • 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 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.
  • 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.
  • Keep event-label mapping conservative: only promote ScummVM event names where binary behavior and slot reuse agree.
  • Mature the reversible script IR until it can represent raw headers, event rows, payload forms, and unresolved opcodes without information loss.
  • Continue extracting readable descriptor-family artifacts, but treat them as evidence aids rather than rename authority.

4. Cache / Allocator / Callback-Object Lane

  • Finish classifying the object rooted at 0x4588 so the allocator finalize path and callback emissions can receive behaviorally meaningful names.
  • Tighten the role of allocator_phase_finalize_pass only where it intersects callback-object semantics or active runtime users.
  • Separate generic cache-manager mechanics from game-specific client behavior wherever caller evidence supports it.
  • Clarify remaining object-role names around tracked handles, dispatch-entry lifecycle helpers, and palette-backed state builders.
  • Keep _ASS_StopAllSFX and the resolved audio-import lane closed; do not treat it as an open blocker again.

5. Rendering, Palette, Animation, And UI Support Lanes

  • Finish the remaining caller-side semantics for raw 0007 rendering helpers, seg049 controller dispatch, seg108 sprite/object helpers, and seg137/138 palette state builders.
  • Revisit 000e:ffb0 and adjacent 000e video/animation overlap only when it blocks active analysis or offers a strong isolated win.
  • Expand the palette/VGA helper family only where it clarifies higher-level behavior rather than duplicating low-level helper names.
  • Keep validating startup/display assumptions against raw 0007/0008/000d caller behavior instead of renaming isolated helpers in a vacuum.

6. Boundary Repair And Function Hygiene

  • Create or repair missing function objects in the highest-traffic unresolved ranges first.
  • Fix only overlaps that block live lanes or high-caller targets.
  • Preserve conservative naming for repaired functions until direct caller or data evidence justifies promotion.
  • Continue rejecting disproven ports or stale hypotheses instead of preserving them in live work queues.

7. Data, Imports, And Resource-Format Coverage

  • Work through the deferred non-CALLF far-pointer relocations when they become necessary for object/table recovery.
  • Expand coverage of weakly mapped resource/data loaders such as FLEX-derived descriptors, tables, caches, and per-shape data files.
  • Cross-check current data-structure assumptions against external references like ScummVM only as supporting evidence, not as rename authority.
  • Keep external import identities synchronized with verified import-table evidence.

8. Completion Criteria

A reasonably complete decompilation state should mean:

  • most actively used subsystems are behaviorally named rather than only structurally named,
  • the major live blockers (000c:db68, 000e:ffb0, hot missing function objects, dark VM selector path, 0x4588 object role) are either resolved or reduced to low-impact residuals,
  • the far-call hot list has very few meaningful unknowns left,
  • the ledger gives a credible whole-program view rather than a sparse seed set,
  • and the remaining gaps are mostly long-tail cleanup, low-traffic helpers, or data polish instead of core architecture uncertainty.

Priority Order

  1. Startup/display transition lane
  2. VM / USECODE selector and loader lane
  3. Coverage-ledger refinement from already-verified notes
  4. High-value overlap repair (000c:db68, then 000e:ffb0 when justified)
  5. 0x4588 callback-object classification
  6. Broader segment sweeps and second-pass data/relocation work

Evidence Anchors

Primary files backing this plan state:

  • crusader_segment_coverage_ledger.csv
  • crusader_decompilation_notes.md
  • docs/overview.md
  • docs/raw-porting-progress.md
  • docs/raw-0008-000c.md
  • docs/raw-000a-000d.md
  • docs/raw-000e.md
  • docs/far-call-targets.md
  • docs/usecode-roundtrip-ir.md
  • docs/scummvm-crusader-reference.md

Update Rule

Update this file when one of the following happens:

  • the headline estimate changes materially,
  • a live blocker is resolved,
  • a subsystem moves from structural to behavioral understanding,
  • a segment cluster is promoted materially in the ledger,
  • or the next resume point changes enough that the current handoff would mislead the next pass.

Keep the file short. Move detailed completed analysis into the appropriate file under docs/ and leave only the current state, blockers, and forward path here.