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

44 KiB
Raw Blame 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
0x28000x2811 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
0x29100x2947 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+slot2; writes entity type table 0x7e1e[slot0x79+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
0x2f50x2f7 Special movement entity
0x595/0x597 Platform/elevator entities
0x31c/0x3220x327 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.
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:c25bCALLF 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.