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
155
docs/crusader-disasm-reference.md
Normal file
155
docs/crusader-disasm-reference.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Crusader Disasm Reference Corpus
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the reusable knowledge in the separate local project at `K:/ghidra/crusader-disasm` and how it should be consumed inside the main `Crusader_Decomp` workflow.
|
||||
|
||||
Treat that repo as an auxiliary evidence corpus, not as direct rename authority for `CRUSADER-RAW.EXE` or the NE-loaded `CRUSADER.EXE` program.
|
||||
|
||||
## Main Assets
|
||||
|
||||
### Handwritten notes
|
||||
|
||||
`misc_crusader_notes.txt` is a compact scratchpad with a few still-useful anchors:
|
||||
|
||||
- keyboard event/code notes: `CTRL-M = 0x432`, `CTRL-L = 0x426`, `CTRL-V = 0x42f`, `CTRL-Q = 0x410`
|
||||
- shape/event examples: `STEAM2` (`shape 1297`) is noted with event `15` (`enterFastArea`) and 24 animation frames
|
||||
- `SnapEgg` (`shape 0x4fe`) is noted as entering the fast area and being handed to a `SnapProcess`
|
||||
- chest/monitor examples preserve concrete shape ids and field snapshots that can be cross-checked against map/object dumps
|
||||
- old standalone function labels such as `FUN_1130_0896` are useful only as historical waypoints; they are not direct rename authority for the current raw or NE Ghidra databases
|
||||
|
||||
Safe use:
|
||||
|
||||
- event/id hinting
|
||||
- shape-id cross-checks
|
||||
- lead generation for later binary confirmation
|
||||
|
||||
Unsafe use:
|
||||
|
||||
- direct function renames based only on these handwritten notes
|
||||
- assuming the old segment numbering matches the current `CRUSADER-RAW.EXE` or `CRUSADER.EXE` imports without a verified address mapping
|
||||
|
||||
### Shape metadata tables
|
||||
|
||||
`shapedata.txt` and especially `shapedata_more_complete.txt` provide a broad `Shape info N data 4(top),5,7,8` table.
|
||||
|
||||
Current best reuse:
|
||||
|
||||
- cross-check `shapeinfo` bytes and flags against item/entity behavior already seen in the binary
|
||||
- identify repeated per-shape flag/value families before promoting any field meaning
|
||||
- support later naming of shape-related globals, cache entries, and trigger/object classes
|
||||
|
||||
This is one of the strongest data-side additions in the external corpus because it is broad, structured, and easy to re-test locally.
|
||||
|
||||
### Static map/object dumps
|
||||
|
||||
`mapdump/map-item-dump.txt` is a large coordinate/event-style dump of placed world items.
|
||||
|
||||
Current best reuse:
|
||||
|
||||
- correlate static placements with `EVENT`, `NPCTRIG`, `CRUZTRIG`, and related USECODE families
|
||||
- cross-check specific shapes noted in `misc_crusader_notes.txt` against real map presence
|
||||
- build targeted map-level test cases for later `CRUSADER.EXE` naming and annotation work
|
||||
|
||||
This file is valuable because it gives real world-layout evidence that complements the owner-loaded USECODE work already documented in `docs/usecode-roundtrip-ir.md`.
|
||||
|
||||
### USECODE opcode list
|
||||
|
||||
`usecode_opcodes.txt` contains an extracted opcode table for the JP release of Crusader: No Remorse.
|
||||
|
||||
Useful properties:
|
||||
|
||||
- broad opcode coverage from `0x00` through `0x7b`
|
||||
- readable names for assignment, local/member reference, control-flow, spawn/process, and search-family operations
|
||||
- a concrete local opcode vocabulary independent of ScummVM/Pentagram naming layers
|
||||
|
||||
Reuse rule:
|
||||
|
||||
- use these names as parser and report hints only
|
||||
- do not assume the textual names alone are enough to rename compiled handlers in the DOS binary
|
||||
|
||||
### Intrinsic and function dumps
|
||||
|
||||
The `unkcoffs/` directory holds older cross-version name tables such as:
|
||||
|
||||
- `reg_functions.txt`
|
||||
- `rem_functions.txt`
|
||||
- `reg_intrinsic_dump.txt`
|
||||
- `rem_intrinsic_dump.txt`
|
||||
- `u8_intrinsic_dump.txt`
|
||||
|
||||
These dumps are useful because they preserve prior RE vocabulary for Remorse/Regret/U8 function and intrinsic families, including many item, actor, audio, palette, and world operations.
|
||||
|
||||
Current safe use:
|
||||
|
||||
- hint-only metadata for intrinsic ordinals, signatures, and broad subsystem labels
|
||||
- cross-version comparison when a Remorse/Regret difference matters
|
||||
- porting candidate generation for the NE `CRUSADER.EXE` project, where segment-based labels may be easier to reconcile than in the flat raw import
|
||||
|
||||
Current unsafe use:
|
||||
|
||||
- direct rename authority for modern Ghidra functions
|
||||
- assuming one game's intrinsic numbering matches another without local confirmation
|
||||
|
||||
### Combat data note
|
||||
|
||||
`combat_dat/readme.txt` records that the extracted `combat.dat` tactic files are identical between No Remorse and No Regret.
|
||||
|
||||
That is small but useful: tactic names from the combat data are portable labels and should be treated as version-stable unless contradicted by later binary evidence.
|
||||
|
||||
## How This Fits The Existing Docs
|
||||
|
||||
This external corpus mainly strengthens four areas already active in `Crusader_Decomp`:
|
||||
|
||||
1. `docs/usecode-roundtrip-ir.md`
|
||||
The opcode list, intrinsic dumps, and static trigger/map data provide local cross-checks for the USECODE parser and event-family work.
|
||||
|
||||
2. `docs/ne-segment1.md`
|
||||
The handwritten note set preserves shape ids, keyboard/event codes, and gameplay object examples that can be matched against the segment-1 gameplay/input lane.
|
||||
|
||||
3. `docs/raw-porting-progress.md`
|
||||
The external notes add candidate gameplay/object labels and map-backed test targets, but they should remain supporting evidence until verified in the raw full-EXE database.
|
||||
|
||||
4. `docs/overview.md`
|
||||
The separate disasm repo is now part of the local evidence stack alongside ScummVM and Pentagram, but unlike those source ports it is a prior RE corpus tied directly to Crusader assets and old disassembly work.
|
||||
|
||||
## Safe Reuse Policy
|
||||
|
||||
Use the external disasm repo for:
|
||||
|
||||
- opcode-name hints
|
||||
- intrinsic/signature hints
|
||||
- shape-id and map-placement cross-checks
|
||||
- event-code and key-code lead generation
|
||||
- candidate subsystem vocabulary before binary confirmation
|
||||
|
||||
Do not use it for:
|
||||
|
||||
- speculative raw-function renames
|
||||
- address mapping without an explicit verified translation
|
||||
- replacing direct binary evidence from `CRUSADER-RAW.EXE` or `CRUSADER.EXE`
|
||||
|
||||
## Immediate Porting Frontier For `CRUSADER.EXE`
|
||||
|
||||
The next practical use of this corpus is not another raw-only note pass. It is a controlled porting pass into the NE-loaded `CRUSADER.EXE` project in Ghidra.
|
||||
|
||||
Best initial targets:
|
||||
|
||||
1. Port already-verified raw names that clearly correspond to NE-segment functions where the segment:offset identity can be confirmed directly.
|
||||
2. Use `unkcoffs/` function and intrinsic dumps as hint-only comparison tables when the NE database exposes clearer segment-local call structure than the flat raw import.
|
||||
3. Use `map-item-dump.txt` plus shape tables to annotate trigger-heavy or object-heavy NE lanes before promoting any names.
|
||||
4. Use `usecode_opcodes.txt` to keep future USECODE parser/report output aligned with an additional local opcode vocabulary, especially where ScummVM and Pentagram leave placeholders.
|
||||
|
||||
## Recommended Order For The Next Porting Pass
|
||||
|
||||
1. Start with one small NE segment or subsystem that already has strong raw names or old disasm vocabulary.
|
||||
2. Prefer functions with direct string, data-table, or caller-role evidence over unlabeled wrappers.
|
||||
3. Use the map/shape corpus to explain data-driven objects first; use the intrinsic/function dumps only as secondary hints.
|
||||
4. Record exact successful raw-to-NE name correspondences so later passes can reuse the mapping instead of re-deriving it.
|
||||
|
||||
## Current High-Value Follow-Ups
|
||||
|
||||
- Build a shape-id crosswalk between `shapedata_more_complete.txt`, `map-item-dump.txt`, and the existing `EVENT` / `NPCTRIG` / `CRUZTRIG` families.
|
||||
- Compare the handwritten key/event codes against the already-named cheat/input paths to see which parts of the old notes are now directly closed.
|
||||
- Use `rem_functions.txt` / `reg_functions.txt` to identify conservative candidate names for still-positional NE functions, but only when the local caller/data evidence matches.
|
||||
- Keep the external disasm corpus explicitly separated from ScummVM/Pentagram-derived evidence so provenance stays clear in future porting notes.
|
||||
218
docs/ne-hole-filling-priorities.md
Normal file
218
docs/ne-hole-filling-priorities.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# CRUSADER.EXE Hole-Filling Priorities From CRUSADER-RAW.EXE
|
||||
|
||||
## Purpose
|
||||
|
||||
This note tracks the highest-value places where the NE-loaded `CRUSADER.EXE` project is still materially unclear, but the older `CRUSADER-RAW.EXE` project already holds enough verified structure to close the gap.
|
||||
|
||||
The goal is not to duplicate the raw notes. The goal is to rank the NE-side holes that are most likely to collapse quickly once the raw-side evidence is ported carefully.
|
||||
|
||||
Evidence used here:
|
||||
|
||||
- live Ghidra MCP checks in the open `CRUSADER.EXE` session
|
||||
- `plan-mid.md`
|
||||
- `crusader_segment_coverage_ledger.csv`
|
||||
- `docs/raw-porting-progress.md`
|
||||
- `docs/raw-0007-rendering.md`
|
||||
- `docs/raw-0008-000c.md`
|
||||
- `docs/raw-000a-000d.md`
|
||||
- `docs/raw-000e.md`
|
||||
- `docs/crusader-disasm-reference.md`
|
||||
|
||||
## Priority 1: VM / USECODE Selector And Owner-Loaded Runtime Lane
|
||||
|
||||
### Why it is still unclear in `CRUSADER.EXE`
|
||||
|
||||
- The NE project already carries the main structural names, but the actual upstream selector into the VM sequencer is still unresolved.
|
||||
- The dark wrappers around slot `0x0a` / `0x0b` are named structurally, not behaviorally.
|
||||
- The owner-resource helper is partially understood, but the exact class-family selection and configured owner-file naming are still open.
|
||||
- Coverage ledger state is still only `Partial` for segments `133`, `134`, and `135`.
|
||||
|
||||
### What the raw project already knows
|
||||
|
||||
- `000d:463a` is now a verified generic masked-create hub: `entity_vm_context_try_create_masked_for_entity`.
|
||||
- `000d:45c5` is a three-way category mapper: `entity_vm_slot_index_from_entity`.
|
||||
- `000d:4c99` and `000d:7000` classify the runtime bootstrap and owner-resource creation path.
|
||||
- `000d:ebe3` is a verified ordered opcode sequencer: `entity_vm_opcode_sequence_run`.
|
||||
- `0005:2c35` is already narrowed to `entity_vm_context_try_create_mask_0400_slot0a_with_offset`, but its outward caller role is still dark.
|
||||
- The raw-side docs also already carry the exact `NPCTRIG` / `EVENT` body-window fits, referent-registry model, and the current selector blocker.
|
||||
- `docs/crusader-disasm-reference.md` already identifies the best auxiliary corpus for this lane: `usecode_opcodes.txt`, `map-item-dump.txt`, `shapedata_more_complete.txt`, and the `unkcoffs/` dumps.
|
||||
|
||||
### Porting work that should close the gap
|
||||
|
||||
- Port the raw-side selector, context, and owner-loader comments into the NE project at the same segment:offset anchors.
|
||||
- Keep the slot-`0x0a` / slot-`0x0b` wrappers conservative until real caller roles are recovered.
|
||||
- Use the external opcode list and map/shape corpus only as hint generators while reconciling the live NE caller path.
|
||||
- Prioritize recovery of the first caller that turns the category spans from `000d:45c5` into a concrete class-family choice.
|
||||
|
||||
### Live anchors in the open NE project
|
||||
|
||||
- `000d:45c5` -> `entity_vm_slot_index_from_entity`
|
||||
- `000d:463a` -> `entity_vm_context_try_create_masked_for_entity`
|
||||
- `000d:4c99` -> `entity_vm_runtime_create`
|
||||
- `000d:ebe3` -> `entity_vm_opcode_sequence_run`
|
||||
- `0005:2c35` -> `entity_vm_context_try_create_mask_0400_slot0a_with_offset`
|
||||
|
||||
### 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 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.
|
||||
- `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.
|
||||
|
||||
## Priority 2: Rendering / Camera / Tile-Visibility / Watch-Controller Lane
|
||||
|
||||
### Why it is still unclear in `CRUSADER.EXE`
|
||||
|
||||
- The raw rendering/camera work is one of the strongest finished structural maps, but the NE ledger still shows only `Partial` coverage for the watch-controller lane (`seg049`) and `None` for several related render-heavy segments.
|
||||
- The exact controller-versus-watched-entity ownership label around `0x2bd8` is still open.
|
||||
- The NE side still does not have the same whole-lane readability that the raw `0007` rendering notes already provide.
|
||||
|
||||
### What the raw project already knows
|
||||
|
||||
- The raw notes already define the draw-list node format, tile visibility pipeline, screen/global state, scroll/camera globals, and the watch-controller family.
|
||||
- `0007:ba00`, `0007:ba45`, and `0007:baea` already have conservative structural names and caller contracts in the raw project.
|
||||
- The coordinate-transform lane is also already documented in a way that can be reused safely for the NE segments that call into it.
|
||||
|
||||
### Porting work that should close the gap
|
||||
|
||||
- Port the controller/object comments around `0x2bd8`, `0x2be0`, and the scroll globals before attempting any more aggressive renames.
|
||||
- Carry the draw-list, tile-grid, and viewport/global names into the NE lane where the same DS globals and call shapes match.
|
||||
- Use `map-item-dump.txt` and `shapedata_more_complete.txt` to explain object-heavy camera/trigger placements only after the binary side matches the raw lane.
|
||||
|
||||
### Evidence anchors
|
||||
|
||||
- Ledger: `seg049` = `Partial`, `seg053` = `None`, multiple surrounding render-heavy segments remain `None`
|
||||
- Raw-side anchor doc: `docs/raw-0007-rendering.md`
|
||||
|
||||
## Priority 3: Startup / Display Transition And Active-Hold Lane
|
||||
|
||||
### Why it is still unclear in `CRUSADER.EXE`
|
||||
|
||||
- The NE startup/display lane is structurally strong, but one of the key bodies is still overlap-damaged.
|
||||
- `000c:db68` currently resolves to `cursor_nav_update_and_dispatch`, but boundary analysis still shows an obviously oversized body spanning `000c:5b68 - 000c:ff1c`.
|
||||
- The renderer-pair role at `0x8c5c/0x8c60` and the exact higher-level label for some preset paths remain open.
|
||||
|
||||
### What the raw project already knows
|
||||
|
||||
- The raw project already separates the seg126/127/136/137/138 transition helpers into a usable startup/display model.
|
||||
- The `0x6828 + 0x40` borrowed hold byte is already separated from the seg108-local `0x4f38` bit-`0x40` lane.
|
||||
- The fade-controller behavior and the shared break/hold state around `0x31a2` are already documented tightly enough to guide NE cleanup.
|
||||
|
||||
### Porting work that should close the gap
|
||||
|
||||
- Use the raw-side boundary and caller notes to split `000c:db68` back into the real local helper bodies before promoting any more labels.
|
||||
- Port the hold-token provenance and the seg108-versus-seg136 separation comments into the NE database.
|
||||
- Keep the remaining renderer-pair naming conservative until the overlap is repaired.
|
||||
|
||||
### Live anchors in the open NE project
|
||||
|
||||
- `000c:db68` -> `cursor_nav_update_and_dispatch`
|
||||
- Boundary analysis for `000c:db40..000c:dc40` still reports `cursor_nav_update_and_dispatch @ 000c:db68 body 000c:5b68 - 000c:ff1c`
|
||||
- Ledger: `seg126`, `seg127`, `seg136`, `seg137`, `seg138` are all only `Partial`
|
||||
|
||||
## Priority 4: `0x4588` Callback / Allocator / Palette Cleanup Lane
|
||||
|
||||
### Why it is still unclear in `CRUSADER.EXE`
|
||||
|
||||
- Many helpers in this lane are already named in the NE project, but the actual subsystem identity of the `0x4588` object is still unresolved.
|
||||
- The same blocker is repeated across the ledger for segments `80`, `82`, `91`, `95`, and `138`, which means the NE project has structure but still lacks the final behavioral classification.
|
||||
|
||||
### What the raw project already knows
|
||||
|
||||
- The raw project already has a stable lifecycle map for `runtime_callback_object_init_once`, `runtime_callback_object_teardown_once`, `allocator_phase_finalize_pass`, and `entity_cleanup_resources_and_dispatch`.
|
||||
- The raw notes already constrain the video-state snapshot pair, one-time guards, vtable call slots, emitted payload pairs, and the relationship to the watch-controller release path.
|
||||
|
||||
### Porting work that should close the gap
|
||||
|
||||
- Port field-level comments around `0x4588`, `0x4590`, `0x4594`, `0x4595`, `0x45a6`, and the emitted payload pairs before attempting any subsystem rename.
|
||||
- Reconcile the raw callback emissions with the NE caller windows in segments `91`, `95`, and `138`.
|
||||
- Only promote a behavior-level subsystem name once those emitted payload pairs and caller families converge in the NE project too.
|
||||
|
||||
### Live anchors in the open NE project
|
||||
|
||||
- `000a:4913` -> `runtime_callback_object_init_once`
|
||||
- `0009:b1c3` -> `allocator_phase_finalize_pass`
|
||||
- `000d:9afd` -> `entity_cleanup_resources_and_dispatch`
|
||||
|
||||
## Priority 5: Parser / RIFF / Animation Video-Subframe Lane
|
||||
|
||||
### Why it is still unclear in `CRUSADER.EXE`
|
||||
|
||||
- The main parser and animation entrypoints are already named in the NE project, but the surrounding segment coverage is still weak.
|
||||
- The ledger still shows `None` for the raw-`000e`-mapped segments `99` through `103`.
|
||||
- `000e:ffb0` is still only a positional function object and remains overlap-damaged.
|
||||
|
||||
### What the raw project already knows
|
||||
|
||||
- The raw notes already define the parser cluster, RIFF chunk matching, animation ring-buffer model, and the audio/video split.
|
||||
- The raw-side caller proof already ties `000e:ffb0` to the `00db` / `00dc` video-frame lane paired with `anim_load_audio_frame`.
|
||||
- The raw notes also already separate the text-oriented parser usage from the binary owner-loaded USECODE lane.
|
||||
|
||||
### Porting work that should close the gap
|
||||
|
||||
- Port the parser and animation comments into the NE segment windows that still show no formal coverage.
|
||||
- Keep `000e:ffb0` conservative, but import the raw-side comment that it is the unresolved video-side subframe loader for the `00db` / `00dc` chunk lane.
|
||||
- Normalize the segment-to-topic mapping for the `000e` range in the NE coverage ledger once those comments are in place.
|
||||
|
||||
### Live anchors in the open NE project
|
||||
|
||||
- `000e:2104` -> `animation_start`
|
||||
- `000e:3639` -> `record_table_parse_buffer`
|
||||
- `000e:ffb0` -> `FUN_000e_ffb0`
|
||||
- Boundary analysis for `000e:ff90..000f:00f0` still reports `FUN_000e_ffb0 @ 000e:ffb0 body 000e:ffb0 - 000f:00e0`
|
||||
|
||||
## Priority 6: Gameplay / Input / Projectile Expansion Into Adjacent NE Lanes
|
||||
|
||||
### Why it is still unclear in `CRUSADER.EXE`
|
||||
|
||||
- Segment `1` itself is already deep, but the adjacent event/dispatch and targeting lanes are not equally complete.
|
||||
- The ledger still shows only `Partial` coverage for `seg021` and `seg043` even though the raw project already holds stronger gameplay-side anchors.
|
||||
- The old disasm corpus has shape and event-code evidence that can explain object-heavy gameplay lanes, but that evidence has not yet been turned into a disciplined NE crosswalk.
|
||||
|
||||
### What the raw project already knows
|
||||
|
||||
- Verified raw imports already exist for `shot_entity_alloc`, `projectile_update_tick`, and the rest of the main projectile chain.
|
||||
- `seg021` already has a verified direct raw import anchor via `entity_count_by_type_a`.
|
||||
- The external corpus adds key/event notes such as `CTRL-Q = 0x410` and map-backed shape examples like `STEAM2` and `SnapEgg`.
|
||||
- The raw side also already narrowed the seg043 start-of-segment hole enough to reject the earlier bad mapping.
|
||||
|
||||
### Porting work that should close the gap
|
||||
|
||||
- Extend from the already-verified gameplay names into adjacent NE dispatch/targeting callers only when the segment:offset and caller role match directly.
|
||||
- Build one conservative shape-id / map-placement crosswalk for trigger-heavy or object-heavy gameplay lanes before promoting new names.
|
||||
- Use the old key/event notes as lead generation only, not as direct rename authority.
|
||||
|
||||
### Live anchors in the open NE project
|
||||
|
||||
- `0007:28ce` -> `shot_entity_alloc`
|
||||
- `0007:371d` -> `projectile_update_tick`
|
||||
- `0007:5a00` has no current symbol
|
||||
- Boundary analysis for `0007:59e0..0007:5b40` still reports `0007:5a00` as a candidate entry and keeps the earlier rejected mapping explicitly closed
|
||||
|
||||
## Secondary Structural Gaps To Keep On The Tracker
|
||||
|
||||
These are real NE holes, but they are weaker `old -> new` closure candidates than the priorities above because the raw-side model is still not strong enough to drive safe naming by itself.
|
||||
|
||||
### `000b:2e00`
|
||||
|
||||
- Still a high-traffic gap in the NE project.
|
||||
- Live symbol lookup returns no symbol at `000b:2e00`.
|
||||
- Boundary analysis for `000b:2de0..000b:3050` shows a missing top-level entry at `000b:2e00` plus several later artificial subfunctions.
|
||||
- Keep it on the list, but do not treat it as the first raw-to-NE porting target.
|
||||
|
||||
## Immediate Actions Justified Now
|
||||
|
||||
- Create and maintain this tracker document.
|
||||
- Use it as the ranked starting point for the first focused `CRUSADER.EXE` porting batch.
|
||||
- Avoid immediate new Ghidra renames in the listed holes unless a pass first repairs the overlapping function objects or recovers the missing callers.
|
||||
- Conservative comments are justified before renames in the VM, transition, and animation lanes because the remaining uncertainty is mostly about caller provenance and boundaries, not about the already-known local contracts.
|
||||
|
||||
## Progress Estimate Impact
|
||||
|
||||
This note still does not justify a headline percentage change by itself.
|
||||
|
||||
The `plan-mid.md` progress estimates should stay unchanged until one of these prioritized NE passes produces verified new symbol, boundary, or ledger promotions beyond this local caller-family classification.
|
||||
|
|
@ -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