Crusader_Decomp/plan-mid.md
2026-04-10 18:14:55 +02:00

50 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

Latest verified batch: docs/regret-hidden-debugger-investigation.md now also records the debugger-side cleanup pass after the first .unk loader/runtime split, the final exhaustive Regret-side caller sweep, and the first practical seeding/simulation model. The live REGRET.EXE database now has the source-pane constructor/pointer/draw/viewport methods, the source-buffer create/load/split/destroy chain, the breakpoint-table helpers, the current-entry push/pop helpers, the interpreter saved-farptr helpers (13f0:0000/003c), the interpreter-context create/init pair (13f0:00e8/0244), the shared slot-chunk accessor at 13f8:1d72, and the named interpreter wrapper at 13f8:10da that feeds usecode_debugger_interpreter_hook. The practical consequence is sharper than before: the remaining blocker is not can we export readable source or even can compiled usecode carry line numbers, because retail Remorse already can, and it is no longer what else in Regret might still be debugger-related. The remaining blocker is now narrowly where inside the already-identified interpreter dispatcher/runtime path does Regret seed the current-entry stack, and how can that same engine-side path be reused or reproduced for stable RUN/step behavior, with the strongest current simulation route now being a small in-process reuse of existing VM context data rather than blind external memory poking or offline-only source-file tricks.

Latest verified batch: docs/jp-remorse-hidden-debugger-investigation.md now records the first debugger-focused comparison pass on /ja/CRUSADER.EXE. Current best read is narrower than the No Regret result but still decision-relevant: the JP Win32 build clearly retains broad executable cheat/debug machinery, yet live byte searches on the active image found no hits for the classic hidden usecode-debugger UI bundle (Goto Line, Watch what?, Inspect what?, Global name, Search for, FILE NOT FOUND, Unable to open this file, Nothing to find, Not found, Done) even though the same method still recovers positive-control strings like JASSICA16, Immortality enabled., and Cheats are now active.. The practical consequence is that JP currently strengthens the broad Win32 cheat/debug preservation story, but not the JP preserved the missing retail debugger bootstrap theory; No Regret remains the stronger sibling-build anchor for the hidden-debugger unlock problem.

Latest verified batch: docs/regret-hidden-debugger-investigation.md now records the forcing-options pass as well as the structural recovery. Current best read is now split cleanly between the analytical and practical sides: analytically, Regret still matters because it recovers the missing writer/bootstrap and the live 1480:6972 vtable override; practically, it also matters because it is now the first build where a debugger bring-up looks realistically forceable without rebuilding the subsystem. The current ranked Regret-side forcing order is: small executable patch into usecode_debugger_open_modal or the break/step auto-open path first, live memory forcing only if the debugger object already exists second, and -u / custom EUSECODE.FLX only as a hybrid context-generator after code or memory has already armed the debugger. That means Regret is now both the best comparison anchor for retail and the best live hack target if the immediate goal is simply to make the menu appear.

Latest verified batch: docs/retail-debugger-entry-options.md now reopens the hidden-debugger entry question with the stronger current live database instead of treating it as only a patch-history problem. Current best read is now sharper in a way that affects next-step choice: fresh live data-use recovery still shows no recovered writer for 1478:659c/659e, fresh decompiles of usecode_debugger_open_for_current_unit, usecode_debugger_open_modal, usecode_debugger_gump_create, and usecode_debugger_handle_event confirm that the debugger UI/event bundle is real but only useful after a valid break-state object and gump already exist, and the retail -u override remains the lowest-risk non-EXE experiment surface without yet proving a script-visible bootstrap for that object. The practical consequence is that the preferred next move is no longer more speculative retail patching first; it is a focused No Regret / JP comparison for a surviving debugger bootstrap/writer, with -u-backed EUSECODE experiments held as the least invasive indirect test surface.

Latest verified batch: docs/startup-map-patch-file.md now closes the long-standing startup string Using map patch file. tightly enough to stop treating it as a vague debug/status artifact. Current best read is that Init_Everything prints that line only when static\fixed.dat exists, and the later fixed-map cache path then prefers the loaded static\fixed.dat archive handle over the base fixed.dat handle for map/fixed-object reads. The remaining uncertainty in this lane is now narrow: whether any later consumer does a finer-grained fallback/merge than the first recovered chooser, not what the startup line is referring to in the first place.

Latest verified batch: docs/psx/psx.md, docs/psx/map-rendering.md, docs/psx/map-viewer-plan.md, and docs/psx/art-binding-recovery.md now tighten the PSX render-side model another step in both Ghidra and the viewer exporter. The earlier DAT_800758d4 consumer finding remains intact and is still wired into the viewer-side cache path as explicit companionExtents metadata, but the bigger practical change in this batch is the first measured art-binding recovery pass for the viewer exporter: the PSX cache builder now treats large zero-block DAT_800758d8 constructor-placement bands as inherited-art candidates, first via same-map DAT_800758cc script-signature donors and then via a constrained nearest-donor fallback inside the current 0x003e..0x0064 family. That rebuild moved the scene set from 58,262 fallback items / 1,714 bundle-mapped items to 25,038 fallback items / 34,938 bundle-mapped items, making early representative maps such as 0, 9, and 43 mostly real-art while leaving map 104 and the remaining 0x0042 / 0x0055..0x0063 constructor-placement band as the clearest unresolved outliers. The practical remaining gap is therefore narrower now: not "why are most PSX scenes placeholders" but "what executable-backed alias/resource rule explains the remaining zero-block constructor-placement families without leaning on donor heuristics."

  • Overall useful decompilation progress: about 59%
  • Reasonable uncertainty band: about 56% to 64%
  • Top 100 far-call target coverage: about 86%
  • Segment spread with meaningful analysis: about 34% to 40%
  • Tooling maturity for continued work: about 83%

