92 KiB
Crusader Decompilation Mid-Project Plan
Purpose
This file is the live mid-project tracker for the Crusader decompilation effort.
Keep it focused on:
- current verified state,
- active blockers,
- next resume work,
- 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-local0x4f38bit-0x40lane, the seg126 control stream is confirmed as file-backed, the paired0x8c5c/0x8c60renderer objects are narrowed to two script-selected preset text lanes, and the neighboring seg127 fade controller now has an exact local contract at0x630a..0x6316. - The current VM/loader batch also justified a small bump:
000d:ebe3is now a named ordered opcode sequencer with a tighter entry/exit contract, the masked-create hub at000d:463ais now a verified owner-table gate rather than an inferred wrapper sink, and the seg070 twin loops underentity_vm_runtime_owner_resource_createnow 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:275fcallers now close toItem_ReceiveHitandSuperSprite_HitAndFinishnon-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..0x14with verified mixed payload shapes (nonevs extra signed word), the new slot-only Ghidra names keep that taxonomy visible without overpromoting event labels, and the000d:22bcstage 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/0x00000410literal, and the remaining frontier is narrowed to data-driven decoding ofEVENTslot0x0aplusNPCTRIGslots0x0a/0x20rather 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 + 2exactly, the class header/subentry math at000d:5066/51fd/53b4is closed against the extractor's raw headers and event rows, and the surviving immortality uncertainty has moved fromcan the loader fit NPCTRIG arithmetic at all?to the narrowerwhich class family is actually selected upstream?question. - The PSX sprite-extraction side is also less speculative now: a dump-grounded pass proved the known-colored wall-console bundle
bundle_000A1B04already exists verbatim in live VRAM at texture page(1,1), and the corrected working color formula is the top-left live CLUT candidate from the atlas, namely the contiguous256-entry slice at GPU row0xF0,x=0; the same rule now produces plausible output across a wider92-bundlemode 1batch instead of only the single cabinet proof case. - The PSX executable-side catalog lane is tighter too:
SLUS_002.68now has comment-backed proof thatwdl_resource_bundle_load_by_indexselects seven hardcoded\LSETn\Lprefixes across thresholds10/20/30/40/50/60, the extracted disc currently ships62level bundles (L0..L58,L62..L64) with a real gap atL59..L61, the executable exposes only15plain-textMission Briefing ^Mission Nstrings, and the mission-complete passcode path now has a closed4-character consonant/digit alphabet at80063ef0plus direct ammo/item/weapon name tables. The remaining PSX passcode gap is now narrower: public cheat-password candidatesXXXXandL0SR/L0SERare not stored as plain ASCII inSLUS_002.68, so the compare path likely uses numeric or transformed validation instead of a flat string table. - The new PSX pre-alpha comparison lane is also anchored now:
/psx/prealpha/SLUS_002.68still carries directCrusader: No Remorsebranding, the same retail-stylewdl_resource_bundle_load_by_index\LSET1\L .. \LSET7\Lthreshold ladder, and the same15mission-briefing/passcode shell, but the unpackedCrusader 2 Pre-Pre Alphadisc currently ships only3level bundles,1XA, and no.STRmovies. The most interesting current mismatches are architectural leftovers that no longer match the disc literally, especially the missing-file\AUDIO\TALK1.XA;1path and the survivingLoadExechelper forMENU.EXE/ENGINE.EXE/PSX.EXE. - 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.csvnow exists for all 145 NE segments and should remain the primary coverage tracker.crusader_decompilation_notes.mdis now only an index; detailed evidence lives indocs/.CRUSADER.EXEis now the default live Ghidra target for ongoing work; verifiedCRUSADER-RAW.EXEresults 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-disasmis 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 indocs/crusader-disasm-reference.md. - The PSX side now has a first explicit pre-alpha comparison note too.
docs/psx/prealpha.mdrecords that/psx/prealpha/SLUS_002.68is still much closer to a reduced No Remorse PSX branch than to a visibly rebranded sequel executable: the live database now haswdl_resource_bundle_load_by_indexrenamed at80038084, and comment-backed notes on the staleTALK1.XAselector helper and the split-LoadExecMENU.EXE/ENGINE.EXE/PSX.EXEpath that no longer matches the current unpacked disc tree. - The workspace now also has a first dedicated offline map-rendering/tooling lane:
tools/render_crusader_map.pycan load a chosenFIXED.DAT, expandGLOB.FLX, decode requiredSHAPES.FLXframes, applyGAMEPAL.PAL, and emit a first-pass PNG from either static set, whiledocs/map-rendering.mdcaptures the current format contracts, the--fixed-datoverride, and the intentionally limited compositor model. - The map/editor-visibility lane is now tighter too. New note
docs/editor-object-visibility.mdrecords liveCRUSADER.EXEproof that the downstream item draw helper1198:02e4(Item_PaintSprite) explicitly returns early onShapeData.flags2 & 1(SI_EDITOR), but the follow-up render-path pass also found the controlling upstream skip at1180:0951..095cin the world-item builder. Current best read is thereforeeditor-tagged shapes are filtered before draw-node allocation in the normal world-item renderer, with a second downstream paint-time guard still present, which also explains why a first patch that only flipped1198:033bproduced no visible change in-game. No recovered retail-debug, cheat/debug hotkey, Laurie/usecode-debugger path, or0x410lane currently re-enables those objects. The closest confirmed toggle remains ScummVM's own_showEditorItemsdebugger command, which is engine-added rather than retail. - The localized-build comparison lane now covers the Japanese Windows-native executable too. New note
docs/jp-remorse-windows9x-investigation.mdrecords that/ja/CRUSADER.EXEis a PE-style Win32 image with native window creation, DirectDraw/DirectSound init, registry-backed config underSoftware\Electronic Arts\Crusader: No Remorse\J1.21, IME/DBCS-facing imports, and aGetVersion-driven Win9x compatibility branch that retriesTlsAlloc()until the slot is above2when the classic Win9x version bit is set. Current best read isreal Windows 9x-native port with likely Win95 intent, with runtime prerequisites still left to test. - The removed-item lane is tighter now too.
docs/removed_items.mdnow records a liveCRUSADER.EXEclose on the inventory/display name path: retail1118:056AisDTable_GetNameForShapeNo,1118:05D5is itsINVALIDfallback returning1478:238C, andWeasel_OnPaintuses that same lookup family. The backing1478:22BCchar *[41]array preserves exact explosive names inline, includingCONCUSSION GRENADE,NERVE GAS GRENADE,EMP GRENADE,SPIDER BOMB,LAND MINE,BLAST PAC, andFUSION PAC, while repeatingINVALIDat slots0/14/26/32. The reusable Remorse dump atout/dtable_get_name_dump.jsonplus companion CSVs now closes the direct-table question too:0548is not a named dtable entry, so its in-gameInvalidlabel is best explained as a plain fallback for an unmapped shape. The same tooling now also closes the Regret comparison side: liveREGRET.EXEuses helper1130:056a, its recovered segment-1480dtable island expands the slot layout to52names with repeatedINVALIDat0/17/36/44, preserves Regret-only names such asBK-16,LNR-81,XP-5,IONIC SHIELD,PLASMA SHIELD,RADIATION SHIELD, andVIR IMAGER, and resolves bomb rows0343,0350,0560,039A, and039Cwhile notably omittingBLAST PAC. Current best read remains narrower than the first pass: the removed grenade variants are real retail dtable names plus map leftovers,LANDMINE/BLASTPAC/FUSPACare active Remorse classes rather than new removed items, andSPIDER BOMBis currently stronger as a cross-game dtable/comparison signal than as a fully closed Remorse finding. - The Japanese localized-build lane now also covers surviving cheat/debug and startup-argument behavior. New note
docs/jp-remorse-cheats-and-launch-params.mdrecords that the JP Win32 build still has a live-lauriespecial-case, a liveJASSICA16cheat-state matcher, a still-executable immortality toggle path, and a working Win32 parser for-debug,-u,-warp,-skill,-mapoff,-egg, and-demo. The same pass also adds one important caveat relative to the older DOS-side docs: the JP Win32 parser is only directly closed for mission-only-warp <mission>so far, not for positional-warp <mission> <x> <y> <z>. - The startup map-selection lane is now tighter across both retail games too: No Remorse still hardcodes
Teleporter_CreateProcessDirect(1, 0x1e, 1)insideGame_Start, while No Regret keeps the same literal selector in two live places, the earlyGame_Startsite at1008:1448and the later authoritative new-game hop inGame_RunNewGameFlowat1030:05c5. The separate-warp missionpath still uses an executable-embedded word table plus-mapoff, and the repo docs now include the dedicated REGRET-side notedocs/regret-game-start.md. Current best read remainsstartup map choice in code, map contents in external FIXED.DAT resources, notmission-start map configured in CRUSADER.CFG. - That same warp-table lane is now exact across both retail DOS executables too. Byte checks against
CRUSADER.EXEandREGRET.EXEnow show matching 17-word-warp missionbase-map tables (0,1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,40) at1478:0488and1480:075c, each followed by a0,0terminator. The public map renderer now also has a dedicated mission-table extractor and generated JSON cache, so scene metadata no longer has to treat mission/base-map usage as an unknown ownership question. - That same startup lane is now tighter at the argument level too. Current best parser/control-flow read in
REGRET.EXEis-warp <mission> [x y z], with X/Y/Z carried as positional argv tokens after the mission number rather than as separate recovered switches. The corresponding runtime branch inGame_RunNewGameFlowis also clearer: nonnegative-eggoverrides beat the coordinate path, while the real eggless-map workaround is-warp <mission> <x> <y> <z>plus-mapoffwith-eggomitted so the game falls into directNPC_Teleportinstead of the teleporter-egg lookup. - The matching No Remorse cross-check is now closed too. Live
CRUSADER.EXEHandleCommandlineArgsat1048:0adcuses the same positional-warp <mission> [x y z]parser shape, andGame_Startat1020:029e/1020:02d0uses the same runtime precedence: direct coordinates only win when the egg override is still negative, otherwise the code falls back toTeleporter_CreateProcessDirect. The parameter-only eggless-map workaround is therefore shared across both retail games, not Regret-specific. - The public map-renderer link lane is tighter again. Cross-game
0x01DBsupport now covers both the earlier frame-1teleporter-light helpers and the remaining Regret map3frame-0telepad helper placements that carry destination ids27/28inquality. The same pass also adds the checked same-mapELEVATORlane: frame-0shape:542sources now link to local teleport-destination eggs by verifiedQLorules (1..0x0f -> same egg id,0x10 -> egg 4). Current best gap is still Regret map3egg102, which does not sit on the verifiedshape:542/shape:307elevator lanes yet. - The editor-helper overlay lane is tighter too. A broader exported-scene sweep now shows that
BRO_BOOT(0x04FE) really does form a repeatable local helper lane into nearby same-QLoSPANELitems, with concrete Remorse examples on maps9,10,11,21,23,160, and246, so the renderer now promotesBRO_BOOT -> SPANELalongside the existing cmd-link, alarm, steam, door, and flame helper arrows. The same follow-up kept two tempting false positives out of the overlay:NPC_ONLY -> 0x04B1andDEATHBOX -> 0x04B1still read better as incidental local overlap than as a dedicated helper-source relationship. The latest tooltip pass also upgrades0x04B1from a mostly structural decode to concrete operation notes: helper dispatch via nearby0x0476, direct target mutation, timed pulses throughTRIGGER.slot_22/DOOR.slot_21, verified link rewrites, and a create-and-drop lane. - The skill-controller lane is tighter too. Shape
0x0120is now closed asFASTSKIL, distinct fromSKILLBOX:enterFastAreawaits briefly, only runs while map-array is clear, uses frame0/1as difficulty thresholds forTRIGGER.slot_20lane0versus1, and uses frame2as an explicitQLo/+1/+2difficulty router. The renderer now exposes that decode in tooltips and adds conservative localFASTSKIL -> 0x04B1helper arrows, with frame-2variants for the recoveredQLo + 1andQLo + 2lanes. - The switch/pad clarification lane is tighter too. Shape
0x0080now closes asBOX_EW, and sampled exported scenes are strong enough to promote a conservativeBOX_EW frame 0 -> nearby same-QLo 0x04B1helper arrow rule. Shape0x04CDnow closes asTRIGPAD, but its broader occupancy/elevator behavior and the negative scene sweep keep it metadata-only instead of promoting a generic cmd-link overlay. Shape0x033Anow reads best as a tinyNUMBERSreadout/display helper family clustered with nearby0x0501/0x0502/0x0503/0x0505/0x0507pieces, so it also stays label-only. - The readable-usecode viewer lane is tighter too. New note
docs/map_renderer/trigger-usecode-links.mdrecords the evidence-backed class/event mapping now used for pinned controller tooltips:BOX_EW,PANELNS,CARD_NS, andSPANELopenuse;FASTSKILopensenterFastArea;SKILLBOX,EVENT,ALARMHAT, andALRMTRIGopenequip;TRIGPADandNPC_ONLYopengotHit; and the0x04B1cmd helper jumps directly toTRIGGER.slot_20, the shared high-slot fan-out lane recovered from the extracted corpus and existing trigger notes. - The command-line lane is tighter around
-unow too. In live non-JapaneseCRUSADER.EXE, the parser case at1048:0a46copies the following token into1478:065a, and the renamedstartup_apply_u_override_if_presentat1420:0cdflater consumes that buffer to resolve/load an alternate usecode/EUSECODE source into1478:6611/6613, mark1478:6615, and rebuild the cumulative slot-base words at1478:8c7c..8c82. Current best read isreal retail startup usecode override, notJP-onlyand notdead string-table residue; the paired consequence is that the older CRUSADER-side-setverattribution should now be treated as reopened until its exact retail consumer is isolated directly. - That same
-ulane is now tighter at the runtime-scope level too. The follow-up notedocs/usecode-startup-override.mdnow records that retail-uappears to replace the single live usecode root at1478:6611/6613, not add a sidecar table:startup_apply_u_override_if_presentoverwrites that root directly, rebuilds the cumulative slot-base words, and later consumers includingUsecode_ItemCallEvent,UsecodeProcess_CreateProcess,Interpreter_NextUsecodeOp, andItem_GetDamagedall read the same replacement root. Current safest tooling implication isruntime swap for the existing Crusader usecode VM, which makes-ua potentially important future validation hook for round-tripped/custom usecode archives once the accepted source format is nailed down. - The same
-ulane is tighter at the token-shape level now too. Live1420:0cdfdoes not use the copied argv token as an arbitrary final filename; it treats1478:065aas theFilespec_GetFullPathpath component while loading the fixed mutable filename templateeusecode.flxfrom1478:07a0through1478:06d6/06d8and forcing the first byte to'e'before both the existence probe and the final load call. Current safest read is thereforepath/root override for standard EUSECODE archive naming, notfree-form filename override. The stock bootstrap side is also better scoped:1478:6611/6613starts zero in the live NE image and the only currently recovered explicit writer there is the-uhelper, so the normal non--useed remains only cross-referenced through the verified raw-side VM bootstrap note rather than fully live-NE-closed. - The same override lane now has a concrete live-NE constructor pair too.
1420:1499is now renamedentity_vm_runtime_createand currently reads as a0x1319-byte runtime-object allocator that zeroes a0x1300-byte front region behaving like0x80stride-0x26slot/runtime records before storing an attached helper pointer at+0x1315/+0x1317.1430:0000is now renamedentity_vm_runtime_owner_resource_createand currently reads as the compact0x14-byte file-backed helper that opens the resolvedeusecode.flxpath, queries entry count through vtable+0x04, allocates a backing buffer at+0x10/+0x12, and materializes indexed owner/resource records through vtable+0x0c. Current safest implication is that-uswaps the live VM runtime object graph, not just a raw archive handle. - 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.pywrites977current pseudocode bodies intoUSECODE/EUSECODE_extracted/pseudocode, and the first focused read of that corpus now showsJELYHACK::use/JELYH2::useas tiny sharedset_info(0x0207) -> process_exclude -> returnstubs 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.mdturns the Pentagram/crusader-disasmcomparison into a concrete parser roadmap, anddocs/usecode-alarmhat-analysis.mdrecords the current best evidence-backed read ofALARMHAT::equipas a frame-driven local alarm-state controller that equips nearbyshape 0x04D0helper objects in different modes. - That same
0x04D0lane is tighter again after the next follow-up:MONSTER::enterFastAreais now verified as the immediate-spawn gate forshape 0x04D0, with the automatic lane only reached whenframe == 0and(mapNum & 0x08) == 0, while frame1skips that hook entirely and remains the best current candidate for paired or externally signaled setups. - The public renderer follow-up now has a better audit surface for that same lane: the
Monster Spawnerspanel lists0x04D0records directly, the tooltip editor writes the verifiedframe/mapNum bit 0x08controls back into exportable FIXED records, and the new arrow overlay is intentionally scoped to evidence-backed link families only. Exported-usecode corroboration now reaches beyondALARMHATtoo:ITEM.slot_2D,FUSPAC.slot_01, andMISS8.slot_20all show nearby0x04D0scans keyed by frame and/orItem.getQLo(...), which strengthens the current low-quality-byte-as-local-signal-key model without promoting it into a universal object pointer. - The USECODE tooling lane now also has a broader equipment-event note:
docs/usecode-equipment-system.mdrecords live binary proof thatItem_Equip/Item_Unequipare real generic usecode event dispatchers gated by owner-row capability masks (0x400/0x800), and that the exported corpus currently contains77equipbodies plus50unequipbodies spread across actor, turret, alarm, conveyor, camera, and hazard classes. Current best read issurviving Ultima-style event vocabulary generalized into activation/setup/state-change semantics, not yetfully proven paper-doll RPG gear subsystem. - The USECODE tooling lane now also has its first implemented readability follow-through from that improvement list:
tools/poc_crusader_usecode_parser.pyandtools/export_usecode_pseudocode.pynow regenerate the full977-body corpus with one verified wrapper alias seed (FREE.waitNTimerTicksfor0A0C:0032), class-name-aware target rendering (FREE.slot_21,BLASTPAC.slot_20,TRIGGER.slot_20, etc.), first-pass selector decoding that upgrades the simpler alarm/triggerloopscrruns intofor ... in nearby_items(shape=..., origin=...)/for ... in nearby_items(family=..., origin=...)loops, and a second readable selector-family fallback that collapses rawloopscr 0x42runs intoselector_0x42(arg0=..., arg1=..., arg2=..., origin=...)annotations orfor ... in selector_0x42(...)loops where the control flow is simple enough. - The USECODE/VM lane now also has a verified generic masked-context creation hub (
000d:463a) plus two concrete sequencer-internal consumer blocks (000d:208b,000d:21ed) built directly onentity_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:0x0007and0x2000:0x0015in 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 guarded0x0010:0x0008materializer that simply returns0on miss after readiness checks. - The USECODE/VM lane now also has a wider verified higher-slot wrapper ladder: the
0005island reaches slot ordinals0x10..0x14, slot0x12is a zero-extra-word lane, slots0x11/0x13/0x14carry extra-word payloads, and the current safest read isslot-stable payload-shape taxonomyrather than direct event-name promotion. - The same higher-slot batch now has its first outward binary anchors: slot
0x12wrapper0005:3171is directly called at0005:1776and0005:1945, the slot0x10guarded lane at0005:3115..3129is still fenced by the0005:30f2..3113class-nibble-4check, and the dark slot0x0a/0x0bwrappers are now instruction-verified as exact signed-additive shims over masks0x00000400/0x00000800even though their outward callers remain unrecovered. - The compiled-side immortality lane is slightly tighter too:
000b:b3b1/000b:b62care now a cheat-event listener constructor/handler pair for the shared cheat/control bundle rather than a hidden0x410producer, and the extractor-sideTELEPADslot-0x20raw_code_offset = 0x00000410hit 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:8c56is now a verified generic event-object dispatcher reading the emitted event id from field+0x6, seg109 helper000b:3d2ais now comment-backed as generic listener-registration infrastructure rather than an emitter, and the strongest remaining player-trigger family is the event-bearingNPCTRIG/EVENTneighborhood rather thanTRIGPAD,SPECIAL,REB_PAD, orTELEPAD. - The immortality lane is tighter again after the extractor extension: generated report
USECODE/EUSECODE_extracted/immortality_target_body_scan.mdnow proves thatEVENT,NPCTRIG,COR_BOOT,REE_BOOT,SFXTRIG,SPECIAL, andTRIGPADbodies contain no inline little-endian0x0410, no dword0x00000410, and no byte-swapped0x1004; the best surviving frontier is now the monolithicEVENTslot0x0abody plus compactNPCTRIGslots0x0a/0x20. - The immortality lane is tighter again after the structure pass: new report
USECODE/EUSECODE_extracted/immortality_body_structure.mdnow showsEVENTslot0x0aas a broad hub clause stream (90internal0x53 0x5c <u16> EVENTsubheaders,383local labels, wideevent/item/source/dest/door/counter/counter2/link/time/post1/post2/floor/flicMantail), whileNPCTRIGstays compact (5subheaders for slot0x0a,1for slot0x20, with narrowreferent/event/item/item2vsreferent/typeNpc/item/item2tails). Current best surviving emitter frontier is thereforeNPCTRIGslot0x0awithNPCTRIGslot0x20as its nearest typed/setup companion, whileEVENTnow 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.mdnow fixes the open-header decode (NPCTRIG 0x0aevent-code byte0x11,NPCTRIG 0x20event-code byte0x01) and shows slot0x0aas a five-step fixed-width clause ladder (0x2fsubheader stride, backward-walking0x2ftargets, per-clausebranch_3f_0a+push_24_51+writeback_57_02motifs) while slot0x20stays typeNpc-heavy (10field_4b_fe_0fhits, nopush_24_51, nowriteback_57_02). The best remaining descriptor-side frontier is therefore no longer theNPCTRIGpair symmetrically; it is specificallyNPCTRIGslot0x0aas the live event-bearing ladder, with slot0x20as 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:5572proves the extra word carried by0005:2c35is additive (slot_value + offset),000d:21ednow has an exactA x Bmatrix contract (byte A = lead-word row count, byte B = shared target-list width), andNPCTRIGslot0x0ais the only surviving compact body that exposes a natural five-row additive selector family (0x0064/0x0093/0x00c2/0x00f1/0x0120, uniform stride0x2f) 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), while0005:2c35itself still has no recovered code or data xrefs. Stack setup at000d:208bhardcodes the000d:5572additive word to0, which does not match theNPCTRIGslot0x0aclause-start or target families. The remaining live selector frontier is therefore the still-overlapped000d:21edcaller frame rather than a normal caller of0005:2c35. - The immortality downstream-use follow-up weakens the remaining direct-selector hypothesis again:
000d:46ecstores the dynamic word from the000d:21edlane into context field+0x34, but000d:21ed -> 000d:22bcnever rereads+0x34or+0x32after creation. The durable uses are the object save/load path instead:000d:498fserializes only the derived low word at+0x10c,000d:4a78reloads that saved word as the additive argument to000d:5572, and000d:4c2d..4c4drebuilds+0x10c/+0x10efrom the live slot value plus that saved offset. The only recovered post-load consumers are a tiny sentinel predicate (FUN_0001_a772checks for exactly0000:0001) and a normalization block (FUN_0002_1860clamps0000:xxxxvalues below0x0080up to0x0080). No recovered compare or dispatch branch matches theNPCTRIGslot0x0aclause-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 the0x80-byte local buffer, whileentity_vm_context_load(000d:4a78) rebuilds the frame pointers, replaysentity_vm_slot_load_value_plus_offsetfrom saved(slot, additive_word), restores+0x10c/+0x10e, and refreshes owner-source pair+0x117/+0x119. That is stronger evidence forpost-selector persistence of derived value statethan 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..21ednow show the hidden pre-call layout explicitly: the seeded+0xd6/+0xd8stream is consumed asword slot_index,word add_a,word add_b,byte setup_len,byte inline_len, and000d:21d0pushesadd_a + add_bas the dynamic word later stored at context+0x34. The same window now proves the caller-side frame shape too: frame base iscaller + [caller+0xd4],[frame+0x0a/+0x0c]is the far pointer passed intoentity_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+0x34and reframes the open question one level earlier: where that frame-local far pointer is seeded from, and whether the summed stream pair still maps toNPCTRIGslot0x0aclause-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/+0xd8stream, popping a far-pointer dword from the caller stream at[caller+0xcc/+0xce], computingframe_base = caller + [caller+0xd4], and storing that dword at[frame_base + placement + 0x4/+0x6]. That means the000d:21edsource 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 is0x0006, and any survivingNPCTRIGlinkage 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:f6e8now shows that a new context's+0xcc/+0xcestream is seeded by copying a caller-supplied setup blob into the object-local buffer, while the slot/additive record fromentity_vm_slot_load_value_plus_offsetseeds the separate+0xd6/+0xd8lane and the owner-table row(+0x10/+0x12) + 0x0d*slot + 4is mirrored separately through0x39ca. Linear raw-byte recovery across000c:f98b..000d:000dalso closes the forward/reverse frame-record family around that lane:000c:fc4b..fcbbis the caller-stream -> frame blob producer that best matches inline-tail placement0x000a, while000c:ff1f..ff83is the frame -> caller-stream dword copier matching the000c:fbf7..fc47far-pointer writer at placement0x0006. 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 thisff1f/ff9f -> fbf7/fc4b -> 000d:21edpropagation chain repeats it, and whether that origin still maps specifically toNPCTRIGslot0x0aor to a broader predecoded VM workspace. - The next immortality pass closes the immediate far-pointer source classification too. Hidden raw bytes at
000c:fa2f..fa5brecover an inner opcode dispatcher on the seeded+0xd6/+0xd8lane, and the same case family now exposes non-recursive caller-stream seeders at000c:fd51,000c:fd91,000c:fdd1, and000c:fe11. The dword case at000c:fe11..fe59reads an inline dword literal from that control stream, subtracts4from[caller+0xcc], and writes the literal dword onto the caller stream before the recursiveff1f/fbf7replay family touches it. That means the immediate compiled-side source for the000d:21edsetup far pointer is now an inline VM control-stream literal, not an owner-row lookup or generic scratch buffer; any survivingNPCTRIGtie has to explain how slot0x0ais decoded into that literal-bearing stream upstream, while slot0x20still 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:46ecnow shows the owner-table row(+0x10/+0x12) + 0x0d*slot + 4feeding only the separate0x39ca[slot]mirror, while the live+0xd6/+0xd8control stream passed intoentity_vm_context_setupcontinues to come fromentity_vm_slot_load_value_plus_offset. The hidden000d:21edpre-call span is now explicit asword slot_index,word add_a,word add_b,byte setup_len,byte inline_len, and the000c:fa2fcase family now separates immediate literal seeders (000c:fd51byte,000c:fd91sign-extended byte->word,000c:fdd1word,000c:fe11dword) from the recursive replay stages (000c:ff1f,000c:ff9f). Current best read is thereforedecoded per-slot VM workspace plus frame replay, notdirect NPCTRIG clause stream, even thoughNPCTRIGslot0x0aremains the strongest surviving upstream descriptor family and slot0x20still 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/+0xd8buffer on a cache miss:000d:5066loads a slot header plus cached6-byte subentry table through the owner-resource wrapper000d:714c, and000d:5305..53d4then reads the selected subentry's byte range directly into a newly allocated value-object buffer at+0x0a/+0x0c, which000d:51fdreturns as the live far pair. That means the immediate workspace is file-backed owner-loaded slot data copied into memory before000c:fa2finterprets 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 toNPCTRIGslot0x0aor only to the broader owner-loaded descriptor workspace, with slot0x20still 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 extractedobject_indexexactly; the first class-header dword is now constrained as the extra-slot count beyond a fixed0x20base table; bytes8..11remain the first code-byte offset; and000d:53b4reads body windows using the same(word len, dword raw_code_offset, code_base)arithmetic emitted by the extractor.NPCTRIGtherefore now has exact owner-loaded body windows in the live runtime format: slot0x0a=0x00da..0x024e(373bytes) and slot0x20=0x024f..0x03a7(345bytes), whileEVENTslot0x0alikewise fits0x00d4..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 lane1..255with class bit0x0002clear ->entity_id + 0x8c7e,(2)class-nibble4lane ->class_byte_0x7e05 + 0x8c80,(3)fallback type lane ->type_word_0x7df9 + 0x8c7c.entity_vm_runtime_init_from_path_if_configuredseeds those bases cumulatively from0x6608..0x660e, and direct caller0005:295findependently reuses the same slot index to test owner-row bit0x0040. That strengthens the read that the compiled side sees category spans plus generic row-capability masks, not a hardNPCTRIG/EVENTclass-family discriminator, before the owner-loaded slot body is decoded. - The first focused NE
CRUSADER.EXEhole-filling pass tightens that same wall one step further without breaking it. In the live NE session,0005:295fis now confirmed as the only recovered non-hub consumer ofentity_vm_slot_index_from_entity, and its only currently recovered callers are0006:43c3,0006:c5f0, and0007:3584. That gives the selector lane three concrete gameplay-side caller families to classify next, while0005:2c35remains 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:4379is now a verified seg031 dispatch-entry subtype gate over objects created by0006:42d9with event type0x236, source type8, subtype/tag at+0x3c, payload/source far pointer at+0x32, and aux words at+0x36/+0x38. Within that family, subtype0x20cat0006:43c3routes into0005:295f, while sibling subtype0x20bat0006:43e5routes into0005:2918using the same aux pair. That localizes the owner-row bit-0x0040consumer 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.EXEdatabase too. Comment-backed anchors landed on the live selector/core pair1420:0dc5/1420:0e3a, the consumer pair10a0:2718/10a0:275f, and the first closed caller-family runner10f0:02d9/10f0:0379, with branch comments at10f0:03c3and10f0:03e5preserving the verified0x20c -> 10a0:275fand0x20b -> 10a0:2718split. 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:295fcallers too. Old0006:c5f0now lands at1128:0ff0insideItem_ReceiveHit, where the non-NPC damage path probesItem_GetDamagedwith hitter sentinel0x4000, packed(damagetype << 8) | damage_lo, and a local flag-out byte; old0007:3584now lands at1138:1384insideSuperSprite_HitAndFinish, where the non-NPC collision lane probes the same helper with packed(firetype << 8) | damagebefore optionally falling through to localItem_ReceiveHitknockback 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 viaDS:0x2833, indexDS:0x283d), and it togglesDS:0x844(cheats_enabled) plus mirrorDS:0x6045, then emits event0x103. The matcher bytes themselves are now rechecked in the live NE image as scan codes24 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 codes0x02/0x07. Live data-use recovery also tightens the latch story:0x6045is written only byKey_CheckCheatToggle(1130:2b72) and the event-0x7eruntime toggle at13e8:203d. The live NE F10 proof is stronger than the earlier folklore-level read: insideKey_HandleOptionKeys(1130:0896), the F10 cheat branch first checksDAT_1478_085f, then0x6045, then reaches1130:0afdand calls helper11c8:01a8; the11c8:018ahelper call in the same function appears later at1130:0cad, in a different branch. The helper identity is now closed from the code too:KeyboardGetExtendedShiftStates(11d0:39e6) uses BIOSINT 16h, AH=12h, whose AH bits are0=left Ctrl,1=left Alt,2=right Ctrl,3=right Alt, so11c8:01a8testing0x0100|0x0400is reallyKeyEvent_IsCtrlDown, and11c8:018atesting0x0200|0x0800is reallyKeyEvent_IsAltDown. Upstream keyboard-path recovery also closes the practical behavior too: the held-key repeat builder at11b8:0129..022bsamples BIOS extended-shift state through11d0:39e6, stores the current31a4modifier snapshot into each repeatedKeyEvent, and queues that event through11d0:3533, so holdingF10first and then pressing physicalCtrllets 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_IsDeadgate at10e8:1fed).DAT_1478_085fis now tighter too: it is set duringGame_Start(1020:0127), cleared at the end ofComputerGump_CreateGump(1398:01f5), and restored byComputerGump_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, event0x410at000c:9703does not toggle immortality; it boolean-togglesDS:0x604f/g_cdTransferDisplayActiveand posts theCD TRANSFER DISPLAY ACTIVE/INACTIVEnotifications under the broader0x844gate, which matches both the user's runtime observation and the oldcrusader-disasmnoteCTRL-Q = 0x410. The olderDS:0x6050lane atimmortality_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, and000b:2882now read more defensibly asusecode_debugger_*helpers, with menu labels likeOpen Unit,View File,Watch,Inspect,Find, andBreak to TDP. Current best read is a hidden usecode debugger / unit inspector, not a retail scrollable cheat list. This also tightens the-lauriesplit:-laurieenables0x844-gated event cheats and debugger-side paths, but not the low-level0x6045keyboard latch, which matches the observedF-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 at13e8:203dunder the broader0x844gate; the onlineCtrl+C = show current locationclaim is wrong for this build and is reallyCtrl+L, whose popup branch formats1478:610cat13e8:255e; and the missing third overlay is not bogus after all, because a separateCtrl+F7branch at13e8:1a20toggles1478:0ee0while the other two F7-family toggles write1478:2bc9and1478:2bca. - The follow-up pass closes the
~versusjassica16confusion more tightly too.jassica16is the earlier raw scan-code matcher that toggles both1478:0844and1478:6045, sets1478:8c52, and can therefore bootstrap the whole cheat state from cold;~is only the later translated logical-0x7ebranch, so Shift is the expected normal gesture on a US layout and that hotkey can only flip1478:6045after1478:0844is already enabled. The same pass also improves the third-overlay classification:Ctrl+F7is not a third generic camera grid but anEggHatcherProcesstrigger-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.mdnow has a consolidated live-NE cheat/debug key matrix, the practical-laurieplusShift+~bootstrap recipe for full keyboard cheats, and a fuller egg-hatcher note grounded inEggHatcher_CreateProcess/EggHatcherProcess_Run: non-monster egg families are enter/leave trigger items with X/Y/Z range checks, whileCtrl+F7visualizes the live egg-hatcher ranges andAlt+F7visualizes the related snap-process egg list. - The
0x85freader side is now clearer too. The live NE database now names the paired13e8transition wrappers asGame_DisableGameplayInputAndRefreshCamera(13e8:0e7d) andGame_RestoreGameplayInputAndClearModalState(13e8:0ef9), which matches their concrete behavior:13e8:0e7dclears the controller/key-input latch1478:27cb, raises the modal overlay-suppression state at1478:2c64/1478:8c53, preserves1478:8c54from1478:2d24, and refreshes camera state;13e8:0ef9performs the inverse restore path and clears the secondary1478:6050latch. The Laurie-only wrapper side is clearer as well:Game_ShowLaurieHintComputerGump(13e8:0e31) is the hidden-lauriecomputer-gump hint path, whileGame_ShowLaurieHintIfGameplayInputActive(13e8:0f4a) only calls it when0x85fis high. The main camera pass consuming the same gate is nowCamera_RedrawViewportAndGameplayOverlays(1180:19c1), with comment-backed1188:010f/1188:0394overlay helpers bracketing the viewport redraw. - The next blocker layer is narrower too. Those modal wrappers are not abstract helpers; inside
World_HandleKeyboardInput_13e8_14b4they 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, event0x7eremains the only other recovered writer of0x6045besidesKey_CheckCheatToggle, so a successfuljassica16match can still be undone later by that independent runtime path.Key_CheckCheatToggleitself is now comment-backed as keydown-only and still requires top-row1/6scan 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.EXEnow has a runtime correction too. The F10 branch at1148:0d0estill reaches the same modifier helper at11e0:01a8, and live testing shows the practical gesture is holdF10first and then pressCtrl, notAlt. 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 ajassica16table at1480:2ff0, but the latch-enabling sequence in No Regret is the second table at1480:2ffc, decoded asloosecannon, which toggles1480:0ac0and mirrors the result into the F10 latch byte1480: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(file0x70d75/ relocation entry0x71d68) and000c:99dd/000c:99e0(file0xc99dd, seg126 chain0x25e0). The deferred0x42f -> 000c:99dd -> 000b:9c0ddesign remains explicitly rejected: it visibly entered the hidden UI path, but it halted with the retailFILE\FLEX.C, line 83failure and dropped into the quit line, so0x42fis the wrong deferred context even though the modal wrapper address itself was valid. The newer direct0007:0d79 -> 000b:9a86current-slot retarget with the narrowed000b:9a8darg patch was also runtime-tested and produced no hidden menu, so the writable/Writable/CRUSADER-PATCHED.EXEtest build is now moved to the next defensible variant instead: restore the direct hook to000a:5276, keep the current-slot wrapper unpatched, and retarget the later controller-side000c:99e0call to000b:9c0dwhile zeroing only the inherited modal-wrapper words at000b:9c4a. - The next retail test build is narrower still. User runtime feedback on the first
Ctrl+Qpatch 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 raw000c:99e8/ live13e8:25e8fromPUSH 0x3e8to a near jump into the shared epilogue at13e8:29a7, so the reused13e8:25dddeferred lane exits immediately after the retargeted13e8:25e0 -> 13a0:020dcall instead of falling through into the original0x42fbranch 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_NextUsecodeOpat1418:04c3..051d, specifically just before the1408:02f5call at1418:051d. That means the bluntCtrl+Q -> 13a0:020dpatch is not merely stuck in the seg109 modal wrapper; it has activated the interpreter-side debugger-state path guarded by non-null0x659c/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 realusecode_debugger_break_state_createobject at1408:0000before 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
0x410body at13e8:230dto lazily construct a seg1408 state object through the existing far-call slot at13e8:2352 -> 1408:0000, stores the returned far pointer into0x659c/0x659e, and then reuses the second existing far-call slot in that same body (13e8:235c) to jump directly tousecode_debugger_open_for_current_unitat13a0:0086with zeroed wrapper arguments. This keeps the patch hotkey-local instead of rewriting the shared seg1408 callback table at1478: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:65abhad first been retargeted to segment109instead of117for13a0:0086), the global callback rewrite still caused startup failure. The surviving script fixes from that pass remain important: the large13e8:230dbody must use on-diskFF FF 00 00placeholders rather than disassembly-resolved far operands, and its patched byte array must include the final trailing0xC7so 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 old1478:65abcallback 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+Qimmediately 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 calling13a0:0086on the keypress itself. - The live candidate is now a per-object callback redirect. The
0x410body at13e8:230dstill creates/stores the seg1408 debugger-state object at0x659c/0x659e, but the second existing far-call slot in that body (13e8:235c) is now retargeted to1408: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 offset0x65abto the private relocated slot0x65af, and the private dword at1478:65afis retargeted from1408:0474to13a0:0086. That should let the next interpreter-side debugger callback open the current-unit UI without inheriting the liveCtrl+Qkey event, while the original shared1478:65abslot 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:230dbody patched/restored,13e8:235cstep-arm call patched/restored, private callback slot1478:65afpatched/restored, and legacy shared callback slot1478:65abheld 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+Qproduces 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+0x75remains gated by the seg1418 nesting counters+0x76/+0x78often 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:230dbody still constructs/stores the seg1408 debugger-state object and repoints that object to the private callback slot1478:65af -> 13a0:0086, but it now writes+0x75 = 0and+0x74 = 1in the object itself rather than retargeting the second13e8:235ccall slot to1408:0419. That matches the surviving UI-side control path at13a0:1e5d, where+0x74is the unconditional break-on-next-callback mode while+0x75is 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:230dbody patched/restored, private callback slot1478:65afpatched/restored, shared callback slot1478:65abheld 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:65afbreak-next variant is now negative evidence as well: the game crashes on launch again, so even the supposedly private65afslot now looks too globally visible to repurpose. Current best implication is that the object-local0x65affirst-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:230dbreak-next object creation/store path, but restores the shared1478:65abslot, stops repointing1478:65afto13a0:0086, patches1408:0474into a tiny guard that returns immediately unless0x659c/0x659eis armed, and uses the apparently unused relocated dword at1478:6597as the far target slot for13a0:0086. This newer6597/1408:0474build now also round-trips cleanly on a fresh copied retail EXE:13e8:230dbody patched/restored, guarded callback code at file0xCEE6Fpatched/restored, wrapper target slot at file0xEA197patched/restored, and all older direct/deferred experiment sites held at original bytes. - The root-cause read on the
65afstartup failures is now sharper:0x65afis not an alternate vtable base at all. The constructor at1408:0000writes0x65abto object offset+0, and the dispatch sites prove that objectCALLF [BX]uses the dword at65ab -> 1408:046fwhile objectCALLF [BX+4]uses the next dword at65af -> 1408:0474. Rewriting the object first word to0x65aftherefore 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:230dlazy 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 as0x65ab, restores the method-1 helper at1408:0474, patches only the method-0 break callback at1408:046fto indirect through the spare relocated dword1478:6597, and uses that slot as the far target for13a0:0086. This corrected046f/6597build also round-trips cleanly on a fresh copied retail EXE:13e8:230dbody patched/restored, break callback code at file0xCEE6Fpatched/restored, deferred target slot at file0xEA197patched/restored, and all older direct/deferred experiment sites held at original bytes. - User runtime on that shared-
046fmethod-0 build is now negative evidence too: startup still crashes, which makes the shared method body just as globally sensitive as the shared65ab/65afvtable 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:230dlazy 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 to0x658f, retargets private method 01478:658f -> 13a0:0086, retargets private method 11478:6593 -> 1408:0474, and leaves the shared callback bodies and shared65ab/65aftable entries untouched. This private-vtable build also round-trips cleanly on a fresh copied retail EXE:13e8:230dbody patched/restored, private method 0 slot at file0xEA18Fpatched/restored, private method 1 slot at file0xEA193patched/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/6593pair 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) = fatalLoad 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..673cis a live function-pointer table containingUsecodeProcess_*,Process_Terminate,Process_Fail, and nearby null handlers, not spare relocated dwords. The script no longer offers those candidates.
- Candidate A (
- 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:046fand the adjacent1408:0474, but0474is a real helper that returnsDX: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:0474was necessary but not sufficient: the shared1408:046fbody 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:230dbody was malformed, and the supposed deferred target slot at1478:6597is no longer treated as spare storage. The current PowerShell build still keeps the retail debugger object's real1478:65abvtable base and still arms break-next through the patched13e8:230dbody, but it now avoids both the shared seg1408 callback bodies and the1478:6597data slot entirely. Instead, it patches the existing interpreterCALLF usecode_debugger_maybe_break_on_current_lineat1418:04b5to a corrected private stub at13e8:232d, and it also reuses the second retail far-call slot inside13e8:230d(13e8:235c) as the actual private UI-call target. The13e8:230dbody itself now correctly handles both cases: reuse and arm an existing debugger-state object at0x659c/659e, or lazily create/store one before arming break-next. One implementation bug from the first O/P refactor is now fixed too: the second13e8:235crelocation 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 retailDispatch_ModalGump. Current candidates:- Candidate O = interpreter callsite retarget ->
13a0:020d, with13a0:024azeroed inherited modal-wrapper words - Candidate P = interpreter callsite retarget ->
13a0:0086, with13a0:008fzeroed inherited current-unit-wrapper words Both apply/restore cleanly on a disposable retail copy and are the next runtime tests.
- Candidate O = interpreter callsite retarget ->
- 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-step65afbuild, break-next65afbuild, guarded0474trampoline, shared046fmethod 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 at1418:04aa..04b5now callsusecode_debugger_maybe_break_on_current_linewhenever the far pointer at0x659c/0x659eis 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 at0x659c/0x659e. That makes the breakpoint callback lane a stronger original-entry candidate than direct event0x103retargeting. - The follow-up doc reconciliation is now closed too.
docs/ne-segment1.mdno 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 at1418:04aa..04b5as the bridge between them. - The live NE
CRUSADER.EXEmapping for that hidden-menu lane is now explicit and comment-backed in Ghidra too: direct hook1130:2b75/2b78, current-slot wrapper13a0:0086with constructor arg site13a0:008d, modal wrapper13a0:020dwith inherited-arg patch subsite13a0:024a, listener create/dispatch13a0:19b1/13a0:1df3, compiled0x410CD-transfer-display body13e8:2303, deferred controller-side hook13e8:25dd/25e0, and the supporting cheat-state data cells at1020:2833,1020:283d,1020:0844,1020:6045,1020:604f, and1020:6050. The0x410body is still documented in place rather than renamed because it remains embedded inside the oversizedWorld_HandleKeyboardInput_13e8_14b4function object. This improved live handoff and patch reproducibility still does not justify a headline estimate change by itself. - The retail
-debugswitch is now separated cleanly from that hidden debugger lane too. LiveHandleCommandlineArgsrecovery inCRUSADER.EXEconfirms a real"-debug"branch at1048:0a93that setsg_debugMsgLevel = 10(1478:87e0), printsDebugging mode ON., and writes1478:0845/0859(g_someDebugFlag/g_someDebugFlag2). The0x87e0threshold is read byConsolePrintf/DebugPrintAndWaitForInput, and0x0859is read by the segment1468VideoPlayer_*neighborhood (1468:2869,1468:2af4, helper1468:2de9). Current best read issurviving debug-output / media-instrumentation switch, notdead parser stub, and still notthe missing seg109/seg1408 usecode debugger bootstrap, because the same pass found no evidence that-debugconstructs or stores the real debugger-state pointer at1478:659c/659e. Focused write-up now lives indocs/retail-debug-arg.md. - That
-debuglane is tighter again after the follow-up deep dive and live Ghidra refinements. The seg1468 readers are now renamed/comment-backed asVideoPlayer_AdvanceFrameAndHandleSkip,VideoPlayer_StreamChunks, andVideoPlayer_DrawDebugTimingOverlay, and the helper body is no longer onlyinstrumentation-looking: it writes two500-byte marker traces into adjacent scanlines near the bottom of a0x280-wide AVI playback buffer, using timing deltas scaled by6000. Current best read is thereforebuilt-in movie-playback timing overlayplus the separateg_debugMsgLevelconsole/positioned-print threshold, still explicitly distinct from the hidden seg109/seg1408 usecode debugger path at1478:659c/659e. - The same
-debuglane 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,-debugalso changes the global seg12d0 print threshold (1478:87e0 = 10) used byConsolePrintf,DebugPrintAndWaitForInput, and the positioned print helpers, but the sink side is now tighter as well:ProbablyPrintDebugMessageformats through the static stdio-style table at1478:6c32..6c81and writes to the handle-1entry at1478:6c46, so the non-video side is ordinary DOSstdoutgated 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 still1478:0845, which remains a parser-set latch with no recovered downstream consumer. - The print inventory behind that same
-debuglane is now materially tighter too. A focused pass recovered concreteConsolePrintf/DebugPrintAndWaitForInputstrings instead of only caller families: startup/arg strings such asDebugging mode ON.,You DO need help!,Enabling ENHANCED mode. (NOT!),Warping to mission %d ...,Defaulting to skill level %d, andDemo mode.; init/config strings such asUsing map patch file.,Running with partial installation.,Running with full installation., andRedirecting mission %d tune to '%s'; cache/swap progress scaffolding such asCreating Swap file [and repeated./ bracket fragments; plus stronger failure/debug-stop fingerprints likeCOULD NOT CREATE GLOB ITEM!,No room for Dispatcher Record/Playback.,End of script! (press any key), andOut of Memory! [%u]. Recovered call levels so far are0x32and0xff, both above the-debugthreshold of10, 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
E69FBand a possible secondary monochrome monitor is now materially weaker too. Local NE-segment mapping puts0xE69FBat live address1478:2dfb, which falls inside theSYSTIMER.Cstring 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 ordinarystdoutat1478:6c46, and targeted searches found no directmono/monochrome/hercules/MDAstrings or obvious monochrome-adapter port/memory references (0x3b4/0x3b5/0x3b8/0x3ba,B000). Current best read is thereforefolklore 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.EXEpass still shows the same broad cheat/debug framework with shifted addresses rather than a rewritten system:-lauriesets the broad master gate at1478:0910, the gameplay-input gate still exists at1478:0927, the lower keyboard-cheat latch still exists at1478:5fb3, event0x410at13e8:2211still toggles the CD transfer display, and13e8:24a5still toggles Hack Mover with English-facing strings. But the sequence side is narrower and more specific now: a direct live byte scan found no exactjassica16table24 1e 1f 1f 17 2e 1e 02 07 00anywhere in Spanish data1478:0000-8c3for1480:0000-1fff, and the old English-side slot at1478:2833now 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 by1478:8ad6before 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 writes1478:8ad6?" Detailed notes now live indocs/spanish-cheat-differences.md. - The follow-up Spanish-cheat pass narrows that further: in the live
/es/CRUSADER.EXEdatabase,1478:0910still has only the-lauriewrite at1050:0985,1478:5fb3still has only the Laurie-hint helper writes at13e8:0071/0077, and1478:8ad6still has no recovered direct writer even though13e8:249btests it before Hack Mover. The old English matcher slot at1478:2833remains repurposed as pointer-like words, and the only explicit multi-key helper recovered in this pass (11d0:024b) is just a generic key-list membership check used from a movement/control cluster, not a cheat-toggle lane. Current best read is therefore now stronger than "unknown moved Spanish matcher": no live replacement cheat-trigger byte matcher has been recovered at all, and the remaining proof frontier is any non--lauriewriter of1478:0910or a real writer for1478:8ad6. - The next localized-build pass narrows the practical keyboard side too. Full decompilation of
World_HandleKeyboardInput(13e8:14b4) still shows the Spanish broad-gate debug family (0x141,0x241,0x410, Hack Mover), but no recovered translated~/0x7echeat-latch branch. That means the old English-laurieplus tilde bootstrap is no longer a defensible Spanish assumption:-lauriestill raises1478:0910, but no runtime tilde writer of1478:5fb3has been recovered, and1478:5fb3itself now reads more like a widely consulted gate byte than a proven English-style keyboard-cheat latch. - The deeper keyboard-handler pass strengthens that again:
1478:5fb3is not just missing a recovered tilde writer, it no longer behaves like a positive enable latch at all. Every recovered consumer inWorld_HandleKeyboardInputrequires1478:5fb3 == 0, and the only recovered writers are the Laurie-hint helper pulse13e8:0071then13e8:0077, which leaves the byte cleared. Current best localized-build answer to the user-facing cheat question is therefore:-laurieis the only recovered positive enabler for the surviving broad Spanish cheat/debug hotkeys, while Hack Mover remains separately blocked behind the still-unwritten1478:8ad6gate. - The next Spanish follow-up narrows two remaining folklore assumptions too. First, the old English immortality-string slots at
1478:2850/2866are repurposed as pointer-like data in/es/CRUSADER.EXE, and no direct uses of those addresses were recovered, so the EnglishF10replenish / immortality path is nowunproven in Spanishrather than merelynot yet re-closed. Second, the new8ad6hunt found a nearby runtime-state cluster at8ad7/8ad8/8ad9, but the actual neighbor writes belong to gameplay-input modal helpers and the Hack-Mover-adjacent runtime helper13e8:282f; they still do not touch8ad6, which keeps the best writer hypothesis pointed at an indirect or script-driven path rather than ordinary compiled keyboard logic.
Recently Closed Or No Longer Live
- The most reusable
misc_crusader_notes.txtscratch items are no longer loose leads.STEAM2event hints are now checked against extracted USECODE rows, the old labelsFUN_1130_0896/FUN_1130_32af/FUN_1020_0000/FUN_1128_026bare closed against live NE names,ItemNPC_AnotherCreateis now explicitly documented as the area-search-gated helperNPC_CreateIfAreaSearchValid,Kernel_11d0_2491is narrowed to a kernel/process snapshot writer,FREE::ordinal3Cis constrained to an alert-clearing randomFREE::ordinal21spawner, andInt01Eis at least tightened fromunknown fire intrinsictoActor::I_maybeFireplus live export1128:11da. ASYLUM.24is 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_defaultownsg_active_dispatch_entry_farptr, while seg049/seg126/seg138 helpers only borrow or clear the shared byte+0x40; the seg1080x4f38lane 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 root0x6ae:0x6b0, and the startup/display family continues to treat0x6aa:0x6acas an inherited mutable external/default base path. - The in-scope
0x31a2transition/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/0x11is still open, and the000c:db68overlap still blocks clean function hygiene fortransition_preentry_step_scripteven though it no longer blocks semantic recovery.
Live Blockers
- The oversized overlap rooted at
000c:db68still blocks clean recovery of the realtransition_preentry_step_scriptfunction object, even though it no longer blocks startup/display semantics. - The
0x4588callback 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. - The USECODE/VM sequencer still lacks the real upstream selector/caller path into
entity_vm_opcode_sequence_run, and wrappersentity_vm_context_try_create_mask_0400_slot0a_with_offset/entity_vm_context_try_create_mask_0800_slot0b_with_offsetremain outward-caller-dark even though their exact signed-additive(slot, mask)contracts are now closed, the generic masked hub at000d:463ais verified, and slot-0x12now has two concrete caller anchors at0005:1776/0005:1945. - High-value missing or weak function objects still exist in hot ranges such as
000b:2e00,0007:5a00, and000e:ffb0;000e:ffb0is now caller-side constrained to the overlapped video-frame chunk lane (00db/00dc) paired withanim_load_audio_frame, but the overlap still blocks clean recovery. - 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.
- The
Ctrl+Q/0x410lane 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 beclass_id + 2, the header/subentry arithmetic at000d:5066/51fd/53b4matches extracted class headers and event rows exactly, andNPCTRIGslot0x0a/0x20now 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:45c5only maps entities into three generic category spans,000d:44dfseeds those spans from0x6608..0x660e,0005:295freuses the same slot index to test owner-row bit0x0040, and0005:2c35still 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/+0xd8control stream and then into the000c:fa2fliteral/replay lane.
Current Focus
- Continue the NE
CRUSADER.EXElane, using verified raw full-EXE and standalone-segment work as cross-reference evidence rather than as the active execution target. - Continue the USECODE/VM lane where the verified masked-create hub (
000d:463a), the internal consumer blocks (000d:208b,000d:21ed), or the newly separatedextra-word masked materializersubfamily can still yield concrete caller, selector, or record-shape evidence rather than repeated direct-xref dead ends. - Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
- Use boundary repair only on active blockers with clear payoff, with
000c:db68now downgraded to optional hygiene unless it blocks adjacent work again. - Revisit the
0x4588callback object only when caller-side evidence is strong enough to support behavioral naming. - Use the new offline map-rendering lane to cross-check shape ids, map placements, and visible world composition against
crusader-disasmshape/map notes before promoting additional rendering- or static-object-related names inCRUSADER.EXE.
Next Resume Point
-
Continue the NE
CRUSADER.EXElane fromdocs/ne-hole-filling-priorities.md, usingdocs/crusader-disasm-reference.md, the raw-focused docs, and priorCRUSADER-RAW.EXEnotes 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. -
Build one conservative shape-id / map-placement crosswalk from
shapedata_more_complete.txtandmapdump/map-item-dump.txtinto the current trigger-heavy class families before promoting any new NE names. -
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. -
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 separatedextra-word masked materializersubfamily can still yield concrete caller, selector, or record-shape evidence rather than repeated direct-xref dead ends. -
Refine the coverage ledger from already-verified notes before broadening into fresh segment sweeps.
-
Use boundary repair only on active blockers with clear payoff, with
000c:db68now downgraded to optional hygiene unless it blocks adjacent work again. -
Revisit the
0x4588callback object only when caller-side evidence is strong enough to support behavioral naming. -
Exercise
tools/render_crusader_map.pyon a few representative No Remorse and No Regret maps, then tighten the paint order usingTYPEFLAG.DATfootpads and any mismatches visible against in-game screenshots orcrusader-disasmmap evidence. -
If the map/editor-visibility lane is revisited, start from
docs/editor-object-visibility.mdand the upstream1180:0951..095cworld-item builder gate first; rule in or rule out a second debug-only world-item builder before spending more time on cheat or command-line searches. -
Continue the PSX pre-alpha lane from
docs/psx/prealpha.md: classify the survivingLoadExeccallers around80046aac, confirm whether theTALK1.XApath is still reachable in practice, and compare the three shippedLSET1bundles against the retail extractor outputs before assuming the build is only a content-pruned No Remorse branch. -
Recover the real upstream caller/selector path into
entity_vm_opcode_sequence_run, most likely by finding the first non-recursive0x6714context-method caller or vtable dispatch site rather than by repeating raw xref queries that still return no direct edges. -
Recover real caller roles for
entity_vm_context_try_create_mask_0400_slot0a_with_offsetandentity_vm_context_try_create_mask_0800_slot0b_with_offsetby treating them as the remaining dark members of the now-verified signed-additive masked-materializer subfamily and comparing them against the newly anchored slot-0x12caller pattern. -
Tighten the newly surfaced higher-slot wrapper ladder around
0005:3115..31da, especially the two slot-0x12caller sites at0005:1776/0005:1945and the slot-0x10guarded callsite, so any future promotion toleaveFastArea/func11|cast/justMoved/AvatarStoleSomething/animGetHitis driven by binary caller behavior rather than by external tables alone. -
Tighten the outward caller chains around the renamed seg006 masked helpers
entity_vm_context_try_create_mask_0008_slot30_with_offset(0006:0ba4) andentity_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. -
Tighten the paired-file-family reading of the seg070 twin loops at
0009:67b6and0009:6916by recovering which temporary buffer and record schema each family populates behindentity_vm_runtime_owner_resource_create. -
Promote additional ledger rows where the current docs already justify
Foothold,Partial, orDeep. -
If the VM lane stalls again, revisit
000e:ffb0from the now-verified00db/00dccaller windows and try to recover an adjacent non-overlapped helper before attempting any boundary repair. -
If the immortality lane is revisited, stay focused on
NPCTRIGslot0x0afirst, with slot0x20still treated as the typed/setup companion andEVENTonly as the generic hub baseline; the three currently recovered direct0005:295fcaller families are now all closed and comment-backed in the live NE program at10f0:02d9,10f0:0379,10f0:03c3,10f0:03e5,1128:0ff0, and1138:1384, so the next defensible step is an earlier producer that assigns subtype0x20b/0x20cinto field+0x3cor otherwise chooses the owner-loaded class family before these generic damage consumers run. -
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, and000d: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
NonetoFoothold/Partialwhere 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/0x11only 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:db68only when a cleantransition_preentry_step_scriptfunction 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_runand 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_offsetandentity_vm_context_try_create_mask_0800_slot0b_with_offset. - Keep separating owner-table-backed
0x39carows 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
0x4588so the allocator finalize path and callback emissions can receive behaviorally meaningful names. - Tighten the role of
allocator_phase_finalize_passonly 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_StopAllSFXand 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:ffb0and 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,0x4588object 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
- Startup/display transition lane
- VM / USECODE selector and loader lane
- Coverage-ledger refinement from already-verified notes
- High-value overlap repair (
000c:db68, then000e:ffb0when justified) 0x4588callback-object classification- Broader segment sweeps and second-pass data/relocation work
Evidence Anchors
Primary files backing this plan state:
crusader_segment_coverage_ledger.csvcrusader_decompilation_notes.mddocs/overview.mddocs/raw-porting-progress.mddocs/raw-0008-000c.mddocs/raw-000a-000d.mddocs/raw-000e.mddocs/crusader-disasm-reference.mddocs/ne-hole-filling-priorities.mddocs/far-call-targets.mddocs/usecode-roundtrip-ir.mddocs/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.