Add PyGhidra Crusader Toolkit and patch scripts

- Introduced README.md for the PyGhidra Crusader Toolkit, detailing setup and usage instructions.
- Added bootstrap_env.ps1 script to create and refresh the Python virtual environment with necessary packages.
- Implemented _tmp_patch_hidden_cheat_menu.py and _tmp_patch_hidden_cheat_menu_deferred.py scripts for patching specific memory addresses in Ghidra.
This commit is contained in:
MaddoScientisto 2026-03-25 08:15:21 +01:00
commit ad6ebd0b86
132 changed files with 41758 additions and 99 deletions

View file

@ -0,0 +1,155 @@
# Crusader Disasm Reference Corpus
## Purpose
This note records the reusable knowledge in the separate local project at `K:/ghidra/crusader-disasm` and how it should be consumed inside the main `Crusader_Decomp` workflow.
Treat that repo as an auxiliary evidence corpus, not as direct rename authority for `CRUSADER-RAW.EXE` or the NE-loaded `CRUSADER.EXE` program.
## Main Assets
### Handwritten notes
`misc_crusader_notes.txt` is a compact scratchpad with a few still-useful anchors:
- keyboard event/code notes: `CTRL-M = 0x432`, `CTRL-L = 0x426`, `CTRL-V = 0x42f`, `CTRL-Q = 0x410`
- shape/event examples: `STEAM2` (`shape 1297`) is noted with event `15` (`enterFastArea`) and 24 animation frames
- `SnapEgg` (`shape 0x4fe`) is noted as entering the fast area and being handed to a `SnapProcess`
- chest/monitor examples preserve concrete shape ids and field snapshots that can be cross-checked against map/object dumps
- old standalone function labels such as `FUN_1130_0896` are useful only as historical waypoints; they are not direct rename authority for the current raw or NE Ghidra databases
Safe use:
- event/id hinting
- shape-id cross-checks
- lead generation for later binary confirmation
Unsafe use:
- direct function renames based only on these handwritten notes
- assuming the old segment numbering matches the current `CRUSADER-RAW.EXE` or `CRUSADER.EXE` imports without a verified address mapping
### Shape metadata tables
`shapedata.txt` and especially `shapedata_more_complete.txt` provide a broad `Shape info N data 4(top),5,7,8` table.
Current best reuse:
- cross-check `shapeinfo` bytes and flags against item/entity behavior already seen in the binary
- identify repeated per-shape flag/value families before promoting any field meaning
- support later naming of shape-related globals, cache entries, and trigger/object classes
This is one of the strongest data-side additions in the external corpus because it is broad, structured, and easy to re-test locally.
### Static map/object dumps
`mapdump/map-item-dump.txt` is a large coordinate/event-style dump of placed world items.
Current best reuse:
- correlate static placements with `EVENT`, `NPCTRIG`, `CRUZTRIG`, and related USECODE families
- cross-check specific shapes noted in `misc_crusader_notes.txt` against real map presence
- build targeted map-level test cases for later `CRUSADER.EXE` naming and annotation work
This file is valuable because it gives real world-layout evidence that complements the owner-loaded USECODE work already documented in `docs/usecode-roundtrip-ir.md`.
### USECODE opcode list
`usecode_opcodes.txt` contains an extracted opcode table for the JP release of Crusader: No Remorse.
Useful properties:
- broad opcode coverage from `0x00` through `0x7b`
- readable names for assignment, local/member reference, control-flow, spawn/process, and search-family operations
- a concrete local opcode vocabulary independent of ScummVM/Pentagram naming layers
Reuse rule:
- use these names as parser and report hints only
- do not assume the textual names alone are enough to rename compiled handlers in the DOS binary
### Intrinsic and function dumps
The `unkcoffs/` directory holds older cross-version name tables such as:
- `reg_functions.txt`
- `rem_functions.txt`
- `reg_intrinsic_dump.txt`
- `rem_intrinsic_dump.txt`
- `u8_intrinsic_dump.txt`
These dumps are useful because they preserve prior RE vocabulary for Remorse/Regret/U8 function and intrinsic families, including many item, actor, audio, palette, and world operations.
Current safe use:
- hint-only metadata for intrinsic ordinals, signatures, and broad subsystem labels
- cross-version comparison when a Remorse/Regret difference matters
- porting candidate generation for the NE `CRUSADER.EXE` project, where segment-based labels may be easier to reconcile than in the flat raw import
Current unsafe use:
- direct rename authority for modern Ghidra functions
- assuming one game's intrinsic numbering matches another without local confirmation
### Combat data note
`combat_dat/readme.txt` records that the extracted `combat.dat` tactic files are identical between No Remorse and No Regret.
That is small but useful: tactic names from the combat data are portable labels and should be treated as version-stable unless contradicted by later binary evidence.
## How This Fits The Existing Docs
This external corpus mainly strengthens four areas already active in `Crusader_Decomp`:
1. `docs/usecode-roundtrip-ir.md`
The opcode list, intrinsic dumps, and static trigger/map data provide local cross-checks for the USECODE parser and event-family work.
2. `docs/ne-segment1.md`
The handwritten note set preserves shape ids, keyboard/event codes, and gameplay object examples that can be matched against the segment-1 gameplay/input lane.
3. `docs/raw-porting-progress.md`
The external notes add candidate gameplay/object labels and map-backed test targets, but they should remain supporting evidence until verified in the raw full-EXE database.
4. `docs/overview.md`
The separate disasm repo is now part of the local evidence stack alongside ScummVM and Pentagram, but unlike those source ports it is a prior RE corpus tied directly to Crusader assets and old disassembly work.
## Safe Reuse Policy
Use the external disasm repo for:
- opcode-name hints
- intrinsic/signature hints
- shape-id and map-placement cross-checks
- event-code and key-code lead generation
- candidate subsystem vocabulary before binary confirmation
Do not use it for:
- speculative raw-function renames
- address mapping without an explicit verified translation
- replacing direct binary evidence from `CRUSADER-RAW.EXE` or `CRUSADER.EXE`
## Immediate Porting Frontier For `CRUSADER.EXE`
The next practical use of this corpus is not another raw-only note pass. It is a controlled porting pass into the NE-loaded `CRUSADER.EXE` project in Ghidra.
Best initial targets:
1. Port already-verified raw names that clearly correspond to NE-segment functions where the segment:offset identity can be confirmed directly.
2. Use `unkcoffs/` function and intrinsic dumps as hint-only comparison tables when the NE database exposes clearer segment-local call structure than the flat raw import.
3. Use `map-item-dump.txt` plus shape tables to annotate trigger-heavy or object-heavy NE lanes before promoting any names.
4. Use `usecode_opcodes.txt` to keep future USECODE parser/report output aligned with an additional local opcode vocabulary, especially where ScummVM and Pentagram leave placeholders.
## Recommended Order For The Next Porting Pass
1. Start with one small NE segment or subsystem that already has strong raw names or old disasm vocabulary.
2. Prefer functions with direct string, data-table, or caller-role evidence over unlabeled wrappers.
3. Use the map/shape corpus to explain data-driven objects first; use the intrinsic/function dumps only as secondary hints.
4. Record exact successful raw-to-NE name correspondences so later passes can reuse the mapping instead of re-deriving it.
## Current High-Value Follow-Ups
- Build a shape-id crosswalk between `shapedata_more_complete.txt`, `map-item-dump.txt`, and the existing `EVENT` / `NPCTRIG` / `CRUZTRIG` families.
- Compare the handwritten key/event codes against the already-named cheat/input paths to see which parts of the old notes are now directly closed.
- Use `rem_functions.txt` / `reg_functions.txt` to identify conservative candidate names for still-positional NE functions, but only when the local caller/data evidence matches.
- Keep the external disasm corpus explicitly separated from ScummVM/Pentagram-derived evidence so provenance stays clear in future porting notes.

View file

