Crusader_Decomp/docs/ne-segment1.md
MaddoScientisto daa363c3d2 Add 'annotate-usecode' command to import USECODE IR JSON annotations
- Introduced a new command 'annotate-usecode' to import USECODE IR JSON annotation hints as Ghidra comments on compiled anchors.
- Added argument parsing for multiple IR JSON files, comment type selection, and a dry-run option.
- Implemented logic to read annotation records from the provided IR files and set comments on the corresponding addresses in Ghidra.
- Enhanced JSON schema to include response structure for the new command.
2026-03-24 18:14:20 +01:00

487 lines
44 KiB
Markdown
Raw 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.
## 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 current string table, while `-laurie` is present as plain text.
- 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.
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.
- 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`.
### 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."` | Correlates to event `0x241` / `0x141` toggle area |
| `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 |
| `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_cheat_flag_604f_toggle` | `0x410` | Toggles `DS:0x604f` (boolean-NOT); notification at `DS:0x60d2` (on) or `DS:0x60ee` (off); 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
### Immortality mechanics (event 0x410 / flag 0x604f)
**How immortality works at the C level:**
`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`.
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
When `0x604f == 0` (Immortality **OFF**, normal path):
- Jump to `0004:c25b``CALLF 0003:ac7e` (seg001:367e) — full damage / death dispatch
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.
**What sends event 0x410 to toggle it:**
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`.
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.
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."
**Secondary handler (000b:b62c):**
`cheat_event_listener_handle_event` (`000b:b62c`) receives event 0x410 through the registration installed by `cheat_event_listener_create` at `000b:b3b1`. When event 0x410 arrives, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to `000b:b7f3` for processing. This is a parallel state-machine path that runs alongside the 000c toggle; likely it drives an associated USECODE process or animation object into state 14.
| 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. |
| `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:
| 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`. |
#### Reachability status in retail binary
- Static constructor callsites for `cheat_event_listener_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.
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).
#### 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:99e1` -> `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` |
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.
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.
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 `cheat_menu_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 patch candidate under test:
- 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`.
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 `cheat_event_listener_create` showed that the leading `push 0x1` at `000b:9a8d` is a distinct 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**.
- "Ctrl+F10 enables god mode" is **not supported** — the verified F10 branch does not require a modifier.
- "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.
- `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`.