Crusader_Decomp/docs/ne-segment1.md

487 lines
44 KiB
Markdown
Raw Normal View History

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