Crusader_Decomp/plan-mid.md

236 lines
47 KiB
Markdown
Raw Normal View History

# 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/`.
- `CRUSADER.EXE` is now the default live Ghidra target for ongoing work; verified `CRUSADER-RAW.EXE` results remain a cross-reference evidence base, especially for seg001/seg021 and earlier cheat/VM batches.
- The raw full-EXE porting workflow remains stable as a supporting evidence path 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 auxiliary local disassembly corpus at `K:/ghidra/crusader-disasm` is now inventoried and integrated as a separate evidence source for shape metadata, static map/object dumps, opcode names, and older Remorse/Regret intrinsic-function vocabularies; its safe-reuse rules and porting implications are captured in `docs/crusader-disasm-reference.md`.
- 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 first focused NE `CRUSADER.EXE` hole-filling pass tightens that same wall one step further without breaking it. In the live NE session, `0005:295f` is now confirmed as the only recovered non-hub consumer of `entity_vm_slot_index_from_entity`, and its only currently recovered callers are `0006:43c3`, `0006:c5f0`, and `0007:3584`. That gives the selector lane three concrete gameplay-side caller families to classify next, while `0005:2c35` remains outward-xref-dark and therefore still does not prove a class-family choice by itself.
- The next focused NE pass closes the first of those caller families structurally. Repaired wrapper `0006:4379` is now a verified seg031 dispatch-entry subtype gate over objects created by `0006:42d9` with event type `0x236`, source type `8`, subtype/tag at `+0x3c`, payload/source far pointer at `+0x32`, and aux words at `+0x36/+0x38`. Within that family, subtype `0x20c` at `0006:43c3` routes into `0005:295f`, while sibling subtype `0x20b` at `0006:43e5` routes into `0005:2918` using the same aux pair. That localizes the owner-row bit-`0x0040` consumer to one subtype-tagged dispatch-entry family, but still does not identify the upstream owner-loaded class family.
- The first doc-to-live-NE integration batch is now applied in the open `CRUSADER.EXE` database too. Comment-backed anchors landed on the live selector/core pair `1420:0dc5` / `1420:0e3a`, the consumer pair `10a0:2718` / `10a0:275f`, and the first closed caller-family runner `10f0:02d9` / `10f0:0379`, with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c -> 10a0:275f` and `0x20b -> 10a0:2718` split. This improves the live NE handoff without justifying a headline progress-estimate change yet.
- The compiled cheat/control lane is now split more cleanly. `cheat_code_check` (`0007:0d0a`) is still the sole hidden 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 matcher bytes themselves are now rechecked in the live NE image as scan codes `24 1e 1f 1f 17 2e 1e 02 07` = `j a s s i c a 1 6`, with the trailing digits specifically using top-row scan codes `0x02` / `0x07`. Live data-use recovery also tightens the latch story: `0x6045` is written only by `Key_CheckCheatToggle` (`1130:2b72`) and the event-`0x7e` runtime toggle at `13e8:203d`. The live NE F10 proof is stronger than the earlier folklore-level read: inside `Key_HandleOptionKeys` (`1130:0896`), the F10 cheat branch first checks `DAT_1478_085f`, then `0x6045`, then reaches `1130:0afd` and calls helper `11c8:01a8`; the `11c8:018a` helper call in the same function appears later at `1130:0cad`, in a different branch. The helper identity is now closed from the code too: `KeyboardGetExtendedShiftStates` (`11d0:39e6`) uses BIOS `INT 16h, AH=12h`, whose AH bits are `0=left Ctrl`, `1=left Alt`, `2=right Ctrl`, `3=right Alt`, so `11c8:01a8` testing `0x0100|0x0400` is really `KeyEvent_IsCtrlDown`, and `11c8:018a` testing `0x0200|0x0800` is really `KeyEvent_IsAltDown`. Upstream keyboard-path recovery also closes the practical behavior too: the held-key repeat builder at `11b8:0129..022b` samples BIOS extended-shift state through `11d0:39e6`, stores the current `31a4` modifier snapshot into each repeated `KeyEvent`, and queues that event through `11d0:3533`, so holding `F10` first and then pressing physical `Ctrl` lets later repeated F10 events reach the immortality branch with refreshed modifier bits. The same repeated F10 event synthesis plus missing debounce explains the multi-modal on/off spam. The F10 immortality sub-branch also only runs for a live current NPC (`NPC_IsDead` gate at `10e8:1fed`). `DAT_1478_085f` is now tighter too: it is set during `Game_Start` (`1020:0127`), cleared at the end of `ComputerGump_CreateGump` (`1398:01f5`), and restored by `ComputerGump_CloseAndResumeGameplay` (`1398:0212`) during the paired computer-gump teardown path before falling into generic gump cleanup. Current safest read is a broader gameplay-input / option-key-active state rather than any cheat-state bit. Separately, event `0x410` at `000c:9703` does **not** toggle immortality; it boolean-toggles `DS:0x604f` / `g_cdTransferDisplayActive` and posts the `CD TRANSFER DISPLAY ACTIVE/INACTIVE` notifications under the broader `0x844` gate, which matches both the user's runtime observation and the old `crusader-disasm` note `CTRL-Q = 0x410`. The older `DS:0x6050` lane at `immortality_activate` (`000c:8231`) remains a separate secondary entity/process path. The old seg109 "cheat menu" label is now narrowed further: in the live NE database, `000b:9a86`, `000b:9c0d`, `000b:b3b1`, `000b:b62c`, `000b:15ac`, `000b:0b52`, `000b:0b06`, and `000b:2882` now read more defensibly as `usecode_debugger_*` helpers, with menu labels like `Open Unit`, `View File`, `Watch`, `Inspect`, `Find`, and `Break to TDP`. Current best read is a hidden usecode debugger / unit inspector, not a retail scrollable cheat list. This also tightens the `-laurie` split: `-laurie` enables `0x844`-gated event cheats and debugger-side paths, but not the low-level `0x6045` keyboard latch, which matches the observed `F`-overlay-on / `F10`-refill-off behavior. 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` -> `usecode_debugger_open_for_current_unit`, `FUN_000b_9c0d` -> `usecode_debugger_open_modal`, `FUN_000b_b3b1` -> `usecode_debugger_gump_create`, `FUN_000b_b62c` -> `usecode_debugger_handle_event`, `FUN_000b_15ac` -> `usecode_debugger_load_unit_file`, `FUN_000b_0b52` -> `usecode_debugger_center_on_line`, `
- The `0x85f` reader side is now clearer too. The live NE database now names the paired `13e8` transition wrappers as `Game_DisableGameplayInputAndRefreshCamera` (`13e8:0e7d`) and `Game_RestoreGameplayInputAndClearModalState` (`13e8:0ef9`), which matches their concrete behavior: `13e8:0e7d` clears the controller/key-input latch `1478:27cb`, raises the modal overlay-suppression state at `1478:2c64` / `1478:8c53`, preserves `1478:8c54` from `1478:2d24`, and refreshes camera state; `13e8:0ef9` performs the inverse restore path and clears the secondary `1478:6050` latch. The Laurie-only wrapper side is clearer as well: `Game_ShowLaurieHintComputerGump` (`13e8:0e31`) is the hidden `-laurie` computer-gump hint path, while `Game_ShowLaurieHintIfGameplayInputActive` (`13e8:0f4a`) only calls it when `0x85f` is high. The main camera pass consuming the same gate is now `Camera_RedrawViewportAndGameplayOverlays` (`1180:19c1`), with comment-backed `1188:010f` / `1188:0394` overlay helpers bracketing the viewport redraw.
- The next blocker layer is narrower too. Those modal wrappers are not abstract helpers; inside `World_HandleKeyboardInput_13e8_14b4` they already wrap concrete user-facing lanes including exit-to-DOS confirmation (`0x22d`), quick save (`0x13f`), quick load (`0x13e`), restart/main-menu handling (`Game_RestartMaybe`), and the neighboring load/menu gump lanes. Separately, event `0x7e` remains the only other recovered writer of `0x6045` besides `Key_CheckCheatToggle`, so a successful `jassica16` match can still be undone later by that independent runtime path. `Key_CheckCheatToggle` itself is now comment-backed as keydown-only and still requires top-row `1` / `6` scan codes at the tail, leaving keypad digits and other non-matching input routes as a still-live explanation for failed tests.
- Cross-game verification against the currently opened `REGRET.EXE` now has a runtime correction too. The F10 branch at `1148:0d0e` still reaches the same modifier helper at `11e0:01a8`, and live testing shows the practical gesture is hold `F10` first and then press `Ctrl`, not `Alt`. The same BIOS-backed helper swap should be verified directly in that target before promoting renames there. The same runtime test also explains the repeated immortality popups: the F10 branch is not debounced, so holding the keys lets repeated F10 keydown events flip immortality on and off multiple times. The real gameplay difference remains the latch code: `1148:34d2` (`Key_CheckSecretCodeSequences`) still contains a `jassica16` table at `1480:2ff0`, but the latch-enabling sequence in No Regret is the second table at `1480:2ffc`, decoded as `loosecannon`, which toggles `1480:0ac0` and mirrors the result into the F10 latch byte `1480:009b`.
- Retail hidden-menu patching remains open, but the failed branches are now better separated from the current writable candidate. Verified file/fixup anchors are `0007:0d75` / `0007:0d79` (file `0x70d75` / relocation entry `0x71d68`) and `000c:99dd` / `000c:99e0` (file `0xc99dd`, seg126 chain `0x25e0`). The deferred `0x42f -> 000c:99dd -> 000b:9c0d` design remains explicitly rejected: it visibly entered the hidden UI path, 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 modal wrapper address itself was valid. The newer direct `0007:0d79 -> 000b:9a86` current-slot retarget with the narrowed `000b:9a8d` arg patch was also runtime-tested and produced no hidden menu, so the writable `/Writable/CRUSADER-PATCHED.EXE` test build is now moved to the next defensible variant instead: restore the direct hook to `000a:5276`, keep the current-slot wrapper unpatched, and retarget the later controller-side `000c:99e0` call to `000b:9c0d` while zeroing only the inherited modal-wrapper words at `000b:9c4a`.
- The hidden-menu orphan model is now materially stronger too. New live renames in seg1408 (`usecode_debugger_break_state_create`, `usecode_debugger_maybe_break_on_current_line`, `usecode_debugger_breakpoint_insert_sorted`, `usecode_debugger_has_breakpoint`, `usecode_debugger_callstack_push_entry`, `usecode_debugger_callstack_pop_entry`, `usecode_debugger_enable_single_step`, `usecode_debugger_clear_step_state`, `usecode_debugger_current_entry_get_unit_name`) line up with the seg109 UI in a way the cheat-only hook never did. The concrete interpreter-side handoff at `1418:04aa..04b5` now calls `usecode_debugger_maybe_break_on_current_line` whenever the far pointer at `0x659c/0x659e` is non-null, and that helper checks `(file,line)` breakpoints before callbacking through the debugger-state object's vtable. Current best read is therefore that the retail orphan happened one layer earlier than the cheat/event experiments: the seg109 current-unit debugger UI likely used to be entered from this seg1408 breakpoint object, but retail no longer appears to instantiate/store that object at `0x659c/0x659e`. That makes the breakpoint callback lane a stronger original-entry candidate than direct event `0x103` retargeting.
- The live NE `CRUSADER.EXE` mapping for that hidden-menu lane is now explicit and comment-backed in Ghidra too: direct hook `1130:2b75/2b78`, current-slot wrapper `13a0:0086` with constructor arg site `13a0:008d`, modal wrapper `13a0:020d` with inherited-arg patch subsite `13a0:024a`, listener create/dispatch `13a0:19b1` / `13a0:1df3`, compiled `0x410` CD-transfer-display body `13e8:2303`, deferred controller-side hook `13e8:25dd/25e0`, and the supporting cheat-state data cells at `1020:2833`, `1020:283d`, `1020:0844`, `1020:6045`, `1020:604f`, and `1020:6050`. The `0x410` body is still documented in place rather than renamed because it remains embedded inside the oversized `World_HandleKeyboardInput_13e8_14b4` function object. This improved live handoff and patch reproducibility still does not justify a headline estimate change by itself.
### 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 `Ctrl+Q` / `0x410` lane still lacks a verified USECODE or higher-level 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 NE `CRUSADER.EXE` lane, using verified raw full-EXE and standalone-segment work as cross-reference evidence rather than as the active execution target.
2. 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.
3. Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
4. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again.
5. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming.
## Next Resume Point
1. Continue the NE `CRUSADER.EXE` lane from `docs/ne-hole-filling-priorities.md`, using `docs/crusader-disasm-reference.md`, the raw-focused docs, and prior `CRUSADER-RAW.EXE` notes as supporting handoff material: prioritize one small segment or subsystem from the ranked list where the old disasm vocabulary, shape/map evidence, and verified raw names all overlap cleanly.
2. Build one conservative shape-id / map-placement crosswalk from `shapedata_more_complete.txt` and `mapdump/map-item-dump.txt` into the current trigger-heavy class families before promoting any new NE names.
3. Use the `unkcoffs/` Remorse/Regret function and intrinsic dumps as hint-only candidate generators for still-positional NE functions, but only when segment-local caller/data evidence agrees.
4. Keep the USECODE/VM lane moving 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.
5. Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
6. Use boundary repair only on active blockers with clear payoff, with `000c:db68` now downgraded to optional hygiene unless it blocks adjacent work again.
7. Revisit the `0x4588` callback object only when caller-side evidence is strong enough to support behavioral naming.
8. 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.
9. 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.
10. 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.
11. 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.
12. 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`.
13. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`.
14. 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.
15. 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 first anchored `0005:295f` caller family is now closed structurally at `0006:4379` (`0x20c -> 0005:295f`, sibling `0x20b -> 0005:2918` inside one seg031 dispatch-entry family) and comment-backed in the live NE program at `10f0:02d9`, `10f0:0379`, `10f0:03c3`, and `10f0:03e5`, so the next defensible step is the remaining caller families at `0006:c5f0` and `0007:3584`, or an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` before the owner-loaded class choice reaches the VM lane.
16. 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/crusader-disasm-reference.md`
- `docs/ne-hole-filling-priorities.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.