Add PyGhidra Crusader Toolkit and patch scripts
- Introduced README.md for the PyGhidra Crusader Toolkit, detailing setup and usage instructions. - Added bootstrap_env.ps1 script to create and refresh the Python virtual environment with necessary packages. - Implemented _tmp_patch_hidden_cheat_menu.py and _tmp_patch_hidden_cheat_menu_deferred.py scripts for patching specific memory addresses in Ghidra.
This commit is contained in:
parent
fafd849beb
commit
ad6ebd0b86
132 changed files with 41758 additions and 99 deletions
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
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 |
|
||||
|
|
@ -273,16 +275,51 @@ Current model for the two cheat bytes:
|
|||
- The separate event-`0x7e` path at `000c:942d` requires `0x844 != 0`, flips `0x6045`, and displays one of two local notification messages (`0x6087` vs `0x6091`).
|
||||
|
||||
`jassica16` status:
|
||||
- No literal `jassica` string is present in the current string table, while `-laurie` is present as plain text.
|
||||
- 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; "Ctrl+F10 enables god mode" is **not** supported by the current code path.
|
||||
- "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.
|
||||
|
||||
### Cheat-related string table (seg014 / `000e:xxxx`)
|
||||
|
||||
| Address | String | Notes |
|
||||
|
|
@ -292,12 +329,12 @@ F10 key behavior (verified in raw build):
|
|||
| `000e:9c91` | `"CHEATS OFF"` | Cheat-off status string |
|
||||
| `000e:9c9c` | `"TARGETING RETICLE ACTIVE."` | Correlates to event `0x441` / byte `0xee0` toggle |
|
||||
| `000e:9cb6` | `"TARGETING RETICLE INACTIVE."` | Paired off-state |
|
||||
| `000e:9cd2` | `"CD TRANSFER DISPLAY ACTIVE."` | Correlates to event `0x241` / `0x141` toggle area |
|
||||
| `000e: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 |
|
||||
| `000e:6450` | `"Immortality disabled."` | No static code xref; USECODE/scripting layer |
|
||||
| `000e:6466` | `"Immortality enabled."` | 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 |
|
||||
|
||||
|
|
@ -313,7 +350,7 @@ All cheat-related event case-handlers reside as shared-frame case bodies within
|
|||
| `000c:942d` | `event_0x7e_cheat_latch_runtime_toggle` | `0x7e` | Requires `0x844 != 0`; flips live latch `DS:0x6045`; notification at `DS:0x6087` (on) or `DS:0x6091` (off) |
|
||||
| `000c:9154` | `event_0x142_cheat_fullscreen_mode1_refresh` | `0x142` | Gate = `DS:0x604b`; palette-black, seg126 shell, mode-1 `000c:3c0e`, tail `0004:70f1` |
|
||||
| `000c:92cd` | `event_0x143_cheat_fullscreen_mode0_refresh` | `0x143` | Same as `0x142` but mode-0 `000c:3c0e`, tail `0004:6f15` |
|
||||
| `000c:9703` | `event_0x410_cheat_flag_604f_toggle` | `0x410` | Toggles `DS:0x604f` (boolean-NOT); notification at `DS:0x60d2` (on) or `DS:0x60ee` (off); gate = `DS:0x844` |
|
||||
| `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)
|
||||
|
||||
|
|
@ -341,48 +378,36 @@ Verified byte tests in the caller-side dispatch:
|
|||
- `0x39` and `0x52` share a branch computing a queued delta via `entity_command_dispatch`
|
||||
- `0x4e` and `0x53` are separate guarded selected-object lanes dispatching through the selected object's method table
|
||||
|
||||
### Immortality mechanics (event 0x410 / flag 0x604f)
|
||||
### F10 immortality vs Ctrl+Q CD transfer display
|
||||
|
||||
**How immortality works at the C level:**
|
||||
The live NE decompile now separates these behaviors cleanly.
|
||||
|
||||
`DS:0x604f` is the Immortality flag. It is toggled by `event_0x410_cheat_flag_604f_toggle` at `000c:9703`.
|
||||
The sole gameplay read site is `player_receive_damage_and_dispatch_effects` (`0004:c055`) at `0004:c205`.
|
||||
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."`
|
||||
|
||||
When `0x604f != 0` (Immortality **ON**), the damage path in `0004:c205` does:
|
||||
1. `CALLF 0009:9ea1` — begin hit-effect lock (animation gating sequence)
|
||||
2. `CALLF 0003:c368(0x10001)` — arm anim-stagger mode (seg001:4d68 path)
|
||||
3. `IDIV 0x40000` — divide the 32-bit incoming damage value by **262,144** → result is effectively 0 for any realistic HP scale
|
||||
4. Apply the negligible reduced damage via `CALLF 0003:dbcc`
|
||||
5. Spin on `DS:0x31a2 != 0` event-break gate before re-enabling channels
|
||||
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**.
|
||||
|
||||
When `0x604f == 0` (Immortality **OFF**, normal path):
|
||||
- Jump to `0004:c25b` → `CALLF 0003:ac7e` (seg001:367e) — full damage / death dispatch
|
||||
The immortality status strings are separate. The live NE decompile plus disassembly of `Key_HandleOptionKeys` directly shows the modifier-gated F10 immortality branch using:
|
||||
|
||||
The hit stagger **still plays** in immortality mode (the Silencer visually flinches). Technically HP decreases by 0 per hit (integer truncation from /262144), so there is no true invulnerability flag that bypasses all HP accounting, just extreme attenuation.
|
||||
- `"Immortality enabled."` at `1478:2866`
|
||||
- `"Immortality disabled."` at `1478:2850`
|
||||
|
||||
**What sends event 0x410 to toggle it:**
|
||||
The same compiled proof also sharpens the modifier claim beyond the earlier folklore-level read:
|
||||
|
||||
The 000c event handler at `000c:9703` is entered via the large cheat-event dispatch switch at `000c:8c56-000c:8d16`. That switch is driven by the seg021 event scheduler, not by the static keyboard dispatch in `keyboard_input_cheat_dispatch`.
|
||||
- `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.
|
||||
|
||||
Key negative result: no function in the compiled C code directly pushes the value `0x410` into the game's event broadcast path. All three occurrences of the immediate `0x410` in the disassembly are: (a) the `CMP BX,0x410` comparison inside the 000c switch, (b) a multi-event subscription list at `000b:b5cb` (registering to receive the event), and (c) an abort-function error code at `000d:5290` unrelated to the cheat.
|
||||
Current strongest read:
|
||||
|
||||
The strongest new compiled-side recovery in this pass is the seg109 listener object behind that subscription site. `cheat_event_listener_create` at `000b:b3b1` allocates one listener object and registers the shared cheat/control event bundle (`0x13d`, `0x1b`, `0x443`, `0x142`, `0x141`, `0x143`, `0x23f`, `0x43e`, `0x41f`, `0x417`, `0x431`, `0x411`, `0x410`, `0x441`, `0x421`, `0x22d`) through the seg109 registration helper at `000b:3d2a`. Its paired `cheat_event_listener_handle_event` body at `000b:b62c` is subscriber-side only: for event `0x410` it rewrites the event object's field `+0x6` to local state `0x0e` and falls into the shared `FUN_000b_b7f3` state-processing tail. That listener does not produce event `0x410`; it only reacts after the event has already been emitted elsewhere.
|
||||
|
||||
The generic compiled dispatch path is one step tighter now too. The larger `000c:8a62` wrapper first peels off local gated cases, then falls into the generic cheat/control event dispatcher at `000c:8c56`, which reads `event_object->code` from field `+0x6` and switches over values like `0x141`, `0x142`, `0x143`, `0x23f`, `0x410`, `0x431`, `0x441`, and `0x443`. That makes the shared event-object contract explicit: `000c:8c56` consumes the original emitted event id from `+0x6`, while `cheat_event_listener_handle_event` reuses the same `+0x6` field as a local state/subcommand code before entering `FUN_000b_b7f3`.
|
||||
|
||||
One extraction-side false lead is now closed too: the `TELEPAD` row in `USECODE/EUSECODE_extracted/class_event_index.tsv` with `raw_code_offset = 0x00000410` is a class-body offset for slot `0x20`, not direct evidence that `TELEPAD` emits gameplay event `0x410`.
|
||||
|
||||
The requested USECODE family sweep also tightened the player-trigger side without closing it. Inside `class_event_index.tsv`, `NPCTRIG` is the only requested family that is both explicitly event-bearing at the descriptor level and also has non-empty callable bodies in the current event-slot extraction (`equip` / slot `0x0a` at raw offset `0x0175`, plus one anonymous slot `0x20` body at raw offset `0x0159`). `SPECIAL`, `TRIGPAD`, and `REB_PAD` all have non-empty callable bodies too, but they remain referent/state neighbors rather than direct event carriers: `SPECIAL` shows bodies for `equip`, `enterFastArea`, `leaveFastArea`, and anonymous slots `0x20/0x21`; `TRIGPAD` shows `gotHit`; `REB_PAD` shows `gotHit` and anonymous slots `0x20/0x21`. None of those extracted bodies currently expose a verified `0x410` immediate or decoded event payload.
|
||||
|
||||
Conclusion: event 0x410 is generated exclusively by the **interpreted USECODE lane** (centered on `EUSECODE.FLX`), not by any static keyboard-level scan-code path in the compiled binary. The F10 keyboard branch in `seg001_input_keyboard_handler` is a separate `0x44` path gated by `0x6045`, not by `0x410`. Separate follow-up work on the imported `ASYLUM.DLL` shows that DLL exports `ASS_*` audio routines, so it should not be conflated with the immortality toggle path. The in-game trigger is still best modeled as a USECODE item or controller script, consistent with the surrounding string evidence (`000e:6337 "CruHealer"`, `000e:6341 "BatteryCharger"`, `000e:6445 "Controller"`, `000e:64ab "AutoFirer"` — these are USECODE process class names bracketing the Immortality string). The new extractor-side report `USECODE/EUSECODE_extracted/immortality_target_body_scan.md` now scans the strongest current bodies in `EVENT`, `NPCTRIG`, `COR_BOOT`, `REE_BOOT`, `SFXTRIG`, `SPECIAL`, and `TRIGPAD` and finds no inline little-endian `0x0410`, no dword `0x00000410`, and no byte-swapped `0x1004` in any of them. That closes the immediate-emitter hypothesis for those currently exposed bodies and narrows the remaining frontier to data-driven decoding of the monolithic `EVENT` slot `0x0a` body and the compact `NPCTRIG` slot `0x0a` / `0x20` bodies, not to `TRIGPAD`, `SPECIAL`, `REB_PAD`, or `TELEPAD`.
|
||||
|
||||
The next extractor pass now pushes that one layer deeper. `USECODE/EUSECODE_extracted/immortality_body_structure.md` shows that `EVENT` slot `0x0a` is structurally a wide generic hub body, not a compact trigger leaf: it carries `90` internal `0x53 0x5c <u16> EVENT` subheaders, `383` local `0x5b` labels, and one wide tail-field set covering `event`, `item`, `source`, `dest`, `door`, `counter`, `counter2`, `link`, `time`, `post1`, `post2`, `floor`, and `flicMan`. By contrast, `NPCTRIG` stays compact and trigger-shaped. Slot `0x0a` has only `5` class-labelled subheaders and a narrow tail-field set (`referent`, `event`, `item`, `item2`), while slot `0x20` has only `1` such subheader and swaps the tail `event` field for `typeNpc` while keeping the same compact `item` / `item2` neighborhood. That is the strongest current player-trigger result: `EVENT` now reads as the generic event hub body, while the likeliest player-facing path is the `NPCTRIG` pair with slot `0x0a` as the compact event-bearing trigger body and slot `0x20` as its nearby typed/setup companion.
|
||||
|
||||
The next focused decode pass sharpens that split enough to treat the two `NPCTRIG` bodies differently instead of as one unresolved pair. New report `USECODE/EUSECODE_extracted/immortality_npctrig_clauses.md` fixes the open-header parse and shows that slot `0x0a` starts with `0x5A 0x06 0x5C 0x013E NPCTRIG ... 0x0B 0x11`, then falls into a five-step clause ladder with subheaders at `0x0064/0x0093/0x00c2/0x00f1/0x0120`. Those subheaders sit on a uniform `0x2f` stride, their targets walk backward by the same amount, and each full-width clause carries one `branch_3f_0a`, one `push_24_51`, and one `writeback_57_02`. Slot `0x20` is structurally different: its prolog ends with event-code byte `0x01`, it has only one class-labelled subheader, no `writeback_57_02`, no `push_24_51`, and ten `field_4b_fe_0f` hits clustered around repeated `0x0a 00/05 4b fe 0f ...` windows before the tail field `69:000a -> typeNpc`. That is the strongest current descriptor-side reduction of the search space: slot `0x0a` now reads like the live event-bearing clause ladder, while slot `0x20` reads more like a typed gate or setup/attachment companion body than like a second emitter.
|
||||
|
||||
The runtime-side bridge is tighter too. The binary already had one exact offset-specialized masked wrapper for slot `0x0a`, `entity_vm_context_try_create_mask_0400_slot0a_with_offset` at `0005:2c35`, and the `000d:21ed -> 000d:22bc` lane is still verified as a slot-backed inline-payload consumer that copies a variable-length byte stream first and then consumes compact metadata bytes plus streamed words. The new body-structure report is consistent with that runtime contract: the surviving `EVENT` / `NPCTRIG` bodies are clause streams with repeated internal subheaders and local labels, not flat literal blobs. That still does **not** prove that `NPCTRIG 0x0a` emits `0x410` directly, but it narrows the best remaining emitter frontier from `EVENT or NPCTRIG` down to `NPCTRIG slot 0x0a` with `NPCTRIG slot 0x20` as the strongest adjacent support body.
|
||||
|
||||
The clause report makes that runtime comparison more concrete too. `0005:2c35` is no longer just an abstract "with offset" wrapper: `entity_vm_slot_load_value_plus_offset` at `000d:5572` now proves the extra word is applied additively to the loaded slot value before `000d:21ed` consumes the result. The internal consumer at `000d:21ed -> 000d:22bc` is tighter as well: after copying the inline blob into the context it reads two signed metadata bytes, uses byte A as the lead-word row count, uses byte B as the shared target-list width, performs `A x B` `entity_link` calls, and pushes back only non-`0x0400` words. That makes `NPCTRIG 0x0a` the only surviving compact body with a natural selector family for this lane: it has `5` evenly spaced clause starts at stride `0x2f`, while slot `0x20` has only one clause and no matching writeback/push motif. So the best current working model is no longer "EVENT or NPCTRIG" or even "NPCTRIG 0x0a plus 0x20 as co-equal bodies"; it is specifically "NPCTRIG slot `0x0a` event-bearing clause ladder, with slot `0x20` as a typed companion/setup body feeding or constraining the same family."
|
||||
- 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):**
|
||||
|
||||
|
|
@ -390,33 +415,100 @@ The clause report makes that runtime comparison more concrete too. `0005:2c35` i
|
|||
|
||||
| Address | Symbol | Role |
|
||||
|-------------|-------------------------------|------|
|
||||
| `0004:c055` | `player_receive_damage_and_dispatch_effects` | Renamed. Contains the `0x604f` immortality gate at `0004:c205`. |
|
||||
| `000b:b3b1` | `cheat_event_listener_create` | Allocates one seg109 listener object and subscribes it to the shared cheat/control event bundle that includes `0x410`. |
|
||||
| `000b:b62c` | `cheat_event_listener_handle_event` | Subscriber-side event mapper: rewrites incoming `0x410` to local state `0x0e` before entering the shared listener state machine. |
|
||||
| `DS:0x604f` | Immortality flag | Set/cleared by event `0x410`. Read only at `0004:c205`. |
|
||||
| `DS:0x60d2` | Immortality-on notification ptr | Near pointer in DS; resolves to far ptr → "Immortality enabled." display. |
|
||||
| `DS:0x60ee` | Immortality-off notification ptr | Near pointer in DS; resolves to far ptr → "Immortality disabled." display. |
|
||||
| `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 cheat menu investigation (seg109 UI lane)
|
||||
|
||||
New compiled-side evidence shows a real but likely dormant cheat-menu UI path:
|
||||
New compiled-side evidence shows that the old "hidden cheat menu" label is misleading. The seg109 UI path is much closer to a hidden **usecode debugger / unit inspector** than to a retail cheat list:
|
||||
|
||||
| Address | Symbol | Role |
|
||||
|-------------|-------------------------------------|------|
|
||||
| `000b:9a86` | `cheat_menu_open_from_current_slot` | Builds a `cheat_event_listener` object, preloads selection from current slot state (`0x659c/0x659e`), pushes it through the sprite tree, and runs a modal draw/update loop. |
|
||||
| `000b:9c0d` | `cheat_menu_open_modal` | Smaller modal wrapper that directly constructs `cheat_event_listener_create(...)`, traverses it, and returns. |
|
||||
| `000b:b3b1` | `cheat_event_listener_create` | Constructor for the listener object. Registers event bundle including `0x23f`, `0x410`, `0x411`, `0x441`, etc. |
|
||||
| `000b:b62c` | `cheat_event_listener_handle_event` | Listener event mapper; event `0x23f` toggles armed/visible state byte `+0x47`; event `0x410` remaps to local state `0x0e` then enters `FUN_000b_b7f3`. |
|
||||
| `000b:9a86` | `usecode_debugger_open_for_current_unit` | Builds the seg109 debugger gump in current-unit mode, derives a usecode path from the live runtime context (`0x659c/0x659e`), loads that file, centers the current line, and then runs a modal UI loop. |
|
||||
| `000b:9c0d` | `usecode_debugger_open_modal` | Smaller generic modal wrapper that opens the same debugger UI without first preloading a current unit/file. |
|
||||
| `000b:b3b1` | `usecode_debugger_gump_create` | Constructor for the debugger object. Builds the panes/menu bar, initializes watch state, resolves the base `usecode` path, and registers the shared control-event bundle including `0x23f`, `0x410`, `0x411`, and `0x441`. |
|
||||
| `000b:b62c` | `usecode_debugger_handle_event` | Debugger event mapper; recovered cases are debugger-style commands (open unit/file, go to line, watch, inspect, clear watches, change global, find/search again, break to debugger). Event `0x23f` is reused as a local debugger-state command; event `0x410` remaps to local state `0x0e` before entering the shared tail. |
|
||||
| `000b:2882` | `usecode_debugger_build_menubar` | Builds the top-level debugger menus: File, Run, Breakpoints, Search, and Data. Recovered entries include `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`. |
|
||||
|
||||
This better explains the long-running negative result about the "infamous 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 `cheat_event_listener_create` are exactly two locations: `000b:9a9b` and `000b:9c56`.
|
||||
- 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 `cheat_event_listener_create` appears there.
|
||||
- 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 menu path is compiled and functional at object level, but likely orphaned/hidden in final gameplay flow (possibly debug/dev-only trigger removed, or only reachable through non-recovered data-driven callback wiring).
|
||||
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 cheat menu than assuming a still-live player-facing scrollable cheat list.
|
||||
|
||||
#### Breakpoint callback lane (new strongest orphan candidate)
|
||||
|
||||
The next live NE pass shifts the strongest likely entry point away from the cheat-toggle helper and toward a surviving **usecode breakpoint callback lane** in seg1408/seg1418.
|
||||
|
||||
New live renames in this lane:
|
||||
|
||||
| Live NE | Name | Role |
|
||||
|---------|------|------|
|
||||
| `1408:0000` | `usecode_debugger_break_state_create` | Allocates and initializes the seg1408 debugger-state object: breakpoint table, current-entry stack, and run-mode flags. |
|
||||
| `1408:0053` | `usecode_debugger_maybe_break_on_current_line` | Runtime breakpoint gate. Stores the current line into the debugger state, resolves the current unit/file name through `1408:0444`, checks the breakpoint table through `1408:029e`, and callbacks through the object's vtable when a break condition is met. |
|
||||
| `1408:00dd` | `usecode_debugger_breakpoint_insert_sorted` | Inserts `(file,line)` breakpoint entries into the seg1408 table in sorted order. |
|
||||
| `1408:029e` | `usecode_debugger_has_breakpoint` | Exact `(file,line)` membership test over the seg1408 breakpoint table. |
|
||||
| `1408:03b0` | `usecode_debugger_callstack_push_entry` | Pushes one current-unit/current-line debugger entry into the seg1408 stack. |
|
||||
| `1408:03f7` | `usecode_debugger_callstack_pop_entry` | Pops one debugger callstack/current-entry record. |
|
||||
| `1408:0419` | `usecode_debugger_enable_single_step` | Arms the step/run-state flags that make the next interpreter lane callback eligible. |
|
||||
| `1408:0432` | `usecode_debugger_clear_step_state` | Clears the step/run flags. |
|
||||
| `1408:0444` | `usecode_debugger_current_entry_get_unit_name` | Returns the active unit/file name pointer from the current debugger entry stack. |
|
||||
|
||||
This matters because `1418:04aa..04b5` is now comment-backed as a concrete interpreter-side handoff into `usecode_debugger_maybe_break_on_current_line`:
|
||||
|
||||
- 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
|
||||
|
||||
|
|
@ -428,21 +520,61 @@ Verified retail anchor points:
|
|||
|----------|--------|---------|-------|
|
||||
| `0x70d75` | `0007:0d75` | cheat matcher emits event `0x103` | retail bytes = `68 03 01 9A FF FF 00 00 83 C4 02`; NE fixup source = `0007:0d79` -> `seg092:0476` |
|
||||
| `0x71d68` | fixup entry for `0007:0d79` | seg039 relocation record | exact retail entry: addr_type `0x03`, rel_type `0x00`, chain_off `0x2b79`, target `seg092:0476` |
|
||||
| `0xc99dd` | `000c:99dd` | later controller-side handler that also executes `push 0x103 / call 000a:5276` | retail fixup source = `000c:99e1` -> `seg092:0476`; this is the first materially safer deferred hook candidate after the direct matcher path failed |
|
||||
| `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 `cheat_menu_open_from_current_slot` | original wrapper uses caller stack words `[BP+8]` and `[BP+6]` plus local armed flag `1` |
|
||||
| `0xb9c48` | `000b:9c48` | arg setup inside `cheat_menu_open_modal` | original wrapper still feeds caller stack words `[BP+8]` and `[BP+6]` into `cheat_event_listener_create`, but starts with local byte `+0x47 = 0` |
|
||||
| `0xb9c48` | `000b:9c48` | modal wrapper prologue; the inherited caller-word patch subsite is `000b:9c4e` / live `13a0:024a` | original wrapper still feeds caller stack words `[BP+8]` and `[BP+6]` into `cheat_event_listener_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: `cheat_menu_open_from_current_slot` consumes caller-supplied words at `[BP+8]` and `[BP+6]`, so the cheat matcher context is the wrong stack shape.
|
||||
- Retargeting the same early cheat-matcher call to `000b:9c0d` got farther: the mouse pointer appeared, proving the hidden menu/display path was being entered. But it still hung with looping music, which points to **timing/context**, not a bad target address. The modal path appears unsafe when entered directly from the keyboard matcher even after the constructor args are forced to zero.
|
||||
- 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` is still the right place to intercept the cheat sequence itself because it is the verified success emission site.
|
||||
- `000c:99dd` is the better candidate for the **actual menu-open call** because it is a later controller/event context, not the raw keyboard matcher frame.
|
||||
- `000b:9c48` is the right argument-fix companion because it is the constructor-argument site for `cheat_menu_open_modal`, and the direct disassembly shows that this is where the wrapper still pulls caller-dependent words.
|
||||
- `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` (`cheat_menu_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:
|
||||
|
||||
|
|
@ -456,10 +588,15 @@ Observed result on retail test build:
|
|||
- But the game then halted with the retail `FILE\FLEX.C, line 83` failure and dropped into the quit/teardown path (`"No pity. No mercy. No remorse."`).
|
||||
- That is strong evidence that event `0x42f` is the wrong deferred hook context for this experiment even though the retargeted address itself was valid enough to enter the UI path.
|
||||
|
||||
Current patch candidate under test:
|
||||
Current Ghidra-side patch plan for a copy:
|
||||
|
||||
- Site 1: keep the original `0007:0d75` bytes and retarget only its existing far-call fixup from `seg092:0476` to `000b:9a86` (`cheat_menu_open_from_current_slot`).
|
||||
- Site 2: patch `000b:9a8d` from `6A 01 FF 76 08 FF 76 06` to `6A 01 6A 00 6A 00 90 90`.
|
||||
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` (`cheat_menu_open_modal`), yielding bytes `9A 0D 02 A0 13`.
|
||||
4. Navigate to `13a0:024a` inside `cheat_menu_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:
|
||||
|
||||
|
|
@ -478,10 +615,14 @@ Risk notes:
|
|||
- "Cheats can be enabled with `-laurie`" is **directly verified**.
|
||||
- "There is a hidden five-byte matcher that toggles cheats" is **directly verified**.
|
||||
- "F10 performs a large cheat-only restore/reset action" is **directly verified**.
|
||||
- "Ctrl+F10 enables god mode" is **not supported** — the verified F10 branch does not require a modifier.
|
||||
- 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.
|
||||
- "Immortality is toggled with a keyboard combo" is **not supported in compiled C code**: event 0x410 has no static keyboard dispatch path. It is USECODE-triggered.
|
||||
- "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`.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue