- Implemented a Python script to extract data from the EUSECODE.FLX file format. - Defined data structures for candidate entries and extracted chunks using dataclasses. - Added functions to read and parse the FLX table, extract candidate data, and generate human-readable output files. - Included functionality for analyzing extracted data, including generating summaries, descriptors, and event family reports. - Implemented utilities for calculating printable ratios, zero ratios, and identifying text-like data. - Added support for writing various output formats, including JSON, TSV, and Markdown.
392 lines
29 KiB
Markdown
392 lines
29 KiB
Markdown
# 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 (0x0060–0x0d5f)
|
||
|
||
| 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 |
|
||
| `0x27ca–0x27ce` | 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 (0–359) |
|
||
| `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 (0x2401–0x5a50)
|
||
|
||
| 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 2–255, 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 0x27ca–0x27ce, 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 (0–15) |
|
||
| `+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 (0x435e–0x44a9)
|
||
|
||
| 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 (0x4659–0x5a99)
|
||
|
||
| 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 (0–15): dx offsets at 0x129b, dy offsets at 0x12ac
|
||
- Values are multiplied by distance (e.g. `*0x500`) for projectile spawn offsets
|
||
|
||
## Collision Detection (0x60c1–0x621e)
|
||
|
||
| 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 (0x6aed–0x6d21)
|
||
|
||
| 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 (0x7490–0x75ff)
|
||
|
||
| Address | Name | Description |
|
||
|----------|---------------|-------------|
|
||
| `0x7490` | `debris_spawn`| Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list |
|
||
| `0x75ff` | `entity_die` | Death handler: spawns 1–4 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.
|
||
|
||
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).
|
||
|
||
**Secondary handler (000b:b62c):**
|
||
|
||
`000b:b62c` subscribes to event 0x410 via the registration at `000b:b5cb`. When event 0x410 is received by this handler, 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`. |
|
||
| `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. |
|
||
|
||
### 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.
|
||
- 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`.
|