Crusader_Decomp/docs/ne-segment1.md

74 KiB
Raw Permalink Blame History

Crusader: No Remorse — NE Segment 1 Game Logic

This file covers the standalone analysis of NE Segment 1 (seg001_code_off_37600_len_8400.bin), imported as a raw binary at base 0x0000, language x86:LE:16:Protected Mode. All 35+ identified functions have been renamed and annotated in Ghidra.

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 (0x00600x0d5f)

Address Name Description
0x0060 cursor_update_hover Hover update: if mouse active & entity set, calls cursor_set_target
0x00e9 cursor_set_target Positions cursor on entity, updates sprite + direction visual
0x0322 cursor_shutdown Frees cursor resources, resets state
0x0398 cursor_animation_update Angle-based cursor rotation (0x27d4, 0-359 → 0x168=360). Sprite at 0x27d6
0x050f cursor_draw_tick Per-frame cursor draw (calls cursor_animation_update if dirty)
0x0c24 action_key_valid Returns 1 if action code (param_1) is a valid game action key
0x0d5f cursor_direction_input Arrow-key input: rotates cursor angle, updates direction sprite

Input Handling

Address Name Description
0x0526 input_keyboard_handler Key dispatch: 0x01=LMB, 0x0D/0x0C=scroll, 0x2C=save, 0x44=load

Cursor State Data (at DS:0x27xx)

Address Field Meaning
0x27c4 cursor_sel1 Selection counter 1
0x27c6 cursor_sel2 Selection counter 2
0x27c8 current_entity Handle to currently targeted entity
0x27ca0x27ce cursor_state Cursor interaction state bytes
0x27d0 cursor_entity_type Current entity type index
0x27d2 z_offset Z-height offset for terrain adjustment
0x27d4 cursor_angle Rotation angle (0359)
0x27d6 cursor_sprite Sprite handle for cursor visual
0x27d8 cursor_dirty Set when cursor needs redraw
0x27d9 cursor_active Master cursor enabled flag
0x27da cursor_no_turn Flag disabling cursor rotation
0x27ed difficulty Enemy accuracy divisor (used in projectile_init_vector)
0x27fd hard_mode Two-step mode (combat vs. explore)
0x27fe move_mode Movement phase flag
0x27ff mouse_active Mouse/input system active
0x28000x2811 various UI state: active sprite, facing byte, cur entity handle
0x283f/0x2841 menu_obj_ptr Active menu/dialog object far pointer
0x2844 in_save In-progress save game flag
0x290e entity_count Number of active entities
0x29100x2947 snap_type_ids[10] Entity types that snap-to-ground in snap_entity_to_ground

Input / Action Dispatch

Address Name Description
0x2420 entity_command_dispatch Dispatches player commands to target entity; reads 0x27d0, 0x2de4, sends events 0x14/0xf, handles state machine 0x27ca/0x27cd/0x27ce
0x279a cheat_code_check Checks input record byte+1 against five bytes starting at 0x2833; toggles 0x844/0x6045, emits helper/event code 0x103

Menu / Event Callbacks

Address Name Description
0x2e53 cursor_event_notify_a Vtable thunk: forwards event to 0x27ca area handler
0x2e96 cursor_event_notify_b Vtable thunk: forwards event to 0x27ca area handler (alt path)
0x2ed9 menu_event_notify_a Vtable thunk: forwards event to 0x2843 (near menu object)
0x2f0c menu_event_notify_b Vtable thunk: forwards event to 0x2843 (alt path)
0x2ff3 stub_noop_2ff3 Empty stub, noop
0x2ff8 entity_collision_callback_a Calls touch handler then func(entity+0x1e, seg, 2); opt: extra func if param_3&1
0x3046 set_active_menu Writes param_1/param_2 to 0x283f/0x2841 (active menu far pointer)
0x3058 entity_collision_callback_b Same as entity_collision_callback_a (second vtable entry)

Entity System (0x24010x5a50)