Measured live naming floor for CRUSADER.EXE right now:

  • total functions: 3032
  • non-anonymous functions: 1795
  • remaining FUN_/nullfn_ placeholders: 1237
  • raw named-function coverage: 59.2%
  • largest current anonymous segment clusters: 1000 (166), 10e8 (62), 1190 (35), 13e8 (23), 13c8 (22)

Why The Estimate Moved

  • The NE CRUSADER.EXE database now has materially more named functions, better caller-role coverage, and broader comment-backed provenance than when this tracker was first drafted.
  • The startup/display lane is no longer a top active section. Its outer ownership and control flow are stable enough that it should stay closed unless new caller evidence changes the model.
  • The cheat/debug lane is also much tighter: the jassica16 latch, the broader -laurie gate, the ~ runtime toggle, the F7-family overlays, the F10/Ctrl behavior, and the 0x410 CD-transfer-display branch are now separated well enough that this lane is mostly documentation and cleanup, not architecture recovery.
  • The USECODE/VM lane has moved from broad structure guesses to a partial runtime model: core loader/runtime helpers are named, owner-loaded slot arithmetic is verified against extracted corpora, several masked-create helpers have real contracts, and the major remaining uncertainty is now the upstream selector/caller path rather than the storage format itself.
  • The map-renderer crosswalk lane also removed a lot of lingering shape ambiguity by closing more controller/helper families directly from extracted corpora plus scene evidence.
  • The combat-tactic data lane is also now materially tighter: COMBAT.DAT is no longer just a named-tactic hint source, but a documented bytecode archive with stable per-record names, verified block structure, a decoded shipped opcode subset, and a practical family-level behavior map for the Dumb, Pivot, Advance, Careful, marker-shuttle, and step-out-shoot tactics.

Current Verified State

Primary Tracking Assets

  • crusader_segment_coverage_ledger.csv remains the main executable-wide coverage tracker and should be updated after each verified batch.
  • crusader_decompilation_notes.md is an index, not the place for long-form analysis.
  • CRUSADER.EXE remains the default live Ghidra target.
  • Verified CRUSADER-RAW.EXE work remains a supporting evidence base for ports, naming provenance, and caller/context cross-checks.

