Crusader_Decomp/plan-mid.md

292 lines
76 KiB
Markdown

# 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 50%
- Reasonable uncertainty band: about 45% 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 live-NE caller-family batch justified another small confidence bump: the remaining direct `0005:295f -> 10a0:275f` callers now close to `Item_ReceiveHit` and `SuperSprite_HitAndFinish` non-NPC damage lanes, which removes the last direct-caller ambiguity in that selector-consumer island even though the upstream class-family selector is still unresolved.
- 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 workspace now also has a first dedicated offline map-rendering/tooling lane: `tools/render_crusader_map.py` can load a chosen `FIXED.DAT`, expand `GLOB.FLX`, decode required `SHAPES.FLX` frames, apply `GAMEPAL.PAL`, and emit a first-pass PNG from either static set, while `docs/map-rendering.md` captures the current format contracts, the `--fixed-dat` override, and the intentionally limited compositor model.
- 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 tooling lane now also has a first full readable corpus export: `tools/export_usecode_pseudocode.py` writes `977` current pseudocode bodies into `USECODE/EUSECODE_extracted/pseudocode`, and the first focused read of that corpus now shows `JELYHACK::use` / `JELYH2::use` as tiny shared `set_info(0x0207) -> process_exclude -> return` stubs rather than hidden active event cores.
- The USECODE tooling lane now also has two new follow-up notes grounded in the exported corpus: `docs/usecode-tool-improvement-plan.md` turns the Pentagram/`crusader-disasm` comparison into a concrete parser roadmap, and `docs/usecode-alarmhat-analysis.md` records the current best evidence-backed read of `ALARMHAT::equip` as a frame-driven local alarm-state controller that equips nearby `shape 0x04D0` helper objects in different modes.
- The USECODE tooling lane now also has a broader equipment-event note: `docs/usecode-equipment-system.md` records live binary proof that `Item_Equip` / `Item_Unequip` are real generic usecode event dispatchers gated by owner-row capability masks (`0x400` / `0x800`), and that the exported corpus currently contains `77` `equip` bodies plus `50` `unequip` bodies spread across actor, turret, alarm, conveyor, camera, and hazard classes. Current best read is `surviving Ultima-style event vocabulary generalized into activation/setup/state-change semantics`, not yet `fully proven paper-doll RPG gear subsystem`.
- The USECODE tooling lane now also has its first implemented readability follow-through from that improvement list: `tools/poc_crusader_usecode_parser.py` and `tools/export_usecode_pseudocode.py` now regenerate the full `977`-body corpus with one verified wrapper alias seed (`FREE.waitNTimerTicks` for `0A0C:0032`), class-name-aware target rendering (`FREE.slot_21`, `BLASTPAC.slot_20`, `TRIGGER.slot_20`, etc.), first-pass selector decoding that upgrades the simpler alarm/trigger `loopscr` runs into `for ... in nearby_items(shape=..., origin=...)` / `for ... in nearby_items(family=..., origin=...)` loops, and a second readable selector-family fallback that collapses raw `loopscr 0x42` runs into `selector_0x42(arg0=..., arg1=..., arg2=..., origin=...)` annotations or `for ... in selector_0x42(...)` loops where the control flow is simple enough.
- 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 next live-NE caller-family pass closes the remaining direct `0005:295f` callers too. Old `0006:c5f0` now lands at `1128:0ff0` inside `Item_ReceiveHit`, where the non-NPC damage path probes `Item_GetDamaged` with hitter sentinel `0x4000`, packed `(damagetype << 8) | damage_lo`, and a local flag-out byte; old `0007:3584` now lands at `1138:1384` inside `SuperSprite_HitAndFinish`, where the non-NPC collision lane probes the same helper with packed `(firetype << 8) | damage` before optionally falling through to local `Item_ReceiveHit` knockback logic. Live comments now anchor both sites, so the selector frontier has moved upstream again to an earlier subtype/class-family producer rather than another direct caller search.
- 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 older seg109 hidden-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`, `FUN_000b_0b06` -> `usecode_debugger_set_line_selection`, `FUN_000b_2882` -> `usecode_debugger_build_menubar`, `FUN_1398_0212` -> `ComputerGump_CloseAndResumeGameplay`.
- The same cheat/control lane is now a little cleaner at the user-facing hotkey level too. A focused live NE pass closed three folklore items: `~` is a real runtime cheat-latch toggle at `13e8:203d` under the broader `0x844` gate; the online `Ctrl+C = show current location` claim is wrong for this build and is really `Ctrl+L`, whose popup branch formats `1478:610c` at `13e8:255e`; and the missing third overlay is not bogus after all, because a separate `Ctrl+F7` branch at `13e8:1a20` toggles `1478:0ee0` while the other two F7-family toggles write `1478:2bc9` and `1478:2bca`.
- The follow-up pass closes the `~` versus `jassica16` confusion more tightly too. `jassica16` is the earlier raw scan-code matcher that toggles both `1478:0844` and `1478:6045`, sets `1478:8c52`, and can therefore bootstrap the whole cheat state from cold; `~` is only the later translated logical-`0x7e` branch, so Shift is the expected normal gesture on a US layout and that hotkey can only flip `1478:6045` after `1478:0844` is already enabled. The same pass also improves the third-overlay classification: `Ctrl+F7` is not a third generic camera grid but an `EggHatcherProcess` trigger-range overlay, which can legitimately appear blank on maps without eligible live egg/hatcher processes.
- The next cheat/overlay refinement pass is now folded back into the docs too. `docs/ne-segment1.md` now has a consolidated live-NE cheat/debug key matrix, the practical `-laurie` plus `Shift+~` bootstrap recipe for full keyboard cheats, and a fuller egg-hatcher note grounded in `EggHatcher_CreateProcess` / `EggHatcherProcess_Run`: non-monster egg families are enter/leave trigger items with X/Y/Z range checks, while `Ctrl+F7` visualizes the live egg-hatcher ranges and `Alt+F7` visualizes the related snap-process egg list.
- 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 next retail test build is narrower still. User runtime feedback on the first `Ctrl+Q` patch is: the mouse pointer appears, then gameplay hangs with only a single right-edge pixel still updating. That makes the remaining failure look more like post-entry control-flow fallout than a bad entry address. The PowerShell patch therefore now also rewrites raw `000c:99e8` / live `13e8:25e8` from `PUSH 0x3e8` to a near jump into the shared epilogue at `13e8:29a7`, so the reused `13e8:25dd` deferred lane exits immediately after the retargeted `13e8:25e0 -> 13a0:020d` call instead of falling through into the original `0x42f` branch tail logic.
- DOSBox-X debugger capture now shows the same hang surviving that tail-skip patch, and the stop point is materially informative: the live runtime state matches seg131 `Interpreter_NextUsecodeOp` at `1418:04c3..051d`, specifically just before the `1408:02f5` call at `1418:051d`. That means the blunt `Ctrl+Q -> 13a0:020d` patch is not merely stuck in the seg109 modal wrapper; it has activated the interpreter-side debugger-state path guarded by non-null `0x659c/0x659e`, and the freeze now looks like a bad or incomplete seg1408 debugger-state lifecycle rather than a simple wrong branch tail. Current best implication: stop iterating on the blunt modal force-open patch and pivot the next patch design toward constructing or safely emulating a real `usecode_debugger_break_state_create` object at `1408:0000` before relying on the seg109 UI lane.
- The next executable patch still follows that pivot, but the boot-time callback rewrite is now explicitly retired. The current PowerShell build repurposes the gated `0x410` body at `13e8:230d` to lazily construct a seg1408 state object through the existing far-call slot at `13e8:2352 -> 1408:0000`, stores the returned far pointer into `0x659c/0x659e`, and then reuses the **second** existing far-call slot in that same body (`13e8:235c`) to jump directly to `usecode_debugger_open_for_current_unit` at `13a0:0086` with zeroed wrapper arguments. This keeps the patch hotkey-local instead of rewriting the shared seg1408 callback table at `1478:65ab`, while the older direct and deferred modal-force-open sites remain restored.
- The callback-table design is now negative evidence rather than the live candidate. Even after fixing an NE-segment indexing mistake (`1478:65ab` had first been retargeted to segment `109` instead of `117` for `13a0:0086`), the global callback rewrite still caused startup failure. The surviving script fixes from that pass remain important: the large `13e8:230d` body must use on-disk `FF FF 00 00` placeholders rather than disassembly-resolved far operands, and its patched byte array must include the final trailing `0xC7` so patch/restore verification matches the retail executable length. With the global callback rewrite removed and the second local call slot retargeted instead, the script now round-trips cleanly on a fresh copied retail EXE (`apply -> patched`, `restore -> original`) and also cleans up the stale old `1478:65ab` callback retarget if that earlier crashing build had already been applied.
- The direct hotkey-to-wrapper retarget is now negative evidence too. The local-call redesign fixed startup and let the game reach gameplay, but pressing `Ctrl+Q` immediately quit through the normal `"No pity. No mercy. No remorse."` shutdown line, which is more consistent with entering the modal UI while the original keypress is still live than with a boot-time relocation problem. The next patch therefore keeps the hotkey-local object creation but stops calling `13a0:0086` on the keypress itself.
- The live candidate is now a per-object callback redirect. The `0x410` body at `13e8:230d` still creates/stores the seg1408 debugger-state object at `0x659c/0x659e`, but the second existing far-call slot in that body (`13e8:235c`) is now retargeted to `1408:0419` (`usecode_debugger_enable_single_step`) instead of directly opening the UI. The created object's first word is rewritten from the shared callback-table offset `0x65ab` to the private relocated slot `0x65af`, and the private dword at `1478:65af` is retargeted from `1408:0474` to `13a0:0086`. That should let the *next* interpreter-side debugger callback open the current-unit UI without inheriting the live `Ctrl+Q` key event, while the original shared `1478:65ab` slot stays restored to the retail no-op.
- The PowerShell patcher now round-trips cleanly for this per-object callback design on a fresh copied retail EXE too: `13e8:230d` body patched/restored, `13e8:235c` step-arm call patched/restored, private callback slot `1478:65af` patched/restored, and legacy shared callback slot `1478:65ab` held at original in both states.
- User runtime on that per-object single-step variant is now also informative negative evidence: the game boots and reaches gameplay, but pressing `Ctrl+Q` produces no visible effect at all, not even the original CD-transfer toast, which implies the hotkey body is being intercepted but the deferred break still is not surfacing. Current best read is that the single-step path at `+0x75` remains gated by the seg1418 nesting counters `+0x76/+0x78` often enough that the callback never fires in the observed test path.
- The live patch candidate therefore now sets **break-next** mode directly instead of single-step mode. The repurposed `13e8:230d` body still constructs/stores the seg1408 debugger-state object and repoints that object to the private callback slot `1478:65af -> 13a0:0086`, but it now writes `+0x75 = 0` and `+0x74 = 1` in the object itself rather than retargeting the second `13e8:235c` call slot to `1408:0419`. That matches the surviving UI-side control path at `13a0:1e5d`, where `+0x74` is the unconditional break-on-next-callback mode while `+0x75` is the nesting-sensitive single-step mode.
- The PowerShell patcher also round-trips cleanly for this break-next design on a fresh copied retail EXE: `13e8:230d` body patched/restored, private callback slot `1478:65af` patched/restored, shared callback slot `1478:65ab` held at original, and the stale second-call-slot cleanup removed from the write path because those bytes now belong to the patched body itself.
- User runtime on that `1478:65af` break-next variant is now negative evidence as well: the game crashes on launch again, so even the supposedly private `65af` slot now looks too globally visible to repurpose. Current best implication is that the object-local `0x65af` first-word rewrite can stay as the arm point, but the actual callback entry must move off the live callback-table dword itself.
- The live candidate is therefore now a **guarded trampoline** at the original seg1408 no-op callback code. The PowerShell patcher keeps the `13e8:230d` break-next object creation/store path, but restores the shared `1478:65ab` slot, stops repointing `1478:65af` to `13a0:0086`, patches `1408:0474` into a tiny guard that returns immediately unless `0x659c/0x659e` is armed, and uses the apparently unused relocated dword at `1478:6597` as the far target slot for `13a0:0086`. This newer `6597`/`1408:0474` build now also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, guarded callback code at file `0xCEE6F` patched/restored, wrapper target slot at file `0xEA197` patched/restored, and all older direct/deferred experiment sites held at original bytes.
- The root-cause read on the `65af` startup failures is now sharper: `0x65af` is not an alternate vtable base at all. The constructor at `1408:0000` writes `0x65ab` to object offset `+0`, and the dispatch sites prove that object `CALLF [BX]` uses the dword at `65ab -> 1408:046f` while object `CALLF [BX+4]` uses the next dword at `65af -> 1408:0474`. Rewriting the object first word to `0x65af` therefore corrupts the second method lookup (`[BX+4]`) instead of selecting a “private callback table”, which explains the launch-time instability and the other inconsistent runtime fallout from the earlier single-step and break-next builds.
- The live candidate is now the narrower **method-0 deferred callback** design. The PowerShell patcher still keeps the `13e8:230d` lazy object creation/store path and still arms break-next mode by writing `+0x75 = 0` / `+0x74 = 1`, but it explicitly preserves the object's vtable base as `0x65ab`, restores the method-1 helper at `1408:0474`, patches only the method-0 break callback at `1408:046f` to indirect through the spare relocated dword `1478:6597`, and uses that slot as the far target for `13a0:0086`. This corrected `046f`/`6597` build also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, break callback code at file `0xCEE6F` patched/restored, deferred target slot at file `0xEA197` patched/restored, and all older direct/deferred experiment sites held at original bytes.
- User runtime on that shared-`046f` method-0 build is now negative evidence too: startup still crashes, which makes the shared method body just as globally sensitive as the shared `65ab/65af` vtable slots. Current implication: the deferred path still looks right, but the callback implementation must move to a truly private per-object table instead of any shared seg1408 body or shared vtable dword.
- The live candidate is now a **private two-entry vtable** built from unused relocated dwords with no current data uses. The PowerShell patcher still keeps the `13e8:230d` lazy object creation/store path and still arms break-next mode with `+0x75 = 0` / `+0x74 = 1`, but it now rewrites the created object's vtable base to `0x658f`, retargets private method 0 `1478:658f -> 13a0:0086`, retargets private method 1 `1478:6593 -> 1408:0474`, and leaves the shared callback bodies and shared `65ab/65af` table entries untouched. This private-vtable build also round-trips cleanly on a fresh copied retail EXE: `13e8:230d` body patched/restored, private method 0 slot at file `0xEA18F` patched/restored, private method 1 slot at file `0xEA193` patched/restored, and all older direct/deferred experiment sites held at original bytes.
- User runtime on that first private-vtable placement is now negative evidence too: startup still crashes, which proves the `658f/6593` pair is also startup-visible despite the lack of direct data-use hits. Current best implication is that the private-vtable strategy itself still looks structurally right, but the specific dword pair must move farther away from the debugger-global cluster and any hidden boot-time consumers.
- The full six-candidate private-vtable harness is now retired. User runtime results:
- Candidate A (`1478:6724/6728`) = DOSBox closes on start
- Candidate B (`1478:672c/6730`) = fatal `Load program failed -- error code 201 -- C:\CRUSADER.EXE`
- Candidate C (`1478:6734/6738`) = DOSBox closes on start
- Candidate D (`1478:6718/671c`) = startup crash
- Candidate E (`1478:6720/6724`) = startup crash
- Candidate F (`1478:6738/673c`) = startup crash
Ghidra follow-up now explains why: `1478:6718..673c` is a live function-pointer table containing `UsecodeProcess_*`, `Process_Terminate`, `Process_Fail`, and nearby null handlers, not spare relocated dwords. The script no longer offers those candidates.
- The first guarded shared-callback pair is now negative evidence too. Candidates G/H still crashed on startup, and the best new explanation is structural: that design overwrote both `1408:046f` and the adjacent `1408:0474`, but `0474` is a real helper that returns `DX:AX = 0`, not dead padding. Destroying that zero-return behavior may itself be enough to destabilize startup.
- The method-0-only shared-callback pair is now negative evidence too. User runtime on Patch 1 / Patch 2 showed both startup-crashing, which means preserving `1408:0474` was necessary but not sufficient: the shared `1408:046f` body is still too broad if it jumps straight into debugger UI code.
- The live patch family is now an **interpreter callsite retarget** design. Candidate M/N are retired negative evidence: both startup-crashed, the embedded private stub inside the patched `13e8:230d` body was malformed, and the supposed deferred target slot at `1478:6597` is no longer treated as spare storage. The current PowerShell build still keeps the retail debugger object's real `1478:65ab` vtable base and still arms break-next through the patched `13e8:230d` body, but it now avoids both the shared seg1408 callback bodies and the `1478:6597` data slot entirely. Instead, it patches the existing interpreter `CALLF usecode_debugger_maybe_break_on_current_line` at `1418:04b5` to a corrected private stub at `13e8:232d`, and it also reuses the second retail far-call slot inside `13e8:230d` (`13e8:235c`) as the actual private UI-call target. The `13e8:230d` body itself now correctly handles both cases: reuse and arm an existing debugger-state object at `0x659c/659e`, or lazily create/store one before arming break-next. One implementation bug from the first O/P refactor is now fixed too: the second `13e8:235c` relocation write is part of candidate application and verification, so the live build now really routes to the selected wrapper instead of accidentally leaving that slot on retail `Dispatch_ModalGump`. Current candidates:
- Candidate O = interpreter callsite retarget -> `13a0:020d`, with `13a0:024a` zeroed inherited modal-wrapper words
- Candidate P = interpreter callsite retarget -> `13a0:0086`, with `13a0:008f` zeroed inherited current-unit-wrapper words
Both apply/restore cleanly on a disposable retail copy and are the next runtime tests.
- Full chronology for this patch line now lives in `docs/retail-debugger-patch-attempts.md`, including the failed global callback rewrite, direct wrapper call, single-step `65af` build, break-next `65af` build, guarded `0474` trampoline, shared `046f` method patch, and the current private-vtable candidate.
- 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 follow-up doc reconciliation is now closed too. `docs/ne-segment1.md` no longer presents the seg109/raw-reference UI addresses (`000b:*`) and the live seg1408 breakpoint-state addresses (`1408:*`) as if they were competing versions of one table; it now uses one combined component map that makes the layering explicit and preserves the interpreter callback at `1418:04aa..04b5` as the bridge between them.
- 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.
- The retail `-debug` switch is now separated cleanly from that hidden debugger lane too. Live `HandleCommandlineArgs` recovery in `CRUSADER.EXE` confirms a real `"-debug"` branch at `1048:0a93` that sets `g_debugMsgLevel = 10` (`1478:87e0`), prints `Debugging mode ON.`, and writes `1478:0845/0859` (`g_someDebugFlag` / `g_someDebugFlag2`). The `0x87e0` threshold is read by `ConsolePrintf` / `DebugPrintAndWaitForInput`, and `0x0859` is read by the segment `1468` `VideoPlayer_*` neighborhood (`1468:2869`, `1468:2af4`, helper `1468:2de9`). Current best read is `surviving debug-output / media-instrumentation switch`, not `dead parser stub`, and still not `the missing seg109/seg1408 usecode debugger bootstrap`, because the same pass found no evidence that `-debug` constructs or stores the real debugger-state pointer at `1478:659c/659e`. Focused write-up now lives in `docs/retail-debug-arg.md`.
- That `-debug` lane is tighter again after the follow-up deep dive and live Ghidra refinements. The seg1468 readers are now renamed/comment-backed as `VideoPlayer_AdvanceFrameAndHandleSkip`, `VideoPlayer_StreamChunks`, and `VideoPlayer_DrawDebugTimingOverlay`, and the helper body is no longer only `instrumentation-looking`: it writes two `500`-byte marker traces into adjacent scanlines near the bottom of a `0x280`-wide AVI playback buffer, using timing deltas scaled by `6000`. Current best read is therefore `built-in movie-playback timing overlay` plus the separate `g_debugMsgLevel` console/positioned-print threshold, still explicitly distinct from the hidden seg109/seg1408 usecode debugger path at `1478:659c/659e`.
- The same `-debug` lane is now bounded more comprehensively too. The user-confirmed moving bottom-of-video dots match the static two-scanline overlay model, so that AVI timing overlay is now the first closed visible effect. Outside the video lane, `-debug` also changes the global seg12d0 print threshold (`1478:87e0 = 10`) used by `ConsolePrintf`, `DebugPrintAndWaitForInput`, and the positioned print helpers, but the sink side is now tighter as well: `ProbablyPrintDebugMessage` formats through the static stdio-style table at `1478:6c32..6c81` and writes to the handle-`1` entry at `1478:6c46`, so the non-video side is ordinary DOS `stdout` gated by the threshold, not a separate hidden debugger console. Current xrefs still show that lane mostly as existing startup/config/cache/joystick/process diagnostics and a small set of dispatch/gump allocation failure-stop paths, not as a second confirmed hidden feature. The unresolved leftover is still `1478:0845`, which remains a parser-set latch with no recovered downstream consumer.
- The print inventory behind that same `-debug` lane is now materially tighter too. A focused pass recovered concrete `ConsolePrintf` / `DebugPrintAndWaitForInput` strings instead of only caller families: startup/arg strings such as `Debugging mode ON.`, `You DO need help!`, `Enabling ENHANCED mode. (NOT!)`, `Warping to mission %d ...`, `Defaulting to skill level %d`, and `Demo mode.`; init/config strings such as `Using map patch file.`, `Running with partial installation.`, `Running with full installation.`, and `Redirecting mission %d tune to '%s'`; cache/swap progress scaffolding such as `Creating Swap file [` and repeated `.` / bracket fragments; plus stronger failure/debug-stop fingerprints like `COULD NOT CREATE GLOB ITEM!`, `No room for Dispatcher Record/Playback.`, `End of script! (press any key)`, and `Out of Memory! [%u]`. Recovered call levels so far are `0x32` and `0xff`, both above the `-debug` threshold of `10`, which reinforces that the practical scarcity of visible text is about path frequency and graphics-mode presentation, not about the threshold still filtering these known messages out.
- The older folklore claim about flat offset `E69FB` and a possible secondary monochrome monitor is now materially weaker too. Local NE-segment mapping puts `0xE69FB` at live address `1478:2dfb`, which falls inside the `SYSTIMER.C` string in a data/name table (`KeyboardProcess`, `KEYIO.C`, `PRIORITY.C`, `SystemTimer`, `SYSTIMER.C`, `AccWait`), not inside executable instructions. The current retail print lane still points to ordinary `stdout` at `1478:6c46`, and targeted searches found no direct `mono`/`monochrome`/`hercules`/`MDA` strings or obvious monochrome-adapter port/memory references (`0x3b4/0x3b5/0x3b8/0x3ba`, `B000`). Current best read is therefore `folklore or address-mapping mistake`, not evidence for a hidden retail secondary-monitor debug display.
- A focused localized-build comparison is now tighter too. The live `/es/CRUSADER.EXE` pass still shows the same broad cheat/debug framework with shifted addresses rather than a rewritten system: `-laurie` sets the broad master gate at `1478:0910`, the gameplay-input gate still exists at `1478:0927`, the lower keyboard-cheat latch still exists at `1478:5fb3`, event `0x410` at `13e8:2211` still toggles the CD transfer display, and `13e8:24a5` still toggles Hack Mover with English-facing strings. But the sequence side is narrower and more specific now: a direct live byte scan found no exact `jassica16` table `24 1e 1f 1f 17 2e 1e 02 07 00` anywhere in Spanish data `1478:0000-8c3f` or `1480:0000-1fff`, and the old English-side slot at `1478:2833` now holds pointer-like words instead of the matcher bytes. The same pass also surfaced a likely Spanish post-sequence latch analog: Hack Mover is pre-gated by `1478:8ad6` before the broad gate check. So the remaining open question is no longer "does Spanish have the same cheat/debug family"; it is "where did the Spanish secret matcher move, and what writes `1478:8ad6`?" Detailed notes now live in `docs/spanish-cheat-differences.md`.
- The follow-up Spanish-cheat pass narrows that further: in the live `/es/CRUSADER.EXE` database, `1478:0910` still has only the `-laurie` write at `1050:0985`, `1478:5fb3` still has only the Laurie-hint helper writes at `13e8:0071/0077`, and `1478:8ad6` still has no recovered direct writer even though `13e8:249b` tests it before Hack Mover. The old English matcher slot at `1478:2833` remains repurposed as pointer-like words, and the only explicit multi-key helper recovered in this pass (`11d0:024b`) is just a generic key-list membership check used from a movement/control cluster, not a cheat-toggle lane. Current best read is therefore now stronger than "unknown moved Spanish matcher": no live replacement cheat-trigger byte matcher has been recovered at all, and the remaining proof frontier is any non-`-laurie` writer of `1478:0910` or a real writer for `1478:8ad6`.
- The next localized-build pass narrows the practical keyboard side too. Full decompilation of `World_HandleKeyboardInput` (`13e8:14b4`) still shows the Spanish broad-gate debug family (`0x141`, `0x241`, `0x410`, Hack Mover), but no recovered translated `~` / `0x7e` cheat-latch branch. That means the old English `-laurie` plus tilde bootstrap is no longer a defensible Spanish assumption: `-laurie` still raises `1478:0910`, but no runtime tilde writer of `1478:5fb3` has been recovered, and `1478:5fb3` itself now reads more like a widely consulted gate byte than a proven English-style keyboard-cheat latch.
- The deeper keyboard-handler pass strengthens that again: `1478:5fb3` is not just missing a recovered tilde writer, it no longer behaves like a positive enable latch at all. Every recovered consumer in `World_HandleKeyboardInput` requires `1478:5fb3 == 0`, and the only recovered writers are the Laurie-hint helper pulse `13e8:0071` then `13e8:0077`, which leaves the byte cleared. Current best localized-build answer to the user-facing cheat question is therefore: `-laurie` is the only recovered positive enabler for the surviving broad Spanish cheat/debug hotkeys, while Hack Mover remains separately blocked behind the still-unwritten `1478:8ad6` gate.
- The next Spanish follow-up narrows two remaining folklore assumptions too. First, the old English immortality-string slots at `1478:2850/2866` are repurposed as pointer-like data in `/es/CRUSADER.EXE`, and no direct uses of those addresses were recovered, so the English `F10` replenish / immortality path is now `unproven in Spanish` rather than merely `not yet re-closed`. Second, the new `8ad6` hunt found a nearby runtime-state cluster at `8ad7/8ad8/8ad9`, but the actual neighbor writes belong to gameplay-input modal helpers and the Hack-Mover-adjacent runtime helper `13e8:282f`; they still do not touch `8ad6`, which keeps the best writer hypothesis pointed at an indirect or script-driven path rather than ordinary compiled keyboard logic.
### Recently Closed Or No Longer Live
- The most reusable `misc_crusader_notes.txt` scratch items are no longer loose leads. `STEAM2` event hints are now checked against extracted USECODE rows, the old labels `FUN_1130_0896` / `FUN_1130_32af` / `FUN_1020_0000` / `FUN_1128_026b` are closed against live NE names, `ItemNPC_AnotherCreate` is now explicitly documented as the area-search-gated helper `NPC_CreateIfAreaSearchValid`, `Kernel_11d0_2491` is narrowed to a kernel/process snapshot writer, `FREE::ordinal3C` is constrained to an alert-clearing random `FREE::ordinal21` spawner, and `Int01E` is at least tightened from `unknown fire intrinsic` to `Actor::I_maybeFire` plus live export `1128:11da`.
- `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.
6. Use the new offline map-rendering lane to cross-check shape ids, map placements, and visible world composition against `crusader-disasm` shape/map notes before promoting additional rendering- or static-object-related names in `CRUSADER.EXE`.
## 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. Exercise `tools/render_crusader_map.py` on a few representative No Remorse and No Regret maps, then tighten the paint order using `TYPEFLAG.DAT` footpads and any mismatches visible against in-game screenshots or `crusader-disasm` map evidence.
9. 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.
10. 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.
11. 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.
12. 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.
13. 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`.
14. Promote additional ledger rows where the current docs already justify `Foothold`, `Partial`, or `Deep`.
15. 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.
16. 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 three currently recovered direct `0005:295f` caller families are now all closed and comment-backed in the live NE program at `10f0:02d9`, `10f0:0379`, `10f0:03c3`, `10f0:03e5`, `1128:0ff0`, and `1138:1384`, so the next defensible step is an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` or otherwise chooses the owner-loaded class family before these generic damage consumers run.
17. 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.