This commit introduces a comprehensive document outlining the various executable-patching attempts aimed at revealing the hidden retail usecode debugger within the CRUSADER.EXE file. The document serves multiple purposes, including preserving negative evidence, recording patch shapes and their rationales, and ensuring that runtime outcomes are linked to specific patch generations. Key sections include: - Ground rules for patching and validation processes. - A table of stable facts regarding the debugger's structure and behavior. - A detailed attempt log documenting each patch's shape, mechanical and runtime results, and verdicts. - Root-cause findings from failed paths, providing insights into the challenges faced during the patching process. - Current live candidates for further testing and exploration. This documentation is intended to streamline future patching efforts and improve the understanding of the underlying mechanics of the debugger.
268 lines
60 KiB
Markdown
268 lines
60 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 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`, `FUN_000b_0b06` -> `usecode_debugger_set_line_selection`, `FUN_000b_2882` -> `usecode_debugger_build_menubar`, `FUN_1398_0212` -> `ComputerGump_CloseAndResumeGameplay`.
|
|
- 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 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.
|