Strong Or Stable Areas

  • seg001 gameplay/input/projectile work is stable enough to support verified raw-name ports into live NE work.
  • The raw 0007 rendering/camera/tile-visibility lane has a strong structural map and now acts more as supporting evidence than as a primary unknown.
  • The 0008 dispatch-helper and 000c state/transition lanes have broad partial coverage, including enough caller-side structure to support practical NE naming work.
  • The VM/USECODE lane now also has one earlier compiled-side producer anchored beyond the old direct Item_GetDamaged / StorageDataProcess_Run callers: AreaSearch_CollideMove is now verified as a paired 0x20b / 0x20c collision-process producer, and the local seg031 queue helpers are named structurally in the live database.
  • That same collision-storage producer surface is now wider too: current direct callers are all movement/physics/animation-side (Item_LegalMoveToPoint, Item_LegalMoveToPointWithCollisionInfo, gravity, animation, supersprite, and fast-area gravity cleanup), and no verified non-collision producer reaches the 0x236 queue yet.
  • The movement/collision lane is tighter at the helper level too: the step-aware seg029 sweep wrappers, the seg031 release-side queue cleanup pair, and the adjacent seg090 directional cache-offset helper are now named in the live database, so the remaining uncertainty in this lane sits earlier in caller policy rather than in the local helper layer.
  • The startup/display lane is materially closed. Shared dispatch-entry ownership, seg126 file-backed control flow, seg127 fade control, and the surrounding palette/presentation helpers are now understood well enough that they should not stay in the live critical path.
  • The cheat/debug lane is mostly closed at the behavior level. The secret-sequence matcher, broader cheat gates, F7 overlays, F10 modifier path, Ctrl+L location popup, Ctrl+Q = 0x410 CD-transfer-display toggle, -debug, and -laurie are all separated far more cleanly than before.
  • The hidden usecode-debugger lane is now structurally understood as a layered orphaned subsystem: seg109 UI pieces, seg1408 break-state helpers, and the seg1418 interpreter handoff are no longer conflated.
  • The USECODE/VM lane now has a workable compiled-side model around entity_vm_runtime_create, entity_vm_runtime_owner_resource_create, entity_vm_context_create_from_slot_index, the masked-create hub at 000d:463a, the persistence/load helpers, and the owner-loaded slot/value arithmetic.
  • The owner-loaded body/range model is no longer speculative. Class-selection uses class_id + 2, header/subentry math matches extracted corpus output, and concrete body windows for NPCTRIG, EVENT, and related families are now verified.
  • The map-renderer/documentation lane now has a stronger shape/controller crosswalk. Recent closures include CRUMORPH, NPC_ONLY, WATCHNS, WATCHEW, CRYOBOX, CRAZYEW, CRAZYNS, VIDEOBOX, PANELEW, GENERATR, and cross-game DEATHBOX, with viewer-side links kept conservative where actor-side state is still runtime-only.
  • The command-line/startup lane is much tighter across both games: -warp <mission> [x y z], -mapoff, -egg, startup teleporter selection, and the -u EUSECODE root override all now have practical behavior models instead of folklore-level descriptions.
  • The PSX lane is no longer just side inventory. Retail/pre-alpha bundle loading, mission-briefing/passcode structure, the reduced-content pre-alpha disc, and now the retail map object's last projection stage all have dedicated notes and enough stable naming to support future targeted passes.
  • The Remorse class-lift preparation lane now has a usable document cluster: overall plan, candidate inventory, endpoint spec, ABI constraints, family notes for EntityDispatchEntry and SpriteNode, a conservative Entity family split, a VM runtime/owner-resource layout note, a compatibility-header draft, and one grouped resume index.
  • The same class-lift prep lane is now more execution-ready: the 0x4588 broker family has its own focused object note, the toolchain story has a dedicated fingerprint-evidence note, and there is now a concrete first-batch class-authoring checklist ready for the first MCP-backed namespace/struct/vtable pass.
  • The live Remorse VM class-lift lane also recovered from a decompiler breakage in Remorse::EntityVmRuntime::Create: the root cause was a hidden-return-storage allocator helper signature at 1000:42e2, Create now decompiles again, and the provisional /Remorse/EntityVmSlotEntry datatype now exists with the stable +0x1e..+0x24 buffer-pair fields named.
  • The live Remorse VM class-lift lane is tighter again: the old UsecodeProcess_* context lifecycle bodies at 1420:0eec, 1420:10b6, 1420:10da, 1420:1162, 1420:118f, and 1420:1278 now live under Remorse::EntityVmContext::{CreateFromSlotIndex, FreeBuffer, SyncGlobalValueAndDispatch, Destroy, Save, Load}, with short raw 000d: provenance comments preserved on each entry.
  • The same VM class-lift lane tightened one step further through local PyGhidra fallback once the live run_write_script(...) route still returned 404 No context found for request: /Remorse/EntityVmContext is now a real datatype, entity_vm_slot_entry_create_or_clear and InitSlotOwnerBuffers now carry EntityVmSlotEntry *, AcquireSlotForEntity now returns EntityVmSlotEntry *, and InitSlots / ReleaseSlots now take direct EntityVmRuntime * this.
  • That pass also made the remaining blocker more precise: Create still cannot hold a fully typed far this without reintroducing hidden __return_storage_ptr__ corruption, so it was restored to the verified split-word custom-storage signature instead of forcing a broken prettier form.
  • Tooling follow-up from that same batch is now clearer too: live MCP read-only Python is usable when Ghidra starts with PyGhidra enabled, but write-side repairs still had to fall back to closed-project local PyGhidra because MCP does not yet expose a constrained live write-script or equivalent custom-storage edit path.
  • The live VM class-lift lane tightened slightly again in-session: 1420:19fd Remorse::EntityVmRuntime::EnsureSlotChunkLoaded now carries a real EntityVmSlotEntry * local for the acquired slot path, so the slot-entry cache tail fields decompile directly instead of through anonymous undefined4 pairs.
  • The matching MCP gap is also clearer now: the old apply_class_layout dry-run null failure no longer reproduces for /Remorse/EntityVmContext, but the real write path still behaves like the older storage-preserving build. Actual apply_class_layout and direct set_function_this_type calls on the context lifecycle methods still fail with Storage size does not match data type size: 2, and live run_write_script(...) still returns 404 No context found for request even with explicit target selectors.
  • Closing the GUI and dropping to the local PyGhidra fallback then landed the blocked context typing work cleanly: CreateFromSlotIndex, FreeBuffer, SyncGlobalValueAndDispatch, Destroy, Save, and Load now all carry EntityVmContext * this as their first parameter in CRUSADER.EXE, which confirms the newer dynamic-storage rewrite is sound even though the live MCP session still is not taking it.
  • The next live verification pass tightened two details. First, the new checked-in storage-aware prototype endpoint still is not the build currently serving the active GUI session: direct live POSTs to /set_function_prototype_storage still answered with the legacy set_function_prototype failure body, and the alias route still returned 404 No context found for request. Second, the direct callers of CreateFromSlotIndex still mostly consume the result as a base process object, so the current conservative UsecodeProcess * return should stay in place until the inheritance-aware datatype story is explicit.
  • The refreshed live MCP build moved that forward materially: set_function_prototype_storage(...) now reaches the real storage-aware implementation in-session and the active-program run_write_script(...) path now executes instead of failing with 404. The new blocker is narrower and more concrete: bare stack: offsets at 10 and above currently need 0x prefixes to preserve the intended stack slots, __cdecl16far still normalizes to plain __cdecl, and Create still cannot collapse to a single EntityVmRuntime * this because the datatype itself still resolves to a 2-byte pointer size.
  • The same live batch also tightened the slot-entry class model: /Remorse/EntityVmSlotEntry now carries match_key_farptr, owner_chunk_count, and owner_data_base in addition to the earlier owner-buffer and chunk-state tails, which makes InitSlotOwnerBuffers, AcquireSlotForEntity, and EnsureSlotChunkLoaded read more like object code and less like anonymous offset arithmetic.
  • The next live batch tightened the adjacent helper map too: the old unnamed 1420:1d72, 1420:1d8d, and 1420:1e17 helpers are now entity_vm_runtime_get_slot_chunk_ptr_at_offset, entity_vm_runtime_release_slot_chunk_ref, and entity_vm_runtime_try_unload_slot_chunk, which makes the slot-entry lifecycle around load, refcount release, and conditional unload materially easier to navigate.
  • The latest live batch turned that helper lane into a small shared record model: /Remorse/EntityVmLoadedChunkRecord now carries the stable next_*, saved_chunk_*, slot_index, and chunk_index anchors, entity_vm_runtime_try_unload_slot_chunk now takes EntityVmLoadedChunkRecord * and returns byte in AL, and entity_vm_runtime_apply_to_matching_owner_rows now iterates over a typed loaded-chunk record instead of anonymous stack-pair scratch state.
  • The adjacent interpreter-side lane is slightly tighter too: local helper 1418:003c is now interpreter_pop_saved_farptr, and the only verified Interpreter_NextUsecodeOp release path at 1418:3330 is commented as a save/restore boundary around entity_vm_runtime_release_slot_chunk_ref instead of being left as anonymous stack traffic.
  • The live class-authoring state moved forward too: Remorse::EntityVmSlotEntry now exists as a real class owner in CRUSADER.EXE, CreateOrClear moved under it with an explicit this parameter and AX pointer return, and the runtime-local chunk helpers plus owner-row iterator/debug path now sit under Remorse::EntityVmRuntime instead of Global.
  • The next live pass improved the runtime class surface further: GetSlotChunkPtrAtOffset now carries the recovered runtime_farptr/slot_index/chunk_index/intra_chunk_offset signature and still returns its far pointer in DX:AX, while ApplyToMatchingOwnerRows now carries the recovered runtime_farptr/slot_index_filter/chunk_index_filter signature and still returns its boolean in AL.
  • The latest live pass removed the old runtime-wide 2-byte-this bottleneck for this cluster: Create, InitSlots, ReleaseSlots, DebugDumpSlotMemory, ReleaseSlotChunkRef, GetSlotChunkPtrAtOffset, TryUnloadSlotChunk, ApplyToMatchingOwnerRows, and EnsureSlotChunkLoaded now all accept an explicit 4-byte EntityVmRuntime * this through /Remorse/EntityVmRuntime *32 custom storage in-session. The remaining live type gap is narrower again: exact /Remorse/EntityVmSlotEntry *32 return/parameter typing still fails on AcquireSlotForEntity and InitSlotOwnerBuffers, so those positions are currently held as neutral dword placeholders instead of prettier but broken slot-entry pointer types.
  • That slot-entry gap is now closed too, and the pointer cleanup widened beyond the runtime core: AcquireSlotForEntity now returns EntityVmSlotEntry *32, InitSlotOwnerBuffers now accepts EntityVmSlotEntry *32, EntityVmOwnerResource::{Create,Destroy} now carry explicit 4-byte this, and the simple EntityVmContext lifecycle methods now do the same. The main remaining VM signature outlier is CreateFromSlotIndex, whose argument pack still needs caller-side recovery rather than just pointer-width cleanup.
  • The next family switch also landed: Remorse::UsecodeDebuggerBreakState now exists as a real class owner with a 0x2f2 provisional datatype plus a first method batch for construction, breakpoint gating, breakpoint table helpers, callstack helpers, and step-state helpers.
  • That debugger batch is already tighter than the initial shell: 1408:01a5 is now verified as BreakpointRemove, 1408:02f5 is now verified as CallstackPushFrame, breakpoint entries are recovered as 0x0b inline-name-plus-line records, and callstack entries are recovered as 0x15 inline-name-plus-three-dword records even though the trailing dword semantics remain open.
  • The next pass landed the debugger struct rewrite in-session too: /Remorse/UsecodeDebuggerBreakpointEntry, /Remorse/UsecodeDebuggerCallstackEntry, and the updated /Remorse/UsecodeDebuggerBreakState array layout now exist live instead of only in notes, and the only verified CallstackPushFrame caller now narrows those three trailing dwords to source_stream_target_farptr, current_frame_payload_farptr, and still-neutral aux_farptr.
  • The latest debugger class-lift pass closed two more bounded gaps without overpromoting semantics: 1408:0230 now lives under Remorse::UsecodeDebuggerBreakState::BreakpointFindFirstForUnitAtOrAfterLine as the breakpoint-table lower-bound helper for (unit_name, line_number) queries, and the retail vtable root at 1478:65ab is now resolved enough to show that MaybeBreakOnCurrentLine dispatches slot 0 into a shipped no-op stub while slot 1 currently returns zero through a second inert method.
  • The next debugger follow-up also closed the planned seg109 consumer pass: 13a0:0291 plus its helper 13a0:045c now show that the current callstack entry's +0x09 lane is a real source-stream cursor consumed byte-by-byte by the debugger formatter and that +0x0d is the paired current-frame payload context used for expression/watch rendering. The remaining open tail-field question is now mostly aux_farptr, not the first two dwords.
  • That naming decision is now landed live rather than only in notes: /Remorse/UsecodeDebuggerCallstackEntry now names offset +0x09 as source_stream_cursor_farptr with an in-session field comment, and CallstackPushFrame now carries the same parameter name in its signature. The debugger-family residue is therefore narrower again: mainly aux_farptr, plus whether the seg109 formatter helpers deserve stable names.
  • That last formatter-helper hesitation is now closed too. The seg109 consumer pair is no longer anonymous in-session: 13a0:0291 now lives as usecode_debugger_format_expression_to_shared_buffer, and 13a0:045c now lives as usecode_debugger_format_descriptor_expression. The debugger-family residue is therefore narrower again: mainly aux_farptr, plus any future evidence that the retail-stub callback slots ever had non-retail behavior.
  • The follow-up retail caller pass did not widen aux_farptr either. get_callers(1408:02f5) still reports only 1418:051d Interpreter_NextUsecodeOp, that caller still pushes literal zero for the trailing field, and the current seg109 formatter consumers still read only +0x09 and +0x0d. For now the right live result is to keep aux_farptr intentionally neutral rather than invent a prettier but weak name.
  • The next bounded class-family step landed too. Remorse::SpriteNode now exists live in CRUSADER.EXE, and the first strong 000b: batch is re-anchored into live 1360: by preserved offset delta from 000b:326e -> 1360:046e: Destroy (1360:046e), IsDirty (1360:0580), MarkDirty (1360:05a6), DispatchEvent (1360:0cb2), and UpdateAndDispatch (1360:12ee) are now class-owned with in-session provenance comments. The remaining SpriteNode work is narrower and safer than before: mainly the constructor side, the exact live anchor for GetOrTraverse, and later vtable/datatype authoring rather than basic family existence.
  • That same SpriteNode pass also moved beyond method ownership into datatype work: /Remorse/SpriteNodeBase now names child_or_next_farptr, local_x_offset, local_y_offset, and dirty_flags, and /Remorse/SpriteNodeVtable now exists as a provisional slot shell exposing +0x14, +0x18, +0x20, and +0x24.
  • The constructor side is now started too: 1360:036a lives as Remorse::SpriteNode::Create with an in-session caveat comment that preserves the remaining wrapper uncertainty. The live search for the old 000a:b988 GetOrTraverse anchor is still open, but the family no longer lacks a constructor-style entry outright.
  • That remaining traversal gap is now closed too. 1360:0955 now lives as Remorse::SpriteNode::GetOrTraverse, and the decompiler comment records the currently safest read of the helper: recurse over child-linked nodes, adjust the incoming query coordinates by the local offsets, and return either the matched node or the default sentinel through the out pointer. The main SpriteNode residue is therefore structural again: constructor-wrapper split, deeper slot naming, and subtype layout boundaries.
  • The next bounded-family start is now landed too. Remorse::CacheBackendObject exists live with 1250:0000 promoted as Create; the decompiler itself carries explicit old 0009:5600 segment metadata on that body, and the current comment records the 0x20-byte allocation plus file-handle/method-table initialization path. That family is still only at its constructor shell, but it is now a live class-lift lane instead of a pure inventory entry.
  • The broader Tier 1 Remorse class sweep is now closed too. EntityVmOwnerResource gained two real accessor wrappers in-session (QueryMaterializationSize and MaterializeChecked) plus a corrected outer-wrapper layout (0x14 bytes total, embedded file base at +0x00..+0x07, helper vtable at +0x08, owner-row table at +0x0c); CacheBackendObject gained the first two non-constructor class methods (LoadEntryTableFromManifest and InitFixedEntryTable) plus a tighter live layout read around +0x10/+0x14/+0x16/+0x18/+0x1c; and SpriteNode::DispatchEvent now ties concrete event codes to concrete vtable slots instead of generic placeholder slot names.
  • The next broader Remorse batch also has its first post-Tier-1 live foothold now. PresentationCallbackBroker is no longer note-only: 12d0:0513 and 12d0:0656 are now live as Remorse::PresentationCallbackBroker::{InitOnce, TeardownOnce} with comments tied directly to the 0x4588/0x458c/0x4590/0x4594/0x4595/0x45a6 lifecycle cluster. The same pass also clarified that WatchEntityController and DialogMenuObject still need a second re-anchor pass before any live authoring: first-pass searches on the obvious type/vtable/callback constants hit unrelated camera/process and controller-save functions rather than safe class-family matches.
  • That second pass is now partly closed. The old WatchEntityController create lane maps onto the live Camera_Init / Camera_CreateProcess cluster at 1180:0000/0045, so those functions now carry provenance comments instead of a weaker forced rename; DialogMenuObject still lacks a safe live re-anchor after a second search on the obvious 0x28b5/0x27ca/0x2843 leads; PresentationCallbackBroker now has its raw 0009:b1c3 finalize-phase caller re-anchored live as allocator_phase_finalize_pass plus two preserved live slot +0x0c callers at 1278:0616 and 1320:1588; CacheBackendObject gained SetEntryNameAndTag at 1250:0910; and the widened SpriteNode::Create caller map now shows that the 0x34 allocation path is the compact shared node constructor used by many GumpCreate_* wrappers.
  • The next planned pilot family also started for real: Remorse::EntityDispatchEntry now exists in-session with provisional /Remorse/EntityDispatchEntryBase and /Remorse/EntityDispatchEntryVtable datatypes, so this family is no longer just a note cluster. The remaining blocker is now concrete rather than vague: the current source note still points at older 0008: / 000d: anchors that are not yet ported back onto the live CRUSADER.EXE method objects, so the first base-method ownership move has to wait on that mapping step instead of being guessed.
  • That mapping step is now partially closed too. The older 0008:ba00 base cluster ports into live 11e0: by offset, and the first base-method batch now lives under Remorse::EntityDispatchEntry: InitBase, SetSourceType, SetEventTypeChecked, SetGroupId, Unlink, and IncrementGroupId. The next blocker on this family is therefore narrower again: not whether the pilot can move methods at all, but which live segments carry the remaining word-list, timed/periodic, and runtime-state methods from the older 0008: / 000d: notes.
  • The runtime-state follow-up is now partially closed too. FadeProcess_Create is explicitly tagged by the decompiler as old 000d:7e00, FUN_1440_0278 matches the old 000d:8078 release path by both offset delta and behavior, and both now live under Remorse::EntityDispatchEntry as InitRuntimeState and ReleaseRuntimeState with a new /Remorse/EntityDispatchEntryRuntimeState overlay datatype. That leaves the remaining EntityDispatchEntry pilot work in a narrower end-of-day state: mainly the word-list destroy lane and the timed/periodic constructor cluster, not the core base or runtime-state surfaces.
  • That pilot moved one more bounded step in-session too. The periodic/timed branch from the old 0008: note cluster is now re-anchored live onto 11e0: well enough to move six more methods under Remorse::EntityDispatchEntry: ConstructVtable3AD2 (11e0:14fb), ConstructVtable3AA6 (11e0:1814), SetUpdatePeriodAndReschedule (11e0:187e), TickPeriodic (11e0:1913), EnableActiveCounters (11e0:19e6), and DisableActiveCounters (11e0:1a33). Each now has an in-session provenance comment tying it back to the old 0008: anchor, so the remaining EntityDispatchEntry blocker is narrower again: the word-list-owned subtype still has no live function objects in the expected 11e0:2000..25a1 window, and a bounded boundary scan did not yet yield safe entries to promote.
  • That remaining EntityDispatchEntry blocker is now closed by a re-anchor correction. The expected 11e0:2000..25a1 window is not code in the current live database; the old 0008:da00..dfa1 word-list-owned subtype actually lives in the 11e8: MList_* cluster, with 11e8:0000 carrying explicit old 0008:da00 segment metadata in the decompiler. That full batch now also lives under Remorse::EntityDispatchEntry: SetWordList0408Terminated, FreeWordList, Destroy, EnsureWordListContains, AppendUniqueWord, RemoveWordValue, GetWordAt, SetWordAt, and FindUnflaggedWordById10, each with an in-session provenance comment. The remaining question on this pilot family is therefore modeling depth rather than location: whether the 11e8: word-list branch deserves its own explicit derived/overlay datatype instead of remaining a method cluster under the shared class owner.
  • CreateFromSlotIndex is no longer a raw anonymous pack either: the live signature now separates owner_source_farptr, pitemno_farptr, mode_flags, slot_index, value_add_offset, intra_chunk_offset, ucparam_farptr, and ucparamsize, with explicit AX:DX return storage restored even though the endpoint still textualizes the function conservatively as plain dword __cdecl.

