Crusader_Decomp/docs/ne-segment1.md

707 lines
74 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Crusader: No Remorse — NE Segment 1 Game Logic
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 |
|----------|---------------------------|-------------|
| `0x0060` | `cursor_update_hover` | Hover update: if mouse active & entity set, calls cursor_set_target |
| `0x00e9` | `cursor_set_target` | Positions cursor on entity, updates sprite + direction visual |
| `0x0322` | `cursor_shutdown` | Frees cursor resources, resets state |
| `0x0398` | `cursor_animation_update` | Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6 |
| `0x050f` | `cursor_draw_tick` | Per-frame cursor draw (calls cursor_animation_update if dirty) |
| `0x0c24` | `action_key_valid` | Returns 1 if action code (param_1) is a valid game action key |
| `0x0d5f` | `cursor_direction_input` | Arrow-key input: rotates cursor angle, updates direction sprite |
## Input Handling
| Address | Name | Description |
|----------|-------------------------|-------------|
| `0x0526` | `input_keyboard_handler`| Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load |
## Cursor State Data (at DS:0x27xx)
| Address | Field | Meaning |
|---------|-------|---------|
| `0x27c4` | cursor_sel1 | Selection counter 1 |
| `0x27c6` | cursor_sel2 | Selection counter 2 |
| `0x27c8` | current_entity | Handle to currently targeted entity |
| `0x27ca0x27ce` | cursor_state | Cursor interaction state bytes |
| `0x27d0` | cursor_entity_type | Current entity type index |
| `0x27d2` | z_offset | Z-height offset for terrain adjustment |
| `0x27d4` | cursor_angle | Rotation angle (0359) |
| `0x27d6` | cursor_sprite | Sprite handle for cursor visual |
| `0x27d8` | cursor_dirty | Set when cursor needs redraw |
| `0x27d9` | cursor_active | Master cursor enabled flag |
| `0x27da` | cursor_no_turn | Flag disabling cursor rotation |
| `0x27ed` | difficulty | Enemy accuracy divisor (used in projectile_init_vector) |
| `0x27fd` | hard_mode | Two-step mode (combat vs. explore) |
| `0x27fe` | move_mode | Movement phase flag |
| `0x27ff` | mouse_active | Mouse/input system active |
| `0x2800``0x2811` | various | UI state: active sprite, facing byte, cur entity handle |
| `0x283f`/`0x2841` | menu_obj_ptr | Active menu/dialog object far pointer |
| `0x2844` | in_save | In-progress save game flag |
| `0x290e` | entity_count | Number of active entities |
| `0x2910``0x2947` | snap_type_ids[10] | Entity types that snap-to-ground in snap_entity_to_ground |
## Input / Action Dispatch
| Address | Name | Description |
|----------|---------------------------|-------------|
| `0x2420` | `entity_command_dispatch` | Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce |
| `0x279a` | `cheat_code_check` | Checks input record byte+1 against five bytes starting at `0x2833`; toggles `0x844`/`0x6045`, emits helper/event code `0x103` |
## Menu / Event Callbacks
| Address | Name | Description |
|----------|---------------------------|-------------|
| `0x2e53` | `cursor_event_notify_a` | Vtable thunk: forwards event to 0x27ca area handler |
| `0x2e96` | `cursor_event_notify_b` | Vtable thunk: forwards event to 0x27ca area handler (alt path) |
| `0x2ed9` | `menu_event_notify_a` | Vtable thunk: forwards event to 0x2843 (near menu object) |
| `0x2f0c` | `menu_event_notify_b` | Vtable thunk: forwards event to 0x2843 (alt path) |
| `0x2ff3` | `stub_noop_2ff3` | Empty stub, noop |
| `0x2ff8` | `entity_collision_callback_a` | Calls touch handler then func(entity+0x1e, seg, 2); opt: extra func if param_3&1 |
| `0x3046` | `set_active_menu` | Writes param_1/param_2 to 0x283f/0x2841 (active menu far pointer) |
| `0x3058` | `entity_collision_callback_b` | Same as entity_collision_callback_a (second vtable entry) |
## Entity System (0x24010x5a50)
| Address | Name | Description |
|----------|------------------------------|-------------|
| `0x2401` | `clear_cursor_selection` | Zeros 0x27c4/0x27c6 (selection counters) |
| `0x2899` | `cursor_switch_target_entity`| Switches cursor target: unloads old entity, loads new, re-registers |
| `0x29d8` | `get_z_offset` | Returns func() + *(0x27d2) = adjusted Z/height |
| `0x2a09` | `is_player_in_range` | Checks if entity is at player (0x2de4) X/Y +/-0xf0 range |
| `0x2a46` | `entity_ai_update_loop` | Loops entities 2255, checks visibility, triggers fire/move |
| `0x2c36` | `ui_update_callback` | Calls cursor_state_clear then vtable[2] on menu object |
| `0x2c6b` | `cursor_state_clear` | Clears cursor state bytes 0x27ca0x27ce, clears entity flag bit1 |
| `0x2c92` | `dialog_spawn` | Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca |
| `0x2d47` | `entity_pick_handler` | Handles entity selection or save-game trigger (type 0x38d) |
| `0x2df9` | `clear_active_menu` | Zeros 0x283f/0x2841 (active menu far pointer) |
| `0x2e18` | `game_mode_init` | Initializes game mode state, resets sprite/cursor/menu state |
| `0x2f3f` | `entity_table_set_sprite` | Reads 0x7df9+slot*2; writes entity type table 0x7e1e[slot*0x79+0x0d]=param_2, +0x10=0 |
| `0x3c97` | `snap_entity_to_ground` | If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY |
| `0x3d6e` | `spawn_entity_checked` | Spawns entity with explosion pool limit check (0x84c0, 0x84c2) |
| `0x3f2f` | `entity_spawn` | Allocates entity, vtable=0x29aa/0x39ca, positions it |
| `0x40d4` | `entity_remove` | Removes entity: destroys sprites, clears 0x2802/0x2804 if needed |
| `0x4172` | `entity_animation_frame_update`| Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality |
| `0x42f8` | `stub_noop_42f8` | Empty stub, noop |
| `0x42fd` | `entity_registry_decrement` | Calls cleanup func then decrements entity count at 0x290e |
| `0x4314` | `entity_sprite_move_delta` | Updates shot sprite handle (entity+0x3f) position by adding delta params |
| `0x4552` | `entity_set_position` | Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51) |
| `0x452b` | `shot_set_spawn_pos` | Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field) |
| `0x4591` | `entity_try_place` | entity_set_position with validation — position only set if placement succeeds |
| `0x5092` | `entity_deactivate` | Calls vtable[2] to deactivate, or finds in registry and removes |
| `0x5a50` | `entity_list_contains` | Checks if entity ptr exists in active entity list at 0x294c |
| `0x5b05` | `stub_noop_5b05` | Empty stub, noop |
## Entity Object Layout (NE Segment 1 entities)
| Offset | Field | Meaning |
|--------|-------|---------|
| `+0x00` | vtable_ptr | Vtable pointer (0x29aa for generic, 0x2a57 for debris) |
| `+0x02` | slot_index | Entity slot index (used for registry at 0x39ca) |
| `+0x04` | entity_type | Entity type ID |
| `+0x19`/`+0x1a` | flags | Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid) |
| `+0x1b` | vel_x | X velocity (clamped ±0x20) |
| `+0x1c` | vel_y | Y velocity (clamped ±0x20) |
| `+0x1d` | vel_z | Z velocity (clamped ±0x10) |
| `+0x1e` | fire_handle | Weapon/fire handle |
| `+0x1f` | is_enemy | 1 if entity is an enemy type |
| `+0x20`/`+0x21` | pos_frac_x/y | Fractional position (sub-tile) for movement |
| `+0x22` | pos_frac_z | Fractional Z |
| `+0x36` | weapon_type | Active weapon type ID |
| `+0x38` | facing | Current facing direction (015) |
| `+0x3c` | sprite_handle | Sprite for this entity |
| `+0x3f` | shot_sprite | Sprite handle for active projectile (0xFFFF = none) |
| `+0x45`/`+0x47`/`+0x49` | world_x/y/z | Current world position (integer) |
| `+0x4f`/`+0x51`/`+0x53` | base_x/y/z | Base/spawn position |
| `+0x54`/`+0x56`/`+0x58` | prev_x/y/z | Previous frame position |
| `+0x59` | attack_active | Attack in progress flag |
| `+0x5a` | at_target | Reached target flag |
| `+0x5e``+0x65` | delta_x/y/z/high | Per-step movement deltas (fixed point) |
| `+0x66`/`+0x68` | step_active | Stepping active (1=yes, 0=off) |
| `+0x6a`/`+0x6c` | weapon_slot/dist | Weapon slot and total travel distance |
| `+0x6e` | delta_z | Alt Z delta |
| `+0x70` | projectile_type | Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain) |
| `+0x72`/`+0x74`/`+0x76` | target_x/y/z | Target position with deviation |
| `+0x77` | target_entity | Target entity handle |
| `+0x79` | secondary_pos | Secondary position struct pointer |
| `+0xad` | owner_entity | Owning entity handle |
| `+0xaf` | shot_owner_flags | Shot owner (entity/player) |
| `+0xb1` | bounce_count | Bounce counter (used with homing, type 5) |
| `+0xb3` | has_bounce | Has bounce trajectory active |
| `+0xbd` | actor_type | Actor type byte (used for direction table lookups) |
## Shot Entity Lifecycle (0x435e0x44a9)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x435e` | `shot_entity_alloc` | Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7 |
| `0x44a9` | `shot_entity_free` | Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1 |
## Projectile / Combat (0x46590x5a99)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x4659` | `projectile_init_vector` | Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536 |
| `0x4a91` | `entity_fire_weapon` | Fires weapon from entity using 0x129b/0x12ac direction offset tables |
| `0x4b18` | `fire_weapon_from_cursor` | Gets cursor angle sprites, fires projectile at cursor target |
| `0x4b78` | `projectile_check_hit` | Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity) |
| `0x4c2e` | `projectile_step_update` | Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked |
| `0x4d28` | `projectile_trace_ray` | Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate |
| `0x51ad` | `projectile_update_tick` | Full projectile tick: move, check reach target, bounce, call projectile_check_hit |
| `0x5a99` | `projectile_apply_hit` | Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner |
## Weapon Type Table (0x2536)
- Each entry is 0x11 bytes (17), accessed as `weapon_type * 0x11`
- `[0]` = step divisor for distance calculation
- `[0x19]` = max range threshold (used in projectile_update_tick)
## Direction Tables (0x129b / 0x12ac)
- Indexed by facing (015): dx offsets at 0x129b, dy offsets at 0x12ac
- Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets
## Collision Detection (0x60c10x621e)
| Address | Name | Description |
|----------|----------------|-------------|
| `0x60c1` | `aabb_overlaps_3d` | 3D AABB overlap test — box layout [xmin,ymin,zmin,_,_,xmax,_,ymax,_,zmax] |
| `0x621e` | `bbox_translate` | Translates a 3D bounding box by (dx, dy, dz) — both min and max points |
## Enemy AI / Spawning (0x6aed0x6d21)
| Address | Name | Description |
|----------|----------------------------|-------------|
| `0x6aed` | `map_find_spawn_point` | Finds map tile matching entity conditions; returns packed XYZ tile coords |
| `0x6bfc` | `actor_find_in_view` | Finds actor visible in current view frustum (temp data at 0x7eca) |
| `0x6ce9` | `enemy_spawn_with_target` | Wrapper: spawns enemy with player as target (param5=1) |
| `0x6d05` | `enemy_spawn_no_target` | Wrapper: spawns enemy without targeting player (param5=0) |
| `0x6d21` | `enemy_spawn_at_position` | Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A) |
## Player / HUD
| Address | Name | Description |
|----------|-------------------------------|-------------|
| `0x50ee` | `player_position_update` | Updates player position from direction data; clamps to screen bounds |
| `0x6ff7` | `player_health_update_and_effect` | Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect |
## Destruction / Death (0x74900x75ff)
| Address | Name | Description |
|----------|---------------|-------------|
| `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list |
| `0x75ff` | `entity_die` | Death handler: spawns 14 debris objects, picks best explosion direction |
## Entity Type Constants (weapon_type/entity class)
| Value | Entity Class |
|--------|--------------|
| `0x17` | Robot/mech type A |
| `0x18` | Robot/mech type B |
| `0x1` through `0x3c` | Various entity/weapon types |
| `0x3d` | Robot/mech type C |
| `0x3e` | Robot/mech type D |
| `0x2f5``0x2f7` | Special movement entity |
| `0x595`/`0x597` | Platform/elevator entities |
| `0x31c`/`0x322``0x327` | Explosive/effect entities |
| `0x38d` | Save game trigger entity |
| `0x426` | Spark/scatter sub-shot |
| `0x59a` | Player cursor/select indicator |
## Entity Data Table at 0x7e1e
- Stride: `0x79` bytes (121 bytes per entry)
- Indexed by entity type (integer) or entity slot
- `+0x59` offset = class-detail flags byte (`entity_class_get_flag8` returns bit `0x08`; other callers also clear bit `0x10` here during at-target facing updates)
- `+0x5a` offset = flags byte (bit4 = special entity flag, bit3 = armor/shield flag)
- `+0x68` = targeting flag
## Map / Resource Tables
| Address | Content |
|---------|---------|
| `0x2833` | Cheat code input sequence (null-terminated) |
| `0x283d` | Cheat sequence match position counter |
| `0x7ded` | Map X coordinate array (2 bytes per entry) |
| `0x7df1` | Map Y coordinate array (2 bytes per entry) |
| `0x7df5` | Map Z array (1 byte per entry) |
| `0x7df9` | Entity state array (2 bytes per slot) |
| `0x7e46` | Player state block far pointer |
| `0x7e1e` | Entity type table (stride 0x79) |
## Entity Vtable Index (NE Segment 1)
| Address | Entity Class |
|---------|-------------|
| `0x28b5` | Dialog/menu object vtable |
| `0x287b` | Cheat-spawned entity (cheat ON) vtable |
| `0x2892` | Cheat-spawned entity (cheat OFF) vtable |
| `0x2969` | Entity registry vtable (stored at 0x39ca+slot*4, not entity's own vtable) |
| `0x297e` | Shot/projectile entity vtable |
| `0x29aa` | Generic/AI entity vtable |
| `0x2a1a` | Corpse entity vtable (variant) |
| `0x2a33` | Actor/corpse entity vtable |
| `0x2a57` | Debris fragment entity vtable |
---
## Cheat System Analysis
### `cheat_code_check` internals
- Direct raw-EXE recovery: `cheat_code_check` in `CRUSADER-RAW.EXE` is `0007:0d0a-0007:0e08`.
- It has exactly one direct caller in this build: `FUN_0007_04dc` at `0007:0511`, which prepares a small local event record and then calls `cheat_code_check` before continuing normal input dispatch.
- `FUN_0007_04dc` itself is not only a low keyboard-queue consumer: `drawlist_init` at `0007:f654` calls it directly during higher-level setup, so the cheat-dispatch body can be reached from at least one non-ISR path in this build.
Variable and constant roles from the recovered body:
- `0x2833` is not a clean dedicated data object in this raw EXE — raw bytes and surrounding disassembly show that it lands in the middle of `entity_animation_frame_update` (`0007:26e2-0007:2867`). Starting on `PUSH AX; CMP byte ptr [0x27fd],0; ...` the first five bytes are `50 80 3e fd 27` followed by a `0x00`.
- `0x283d` is the current match index into that byte table. On a successful byte match it increments; on mismatch it resets to zero and immediately retries the current input byte against the first table byte (overlapping prefixes still work).
- `input_event_record[+1]` is the compared input/event token.
- `0x844` is the main cheat-enable flag. The success path toggles it by converting the current byte to a boolean and negating it (`0 -> 1`, non-zero -> `0`).
- `0x6045` is written with the same post-toggle value as `0x844` — a mirrored cheat-state latch.
- Constant `0x103` is pushed into the shared helper at `000a:5276` immediately after the toggle (emit the cheat-toggle side effect).
- `0x8c52` is forced to `1` on success before the side-effect path continues.
After a full table match, the code resets `0x283d` to zero, sets `0x8c52 = 1`, toggles `0x844` and `0x6045`, and calls the shared `0x103` helper. It then branches on the new cheat state: cheats-on uses `DS:0x287b`; cheats-off uses `DS:0x2892`.
### Cheat-enable sources
Two independent cheat-enable sources verified:
1. The hidden input matcher in `cheat_code_check` toggles `0x844` and `0x6045` after matching the five-byte event-code table at `0x2833`.
2. The command-line parser at `0004:635c-0004:63b8` recognizes the literal switch `-laurie` and directly sets `0x844 = 1`. This path does **not** write `0x6045`.
Current model for the two cheat bytes:
- `0x844` = master cheat-permitted / cheat-framework-enabled flag.
- `0x6045` = live cheat-active mirror latch used by low-level keyboard handling.
- The hidden five-byte matcher enables both at once, but `-laurie` only enables the master flag.
- 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 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, 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.
### Keyboard folklore correction pass (`CRUSADER.EXE` live NE)
- `~` is real in this build. In `World_HandleKeyboardInput_13e8_14b4`, the branch at `13e8:202d..203d` requires `0x844 != 0`, flips the live keyboard-cheat latch `0x6045`, and posts the paired on/off messages at `0x6087` / `0x6091`.
- `Ctrl+C` is **not** the current-location popup in this build. The actual location-display branch is `Ctrl+L` (`0x426`), which formats the avatar's X/Y/Z into the `1478:610c` string and displays it at `13e8:255e..25ac`.
- The third F7-family overlay is also real. The three cheat-gated overlay toggles live at `13e8:1a7c` (`0x141`), `13e8:1a50` (`0x241`), and `13e8:1a20` (`0x441`), writing `1478:2bca`, `1478:2bc9`, and `1478:0ee0` respectively before forcing a camera refresh through `[g_cameraProcess+0x2c]`.
- The unresolved online entry is therefore best corrected as `Ctrl+F7 exists but is cheat-gated`; the obviously bogus list item is `Ctrl+C`, which should be `Ctrl+L` for the coordinate popup.
### `~` versus `jassica16`
- These are **not** the same mechanism. `jassica16` is handled earlier in `Key_CheckCheatToggle` as a raw scan-code matcher over `1478:2833`, while `~` is handled later in `World_HandleKeyboardInput_13e8_14b4` as a translated logical key value (`0x7e`).
- That means Shift is probably normal behavior here, not a DOSBox-only artifact. On a standard US DOS keyboard layout, plain grave is `` ` `` (`0x60`) and Shift+grave is `~` (`0x7e`); the live handler we recovered is the `0x7e` branch, and no separate backtick/`0x60` hotkey path was recovered in this handler.
- DOSBox can still affect host-to-DOS layout mapping, but the recovered game logic is asking for the translated `~` character value, not the raw grave-key scancode. So on a US layout the expected in-game gesture is Shift+grave.
- `jassica16` is the stronger unlock path. On success it sets `0x8c52 = 1`, toggles both `0x844` and `0x6045`, emits the `0x103` voice/helper side effect, and shows the `1478:287b` / `1478:2892` cheat-state modals.
- Plain `~` is narrower. It only works after the broader `0x844` gate is already on, and then it flips just the live keyboard-cheat latch `0x6045` while showing the `0x6087` / `0x6091` on/off messages.
- Practically: `jassica16` can bootstrap cheats from a cold state because it toggles the master gate and the low-level latch together; `~` cannot bootstrap that state by itself because its own branch first requires `0x844 != 0`.
- The extra `0x8c52` write also means `jassica16` is not just a different UI for the same toggle. It leaves a second latch behind that later keyboard-side cheat/debug branches can test, while plain `~` does not touch that latch.
### What `Ctrl+F7` is supposed to show
- `Ctrl+F7` (`13e8:1a20` -> `1478:0ee0`) is not just a third generic grid. In the camera redraw path, `Camera_1180_15ef` treats `0x0ee0` differently from the simpler `0x2bca` grid flag.
- Plain `F7` (`1478:2bca`) draws a coarse `3 x 3` isometric world-cell grid around the camera by stepping `0x200` world units and drawing diamond tile boundaries.
- `Alt+F7` (`1478:2bc9`) calls `Snap_1058_0814`, which walks the snap-process egg list and draws per-entry diamonds from packed egg range data. That is closer to a snap/trigger coverage overlay than to a camera-aligned background grid.
- `Ctrl+F7` (`1478:0ee0`) walks `EggHatcherProcess` objects and calls `EggHatcher_1090_0921`, which draws diamond trigger ranges using `Egg_GetXRange`, `Egg_GetYRange`, and the shared diamond helper at `1180:1ce5`.
- So the intended visual for `Ctrl+F7` is an **egg / hatch trigger-range overlay**. It should highlight live egg-hatcher or monster-egg regions, not fill the screen with a regular map grid.
- That also explains why it can appear to do nothing. If the current map has no live `EggHatcherProcess` objects, or a non-monster-egg trigger has already hatched while normal gameplay input is active, the draw loop has nothing eligible to outline even though the toggle flag itself changed.
### Egg-Hatcher Runtime Notes
- The live NE runtime has a dedicated egg-hatcher process type: `EggHatcher_CreateProcess` builds process type `0x20f`, stores one target item number in the process, and names it `EggHatcher`.
- The corresponding runner `EggHatcherProcess_Run` reads the avatar footprint and compares it against the egg item's center, `Egg_GetXRange`, `Egg_GetYRange`, and a vertical window of about `+/- 0x30` Z units.
- For non-monster egg families, entering that range sets the process `ishatched` byte and calls `Item_CallHatchEvent`; leaving the range clears `ishatched` and calls `Item_CallUnhatchEvent`.
- The overlay helper `EggHatcher_1090_0921` uses the same range values to draw the visible diamond outlines for `Ctrl+F7`. In other words, the visualizer is showing the same trigger footprint the runtime is testing against.
- Current safest gameplay-facing read: these "eggs" are hidden trigger items that fire enter/leave behaviors when the avatar crosses their footprint. Some are likely classic trap/trigger/controller eggs rather than literal spawn objects.
- The wider workspace evidence agrees with that interpretation. The extracted USECODE corpus contains egg-like labels such as `TRIGEGG`, `ONCEEGG`, `DOOREGG`, `LAZEREGG`, `STEAMEGG`, `MISS1EGG`, `GRENEGG`, and `REB_EGG`, while the older `crusader-disasm` notes explicitly call out `SnapEgg` as a fast-area participant.
### How To Find One In Practice
- The most reliable bootstrap is now: start with `-laurie`, then press `Shift+~` to flip the live keyboard cheat latch on, then use the F7-family overlays.
- Use `Ctrl+F7` when you want true egg-hatcher ranges. If nothing appears, switch to `Alt+F7` too; that overlay walks the snap-process egg list and can reveal related trigger coverage that is not currently present in the live egg-hatcher process list.
- Search in gameplay spaces that are likely controlled by hidden trigger items rather than by obvious UI state: door thresholds, ambush spawn zones, laser or steam hazards, missile/grenade trap corridors, teleporter pads, and one-shot scripted encounter rooms.
- If a region only reacts once, walk out of the area and re-enter it. The live runner explicitly tracks enter/leave transitions and only calls the hatch/unhatch events when the avatar crosses the trigger boundary.
- Monster-egg handling still looks special-cased, so a blank `Ctrl+F7` result does not prove a map has no egg-related logic at all; it only proves there are no eligible live `EggHatcherProcess` outlines being drawn at that moment.
### Cheat / Debug Key Matrix
Cheat-state legend used below:
- `master gate (0x844)`: broader Laurie/debug permission state. `-laurie` sets this directly.
- `keyboard latch (0x6045)`: full keyboard-cheat latch used by the low-level F10 path and some other direct keyboard branches.
- `sequence latch (0x8c52)`: extra post-sequence latch left behind by `jassica16`; plain `~` does not set it.
- `input-active gate (0x85f)`: broader gameplay-input state. This is not a cheat bit, but it still suppresses some option-key behavior when the game is in modal/non-gameplay states.
| Input | State Needed | Effect | Evidence / Notes |
|------|--------------|--------|------------------|
| `-laurie` | none at startup | Sets the `master gate (0x844)` only | This is the easiest way to unlock the broader debug/event family without entering the hidden key sequence. |
| `jassica16` | none | Toggles `0x844` and `0x6045`, sets `0x8c52`, emits `0x103`, shows cheat active/inactive modal | Raw scan-code matcher, not a translated text string. Can bootstrap full cheat state from cold. |
| `Shift+~` / `~` | `0x844` already on | Toggles only `0x6045`; shows the on/off cheat-latch modal | On a US layout the game is checking logical `0x7e`, so Shift is the expected normal gesture. With `-laurie`, this becomes a practical way to enable full keyboard cheats. |
| `F10` | `0x85f` and `0x6045` | Refill/loadout branch: restore/refill, credits, items, ammo, weapon set | This is the plain full-keyboard-cheat branch in `Key_HandleOptionKeys`. |
| `Ctrl+F10` | `0x85f` and `0x6045`; current NPC alive | Toggles the current controlled NPC's immortal flag | Same F10 branch, but only the modifier-gated immortality sub-path. |
| `Ctrl+L` | no cheat gate recovered | Shows current avatar X/Y/Z popup | Online `Ctrl+C` folklore is wrong for this build. |
| `Ctrl+C` | no live branch recovered | No confirmed effect in this build | Best current correction is `Ctrl+L`. |
| `Ctrl+V` | no cheat gate recovered | Displays internal stats / version / memory info | This branch appears to be available without the cheat gates. |
| `Ctrl+Q` | `0x844` | Toggles CD transfer display state (`0x604f`) | Distinct from immortality. Uses the broader master gate, not the full keyboard latch. |
| `F7` | `0x844` | Draws coarse `3 x 3` camera-aligned world-cell grid | Simple isometric background grid. |
| `Alt+F7` | `0x844` | Draws snap-process egg diamonds | Better read as snap/trigger coverage overlay than generic grid. |
| `Ctrl+F7` | `0x844` | Draws egg-hatcher trigger diamonds | Visualizes live `EggHatcherProcess` ranges; can look blank on maps without eligible live processes. |
| `F` | `0x844` | Toggles object/framework paint overlay | Laurie/debug-side visual lane, not a full keyboard-cheat-latch feature. |
| `H` | `0x844` and `0x8c52` | Toggles Hack Mover | Strongest current read is that this needs the broader Laurie/debug gate plus the extra post-`jassica16` sequence latch. |
Practical state recipes:
- `-laurie` by itself gives you the broader Laurie/debug event family (`0x844`) but not full keyboard cheats.
- `-laurie` plus `Shift+~` is enough to reach the full keyboard-cheat state in live play, because `-laurie` supplies `0x844` and `~` then flips `0x6045`.
- `jassica16` is still the most complete single-step unlock, because it toggles both cheat bytes together and also leaves the extra `0x8c52` sequence latch behind for later branches such as Hack Mover.
### Cheat-related string table (seg014 / `000e:xxxx`)
| Address | String | Notes |
|-------------|-------------------------------------------------|-------|
| `000e:9c5e` | `"FART ...TRY... -laurie (Have fun, Jely)"` | Dev Easter-egg comment; no static code xref |
| `000e:9c87` | `"CHEATS ON"` | Cheat-on status string |
| `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."` | 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 |
| `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 |
### Cheat event dispatch summary (000c segment)
All cheat-related event case-handlers reside as shared-frame case bodies within a large event dispatch function in segment 000c. Each body inherits BP from the enclosing prologue and exits via `POP DI; POP SI; LEAVE; RETF`.
| Address | Symbol | Event | Action |
|-------------|------------------------------------------------|---------|--------|
| `000c:8e16` | `event_0x441_cheat_debug_overlay_toggle` | `0x441` | Toggles `DS:0xee0` (boolean-NOT); calls `[0x2bd8]` vtable `+0x2c`; gate = `DS:0x844` |
| `000c:8e46` | `event_0x241_cheat_debug_overlay_toggle` | `0x241` | Toggles `DS:0x2bc9` (1-current); same vtable dispatch; gate = `DS:0x844` |
| `000c:8e72` | `event_0x141_cheat_debug_overlay_toggle` | `0x141` | Toggles `DS:0x2bca` (1-current); same vtable dispatch; gate = `DS:0x844` |
| `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_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)
| Address | Name | Description |
|-------------|-----------------------------------------------|-------------|
| `0007:04dc` | `keyboard_input_cheat_dispatch` | Processes one keyboard event: calls `cheat_code_check`, then dispatches on raw scan-code `[record+1]`. Tab/J (0x0f/0x24) → context-sensitive entity action via FUN_0005_e119/252; KP* (0x37) → `cheat_entity_slot_cycle_and_update_sprite`; Space (0x39) → movement/entity_command_dispatch; KP- (0x4a) → `cheat_anim_type_cycle_and_refresh`; KP+/KP0/KPDel (0x4e/0x52/0x53) → selected object vtable `+0x18`(0xb,...) dispatch. ASCII H (0x48) absent; HACK MOVER comes from a higher scripting layer. |
| `0007:0d0a` | `cheat_code_check` | Five-byte stateful matcher at `DS:0x2833`; toggles `0x844`+`0x6045`; cheats-on notification via `display_null_check_dispatch(..., 0x287b)`; cheats-off via `display_null_check_dispatch(..., 0x2892)`. |
### Cheat-dispatch helpers (000c:81xx)
| Address | Name | Description |
|-------------|-----------------------------------------------|-------------|
| `000c:8072` | `cheat_entity_slot_cycle_and_update_sprite` | Cycles slot 1..5 for `0x7e22` entity; picks sprite ID by class flags; calls `entity_table_set_sprite`. |
| `000c:81c0` | `cheat_anim_type_cycle_and_refresh` | Cycles animation-type 0x0b..0x19 for `0x7e22`; writes per-entity `+0x19`; calls `0008:4bba(0x20)`. |
| `000c:8221` | `cheat_flag_6050_clear` | Clears `DS:0x6050` to 0. |
| `000c:8227` | `cheat_flag_6050_read` | Returns `DS:0x6050` in AL. |
| `000c:822b` | `cheat_flag_6050_set` | Sets `DS:0x6050` to 1. |
### Additional cheat-dispatch hotkeys in `keyboard_input_cheat_dispatch`
Verified byte tests in the caller-side dispatch:
- `0x37` calls `000c:8072` (`cheat_entity_slot_cycle_and_update_sprite`)
- `0x4a` calls `000c:81c0` (`cheat_anim_type_cycle_and_refresh`)
- `0x0f` and `0x24` share a context-sensitive branch via `FUN_0005_e252` with event IDs `0x3a`, `0x38`, or `0x0b`
- `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
### F10 immortality vs Ctrl+Q CD transfer display
The live NE decompile now separates these behaviors cleanly.
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."`
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**.
The immortality status strings are separate. The live NE decompile plus disassembly of `Key_HandleOptionKeys` directly shows the modifier-gated F10 immortality branch using:
- `"Immortality enabled."` at `1478:2866`
- `"Immortality disabled."` at `1478:2850`
The same compiled proof also sharpens the modifier claim beyond the earlier folklore-level read:
- `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.
Current strongest read:
- 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):**
`usecode_debugger_handle_event` (`000b:b62c`, live `13a0:1df3`) receives event 0x410 through the registration installed by `usecode_debugger_gump_create` at `000b:b3b1` (`13a0:19b1`). When event 0x410 arrives, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to the shared debugger tail. This is a parallel state-machine path that runs alongside the 000c toggle; current best read is that it drives the hidden usecode-debugger state machine rather than a standalone cheat-only listener.
| Address | Symbol | Role |
|-------------|-------------------------------|------|
| `000b:b3b1` | `usecode_debugger_gump_create` | Allocates the seg109 debugger gump object, initializes its panes/watch state, and subscribes it to the shared debugger/control event bundle that includes `0x410`. |
| `000b:b62c` | `usecode_debugger_handle_event` | Debugger-side event mapper: rewrites incoming `0x410` to local state `0x0e` before entering the shared debugger state machine. |
| `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 usecode debugger investigation (seg109 UI lane)
New compiled-side evidence shows that the older "hidden cheat menu" label was misleading. The recovered path is much closer to a hidden **usecode debugger / unit inspector** than to a retail cheat list.
The two address families that looked inconsistent in older notes are actually two connected layers of the same subsystem, not competing identifications:
- the `000b:*` / live `13a0:*` functions build and drive the modal debugger UI,
- the `1408:*` helpers hold breakpoint/callstack state,
- and the interpreter hook at `1418:04aa..04b5` ties the runtime back into that debugger state.
Combined debugger component table:
| Layer | Raw/reference anchor | Live NE Ghidra | Symbol | Verified role |
|-------|----------------------|----------------|--------|---------------|
| UI wrapper | `000b:9a86` | `13a0:0086` | `usecode_debugger_open_for_current_unit` | Builds the debugger gump in current-unit mode, resolves the active unit name from the live debugger state at `0x659c/0x659e`, loads the corresponding usecode file, centers on the current line, and enters the modal debugger UI. |
| UI wrapper | `000b:9c0d` | `13a0:020d` | `usecode_debugger_open_modal` | Smaller generic modal wrapper for the same debugger gump; it skips current-unit preload and simply opens the modal UI. |
| UI constructor | `000b:b3b1` | `13a0:19b1` | `usecode_debugger_gump_create` | Allocates the root debugger gump, builds the menu bar and panes, initializes watch state, resolves the base `usecode` path, and registers the shared debugger/control event bundle including `0x23f`, `0x410`, `0x411`, and `0x441`. |
| UI dispatcher | `000b:b62c` | `13a0:1df3` | `usecode_debugger_handle_event` | Main debugger event dispatcher. Recovered cases are debugger-style commands: open unit/file, go to line, watch, inspect, clear watches, change global, find/search again, and break to TDP. Incoming event `0x410` is remapped to local state `0x0e`. |
| UI menu builder | `000b:2882` | `13a0:2882` | `usecode_debugger_build_menubar` | Builds the hidden debugger menu bar with File, Run, Breakpoints, Search, and Data menus plus entries such as `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`. |
| Break-state constructor | n/a | `1408:0000` | `usecode_debugger_break_state_create` | Allocates and initializes the seg1408 debugger-state object: vtable, breakpoint table, current-entry stack, and run/step flags. This is the object the rest of the breakpoint lane appears to expect at `0x659c/0x659e`. |
| Breakpoint gate | n/a | `1408:0053` | `usecode_debugger_maybe_break_on_current_line` | Stores the current interpreted line, resolves the active unit name through `1408:0444`, checks file+line breakpoints through `1408:029e`, evaluates run/step flags, and callbacks through the object's vtable when a break condition is met. |
| Breakpoint helper | n/a | `1408:00dd` | `usecode_debugger_breakpoint_insert_sorted` | Inserts `(file,line)` breakpoint entries into the seg1408 table in sorted order. |
| Breakpoint helper | n/a | `1408:029e` | `usecode_debugger_has_breakpoint` | Exact `(file,line)` membership test over the seg1408 breakpoint table. |
| Callstack helper | n/a | `1408:03b0` | `usecode_debugger_callstack_push_entry` | Pushes the current unit/line debugger entry onto the seg1408 callstack. |
| Callstack helper | n/a | `1408:03f7` | `usecode_debugger_callstack_pop_entry` | Pops one debugger callstack entry and asserts on underflow. |
| Step-state helper | n/a | `1408:0419` | `usecode_debugger_enable_single_step` | Arms the single-step mode flags in the debugger-state object. |
| Step-state helper | n/a | `1408:0432` | `usecode_debugger_clear_step_state` | Clears the break/step flags in the debugger-state object. |
| Current-entry helper | n/a | `1408:0444` | `usecode_debugger_current_entry_get_unit_name` | Returns the active unit/file name pointer from the current debugger entry stack. |
| Interpreter hook | n/a | `1418:04aa..04b5` | interpreter-side break callback site | Checks whether `0x659c/0x659e` is non-null, pushes the current interpreted line, then calls `1408:0053`. This is the concrete runtime handoff that links usecode execution to the hidden debugger state. |
This better explains the long-running negative result about the supposed 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 `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 `usecode_debugger_gump_create` appears there.
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 debugger entry than assuming a still-live player-facing scrollable cheat list.
#### Breakpoint callback lane (new strongest orphan candidate)
The strongest likely original entry point is no longer the cheat-toggle helper. It is the surviving **usecode breakpoint callback lane** summarized in the combined table above.
The key live proof is the interpreter-side handoff at `1418:04aa..04b5`:
- 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
The practical patch work ended up being mostly about **finding a call site whose runtime context matches the hidden menu wrappers**, not just finding any place that reaches `000a:5276`.
Verified retail anchor points:
| File off | Ghidra | Meaning | Notes |
|----------|--------|---------|-------|
| `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:99e0` -> `seg092:0476`; this is the first materially safer deferred hook candidate after the direct matcher path failed |
| `0xb9a8d` | `000b:9a8d` | arg setup inside `usecode_debugger_open_for_current_unit` | wrapper-local constructor setup uses caller stack words `[BP+8]` and `[BP+6]` plus the current-unit mode flag `1` before calling `usecode_debugger_gump_create` |
| `0xb9c48` | `000b:9c48` | modal wrapper prologue; the inherited caller-word patch subsite is `000b:9c4e` / live `13a0:024a` | wrapper-local setup still feeds caller stack words `[BP+8]` and `[BP+6]` into `usecode_debugger_gump_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: `usecode_debugger_open_for_current_unit` 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` 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` (`usecode_debugger_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:
- Site 1 tried changing `0007:0d75` from `push 0x103` to `push 0x42f`, keeping the original event-dispatch helper call intact.
- Site 2 retargeted the `000c:99e1` relocation so the `0x42f` handler's internal `push 0x103 / call 000a:5276` sequence called `usecode_debugger_open_modal` instead.
- Site 3 patched `000b:9c48` from `6A 00 FF 76 08 FF 76 06` to `6A 00 6A 00 6A 00 90 90`.
Observed result on retail test build:
- The game no longer failed at startup, and the mouse pointer appeared when the cheat fired, confirming that the hidden modal UI path was being entered.
- 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 Ghidra-side patch plan for a copy:
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` (`usecode_debugger_open_modal`), yielding bytes `9A 0D 02 A0 13`.
4. Navigate to `13a0:024a` inside `usecode_debugger_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:
- Earlier direct-hook attempts proved that inheriting the two caller-frame words at `000b:9a8f/9a92` is unsafe from the cheat matcher context.
- But later decompilation of `usecode_debugger_gump_create` showed that the leading `push 0x1` at `000b:9a8d` is a distinct current-unit mode byte used by the constructor path, so zeroing all three pushed values was too aggressive.
- The current patch therefore preserves the leading `1` and only forces the two ambiguous 16-bit parameters to zero.
Risk notes:
- These remain behavioral exploration hacks, not correctness fixes.
- The evidence now strongly suggests the hard part is runtime context and event timing, not discovering the retail file offsets.
- If the revised direct `0007:0d79 -> 000b:9a86` path with the narrower `000b:9a8d` wrapper patch still fails, the next step should be a queue/defer design or a trampoline/cave patch rather than another blind event substitution.
### Conservative folklore verification
- "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**.
- 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.
- "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`.