707 lines
74 KiB
Markdown
707 lines
74 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.
|
||
|
||
For current project work, treat this file as a verified evidence source to be cross-referenced into the live `CRUSADER.EXE` session. When a claim here is used in the NE database or notes, keep the NE address and the older segment/raw address linked together where practical.
|
||
|
||
## Cursor Subsystem (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 normal runtime string table or the verified command-line parser, while `-laurie` is present as plain text.
|
||
- The live NE export/naming trail does preserve older user-defined symbol names for the matcher cells (`g_jassica16Scans` at `1020:2833` and `g_jassica16Offset` at `1020:283d`), which fits the long-standing community name even though the compiled matcher source bytes are not a plain ASCII string in this build.
|
||
- The matcher bytes themselves are now rechecked directly in the live NE image: `24 1e 1f 1f 17 2e 1e 02 07 00`, which is the scan-code sequence for `j a s s i c a 1 6`.
|
||
- That matters for runtime testing: the trailing `1` and `6` are the top-row digit scan codes (`0x02`, `0x07`), not numpad digits. If those keys are entered through a different scan-code-producing path, the matcher will not complete.
|
||
- The ordinary keyboard ISR producer still does not support the old byte-for-character model cleanly: it feeds normalized scan-code-style values into record byte `+1`, while the matcher source at `0x2833` is a live code-byte sequence with two values (`0x80`, `0xfd`) that do not fit that ISR path.
|
||
- The direct call `drawlist_init -> FUN_0007_04dc` is the first concrete static evidence for a higher-level path in this build.
|
||
- Observed runtime behavior now fits the toggle model cleanly: if `-laurie` has already forced `0x844 = 1`, triggering the hidden matcher again will toggle both `0x844` and `0x6045` back to `0`, which explains the user-observed "jassica16 disables cheats when laurie is active" behavior.
|
||
- Live data-use recovery tightens the latch story further: `0x6045` is written only by `Key_CheckCheatToggle` (`1130:2b72`) and the separate event-`0x7e` runtime toggle (`13e8:203d`). So if `jassica16` really completes and no later `0x7e` toggle fires, the low-level cheat latch should stay on.
|
||
|
||
F10 key behavior (verified in raw build):
|
||
- `seg001_input_keyboard_handler` at `0006:ec29` handles input byte `0x44` and immediately returns unless cheats are enabled through `0x6045`.
|
||
- "Plain F10 when cheats are enabled" is verified, and the immortality toggle is a separate Ctrl-gated F10 sub-branch.
|
||
- Live NE cross-check: `Key_HandleOptionKeys` at `1130:0896` / `0007:0a36` confirms the same gating and sharpens the keyboard result further. Once the low-level cheat latch `0x6045` is on, plain F10 runs the large restore/refill/loadout branch, while the modifier-gated F10 sub-branch directly toggles the current controlled NPC's immortal flag and posts the local `"Immortality enabled."` / `"Immortality disabled."` messages.
|
||
- The helper identity is now closed from the code too, not only from runtime behavior. `KeyboardGetExtendedShiftStates` (`11d0:39e6`) uses BIOS `INT 16h, AH=12h`, whose returned AH bits are `0=left Ctrl`, `1=left Alt`, `2=right Ctrl`, `3=right Alt`. The helper at `11c8:01a8` tests `0x0100 | 0x0400`, so it is really `KeyEvent_IsCtrlDown`; the helper at `11c8:018a` tests `0x0200 | 0x0800`, so it is really `KeyEvent_IsAltDown`. The older Alt/Ctrl labels were reversed.
|
||
- The remaining timing question is now closed by the upstream keyboard path too. The held-key repeat builder at `11b8:0129..022b` samples the current BIOS extended-shift state through `11d0:39e6`, stores that snapshot from `31a4` into each synthesized `KeyEvent` at `11b8:01d5`, and queues the 12-byte event through `11d0:3533`. `Keyboard_GetLastKeyEvent` (`11b8:0457`) later returns that exact snapshot, and `Controller_HandleKeyEvent` (`1130:2211`) copies it before calling `Key_HandleOptionKeys`.
|
||
- That engine-side repeat synthesis explains both observed quirks cleanly. Holding `F10` first causes the game to keep generating repeated F10 keydown-style events; once physical `Ctrl` is pressed, later repeated F10 events carry the refreshed modifier snapshot and can satisfy the immortality branch. The same lack of one-shot suppression is why holding the keys too long can flip immortality on and off repeatedly and spawn multiple enable/disable modals.
|
||
- The live NE string addresses for the F10 immortality messages are `1478:2850` = `"Immortality disabled."` and `1478:2866` = `"Immortality enabled."` The earlier `1478:6450/6466` note was incorrect.
|
||
- One more runtime gate is now explicit too: the F10 path first checks `DAT_1478_085f` at `1130:0a29`, then `0x6045` at `1130:0a36`.
|
||
- The `0x85f` state now has a tighter live model. It is set during `Game_Start` (`1020:0127`), cleared at the end of `ComputerGump_CreateGump` (`1398:01f5`) just before returning the modal gump, and restored in `ComputerGump_CloseAndResumeGameplay` (`1398:0233`) during the computer-gump cleanup path before falling into the base-gump teardown. Across the wider codebase it gates controller/joystick/camera and option-key handling, so the current safest read is **gameplay input / option-key active state**, not a cheat byte.
|
||
- That makes the paired `1398` helpers easier to read too: `ComputerGump_CreateGump` suspends normal gameplay input by clearing `0x85f`, and `ComputerGump_CloseAndResumeGameplay` appears to be the computer-gump close/teardown override that restores gameplay input, releases any pending text buffer at `+0x34/+0x36`, refreshes the UI/controller state, and then falls through the generic gump cleanup/free path.
|
||
- The hottest `0x85f` readers are clearer in the live NE database now. `Controller_HandleKeyEvent` (`1130:2334`) first checks the separate controller-enable latch at `1478:27cb`, then forwards into `Key_HandleOptionKeys`, and only after that rechecks `0x85f` before allowing ordinary gameplay key processing. The paired `13e8` wrappers around that flow are now renamed `Game_DisableGameplayInputAndRefreshCamera` (`13e8:0e7d`) and `Game_RestoreGameplayInputAndClearModalState` (`13e8:0ef9`): together they clear/restore `1478:27cb`, flip the overlay-suppression byte `1478:2c64`, toggle transient state `1478:8c53`, and preserve `1478:8c54` as a saved copy of `1478:2d24` while the modal camera/input transition is active.
|
||
- The Laurie side is narrower too. `Game_ShowLaurieHintComputerGump` (`13e8:0e31`) is the hidden computer-gump hint path that reaches the `FART ...TRY... -laurie (Have fun, Jely)` string, and `Game_ShowLaurieHintIfGameplayInputActive` (`13e8:0f4a`) is only a tiny `0x85f`-gated wrapper around it. So the Laurie hint path and the F10 immortality path are both suppressed by the same broader gameplay-input gate, even though they are otherwise separate features.
|
||
- The main camera pass in that same lane is now named `Camera_RedrawViewportAndGameplayOverlays` (`1180:19c1`). It brackets the viewport blit with two newly named overlay helpers, `GameplayOverlayWindows_DrawTracked` (`1188:010f`) and `GameplayOverlayWindows_ClearDirtyRects` (`1188:0394`), and it also uses `0x85f` to choose between the avatar-centered redraw rectangle and the wider modal/non-gameplay redraw path.
|
||
- The `0x85f` callers inside `World_HandleKeyboardInput_13e8_14b4` are now concrete enough to explain user-visible failures better. The same modal disable/restore pair wraps the exit-to-DOS confirmation lane (`0x22d`), quick save (`0x13f`), quick load (`0x13e`), restart/main-menu handling (`Game_RestartMaybe`), and the load/menu gump lanes around `0x142` / `0x143` / `0x13c`. If play is currently inside one of those modal transitions, the F10 immortality path is suppressed before the cheat latch is even consulted.
|
||
- The runtime cheat-latch override is also firmer now. Event `0x7e` inside `World_HandleKeyboardInput_13e8_14b4` is the only other recovered writer of `0x6045` besides `Key_CheckCheatToggle`, and it independently flips the keyboard cheat latch while only requiring the broader `0x844` permission gate. So a successful `jassica16` match can still be undone later by that separate `0x7e` path.
|
||
- `Key_CheckCheatToggle` itself is now comment-backed as an exact scan-code matcher: only keydown events participate, and the final `1` / `6` bytes in `g_jassica16Scans` are still top-row scan codes `0x02` / `0x07`. That keeps keypad `1` / `6` or any non-scan-code-equivalent input path as a live explanation for failed attempts.
|
||
|
||
### No Regret cross-check
|
||
|
||
- The currently opened `REGRET.EXE` uses the same overall F10 structure but not the same cheat-sequence semantics. Its `Key_HandleOptionKeys` lives at `1148:0a9a`, and the F10 branch at `1148:0d0e` first checks `1480:0adc`, then `1480:009b`, and then calls `11e0:01a8` before toggling the controlled NPC's immortal flag and displaying `1480:3052 = "Immortality disabled."` / `1480:3068 = "Immortality enabled."`.
|
||
- Live runtime testing tightened the practical input story further. In both games, the user was only able to trigger immortality by pressing `F10` first and then pressing `Ctrl` while continuing to hold `F10`; `F10` + `Alt` did nothing. The No Remorse helper swap is now code-proven from the BIOS bit layout. `REGRET.EXE` almost certainly follows the same convention in its parallel helper pair, but that target should be reopened and fixed directly before promoting the same renames there.
|
||
- The hidden key-sequence function is also different enough to matter. `1148:34d2` is now renamed `Key_CheckSecretCodeSequences`. Its first scan-code table at `1480:2ff0` is still `jassica16` (`24 1e 1f 1f 17 2e 1e 02 07 00`), but in No Regret that sequence triggers the `"Of course we changed the cheats..."` lane instead of the main cheat latch.
|
||
- The actual No Regret cheat-toggle sequence is the second table at `1480:2ffc`, which decodes as `loosecannon` plus top-row `1` / `6` tail scan codes (`26 18 18 1f 12 2e 1e 31 31 18 31 02 07 00`). Completing that sequence toggles `1480:0ac0` and mirrors the result into the low-level F10 latch `1480:009b`, then posts `"Cheats are now active."` at `1480:30be` or `"Cheats are now inactive."` at `1480:30d5`.
|
||
- The repeated modal behavior also now makes sense from the compiled path. The No Regret F10 branch has no one-shot debounce; it only requires a keydown-style event and the modifier helper to pass. So if `F10` is held long enough for key repeat, the branch can run again and again, toggling immortality on and off and spawning multiple enable/disable modals in succession.
|
||
- The strongest No Regret contrast is therefore no longer `Alt` versus `Ctrl`; it is that the latch-enabling secret code changed from `jassica16` to `loosecannon`, while the physical modifier gesture for the F10 immortality toggle behaves as `F10`-then-`Ctrl` in live play.
|
||
- The immortality sub-branch is also only reached for a live current NPC: after the `0x6045` gate, the code calls `NPC_IsDead` at `10e8:1fed`; the modifier-gated F10 path begins only on the zero/not-dead result.
|
||
- When a current `0x7e22` entity exists, the branch resolves the current selection and refreshes per-entity bookkeeping.
|
||
- In the `local_4 == 1` case the branch becomes a large restore/reset routine that tears down and rebuilds multiple linked objects around `0x7e22`, retries dispatch up to `0x14` times per stage, and fires the event batch `0x33d`, `0x33f`, `0x340`, `0x341`, `0x33e` before re-enabling channels `4`, `1`, and `0`.
|
||
|
||
What `-laurie` appears to enable by itself:
|
||
- It sets the broad cheat-permission flag `0x844` and shows the startup-side "Cheats are now active." notification, but it does **not** set the low-level keyboard cheat latch `0x6045`.
|
||
- That means `-laurie` is enough for the compiled event handlers gated only by `0x844` to operate, including the debug-overlay family (`0x441`, `0x241`, `0x141`) and the **CD transfer display** toggle handler (`0x410`).
|
||
- It is also enough to permit the separate event-`0x7e` runtime toggle path, whose entire job is to flip `0x6045` later and post the `0x6087` / `0x6091` notifications.
|
||
- It is **not** enough for low-level keyboard-only cheat branches that check `0x6045` directly. That is why the user can see the `0x844`-gated debug-box behavior while plain F10 still behaves as if full keyboard cheats are off.
|
||
|
||
### Keyboard folklore correction pass (`CRUSADER.EXE` live NE)
|
||
|
||
- `~` is real in this build. In `World_HandleKeyboardInput_13e8_14b4`, the branch at `13e8:202d..203d` requires `0x844 != 0`, flips the live keyboard-cheat latch `0x6045`, and posts the paired on/off messages at `0x6087` / `0x6091`.
|
||
- `Ctrl+C` is **not** the current-location popup in this build. The actual location-display branch is `Ctrl+L` (`0x426`), which formats the avatar's X/Y/Z into the `1478:610c` string and displays it at `13e8:255e..25ac`.
|
||
- The third F7-family overlay is also real. The three cheat-gated overlay toggles live at `13e8:1a7c` (`0x141`), `13e8:1a50` (`0x241`), and `13e8:1a20` (`0x441`), writing `1478:2bca`, `1478:2bc9`, and `1478:0ee0` respectively before forcing a camera refresh through `[g_cameraProcess+0x2c]`.
|
||
- The unresolved online entry is therefore best corrected as `Ctrl+F7 exists but is cheat-gated`; the obviously bogus list item is `Ctrl+C`, which should be `Ctrl+L` for the coordinate popup.
|
||
|
||
### `~` versus `jassica16`
|
||
|
||
- These are **not** the same mechanism. `jassica16` is handled earlier in `Key_CheckCheatToggle` as a raw scan-code matcher over `1478:2833`, while `~` is handled later in `World_HandleKeyboardInput_13e8_14b4` as a translated logical key value (`0x7e`).
|
||
- That means Shift is probably normal behavior here, not a DOSBox-only artifact. On a standard US DOS keyboard layout, plain grave is `` ` `` (`0x60`) and Shift+grave is `~` (`0x7e`); the live handler we recovered is the `0x7e` branch, and no separate backtick/`0x60` hotkey path was recovered in this handler.
|
||
- DOSBox can still affect host-to-DOS layout mapping, but the recovered game logic is asking for the translated `~` character value, not the raw grave-key scancode. So on a US layout the expected in-game gesture is Shift+grave.
|
||
- `jassica16` is the stronger unlock path. On success it sets `0x8c52 = 1`, toggles both `0x844` and `0x6045`, emits the `0x103` voice/helper side effect, and shows the `1478:287b` / `1478:2892` cheat-state modals.
|
||
- Plain `~` is narrower. It only works after the broader `0x844` gate is already on, and then it flips just the live keyboard-cheat latch `0x6045` while showing the `0x6087` / `0x6091` on/off messages.
|
||
- Practically: `jassica16` can bootstrap cheats from a cold state because it toggles the master gate and the low-level latch together; `~` cannot bootstrap that state by itself because its own branch first requires `0x844 != 0`.
|
||
- The extra `0x8c52` write also means `jassica16` is not just a different UI for the same toggle. It leaves a second latch behind that later keyboard-side cheat/debug branches can test, while plain `~` does not touch that latch.
|
||
|
||
### What `Ctrl+F7` is supposed to show
|
||
|
||
- `Ctrl+F7` (`13e8:1a20` -> `1478:0ee0`) is not just a third generic grid. In the camera redraw path, `Camera_1180_15ef` treats `0x0ee0` differently from the simpler `0x2bca` grid flag.
|
||
- Plain `F7` (`1478:2bca`) draws a coarse `3 x 3` isometric world-cell grid around the camera by stepping `0x200` world units and drawing diamond tile boundaries.
|
||
- `Alt+F7` (`1478:2bc9`) calls `Snap_1058_0814`, which walks the snap-process egg list and draws per-entry diamonds from packed egg range data. That is closer to a snap/trigger coverage overlay than to a camera-aligned background grid.
|
||
- `Ctrl+F7` (`1478:0ee0`) walks `EggHatcherProcess` objects and calls `EggHatcher_1090_0921`, which draws diamond trigger ranges using `Egg_GetXRange`, `Egg_GetYRange`, and the shared diamond helper at `1180:1ce5`.
|
||
- So the intended visual for `Ctrl+F7` is an **egg / hatch trigger-range overlay**. It should highlight live egg-hatcher or monster-egg regions, not fill the screen with a regular map grid.
|
||
- That also explains why it can appear to do nothing. If the current map has no live `EggHatcherProcess` objects, or a non-monster-egg trigger has already hatched while normal gameplay input is active, the draw loop has nothing eligible to outline even though the toggle flag itself changed.
|
||
|
||
### Egg-Hatcher Runtime Notes
|
||
|
||
- The live NE runtime has a dedicated egg-hatcher process type: `EggHatcher_CreateProcess` builds process type `0x20f`, stores one target item number in the process, and names it `EggHatcher`.
|
||
- The corresponding runner `EggHatcherProcess_Run` reads the avatar footprint and compares it against the egg item's center, `Egg_GetXRange`, `Egg_GetYRange`, and a vertical window of about `+/- 0x30` Z units.
|
||
- For non-monster egg families, entering that range sets the process `ishatched` byte and calls `Item_CallHatchEvent`; leaving the range clears `ishatched` and calls `Item_CallUnhatchEvent`.
|
||
- The overlay helper `EggHatcher_1090_0921` uses the same range values to draw the visible diamond outlines for `Ctrl+F7`. In other words, the visualizer is showing the same trigger footprint the runtime is testing against.
|
||
- Current safest gameplay-facing read: these "eggs" are hidden trigger items that fire enter/leave behaviors when the avatar crosses their footprint. Some are likely classic trap/trigger/controller eggs rather than literal spawn objects.
|
||
- The wider workspace evidence agrees with that interpretation. The extracted USECODE corpus contains egg-like labels such as `TRIGEGG`, `ONCEEGG`, `DOOREGG`, `LAZEREGG`, `STEAMEGG`, `MISS1EGG`, `GRENEGG`, and `REB_EGG`, while the older `crusader-disasm` notes explicitly call out `SnapEgg` as a fast-area participant.
|
||
|
||
### How To Find One In Practice
|
||
|
||
- The most reliable bootstrap is now: start with `-laurie`, then press `Shift+~` to flip the live keyboard cheat latch on, then use the F7-family overlays.
|
||
- Use `Ctrl+F7` when you want true egg-hatcher ranges. If nothing appears, switch to `Alt+F7` too; that overlay walks the snap-process egg list and can reveal related trigger coverage that is not currently present in the live egg-hatcher process list.
|
||
- Search in gameplay spaces that are likely controlled by hidden trigger items rather than by obvious UI state: door thresholds, ambush spawn zones, laser or steam hazards, missile/grenade trap corridors, teleporter pads, and one-shot scripted encounter rooms.
|
||
- If a region only reacts once, walk out of the area and re-enter it. The live runner explicitly tracks enter/leave transitions and only calls the hatch/unhatch events when the avatar crosses the trigger boundary.
|
||
- Monster-egg handling still looks special-cased, so a blank `Ctrl+F7` result does not prove a map has no egg-related logic at all; it only proves there are no eligible live `EggHatcherProcess` outlines being drawn at that moment.
|
||
|
||
### Cheat / Debug Key Matrix
|
||
|
||
Cheat-state legend used below:
|
||
|
||
- `master gate (0x844)`: broader Laurie/debug permission state. `-laurie` sets this directly.
|
||
- `keyboard latch (0x6045)`: full keyboard-cheat latch used by the low-level F10 path and some other direct keyboard branches.
|
||
- `sequence latch (0x8c52)`: extra post-sequence latch left behind by `jassica16`; plain `~` does not set it.
|
||
- `input-active gate (0x85f)`: broader gameplay-input state. This is not a cheat bit, but it still suppresses some option-key behavior when the game is in modal/non-gameplay states.
|
||
|
||
| Input | State Needed | Effect | Evidence / Notes |
|
||
|------|--------------|--------|------------------|
|
||
| `-laurie` | none at startup | Sets the `master gate (0x844)` only | This is the easiest way to unlock the broader debug/event family without entering the hidden key sequence. |
|
||
| `jassica16` | none | Toggles `0x844` and `0x6045`, sets `0x8c52`, emits `0x103`, shows cheat active/inactive modal | Raw scan-code matcher, not a translated text string. Can bootstrap full cheat state from cold. |
|
||
| `Shift+~` / `~` | `0x844` already on | Toggles only `0x6045`; shows the on/off cheat-latch modal | On a US layout the game is checking logical `0x7e`, so Shift is the expected normal gesture. With `-laurie`, this becomes a practical way to enable full keyboard cheats. |
|
||
| `F10` | `0x85f` and `0x6045` | Refill/loadout branch: restore/refill, credits, items, ammo, weapon set | This is the plain full-keyboard-cheat branch in `Key_HandleOptionKeys`. |
|
||
| `Ctrl+F10` | `0x85f` and `0x6045`; current NPC alive | Toggles the current controlled NPC's immortal flag | Same F10 branch, but only the modifier-gated immortality sub-path. |
|
||
| `Ctrl+L` | no cheat gate recovered | Shows current avatar X/Y/Z popup | Online `Ctrl+C` folklore is wrong for this build. |
|
||
| `Ctrl+C` | no live branch recovered | No confirmed effect in this build | Best current correction is `Ctrl+L`. |
|
||
| `Ctrl+V` | no cheat gate recovered | Displays internal stats / version / memory info | This branch appears to be available without the cheat gates. |
|
||
| `Ctrl+Q` | `0x844` | Toggles CD transfer display state (`0x604f`) | Distinct from immortality. Uses the broader master gate, not the full keyboard latch. |
|
||
| `F7` | `0x844` | Draws coarse `3 x 3` camera-aligned world-cell grid | Simple isometric background grid. |
|
||
| `Alt+F7` | `0x844` | Draws snap-process egg diamonds | Better read as snap/trigger coverage overlay than generic grid. |
|
||
| `Ctrl+F7` | `0x844` | Draws egg-hatcher trigger diamonds | Visualizes live `EggHatcherProcess` ranges; can look blank on maps without eligible live processes. |
|
||
| `F` | `0x844` | Toggles object/framework paint overlay | Laurie/debug-side visual lane, not a full keyboard-cheat-latch feature. |
|
||
| `H` | `0x844` and `0x8c52` | Toggles Hack Mover | Strongest current read is that this needs the broader Laurie/debug gate plus the extra post-`jassica16` sequence latch. |
|
||
|
||
Practical state recipes:
|
||
|
||
- `-laurie` by itself gives you the broader Laurie/debug event family (`0x844`) but not full keyboard cheats.
|
||
- `-laurie` plus `Shift+~` is enough to reach the full keyboard-cheat state in live play, because `-laurie` supplies `0x844` and `~` then flips `0x6045`.
|
||
- `jassica16` is still the most complete single-step unlock, because it toggles both cheat bytes together and also leaves the extra `0x8c52` sequence latch behind for later branches such as Hack Mover.
|
||
|
||
### Cheat-related string table (seg014 / `000e:xxxx`)
|
||
|
||
| Address | String | Notes |
|
||
|-------------|-------------------------------------------------|-------|
|
||
| `000e:9c5e` | `"FART ...TRY... -laurie (Have fun, Jely)"` | Dev Easter-egg comment; no static code xref |
|
||
| `000e:9c87` | `"CHEATS ON"` | Cheat-on status string |
|
||
| `000e:9c91` | `"CHEATS OFF"` | Cheat-off status string |
|
||
| `000e:9c9c` | `"TARGETING RETICLE ACTIVE."` | Correlates to event `0x441` / byte `0xee0` toggle |
|
||
| `000e:9cb6` | `"TARGETING RETICLE INACTIVE."` | Paired off-state |
|
||
| `000e:9cd2` | `"CD TRANSFER DISPLAY ACTIVE."` | Directly matches live event `0x410` / `DS:0x604f` toggle |
|
||
| `000e:9cee` | `"CD TRANSFER DISPLAY INACTIVE."` | Paired off-state |
|
||
| `000e:9dff` | `"HACK MOVER ON"` | No static code xref; USECODE/scripting layer |
|
||
| `000e:9e0d` | `"HACK MOVER OFF"` | No static code xref; USECODE/scripting layer |
|
||
| `1478:2850` | `"Immortality disabled."` | Used by the modifier-gated F10 immortality branch in `Key_HandleOptionKeys` |
|
||
| `1478:2866` | `"Immortality enabled."` | Used by the modifier-gated F10 immortality branch in `Key_HandleOptionKeys` |
|
||
| `000e:647b` | `"Cheats are now active."` | Shown in `-laurie` startup path |
|
||
| `000e:6492` | `"Cheats are now inactive."` | Paired off-state |
|
||
|
||
### Cheat event dispatch summary (000c segment)
|
||
|
||
All cheat-related event case-handlers reside as shared-frame case bodies within a large event dispatch function in segment 000c. Each body inherits BP from the enclosing prologue and exits via `POP DI; POP SI; LEAVE; RETF`.
|
||
|
||
| Address | Symbol | Event | Action |
|
||
|-------------|------------------------------------------------|---------|--------|
|
||
| `000c:8e16` | `event_0x441_cheat_debug_overlay_toggle` | `0x441` | Toggles `DS:0xee0` (boolean-NOT); calls `[0x2bd8]` vtable `+0x2c`; gate = `DS:0x844` |
|
||
| `000c:8e46` | `event_0x241_cheat_debug_overlay_toggle` | `0x241` | Toggles `DS:0x2bc9` (1-current); same vtable dispatch; gate = `DS:0x844` |
|
||
| `000c:8e72` | `event_0x141_cheat_debug_overlay_toggle` | `0x141` | Toggles `DS:0x2bca` (1-current); same vtable dispatch; gate = `DS:0x844` |
|
||
| `000c:942d` | `event_0x7e_cheat_latch_runtime_toggle` | `0x7e` | Requires `0x844 != 0`; flips live latch `DS:0x6045`; notification at `DS:0x6087` (on) or `DS:0x6091` (off) |
|
||
| `000c:9154` | `event_0x142_cheat_fullscreen_mode1_refresh` | `0x142` | Gate = `DS:0x604b`; palette-black, seg126 shell, mode-1 `000c:3c0e`, tail `0004:70f1` |
|
||
| `000c:92cd` | `event_0x143_cheat_fullscreen_mode0_refresh` | `0x143` | Same as `0x142` but mode-0 `000c:3c0e`, tail `0004:6f15` |
|
||
| `000c:9703` | `event_0x410_cd_transfer_display_toggle` | `0x410` | Toggles `DS:0x604f` / `g_cdTransferDisplayActive`; notification at `DS:0x60d2` (active) or `DS:0x60ee` (inactive); gate = `DS:0x844` |
|
||
|
||
### Cheat-dispatch keyboard functions (seg007)
|
||
|
||
| Address | Name | Description |
|
||
|-------------|-----------------------------------------------|-------------|
|
||
| `0007:04dc` | `keyboard_input_cheat_dispatch` | Processes one keyboard event: calls `cheat_code_check`, then dispatches on raw scan-code `[record+1]`. Tab/J (0x0f/0x24) → context-sensitive entity action via FUN_0005_e119/252; KP* (0x37) → `cheat_entity_slot_cycle_and_update_sprite`; Space (0x39) → movement/entity_command_dispatch; KP- (0x4a) → `cheat_anim_type_cycle_and_refresh`; KP+/KP0/KPDel (0x4e/0x52/0x53) → selected object vtable `+0x18`(0xb,...) dispatch. ASCII H (0x48) absent; HACK MOVER comes from a higher scripting layer. |
|
||
| `0007:0d0a` | `cheat_code_check` | Five-byte stateful matcher at `DS:0x2833`; toggles `0x844`+`0x6045`; cheats-on notification via `display_null_check_dispatch(..., 0x287b)`; cheats-off via `display_null_check_dispatch(..., 0x2892)`. |
|
||
|
||
### Cheat-dispatch helpers (000c:81xx)
|
||
|
||
| Address | Name | Description |
|
||
|-------------|-----------------------------------------------|-------------|
|
||
| `000c:8072` | `cheat_entity_slot_cycle_and_update_sprite` | Cycles slot 1..5 for `0x7e22` entity; picks sprite ID by class flags; calls `entity_table_set_sprite`. |
|
||
| `000c:81c0` | `cheat_anim_type_cycle_and_refresh` | Cycles animation-type 0x0b..0x19 for `0x7e22`; writes per-entity `+0x19`; calls `0008:4bba(0x20)`. |
|
||
| `000c:8221` | `cheat_flag_6050_clear` | Clears `DS:0x6050` to 0. |
|
||
| `000c:8227` | `cheat_flag_6050_read` | Returns `DS:0x6050` in AL. |
|
||
| `000c:822b` | `cheat_flag_6050_set` | Sets `DS:0x6050` to 1. |
|
||
|
||
### Additional cheat-dispatch hotkeys in `keyboard_input_cheat_dispatch`
|
||
|
||
Verified byte tests in the caller-side dispatch:
|
||
- `0x37` calls `000c:8072` (`cheat_entity_slot_cycle_and_update_sprite`)
|
||
- `0x4a` calls `000c:81c0` (`cheat_anim_type_cycle_and_refresh`)
|
||
- `0x0f` and `0x24` share a context-sensitive branch via `FUN_0005_e252` with event IDs `0x3a`, `0x38`, or `0x0b`
|
||
- `0x39` and `0x52` share a branch computing a queued delta via `entity_command_dispatch`
|
||
- `0x4e` and `0x53` are separate guarded selected-object lanes dispatching through the selected object's method table
|
||
|
||
### F10 immortality vs Ctrl+Q CD transfer display
|
||
|
||
The live NE decompile now separates these behaviors cleanly.
|
||
|
||
1. **Direct keyboard immortality lane**: inside `Key_HandleOptionKeys` (`1130:0896` / live F10 branch at `1130:0a36`), once full keyboard cheats are active through `0x6045`, the modifier-gated F10 sub-branch toggles the current controlled NPC's immortal flag directly via `NPC_GetIsImmortal` / `NPC_SetImmortal` / `NPC_ClearImmortal`. Live runtime testing now says the practical gesture is hold `F10` first and then press physical `Ctrl`.
|
||
2. **Cheat-only CD transfer display lane**: event `0x410` reaches the compiled handler at `000c:9703` (`13e8:2303` live), which toggles `DS:0x604f` / `g_cdTransferDisplayActive` under the broader `0x844` gate and posts the strings at `1478:60d2` / `1478:60ee`:
|
||
- `"CD TRANSFER DISPLAY ACTIVE."`
|
||
- `"CD TRANSFER DISPLAY INACTIVE."`
|
||
|
||
That means the older disasm scratch note in `crusader-disasm/misc_crusader_notes.txt` (`CTRL-Q = 0x410`) now lines up well with the user's runtime observation: **Ctrl+Q is the historical control-key note for the CD transfer display toggle, not for immortality**.
|
||
|
||
The immortality status strings are separate. The live NE decompile plus disassembly of `Key_HandleOptionKeys` directly shows the modifier-gated F10 immortality branch using:
|
||
|
||
- `"Immortality enabled."` at `1478:2866`
|
||
- `"Immortality disabled."` at `1478:2850`
|
||
|
||
The same compiled proof also sharpens the modifier claim beyond the earlier folklore-level read:
|
||
|
||
- `1130:0afd` calls `KeyEvent_IsAltDown` inside the F10 cheat branch.
|
||
- The F10 branch then chooses between the `1478:2850` and `1478:2866` strings.
|
||
- There is no `KeyEvent_IsCtrlDown` test anywhere in that F10 branch.
|
||
- The `KeyEvent_IsCtrlDown` call at `1130:0cad` belongs to a later, unrelated branch in `Key_HandleOptionKeys`, not to F10.
|
||
|
||
Current strongest read:
|
||
|
||
- The recovered compiled path still reaches a single modifier helper from the F10 immortality branch rather than testing both modifier families in that branch.
|
||
- Live runtime behavior now says the practical gesture is `F10`-then-`Ctrl` once the `0x6045` cheat latch is active.
|
||
- `Ctrl+Q` / event `0x410` toggles the CD transfer display state instead.
|
||
- If `jassica16` has been entered and the F10 immortality gesture still appears dead in live play, the most likely compiled-code explanations are: the scan-code matcher never actually completed, the cheat latch was toggled back off, the broader gameplay-input state at `0x85f` is down because the game is in a modal/non-gameplay state, or the key never reaches the game at all.
|
||
- The most practical runtime confirmation point is the cheat notification: a successful matcher pass goes through `Key_CheckCheatToggle` and pushes `DS:0x287b` (`Cheats are now active.`) or `DS:0x2892` (`Cheats are now inactive.`). If that notification does not appear, the latch almost certainly never changed.
|
||
|
||
**Secondary handler (000b:b62c):**
|
||
|
||
`usecode_debugger_handle_event` (`000b:b62c`, live `13a0:1df3`) receives event 0x410 through the registration installed by `usecode_debugger_gump_create` at `000b:b3b1` (`13a0:19b1`). When event 0x410 arrives, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to the shared debugger tail. This is a parallel state-machine path that runs alongside the 000c toggle; current best read is that it drives the hidden usecode-debugger state machine rather than a standalone cheat-only listener.
|
||
|
||
| Address | Symbol | Role |
|
||
|-------------|-------------------------------|------|
|
||
| `000b:b3b1` | `usecode_debugger_gump_create` | Allocates the seg109 debugger gump object, initializes its panes/watch state, and subscribes it to the shared debugger/control event bundle that includes `0x410`. |
|
||
| `000b:b62c` | `usecode_debugger_handle_event` | Debugger-side event mapper: rewrites incoming `0x410` to local state `0x0e` before entering the shared debugger state machine. |
|
||
| `DS:0x604f` | `g_cdTransferDisplayActive` | Set/cleared by event `0x410`. |
|
||
| `DS:0x60d2` | CD-transfer-on notification ptr | Near pointer in DS; resolves to far ptr → "CD TRANSFER DISPLAY ACTIVE." |
|
||
| `DS:0x60ee` | CD-transfer-off notification ptr | Near pointer in DS; resolves to far ptr → "CD TRANSFER DISPLAY INACTIVE." |
|
||
| `000a:b988` | `video_bios_state_snapshot` | Called after notification display in the 0x410 toggle to refresh screen state. |
|
||
|
||
### Hidden usecode debugger investigation (seg109 UI lane)
|
||
|
||
New compiled-side evidence shows that the older "hidden cheat menu" label was misleading. The recovered path is much closer to a hidden **usecode debugger / unit inspector** than to a retail cheat list.
|
||
|
||
The two address families that looked inconsistent in older notes are actually two connected layers of the same subsystem, not competing identifications:
|
||
|
||
- the `000b:*` / live `13a0:*` functions build and drive the modal debugger UI,
|
||
- the `1408:*` helpers hold breakpoint/callstack state,
|
||
- and the interpreter hook at `1418:04aa..04b5` ties the runtime back into that debugger state.
|
||
|
||
Combined debugger component table:
|
||
|
||
| Layer | Raw/reference anchor | Live NE Ghidra | Symbol | Verified role |
|
||
|-------|----------------------|----------------|--------|---------------|
|
||
| UI wrapper | `000b:9a86` | `13a0:0086` | `usecode_debugger_open_for_current_unit` | Builds the debugger gump in current-unit mode, resolves the active unit name from the live debugger state at `0x659c/0x659e`, loads the corresponding usecode file, centers on the current line, and enters the modal debugger UI. |
|
||
| UI wrapper | `000b:9c0d` | `13a0:020d` | `usecode_debugger_open_modal` | Smaller generic modal wrapper for the same debugger gump; it skips current-unit preload and simply opens the modal UI. |
|
||
| UI constructor | `000b:b3b1` | `13a0:19b1` | `usecode_debugger_gump_create` | Allocates the root debugger gump, builds the menu bar and panes, initializes watch state, resolves the base `usecode` path, and registers the shared debugger/control event bundle including `0x23f`, `0x410`, `0x411`, and `0x441`. |
|
||
| UI dispatcher | `000b:b62c` | `13a0:1df3` | `usecode_debugger_handle_event` | Main debugger event dispatcher. Recovered cases are debugger-style commands: open unit/file, go to line, watch, inspect, clear watches, change global, find/search again, and break to TDP. Incoming event `0x410` is remapped to local state `0x0e`. |
|
||
| UI menu builder | `000b:2882` | `13a0:2882` | `usecode_debugger_build_menubar` | Builds the hidden debugger menu bar with File, Run, Breakpoints, Search, and Data menus plus entries such as `Open Unit`, `View File`, `Run to cursor`, `Trace into`, `Step over`, `Run until return`, `Toggle F2`, `Break to TDP`, `Find`, `Search again`, `Go to line`, `Watch`, `Inspect`, `Change Global`, and `Quit`. |
|
||
| Break-state constructor | n/a | `1408:0000` | `usecode_debugger_break_state_create` | Allocates and initializes the seg1408 debugger-state object: vtable, breakpoint table, current-entry stack, and run/step flags. This is the object the rest of the breakpoint lane appears to expect at `0x659c/0x659e`. |
|
||
| Breakpoint gate | n/a | `1408:0053` | `usecode_debugger_maybe_break_on_current_line` | Stores the current interpreted line, resolves the active unit name through `1408:0444`, checks file+line breakpoints through `1408:029e`, evaluates run/step flags, and callbacks through the object's vtable when a break condition is met. |
|
||
| Breakpoint helper | n/a | `1408:00dd` | `usecode_debugger_breakpoint_insert_sorted` | Inserts `(file,line)` breakpoint entries into the seg1408 table in sorted order. |
|
||
| Breakpoint helper | n/a | `1408:029e` | `usecode_debugger_has_breakpoint` | Exact `(file,line)` membership test over the seg1408 breakpoint table. |
|
||
| Callstack helper | n/a | `1408:03b0` | `usecode_debugger_callstack_push_entry` | Pushes the current unit/line debugger entry onto the seg1408 callstack. |
|
||
| Callstack helper | n/a | `1408:03f7` | `usecode_debugger_callstack_pop_entry` | Pops one debugger callstack entry and asserts on underflow. |
|
||
| Step-state helper | n/a | `1408:0419` | `usecode_debugger_enable_single_step` | Arms the single-step mode flags in the debugger-state object. |
|
||
| Step-state helper | n/a | `1408:0432` | `usecode_debugger_clear_step_state` | Clears the break/step flags in the debugger-state object. |
|
||
| Current-entry helper | n/a | `1408:0444` | `usecode_debugger_current_entry_get_unit_name` | Returns the active unit/file name pointer from the current debugger entry stack. |
|
||
| Interpreter hook | n/a | `1418:04aa..04b5` | interpreter-side break callback site | Checks whether `0x659c/0x659e` is non-null, pushes the current interpreted line, then calls `1408:0053`. This is the concrete runtime handoff that links usecode execution to the hidden debugger state. |
|
||
|
||
This better explains the long-running negative result about the supposed scrollable cheat menu: the hidden UI we can actually recover is not a plain scrollable cheat list at all. It is a modal debugger/unit-inspector front-end that expects valid usecode file context and developer-style command routing.
|
||
|
||
#### Reachability status in retail binary
|
||
|
||
- Static constructor callsites for `usecode_debugger_gump_create` are exactly two locations: `000b:9a9b` and `000b:9c56`.
|
||
- Static inbound xrefs to the wrapper entries `000b:9a86` and `000b:9c0d` are currently empty in the recovered code graph.
|
||
- The cheat-code matcher `cheat_code_check` (`0007:0d0a`) toggles `0x844/0x6045` and emits event `0x103`; it does **not** call these menu wrappers directly.
|
||
- The 000c handler for `0x103` (`000c:99dd`) executes a status/refresh lane and notification path; no direct call to `usecode_debugger_gump_create` appears there.
|
||
|
||
Current best read: this debugger path is compiled and functional at object level, but likely orphaned/hidden in final gameplay flow (possibly dev-only entry removed, or only reachable through non-recovered data-driven callback wiring). That orphaned status is a better fit for the missing retail debugger entry than assuming a still-live player-facing scrollable cheat list.
|
||
|
||
#### Breakpoint callback lane (new strongest orphan candidate)
|
||
|
||
The strongest likely original entry point is no longer the cheat-toggle helper. It is the surviving **usecode breakpoint callback lane** summarized in the combined table above.
|
||
|
||
The key live proof is the interpreter-side handoff at `1418:04aa..04b5`:
|
||
|
||
- it first checks whether `0x659c/0x659e` is non-null,
|
||
- then pushes the current interpreted line,
|
||
- then calls `1408:0053`.
|
||
|
||
That means the live binary still contains a generic **"break here if debugger state exists"** lane in the usecode interpreter.
|
||
|
||
Current best orphan model:
|
||
|
||
- `0x659c/0x659e` is not just passive current-unit metadata; it behaves like the far pointer to the seg1408 debugger-state object.
|
||
- The seg109 UI wrappers consume that same object shape naturally. `usecode_debugger_open_for_current_unit` (`13a0:0086`) is especially consistent with this model because it expects a live current-unit state, resolves the active unit filename, loads the corresponding usecode file, centers on the current line, and then enters the modal UI.
|
||
- Direct static inbound xrefs to `13a0:0086` / `13a0:020d` can therefore stay empty even if the original debugger entry was real, because the missing handoff could have been **callback/vtable based** rather than a direct `CALLF` to the wrapper.
|
||
- The current negative result on `usecode_debugger_break_state_create` is important too: neither the live instruction search nor the repo relocation corpus currently shows a surviving retail constructor call for `1408:0000`. If that object is never instantiated and stored into `0x659c/0x659e`, the interpreter breakpoint hook stays compiled but dormant, which is exactly the orphan pattern now seen in the retail binary.
|
||
|
||
This moves the strongest likely original entry point from "cheat code success calls the menu directly" to **"a debugger-state object at `0x659c/0x659e` used the seg1408 breakpoint callback path to reach the seg109 current-unit debugger UI, but the retail build no longer instantiates or wires that object"**.
|
||
|
||
Practical force-enable paths now split more cleanly:
|
||
|
||
1. **Closest to the apparent original design:** instantiate `usecode_debugger_break_state_create`, store the far pointer into `0x659c/0x659e`, and ensure its callback/vtable target opens `13a0:0086` or `13a0:020d`. Then let the existing interpreter callback at `1418:04b5` trip normally.
|
||
2. **Executable patch near the surviving hook:** patch the seg1408 callback target or the `1418:04b5` breakpoint handoff so a valid debugger-state object enters the seg109 UI when a line/step condition fires.
|
||
3. **Blunt modal force-open:** keep using the already-documented cheat/event retarget experiments (`1130:2b78`, `13e8:25e0`) when the goal is only to prove UI reachability, not to reconstruct the original control flow.
|
||
|
||
#### Usecode-script viability as an alternative entry path
|
||
|
||
Cross-referencing the live NE work, the `crusader-disasm` usecode listings, and the ScummVM Crusader intrinsic table now gives a more precise answer to "can usecode do this?": **partially, but probably not directly enough to replace an EXE-side debugger-state fix**.
|
||
|
||
What the current evidence says:
|
||
|
||
- Crusader usecode clearly can open several normal modal UI paths. The ScummVM Remorse intrinsic table exposes `CruStatusGump::I_showStatusGump` (`Intrinsic05F`), `KeypadGump::I_showKeypad` (`Intrinsic0C4`), `ComputerGump::I_readComputer` (`Intrinsic0FE`), and `WeaselGump::I_showWeaselGump` (`Intrinsic134`). So a script hack that opens an ordinary gump is completely plausible.
|
||
- The same table does **not** expose anything that reads like "open usecode debugger", "construct debugger state", or "register breakpoint callback". That matters because the strongest compiled-side entry model now depends on the missing seg1408 debugger-state object at `0x659c/0x659e`, not just on some generic modal UI primitive.
|
||
- The compiled-side breakpoint lane still expects a live debugger object. `1418:04aa..04b5` only reaches `usecode_debugger_maybe_break_on_current_line` when `0x659c/0x659e` is already non-null, and the retail binary still has no recovered constructor path for `usecode_debugger_break_state_create`. Nothing in the current usecode evidence shows a script-visible way to instantiate that object or store it into the required global far pointer.
|
||
- The script/event frontier remains data-driven rather than explicit. Extracted usecode still points to `EVENT`, `_BOOT`, and especially `NPCTRIG` as the strongest active-event families, while `SURCAMNS` / `SURCAMEW` remain callback-style `eventTrigger` holders rather than proven active-event cores. The direct body scan also still finds no inline `0x0410` / `0x00000410` literal in `EVENT`, `NPCTRIG`, `SPECIAL`, or `TRIGPAD`, so the existing `Ctrl+Q` / `0x410` lane does not currently look like a plain script literal we can just drop into a chest body.
|
||
|
||
Accessible object candidates do still matter, but they split into better and worse testbeds:
|
||
|
||
- `MONITNS` / computer-adjacent objects are the best script-side probe. `crusader-disasm` places the first monitor at item `9254`, shape `258`, coordinates `(60798,59518,24)`, and its `MONITNS::use()` body is live. That family is already adjacent to normal computer/camera UI behavior, which makes it a better fit for testing whether usecode can be coerced into a hidden developer-facing UI transition.
|
||
- `SURCAMNS` / `SURCAMEW` are also stronger than a chest for experimentation. Their usecode bodies already move the camera and spawn follow-up ordinals, and descriptor-side work shows explicit `eventTrigger` fields plus repeated callback-oriented bodies. Even so, the current extractor evidence still treats them as callback holders, not as proven direct emitters of the seg109 debugger/control bundle.
|
||
- `NPCTRIG` remains the strongest compact event-bearing family if the real route is "usecode event machinery eventually reaches a hidden control path." Its slot `0x0A` body is the best surviving active-event frontier, but current binary work still bottoms out at "decoded VM workspace / caller stream" rather than a direct, script-readable `open debugger` operation.
|
||
- `CHEST_EW` is a weak candidate if the goal is specifically to reach the hidden debugger. The first chest is easy to reach, but its `CHEST_EW::use()` body mostly does chest animation, audio, waits, and a `FREE::ordinal2D` object-creation path. That makes it fine as a general proof-of-hack host, but not an evidence-backed match for the debugger/control lane.
|
||
|
||
Current best practical answer:
|
||
|
||
- **Viable for experimentation:** yes. A usecode mod can almost certainly be attached to an accessible early object, and a monitor/computer-style object is a better host than a chest.
|
||
- **Viable as a clean direct debugger-launch substitute for EXE patching:** not yet. The strongest known hidden-debugger entry still depends on missing compiled-side debugger state, and current usecode research has not surfaced a script-visible primitive that recreates that state or calls the seg109 debugger wrappers directly.
|
||
- **Most defensible usecode-first experiment:** hijack an early monitor / computer-adjacent use handler (`MONITNS` first, `SURCAM*` second) and try to route it into an already-existing modal/UI-bearing engine path, while treating `NPCTRIG` / `EVENT` as the deeper data-driven frontier if the real control path turns out to be event-mediated.
|
||
|
||
#### Retail patch-targeting trail
|
||
|
||
The practical patch work ended up being mostly about **finding a call site whose runtime context matches the hidden menu wrappers**, not just finding any place that reaches `000a:5276`.
|
||
|
||
Verified retail anchor points:
|
||
|
||
| File off | Ghidra | Meaning | Notes |
|
||
|----------|--------|---------|-------|
|
||
| `0x70d75` | `0007:0d75` | cheat matcher emits event `0x103` | retail bytes = `68 03 01 9A FF FF 00 00 83 C4 02`; NE fixup source = `0007:0d79` -> `seg092:0476` |
|
||
| `0x71d68` | fixup entry for `0007:0d79` | seg039 relocation record | exact retail entry: addr_type `0x03`, rel_type `0x00`, chain_off `0x2b79`, target `seg092:0476` |
|
||
| `0xc99dd` | `000c:99dd` | later controller-side handler that also executes `push 0x103 / call 000a:5276` | retail fixup source = `000c:99e0` -> `seg092:0476`; this is the first materially safer deferred hook candidate after the direct matcher path failed |
|
||
| `0xb9a8d` | `000b:9a8d` | arg setup inside `usecode_debugger_open_for_current_unit` | wrapper-local constructor setup uses caller stack words `[BP+8]` and `[BP+6]` plus the current-unit mode flag `1` before calling `usecode_debugger_gump_create` |
|
||
| `0xb9c48` | `000b:9c48` | modal wrapper prologue; the inherited caller-word patch subsite is `000b:9c4e` / live `13a0:024a` | wrapper-local setup still feeds caller stack words `[BP+8]` and `[BP+6]` into `usecode_debugger_gump_create`, but starts with local defaults `-1`, `-1`, `0` |
|
||
|
||
#### Live NE `CRUSADER.EXE` mapping in Ghidra
|
||
|
||
The older file offsets and raw-style segment addresses remain useful provenance, but the patch should now be planned against the live NE program that is open in Ghidra.
|
||
|
||
The following locations are confirmed directly in the live `CRUSADER.EXE` listing:
|
||
|
||
| Live NE Ghidra | Raw/reference anchor | Meaning |
|
||
|----------------|----------------------|---------|
|
||
| `1130:2b75` | `0007:0d75` | `cheat_code_check` success lane: toggles `0x844/0x6045`, then emits event `0x103` via existing `CALLF 12d8:0476` at `1130:2b78` |
|
||
| `13a0:0086` | `000b:9a86` | `usecode_debugger_open_for_current_unit`; larger hidden debugger wrapper and current best direct retarget target |
|
||
| `13a0:008d` | `000b:9a8d` | current-slot constructor arg site: `PUSH 1`, `PUSH [BP+8]`, `PUSH [BP+6]`, `PUSH 0`, `PUSH 0`, `CALL 13a0:19b1` |
|
||
| `13a0:020d` | `000b:9c0d` | `usecode_debugger_open_modal`; smaller modal wrapper |
|
||
| `13a0:0244` | `000b:9c48` | modal wrapper prologue; inherited caller-word patch subsite is `13a0:024a` |
|
||
| `13a0:19b1` | `000b:b3b1` | `usecode_debugger_gump_create`; registers the shared debugger/control event bundle including `0x410` |
|
||
| `13a0:1df3` | `000b:b62c` | `usecode_debugger_handle_event`; debugger-side dispatcher that remaps incoming `0x410` to local state `0x0e` |
|
||
| `13e8:2303` | `000c:9703` | compiled CD transfer display toggle handler for event `0x410`; boolean-toggles `DS:0x604f` / `g_cdTransferDisplayActive` and posts the active/inactive notifications |
|
||
| `13e8:25dd` | `000c:99dd` | deferred controller-side `0x103` lane; the live call opcode begins at `13e8:25e0` and prior `0x42f` retarget tests hit the retail `FLEX.C` line 83 failure |
|
||
|
||
Provenance split:
|
||
|
||
- `crusader-disasm` and the older retail-offset patch notes were used only to recover candidate lanes and preserve file-format history.
|
||
- The target selection above is confirmed from the live NE `CRUSADER.EXE` disassembly and comments now stored in Ghidra itself.
|
||
|
||
Live cheat-data anchors now comment-backed in Ghidra:
|
||
|
||
| Live NE data | Meaning |
|
||
|--------------|---------|
|
||
| `1020:2833` | 5-byte cheat matcher table consumed by `cheat_code_check` |
|
||
| `1020:283d` | cheat matcher index/state byte advanced during sequence validation |
|
||
| `1020:0844` | `cheats_enabled` gate byte checked before event `0x410` can toggle the CD transfer display |
|
||
| `1020:6045` | status/mirror byte updated alongside `1020:0844` when the cheat matcher succeeds |
|
||
| `1020:604f` | `g_cdTransferDisplayActive`; toggled by the compiled `0x410` handler |
|
||
| `1020:6050` | secondary cheat-related state from the older activation lane; distinct from the `0x410` CD transfer display toggle |
|
||
|
||
One remaining function-hygiene caveat:
|
||
|
||
- The live `0x410` handler body at `13e8:2303` is comment-backed and behaviorally clear, but it still sits inside the oversized `World_HandleKeyboardInput_13e8_14b4` function object in the current NE database. That is why this batch documents the handler in place instead of forcing a boundary repair just to land a new function name.
|
||
|
||
What failed and why:
|
||
|
||
- Direct retarget of `0007:0d79` to `000b:9a86` crashed at startup when the NE relocation table was patched incorrectly as a raw far pointer. That was a file-format problem, not a semantic proof.
|
||
- After the patcher was made NE-fixup-aware, direct retarget to `000b:9a86` no longer broke startup, but the game hung when the cheat actually fired. Disassembly shows why: `usecode_debugger_open_for_current_unit` consumes caller-supplied words at `[BP+8]` and `[BP+6]`, so the cheat matcher context is the wrong stack shape.
|
||
- Retargeting the same early cheat-matcher call to `000b:9c0d` got farther: the mouse pointer appeared, proving the hidden menu/display path was being entered. But it still hung with looping music, which points to **timing/context**, not a bad target address. The modal path appears unsafe when entered directly from the keyboard matcher even after the constructor args are forced to zero.
|
||
- The narrower direct current-slot patch was then runtime-tested on `/Writable/CRUSADER-PATCHED.EXE` with bytes verified as `1130:2b78 = 9A 86 00 A0 13` and `13a0:008d = 6A 01 6A 00 6A 00 90 90`. User test result: the normal cheat-toggle path still appeared, but no hidden menu appeared. That closes the direct current-slot route as a practical candidate, not just a theoretical one.
|
||
|
||
Current best patch rationale:
|
||
|
||
- `0007:0d75` remains the verified cheat-sequence success site, but the direct `1130:2b78 -> 13a0:0086` retarget is no longer the best live patch because it has now failed both analytically and at runtime.
|
||
- The first materially safer deferred hook remains the controller-side `000c:99dd` lane, where the live call opcode begins at `13e8:25e0`. That path preserves the real `0x103` event context instead of substituting `0x42f`, which is the strongest evidence-backed difference from the rejected deferred experiment.
|
||
- The chosen writable patch therefore restores `1130:2b78` to `CALLF 12d8:0476`, restores `13a0:008d` to the original current-slot wrapper bytes, retargets `13e8:25e0` to `13a0:020d` (`usecode_debugger_open_modal`), and zeros only the inherited caller-word pushes at `13a0:024a` while preserving the modal wrapper's leading local defaults (`PUSH -1`, `PUSH -1`, `PUSH 0`).
|
||
- The deferred `0x42f` branch remains negative evidence only: it proved the modal wrapper can enter the hidden UI path, but it also proved that substituting the event id or landing in the wrong deferred context trips the retail `FLEX.C` failure.
|
||
|
||
Rejected follow-up patch design:
|
||
|
||
- Site 1 tried changing `0007:0d75` from `push 0x103` to `push 0x42f`, keeping the original event-dispatch helper call intact.
|
||
- Site 2 retargeted the `000c:99e1` relocation so the `0x42f` handler's internal `push 0x103 / call 000a:5276` sequence called `usecode_debugger_open_modal` instead.
|
||
- Site 3 patched `000b:9c48` from `6A 00 FF 76 08 FF 76 06` to `6A 00 6A 00 6A 00 90 90`.
|
||
|
||
Observed result on retail test build:
|
||
|
||
- The game no longer failed at startup, and the mouse pointer appeared when the cheat fired, confirming that the hidden modal UI path was being entered.
|
||
- But the game then halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit/teardown path (`"No pity. No mercy. No remorse."`).
|
||
- That is strong evidence that event `0x42f` is the wrong deferred hook context for this experiment even though the retargeted address itself was valid enough to enter the UI path.
|
||
|
||
Current Ghidra-side patch plan for a copy:
|
||
|
||
1. Open the writable `/Writable/CRUSADER-PATCHED.EXE` program in Ghidra or PyGhidra, not the raw full-EXE database.
|
||
2. Restore the disproven direct-hook sites: `1130:2b78` back to `9A 76 04 D8 12` (`CALLF 12d8:0476`) and `13a0:008d` back to `6A 01 FF 76 08 FF 76 06`.
|
||
3. Navigate to the later controller-side `0x103` lane at `13e8:25e0` and retarget that `CALLF 12d8:0476` operand to `13a0:020d` (`usecode_debugger_open_modal`), yielding bytes `9A 0D 02 A0 13`.
|
||
4. Navigate to `13a0:024a` inside `usecode_debugger_open_modal`. Replace only the inherited caller-frame pushes with `PUSH 0` / `PUSH 0` (`6A 00 6A 00 90 90`) and leave the leading `PUSH -1`, `PUSH -1`, `PUSH 0` defaults intact.
|
||
5. Do not reintroduce the `0x42f` substitution or the direct `13a0:0086` current-slot hook in the same test build. They are now negative evidence, not live candidates.
|
||
|
||
These edits are now applied and byte-verified on `/Writable/CRUSADER-PATCHED.EXE`. The live NE `CRUSADER.EXE` analysis database remains documentation-only for this batch.
|
||
|
||
Rationale for the revised wrapper patch:
|
||
|
||
- Earlier direct-hook attempts proved that inheriting the two caller-frame words at `000b:9a8f/9a92` is unsafe from the cheat matcher context.
|
||
- But later decompilation of `usecode_debugger_gump_create` showed that the leading `push 0x1` at `000b:9a8d` is a distinct current-unit mode byte used by the constructor path, so zeroing all three pushed values was too aggressive.
|
||
- The current patch therefore preserves the leading `1` and only forces the two ambiguous 16-bit parameters to zero.
|
||
|
||
Risk notes:
|
||
|
||
- These remain behavioral exploration hacks, not correctness fixes.
|
||
- The evidence now strongly suggests the hard part is runtime context and event timing, not discovering the retail file offsets.
|
||
- If the revised direct `0007:0d79 -> 000b:9a86` path with the narrower `000b:9a8d` wrapper patch still fails, the next step should be a queue/defer design or a trampoline/cave patch rather than another blind event substitution.
|
||
|
||
### Conservative folklore verification
|
||
|
||
- "Cheats can be enabled with `-laurie`" is **directly verified**.
|
||
- "There is a hidden five-byte matcher that toggles cheats" is **directly verified**.
|
||
- "F10 performs a large cheat-only restore/reset action" is **directly verified**.
|
||
- The current live NE decompile alone does not cleanly settle the physical Ctrl-vs-Alt labeling without the runtime correction.
|
||
- "The F10 immortality branch directly toggles the current controlled NPC's immortal flag once full keyboard cheats are active" is now **directly verified** in `Key_HandleOptionKeys`.
|
||
- Live runtime testing now says the practical physical input is `F10`-then-`Ctrl`, so the current helper naming should not be treated as definitive physical-key proof on its own.
|
||
- "Ctrl+Q shows `CD TRANSFER DISPLAY ACTIVE.` when cheats are enabled" now matches the live NE `0x410` handler and the historical `crusader-disasm` control-key note.
|
||
- "H enables hack mover" is **real at runtime** (strings confirmed), but not found in the static low-level byte dispatch; the activation comes from the USECODE scripting layer.
|
||
- "Immortality makes the player invincible" is **partially verified**: damage is divided by 262,144, making HP loss negligible; the hit stagger still plays. There is no bypass of the HP system entirely.
|
||
- "Event 0x410 is emitted by a recovered static keyboard path" is still **not supported** in compiled C code.
|
||
- "There is no keyboard immortality combo at all" is now **false**: the live NE controller option-key handler directly verifies a modifier-gated F10 keyboard immortality toggle once the `0x6045` cheat latch is active, and live runtime testing shows the practical gesture is `F10`-then-`Ctrl`.
|
||
- `TELEPAD` slot `0x20` in `class_event_index.tsv` is **not** direct `0x410` event evidence; its `0x00000410` value is the extracted class-body offset for that slot.
|
||
- Among the requested USECODE families, `NPCTRIG` is the strongest remaining player-trigger candidate because it is explicitly event-bearing and also has extracted callable bodies, while `TRIGPAD`, `SPECIAL`, and `REB_PAD` currently read as neighboring referent/state/controller bodies rather than direct event carriers.
|
||
- The hidden five-byte matcher compares bytes from live code at `0007:2833`, and the ordinary keyboard ISR producer does not naturally emit byte values `0x80` and `0xfd` into record byte `+1`.
|