Address Name Description
0x2401 clear_cursor_selection Zeros 0x27c4/0x27c6 (selection counters)
0x2899 cursor_switch_target_entity Switches cursor target: unloads old entity, loads new, re-registers
0x29d8 get_z_offset Returns func() + *(0x27d2) = adjusted Z/height
0x2a09 is_player_in_range Checks if entity is at player (0x2de4) X/Y +/-0xf0 range
0x2a46 entity_ai_update_loop Loops entities 2255, checks visibility, triggers fire/move
0x2c36 ui_update_callback Calls cursor_state_clear then vtable[2] on menu object
0x2c6b cursor_state_clear Clears cursor state bytes 0x27ca0x27ce, clears entity flag bit1
0x2c92 dialog_spawn Allocates dialog object, vtable=0x28b5, registers callback at 0x39ca
0x2d47 entity_pick_handler Handles entity selection or save-game trigger (type 0x38d)
0x2df9 clear_active_menu Zeros 0x283f/0x2841 (active menu far pointer)
0x2e18 game_mode_init Initializes game mode state, resets sprite/cursor/menu state
0x2f3f entity_table_set_sprite Reads 0x7df9+slot2; writes entity type table 0x7e1e[slot0x79+0x0d]=param_2, +0x10=0
0x3c97 snap_entity_to_ground If entity type in snap_type_ids[10], resets Z to 0xf0 and adjusts XY
0x3d6e spawn_entity_checked Spawns entity with explosion pool limit check (0x84c0, 0x84c2)
0x3f2f entity_spawn Allocates entity, vtable=0x29aa/0x39ca, positions it
0x40d4 entity_remove Removes entity: destroys sprites, clears 0x2802/0x2804 if needed
0x4172 entity_animation_frame_update Advances/retreats anim frame ([+0x1d]) toward target [+0x1c/0x1b] based on quality
0x42f8 stub_noop_42f8 Empty stub, noop
0x42fd entity_registry_decrement Calls cleanup func then decrements entity count at 0x290e
0x4314 entity_sprite_move_delta Updates shot sprite handle (entity+0x3f) position by adding delta params
0x4552 entity_set_position Sets entity+0x3e (type_handle), world_x/y (entity+0x45/47), base_x/y (entity+0x4f/51)
0x452b shot_set_spawn_pos Calls entity_set_position then sets entity+0xbe = param_3 (extra spawn field)
0x4591 entity_try_place entity_set_position with validation — position only set if placement succeeds
0x5092 entity_deactivate Calls vtable[2] to deactivate, or finds in registry and removes
0x5a50 entity_list_contains Checks if entity ptr exists in active entity list at 0x294c
0x5b05 stub_noop_5b05 Empty stub, noop

Entity Object Layout (NE Segment 1 entities)

Offset Field Meaning
+0x00 vtable_ptr Vtable pointer (0x29aa for generic, 0x2a57 for debris)
+0x02 slot_index Entity slot index (used for registry at 0x39ca)
+0x04 entity_type Entity type ID
+0x19/+0x1a flags Entity flags (bit0=debris, bit1=cleared by cursor_state_clear, bit6=active, bit8=valid)
+0x1b vel_x X velocity (clamped ±0x20)
+0x1c vel_y Y velocity (clamped ±0x20)
+0x1d vel_z Z velocity (clamped ±0x10)
+0x1e fire_handle Weapon/fire handle
+0x1f is_enemy 1 if entity is an enemy type
+0x20/+0x21 pos_frac_x/y Fractional position (sub-tile) for movement
+0x22 pos_frac_z Fractional Z
+0x36 weapon_type Active weapon type ID
+0x38 facing Current facing direction (015)
+0x3c sprite_handle Sprite for this entity
+0x3f shot_sprite Sprite handle for active projectile (0xFFFF = none)
+0x45/+0x47/+0x49 world_x/y/z Current world position (integer)
+0x4f/+0x51/+0x53 base_x/y/z Base/spawn position
+0x54/+0x56/+0x58 prev_x/y/z Previous frame position
+0x59 attack_active Attack in progress flag
+0x5a at_target Reached target flag
+0x5e+0x65 delta_x/y/z/high Per-step movement deltas (fixed point)
+0x66/+0x68 step_active Stepping active (1=yes, 0=off)
+0x6a/+0x6c weapon_slot/dist Weapon slot and total travel distance
+0x6e delta_z Alt Z delta
+0x70 projectile_type Projectile class (2/0xD=splash, 3=spread, 5=homing, 0xE=chain)
+0x72/+0x74/+0x76 target_x/y/z Target position with deviation
+0x77 target_entity Target entity handle
+0x79 secondary_pos Secondary position struct pointer
+0xad owner_entity Owning entity handle
+0xaf shot_owner_flags Shot owner (entity/player)
+0xb1 bounce_count Bounce counter (used with homing, type 5)
+0xb3 has_bounce Has bounce trajectory active
+0xbd actor_type Actor type byte (used for direction table lookups)

