Pseudocode and stuff

This commit is contained in:
MaddoScientisto 2026-03-25 23:32:13 +01:00
commit ee33f94b4b
466 changed files with 27770 additions and 276 deletions

View file

@ -91,6 +91,28 @@ Current unsafe use:
- direct rename authority for modern Ghidra functions
- assuming one game's intrinsic numbering matches another without local confirmation
### Validated follow-up on `misc_crusader_notes.txt`
Several of the old scratchpad items are now tight enough to record as verified follow-up rather than open leads.
- `STEAM2` (`shape 1297`) is no longer just a handwritten event hint. Local extracted data now confirms non-empty `STEAM2::enterFastArea` (`event 0x0f`) and `STEAM2::gotHit` (`event 0x06`) bodies in `USECODE/EUSECODE_extracted/class_event_index.tsv`, which matches the old note's `event 15 (enter fast area)` and `Got Hit: spawn process` direction.
- The old standalone labels `FUN_1130_0896`, `FUN_1130_32af`, `FUN_1020_0000`, and `FUN_1128_026b` are now closed in the live NE database as `Key_HandleOptionKeys`, `NPC_ResetToStartOfAnim`, `Game_Start`, and `Item_ReceiveHit`. The historical notes were directionally useful, but the live names now capture their real scope: `NPC_ResetToStartOfAnim` resets an NPC/item frame to the first frame of a chosen animation and updates the stored last-anim state, while `Item_ReceiveHit` is the main damage/death handler rather than a loose "various deaths" helper.
- The `Item_ReceiveHit` decomp also closes several handwritten shape hints directly from code. It uses `0x576` for the burning human replacement path (`flaming guy running around`), `0x5a9` and `0x52b` for shield-zap sprites, and still routes controlled-NPC death through `Target_PutTargetingReticleOnItem(0)`, which keeps the old `0x59a` reticle note on the right subsystem.
- The old `ItemNPC_AnotherCreate` area-search TODO is now directly closed in the live NE program: `10e8:2710` (now renamed `NPC_CreateIfAreaSearchValid`) allocates/initializes an `AreaSearch` struct, checks `AreaSearch_IsValidPositionPt`, and only then runs `NPC_Create` plus `Item_PopToCoords`. So the practical role is `create NPC only if the requested point passes area-search placement validation`, not a second independent spawn path.
- The handwritten `Kernel_11d0_2491` gloss was incomplete. The live function is not merely "prints kernel info"; it serializes the process table, timer/keyboard/mouse process-id lists, process sizes, and per-process state through file-like writer callbacks, then restores timer/interrupt state afterward. Current safest read is `kernel/process snapshot writer` rather than a simple printf-style diagnostics helper.
- `FREE::ordinal3C` is still corpus-side evidence rather than live-NE proof, but the old disassembly now constrains it much better than the original note implied. It clears the alert state, checks avatar stasis, rolls random thresholds, and spawns `FREE::ordinal21` with a small set of ordinals (`0x0e`, `0x0f`, `0x00b6`, `0x00d2`). That makes `random global FREE event chooser after alert clear` a safer description than the narrower `random voices when alarm is disabled` guess.
- The old `high priority unknown intrinsic: 01E - fire` note is now effectively closed as a hint. The old Remorse intrinsic tables map `Int01E` to `Actor::I_maybeFire(...)`, and the current live export map ties `Int01E` to `1128:11da`. That is still hint-level naming until the compiled body is analyzed locally, but it is no longer an unidentified ordinal.
- The `combat.dat` scratch note is also better grounded now. The external corpus already showed the extracted tactic files are identical between Remorse and Regret, and the live `CRUSADER.EXE` export map now has NPC fields such as `combatDatTacticPtr`, `combatDatTacticCurOffset`, `combatDatBlockNo`, and `tacticNo`. So the named tactic strings are good portable data labels, but they should still be attached to tactic records and NPC state fields, not promoted directly into compiled function names.
Two shape hints also have light compiled-side support now:
- `0x59a` is passed into `SpriteProcess::I_createSprite` from the cursor/targeting lane in segment `1130`, which fits the old `targetting reticle` note.
- `0x5a3` is passed into `SpriteProcess::I_createSprite` from the `13e8` gameplay/UI lane and stored at `0x6054`, which fits the old `use item crosshairs` note.
Remaining caution:
- these follow-ups make the scratchpad much more reusable, but `misc_crusader_notes.txt` is still an auxiliary corpus. The verified parts above should be cited with their live NE addresses or extracted-data files when reused elsewhere in the repo.
### Combat data note
`combat_dat/readme.txt` records that the extracted `combat.dat` tactic files are identical between No Remorse and No Regret.