Areas That Are No Longer Live Priorities

  • Startup/display transition recovery is no longer a front-line blocker unless overlap repair becomes necessary for adjacent work.
  • The general cheat/debug key matrix no longer needs broad exploratory work.
  • The -debug switch is no longer an open mystery; remaining work there is mostly sink-side cleanup and documentation.
  • The earlier executable-patch experiments around the hidden debugger are documented history, not a current decompilation priority unless new evidence changes the entry model.

Live Blockers

  1. The main remaining VM uncertainty is the real upstream selector/caller path into entity_vm_opcode_sequence_run and adjacent masked-create helpers. One earlier producer is now closed at AreaSearch_CollideMove for the 0x236 collision-storage family, but the owner-loaded class-family chooser and any broader non-collision producers are still upstream-dark.
  2. The dark masked-materializer wrappers still need caller-role recovery, especially the signed-additive slot-0x0a / slot-0x0b pair and the surrounding higher-slot wrapper ladder.
  3. The callback object rooted at 0x4588 still lacks a behaviorally safe subsystem name even though its allocation/finalize neighborhood is better constrained.
  4. A few hot or awkward function ranges still lack clean function objects or good boundaries, especially around 000c:db68, 000e:ffb0, and several caller-dense gaps in 0007, 000b, and 000e.
  5. Weakly covered resource/data-loader families and non-CALLF far-pointer relocations are still a second-pass blocker for some object/table recovery work.
  6. The segment ledger has improved, but it still trails the actual verified state in the notes and Ghidra database. Promoting known segments from documented evidence remains real work, not bookkeeping trivia.