Shot Entity Lifecycle (0x435e0x44a9)

Address Name Description
0x435e shot_entity_alloc Alloc/init shot entity: vtable=0x297e, registry vtable=0x2969, zeros all state, copies player pos to +0xb5/b7
0x44a9 shot_entity_free Cleans up shot entity: frees sprite at +0x3c if valid (set to 0xFFFF), clears callbacks; optional full free if flag&1

Projectile / Combat (0x46590x5a99)

Address Name Description
0x4659 projectile_init_vector Sets up shot trajectory: target XY±deviation, step rate from weapon table at 0x2536
0x4a91 entity_fire_weapon Fires weapon from entity using 0x129b/0x12ac direction offset tables
0x4b18 fire_weapon_from_cursor Gets cursor angle sprites, fires projectile at cursor target
0x4b78 projectile_check_hit Hit test: if entity_type==0 uses bbox+0x79; else full 3D range; copies +0xa0→+0x77 (hit entity)
0x4c2e projectile_step_update Advances projectile one step; type 3 spawns sub-shots via spawn_entity_checked
0x4d28 projectile_trace_ray Interpolated path trace: divides distance/0x10 into steps, collision checks each step; on hit calls projectile_apply_hit + entity_deactivate
0x51ad projectile_update_tick Full projectile tick: move, check reach target, bounce, call projectile_check_hit
0x5a99 projectile_apply_hit Applies hit effects: if impacted obj byte+6 non-zero, calls damage func with weapon_slot/type/target/owner

Weapon Type Table (0x2536)

  • Each entry is 0x11 bytes (17), accessed as weapon_type * 0x11
  • [0] = step divisor for distance calculation
  • [0x19] = max range threshold (used in projectile_update_tick)

Direction Tables (0x129b / 0x12ac)

  • Indexed by facing (015): dx offsets at 0x129b, dy offsets at 0x12ac
  • Values are multiplied by distance (e.g. *0x500) for projectile spawn offsets

Collision Detection (0x60c10x621e)

Address Name Description
0x60c1 aabb_overlaps_3d 3D AABB overlap test — box layout [xmin,ymin,zmin,,,xmax,,ymax,,zmax]
0x621e bbox_translate Translates a 3D bounding box by (dx, dy, dz) — both min and max points

Enemy AI / Spawning (0x6aed0x6d21)

Address Name Description
0x6aed map_find_spawn_point Finds map tile matching entity conditions; returns packed XYZ tile coords
0x6bfc actor_find_in_view Finds actor visible in current view frustum (temp data at 0x7eca)
0x6ce9 enemy_spawn_with_target Wrapper: spawns enemy with player as target (param5=1)
0x6d05 enemy_spawn_no_target Wrapper: spawns enemy without targeting player (param5=0)
0x6d21 enemy_spawn_at_position Full enemy spawn: activates entity, assigns velocity from direction table (0x2a00/4/A)

Player / HUD

Address Name Description
0x50ee player_position_update Updates player position from direction data; clamps to screen bounds
0x6ff7 player_health_update_and_effect Encodes player HP into RGB bitfields at 0x7e46+0x1bec, spawns effect

Destruction / Death (0x74900x75ff)

Address Name Description
0x7490 debris_spawn Spawns debris/fragment: vtable=0x2a57/0x2a1a, velocity, facing, linked list
0x75ff entity_die Death handler: spawns 14 debris objects, picks best explosion direction

Entity Type Constants (weapon_type/entity class)

Value Entity Class
0x17 Robot/mech type A
0x18 Robot/mech type B
0x1 through 0x3c Various entity/weapon types
0x3d Robot/mech type C
0x3e Robot/mech type D
0x2f50x2f7 Special movement entity
0x595/0x597 Platform/elevator entities
0x31c/0x3220x327 Explosive/effect entities
0x38d Save game trigger entity
0x426 Spark/scatter sub-shot
0x59a Player cursor/select indicator

