- 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.
35 KiB
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:
- current verified state,
- active blockers,
- next resume work,
- 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-local0x4f38bit-0x40lane, the seg126 control stream is confirmed as file-backed, the paired0x8c5c/0x8c60renderer objects are narrowed to two script-selected preset text lanes, and the neighboring seg127 fade controller now has an exact local contract at0x630a..0x6316. - The current VM/loader batch also justified a small bump:
000d:ebe3is now a named ordered opcode sequencer with a tighter entry/exit contract, the masked-create hub at000d:463ais now a verified owner-table gate rather than an inferred wrapper sink, and the seg070 twin loops underentity_vm_runtime_owner_resource_createnow 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..0x14with verified mixed payload shapes (nonevs extra signed word), the new slot-only Ghidra names keep that taxonomy visible without overpromoting event labels, and the000d:22bcstage 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/0x00000410literal, and the remaining frontier is narrowed to data-driven decoding ofEVENTslot0x0aplusNPCTRIGslots0x0a/0x20rather 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 + 2exactly, the class header/subentry math at000d:5066/51fd/53b4is closed against the extractor's raw headers and event rows, and the surviving immortality uncertainty has moved fromcan the loader fit NPCTRIG arithmetic at all?to the narrowerwhich 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.csvnow exists for all 145 NE segments and should remain the primary coverage tracker.crusader_decompilation_notes.mdis now only an index; detailed evidence lives indocs/.- 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 onentity_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:0x0007and0x2000:0x0015in 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 guarded0x0010:0x0008materializer that simply returns0on miss after readiness checks. - The USECODE/VM lane now also has a wider verified higher-slot wrapper ladder: the
0005island reaches slot ordinals0x10..0x14, slot0x12is a zero-extra-word lane, slots0x11/0x13/0x14carry extra-word payloads, and the current safest read isslot-stable payload-shape taxonomyrather than direct event-name promotion. - The same higher-slot batch now has its first outward binary anchors: slot
0x12wrapper0005:3171is directly called at0005:1776and0005:1945, the slot0x10guarded lane at0005:3115..3129is still fenced by the0005:30f2..3113class-nibble-4check, and the dark slot0x0a/0x0bwrappers are now instruction-verified as exact signed-additive shims over masks0x00000400/0x00000800even though their outward callers remain unrecovered. - The compiled-side immortality lane is slightly tighter too:
000b:b3b1/000b:b62care now a cheat-event listener constructor/handler pair for the shared cheat/control bundle rather than a hidden0x410producer, and the extractor-sideTELEPADslot-0x20raw_code_offset = 0x00000410hit 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:8c56is now a verified generic event-object dispatcher reading the emitted event id from field+0x6, seg109 helper000b:3d2ais now comment-backed as generic listener-registration infrastructure rather than an emitter, and the strongest remaining player-trigger family is the event-bearingNPCTRIG/EVENTneighborhood rather thanTRIGPAD,SPECIAL,REB_PAD, orTELEPAD. - The immortality lane is tighter again after the extractor extension: generated report
USECODE/EUSECODE_extracted/immortality_target_body_scan.mdnow proves thatEVENT,NPCTRIG,COR_BOOT,REE_BOOT,SFXTRIG,SPECIAL, andTRIGPADbodies contain no inline little-endian0x0410, no dword0x00000410, and no byte-swapped0x1004; the best surviving frontier is now the monolithicEVENTslot0x0abody plus compactNPCTRIGslots0x0a/0x20. - The immortality lane is tighter again after the structure pass: new report
USECODE/EUSECODE_extracted/immortality_body_structure.mdnow showsEVENTslot0x0aas a broad hub clause stream (90internal0x53 0x5c <u16> EVENTsubheaders,383local labels, wideevent/item/source/dest/door/counter/counter2/link/time/post1/post2/floor/flicMantail), whileNPCTRIGstays compact (5subheaders for slot0x0a,1for slot0x20, with narrowreferent/event/item/item2vsreferent/typeNpc/item/item2tails). Current best surviving emitter frontier is thereforeNPCTRIGslot0x0awithNPCTRIGslot0x20as its nearest typed/setup companion, whileEVENTnow 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.mdnow fixes the open-header decode (NPCTRIG 0x0aevent-code byte0x11,NPCTRIG 0x20event-code byte0x01) and shows slot0x0aas a five-step fixed-width clause ladder (0x2fsubheader stride, backward-walking0x2ftargets, per-clausebranch_3f_0a+push_24_51+writeback_57_02motifs) while slot0x20stays typeNpc-heavy (10field_4b_fe_0fhits, nopush_24_51, nowriteback_57_02). The best remaining descriptor-side frontier is therefore no longer theNPCTRIGpair symmetrically; it is specificallyNPCTRIGslot0x0aas the live event-bearing ladder, with slot0x20as 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:5572proves the extra word carried by0005:2c35is additive (slot_value + offset),000d:21ednow has an exactA x Bmatrix contract (byte A = lead-word row count, byte B = shared target-list width), andNPCTRIGslot0x0ais the only surviving compact body that exposes a natural five-row additive selector family (0x0064/0x0093/0x00c2/0x00f1/0x0120, uniform stride0x2f) 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), while0005:2c35itself still has no recovered code or data xrefs. Stack setup at000d:208bhardcodes the000d:5572additive word to0, which does not match theNPCTRIGslot0x0aclause-start or target families. The remaining live selector frontier is therefore the still-overlapped000d:21edcaller frame rather than a normal caller of0005:2c35. - The immortality downstream-use follow-up weakens the remaining direct-selector hypothesis again:
000d:46ecstores the dynamic word from the000d:21edlane into context field+0x34, but000d:21ed -> 000d:22bcnever rereads+0x34or+0x32after creation. The durable uses are the object save/load path instead:000d:498fserializes only the derived low word at+0x10c,000d:4a78reloads that saved word as the additive argument to000d:5572, and000d:4c2d..4c4drebuilds+0x10c/+0x10efrom the live slot value plus that saved offset. The only recovered post-load consumers are a tiny sentinel predicate (FUN_0001_a772checks for exactly0000:0001) and a normalization block (FUN_0002_1860clamps0000:xxxxvalues below0x0080up to0x0080). No recovered compare or dispatch branch matches theNPCTRIGslot0x0aclause-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 the0x80-byte local buffer, whileentity_vm_context_load(000d:4a78) rebuilds the frame pointers, replaysentity_vm_slot_load_value_plus_offsetfrom saved(slot, additive_word), restores+0x10c/+0x10e, and refreshes owner-source pair+0x117/+0x119. That is stronger evidence forpost-selector persistence of derived value statethan 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..21ednow show the hidden pre-call layout explicitly: the seeded+0xd6/+0xd8stream is consumed asword slot_index,word add_a,word add_b,byte setup_len,byte inline_len, and000d:21d0pushesadd_a + add_bas the dynamic word later stored at context+0x34. The same window now proves the caller-side frame shape too: frame base iscaller + [caller+0xd4],[frame+0x0a/+0x0c]is the far pointer passed intoentity_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+0x34and reframes the open question one level earlier: where that frame-local far pointer is seeded from, and whether the summed stream pair still maps toNPCTRIGslot0x0aclause-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/+0xd8stream, popping a far-pointer dword from the caller stream at[caller+0xcc/+0xce], computingframe_base = caller + [caller+0xd4], and storing that dword at[frame_base + placement + 0x4/+0x6]. That means the000d:21edsource 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 is0x0006, and any survivingNPCTRIGlinkage 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:f6e8now shows that a new context's+0xcc/+0xcestream is seeded by copying a caller-supplied setup blob into the object-local buffer, while the slot/additive record fromentity_vm_slot_load_value_plus_offsetseeds the separate+0xd6/+0xd8lane and the owner-table row(+0x10/+0x12) + 0x0d*slot + 4is mirrored separately through0x39ca. Linear raw-byte recovery across000c:f98b..000d:000dalso closes the forward/reverse frame-record family around that lane:000c:fc4b..fcbbis the caller-stream -> frame blob producer that best matches inline-tail placement0x000a, while000c:ff1f..ff83is the frame -> caller-stream dword copier matching the000c:fbf7..fc47far-pointer writer at placement0x0006. 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 thisff1f/ff9f -> fbf7/fc4b -> 000d:21edpropagation chain repeats it, and whether that origin still maps specifically toNPCTRIGslot0x0aor to a broader predecoded VM workspace. - The next immortality pass closes the immediate far-pointer source classification too. Hidden raw bytes at
000c:fa2f..fa5brecover an inner opcode dispatcher on the seeded+0xd6/+0xd8lane, and the same case family now exposes non-recursive caller-stream seeders at000c:fd51,000c:fd91,000c:fdd1, and000c:fe11. The dword case at000c:fe11..fe59reads an inline dword literal from that control stream, subtracts4from[caller+0xcc], and writes the literal dword onto the caller stream before the recursiveff1f/fbf7replay family touches it. That means the immediate compiled-side source for the000d:21edsetup far pointer is now an inline VM control-stream literal, not an owner-row lookup or generic scratch buffer; any survivingNPCTRIGtie has to explain how slot0x0ais decoded into that literal-bearing stream upstream, while slot0x20still 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:46ecnow shows the owner-table row(+0x10/+0x12) + 0x0d*slot + 4feeding only the separate0x39ca[slot]mirror, while the live+0xd6/+0xd8control stream passed intoentity_vm_context_setupcontinues to come fromentity_vm_slot_load_value_plus_offset. The hidden000d:21edpre-call span is now explicit asword slot_index,word add_a,word add_b,byte setup_len,byte inline_len, and the000c:fa2fcase family now separates immediate literal seeders (000c:fd51byte,000c:fd91sign-extended byte->word,000c:fdd1word,000c:fe11dword) from the recursive replay stages (000c:ff1f,000c:ff9f). Current best read is thereforedecoded per-slot VM workspace plus frame replay, notdirect NPCTRIG clause stream, even thoughNPCTRIGslot0x0aremains the strongest surviving upstream descriptor family and slot0x20still 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/+0xd8buffer on a cache miss:000d:5066loads a slot header plus cached6-byte subentry table through the owner-resource wrapper000d:714c, and000d:5305..53d4then reads the selected subentry's byte range directly into a newly allocated value-object buffer at+0x0a/+0x0c, which000d:51fdreturns as the live far pair. That means the immediate workspace is file-backed owner-loaded slot data copied into memory before000c:fa2finterprets 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 toNPCTRIGslot0x0aor only to the broader owner-loaded descriptor workspace, with slot0x20still 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 extractedobject_indexexactly; the first class-header dword is now constrained as the extra-slot count beyond a fixed0x20base table; bytes8..11remain the first code-byte offset; and000d:53b4reads body windows using the same(word len, dword raw_code_offset, code_base)arithmetic emitted by the extractor.NPCTRIGtherefore now has exact owner-loaded body windows in the live runtime format: slot0x0a=0x00da..0x024e(373bytes) and slot0x20=0x024f..0x03a7(345bytes), whileEVENTslot0x0alikewise fits0x00d4..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 lane1..255with class bit0x0002clear ->entity_id + 0x8c7e,(2)class-nibble4lane ->class_byte_0x7e05 + 0x8c80,(3)fallback type lane ->type_word_0x7df9 + 0x8c7c.entity_vm_runtime_init_from_path_if_configuredseeds those bases cumulatively from0x6608..0x660e, and direct caller0005:295findependently reuses the same slot index to test owner-row bit0x0040. That strengthens the read that the compiled side sees category spans plus generic row-capability masks, not a hardNPCTRIG/EVENTclass-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 viaDS:0x2833, indexDS:0x283d), and it togglesDS:0x844(cheats_enabled) plus mirrorDS:0x6045, then emits event0x103. The actual user-visible immortality toggle is event0x410at000c:9703, which boolean-togglesDS:0x604fand posts the on/off notifications (gate =DS:0x844). The olderDS:0x6050lane atimmortality_activate(000c:8231) remains a secondary entity/process path, not the primary player immortality toggle. Hidden seg109 menu wrapperscheat_menu_open_from_current_slot(000b:9a86) andcheat_menu_open_modal(000b:9c0d) are now named and verified to constructcheat_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(file0x70d75/ relocation entry0x71d68) and000c:99dd/000c:99e1(file0xc99dd, seg126 chain0x25e1). The deferred0x42f -> 000c:99dd -> 000b:9c0ddesign 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 retailFILE\FLEX.C, line 83failure and dropped into the quit line, so0x42fis the wrong deferred context even though the address retarget itself was valid. The current live candidate is back on the direct0007:0d79 -> 000b:9a86retarget, but with a narrower wrapper patch at000b:9a8dthat preserves the leading mode byte1and only zeros the two ambiguous 16-bit parameters.
Recently Closed Or No Longer Live
ASYLUM.24is 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_defaultownsg_active_dispatch_entry_farptr, while seg049/seg126/seg138 helpers only borrow or clear the shared byte+0x40; the seg1080x4f38lane 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 root0x6ae:0x6b0, and the startup/display family continues to treat0x6aa:0x6acas an inherited mutable external/default base path. - The in-scope
0x31a2transition/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/0x11is still open, and the000c:db68overlap still blocks clean function hygiene fortransition_preentry_step_scripteven though it no longer blocks semantic recovery.
Live Blockers
- The oversized overlap rooted at
000c:db68still blocks clean recovery of the realtransition_preentry_step_scriptfunction object, even though it no longer blocks startup/display semantics. - The
0x4588callback 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. - The USECODE/VM sequencer still lacks the real upstream selector/caller path into
entity_vm_opcode_sequence_run, and wrappersentity_vm_context_try_create_mask_0400_slot0a_with_offset/entity_vm_context_try_create_mask_0800_slot0b_with_offsetremain outward-caller-dark even though their exact signed-additive(slot, mask)contracts are now closed, the generic masked hub at000d:463ais verified, and slot-0x12now has two concrete caller anchors at0005:1776/0005:1945. - High-value missing or weak function objects still exist in hot ranges such as
000b:2e00,0007:5a00, and000e:ffb0;000e:ffb0is now caller-side constrained to the overlapped video-frame chunk lane (00db/00dc) paired withanim_load_audio_frame, but the overlap still blocks clean recovery. - 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.
- The immortality/
0x410lane 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 beclass_id + 2, the header/subentry arithmetic at000d:5066/51fd/53b4matches extracted class headers and event rows exactly, andNPCTRIGslot0x0a/0x20now 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:45c5only maps entities into three generic category spans,000d:44dfseeds those spans from0x6608..0x660e,0005:295freuses the same slot index to test owner-row bit0x0040, and0005:2c35still 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/+0xd8control stream and then into the000c:fa2fliteral/replay lane.
Current Focus
- Continue the USECODE/VM lane where the verified masked-create hub (
000d:463a), the internal consumer blocks (000d:208b,000d:21ed), or the newly separatedextra-word masked materializersubfamily can still yield concrete caller, selector, or record-shape evidence rather than repeated direct-xref dead ends. - Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
- Use boundary repair only on active blockers with clear payoff, with
000c:db68now downgraded to optional hygiene unless it blocks adjacent work again. - Revisit the
0x4588callback object only when caller-side evidence is strong enough to support behavioral naming.
Next Resume Point
- Recover the real upstream caller/selector path into
entity_vm_opcode_sequence_run, most likely by finding the first non-recursive0x6714context-method caller or vtable dispatch site rather than by repeating raw xref queries that still return no direct edges. - Recover real caller roles for
entity_vm_context_try_create_mask_0400_slot0a_with_offsetandentity_vm_context_try_create_mask_0800_slot0b_with_offsetby treating them as the remaining dark members of the now-verified signed-additive masked-materializer subfamily and comparing them against the newly anchored slot-0x12caller pattern. - Tighten the newly surfaced higher-slot wrapper ladder around
0005:3115..31da, especially the two slot-0x12caller sites at0005:1776/0005:1945and the slot-0x10guarded callsite, so any future promotion toleaveFastArea/func11|cast/justMoved/AvatarStoleSomething/animGetHitis driven by binary caller behavior rather than by external tables alone. - Tighten the outward caller chains around the renamed seg006 masked helpers
entity_vm_context_try_create_mask_0008_slot30_with_offset(0006:0ba4) andentity_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. - Tighten the paired-file-family reading of the seg070 twin loops at
0009:67b6and0009:6916by recovering which temporary buffer and record schema each family populates behindentity_vm_runtime_owner_resource_create. - Promote additional ledger rows where the current docs already justify
Foothold,Partial, orDeep. - Revisit
0x4588only 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. - If the VM lane stalls again, revisit
000e:ffb0from the now-verified00db/00dccaller windows and try to recover an adjacent non-overlapped helper before attempting any boundary repair. - If the immortality lane is revisited, stay focused on
NPCTRIGslot0x0afirst, with slot0x20still treated as the typed/setup companion andEVENTonly 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 from000d:45c5into 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-loadedNPCTRIGandEVENTbody windows. - 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, and000d: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
NonetoFoothold/Partialwhere 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/0x11only 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:db68only when a cleantransition_preentry_step_scriptfunction 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_runand 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_offsetandentity_vm_context_try_create_mask_0800_slot0b_with_offset. - Keep separating owner-table-backed
0x39carows 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
0x4588so the allocator finalize path and callback emissions can receive behaviorally meaningful names. - Tighten the role of
allocator_phase_finalize_passonly 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_StopAllSFXand 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:ffb0and 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,0x4588object 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
- Startup/display transition lane
- VM / USECODE selector and loader lane
- Coverage-ledger refinement from already-verified notes
- High-value overlap repair (
000c:db68, then000e:ffb0when justified) 0x4588callback-object classification- Broader segment sweeps and second-pass data/relocation work
Evidence Anchors
Primary files backing this plan state:
crusader_segment_coverage_ledger.csvcrusader_decompilation_notes.mddocs/overview.mddocs/raw-porting-progress.mddocs/raw-0008-000c.mddocs/raw-000a-000d.mddocs/raw-000e.mddocs/far-call-targets.mddocs/usecode-roundtrip-ir.mddocs/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.