Current Focus

  1. Keep the live NE CRUSADER.EXE lane as the default working surface, using raw/full-EXE and standalone-segment work only as supporting evidence.
  2. Keep the VM/USECODE lane focused on selector recovery, caller-role recovery, and record-shape confirmation rather than repeating storage-format validation that is already closed.
  3. Promote ledger coverage from existing verified notes before broadening into fresh executable-wide sweeps.
  4. Use overlap repair only where it unlocks an active high-payoff lane.
  5. Use the map-renderer/tooling lane to validate shape ids, map placements, and viewer semantics before promoting additional static-object names in Ghidra.
  6. Keep the PSX lane focused on the final state/variant/art bridge now that the first post-spawn interaction/reselection cluster is named; avoid broad renderer-side heuristics that bypass those runtime paths.

Next Resume Point

  1. Resume the hidden-debugger lane from docs/regret-hidden-debugger-investigation.md, docs/jp-remorse-hidden-debugger-investigation.md, and docs/retail-debugger-entry-options.md: use No Regret, not JP, as the primary sibling-build anchor and compare retail CRUSADER.EXE directly against the now-closed Regret bootstrap/state-hook/runtime map. The concrete next target is no longer another broad Regret sweep; it is the smallest retail analogue of Regret 1398:0000, the missing writer for retail 1478:659c/659e, and any retail interpreter-side handoff that could be reattached without the wider old patch chains.
  2. In parallel with that comparison, keep the -u / replacement-EUSECODE.FLX lane alive as the least invasive practical experiment surface: prefer monitor/computer, SURCAM*, and NPCTRIG families over generic container scripts, and look only for indirect compiled control bridges rather than assuming usecode can already construct the debugger state directly.
  3. Continue broad sweeps in the same 12f8 / 13c8 / 13f8 UI-gump neighborhood so the next write window can harvest more obvious virtual-slot and wrapper names before switching back to deeper caller-policy work.
  4. Resume from docs/ne-hole-filling-priorities.md only after the current UI edge stops yielding cheap structural wins; the immediate best candidates are the remaining anonymous sibling methods in the main-menu, quick-save/load/exit, and adjacent button-gump families. Current side-cluster progress: the live session now has named media/audio helpers (FlicPlayProcess_Destroy, FlicWaitProcess_Destroy, MusicPlayerProcess_RunNoop, MusicPlayerProcess_Destroy, AssProcess_Destroy, FlicWaitProcess_VtableSlot10TickAndMaybeAdvance, MusicPlayerProcess_VtableSlot10Noop, AssProcess_VtableSlot5ClearCreatedFlag, AssProcess_VtableSlot6SetCreatedFlag, VideoPlayer_InitializePlayback, VideoPlayer_OpenMediaFiles, VideoPlayer_AllocPlaybackBuffers, VideoPlayer_OpenMoviListAndPrimeStreams, VideoPlayer_StopAndDestroyWrapper, VideoPlayerProcess_VtableSlot11Noop, File_Exists, VideoPlayer_FormatErrorMessage, VideoPlayer_AdvanceChunkCursor, VideoPlayer_AdvanceChunkCursorWrapper, VideoPlayer_LoadAudioChunk, VideoPlayer_LoadVideoChunk, VideoPlayer_BlitDecodedFrame, Music_RestorePreviousTrackFromStack, Music_LoadStateAndReplayCurrentTrack, Music_SaveState, ASS_StoreInitCallbackState), a fully named savegame UI cluster (SavegameNameField_MapInputChar, SavegameMenu_Destroy, SavegameMenu_HandleKey, SavegameMenu_HandleSlotAction, SavegameSlot_DrawCornerDecorations, SavegameSlotGump_Create, SavegameSlotGump_Destroy, SavegameNameField_HandleKey, SavegameSlot_HandleClick, SavegameSlot_BeginEditOrActivate, SavegameNameField_Draw, SavegameSlot_Select, SavegameSlot_GetLabelPtr, SavegameSlot_SetLabel, File_CloseAndMaybeFree), a newly named main-menu shell (MainMenu_Destroy, MainMenu_DrawCornerDecorations, MainMenu_HandleButtonClick, MainMenu_HandleKey, MainMenu_ActivateSelection) plus one additional main-menu subcluster (MainMenuOptionsPanel_Create, MainMenuOptionButtonGump_Create, MainMenuOptionButtonGump_HandlePointerEvent, MainMenuOptionButtonGump_SelectPeer, MainMenuOptionButtonGump_Draw), a tightened help-gump subcluster (HelpGump_RefreshPage, HelpGump_HandleAdvanceAction, HelpGump_HandleNavigationKey, HelpGump_RunAmbientSfxTick), a cleaned-up 10f8: item-type helper pair (ItemScript_AppendBytes, ItemTypeflagRecord_ResetDefaults), a large ownership-backed process cleanup batch (MapJumpProcess_Destroy, FadeProcess1_Destroy, AnimProcess_Destroy, ItemProcess_Destroy, SuperSpriteProcess_Destroy, OneFrameDelayProc_Destroy, CameraProcess_Destroy, KeyDaemonProcess_Destroy, KeyboardProcess_Destroy, AccWaitProcess_Destroy, SystemTimerProcess_Destroy, BiosProcess_Destroy, CustomWaitProcess_Destroy, DumbTimerProcess_Destroy, CycleProcess_Destroy, FadeProcAlt_Destroy, MyTimerProcess_Destroy), a companion broad slot-method batch (MapJumpProcess_VtableSlot10AdvanceItemFind, AnimProcess_VtableSlot10DispatchByPort, FadeProcess2_VtableSlot10BlendTowardTargetPalette, AttackProcess_VtableSlot10DispatchByClip, WaitProcessFamily_VtableSlot10DispatchByPair, AccWaitProcess_VtableSlot10DispatchByAnimation, BiosProcess_VtableSlot10DosRealFarCall, CustomWaitProcess_VtableSlot11ArmAndRun, MyTimerProcess_VtableSlot10IncrementCounterOnTick, BaseCameraProcess_VtableSlot10SetViewportRect, BaseCameraProcess_VtableSlot11FreeBuffer), a broad UI/gump ownership cleanup batch (StdIntHandlerProcess_Destroy, GumpShared_DestroyNoop, KeyboardInputHandler_DestroyNoop, GumpShared_VtableSlot10Noop, KeyboardInputHandler_VtableSlot10Noop, KeyboardInputHandler_VtableSlot11Noop, ButtonGump_Destroy, KeypadGump_Destroy, KeypadButtonGump_Destroy, HelpGump_Destroy, RunCreditsProcess_Destroy, QuickSaveLoadExitGump_Destroy, Gump13f80383_Destroy, Gump13f80383_Draw), another structural process-family cleanup batch (AnimProcess_RunNoop, Process1048_0000_RunNoop, Process1048_0000_Destroy, AnimPrimitiveProcessSomethingElse_Destroy, AnimPrimitiveProcessFamily_VtableSlot11CallSlot3, Process1188_0000_RunOnTimerDelta, Process1188_0000_Destroy), and a final tiny conservative broad-sweep batch (SystemTimerProcess_RunNoop, Gump13f80383_VtableSlot10Noop, Gump13f80383_VtableSlot11Noop). The next defensible step can now keep sweeping broadly for ownership-backed leftovers, push deeper into subordinate menu/dialog families, or return to unfinished media helpers.