Entity Data Table at 0x7e1e

  • Stride: 0x79 bytes (121 bytes per entry)
  • Indexed by entity type (integer) or entity slot
  • +0x59 offset = class-detail flags byte (entity_class_get_flag8 returns bit 0x08; other callers also clear bit 0x10 here during at-target facing updates)
  • +0x5a offset = flags byte (bit4 = special entity flag, bit3 = armor/shield flag)
  • +0x68 = targeting flag

Map / Resource Tables

Address Content
0x2833 Cheat code input sequence (null-terminated)
0x283d Cheat sequence match position counter
0x7ded Map X coordinate array (2 bytes per entry)
0x7df1 Map Y coordinate array (2 bytes per entry)
0x7df5 Map Z array (1 byte per entry)
0x7df9 Entity state array (2 bytes per slot)
0x7e46 Player state block far pointer
0x7e1e Entity type table (stride 0x79)

Entity Vtable Index (NE Segment 1)

Address Entity Class
0x28b5 Dialog/menu object vtable
0x287b Cheat-spawned entity (cheat ON) vtable
0x2892 Cheat-spawned entity (cheat OFF) vtable
0x2969 Entity registry vtable (stored at 0x39ca+slot*4, not entity's own vtable)
0x297e Shot/projectile entity vtable
0x29aa Generic/AI entity vtable
0x2a1a Corpse entity vtable (variant)
0x2a33 Actor/corpse entity vtable
0x2a57 Debris fragment entity vtable

Cheat System Analysis

cheat_code_check internals

  • Direct raw-EXE recovery: cheat_code_check in CRUSADER-RAW.EXE is 0007:0d0a-0007:0e08.
  • It has exactly one direct caller in this build: FUN_0007_04dc at 0007:0511, which prepares a small local event record and then calls cheat_code_check before continuing normal input dispatch.
  • FUN_0007_04dc itself is not only a low keyboard-queue consumer: drawlist_init at 0007:f654 calls it directly during higher-level setup, so the cheat-dispatch body can be reached from at least one non-ISR path in this build.

Variable and constant roles from the recovered body:

  • 0x2833 is not a clean dedicated data object in this raw EXE — raw bytes and surrounding disassembly show that it lands in the middle of entity_animation_frame_update (0007:26e2-0007:2867). Starting on PUSH AX; CMP byte ptr [0x27fd],0; ... the first five bytes are 50 80 3e fd 27 followed by a 0x00.
  • 0x283d is the current match index into that byte table. On a successful byte match it increments; on mismatch it resets to zero and immediately retries the current input byte against the first table byte (overlapping prefixes still work).
  • input_event_record[+1] is the compared input/event token.
  • 0x844 is the main cheat-enable flag. The success path toggles it by converting the current byte to a boolean and negating it (0 -> 1, non-zero -> 0).
  • 0x6045 is written with the same post-toggle value as 0x844 — a mirrored cheat-state latch.
  • Constant 0x103 is pushed into the shared helper at 000a:5276 immediately after the toggle (emit the cheat-toggle side effect).
  • 0x8c52 is forced to 1 on success before the side-effect path continues.

After a full table match, the code resets 0x283d to zero, sets 0x8c52 = 1, toggles 0x844 and 0x6045, and calls the shared 0x103 helper. It then branches on the new cheat state: cheats-on uses DS:0x287b; cheats-off uses DS:0x2892.

Cheat-enable sources

Two independent cheat-enable sources verified:

  1. The hidden input matcher in cheat_code_check toggles 0x844 and 0x6045 after matching the five-byte event-code table at 0x2833.
  2. The command-line parser at 0004:635c-0004:63b8 recognizes the literal switch -laurie and directly sets 0x844 = 1. This path does not write 0x6045.

Current model for the two cheat bytes:

  • 0x844 = master cheat-permitted / cheat-framework-enabled flag.
  • 0x6045 = live cheat-active mirror latch used by low-level keyboard handling.
  • The hidden five-byte matcher enables both at once, but -laurie only enables the master flag.
  • The separate event-0x7e path at 000c:942d requires 0x844 != 0, flips 0x6045, and displays one of two local notification messages (0x6087 vs 0x6091).

jassica16 status:

  • No literal jassica string is present in the 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.
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.