@ -0,0 +1,218 @@
# CRUSADER.EXE Hole-Filling Priorities From CRUSADER-RAW.EXE
## Purpose
This note tracks the highest-value places where the NE-loaded `CRUSADER.EXE` project is still materially unclear, but the older `CRUSADER-RAW.EXE` project already holds enough verified structure to close the gap.
The goal is not to duplicate the raw notes. The goal is to rank the NE-side holes that are most likely to collapse quickly once the raw-side evidence is ported carefully.
Evidence used here:
- live Ghidra MCP checks in the open `CRUSADER.EXE` session
- `plan-mid.md`
- `crusader_segment_coverage_ledger.csv`
- `docs/raw-porting-progress.md`
- `docs/raw-0007-rendering.md`
- `docs/raw-0008-000c.md`
- `docs/raw-000a-000d.md`
- `docs/raw-000e.md`
- `docs/crusader-disasm-reference.md`
## Priority 1: VM / USECODE Selector And Owner-Loaded Runtime Lane
### Why it is still unclear in `CRUSADER.EXE`
- The NE project already carries the main structural names, but the actual upstream selector into the VM sequencer is still unresolved.
- The dark wrappers around slot `0x0a` / `0x0b` are named structurally, not behaviorally.
- The owner-resource helper is partially understood, but the exact class-family selection and configured owner-file naming are still open.
- Coverage ledger state is still only `Partial` for segments `133`, `134`, and `135`.
### What the raw project already knows
- `000d:463a` is now a verified generic masked-create hub: `entity_vm_context_try_create_masked_for_entity`.
- `000d:45c5` is a three-way category mapper: `entity_vm_slot_index_from_entity`.
- `000d:4c99` and `000d:7000` classify the runtime bootstrap and owner-resource creation path.
- `000d:ebe3` is a verified ordered opcode sequencer: `entity_vm_opcode_sequence_run`.
- `0005:2c35` is already narrowed to `entity_vm_context_try_create_mask_0400_slot0a_with_offset`, but its outward caller role is still dark.
- The raw-side docs also already carry the exact `NPCTRIG` / `EVENT` body-window fits, referent-registry model, and the current selector blocker.
- `docs/crusader-disasm-reference.md` already identifies the best auxiliary corpus for this lane: `usecode_opcodes.txt`, `map-item-dump.txt`, `shapedata_more_complete.txt`, and the `unkcoffs/` dumps.
### Porting work that should close the gap
- Port the raw-side selector, context, and owner-loader comments into the NE project at the same segment:offset anchors.
- Keep the slot-`0x0a` / slot-`0x0b` wrappers conservative until real caller roles are recovered.
- Use the external opcode list and map/shape corpus only as hint generators while reconciling the live NE caller path.
- Prioritize recovery of the first caller that turns the category spans from `000d:45c5` into a concrete class-family choice.
### Live anchors in the open NE project
- `000d:45c5` -> `entity_vm_slot_index_from_entity`
- `000d:463a` -> `entity_vm_context_try_create_masked_for_entity`
- `000d:4c99` -> `entity_vm_runtime_create`
- `000d:ebe3` -> `entity_vm_opcode_sequence_run`
- `0005:2c35` -> `entity_vm_context_try_create_mask_0400_slot0a_with_offset`
### Latest verified NE pass
- `0005:295f` is now the only recovered non-hub consumer of `entity_vm_slot_index_from_entity` in the open NE session. It recomputes the slot index, tests owner-row bit `0x0040` directly, exposes that result to the caller, and only then optionally calls `entity_vm_context_try_create_masked_for_entity` with slot `0x06` and mask `0x0040`.
- The direct NE callers currently recovered for `0005:295f` are `0006:43c3`, `0006:c5f0`, and `0007:3584`. That materially narrows the selector frontier to three gameplay-side caller families instead of the older wider `find any caller of 000d:45c5` search.
- The first anchored caller family is now structurally classified in the live NE session. Repaired wrapper `0006:4379` is a seg031 dispatch-entry subtype gate over objects built by `0006:42d9`: event type `0x236`, source type `8`, subtype/tag at `+0x3c`, dword payload/source far pointer at `+0x32`, and aux words at `+0x36/+0x38`.
- In that wrapper, subtype `0x20c` at `0006:43c3` routes into `0005:295f`, while nearby sibling subtype `0x20b` at `0006:43e5` routes into `0005:2918` (`slot 0x05` / `mask 0x0020`) using the same `+0x36/+0x38` aux pair. The split is therefore local to one dispatch-entry family, not a direct compiled `NPCTRIG` / `EVENT` class-family selector by itself.
- The `0006:43c3` lane now shows where the owner-row bit-`0x0040` probe is consumed locally: inside a subtype-`0x20c` dispatch-entry object rather than at a generic descriptor-choice site. That improves caller provenance for `0005:295f`, but it still does not prove which owner-loaded class family seeded the later VM data.
- `0005:2c35` remains outward-dark in the current NE session: instruction search still shows no recovered code or data xrefs, and its proven local role is still only `sign-extended additive word -> slot 0x0a / mask 0x0400 -> generic masked hub`.
- The first live `CRUSADER.EXE` integration batch is now applied for this lane. Comment-backed anchors were added at `1420:0dc5` (`Item_GetUsecodeClassId`), `1420:0e3a` (`Usecode_ItemCallEvent`), `10a0:2718` (`Item_Hit`), `10a0:275f` (`Item_GetDamaged`), `10f0:02d9` (`StorageDataProcess_Create`), and `10f0:0379` (`StorageDataProcess_Run`), with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c` / `0x20b` split in the open NE program.
- Result of this pass: the compiled-side selector evidence still bottoms out at category spans plus owner-row capability bits, not a concrete `NPCTRIG` / `EVENT` class-family choice. The next defensible NE step is to classify the three `0005:295f` caller families or recover an earlier producer that feeds the later owner-loaded class lane.
## Priority 2: Rendering / Camera / Tile-Visibility / Watch-Controller Lane
### Why it is still unclear in `CRUSADER.EXE`
- The raw rendering/camera work is one of the strongest finished structural maps, but the NE ledger still shows only `Partial` coverage for the watch-controller lane (`seg049`) and `None` for several related render-heavy segments.
- The exact controller-versus-watched-entity ownership label around `0x2bd8` is still open.
- The NE side still does not have the same whole-lane readability that the raw `0007` rendering notes already provide.
### What the raw project already knows
- The raw notes already define the draw-list node format, tile visibility pipeline, screen/global state, scroll/camera globals, and the watch-controller family.
- `0007:ba00`, `0007:ba45`, and `0007:baea` already have conservative structural names and caller contracts in the raw project.
- The coordinate-transform lane is also already documented in a way that can be reused safely for the NE segments that call into it.
### Porting work that should close the gap
- Port the controller/object comments around `0x2bd8`, `0x2be0`, and the scroll globals before attempting any more aggressive renames.
- Carry the draw-list, tile-grid, and viewport/global names into the NE lane where the same DS globals and call shapes match.
- Use `map-item-dump.txt` and `shapedata_more_complete.txt` to explain object-heavy camera/trigger placements only after the binary side matches the raw lane.
### Evidence anchors
- Ledger: `seg049` = `Partial`, `seg053` = `None`, multiple surrounding render-heavy segments remain `None`
- Raw-side anchor doc: `docs/raw-0007-rendering.md`
## Priority 3: Startup / Display Transition And Active-Hold Lane
### Why it is still unclear in `CRUSADER.EXE`
- The NE startup/display lane is structurally strong, but one of the key bodies is still overlap-damaged.
- `000c:db68` currently resolves to `cursor_nav_update_and_dispatch`, but boundary analysis still shows an obviously oversized body spanning `000c:5b68 - 000c:ff1c`.
- The renderer-pair role at `0x8c5c/0x8c60` and the exact higher-level label for some preset paths remain open.
### What the raw project already knows
- The raw project already separates the seg126/127/136/137/138 transition helpers into a usable startup/display model.
- The `0x6828 + 0x40` borrowed hold byte is already separated from the seg108-local `0x4f38` bit-`0x40` lane.
- The fade-controller behavior and the shared break/hold state around `0x31a2` are already documented tightly enough to guide NE cleanup.
### Porting work that should close the gap
- Use the raw-side boundary and caller notes to split `000c:db68` back into the real local helper bodies before promoting any more labels.
- Port the hold-token provenance and the seg108-versus-seg136 separation comments into the NE database.
- Keep the remaining renderer-pair naming conservative until the overlap is repaired.
### Live anchors in the open NE project
- `000c:db68` -> `cursor_nav_update_and_dispatch`
- Boundary analysis for `000c:db40..000c:dc40` still reports `cursor_nav_update_and_dispatch @ 000c:db68 body 000c:5b68 - 000c:ff1c`
- Ledger: `seg126`, `seg127`, `seg136`, `seg137`, `seg138` are all only `Partial`
## Priority 4: `0x4588` Callback / Allocator / Palette Cleanup Lane
### Why it is still unclear in `CRUSADER.EXE`
- Many helpers in this lane are already named in the NE project, but the actual subsystem identity of the `0x4588` object is still unresolved.
- The same blocker is repeated across the ledger for segments `80`, `82`, `91`, `95`, and `138`, which means the NE project has structure but still lacks the final behavioral classification.
### What the raw project already knows
- The raw project already has a stable lifecycle map for `runtime_callback_object_init_once`, `runtime_callback_object_teardown_once`, `allocator_phase_finalize_pass`, and `entity_cleanup_resources_and_dispatch`.
- The raw notes already constrain the video-state snapshot pair, one-time guards, vtable call slots, emitted payload pairs, and the relationship to the watch-controller release path.
### Porting work that should close the gap
- Port field-level comments around `0x4588`, `0x4590`, `0x4594`, `0x4595`, `0x45a6`, and the emitted payload pairs before attempting any subsystem rename.
- Reconcile the raw callback emissions with the NE caller windows in segments `91`, `95`, and `138`.
- Only promote a behavior-level subsystem name once those emitted payload pairs and caller families converge in the NE project too.
### Live anchors in the open NE project
- `000a:4913` -> `runtime_callback_object_init_once`
- `0009:b1c3` -> `allocator_phase_finalize_pass`
- `000d:9afd` -> `entity_cleanup_resources_and_dispatch`
## Priority 5: Parser / RIFF / Animation Video-Subframe Lane
### Why it is still unclear in `CRUSADER.EXE`
- The main parser and animation entrypoints are already named in the NE project, but the surrounding segment coverage is still weak.
- The ledger still shows `None` for the raw-`000e`-mapped segments `99` through `103`.
- `000e:ffb0` is still only a positional function object and remains overlap-damaged.
### What the raw project already knows
- The raw notes already define the parser cluster, RIFF chunk matching, animation ring-buffer model, and the audio/video split.
- The raw-side caller proof already ties `000e:ffb0` to the `00db` / `00dc` video-frame lane paired with `anim_load_audio_frame`.
- The raw notes also already separate the text-oriented parser usage from the binary owner-loaded USECODE lane.
### Porting work that should close the gap
- Port the parser and animation comments into the NE segment windows that still show no formal coverage.
- Keep `000e:ffb0` conservative, but import the raw-side comment that it is the unresolved video-side subframe loader for the `00db` / `00dc` chunk lane.
- Normalize the segment-to-topic mapping for the `000e` range in the NE coverage ledger once those comments are in place.
### Live anchors in the open NE project
- `000e:2104` -> `animation_start`
- `000e:3639` -> `record_table_parse_buffer`
- `000e:ffb0` -> `FUN_000e_ffb0`
- Boundary analysis for `000e:ff90..000f:00f0` still reports `FUN_000e_ffb0 @ 000e:ffb0 body 000e:ffb0 - 000f:00e0`
## Priority 6: Gameplay / Input / Projectile Expansion Into Adjacent NE Lanes
### Why it is still unclear in `CRUSADER.EXE`
- Segment `1` itself is already deep, but the adjacent event/dispatch and targeting lanes are not equally complete.
- The ledger still shows only `Partial` coverage for `seg021` and `seg043` even though the raw project already holds stronger gameplay-side anchors.
- The old disasm corpus has shape and event-code evidence that can explain object-heavy gameplay lanes, but that evidence has not yet been turned into a disciplined NE crosswalk.
### What the raw project already knows
- Verified raw imports already exist for `shot_entity_alloc`, `projectile_update_tick`, and the rest of the main projectile chain.
- `seg021` already has a verified direct raw import anchor via `entity_count_by_type_a`.
- The external corpus adds key/event notes such as `CTRL-Q = 0x410` and map-backed shape examples like `STEAM2` and `SnapEgg`.
- The raw side also already narrowed the seg043 start-of-segment hole enough to reject the earlier bad mapping.
### Porting work that should close the gap
- Extend from the already-verified gameplay names into adjacent NE dispatch/targeting callers only when the segment:offset and caller role match directly.
- Build one conservative shape-id / map-placement crosswalk for trigger-heavy or object-heavy gameplay lanes before promoting new names.
- Use the old key/event notes as lead generation only, not as direct rename authority.
### Live anchors in the open NE project
- `0007:28ce` -> `shot_entity_alloc`
- `0007:371d` -> `projectile_update_tick`
- `0007:5a00` has no current symbol
- Boundary analysis for `0007:59e0..0007:5b40` still reports `0007:5a00` as a candidate entry and keeps the earlier rejected mapping explicitly closed
## Secondary Structural Gaps To Keep On The Tracker
These are real NE holes, but they are weaker `old -> new` closure candidates than the priorities above because the raw-side model is still not strong enough to drive safe naming by itself.
### `000b:2e00`
- Still a high-traffic gap in the NE project.
- Live symbol lookup returns no symbol at `000b:2e00`.
- Boundary analysis for `000b:2de0..000b:3050` shows a missing top-level entry at `000b:2e00` plus several later artificial subfunctions.
- Keep it on the list, but do not treat it as the first raw-to-NE porting target.
## Immediate Actions Justified Now
- Create and maintain this tracker document.
- Use it as the ranked starting point for the first focused `CRUSADER.EXE` porting batch.
- Avoid immediate new Ghidra renames in the listed holes unless a pass first repairs the overlapping function objects or recovers the missing callers.
- Conservative comments are justified before renames in the VM, transition, and animation lanes because the remaining uncertainty is mostly about caller provenance and boundaries, not about the already-known local contracts.
## Progress Estimate Impact
This note still does not justify a headline percentage change by itself.
The `plan-mid.md` progress estimates should stay unchanged until one of these prioritized NE passes produces verified new symbol, boundary, or ledger promotions beyond this local caller-family classification.

View file

@ -2,6 +2,8 @@
This file covers the standalone analysis of NE Segment 1 (`seg001_code_off_37600_len_8400.bin`), imported as a raw binary at base `0x0000`, language `x86:LE:16:Protected Mode`. All 35+ identified functions have been renamed and annotated in Ghidra.
For current project work, treat this file as a verified evidence source to be cross-referenced into the live `CRUSADER.EXE` session. When a claim here is used in the NE database or notes, keep the NE address and the older segment/raw address linked together where practical.
## Cursor Subsystem (0x00600x0d5f)
| Address | Name | Description |
@ -273,16 +275,51 @@ Current model for the two cheat bytes:
- The separate event-`0x7e` path at `000c:942d` requires `0x844 != 0`, flips `0x6045`, and displays one of two local notification messages (`0x6087` vs `0x6091`).
`jassica16` status:
- No literal `jassica` string is present in the current string table, while `-laurie` is present as plain text.
- No literal `jassica` string is present in the normal runtime string table or the verified command-line parser, while `-laurie` is present as plain text.
- The live NE export/naming trail does preserve older user-defined symbol names for the matcher cells (`g_jassica16Scans` at `1020:2833` and `g_jassica16Offset` at `1020:283d`), which fits the long-standing community name even though the compiled matcher source bytes are not a plain ASCII string in this build.
- The matcher bytes themselves are now rechecked directly in the live NE image: `24 1e 1f 1f 17 2e 1e 02 07 00`, which is the scan-code sequence for `j a s s i c a 1 6`.
- That matters for runtime testing: the trailing `1` and `6` are the top-row digit scan codes (`0x02`, `0x07`), not numpad digits. If those keys are entered through a different scan-code-producing path, the matcher will not complete.
- The ordinary keyboard ISR producer still does not support the old byte-for-character model cleanly: it feeds normalized scan-code-style values into record byte `+1`, while the matcher source at `0x2833` is a live code-byte sequence with two values (`0x80`, `0xfd`) that do not fit that ISR path.
- The direct call `drawlist_init -> FUN_0007_04dc` is the first concrete static evidence for a higher-level path in this build.
- Observed runtime behavior now fits the toggle model cleanly: if `-laurie` has already forced `0x844 = 1`, triggering the hidden matcher again will toggle both `0x844` and `0x6045` back to `0`, which explains the user-observed "jassica16 disables cheats when laurie is active" behavior.
- Live data-use recovery tightens the latch story further: `0x6045` is written only by `Key_CheckCheatToggle` (`1130:2b72`) and the separate event-`0x7e` runtime toggle (`13e8:203d`). So if `jassica16` really completes and no later `0x7e` toggle fires, the low-level cheat latch should stay on.
F10 key behavior (verified in raw build):
- `seg001_input_keyboard_handler` at `0006:ec29` handles input byte `0x44` and immediately returns unless cheats are enabled through `0x6045`.
- "Plain F10 when cheats are enabled" is verified; "Ctrl+F10 enables god mode" is **not** supported by the current code path.
- "Plain F10 when cheats are enabled" is verified, and the immortality toggle is a separate Ctrl-gated F10 sub-branch.
- Live NE cross-check: `Key_HandleOptionKeys` at `1130:0896` / `0007:0a36` confirms the same gating and sharpens the keyboard result further. Once the low-level cheat latch `0x6045` is on, plain F10 runs the large restore/refill/loadout branch, while the modifier-gated F10 sub-branch directly toggles the current controlled NPC's immortal flag and posts the local `"Immortality enabled."` / `"Immortality disabled."` messages.
- The helper identity is now closed from the code too, not only from runtime behavior. `KeyboardGetExtendedShiftStates` (`11d0:39e6`) uses BIOS `INT 16h, AH=12h`, whose returned AH bits are `0=left Ctrl`, `1=left Alt`, `2=right Ctrl`, `3=right Alt`. The helper at `11c8:01a8` tests `0x0100 | 0x0400`, so it is really `KeyEvent_IsCtrlDown`; the helper at `11c8:018a` tests `0x0200 | 0x0800`, so it is really `KeyEvent_IsAltDown`. The older Alt/Ctrl labels were reversed.
- The remaining timing question is now closed by the upstream keyboard path too. The held-key repeat builder at `11b8:0129..022b` samples the current BIOS extended-shift state through `11d0:39e6`, stores that snapshot from `31a4` into each synthesized `KeyEvent` at `11b8:01d5`, and queues the 12-byte event through `11d0:3533`. `Keyboard_GetLastKeyEvent` (`11b8:0457`) later returns that exact snapshot, and `Controller_HandleKeyEvent` (`1130:2211`) copies it before calling `Key_HandleOptionKeys`.
- That engine-side repeat synthesis explains both observed quirks cleanly. Holding `F10` first causes the game to keep generating repeated F10 keydown-style events; once physical `Ctrl` is pressed, later repeated F10 events carry the refreshed modifier snapshot and can satisfy the immortality branch. The same lack of one-shot suppression is why holding the keys too long can flip immortality on and off repeatedly and spawn multiple enable/disable modals.
- The live NE string addresses for the F10 immortality messages are `1478:2850` = `"Immortality disabled."` and `1478:2866` = `"Immortality enabled."` The earlier `1478:6450/6466` note was incorrect.
- One more runtime gate is now explicit too: the F10 path first checks `DAT_1478_085f` at `1130:0a29`, then `0x6045` at `1130:0a36`.
- The `0x85f` state now has a tighter live model. It is set during `Game_Start` (`1020:0127`), cleared at the end of `ComputerGump_CreateGump` (`1398:01f5`) just before returning the modal gump, and restored in `ComputerGump_CloseAndResumeGameplay` (`1398:0233`) during the computer-gump cleanup path before falling into the base-gump teardown. Across the wider codebase it gates controller/joystick/camera and option-key handling, so the current safest read is **gameplay input / option-key active state**, not a cheat byte.
- That makes the paired `1398` helpers easier to read too: `ComputerGump_CreateGump` suspends normal gameplay input by clearing `0x85f`, and `ComputerGump_CloseAndResumeGameplay` appears to be the computer-gump close/teardown override that restores gameplay input, releases any pending text buffer at `+0x34/+0x36`, refreshes the UI/controller state, and then falls through the generic gump cleanup/free path.
- The hottest `0x85f` readers are clearer in the live NE database now. `Controller_HandleKeyEvent` (`1130:2334`) first checks the separate controller-enable latch at `1478:27cb`, then forwards into `Key_HandleOptionKeys`, and only after that rechecks `0x85f` before allowing ordinary gameplay key processing. The paired `13e8` wrappers around that flow are now renamed `Game_DisableGameplayInputAndRefreshCamera` (`13e8:0e7d`) and `Game_RestoreGameplayInputAndClearModalState` (`13e8:0ef9`): together they clear/restore `1478:27cb`, flip the overlay-suppression byte `1478:2c64`, toggle transient state `1478:8c53`, and preserve `1478:8c54` as a saved copy of `1478:2d24` while the modal camera/input transition is active.
- The Laurie side is narrower too. `Game_ShowLaurieHintComputerGump` (`13e8:0e31`) is the hidden computer-gump hint path that reaches the `FART ...TRY... -laurie (Have fun, Jely)` string, and `Game_ShowLaurieHintIfGameplayInputActive` (`13e8:0f4a`) is only a tiny `0x85f`-gated wrapper around it. So the Laurie hint path and the F10 immortality path are both suppressed by the same broader gameplay-input gate, even though they are otherwise separate features.
- The main camera pass in that same lane is now named `Camera_RedrawViewportAndGameplayOverlays` (`1180:19c1`). It brackets the viewport blit with two newly named overlay helpers, `GameplayOverlayWindows_DrawTracked` (`1188:010f`) and `GameplayOverlayWindows_ClearDirtyRects` (`1188:0394`), and it also uses `0x85f` to choose between the avatar-centered redraw rectangle and the wider modal/non-gameplay redraw path.
- The `0x85f` callers inside `World_HandleKeyboardInput_13e8_14b4` are now concrete enough to explain user-visible failures better. The same modal disable/restore pair wraps the exit-to-DOS confirmation lane (`0x22d`), quick save (`0x13f`), quick load (`0x13e`), restart/main-menu handling (`Game_RestartMaybe`), and the load/menu gump lanes around `0x142` / `0x143` / `0x13c`. If play is currently inside one of those modal transitions, the F10 immortality path is suppressed before the cheat latch is even consulted.
- The runtime cheat-latch override is also firmer now. Event `0x7e` inside `World_HandleKeyboardInput_13e8_14b4` is the only other recovered writer of `0x6045` besides `Key_CheckCheatToggle`, and it independently flips the keyboard cheat latch while only requiring the broader `0x844` permission gate. So a successful `jassica16` match can still be undone later by that separate `0x7e` path.
- `Key_CheckCheatToggle` itself is now comment-backed as an exact scan-code matcher: only keydown events participate, and the final `1` / `6` bytes in `g_jassica16Scans` are still top-row scan codes `0x02` / `0x07`. That keeps keypad `1` / `6` or any non-scan-code-equivalent input path as a live explanation for failed attempts.
### No Regret cross-check
- The currently opened `REGRET.EXE` uses the same overall F10 structure but not the same cheat-sequence semantics. Its `Key_HandleOptionKeys` lives at `1148:0a9a`, and the F10 branch at `1148:0d0e` first checks `1480:0adc`, then `1480:009b`, and then calls `11e0:01a8` before toggling the controlled NPC's immortal flag and displaying `1480:3052 = "Immortality disabled."` / `1480:3068 = "Immortality enabled."`.
- Live runtime testing tightened the practical input story further. In both games, the user was only able to trigger immortality by pressing `F10` first and then pressing `Ctrl` while continuing to hold `F10`; `F10` + `Alt` did nothing. The No Remorse helper swap is now code-proven from the BIOS bit layout. `REGRET.EXE` almost certainly follows the same convention in its parallel helper pair, but that target should be reopened and fixed directly before promoting the same renames there.
- The hidden key-sequence function is also different enough to matter. `1148:34d2` is now renamed `Key_CheckSecretCodeSequences`. Its first scan-code table at `1480:2ff0` is still `jassica16` (`24 1e 1f 1f 17 2e 1e 02 07 00`), but in No Regret that sequence triggers the `"Of course we changed the cheats..."` lane instead of the main cheat latch.
- The actual No Regret cheat-toggle sequence is the second table at `1480:2ffc`, which decodes as `loosecannon` plus top-row `1` / `6` tail scan codes (`26 18 18 1f 12 2e 1e 31 31 18 31 02 07 00`). Completing that sequence toggles `1480:0ac0` and mirrors the result into the low-level F10 latch `1480:009b`, then posts `"Cheats are now active."` at `1480:30be` or `"Cheats are now inactive."` at `1480:30d5`.
- The repeated modal behavior also now makes sense from the compiled path. The No Regret F10 branch has no one-shot debounce; it only requires a keydown-style event and the modifier helper to pass. So if `F10` is held long enough for key repeat, the branch can run again and again, toggling immortality on and off and spawning multiple enable/disable modals in succession.
- The strongest No Regret contrast is therefore no longer `Alt` versus `Ctrl`; it is that the latch-enabling secret code changed from `jassica16` to `loosecannon`, while the physical modifier gesture for the F10 immortality toggle behaves as `F10`-then-`Ctrl` in live play.
- The immortality sub-branch is also only reached for a live current NPC: after the `0x6045` gate, the code calls `NPC_IsDead` at `10e8:1fed`; the modifier-gated F10 path begins only on the zero/not-dead result.
- When a current `0x7e22` entity exists, the branch resolves the current selection and refreshes per-entity bookkeeping.
- In the `local_4 == 1` case the branch becomes a large restore/reset routine that tears down and rebuilds multiple linked objects around `0x7e22`, retries dispatch up to `0x14` times per stage, and fires the event batch `0x33d`, `0x33f`, `0x340`, `0x341`, `0x33e` before re-enabling channels `4`, `1`, and `0`.
What `-laurie` appears to enable by itself:
- It sets the broad cheat-permission flag `0x844` and shows the startup-side "Cheats are now active." notification, but it does **not** set the low-level keyboard cheat latch `0x6045`.
- That means `-laurie` is enough for the compiled event handlers gated only by `0x844` to operate, including the debug-overlay family (`0x441`, `0x241`, `0x141`) and the **CD transfer display** toggle handler (`0x410`).
- It is also enough to permit the separate event-`0x7e` runtime toggle path, whose entire job is to flip `0x6045` later and post the `0x6087` / `0x6091` notifications.
- It is **not** enough for low-level keyboard-only cheat branches that check `0x6045` directly. That is why the user can see the `0x844`-gated debug-box behavior while plain F10 still behaves as if full keyboard cheats are off.
### Cheat-related string table (seg014 / `000e:xxxx`)
| Address | String | Notes |
@ -292,12 +329,12 @@ F10 key behavior (verified in raw build):
| `000e:9c91` | `"CHEATS OFF"` | Cheat-off status string |
| `000e:9c9c` | `"TARGETING RETICLE ACTIVE."` | Correlates to event `0x441` / byte `0xee0` toggle |
| `000e:9cb6` | `"TARGETING RETICLE INACTIVE."` | Paired off-state |
| `000e:9cd2` | `"CD TRANSFER DISPLAY ACTIVE."` | Correlates to event `0x241` / `0x141` toggle area |
| `000e:9cd2` | `"CD TRANSFER DISPLAY ACTIVE."` | Directly matches live event `0x410` / `DS:0x604f` toggle |
| `000e:9cee` | `"CD TRANSFER DISPLAY INACTIVE."` | Paired off-state |
| `000e:9dff` | `"HACK MOVER ON"` | No static code xref; USECODE/scripting layer |
| `000e:9e0d` | `"HACK MOVER OFF"` | No static code xref; USECODE/scripting layer |
| `000e:6450` | `"Immortality disabled."` | No static code xref; USECODE/scripting layer |
| `000e:6466` | `"Immortality enabled."` | No static code xref; USECODE/scripting layer |
| `1478:2850` | `"Immortality disabled."` | Used by the modifier-gated F10 immortality branch in `Key_HandleOptionKeys` |
| `1478:2866` | `"Immortality enabled."` | Used by the modifier-gated F10 immortality branch in `Key_HandleOptionKeys` |
| `000e:647b` | `"Cheats are now active."` | Shown in `-laurie` startup path |
| `000e:6492` | `"Cheats are now inactive."` | Paired off-state |
@ -313,7 +350,7 @@ All cheat-related event case-handlers reside as shared-frame case bodies within
| `000c:942d` | `event_0x7e_cheat_latch_runtime_toggle` | `0x7e` | Requires `0x844 != 0`; flips live latch `DS:0x6045`; notification at `DS:0x6087` (on) or `DS:0x6091` (off) |
| `000c:9154` | `event_0x142_cheat_fullscreen_mode1_refresh` | `0x142` | Gate = `DS:0x604b`; palette-black, seg126 shell, mode-1 `000c:3c0e`, tail `0004:70f1` |
| `000c:92cd` | `event_0x143_cheat_fullscreen_mode0_refresh` | `0x143` | Same as `0x142` but mode-0 `000c:3c0e`, tail `0004:6f15` |
| `000c:9703` | `event_0x410_cheat_flag_604f_toggle` | `0x410` | Toggles `DS:0x604f` (boolean-NOT); notification at `DS:0x60d2` (on) or `DS:0x60ee` (off); gate = `DS:0x844` |
| `000c:9703` | `event_0x410_cd_transfer_display_toggle` | `0x410` | Toggles `DS:0x604f` / `g_cdTransferDisplayActive`; notification at `DS:0x60d2` (active) or `DS:0x60ee` (inactive); gate = `DS:0x844` |
### Cheat-dispatch keyboard functions (seg007)
@ -341,48 +378,36 @@ Verified byte tests in the caller-side dispatch:
- `0x39` and `0x52` share a branch computing a queued delta via `entity_command_dispatch`
- `0x4e` and `0x53` are separate guarded selected-object lanes dispatching through the selected object's method table
### Immortality mechanics (event 0x410 / flag 0x604f)
### F10 immortality vs Ctrl+Q CD transfer display
**How immortality works at the C level:**
The live NE decompile now separates these behaviors cleanly.
`DS:0x604f` is the Immortality flag. It is toggled by `event_0x410_cheat_flag_604f_toggle` at `000c:9703`.
The sole gameplay read site is `player_receive_damage_and_dispatch_effects` (`0004:c055`) at `0004:c205`.
1. **Direct keyboard immortality lane**: inside `Key_HandleOptionKeys` (`1130:0896` / live F10 branch at `1130:0a36`), once full keyboard cheats are active through `0x6045`, the modifier-gated F10 sub-branch toggles the current controlled NPC's immortal flag directly via `NPC_GetIsImmortal` / `NPC_SetImmortal` / `NPC_ClearImmortal`. Live runtime testing now says the practical gesture is hold `F10` first and then press physical `Ctrl`.
2. **Cheat-only CD transfer display lane**: event `0x410` reaches the compiled handler at `000c:9703` (`13e8:2303` live), which toggles `DS:0x604f` / `g_cdTransferDisplayActive` under the broader `0x844` gate and posts the strings at `1478:60d2` / `1478:60ee`:
- `"CD TRANSFER DISPLAY ACTIVE."`
- `"CD TRANSFER DISPLAY INACTIVE."`
When `0x604f != 0` (Immortality **ON**), the damage path in `0004:c205` does:
1. `CALLF 0009:9ea1` — begin hit-effect lock (animation gating sequence)
2. `CALLF 0003:c368(0x10001)` — arm anim-stagger mode (seg001:4d68 path)
3. `IDIV 0x40000` — divide the 32-bit incoming damage value by **262,144** → result is effectively 0 for any realistic HP scale
4. Apply the negligible reduced damage via `CALLF 0003:dbcc`
5. Spin on `DS:0x31a2 != 0` event-break gate before re-enabling channels
That means the older disasm scratch note in `crusader-disasm/misc_crusader_notes.txt` (`CTRL-Q = 0x410`) now lines up well with the user's runtime observation: **Ctrl+Q is the historical control-key note for the CD transfer display toggle, not for immortality**.
When `0x604f == 0` (Immortality **OFF**, normal path):
- Jump to `0004:c25b``CALLF 0003:ac7e` (seg001:367e) — full damage / death dispatch
The immortality status strings are separate. The live NE decompile plus disassembly of `Key_HandleOptionKeys` directly shows the modifier-gated F10 immortality branch using:
The hit stagger **still plays** in immortality mode (the Silencer visually flinches). Technically HP decreases by 0 per hit (integer truncation from /262144), so there is no true invulnerability flag that bypasses all HP accounting, just extreme attenuation.
- `"Immortality enabled."` at `1478:2866`
- `"Immortality disabled."` at `1478:2850`
**What sends event 0x410 to toggle it:**
The same compiled proof also sharpens the modifier claim beyond the earlier folklore-level read:
The 000c event handler at `000c:9703` is entered via the large cheat-event dispatch switch at `000c:8c56-000c:8d16`. That switch is driven by the seg021 event scheduler, not by the static keyboard dispatch in `keyboard_input_cheat_dispatch`.
- `1130:0afd` calls `KeyEvent_IsAltDown` inside the F10 cheat branch.
- The F10 branch then chooses between the `1478:2850` and `1478:2866` strings.
- There is no `KeyEvent_IsCtrlDown` test anywhere in that F10 branch.
- The `KeyEvent_IsCtrlDown` call at `1130:0cad` belongs to a later, unrelated branch in `Key_HandleOptionKeys`, not to F10.
Key negative result: no function in the compiled C code directly pushes the value `0x410` into the game's event broadcast path. All three occurrences of the immediate `0x410` in the disassembly are: (a) the `CMP BX,0x410` comparison inside the 000c switch, (b) a multi-event subscription list at `000b:b5cb` (registering to receive the event), and (c) an abort-function error code at `000d:5290` unrelated to the cheat.
Current strongest read:
The strongest new compiled-side recovery in this pass is the seg109 listener object behind that subscription site. `cheat_event_listener_create` at `000b:b3b1` allocates one listener object and registers the shared cheat/control event bundle (`0x13d`, `0x1b`, `0x443`, `0x142`, `0x141`, `0x143`, `0x23f`, `0x43e`, `0x41f`, `0x417`, `0x431`, `0x411`, `0x410`, `0x441`, `0x421`, `0x22d`) through the seg109 registration helper at `000b:3d2a`. Its paired `cheat_event_listener_handle_event` body at `000b:b62c` is subscriber-side only: for event `0x410` it rewrites the event object's field `+0x6` to local state `0x0e` and falls into the shared `FUN_000b_b7f3` state-processing tail. That listener does not produce event `0x410`; it only reacts after the event has already been emitted elsewhere.
The generic compiled dispatch path is one step tighter now too. The larger `000c:8a62` wrapper first peels off local gated cases, then falls into the generic cheat/control event dispatcher at `000c:8c56`, which reads `event_object->code` from field `+0x6` and switches over values like `0x141`, `0x142`, `0x143`, `0x23f`, `0x410`, `0x431`, `0x441`, and `0x443`. That makes the shared event-object contract explicit: `000c:8c56` consumes the original emitted event id from `+0x6`, while `cheat_event_listener_handle_event` reuses the same `+0x6` field as a local state/subcommand code before entering `FUN_000b_b7f3`.
One extraction-side false lead is now closed too: the `TELEPAD` row in `USECODE/EUSECODE_extracted/class_event_index.tsv` with `raw_code_offset = 0x00000410` is a class-body offset for slot `0x20`, not direct evidence that `TELEPAD` emits gameplay event `0x410`.
The requested USECODE family sweep also tightened the player-trigger side without closing it. Inside `class_event_index.tsv`, `NPCTRIG` is the only requested family that is both explicitly event-bearing at the descriptor level and also has non-empty callable bodies in the current event-slot extraction (`equip` / slot `0x0a` at raw offset `0x0175`, plus one anonymous slot `0x20` body at raw offset `0x0159`). `SPECIAL`, `TRIGPAD`, and `REB_PAD` all have non-empty callable bodies too, but they remain referent/state neighbors rather than direct event carriers: `SPECIAL` shows bodies for `equip`, `enterFastArea`, `leaveFastArea`, and anonymous slots `0x20/0x21`; `TRIGPAD` shows `gotHit`; `REB_PAD` shows `gotHit` and anonymous slots `0x20/0x21`. None of those extracted bodies currently expose a verified `0x410` immediate or decoded event payload.
Conclusion: event 0x410 is generated exclusively by the **interpreted USECODE lane** (centered on `EUSECODE.FLX`), not by any static keyboard-level scan-code path in the compiled binary. The F10 keyboard branch in `seg001_input_keyboard_handler` is a separate `0x44` path gated by `0x6045`, not by `0x410`. Separate follow-up work on the imported `ASYLUM.DLL` shows that DLL exports `ASS_*` audio routines, so it should not be conflated with the immortality toggle path. The in-game trigger is still best modeled as a USECODE item or controller script, consistent with the surrounding string evidence (`000e:6337 "CruHealer"`, `000e:6341 "BatteryCharger"`, `000e:6445 "Controller"`, `000e:64ab "AutoFirer"` — these are USECODE process class names bracketing the Immortality string). The new extractor-side report `USECODE/EUSECODE_extracted/immortality_target_body_scan.md` now scans the strongest current bodies in `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `SPECIAL`, and `TRIGPAD` and finds no inline little-endian `0x0410`, no dword `0x00000410`, and no byte-swapped `0x1004` in any of them. That closes the immediate-emitter hypothesis for those currently exposed bodies and narrows the remaining frontier to data-driven decoding of the monolithic `EVENT` slot `0x0a` body and the compact `NPCTRIG` slot `0x0a` / `0x20` bodies, not to `TRIGPAD`, `SPECIAL`, `REB_PAD`, or `TELEPAD`.
The next extractor pass now pushes that one layer deeper. `USECODE/EUSECODE_extracted/immortality_body_structure.md` shows that `EVENT` slot `0x0a` is structurally a wide generic hub body, not a compact trigger leaf: it carries `90` internal `0x53 0x5c <u16> EVENT` subheaders, `383` local `0x5b` labels, and one wide tail-field set covering `event`, `item`, `source`, `dest`, `door`, `counter`, `counter2`, `link`, `time`, `post1`, `post2`, `floor`, and `flicMan`. By contrast, `NPCTRIG` stays compact and trigger-shaped. Slot `0x0a` has only `5` class-labelled subheaders and a narrow tail-field set (`referent`, `event`, `item`, `item2`), while slot `0x20` has only `1` such subheader and swaps the tail `event` field for `typeNpc` while keeping the same compact `item` / `item2` neighborhood. That is the strongest current player-trigger result: `EVENT` now reads as the generic event hub body, while the likeliest player-facing path is the `NPCTRIG` pair with slot `0x0a` as the compact event-bearing trigger body and slot `0x20` as its nearby typed/setup companion.
The next focused decode pass sharpens that split enough to treat the two `NPCTRIG` bodies differently instead of as one unresolved pair. New report `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` fixes the open-header parse and shows that slot `0x0a` starts with `0x5A 0x06 0x5C 0x013E NPCTRIG ... 0x0B 0x11`, then falls into a five-step clause ladder with subheaders at `0x0064/0x0093/0x00c2/0x00f1/0x0120`. Those subheaders sit on a uniform `0x2f` stride, their targets walk backward by the same amount, and each full-width clause carries one `branch_3f_0a`, one `push_24_51`, and one `writeback_57_02`. Slot `0x20` is structurally different: its prolog ends with event-code byte `0x01`, it has only one class-labelled subheader, no `writeback_57_02`, no `push_24_51`, and ten `field_4b_fe_0f` hits clustered around repeated `0x0a 00/05 4b fe 0f ...` windows before the tail field `69:000a -> typeNpc`. That is the strongest current descriptor-side reduction of the search space: slot `0x0a` now reads like the live event-bearing clause ladder, while slot `0x20` reads more like a typed gate or setup/attachment companion body than like a second emitter.
The runtime-side bridge is tighter too. The binary already had one exact offset-specialized masked wrapper for slot `0x0a`, `entity_vm_context_try_create_mask_0400_slot0a_with_offset` at `0005:2c35`, and the `000d:21ed -> 000d:22bc` lane is still verified as a slot-backed inline-payload consumer that copies a variable-length byte stream first and then consumes compact metadata bytes plus streamed words. The new body-structure report is consistent with that runtime contract: the surviving `EVENT` / `NPCTRIG` bodies are clause streams with repeated internal subheaders and local labels, not flat literal blobs. That still does **not** prove that `NPCTRIG 0x0a` emits `0x410` directly, but it narrows the best remaining emitter frontier from `EVENT or NPCTRIG` down to `NPCTRIG slot 0x0a` with `NPCTRIG slot 0x20` as the strongest adjacent support body.
The clause report makes that runtime comparison more concrete too. `0005:2c35` is no longer just an abstract "with offset" wrapper: `entity_vm_slot_load_value_plus_offset` at `000d:5572` now proves the extra word is applied additively to the loaded slot value before `000d:21ed` consumes the result. The internal consumer at `000d:21ed -> 000d:22bc` is tighter as well: after copying the inline blob into the context it reads two signed metadata bytes, uses byte A as the lead-word row count, uses byte B as the shared target-list width, performs `A x B` `entity_link` calls, and pushes back only non-`0x0400` words. That makes `NPCTRIG 0x0a` the only surviving compact body with a natural selector family for this lane: it has `5` evenly spaced clause starts at stride `0x2f`, while slot `0x20` has only one clause and no matching writeback/push motif. So the best current working model is no longer "EVENT or NPCTRIG" or even "NPCTRIG 0x0a plus 0x20 as co-equal bodies"; it is specifically "NPCTRIG slot `0x0a` event-bearing clause ladder, with slot `0x20` as a typed companion/setup body feeding or constraining the same family."
- The recovered compiled path still reaches a single modifier helper from the F10 immortality branch rather than testing both modifier families in that branch.
- Live runtime behavior now says the practical gesture is `F10`-then-`Ctrl` once the `0x6045` cheat latch is active.
- `Ctrl+Q` / event `0x410` toggles the CD transfer display state instead.
- If `jassica16` has been entered and the F10 immortality gesture still appears dead in live play, the most likely compiled-code explanations are: the scan-code matcher never actually completed, the cheat latch was toggled back off, the broader gameplay-input state at `0x85f` is down because the game is in a modal/non-gameplay state, or the key never reaches the game at all.
- The most practical runtime confirmation point is the cheat notification: a successful matcher pass goes through `Key_CheckCheatToggle` and pushes `DS:0x287b` (`Cheats are now active.`) or `DS:0x2892` (`Cheats are now inactive.`). If that notification does not appear, the latch almost certainly never changed.
**Secondary handler (000b:b62c):**
@ -390,33 +415,100 @@ The clause report makes that runtime comparison more concrete too. `0005:2c35` i
| Address | Symbol | Role |
|-------------|-------------------------------|------|
| `0004:c055` | `player_receive_damage_and_dispatch_effects` | Renamed. Contains the `0x604f` immortality gate at `0004:c205`. |
| `000b:b3b1` | `cheat_event_listener_create` | Allocates one seg109 listener object and subscribes it to the shared cheat/control event bundle that includes `0x410`. |
| `000b:b62c` | `cheat_event_listener_handle_event` | Subscriber-side event mapper: rewrites incoming `0x410` to local state `0x0e` before entering the shared listener state machine. |
| `DS:0x604f` | Immortality flag | Set/cleared by event `0x410`. Read only at `0004:c205`. |
| `DS:0x60d2` | Immortality-on notification ptr | Near pointer in DS; resolves to far ptr → "Immortality enabled." display. |
| `DS:0x60ee` | Immortality-off notification ptr | Near pointer in DS; resolves to far ptr → "Immortality disabled." display. |
| `DS:0x604f` | `g_cdTransferDisplayActive` | Set/cleared by event `0x410`. |
| `DS:0x60d2` | CD-transfer-on notification ptr | Near pointer in DS; resolves to far ptr → "CD TRANSFER DISPLAY ACTIVE." |
| `DS:0x60ee` | CD-transfer-off notification ptr | Near pointer in DS; resolves to far ptr → "CD TRANSFER DISPLAY INACTIVE." |
| `000a:b988` | `video_bios_state_snapshot` | Called after notification display in the 0x410 toggle to refresh screen state. |
### Hidden cheat menu investigation (seg109 UI lane)
New compiled-side evidence shows a real but likely dormant cheat-menu UI path:
New compiled-side evidence shows that the old "hidden cheat menu" label is misleading. The seg109 UI path is much closer to a hidden **usecode debugger / unit inspector** than to a retail cheat list:
| Address | Symbol | Role |
|-------------|-------------------------------------|------|
| `000b:9a86` | `cheat_menu_open_from_current_slot` | Builds a `cheat_event_listener` object, preloads selection from current slot state (`0x659c/0x659e`), pushes it through the sprite tree, and runs a modal draw/update loop. |
| `000b:9c0d` | `cheat_menu_open_modal` | Smaller modal wrapper that directly constructs `cheat_event_listener_create(...)`, traverses it, and returns. |
| `000b:b3b1` | `cheat_event_listener_create` | Constructor for the listener object. Registers event bundle including `0x23f`, `0x410`, `0x411`, `0x441`, etc. |
| `000b:b62c` | `cheat_event_listener_handle_event` | Listener event mapper; event `0x23f` toggles armed/visible state byte `+0x47`; event `0x410` remaps to local state `0x0e` then enters `FUN_000b_b7f3`. |
| `000b:9a86` | `usecode_debugger_open_for_current_unit` | Builds the seg109 debugger gump in current-unit mode, derives a usecode path from the live runtime context (`0x659c/0x659e`), loads that file, centers the current line, and then runs a modal UI loop. |
| `000b:9c0d` | `usecode_debugger_open_modal` | Smaller generic modal wrapper that opens the same debugger UI without first preloading a current unit/file. |
| `000b:b3b1` | `usecode_debugger_gump_create` | Constructor for the debugger object. Builds the panes/menu bar, initializes watch state, resolves the base `usecode` path, and registers the shared control-event bundle including `0x23f`, `0x410`, `0x411`, and `0x441`. |
| `000b:b62c` | `usecode_debugger_handle_event` | Debugger event mapper; recovered cases are debugger-style commands (open unit/file, go to line, watch, inspect, clear watches, change global, find/search again, break to debugger). Event `0x23f` is reused as a local debugger-state command; event `0x410` remaps to local state `0x0e` before entering the shared tail. |
| `000b:2882` | `usecode_debugger_build_menubar` | Builds the top-level debugger menus: File, Run, Breakpoints, Search, and Data. Recovered entries include `Open Unit`, `View File`, `Run to cursor`, `Trace into`, `Step over`, `Run until return`, `Toggle F2`, `Break to TDP`, `Find`, `Search again`, `Go to line`, `Watch`, `Inspect`, `Change Global`, and `Quit`. |
This better explains the long-running negative result about the "infamous scrollable cheat menu": the hidden UI we can actually recover is not a plain scrollable cheat list at all. It is a modal debugger/unit-inspector front-end that expects valid usecode file context and developer-style command routing.
#### Reachability status in retail binary
- Static constructor callsites for `cheat_event_listener_create` are exactly two locations: `000b:9a9b` and `000b:9c56`.
- Static constructor callsites for `usecode_debugger_gump_create` are exactly two locations: `000b:9a9b` and `000b:9c56`.
- Static inbound xrefs to the wrapper entries `000b:9a86` and `000b:9c0d` are currently empty in the recovered code graph.
- The cheat-code matcher `cheat_code_check` (`0007:0d0a`) toggles `0x844/0x6045` and emits event `0x103`; it does **not** call these menu wrappers directly.
- The 000c handler for `0x103` (`000c:99dd`) executes a status/refresh lane and notification path; no direct call to `cheat_event_listener_create` appears there.
- The 000c handler for `0x103` (`000c:99dd`) executes a status/refresh lane and notification path; no direct call to `usecode_debugger_gump_create` appears there.
Current best read: this menu path is compiled and functional at object level, but likely orphaned/hidden in final gameplay flow (possibly debug/dev-only trigger removed, or only reachable through non-recovered data-driven callback wiring).
Current best read: this debugger path is compiled and functional at object level, but likely orphaned/hidden in final gameplay flow (possibly dev-only entry removed, or only reachable through non-recovered data-driven callback wiring). That orphaned status is a better fit for the missing retail cheat menu than assuming a still-live player-facing scrollable cheat list.
#### Breakpoint callback lane (new strongest orphan candidate)
The next live NE pass shifts the strongest likely entry point away from the cheat-toggle helper and toward a surviving **usecode breakpoint callback lane** in seg1408/seg1418.
New live renames in this lane:
| Live NE | Name | Role |
|---------|------|------|
| `1408:0000` | `usecode_debugger_break_state_create` | Allocates and initializes the seg1408 debugger-state object: breakpoint table, current-entry stack, and run-mode flags. |
| `1408:0053` | `usecode_debugger_maybe_break_on_current_line` | Runtime breakpoint gate. Stores the current line into the debugger state, resolves the current unit/file name through `1408:0444`, checks the breakpoint table through `1408:029e`, and callbacks through the object's vtable when a break condition is met. |
| `1408:00dd` | `usecode_debugger_breakpoint_insert_sorted` | Inserts `(file,line)` breakpoint entries into the seg1408 table in sorted order. |
| `1408:029e` | `usecode_debugger_has_breakpoint` | Exact `(file,line)` membership test over the seg1408 breakpoint table. |
| `1408:03b0` | `usecode_debugger_callstack_push_entry` | Pushes one current-unit/current-line debugger entry into the seg1408 stack. |
| `1408:03f7` | `usecode_debugger_callstack_pop_entry` | Pops one debugger callstack/current-entry record. |
| `1408:0419` | `usecode_debugger_enable_single_step` | Arms the step/run-state flags that make the next interpreter lane callback eligible. |
| `1408:0432` | `usecode_debugger_clear_step_state` | Clears the step/run flags. |
| `1408:0444` | `usecode_debugger_current_entry_get_unit_name` | Returns the active unit/file name pointer from the current debugger entry stack. |
This matters because `1418:04aa..04b5` is now comment-backed as a concrete interpreter-side handoff into `usecode_debugger_maybe_break_on_current_line`:
- it first checks whether `0x659c/0x659e` is non-null,
- then pushes the current interpreted line,
- then calls `1408:0053`.
That means the live binary still contains a generic **"break here if debugger state exists"** lane in the usecode interpreter.
Current best orphan model:
- `0x659c/0x659e` is not just passive current-unit metadata; it behaves like the far pointer to the seg1408 debugger-state object.
- The seg109 UI wrappers consume that same object shape naturally. `usecode_debugger_open_for_current_unit` (`13a0:0086`) is especially consistent with this model because it expects a live current-unit state, resolves the active unit filename, loads the corresponding usecode file, centers on the current line, and then enters the modal UI.
- Direct static inbound xrefs to `13a0:0086` / `13a0:020d` can therefore stay empty even if the original debugger entry was real, because the missing handoff could have been **callback/vtable based** rather than a direct `CALLF` to the wrapper.
- The current negative result on `usecode_debugger_break_state_create` is important too: neither the live instruction search nor the repo relocation corpus currently shows a surviving retail constructor call for `1408:0000`. If that object is never instantiated and stored into `0x659c/0x659e`, the interpreter breakpoint hook stays compiled but dormant, which is exactly the orphan pattern now seen in the retail binary.
This moves the strongest likely original entry point from "cheat code success calls the menu directly" to **"a debugger-state object at `0x659c/0x659e` used the seg1408 breakpoint callback path to reach the seg109 current-unit debugger UI, but the retail build no longer instantiates or wires that object"**.
Practical force-enable paths now split more cleanly:
1. **Closest to the apparent original design:** instantiate `usecode_debugger_break_state_create`, store the far pointer into `0x659c/0x659e`, and ensure its callback/vtable target opens `13a0:0086` or `13a0:020d`. Then let the existing interpreter callback at `1418:04b5` trip normally.
2. **Executable patch near the surviving hook:** patch the seg1408 callback target or the `1418:04b5` breakpoint handoff so a valid debugger-state object enters the seg109 UI when a line/step condition fires.
3. **Blunt modal force-open:** keep using the already-documented cheat/event retarget experiments (`1130:2b78`, `13e8:25e0`) when the goal is only to prove UI reachability, not to reconstruct the original control flow.
#### Usecode-script viability as an alternative entry path
Cross-referencing the live NE work, the `crusader-disasm` usecode listings, and the ScummVM Crusader intrinsic table now gives a more precise answer to "can usecode do this?": **partially, but probably not directly enough to replace an EXE-side debugger-state fix**.
What the current evidence says:
- Crusader usecode clearly can open several normal modal UI paths. The ScummVM Remorse intrinsic table exposes `CruStatusGump::I_showStatusGump` (`Intrinsic05F`), `KeypadGump::I_showKeypad` (`Intrinsic0C4`), `ComputerGump::I_readComputer` (`Intrinsic0FE`), and `WeaselGump::I_showWeaselGump` (`Intrinsic134`). So a script hack that opens an ordinary gump is completely plausible.
- The same table does **not** expose anything that reads like "open usecode debugger", "construct debugger state", or "register breakpoint callback". That matters because the strongest compiled-side entry model now depends on the missing seg1408 debugger-state object at `0x659c/0x659e`, not just on some generic modal UI primitive.
- The compiled-side breakpoint lane still expects a live debugger object. `1418:04aa..04b5` only reaches `usecode_debugger_maybe_break_on_current_line` when `0x659c/0x659e` is already non-null, and the retail binary still has no recovered constructor path for `usecode_debugger_break_state_create`. Nothing in the current usecode evidence shows a script-visible way to instantiate that object or store it into the required global far pointer.
- The script/event frontier remains data-driven rather than explicit. Extracted usecode still points to `EVENT`, `_BOOT`, and especially `NPCTRIG` as the strongest active-event families, while `SURCAMNS` / `SURCAMEW` remain callback-style `eventTrigger` holders rather than proven active-event cores. The direct body scan also still finds no inline `0x0410` / `0x00000410` literal in `EVENT`, `NPCTRIG`, `SPECIAL`, or `TRIGPAD`, so the existing `Ctrl+Q` / `0x410` lane does not currently look like a plain script literal we can just drop into a chest body.
Accessible object candidates do still matter, but they split into better and worse testbeds:
- `MONITNS` / computer-adjacent objects are the best script-side probe. `crusader-disasm` places the first monitor at item `9254`, shape `258`, coordinates `(60798,59518,24)`, and its `MONITNS::use()` body is live. That family is already adjacent to normal computer/camera UI behavior, which makes it a better fit for testing whether usecode can be coerced into a hidden developer-facing UI transition.
- `SURCAMNS` / `SURCAMEW` are also stronger than a chest for experimentation. Their usecode bodies already move the camera and spawn follow-up ordinals, and descriptor-side work shows explicit `eventTrigger` fields plus repeated callback-oriented bodies. Even so, the current extractor evidence still treats them as callback holders, not as proven direct emitters of the seg109 debugger/control bundle.
- `NPCTRIG` remains the strongest compact event-bearing family if the real route is "usecode event machinery eventually reaches a hidden control path." Its slot `0x0A` body is the best surviving active-event frontier, but current binary work still bottoms out at "decoded VM workspace / caller stream" rather than a direct, script-readable `open debugger` operation.
- `CHEST_EW` is a weak candidate if the goal is specifically to reach the hidden debugger. The first chest is easy to reach, but its `CHEST_EW::use()` body mostly does chest animation, audio, waits, and a `FREE::ordinal2D` object-creation path. That makes it fine as a general proof-of-hack host, but not an evidence-backed match for the debugger/control lane.
Current best practical answer:
- **Viable for experimentation:** yes. A usecode mod can almost certainly be attached to an accessible early object, and a monitor/computer-style object is a better host than a chest.
- **Viable as a clean direct debugger-launch substitute for EXE patching:** not yet. The strongest known hidden-debugger entry still depends on missing compiled-side debugger state, and current usecode research has not surfaced a script-visible primitive that recreates that state or calls the seg109 debugger wrappers directly.
- **Most defensible usecode-first experiment:** hijack an early monitor / computer-adjacent use handler (`MONITNS` first, `SURCAM*` second) and try to route it into an already-existing modal/UI-bearing engine path, while treating `NPCTRIG` / `EVENT` as the deeper data-driven frontier if the real control path turns out to be event-mediated.
#### Retail patch-targeting trail
@ -428,21 +520,61 @@ Verified retail anchor points:
|----------|--------|---------|-------|
| `0x70d75` | `0007:0d75` | cheat matcher emits event `0x103` | retail bytes = `68 03 01 9A FF FF 00 00 83 C4 02`; NE fixup source = `0007:0d79` -> `seg092:0476` |
| `0x71d68` | fixup entry for `0007:0d79` | seg039 relocation record | exact retail entry: addr_type `0x03`, rel_type `0x00`, chain_off `0x2b79`, target `seg092:0476` |
| `0xc99dd` | `000c:99dd` | later controller-side handler that also executes `push 0x103 / call 000a:5276` | retail fixup source = `000c:99e1` -> `seg092:0476`; this is the first materially safer deferred hook candidate after the direct matcher path failed |
| `0xc99dd` | `000c:99dd` | later controller-side handler that also executes `push 0x103 / call 000a:5276` | retail fixup source = `000c:99e0` -> `seg092:0476`; this is the first materially safer deferred hook candidate after the direct matcher path failed |
| `0xb9a8d` | `000b:9a8d` | arg setup inside `cheat_menu_open_from_current_slot` | original wrapper uses caller stack words `[BP+8]` and `[BP+6]` plus local armed flag `1` |
| `0xb9c48` | `000b:9c48` | arg setup inside `cheat_menu_open_modal` | original wrapper still feeds caller stack words `[BP+8]` and `[BP+6]` into `cheat_event_listener_create`, but starts with local byte `+0x47 = 0` |
| `0xb9c48` | `000b:9c48` | modal wrapper prologue; the inherited caller-word patch subsite is `000b:9c4e` / live `13a0:024a` | original wrapper still feeds caller stack words `[BP+8]` and `[BP+6]` into `cheat_event_listener_create`, but starts with local defaults `-1`, `-1`, `0` |
#### Live NE `CRUSADER.EXE` mapping in Ghidra
The older file offsets and raw-style segment addresses remain useful provenance, but the patch should now be planned against the live NE program that is open in Ghidra.
The following locations are confirmed directly in the live `CRUSADER.EXE` listing:
| Live NE Ghidra | Raw/reference anchor | Meaning |
|----------------|----------------------|---------|
| `1130:2b75` | `0007:0d75` | `cheat_code_check` success lane: toggles `0x844/0x6045`, then emits event `0x103` via existing `CALLF 12d8:0476` at `1130:2b78` |
| `13a0:0086` | `000b:9a86` | `usecode_debugger_open_for_current_unit`; larger hidden debugger wrapper and current best direct retarget target |
| `13a0:008d` | `000b:9a8d` | current-slot constructor arg site: `PUSH 1`, `PUSH [BP+8]`, `PUSH [BP+6]`, `PUSH 0`, `PUSH 0`, `CALL 13a0:19b1` |
| `13a0:020d` | `000b:9c0d` | `usecode_debugger_open_modal`; smaller modal wrapper |
| `13a0:0244` | `000b:9c48` | modal wrapper prologue; inherited caller-word patch subsite is `13a0:024a` |
| `13a0:19b1` | `000b:b3b1` | `usecode_debugger_gump_create`; registers the shared debugger/control event bundle including `0x410` |
| `13a0:1df3` | `000b:b62c` | `usecode_debugger_handle_event`; debugger-side dispatcher that remaps incoming `0x410` to local state `0x0e` |
| `13e8:2303` | `000c:9703` | compiled CD transfer display toggle handler for event `0x410`; boolean-toggles `DS:0x604f` / `g_cdTransferDisplayActive` and posts the active/inactive notifications |
| `13e8:25dd` | `000c:99dd` | deferred controller-side `0x103` lane; the live call opcode begins at `13e8:25e0` and prior `0x42f` retarget tests hit the retail `FLEX.C` line 83 failure |
Provenance split:
- `crusader-disasm` and the older retail-offset patch notes were used only to recover candidate lanes and preserve file-format history.
- The target selection above is confirmed from the live NE `CRUSADER.EXE` disassembly and comments now stored in Ghidra itself.
Live cheat-data anchors now comment-backed in Ghidra:
| Live NE data | Meaning |
|--------------|---------|
| `1020:2833` | 5-byte cheat matcher table consumed by `cheat_code_check` |
| `1020:283d` | cheat matcher index/state byte advanced during sequence validation |
| `1020:0844` | `cheats_enabled` gate byte checked before event `0x410` can toggle the CD transfer display |
| `1020:6045` | status/mirror byte updated alongside `1020:0844` when the cheat matcher succeeds |
| `1020:604f` | `g_cdTransferDisplayActive`; toggled by the compiled `0x410` handler |
| `1020:6050` | secondary cheat-related state from the older activation lane; distinct from the `0x410` CD transfer display toggle |
One remaining function-hygiene caveat:
- The live `0x410` handler body at `13e8:2303` is comment-backed and behaviorally clear, but it still sits inside the oversized `World_HandleKeyboardInput_13e8_14b4` function object in the current NE database. That is why this batch documents the handler in place instead of forcing a boundary repair just to land a new function name.
What failed and why:
- Direct retarget of `0007:0d79` to `000b:9a86` crashed at startup when the NE relocation table was patched incorrectly as a raw far pointer. That was a file-format problem, not a semantic proof.
- After the patcher was made NE-fixup-aware, direct retarget to `000b:9a86` no longer broke startup, but the game hung when the cheat actually fired. Disassembly shows why: `cheat_menu_open_from_current_slot` consumes caller-supplied words at `[BP+8]` and `[BP+6]`, so the cheat matcher context is the wrong stack shape.
- Retargeting the same early cheat-matcher call to `000b:9c0d` got farther: the mouse pointer appeared, proving the hidden menu/display path was being entered. But it still hung with looping music, which points to **timing/context**, not a bad target address. The modal path appears unsafe when entered directly from the keyboard matcher even after the constructor args are forced to zero.
- The narrower direct current-slot patch was then runtime-tested on `/Writable/CRUSADER-PATCHED.EXE` with bytes verified as `1130:2b78 = 9A 86 00 A0 13` and `13a0:008d = 6A 01 6A 00 6A 00 90 90`. User test result: the normal cheat-toggle path still appeared, but no hidden menu appeared. That closes the direct current-slot route as a practical candidate, not just a theoretical one.
Current best patch rationale:
- `0007:0d75` is still the right place to intercept the cheat sequence itself because it is the verified success emission site.
- `000c:99dd` is the better candidate for the **actual menu-open call** because it is a later controller/event context, not the raw keyboard matcher frame.
- `000b:9c48` is the right argument-fix companion because it is the constructor-argument site for `cheat_menu_open_modal`, and the direct disassembly shows that this is where the wrapper still pulls caller-dependent words.
- `0007:0d75` remains the verified cheat-sequence success site, but the direct `1130:2b78 -> 13a0:0086` retarget is no longer the best live patch because it has now failed both analytically and at runtime.
- The first materially safer deferred hook remains the controller-side `000c:99dd` lane, where the live call opcode begins at `13e8:25e0`. That path preserves the real `0x103` event context instead of substituting `0x42f`, which is the strongest evidence-backed difference from the rejected deferred experiment.
- The chosen writable patch therefore restores `1130:2b78` to `CALLF 12d8:0476`, restores `13a0:008d` to the original current-slot wrapper bytes, retargets `13e8:25e0` to `13a0:020d` (`cheat_menu_open_modal`), and zeros only the inherited caller-word pushes at `13a0:024a` while preserving the modal wrapper's leading local defaults (`PUSH -1`, `PUSH -1`, `PUSH 0`).
- The deferred `0x42f` branch remains negative evidence only: it proved the modal wrapper can enter the hidden UI path, but it also proved that substituting the event id or landing in the wrong deferred context trips the retail `FLEX.C` failure.
Rejected follow-up patch design:
@ -456,10 +588,15 @@ Observed result on retail test build:
- But the game then halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit/teardown path (`"No pity. No mercy. No remorse."`).
- That is strong evidence that event `0x42f` is the wrong deferred hook context for this experiment even though the retargeted address itself was valid enough to enter the UI path.
Current patch candidate under test:
Current Ghidra-side patch plan for a copy:
- Site 1: keep the original `0007:0d75` bytes and retarget only its existing far-call fixup from `seg092:0476` to `000b:9a86` (`cheat_menu_open_from_current_slot`).
- Site 2: patch `000b:9a8d` from `6A 01 FF 76 08 FF 76 06` to `6A 01 6A 00 6A 00 90 90`.
1. Open the writable `/Writable/CRUSADER-PATCHED.EXE` program in Ghidra or PyGhidra, not the raw full-EXE database.
2. Restore the disproven direct-hook sites: `1130:2b78` back to `9A 76 04 D8 12` (`CALLF 12d8:0476`) and `13a0:008d` back to `6A 01 FF 76 08 FF 76 06`.
3. Navigate to the later controller-side `0x103` lane at `13e8:25e0` and retarget that `CALLF 12d8:0476` operand to `13a0:020d` (`cheat_menu_open_modal`), yielding bytes `9A 0D 02 A0 13`.
4. Navigate to `13a0:024a` inside `cheat_menu_open_modal`. Replace only the inherited caller-frame pushes with `PUSH 0` / `PUSH 0` (`6A 00 6A 00 90 90`) and leave the leading `PUSH -1`, `PUSH -1`, `PUSH 0` defaults intact.
5. Do not reintroduce the `0x42f` substitution or the direct `13a0:0086` current-slot hook in the same test build. They are now negative evidence, not live candidates.
These edits are now applied and byte-verified on `/Writable/CRUSADER-PATCHED.EXE`. The live NE `CRUSADER.EXE` analysis database remains documentation-only for this batch.
Rationale for the revised wrapper patch:
@ -478,10 +615,14 @@ Risk notes:
- "Cheats can be enabled with `-laurie`" is **directly verified**.
- "There is a hidden five-byte matcher that toggles cheats" is **directly verified**.
- "F10 performs a large cheat-only restore/reset action" is **directly verified**.
- "Ctrl+F10 enables god mode" is **not supported** — the verified F10 branch does not require a modifier.
- The current live NE decompile alone does not cleanly settle the physical Ctrl-vs-Alt labeling without the runtime correction.
- "The F10 immortality branch directly toggles the current controlled NPC's immortal flag once full keyboard cheats are active" is now **directly verified** in `Key_HandleOptionKeys`.
- Live runtime testing now says the practical physical input is `F10`-then-`Ctrl`, so the current helper naming should not be treated as definitive physical-key proof on its own.
- "Ctrl+Q shows `CD TRANSFER DISPLAY ACTIVE.` when cheats are enabled" now matches the live NE `0x410` handler and the historical `crusader-disasm` control-key note.
- "H enables hack mover" is **real at runtime** (strings confirmed), but not found in the static low-level byte dispatch; the activation comes from the USECODE scripting layer.
- "Immortality makes the player invincible" is **partially verified**: damage is divided by 262,144, making HP loss negligible; the hit stagger still plays. There is no bypass of the HP system entirely.
- "Immortality is toggled with a keyboard combo" is **not supported in compiled C code**: event 0x410 has no static keyboard dispatch path. It is USECODE-triggered.
- "Event 0x410 is emitted by a recovered static keyboard path" is still **not supported** in compiled C code.
- "There is no keyboard immortality combo at all" is now **false**: the live NE controller option-key handler directly verifies a modifier-gated F10 keyboard immortality toggle once the `0x6045` cheat latch is active, and live runtime testing shows the practical gesture is `F10`-then-`Ctrl`.
- `TELEPAD` slot `0x20` in `class_event_index.tsv` is **not** direct `0x410` event evidence; its `0x00000410` value is the extracted class-body offset for that slot.
- Among the requested USECODE families, `NPCTRIG` is the strongest remaining player-trigger candidate because it is explicitly event-bearing and also has extracted callable bodies, while `TRIGPAD`, `SPECIAL`, and `REB_PAD` currently read as neighboring referent/state/controller bodies rather than direct event carriers.
- The hidden five-byte matcher compares bytes from live code at `0007:2833`, and the ordinary keyboard ISR producer does not naturally emit byte values `0x80` and `0xfd` into record byte `+1`.