View file

@ -55,13 +55,15 @@ Evidence used here:
### Latest verified NE pass
- `0005:295f` is now the only recovered non-hub consumer of `entity_vm_slot_index_from_entity` in the open NE session. It recomputes the slot index, tests owner-row bit `0x0040` directly, exposes that result to the caller, and only then optionally calls `entity_vm_context_try_create_masked_for_entity` with slot `0x06` and mask `0x0040`.
- The direct NE callers currently recovered for `0005:295f` are `0006:43c3`, `0006:c5f0`, and `0007:3584`. That materially narrows the selector frontier to three gameplay-side caller families instead of the older wider `find any caller of 000d:45c5` search.
- The direct NE callers currently recovered for `0005:295f` are still only `0006:43c3`, `0006:c5f0`, and `0007:3584`, but they are now all structurally classified in the live NE session instead of only enumerated as open caller families.
- The first anchored caller family is now structurally classified in the live NE session. Repaired wrapper `0006:4379` is a seg031 dispatch-entry subtype gate over objects built by `0006:42d9`: event type `0x236`, source type `8`, subtype/tag at `+0x3c`, dword payload/source far pointer at `+0x32`, and aux words at `+0x36/+0x38`.
- In that wrapper, subtype `0x20c` at `0006:43c3` routes into `0005:295f`, while nearby sibling subtype `0x20b` at `0006:43e5` routes into `0005:2918` (`slot 0x05` / `mask 0x0020`) using the same `+0x36/+0x38` aux pair. The split is therefore local to one dispatch-entry family, not a direct compiled `NPCTRIG` / `EVENT` class-family selector by itself.
- The `0006:43c3` lane now shows where the owner-row bit-`0x0040` probe is consumed locally: inside a subtype-`0x20c` dispatch-entry object rather than at a generic descriptor-choice site. That improves caller provenance for `0005:295f`, but it still does not prove which owner-loaded class family seeded the later VM data.
- The second direct caller family is now closed too. Old `0006:c5f0` lands at live call site `1128:0ff0` inside `Item_ReceiveHit`, where the non-NPC item path calls `Item_GetDamaged` with hitter sentinel `0x4000`, packed damage `(damagetype << 8) | damage_lo`, and a local flag-out byte that records the returned owner-row bit-`0x0040` capability before the local destruction / impact follow-up.
- The third direct caller family is now closed too. Old `0007:3584` lands at live call site `1138:1384` inside `SuperSprite_HitAndFinish`, where the non-NPC collision lane probes `Item_GetDamaged` with hitter sentinel `0x4000` and packed damage `(firetype << 8) | damage`; only when that flag-out byte stays clear and the target is not fixed does the lane fall through into the local `Item_ReceiveHit` knockback path.
- `0005:2c35` remains outward-dark in the current NE session: instruction search still shows no recovered code or data xrefs, and its proven local role is still only `sign-extended additive word -> slot 0x0a / mask 0x0400 -> generic masked hub`.
- The first live `CRUSADER.EXE` integration batch is now applied for this lane. Comment-backed anchors were added at `1420:0dc5` (`Item_GetUsecodeClassId`), `1420:0e3a` (`Usecode_ItemCallEvent`), `10a0:2718` (`Item_Hit`), `10a0:275f` (`Item_GetDamaged`), `10f0:02d9` (`StorageDataProcess_Create`), and `10f0:0379` (`StorageDataProcess_Run`), with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c` / `0x20b` split in the open NE program.
- Result of this pass: the compiled-side selector evidence still bottoms out at category spans plus owner-row capability bits, not a concrete `NPCTRIG` / `EVENT` class-family choice. The next defensible NE step is to classify the three `0005:295f` caller families or recover an earlier producer that feeds the later owner-loaded class lane.
- The live `CRUSADER.EXE` integration batch is now extended for this lane. Comment-backed anchors were already present at `1420:0dc5` (`Item_GetUsecodeClassId`), `1420:0e3a` (`Usecode_ItemCallEvent`), `10a0:2718` (`Item_Hit`), `10a0:275f` (`Item_GetDamaged`), `10f0:02d9` (`StorageDataProcess_Create`), and `10f0:0379` (`StorageDataProcess_Run`), with branch comments at `10f0:03c3` and `10f0:03e5` preserving the verified `0x20c` / `0x20b` split; new live comments now also anchor the remaining direct caller sites at `1128:0ff0` and `1138:1384`.
- Result of this pass: all currently recovered direct `0005:295f` caller families are now closed, but the compiled-side selector evidence still bottoms out at subtype-gated dispatch or generic gameplay damage consumers plus owner-row capability bits, not a concrete `NPCTRIG` / `EVENT` class-family choice. The next defensible NE step is therefore an earlier producer that assigns subtype `0x20b/0x20c` into field `+0x3c` or otherwise chooses the owner-loaded class family before these generic damage consumers run.
## Priority 2: Rendering / Camera / Tile-Visibility / Watch-Controller Lane

View file

@ -320,6 +320,81 @@ What `-laurie` appears to enable by itself:
- It is also enough to permit the separate event-`0x7e` runtime toggle path, whose entire job is to flip `0x6045` later and post the `0x6087` / `0x6091` notifications.
- It is **not** enough for low-level keyboard-only cheat branches that check `0x6045` directly. That is why the user can see the `0x844`-gated debug-box behavior while plain F10 still behaves as if full keyboard cheats are off.
### Keyboard folklore correction pass (`CRUSADER.EXE` live NE)
- `~` is real in this build. In `World_HandleKeyboardInput_13e8_14b4`, the branch at `13e8:202d..203d` requires `0x844 != 0`, flips the live keyboard-cheat latch `0x6045`, and posts the paired on/off messages at `0x6087` / `0x6091`.
- `Ctrl+C` is **not** the current-location popup in this build. The actual location-display branch is `Ctrl+L` (`0x426`), which formats the avatar's X/Y/Z into the `1478:610c` string and displays it at `13e8:255e..25ac`.
- The third F7-family overlay is also real. The three cheat-gated overlay toggles live at `13e8:1a7c` (`0x141`), `13e8:1a50` (`0x241`), and `13e8:1a20` (`0x441`), writing `1478:2bca`, `1478:2bc9`, and `1478:0ee0` respectively before forcing a camera refresh through `[g_cameraProcess+0x2c]`.
- The unresolved online entry is therefore best corrected as `Ctrl+F7 exists but is cheat-gated`; the obviously bogus list item is `Ctrl+C`, which should be `Ctrl+L` for the coordinate popup.
### `~` versus `jassica16`
- These are **not** the same mechanism. `jassica16` is handled earlier in `Key_CheckCheatToggle` as a raw scan-code matcher over `1478:2833`, while `~` is handled later in `World_HandleKeyboardInput_13e8_14b4` as a translated logical key value (`0x7e`).
- That means Shift is probably normal behavior here, not a DOSBox-only artifact. On a standard US DOS keyboard layout, plain grave is `` ` `` (`0x60`) and Shift+grave is `~` (`0x7e`); the live handler we recovered is the `0x7e` branch, and no separate backtick/`0x60` hotkey path was recovered in this handler.
- DOSBox can still affect host-to-DOS layout mapping, but the recovered game logic is asking for the translated `~` character value, not the raw grave-key scancode. So on a US layout the expected in-game gesture is Shift+grave.
- `jassica16` is the stronger unlock path. On success it sets `0x8c52 = 1`, toggles both `0x844` and `0x6045`, emits the `0x103` voice/helper side effect, and shows the `1478:287b` / `1478:2892` cheat-state modals.
- Plain `~` is narrower. It only works after the broader `0x844` gate is already on, and then it flips just the live keyboard-cheat latch `0x6045` while showing the `0x6087` / `0x6091` on/off messages.
- Practically: `jassica16` can bootstrap cheats from a cold state because it toggles the master gate and the low-level latch together; `~` cannot bootstrap that state by itself because its own branch first requires `0x844 != 0`.
- The extra `0x8c52` write also means `jassica16` is not just a different UI for the same toggle. It leaves a second latch behind that later keyboard-side cheat/debug branches can test, while plain `~` does not touch that latch.
### What `Ctrl+F7` is supposed to show
- `Ctrl+F7` (`13e8:1a20` -> `1478:0ee0`) is not just a third generic grid. In the camera redraw path, `Camera_1180_15ef` treats `0x0ee0` differently from the simpler `0x2bca` grid flag.
- Plain `F7` (`1478:2bca`) draws a coarse `3 x 3` isometric world-cell grid around the camera by stepping `0x200` world units and drawing diamond tile boundaries.
- `Alt+F7` (`1478:2bc9`) calls `Snap_1058_0814`, which walks the snap-process egg list and draws per-entry diamonds from packed egg range data. That is closer to a snap/trigger coverage overlay than to a camera-aligned background grid.
- `Ctrl+F7` (`1478:0ee0`) walks `EggHatcherProcess` objects and calls `EggHatcher_1090_0921`, which draws diamond trigger ranges using `Egg_GetXRange`, `Egg_GetYRange`, and the shared diamond helper at `1180:1ce5`.
- So the intended visual for `Ctrl+F7` is an **egg / hatch trigger-range overlay**. It should highlight live egg-hatcher or monster-egg regions, not fill the screen with a regular map grid.
- That also explains why it can appear to do nothing. If the current map has no live `EggHatcherProcess` objects, or a non-monster-egg trigger has already hatched while normal gameplay input is active, the draw loop has nothing eligible to outline even though the toggle flag itself changed.
### Egg-Hatcher Runtime Notes
- The live NE runtime has a dedicated egg-hatcher process type: `EggHatcher_CreateProcess` builds process type `0x20f`, stores one target item number in the process, and names it `EggHatcher`.
- The corresponding runner `EggHatcherProcess_Run` reads the avatar footprint and compares it against the egg item's center, `Egg_GetXRange`, `Egg_GetYRange`, and a vertical window of about `+/- 0x30` Z units.
- For non-monster egg families, entering that range sets the process `ishatched` byte and calls `Item_CallHatchEvent`; leaving the range clears `ishatched` and calls `Item_CallUnhatchEvent`.
- The overlay helper `EggHatcher_1090_0921` uses the same range values to draw the visible diamond outlines for `Ctrl+F7`. In other words, the visualizer is showing the same trigger footprint the runtime is testing against.
- Current safest gameplay-facing read: these "eggs" are hidden trigger items that fire enter/leave behaviors when the avatar crosses their footprint. Some are likely classic trap/trigger/controller eggs rather than literal spawn objects.
- The wider workspace evidence agrees with that interpretation. The extracted USECODE corpus contains egg-like labels such as `TRIGEGG`, `ONCEEGG`, `DOOREGG`, `LAZEREGG`, `STEAMEGG`, `MISS1EGG`, `GRENEGG`, and `REB_EGG`, while the older `crusader-disasm` notes explicitly call out `SnapEgg` as a fast-area participant.
### How To Find One In Practice
- The most reliable bootstrap is now: start with `-laurie`, then press `Shift+~` to flip the live keyboard cheat latch on, then use the F7-family overlays.
- Use `Ctrl+F7` when you want true egg-hatcher ranges. If nothing appears, switch to `Alt+F7` too; that overlay walks the snap-process egg list and can reveal related trigger coverage that is not currently present in the live egg-hatcher process list.
- Search in gameplay spaces that are likely controlled by hidden trigger items rather than by obvious UI state: door thresholds, ambush spawn zones, laser or steam hazards, missile/grenade trap corridors, teleporter pads, and one-shot scripted encounter rooms.
- If a region only reacts once, walk out of the area and re-enter it. The live runner explicitly tracks enter/leave transitions and only calls the hatch/unhatch events when the avatar crosses the trigger boundary.
- Monster-egg handling still looks special-cased, so a blank `Ctrl+F7` result does not prove a map has no egg-related logic at all; it only proves there are no eligible live `EggHatcherProcess` outlines being drawn at that moment.
### Cheat / Debug Key Matrix
Cheat-state legend used below:
- `master gate (0x844)`: broader Laurie/debug permission state. `-laurie` sets this directly.
- `keyboard latch (0x6045)`: full keyboard-cheat latch used by the low-level F10 path and some other direct keyboard branches.
- `sequence latch (0x8c52)`: extra post-sequence latch left behind by `jassica16`; plain `~` does not set it.
- `input-active gate (0x85f)`: broader gameplay-input state. This is not a cheat bit, but it still suppresses some option-key behavior when the game is in modal/non-gameplay states.
| Input | State Needed | Effect | Evidence / Notes |
|------|--------------|--------|------------------|
| `-laurie` | none at startup | Sets the `master gate (0x844)` only | This is the easiest way to unlock the broader debug/event family without entering the hidden key sequence. |
| `jassica16` | none | Toggles `0x844` and `0x6045`, sets `0x8c52`, emits `0x103`, shows cheat active/inactive modal | Raw scan-code matcher, not a translated text string. Can bootstrap full cheat state from cold. |
| `Shift+~` / `~` | `0x844` already on | Toggles only `0x6045`; shows the on/off cheat-latch modal | On a US layout the game is checking logical `0x7e`, so Shift is the expected normal gesture. With `-laurie`, this becomes a practical way to enable full keyboard cheats. |
| `F10` | `0x85f` and `0x6045` | Refill/loadout branch: restore/refill, credits, items, ammo, weapon set | This is the plain full-keyboard-cheat branch in `Key_HandleOptionKeys`. |
| `Ctrl+F10` | `0x85f` and `0x6045`; current NPC alive | Toggles the current controlled NPC's immortal flag | Same F10 branch, but only the modifier-gated immortality sub-path. |
| `Ctrl+L` | no cheat gate recovered | Shows current avatar X/Y/Z popup | Online `Ctrl+C` folklore is wrong for this build. |
| `Ctrl+C` | no live branch recovered | No confirmed effect in this build | Best current correction is `Ctrl+L`. |
| `Ctrl+V` | no cheat gate recovered | Displays internal stats / version / memory info | This branch appears to be available without the cheat gates. |
| `Ctrl+Q` | `0x844` | Toggles CD transfer display state (`0x604f`) | Distinct from immortality. Uses the broader master gate, not the full keyboard latch. |
| `F7` | `0x844` | Draws coarse `3 x 3` camera-aligned world-cell grid | Simple isometric background grid. |
| `Alt+F7` | `0x844` | Draws snap-process egg diamonds | Better read as snap/trigger coverage overlay than generic grid. |
| `Ctrl+F7` | `0x844` | Draws egg-hatcher trigger diamonds | Visualizes live `EggHatcherProcess` ranges; can look blank on maps without eligible live processes. |
| `F` | `0x844` | Toggles object/framework paint overlay | Laurie/debug-side visual lane, not a full keyboard-cheat-latch feature. |
| `H` | `0x844` and `0x8c52` | Toggles Hack Mover | Strongest current read is that this needs the broader Laurie/debug gate plus the extra post-`jassica16` sequence latch. |
Practical state recipes:
- `-laurie` by itself gives you the broader Laurie/debug event family (`0x844`) but not full keyboard cheats.
- `-laurie` plus `Shift+~` is enough to reach the full keyboard-cheat state in live play, because `-laurie` supplies `0x844` and `~` then flips `0x6045`.
- `jassica16` is still the most complete single-step unlock, because it toggles both cheat bytes together and also leaves the extra `0x8c52` sequence latch behind for later branches such as Hack Mover.
### Cheat-related string table (seg014 / `000e:xxxx`)
| Address | String | Notes |
@ -411,30 +486,48 @@ Current strongest read:
**Secondary handler (000b:b62c):**
`cheat_event_listener_handle_event` (`000b:b62c`) receives event 0x410 through the registration installed by `cheat_event_listener_create` at `000b:b3b1`. When event 0x410 arrives, it writes state code `0xe` (decimal 14) into the event object's field `+0x6` and passes it to `000b:b7f3` for processing. This is a parallel state-machine path that runs alongside the 000c toggle; likely it drives an associated USECODE process or animation object into state 14.
`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` | `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. |
| `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 cheat menu investigation (seg109 UI lane)
### Hidden usecode debugger investigation (seg109 UI lane)
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:
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.
| Address | Symbol | Role |
|-------------|-------------------------------------|------|
| `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`. |
The two address families that looked inconsistent in older notes are actually two connected layers of the same subsystem, not competing identifications:
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.
- 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
@ -443,27 +536,13 @@ This better explains the long-running negative result about the "infamous scroll
- 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 cheat menu than assuming a still-live player-facing scrollable cheat list.
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 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.
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.
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`:
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,
@ -521,8 +600,8 @@ 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: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` | 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` |
| `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
@ -565,7 +644,7 @@ One remaining function-hygiene caveat:
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.
- 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.
@ -573,13 +652,13 @@ 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` (`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 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 `cheat_menu_open_modal` instead.
- 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:
@ -592,8 +671,8 @@ 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` (`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.
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.
@ -601,7 +680,7 @@ These edits are now applied and byte-verified on `/Writable/CRUSADER-PATCHED.EXE
Rationale for the revised wrapper patch:
- Earlier direct-hook attempts proved that inheriting the two caller-frame words at `000b:9a8f/9a92` is unsafe from the cheat matcher context.
- But later decompilation of `cheat_event_listener_create` showed that the leading `push 0x1` at `000b:9a8d` is a distinct mode byte used by the constructor path, so zeroing all three pushed values was too aggressive.
- 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:

View file

@ -134,15 +134,17 @@ Use comments on the compiled runtime functions that already consume or materiali
Best current anchors:
- `000d:51fd` = slot value load path
- `000d:5572` = slot value plus additive word
- `000d:46ec` = context create from slot index
- `000d:0988` = referent-chain mutation family (`0x18..0x1b`)
- `000d:208b` = materialize-or-forward value lane
- `000d:21ed` = inline payload prepend stage
- `000d:22bc` = decoded matrix/pushback consumer
- `000d:2104` = mixed immediate/object finalize-to-outptr stage
- `000d:ebe3` = opcode sequence runner
Comment payload should stay short and evidence-heavy, for example:
`POC USECODE body anchor: NPCTRIG slot 0x0A -> body 0x00DA..0x024F, raw word 0x013E, payload shape unresolved, parsed via tools/poc_crusader_usecode_parser.py`
`POC USECODE body anchor: NPCTRIG slot 0x0A -> body 0x00DA..0x024F, raw word 0x013E, 5 local/debug rows after ret, parsed via tools/poc_crusader_usecode_parser.py`
### 3. Optional comment bundles per runtime family

View file

@ -113,6 +113,28 @@ The safe reading is:
The first script IR should preserve exact recompilation inputs before it tries to look pretty.
## Current Parser Views
The current proof-of-concept parser now emits three complementary views for a single class/slot body:
- JSON IR: the authoritative machine-facing output for tooling and any future assembler.
- Flat text listing: a byte-faithful decode with offsets, raw bytes, and trailer sections.
- Script view: a more readable block-labeled decompilation with locals, labels, and stack-VM statements.
- Pseudocode view: a higher-level decompilation that tries to collapse common compare ladders and stack expressions into programming-language-like control flow.
The script and pseudocode views are intentionally descriptive rather than authoritative. They are meant to help read bodies like `NPCTRIG 0x0A` or `EVENT 0x0A` without losing the exact JSON IR that a round-trip compiler will need.
## Deferred Readability Follow-Ups
Keep these parser-facing readability tasks for later while the current focus stays on broad pseudocode export and class-family understanding:
1. Replace unresolved `class_XXXX_slot_YY` call labels with behavior-backed names where the compiled/runtime evidence is strong enough.
2. Replace placeholder argument names such as `arg_06` with semantic names inferred from stable usage patterns.
3. Detect more control-flow shapes beyond compare ladders, especially simple loops and early-return guards.
4. Collapse common spawn/setup idioms into more domain-specific statements when the stack pattern is consistent.
5. Run the pseudocode renderer across larger families like `EVENT`, `_BOOT`, and `SURCAM*` and tighten the heuristics where they still leak VM structure.
6. Add small behavior-level comments only where they help explain gameplay meaning rather than VM mechanics.
### Unit of decompilation
The IR should be organized as:
@ -219,6 +241,7 @@ The compiler side will need more than pretty script text. At minimum it must pre
- Width/sign information for immediates
- Inline versus indirect payload form
- String payload encoding and terminators
- Post-`ret` debug/local symbol trailers, including the local count byte and each per-local metadata row
- Any unknown opcode byte sequences verbatim
If any of those are dropped, a source-level editor can still be readable, but it will stop being a trustworthy recompilation format.
@ -396,9 +419,20 @@ event:
derived_body_length: 373
repeated_template_status: ""
body:
end_reason: end_opcode
end_reason: debug_symbols_then_end
raw_body_sha1: <digest>
unknown_trailing_bytes: ""
debug_symbol_offset: 0x0143
debug_symbol_count: 5
debug_symbols:
- index: 0x00
type_id: 0x69
bp_repr: [BP+00h]
name: referent
- index: 0x01
type_id: 0x69
bp_repr: [BP+0Ah]
name: event
ops:
- offset: 0x0000
absolute_body_offset: 0x00da
@ -417,9 +451,12 @@ ops:
annotation_hints:
runtime_family: slot-backed-owner-loaded-body
compiled_anchors:
- 000d:51fd
- 000d:5572
- 000d:46ec
- 000d:0988
- 000d:208b
- 000d:21ed
- 000d:22bc
- 000d:2104
- 000d:ebe3
```
@ -431,7 +468,7 @@ annotation_hints:
`event` keeps the exact six-byte row meaningfully split into authoritative fields plus the derived body window.
`body` records how far the parser got and whether any bytes remain undecoded or trailing.
`body` records how far the parser got, whether the body terminated at a real `0x7a` end marker, and whether a post-`ret` local/debug trailer was parsed instead of being misclassified as stray opcodes.
`ops` is intentionally lossless. Each decoded op keeps:
@ -442,6 +479,8 @@ annotation_hints:
- exact raw bytes for the whole op
- parsed operands as typed fields
`debug_symbols` preserves the owner-loaded post-`ret` local metadata block. Current evidence from `crusader-disasm` and the live extracted chunks shows that many bodies end as: executable ops -> `ret` -> local/debug symbol rows -> `0x7a` end. Those rows are not executable bytecode and should survive round-trip as structured metadata rather than raw tail bytes.
`annotation_hints` is the bridge to Ghidra. It is not a source-language feature. It exists so a later importer can attach the right comments and bookmarks to the compiled VM/runtime addresses without trying to infer them from free text.
### Opcode result policy
@ -451,7 +490,7 @@ The parser should use four result classes only:
- `decoded_op`: normal parsed opcode with structured operands
- `unknown_opcode`: one-byte opcode not yet modeled; stop or fall back conservatively
- `raw_tail`: remaining undecoded bytes after a stop condition
- `debug_blob`: symbol/debug tail such as `0x5c`-anchored metadata
- `debug_blob`: post-`ret` local/debug trailer ending in `0x7a`
That keeps the IR trustworthy even before the whole Crusader VM is modeled.
@ -474,16 +513,23 @@ annotation_hints:
runtime_family: slot-backed-owner-loaded-body
payload_shape_hint: signed_word
compiled_anchors:
- address: 000d:51fd
role: slot_value_loader
- address: 000d:5572
role: slot_value_plus_offset
- address: 000d:46ec
role: context_create_from_slot
- address: 000d:ebe3
role: opcode_sequence_run
- address: 000d:0988
role: referent_chain_mutator
- address: 000d:208b
role: materialize_or_forward_value
- address: 000d:21ed
role: prepend_inline_payload
- address: 000d:22bc
role: matrix_pushback_stage
- address: 000d:2104
role: finalize_to_outptr
- address: 000d:ebe3
role: opcode_sequence_run
runtime_stage_hints:
- stage_address: 000d:0988
ir_name: APPEND_UNIQUE_INDIRECT
```
This is deliberately smaller than a full import format. It keeps the parser reusable even if the first Ghidra-side importer is only a comment/bookmark script.