The latest micro-batch also corrected one structural naming mistake in the shared gump lane: GumpShared_VtableSlot3Noop, GumpShared_VtableSlot7Noop, GumpShared_VtableSlot8Noop, GumpShared_VtableSlot9Noop, GumpShared_VtableSlot16Noop, and GumpShared_VtableSlot17Noop now replace the older keyboard-only labels after direct table reuse showed those no-op slots are shared by help/menu/gump families.

The newest broad-sweep UI batch tightened three more local families without needing deeper subsystem claims: GumpShared_DestroyCommon is now the shared gump base destroy helper at 12f8:02e4; the quick save/load/exit modal now has QuickSaveLoadExitGump_Create, QuickSaveLoadExitGump_HandleChildButtonEvent, QuickSaveLoadExitGump_HandleKey, and QuickSaveLoadExitGump_DrawLabel; the adjacent main-menu options-panel wrapper lane now has MainMenuOptionsPanelButtonGump_Create, MainMenuOptionsPanelButtonGump_DrawLabel, MainMenuOptionsPanelButtonGump_Select, and MainMenuOptionsPanelButtonGump_Deselect; and a second 13c8: options-menu lane now has MainMenuOptionsMenu_{Create,Destroy,GetOptionRect,HandleChildButtonEvent,HandleKey,DrawTitle} plus MainMenuOptionsMenuButtonGump_DrawLabel. The next low-risk follow-up in this same neighborhood is therefore narrower again: remaining anonymous sibling methods in 13c8: / 13f8: and any matching button-gump virtual slots in 1308: that can be named structurally from local family behavior. 5. Stay on the VM lane and move one step earlier than the now-mapped movement/collision helper set around AreaSearch_CollideMove: the local seg029/031/090 helper layer is now named, so the next work is the policy/dispatch layer that decides when those legal-move, gravity, animation, or supersprite paths instantiate the local 0x236 collision-storage queue, plus verification of whether any non-collision producer feeds the same StorageDataProcess_Create / Run family. 6. Recover caller roles for the remaining dark signed-additive masked wrappers, especially the slot-0x0a / slot-0x0b pair, and compare them against the now-anchored slot-0x12 caller pattern. 7. Tighten the higher-slot wrapper ladder around 0005:3115..31da so future event-label promotion depends on compiled caller behavior instead of external tables. 8. Tighten the seg006 masked-helper caller chains so the local state-selector/value family can be tied to concrete gameplay subsystems. 9. Classify the paired seg070 loops behind entity_vm_runtime_owner_resource_create, especially which temporary buffers and record schemas each family populates. 10. Stay on the Remorse VM class-lift batch while the repaired runtime lane is warm: use the now-recovered CreateFromSlotIndex caller pack to decide whether any remaining scalar positions deserve stronger typedefs, but keep the return semantically conservative until the base-process inheritance model is explicit enough to justify a prettier live return type. 11. The current broader Remorse follow-up batch is now materially tighter: WatchEntityController is effectively re-identified as the live camera-process create lane, DialogMenuObject is the last compact family here without a safe live re-anchor, PresentationCallbackBroker now has install/teardown plus both slot +0x08 and preserved slot +0x0c caller evidence, CacheBackendObject has its indexed entry writer, and SpriteNode::Create now looks like the shared compact node constructor for GumpCreate_* wrappers. The clearest next unresolved items are therefore: a safer live reanchor for DialogMenuObject, a decision on whether the camera-process lane should stay under the stronger live Camera_* naming or also receive a class-owner layer, deeper slot +0x0c payload classification in the broker lane, and higher-level subtype/layout work above the compact SpriteNode base. 12. In the local GhidraMCP upgrade lane, add support for dual POST body decoding (application/json plus form-urlencoded) and a constrained live write-side PyGhidra endpoint family so future custom-storage/type repairs can stay inside the active MCP session when Python is enabled. 13. Promote additional ledger rows directly from already-verified docs and live comments, especially where segments already deserve Foothold, Partial, or Deep; the new seg029 step-aware sweep batch, seg031 queue-release batch, seg090 movement-helper batch, seg033 NPC-process foothold, and seg032 item-type foothold should be the immediate template. 14. If the VM lane stalls, revisit 000e:ffb0 from the now-better-constrained video/audio caller windows and try to recover an adjacent non-overlapped helper before attempting broad boundary repair. 15. Continue the map-renderer cross-check lane by building one conservative shape-id/map-placement crosswalk from shapedata_more_complete.txt, extracted corpora, and authored scene evidence before promoting more trigger-heavy classes in NE. 16. Keep the PSX pre-alpha lane alive as a secondary target: classify the LoadExec callers, test whether the stale TALK1.XA path is still reachable, and compare the shipped LSET1 bundles against the retail extractor outputs. 17. Continue the retail PSX state/art lane from the new art-binding recovery baseline: keep DAT_800758d4 on the runtime-bounds side unless new family-specific evidence contradicts it, treat map 104 plus the remaining 0x0042 / 0x0055..0x0063 zero-block constructor-placement band as the primary regression target, and trace the next family-specific callers around psx_type4_reselect_motion_state, FUN_80028c94, constructor-side resource creation, and the drawable-resource/frame submission lane until the remaining donor-based fallback logic can be replaced with an executable-backed alias/resource rule.

Remaining Work To Reach A Reasonably Complete Decompilation State

1. Coverage And Tracker Completion

  • Keep turning the seeded 145-row ledger into a trustworthy whole-program dashboard.
  • Sweep remaining lightly covered segment clusters by adjacency and call relationships rather than one-off function hunting.
  • Keep the plan, the docs, the ledger, and the live Ghidra comments synchronized after each verified batch.

2. VM / USECODE / Scripting Lane

  • Close the upstream selector/caller path into the sequencer and masked-create families.
  • Finish separating owner-row-backed data from runtime-decoded control streams and dispatch-entry seed records.
  • Expand caller-backed event-label promotion only where binary behavior and slot reuse agree.
  • Keep maturing the tooling bridge from extracted corpora into compiled-side annotation/import workflows.

3. Callback / Allocator / Object-Role Lane

  • Classify the 0x4588 callback object strongly enough for a real subsystem name.
  • Separate generic cache/allocator mechanics from game-specific client behavior where caller evidence supports it.
  • Keep low-level helper names conservative until behavior, not just structure, is clear.

4. Rendering / Animation / UI Support Lanes

  • Keep the rendering/palette/animation lanes focused on caller-side semantics and cleanup, not exploratory renaming in isolation.
  • Revisit 000e:ffb0 and adjacent overlap-heavy video helpers only when the payoff is clear.
  • Use map-renderer evidence and extracted corpora to validate static-object and helper/controller naming before promoting it into live NE work.

5. Data / Resource / Relocation Coverage

  • Tackle deferred non-CALLF far-pointer relocations when they are needed for active table/object recovery.
  • Broaden weakly covered resource/data-loader families where they block real subsystem classification.
  • Keep external references like ScummVM or older disasm corpora as evidence aids, not rename authority.

Priority Order

  1. VM / USECODE selector and caller recovery
  2. Coverage-ledger refinement from already-verified notes
  3. Callback-object classification around 0x4588
  4. High-value boundary repair when it unlocks active work
  5. Broader segment sweeps and second-pass data/relocation work
  6. Secondary map-renderer and PSX follow-up lanes

Evidence Anchors

Primary files backing this plan state:

  • crusader_segment_coverage_ledger.csv
  • crusader_decompilation_notes.md
  • docs/overview.md
  • docs/ne-hole-filling-priorities.md
  • docs/crusader-disasm-reference.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

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 this file short. Move detailed completed analysis into the appropriate file under docs/ and leave only the current state, blockers, and forward path here.