Crusader_Decomp/plan-mid.md
Maddo ecfe25087a Add new files and update project state for Spanish localization analysis
- Introduced new binary files for user data and database.
- Updated project state to include new paths for Spanish executable.
- Added detailed notes on Spanish cheat/debug differences in a new document.
- Revised mid-project plan to reflect findings from localized build comparison.
2026-03-26 14:11:34 +01:00

71 KiB

Crusader Decompilation Mid-Project Plan

Purpose

This file is the live mid-project tracker for the Crusader decompilation effort.

Keep it focused on:

  1. current verified state,
  2. active blockers,
  3. next resume work,
  4. and the remaining path to a reasonably complete decompilation.

Detailed completed analysis belongs in the files under docs/, not in this plan.

Progress Snapshot

  • 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 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/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.

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.

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 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.

  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.