Documentation improvements
This commit is contained in:
parent
d78808d6b5
commit
c34f481c3a
34 changed files with 2800 additions and 20 deletions
210
docs/command-line-parameters.md
Normal file
210
docs/command-line-parameters.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# Crusader Command-Line Parameters
|
||||
|
||||
## Scope
|
||||
|
||||
This note consolidates the currently recovered startup/debug argument set across the retail Crusader executables.
|
||||
|
||||
The current strongest parser/control-flow evidence now comes from both live targets:
|
||||
|
||||
- `REGRET.EXE`
|
||||
- `CRUSADER.EXE`
|
||||
|
||||
Two conclusions matter most for the recent startup-map work:
|
||||
|
||||
- the executable patch was still required to change the stock fresh-game start map
|
||||
- the built-in manual/debug warp lane is real, and its direct-coordinate form is `-warp <mission> [x y z]`
|
||||
|
||||
That direct-coordinate syntax is now confirmed in both retail games, not just Regret.
|
||||
|
||||
## Short Answer
|
||||
|
||||
If the question is `could the normal first-mission executable hack have been replaced by parameters all along?`, the answer is still no.
|
||||
|
||||
The stock new-game path hardcodes map `1`, egg `0x1e` in code. The parameterized warp path is a separate startup/debug branch that only runs when `-warp` is present.
|
||||
|
||||
If the question is `can the executable already warp into another map without patching files?`, the answer is yes.
|
||||
|
||||
The current best-proven forms are:
|
||||
|
||||
- `-warp <mission>`
|
||||
- `-warp <mission> <x> <y> <z>`
|
||||
- optional `-mapoff <delta>` and `-egg <id>` modifiers
|
||||
|
||||
The important restriction is that `-egg` beats X/Y/Z. If you supply a nonnegative egg override, the game routes through the teleporter-egg path and does not use the direct-coordinate teleport.
|
||||
|
||||
That precedence rule is also now confirmed in both retail games:
|
||||
|
||||
- `REGRET.EXE` routes through `Game_RunNewGameFlow`
|
||||
- `CRUSADER.EXE` routes through `Game_Start`
|
||||
|
||||
For the specific `-u` question, the current live No Remorse answer is now yes.
|
||||
|
||||
The regular non-Japanese `CRUSADER.EXE` still contains a real `-u` lane, and the current best read is no longer `unresolved leftover token`. In this build it is a live startup override for the usecode/EUSECODE load source.
|
||||
|
||||
## Parameter Table
|
||||
|
||||
| Argument | Current best syntax | Confidence | Current best effect |
|
||||
|------|------|------|------|
|
||||
| `-?` | `-?` | Medium | Help/usage-style startup path. The option is present in the parser table, and the nearby startup strings include `You DO need help!`. The exact REGRET-side case-to-string tie was not re-closed in this pass. |
|
||||
| `-u` | `-u <arg>` | High | Live in retail `CRUSADER.EXE`. The parser copies the following token into `1478:065a`, and `startup_apply_u_override_if_present` later uses that buffer as a startup usecode/EUSECODE load-source override. The JP build matches the same interpretation. |
|
||||
| `-debug` | `-debug` | High | Live parser branch. Raises the debug-print threshold, prints `Debugging mode ON.`, and enables the known movie-player timing overlay / debug-print lane. |
|
||||
| `-setver` | `-setver <text>` | Medium | Still present in the broader Crusader parser-family tables, but the earlier CRUSADER-side assignment of the string-copy branch now belongs to `-u`. Treat the exact retail `CRUSADER.EXE` `-setver` behavior as still unresolved here; the older version/banner read is now best treated as REGRET-weighted rather than closed for both games. |
|
||||
| `-asylum` | `-asylum` | High | Joke/no-op style switch. Current best recovered effect is printing `Enabling ENHANCED mode. (NOT!)` with no stronger gameplay-side state change recovered in this pass. |
|
||||
| `-warp` | `-warp <mission>` | High | Enables the manual/debug warp lane by setting `g_warpToLevelNoArg`. The runtime then computes `target_map = mission_table[mission] + mapoff`. |
|
||||
| `-warp` with coordinates | `-warp <mission> <x> <y> <z>` | High | Direct coordinate warp. After parsing the mission number, the parser checks the next argv token. If it is present and does not begin with `-`, the next three argv tokens are parsed as X, Y, and Z. |
|
||||
| `-skill` | `-skill <n>` | High | Parses a difficulty override into `1480:0ace`. The parser clamps `0` up to `1`, then `Game_RunNewGameFlow` copies the value into the difficulty level before the mission-start hop. |
|
||||
| `-mapoff` | `-mapoff <delta>` | High | Adds an offset to the mission-table result inside the manual/debug warp path. It does not affect the stock fresh-game selector when `-warp` is absent. |
|
||||
| `-egg` | `-egg <id>` | High | Sets the destination egg override used by the warp path. When the value is nonnegative, it takes precedence over the direct-coordinate teleport branch. |
|
||||
| `-demo` | `-demo` | High | Sets `1480:0ad4`, prints `Demo mode.`, and changes the ORIGIN/ANIM01 startup-video behavior in `Game_RunNewGameFlow`. |
|
||||
| `-laurie` | `-laurie` | High | Separate from the main `HandleCommandlineArgs` switch table. This is the broader cheat/debug-enablement argument already documented in the live No Remorse notes. |
|
||||
|
||||
## Warp Syntax And Precedence
|
||||
|
||||
The current best evidence-backed parser behavior is now confirmed in both `REGRET.EXE` and `CRUSADER.EXE`.
|
||||
|
||||
In the No Remorse live database, the exact matching parser case is `HandleCommandlineArgs` at `1048:0adc`, and the matching consumer branch is in `Game_Start` at `1020:029e` / `1020:02d0`.
|
||||
|
||||
In the Regret live database, the matching parser case is `HandleCommandlineArgs` at `1058:0cd2`, and the matching consumer branch is in `Game_RunNewGameFlow` at `1030:0628` / `1030:069a`.
|
||||
|
||||
The shared behavior is:
|
||||
|
||||
1. `-warp` always consumes one numeric mission argument.
|
||||
2. The parser then peeks at the next argv token.
|
||||
3. If that token is missing or begins with `-`, the game takes the mission-only path and prints `Warping to mission %d.`.
|
||||
4. Otherwise the parser consumes the next three argv tokens as X, Y, and Z, stores them in `1480:0ac8`, `1480:0aca`, and `1480:0acc`, and prints `Warping to mission %d @ x:%d y:%d z:%d.`.
|
||||
|
||||
For No Remorse the corresponding X/Y/Z globals are the earlier siblings:
|
||||
|
||||
- `1478:084c` = X override
|
||||
- `1478:084e` = Y override
|
||||
- `1478:0850` = Z override
|
||||
|
||||
That means the recovered syntax is positional:
|
||||
|
||||
- `-warp <mission>`
|
||||
- `-warp <mission> <x> <y> <z>`
|
||||
|
||||
This pass did not recover any separate literal `-x`, `-y`, or `-z` switches in the startup parser.
|
||||
|
||||
At runtime, both games apply the same precedence:
|
||||
|
||||
1. Compute the target map from the embedded mission-to-map table plus `-mapoff`.
|
||||
2. If X is unset (`0xffff` / `-1`), use the egg teleporter path.
|
||||
3. If X is set but `-egg` is also nonnegative, still use the egg teleporter path.
|
||||
4. Only when X/Y/Z are present and the egg override is still negative does the game bypass egg lookup and call `NPC_Teleport` directly.
|
||||
|
||||
No Remorse’s direct consumer branch is especially clear:
|
||||
|
||||
- `1020:029e` checks whether X is still `-1`
|
||||
- `1020:02a5` / `1020:02d0` check whether `-egg` is nonnegative
|
||||
- `1020:02d7..02eb` call `NPC_Teleport` only when X/Y/Z are present and egg override is still negative
|
||||
|
||||
## Eggless Maps And The Non-Patching Workaround
|
||||
|
||||
This directly answers the recent map-254 style question.
|
||||
|
||||
There is a parameter-only route into maps with no usable destination egg, but it is not `-egg`.
|
||||
|
||||
The working shape is:
|
||||
|
||||
- `-warp <mission> <x> <y> <z> -mapoff <delta>`
|
||||
|
||||
with `-egg` omitted.
|
||||
|
||||
Why that matters:
|
||||
|
||||
- `-egg` keeps the code on the teleporter/egg path
|
||||
- the direct-coordinate form falls into `NPC_Teleport`
|
||||
- the inspected `NPC_Teleport` lane writes the exact map/X/Y/Z values and moves the camera there
|
||||
- the currently inspected teleport path does not show any automatic `snap-to-ground` or `find a safe spawn` rescue
|
||||
|
||||
So the non-patching workaround for an eggless map is `known-good coordinates`, not `some hidden eggless-map flag`.
|
||||
|
||||
If the supplied X/Y/Z values are bad, the game can still strand the avatar in empty space even though the warp itself succeeded.
|
||||
|
||||
## What `-u` Does In Retail `CRUSADER.EXE`
|
||||
|
||||
The live non-Japanese No Remorse database now closes `-u` much more tightly than the older note.
|
||||
|
||||
In `HandleCommandlineArgs`, the retail parser case at `1048:0a46` consumes the next argv token and copies it into the fixed startup buffer at `1478:065a`.
|
||||
|
||||
The only recovered consumer of that buffer in the same retail executable is `startup_apply_u_override_if_present` at `1420:0cdf`.
|
||||
|
||||
Recovered behavior in that helper:
|
||||
|
||||
- check whether `1478:065a` is non-empty
|
||||
- if empty, leave the default startup path alone and return
|
||||
- if non-empty, route startup through an alternate load/resolve path using the copied string
|
||||
- store the resulting far pointer in `1478:6611/6613`
|
||||
- set the loaded-state byte at `1478:6615`
|
||||
- rebuild the cumulative slot-base words at `1478:8c7c..8c82`
|
||||
|
||||
That is strong retail No Remorse evidence for this meaning:
|
||||
|
||||
> `-u <arg>` is a live startup override for the usecode/EUSECODE load source.
|
||||
|
||||
What is still not fully closed from this static pass is the exact Filespec path syntax for the copied token.
|
||||
|
||||
What is now materially tighter:
|
||||
|
||||
- the helper does **not** treat the argv token as the final archive filename
|
||||
- it uses the token as the `path` component to `Filespec_GetFullPath`
|
||||
- it uses the mutable filename template at `1478:07a0`, which is `eusecode.flx`, as the fixed `filename` component
|
||||
- it forces that template's first byte to `'e'` before the existence probe and final load call
|
||||
|
||||
So the safest current retail read is:
|
||||
|
||||
- `-u <arg>` expects a directory/resource-root style path argument for the standard `eusecode.flx` archive family
|
||||
- it does **not** currently look like a free-form arbitrary filename override
|
||||
|
||||
But the important uncertainty is now only `exact naming rules`, not `whether the switch is real`. In the regular non-Japanese `CRUSADER.EXE`, the switch is clearly still live.
|
||||
|
||||
This also aligns with the already-stronger JP Win32 result, where the matching `-u` lane was recovered as the same kind of usecode override.
|
||||
|
||||
For the deeper runtime-side investigation of whether `-u` replaces or augments the stock usecode root, and what game systems that replacement feeds, see [docs/usecode-startup-override.md](docs/usecode-startup-override.md).
|
||||
|
||||
Current best answer from that follow-up is:
|
||||
|
||||
- `-u` behaves like a replacement of the live usecode runtime root, not an additive overlay
|
||||
- the replaced root is then used by normal item-event dispatch, usecode process creation, interpreter bytecode stepping, and gameplay-side scripted capability checks
|
||||
|
||||
The current best replacement-vs-addition answer is also now stronger:
|
||||
|
||||
- there is one live root at `1478:6611/6613`
|
||||
- `startup_apply_u_override_if_present` overwrites that root directly when `-u` is present
|
||||
- later event/process/interpreter consumers read the replacement root through the same global pair
|
||||
|
||||
So the safest current retail read is `full runtime root replacement for the session`, not `load one extra add-on script beside stock usecode`.
|
||||
|
||||
## What `-setver` Most Likely Does
|
||||
|
||||
`-setver` should now be treated more cautiously in this consolidated note.
|
||||
|
||||
For retail `CRUSADER.EXE`, the string-copy branch that had previously been grouped under `-setver` is now better closed as the live `-u` handler described above.
|
||||
|
||||
That means the older `displayed version/build string override` reading is no longer closed for the non-Japanese No Remorse executable itself.
|
||||
|
||||
Current evidence bundle:
|
||||
|
||||
- the option name is still present in the broader Crusader parser-family tables
|
||||
- older REGRET-side evidence still points toward a startup/UI presentation role rather than gameplay control
|
||||
- but this pass did not directly re-close the exact retail No Remorse `-setver` consumer after reassigning the resolved string-copy branch to `-u`
|
||||
|
||||
Current best read:
|
||||
|
||||
- `-setver <text>` still looks more like a presentation/version banner switch than a gameplay/startup-map selector
|
||||
- but that statement is now provisional for consolidated retail notes until the retail `CRUSADER.EXE` handler is isolated directly
|
||||
|
||||
The exact retail No Remorse `-setver` branch is therefore an open cleanup item again, while `-u` is now the closed string-consuming retail startup override.
|
||||
|
||||
## No Remorse Cross-Check Summary
|
||||
|
||||
The main recent uncertainty was whether Regret had gained an extra direct-coordinate warp feature that No Remorse lacked. The live `CRUSADER.EXE` pass now closes that question.
|
||||
|
||||
Current best answer:
|
||||
|
||||
- No Remorse supports the same positional `-warp <mission> [x y z]` syntax.
|
||||
- No Remorse uses the same `-egg` precedence rule over X/Y/Z.
|
||||
- No Remorse reaches the same direct `NPC_Teleport` style fallback when coordinates are present and the egg override is still negative.
|
||||
|
||||
So the parameter-only workaround for eggless maps is not Regret-specific. The same approach should work in No Remorse too, as long as the supplied coordinates are valid for the chosen target map.
|
||||
225
docs/editor-object-visibility.md
Normal file
225
docs/editor-object-visibility.md
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
# Editor-Object Visibility In Retail Crusader
|
||||
|
||||
## Question
|
||||
|
||||
Investigate whether the retail game explicitly hides editor-only map objects during gameplay, and whether any built-in debug, cheat, or command-line switch can enable those objects in the normal in-game renderer.
|
||||
|
||||
Active analysis target for the binary side was live `CRUSADER.EXE` in the Ghidra MCP session.
|
||||
|
||||
## Result
|
||||
|
||||
Current best answer:
|
||||
|
||||
- Yes, the retail executable explicitly hides editor-tagged shapes in the normal world-item render path, with at least two recovered `SI_EDITOR` gates.
|
||||
- No built-in retail flag, cheat, or command-line switch has been recovered so far that re-enables those editor objects in the normal in-game renderer.
|
||||
- The closest confirmed "show editor items" toggle is in ScummVM/Pentagram, not in the original retail executable.
|
||||
|
||||
## Binary Findings In `CRUSADER.EXE`
|
||||
|
||||
### 1. The downstream sprite painter explicitly skips `SI_EDITOR`
|
||||
|
||||
The strongest direct proof is live function `Item_PaintSprite` at `1198:02e4`.
|
||||
|
||||
Verified decompile behavior:
|
||||
|
||||
- It loads the current item's shape number from `g_itemShapeNos`.
|
||||
- It resolves the matching `ShapeData` entry from `g_shapeData`.
|
||||
- It immediately checks `local_8->flags2 & 1`.
|
||||
- When that bit is set, the function returns before caching or drawing the shape.
|
||||
|
||||
Relevant instruction window at `1198:0332..033d`:
|
||||
|
||||
```text
|
||||
1198:0332 MOV AL,byte ptr ES:[BX + 0x6]
|
||||
1198:0336 AND AX,0x1
|
||||
1198:0339 OR AX,AX
|
||||
1198:033b JZ 1198:0340
|
||||
1198:033d JMP 1198:0959
|
||||
```
|
||||
|
||||
`1198:0959` is the function epilogue, so the branch is an unconditional early-out for bit `0` of shape byte `6`.
|
||||
|
||||
This aligns with the existing `SI_EDITOR` comment at `1198:0339` and with the open-source Crusader typeflag decoders, where Crusader `TYPEFLAG.DAT` byte `6`, bit `0` maps to `SI_EDITOR`.
|
||||
|
||||
### 2. The paired setup path still processes those items
|
||||
|
||||
The neighboring helper `Item_SetScreenBoxAndWorldCoords` at `1198:095d` still computes:
|
||||
|
||||
- world coordinates,
|
||||
- projected screen coordinates,
|
||||
- bounding boxes,
|
||||
- and weapon-overlay extents.
|
||||
|
||||
That function does **not** perform an `SI_EDITOR` check.
|
||||
|
||||
Current safest read after the first pass was:
|
||||
|
||||
- editor objects remain present in the live item/cache/display-list setup state,
|
||||
- but the normal sprite paint stage refuses to draw them.
|
||||
|
||||
That read turned out to be incomplete rather than wrong.
|
||||
|
||||
### 3. The active world-item render-list builder also skips `SI_EDITOR` earlier
|
||||
|
||||
Follow-up tracing from the live camera redraw path into segment `1180` found a more important upstream gate in the world-item render-list builder.
|
||||
|
||||
Relevant instruction window at `1180:0951..095c`:
|
||||
|
||||
```text
|
||||
1180:0951 MOV AL,byte ptr ES:[BX + 0x6]
|
||||
1180:0955 AND AX,0x1
|
||||
1180:0958 OR AX,AX
|
||||
1180:095a JZ 1180:095f
|
||||
1180:095c JMP 1180:0bec
|
||||
```
|
||||
|
||||
This is the same Crusader `TYPEFLAG.DAT` byte `6`, bit `0` test, but here the consequence is stronger than in `Item_PaintSprite`: the builder skips the world-item draw-node allocation path entirely before that item can ever reach the normal draw dispatch.
|
||||
|
||||
That closes the runtime discrepancy from the first patch attempt. Patching only the later `1198:033b` branch changed a real live draw method, but it did **not** make editor objects visible in-game because the active renderer had already filtered them out earlier at `1180:0951..095c`.
|
||||
|
||||
### 4. Why the first executable patch appeared to do nothing
|
||||
|
||||
The first public patch only flipped the downstream `Item_PaintSprite` branch:
|
||||
|
||||
```text
|
||||
1198:033b JZ 1198:0340 -> EB 03
|
||||
```
|
||||
|
||||
That patch was not dead code. Export relocation evidence still shows the draw-node method slot at `1478:2c1f` dispatching to `1198:02e4`, so the downstream painter is a real live node callback.
|
||||
|
||||
But the active world-item renderer never reached that callback for editor-tagged shapes, because the upstream builder branch at `1180:095a..095c` had already skipped node creation.
|
||||
|
||||
Current patching implication:
|
||||
|
||||
- patching `1198:033b` alone is insufficient,
|
||||
- the upstream `1180:095a` branch is the controlling visibility gate for normal world-item rendering,
|
||||
- and a practical visibility patch should flip both recovered `SI_EDITOR` branches together.
|
||||
|
||||
### 5. The recovered item-flag helpers match the same shape-bit model
|
||||
|
||||
`Item_GetTypeFlagCrusader` reads arbitrary shape/typeflag bits from the per-shape `ShapeData` records.
|
||||
|
||||
Additional direct flag helpers nearby include:
|
||||
|
||||
- `Item_IsTargetable`, which reads `flags2 & 0x10`
|
||||
- `Item_IsShapeFlagOccl`, which reads `flags0 & 0x10`
|
||||
|
||||
That supports the interpretation that `Item_PaintSprite` is not testing some temporary per-item state. It is reading a stable per-shape editor flag.
|
||||
|
||||
## Negative Search For A Retail "Show Editor Items" Toggle
|
||||
|
||||
I checked the known retail debug/cheat/control lanes already documented in the live database and repo notes.
|
||||
|
||||
### `-debug`
|
||||
|
||||
Retail `-debug` is live at `1048:0a93`, but the current closed behavior is:
|
||||
|
||||
- raise `g_debugMsgLevel` to `10`
|
||||
- print `Debugging mode ON.`
|
||||
- set `1478:0845`
|
||||
- enable the seg1468 video-player timing overlay via `1478:0859`
|
||||
|
||||
No recovered link from `-debug` reaches the `SI_EDITOR` skip in `Item_PaintSprite` or enables a parallel item-paint path.
|
||||
|
||||
### Cheat/debug hotkeys
|
||||
|
||||
Recovered cheat/debug hotkeys do toggle several overlays, but the confirmed ones are unrelated to editor-shape rendering:
|
||||
|
||||
- `Ctrl+F7` at `13e8:1a20` toggles `1478:0ee0`, the egg-hatcher trigger-range overlay.
|
||||
- `Alt+F7` at `13e8:1a50` toggles `1478:2bc9`, another cheat-gated overlay lane.
|
||||
- The remaining F7-family toggle uses `1478:2bca` for the coarse grid overlay.
|
||||
|
||||
Those paths force camera redraws, but the recovered behavior is overlay drawing, not "draw editor-tagged world items."
|
||||
|
||||
### Hidden usecode debugger / Laurie lane
|
||||
|
||||
The hidden seg109/seg1408 usecode-debugger lane remains real, and `-laurie` plus the cheat/debug latches still enable related hidden behavior. But no recovered debugger-side state or menu action currently bypasses the `Item_PaintSprite` editor skip, and no candidate global has been found that matches ScummVM's later `showEditorItems` concept.
|
||||
|
||||
### Event `0x410`
|
||||
|
||||
This lane remains unrelated.
|
||||
|
||||
Current live conclusion is unchanged:
|
||||
|
||||
- retail `0x410` toggles the CD transfer display state,
|
||||
- it is not the immortality toggle,
|
||||
- and it is not an editor-object visibility switch.
|
||||
|
||||
## Cross-Check Against Open-Source Engine Behavior
|
||||
|
||||
ScummVM and Pentagram both independently preserve the same semantic meaning for the Crusader editor flag.
|
||||
|
||||
### Flag mapping
|
||||
|
||||
In both codebases, Crusader `TYPEFLAG.DAT` byte `6`, bit `0` maps to `SI_EDITOR`.
|
||||
|
||||
ScummVM `type_flags.cpp`:
|
||||
|
||||
```cpp
|
||||
if (data[6] & 0x01) si._flags |= ShapeInfo::SI_EDITOR;
|
||||
```
|
||||
|
||||
Pentagram `TypeFlags.cpp`:
|
||||
|
||||
```cpp
|
||||
if (data[6] & 0x01) si.flags |= ShapeInfo::SI_EDITOR;
|
||||
```
|
||||
|
||||
### Runtime treatment
|
||||
|
||||
ScummVM `game_map_gump.cpp` shows the exact higher-level behavior one would expect from the retail binary finding:
|
||||
|
||||
```cpp
|
||||
if (!showEditorItems && item->getShapeInfo()->is_editor())
|
||||
continue;
|
||||
```
|
||||
|
||||
Pentagram documents the same meaning directly in `docs/u8typeflag.txt`:
|
||||
|
||||
- `bit 4 : editor shape (don't render in-game)`
|
||||
|
||||
For Crusader, the bit location differs in the later 9-byte Crusader format, but the semantic meaning is the same.
|
||||
|
||||
### Important limit of this cross-check
|
||||
|
||||
ScummVM exposes its own engine variable and debugger command:
|
||||
|
||||
- engine field: `_showEditorItems`
|
||||
- setter: `setShowEditorItems(bool flag)`
|
||||
- debugger command: `showEditorItems [on|off]`
|
||||
|
||||
That is useful as behavioral confirmation, but it is **not** evidence that retail `CRUSADER.EXE` ships with an equivalent built-in toggle.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The retail executable does contain explicit logic to hide editor-only objects during ordinary gameplay rendering.
|
||||
|
||||
Current best model:
|
||||
|
||||
1. map/editor objects exist in the same broad world-item/runtime data flow as ordinary items,
|
||||
2. the active world-item builder in segment `1180` checks `SI_EDITOR` and skips draw-node allocation when that bit is set,
|
||||
3. if such an item still reached the standard sprite painter, `Item_PaintSprite` would also early-out on the same bit,
|
||||
4. and no recovered retail debug/cheat/argument lane currently bypasses either gate.
|
||||
|
||||
I did **not** recover any retail equivalent of:
|
||||
|
||||
- a `showEditorItems` global,
|
||||
- a command-line switch that enables editor-object rendering,
|
||||
- a cheat/debug hotkey that enables editor-object rendering,
|
||||
- or a hidden debugger command that obviously bypasses the `SI_EDITOR` skip.
|
||||
|
||||
So the present evidence supports:
|
||||
|
||||
- explicit retail hiding behavior exists,
|
||||
- but no official or currently recovered internal toggle exists to reveal those objects in the shipped game.
|
||||
|
||||
If the goal is to see them in the original executable, the most direct current path is no longer searching cheats or args first. It is either:
|
||||
|
||||
- finding a second, debug-only display-list builder that bypasses the `1180:0951` gate, or
|
||||
- patching both recovered `SI_EDITOR` branches in a writable copy.
|
||||
|
||||
## Next RE Follow-Up If Revisited
|
||||
|
||||
- Recover the exact containing function and wider caller chain for the `1180:0951` world-item builder skip and check whether any alternate debug-only builder exists.
|
||||
- Inspect the segment `1180` redraw/display-list builders for a second path that draws world items outside the normal world-item allocation branch.
|
||||
- If a runtime proof is wanted, patch both recovered `SI_EDITOR` branches only on a writable executable copy and test whether editor shapes become visible without destabilizing collision or selection.
|
||||
369
docs/first-mission-map-selection.md
Normal file
369
docs/first-mission-map-selection.md
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
# First Mission Map Selection
|
||||
|
||||
## Question
|
||||
|
||||
What determines which map a fresh game starts on, and would changing the first mission from map `1` to map `248` require a code patch or an external data-file change?
|
||||
|
||||
For a deeper REGRET-side control-flow breakdown, including the now-named helper functions and startup globals around `Game_Start`, see `docs/regret-game-start.md`.
|
||||
|
||||
## Short Answer
|
||||
|
||||
For a normal new game, the start map is chosen in code, not from `CRUSADER.CFG` or another external mission-mapping file.
|
||||
|
||||
The live `CRUSADER.EXE` fresh-game path hardcodes:
|
||||
|
||||
- map array `1`
|
||||
- teleport egg `0x1e`
|
||||
|
||||
The relevant Remorse call is in `Game_Start`:
|
||||
|
||||
- `1020:0243 PUSH 0x1e0001`
|
||||
- `1020:0249 CALLF 0x1090:04ce`
|
||||
|
||||
No Regret also hardcodes the same values, but there are two relevant selectors:
|
||||
|
||||
- an early menu-start selector in `Game_Start` at `1008:1448`
|
||||
- the actual mission-start selector later in `FUN_1030_032d` at `1030:05c5`
|
||||
|
||||
Those sites all encode map `1`, egg `0x1e` in code. For No Regret specifically, patching only the earlier `Game_Start` site is not enough to change where a real new game starts, because the later `FUN_1030_032d` mission-start path repeats the same hardcoded selector.
|
||||
|
||||
Changing fresh-game startup from map `1` to map `248` would therefore be a program-code modification in the startup path, not a config-file tweak.
|
||||
|
||||
## Evidence Chain
|
||||
|
||||
### 1. Command-line parsing does not pick the default fresh-game map
|
||||
|
||||
`HandleCommandlineArgs` recognizes these mission/map-related switches:
|
||||
|
||||
- `-warp` -> stores mission number in `g_warpToLevelNoArg`
|
||||
- `-mapoff` -> stores an additive map offset in `g_mapoffArgValue`
|
||||
- `-egg` -> stores an egg override in `g_eggArgValue`
|
||||
|
||||
The same startup lane also clearly supports optional warp coordinates at runtime. The currently recovered evidence for that is the startup status string:
|
||||
|
||||
- `Warping to mission %d @ x:%d y:%d z:%d.`
|
||||
|
||||
and the paired startup globals used by the REGRET-side flow:
|
||||
|
||||
- `1480:0ac8` = warp X override
|
||||
- `1480:0aca` = warp Y override
|
||||
- `1480:0acc` = warp Z override
|
||||
|
||||
The parser/control-flow work is now tight enough to promote the actual syntax: `-warp <mission> [x y z]`.
|
||||
|
||||
That syntax is now confirmed directly in both retail games. In `CRUSADER.EXE`, the parser case at `1048:0adc` mirrors the Regret-side branch: it defaults X to `0xffff`, parses the mission into `1478:084a`, then either logs the mission-only path or consumes the next three argv tokens into `1478:084c`, `1478:084e`, and `1478:0850`.
|
||||
|
||||
The parser does not expose separate recovered `-x`, `-y`, or `-z` switches. Instead, after it parses the mission number, it looks at the next argv token. If the next token is missing or begins with `-`, the code takes the mission-only path. Otherwise it consumes the next three argv tokens as X, Y, and Z.
|
||||
|
||||
One important runtime detail also matters for later map-warp experiments: a nonnegative `-egg` override beats the coordinate path. This is now confirmed in both games. In No Remorse, `Game_Start` checks `1478:084c` at `1020:029e` and `1478:0856` at `1020:02a5` / `1020:02d0`; if `-egg` is nonnegative, the code still routes through `Teleporter_CreateProcessDirect` and ignores the direct-coordinate `NPC_Teleport` path.
|
||||
|
||||
This function only records override values. It does not set the normal fresh-game start map.
|
||||
|
||||
Relevant globals:
|
||||
|
||||
- `1478:084a` = `g_warpToLevelNoArg`
|
||||
- `1478:0854` = `g_mapoffArgValue`
|
||||
- `1478:0856` = `g_eggArgValue`
|
||||
|
||||
### 2. Normal new-game startup is hardcoded in `Game_Start`
|
||||
|
||||
`Game_Start` has two distinct startup branches:
|
||||
|
||||
- normal new game: no `-warp` argument
|
||||
- debug/manual warp: `g_warpToLevelNoArg != -1`
|
||||
|
||||
In the normal new-game branch, the game does this directly:
|
||||
|
||||
```c
|
||||
Camera_MoveTo(0,0,0,0);
|
||||
g_globsDone = 0;
|
||||
Camera_SetCentreOn(1);
|
||||
Teleporter_CreateProcessDirect(1,0x1e,1);
|
||||
```
|
||||
|
||||
The decompiler comment already matches the runtime meaning:
|
||||
|
||||
- `start on level 1, starting egg (0x1e)`
|
||||
|
||||
Disassembly of the key site:
|
||||
|
||||
- `1020:0241 PUSH 0x1`
|
||||
- `1020:0243 PUSH 0x1e0001`
|
||||
- `1020:0249 CALLF 0x1090:04ce`
|
||||
|
||||
This is the strongest direct proof that fresh-game mission start is code-selected.
|
||||
|
||||
### 2a. No Regret cross-check: `Game_Start` still contains an early hardcoded selector
|
||||
|
||||
The currently opened `REGRET.EXE` session shows the same hardcoded startup arguments in its own `Game_Start` body.
|
||||
|
||||
Relevant decompiler lane:
|
||||
|
||||
```c
|
||||
Cameara_SetCameraOn(0);
|
||||
Camera_MoveTo(0,0,0,0);
|
||||
DAT_1480_1453 = 0;
|
||||
Cameara_SetCameraOn(1);
|
||||
Teleporter_ChangeMap(1,0x1e,1);
|
||||
```
|
||||
|
||||
The `DAT_1480_1453 = 0` write at `1008:1437` is a useful local anchor, because the startup selector call follows immediately after it.
|
||||
|
||||
Disassembly of the key site:
|
||||
|
||||
- `1008:1437 MOV byte ptr [0x1453],0x0`
|
||||
- `1008:1446 PUSH 0x1`
|
||||
- `1008:1448 PUSH 0x1e0001`
|
||||
- `1008:144e CALLF 0x1030:0000`
|
||||
|
||||
This is a real hardcoded selector, but it is not the only No Regret control point.
|
||||
|
||||
### 2b. No Regret actual mission-1 start: the later new-game path repeats the same selector in `FUN_1030_032d`
|
||||
|
||||
The actual No Regret new-game path later reinitializes the world, runs the new-game videos/modal flow, and then performs the mission-start hop inside `FUN_1030_032d`.
|
||||
|
||||
Relevant decompiler lane:
|
||||
|
||||
```c
|
||||
if (g_warpToLevelNoArg == -1) {
|
||||
Camera_MoveTo(0,0,0,0);
|
||||
DAT_1480_1453 = 0;
|
||||
Cameara_SetCameraOn(1);
|
||||
FUN_1030_022e();
|
||||
Teleporter_ChangeMap(1,0x1e,1);
|
||||
Fade_SetPaletteToAllBlack();
|
||||
}
|
||||
```
|
||||
|
||||
Disassembly of the key selector site:
|
||||
|
||||
- `1030:05c3 PUSH 0x1`
|
||||
- `1030:05c5 PUSH 0x1e0001`
|
||||
- `1030:05cb PUSH CS`
|
||||
- `1030:05cc CALL 0x1030:0000`
|
||||
|
||||
This is the selector that matters for an actual No Regret mission-1 start after the early startup/menu flow. That is why changing only the earlier `1008:1448` site did not redirect a real new game.
|
||||
|
||||
So No Regret still does not move fresh-game map selection into external config, but the effective mission-start path is split across two hardcoded selectors instead of one.
|
||||
|
||||
### 3. The debug `-warp mission` path uses an executable-embedded mission-to-map table
|
||||
|
||||
If `g_warpToLevelNoArg != -1`, `Game_Start` does not use the hardcoded `1`. Instead it computes:
|
||||
|
||||
```c
|
||||
iVar5 = *(int *)(g_warpToLevelNoArg * 2 + 0x488) + g_mapoffArgValue;
|
||||
```
|
||||
|
||||
Disassembly:
|
||||
|
||||
- `1020:0254 MOV BX,word ptr [0x84a]`
|
||||
- `1020:0258 SHL BX,0x1`
|
||||
- `1020:025a MOV AX,word ptr [BX + 0x488]`
|
||||
- `1020:025e ADD AX,word ptr [0x854]`
|
||||
|
||||
Static bytes at `1478:0488`:
|
||||
|
||||
```text
|
||||
1478:0488: 00 00 01 00 03 00 05 00 07 00 09 00 0b 00 0d 00
|
||||
1478:0498: 0f 00 11 00 13 00 15 00 17 00 19 00 1b 00 1d 00
|
||||
1478:04a8: 28 00
|
||||
```
|
||||
|
||||
Interpreted as little-endian words, that table is:
|
||||
|
||||
- `0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 40`
|
||||
|
||||
Expanded as mission-to-map entries, the current recovered No Remorse table is:
|
||||
|
||||
| `-warp` mission | Base map from table |
|
||||
|------|------|
|
||||
| `0` | `0` |
|
||||
| `1` | `1` |
|
||||
| `2` | `3` |
|
||||
| `3` | `5` |
|
||||
| `4` | `7` |
|
||||
| `5` | `9` |
|
||||
| `6` | `11` |
|
||||
| `7` | `13` |
|
||||
| `8` | `15` |
|
||||
| `9` | `17` |
|
||||
| `10` | `19` |
|
||||
| `11` | `21` |
|
||||
| `12` | `23` |
|
||||
| `13` | `25` |
|
||||
| `14` | `27` |
|
||||
| `15` | `29` |
|
||||
| `16` | `40` |
|
||||
|
||||
So the `-warp mission` path also uses code/data embedded in the executable, not a separate mission-mapping file. The first real mission entry maps to `1`, and `-mapoff` can then shift it.
|
||||
|
||||
`-mapoff` therefore matters only inside the manual/debug warp path. It does not affect the ordinary fresh-game selector when no `-warp` argument is present.
|
||||
|
||||
Practical implication for the earlier patch question:
|
||||
|
||||
- if the goal was `start a normal new game on a different first map`, the executable patch was still required
|
||||
- if the goal was `enter a chosen mission/map through the debug/manual warp path`, `-warp` plus `-mapoff` already provided a built-in route without patching the startup selector itself
|
||||
- if the goal was `reach a map with no usable egg`, the built-in non-patching route is `-warp <mission> <x> <y> <z>` plus `-mapoff`, with `-egg` omitted so the code uses the direct `NPC_Teleport` path
|
||||
|
||||
The No Remorse cross-check now makes that last point stronger: this eggless-map workaround is not just a Regret quirk. `CRUSADER.EXE` uses the same parser shape and the same consumer-side precedence.
|
||||
|
||||
### 3a. Why `-warp 0 28670 30718 0 -mapoff 246` likely lands in a bad spot in No Remorse
|
||||
|
||||
The current cached scene data from `Crusader_Decomp_Public/map_renderer/.cache/scene-cache/remorse/map-246/bb7e36195d39ac72/scene.json` confirms that map `246` is real and nonempty:
|
||||
|
||||
- `rawItemCount = 504`
|
||||
- `itemCount = 1080`
|
||||
- `terrain = 902`
|
||||
- `egg = 149`
|
||||
- `editor = 10`
|
||||
- `npcLinkedItems = 16`
|
||||
- `invalidItemCount = 0`
|
||||
|
||||
So the black-room result is not explained by `map 246 does not exist` or `the cache is effectively empty`.
|
||||
|
||||
The more likely problem is the coordinate pair itself.
|
||||
|
||||
`-warp 0 -mapoff 246` does resolve to map `246`, because mission `0` maps to base map `0` and the runtime then adds the `246` offset.
|
||||
|
||||
But the cached scene data does **not** show `x = 28670, y = 30718` as a recovered world-coordinate pair on map `246`.
|
||||
|
||||
What the cache does show is that those numbers belong to different active coordinate bands on that map:
|
||||
|
||||
- `x = 28670` appears repeatedly with y-values such as `19454`, `20478`, `21502`, `22526`, `23550`, `24574`, and `25598`
|
||||
- `x = 30718` appears repeatedly with the same lower/mid y-bands such as `19454`, `20478`, `21502`, `22526`, `23550`, `24574`, and `25598`
|
||||
- `y = 30718` is also real on map `246`, but in a different band; one cached pair there is `x = 22526, y = 30718, z = 0`
|
||||
|
||||
That means the failing command is very likely mixing a valid X lane from one populated region with a valid Y lane from a different populated region.
|
||||
|
||||
Current best explanation for the black-room result:
|
||||
|
||||
- the direct `NPC_Teleport` path is working
|
||||
- map `246` itself is present and populated
|
||||
- but `28670,30718,0` is not a recovered occupied/world-backed placement from the cached scene, so the avatar is probably landing in an unhelpful void or dark staging cell rather than on the visible floor grid
|
||||
|
||||
The cache suggests more defensible test candidates would be real paired coordinates already present in the scene, for example:
|
||||
|
||||
- `28670,25598,0`
|
||||
- `30718,25598,0`
|
||||
- `22526,30718,0`
|
||||
|
||||
Those are not yet promoted here as guaranteed safe spawn points, but they are materially stronger than mixing one X value and one Y value that the cache never shows together as a real item location.
|
||||
|
||||
## How The Map Number Is Consumed
|
||||
|
||||
`Teleporter_CreateProcessDirect` stores the passed map number into the teleporter process:
|
||||
|
||||
- `struct TeleporterProcess::mapno` at offset `0x32`
|
||||
|
||||
`TeleporterProcess_Run` then compares that `mapno` against `g_currentMapArray`.
|
||||
|
||||
If the target map differs from the current map, it moves the avatar/camera onto that map array before searching for the destination teleport egg.
|
||||
|
||||
Relevant logic:
|
||||
|
||||
```c
|
||||
if (p_proc->mapno != g_currentMapArray) {
|
||||
if (DAT_1478_085f == '\0') {
|
||||
Camera_MoveTo(0,0,0,p_proc->mapno);
|
||||
}
|
||||
else {
|
||||
NPC_Teleport(&g_avatarItemNo,0,0,0,(byte)p_proc->mapno);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then it searches for a teleport egg with the requested id and completes the move on that same target map.
|
||||
|
||||
This means the startup value is a real map-array selector, not just a mission label.
|
||||
|
||||
## Where The External Data Starts
|
||||
|
||||
The selected map contents are external data, even though the startup choice is hardcoded.
|
||||
|
||||
The clearest loader anchor is `ItemCache_InitAndLoadFixedDat`, which:
|
||||
|
||||
- initializes item data
|
||||
- calls `FixedDat_LoadData(...)`
|
||||
- checks for `static\fixed.dat`
|
||||
- loads the optional patch-layer `static\fixed.dat`
|
||||
- resets `g_currentMapArray = 0xffff`
|
||||
|
||||
`FixedDat_LoadData` builds a full path using `g_fixedDatFilenamePtr`, which is the game-side `fixed.dat` asset name.
|
||||
|
||||
Cross-check from the existing map-rendering note:
|
||||
|
||||
- `FIXED.DAT` contains a map table
|
||||
- map count is stored at file offset `0x54`
|
||||
- map entries live in a table at file offset `0x80`
|
||||
|
||||
So the world geometry/object placement for map `1`, map `248`, and the rest lives in external map resources, but the decision to start a fresh game on map `1` is made in code.
|
||||
|
||||
## What Is Not Controlling Fresh-Game Map Selection
|
||||
|
||||
### `CRUSADER.CFG`
|
||||
|
||||
The local `CRUSADER.CFG` only contains sound/video/install settings such as:
|
||||
|
||||
- `irq`
|
||||
- `dma`
|
||||
- `port`
|
||||
- `sound`
|
||||
- `music`
|
||||
- `fullinstall`
|
||||
- `bigvideo`
|
||||
- `subtitles`
|
||||
- `cdletter`
|
||||
- `flicpath`
|
||||
|
||||
No mission-start or map-start key is present.
|
||||
|
||||
### `LoadConfigFile`
|
||||
|
||||
`LoadConfigFile` parses `crusader.cfg`, but the recovered keys are audio/video/control/install options and custom music-tune redirects. This function does not provide a fresh-game mission-to-map selector.
|
||||
|
||||
## Practical Conclusion For A Map-248 Hack
|
||||
|
||||
If the goal is specifically:
|
||||
|
||||
- normal first mission
|
||||
- fresh game
|
||||
- load map `248` instead of map `1`
|
||||
|
||||
then the controlling point is the hardcoded new-game startup call inside `Game_Start`, not an external config file.
|
||||
|
||||
The cleanest future patch-design targets are therefore:
|
||||
|
||||
- the immediate startup call that currently passes map `1`, egg `0x1e`
|
||||
- or a nearby wrapper rewrite that substitutes the startup map before `Teleporter_CreateProcessDirect` runs
|
||||
|
||||
The external data side still matters, because map `248` must exist as a valid entry in the shipped map resources and must contain a compatible teleport egg if the startup egg remains `0x1e`.
|
||||
|
||||
## Script Patcher Update
|
||||
|
||||
The public PowerShell patcher now supports both supported retail executables:
|
||||
|
||||
- `CRUSADER.EXE` for No Remorse
|
||||
- `REGRET.EXE` for No Regret
|
||||
|
||||
It now chooses the game mode from whichever supported executable is present next to the script, then resolves the on-disk patch site by scanning for selector signatures instead of relying on one fixed file offset.
|
||||
|
||||
The runtime signatures used by the patcher are the selector payload plus the nearby call/stack cleanup shape:
|
||||
|
||||
```text
|
||||
No Remorse, and No Regret menu-start:
|
||||
6A 01 66 68 01 00 1E 00 9A ?? ?? ?? ?? 83 C4 06
|
||||
|
||||
No Regret mission-start:
|
||||
6A 01 66 68 01 00 1E 00 0E E8 ?? ?? 83 C4 06
|
||||
```
|
||||
|
||||
For No Regret, the patcher now updates both hardcoded selectors so the later mission-start path cannot silently override the earlier menu-start one.
|
||||
|
||||
## Current Best Answer
|
||||
|
||||
- Fresh-game first-mission start map is determined in code.
|
||||
- No Remorse hardcodes map `1`, egg `0x1e` in `Game_Start`.
|
||||
- No Regret hardcodes the same values twice: once in `Game_Start`, and again in the later `FUN_1030_032d` mission-start path that actually controls a real new game.
|
||||
- The debug `-warp mission` path uses an executable-embedded mission-to-map word table at `1478:0488`, plus `-mapoff`.
|
||||
- External files such as `FIXED.DAT` hold the actual map contents.
|
||||
- `CRUSADER.CFG` does not control which map a new game starts on.
|
||||
169
docs/jp-remorse-cheats-and-launch-params.md
Normal file
169
docs/jp-remorse-cheats-and-launch-params.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# Japanese No Remorse Cheats And Launch Parameters
|
||||
|
||||
## Scope
|
||||
|
||||
This note records the current live-Ghidra read of `/ja/CRUSADER.EXE` for two questions:
|
||||
|
||||
1. did the hidden cheat/debug features survive in the Japanese Windows build?
|
||||
2. which launch parameters still survive in executable code?
|
||||
|
||||
Active Ghidra target for this note: `/ja/CRUSADER.EXE`.
|
||||
|
||||
## Short Answer
|
||||
|
||||
Yes, important cheat/debug features clearly survived in the JP Win32 build.
|
||||
|
||||
Current strongest proof:
|
||||
|
||||
- `-laurie` is still parsed explicitly and still enables the broader cheat/debug gates
|
||||
- the hidden `JASSICA16` sequence matcher still exists as live code
|
||||
- the JP option-key cheat handler still contains the immortality on/off path with live UI messages
|
||||
- the JP parser still recognizes a broad launch-option table including `-debug`, `-u`, `-setver`, `-vesatest`, `-asylum`, `-warp`, `-skill`, `-mapoff`, `-egg`, and `-demo`
|
||||
|
||||
The main caution from this pass is that the JP Win32 parser is not yet proven to support every old DOS-side syntax exactly as previously documented. In particular, this pass did **not** recover a JP parser branch that consumes positional X/Y/Z arguments after `-warp`; the live JP parser block currently closes only the `-warp <mission>` form.
|
||||
|
||||
## Main Findings
|
||||
|
||||
### 1. `-laurie` definitely survived
|
||||
|
||||
`00466774`, now named `handle_commandline_args_jp`, still special-cases `-laurie` outside the normal option table.
|
||||
|
||||
Recovered behavior:
|
||||
|
||||
- exact literal compare against `-laurie`
|
||||
- sets the two broad cheat/debug gate bytes at `0x4957d8` and `0x495830`
|
||||
- prints the message at `0046ea5a`: `FART ...TRY... -laurie (Have fun, Jely)`
|
||||
|
||||
This is strong evidence that the JP Windows build still preserves the Laurie/debug enable lane rather than removing it.
|
||||
|
||||
### 2. The `JASSICA16` hidden-sequence lane definitely survived
|
||||
|
||||
`00418454`, now named `key_check_jassica16_toggle_jp`, is a live hidden-sequence matcher.
|
||||
|
||||
Recovered behavior:
|
||||
|
||||
- consumes incoming key bytes
|
||||
- compares them against a byte sequence table starting at `0x47b1c9`
|
||||
- on full match, toggles the global cheat-active state
|
||||
- emits the live messages:
|
||||
- `Cheats are now active.`
|
||||
- `Cheats are now inactive.`
|
||||
|
||||
Adjacent JP data still contains the literal string `JASSICA16` at `0047b1cc`.
|
||||
|
||||
The safest current read is:
|
||||
|
||||
- the hidden `JASSICA16` cheat-enable concept survived
|
||||
- the JP executable still has executable code that toggles cheat state from a secret key sequence
|
||||
|
||||
### 3. The option-key cheat handler still contains immortality
|
||||
|
||||
`00415eec`, now named `key_handle_option_cheats_jp`, still contains the immortality toggle path.
|
||||
|
||||
Recovered behavior inside that function:
|
||||
|
||||
- checks cheat/debug gating before entering the relevant branch
|
||||
- still reaches both live strings:
|
||||
- `Immortality enabled.`
|
||||
- `Immortality disabled.`
|
||||
- still pushes those strings into the normal in-game message path
|
||||
|
||||
This is direct proof that at least one high-value keyboard cheat survived beyond the sequence matcher itself.
|
||||
|
||||
The same function is larger than just the immortality branch and still appears to host a broader cheat/debug action menu. This pass did not fully close every other branch in that handler.
|
||||
|
||||
### 4. `-u` survived, and the JP build gives it a clearer meaning than the older notes
|
||||
|
||||
The JP parser block at `0046688f` consumes the next argv token and copies it into the fixed buffer at `0x47be40`.
|
||||
|
||||
That buffer is later consumed by `00466ebc`, now named `startup_apply_u_override_if_present`.
|
||||
|
||||
Recovered behavior in `startup_apply_u_override_if_present`:
|
||||
|
||||
- checks whether `s_usecode_0047be40` is non-empty
|
||||
- if so, routes startup into a replacement usecode/EUSECODE load path instead of the default one
|
||||
|
||||
This is the strongest current JP-side closure for `-u`:
|
||||
|
||||
> `-u <path-or-name>` is a live startup override for the usecode/EUSECODE load source.
|
||||
|
||||
That is materially better than the older unresolved `-u` note.
|
||||
|
||||
## Launch Parameter Table
|
||||
|
||||
### Definitely recovered as live JP parser behavior
|
||||
|
||||
| Argument | Current best JP syntax | Confidence | JP evidence |
|
||||
|------|------|------|------|
|
||||
| `-laurie` | `-laurie` | High | Explicit special-case in `handle_commandline_args_jp`; sets cheat/debug gates and prints the Laurie joke string. |
|
||||
| `-debug` | `-debug` | High | Dedicated parser block sets debug state, writes `0x0a` to the debug level word at `0x482620`, and prints `Debugging mode ON.`. |
|
||||
| `-u` | `-u <arg>` | High | Parser consumes the next token into `0x47be40`; later startup code uses that as a live usecode override path. |
|
||||
| `-warp` | `-warp <mission>` | High | JP parser consumes one following numeric token, stores it, and prints `Warping to mission %d.`. |
|
||||
| `-skill` | `-skill <n>` | High | JP parser consumes one following numeric token and prints `Defaulting to skill level %d`. |
|
||||
| `-mapoff` | `-mapoff <delta>` | High | JP parser consumes one following numeric token and prints `Map offset = %d`. |
|
||||
| `-egg` | `-egg <id>` | High | JP parser consumes one following numeric token and prints `Destination Egg = %d`. |
|
||||
| `-demo` | `-demo` | High | JP parser sets the demo flag and prints `Demo mode.`. |
|
||||
|
||||
### Present in the JP parser table, but not fully re-closed in this pass
|
||||
|
||||
| Argument | Current best JP syntax | Confidence | Current best JP read |
|
||||
|------|------|------|------|
|
||||
| `-?` | `-?` | Medium | Still present in the JP option table. Likely some help/usage-style path, but the exact handler was not isolated in this pass. |
|
||||
| `-setver` | `-setver <text>` | Medium | Still present in the JP option table. Existing non-JP notes still make `displayed version/build string override` the best read, but this exact JP handler was not re-closed here. |
|
||||
| `-vesatest` | `-vesatest` | Low | Still present in the JP option table. The exact JP-side effect remains unresolved. |
|
||||
| `-asylum` | `-asylum` | Low | Still present in the JP option table. The exact JP-side effect remains unresolved in this pass. |
|
||||
|
||||
## Important JP-Specific Caution About `-warp`
|
||||
|
||||
The older DOS-side command-line note documents a positional coordinate form:
|
||||
|
||||
- `-warp <mission> <x> <y> <z>`
|
||||
|
||||
This pass did **not** re-close that form in the JP Win32 parser.
|
||||
|
||||
What the live JP parser block at `004668e2` actually does:
|
||||
|
||||
- call the tokenizer once more
|
||||
- parse a single numeric token
|
||||
- store that value as the mission id
|
||||
- print `Warping to mission %d.`
|
||||
- return to normal token scanning
|
||||
|
||||
What this pass did **not** recover:
|
||||
|
||||
- a branch that consumes three extra positional numeric tokens after `-warp`
|
||||
- a JP-side format string of the older `Warping to mission %d @ x:%d y:%d z:%d.` style
|
||||
|
||||
So the safest current JP statement is:
|
||||
|
||||
> `-warp <mission>` is directly proven. The positional JP Win32 coordinate form is not yet proven by this pass and should not be assumed from the older DOS-side note.
|
||||
|
||||
## Cheat-State Model After This Pass
|
||||
|
||||
The current strongest JP cheat/debug model is:
|
||||
|
||||
1. `-laurie` still raises the broad cheat/debug enable gates.
|
||||
2. The hidden `JASSICA16` matcher still exists and can toggle cheat-active state independently.
|
||||
3. The option-key handler still contains at least the immortality toggle and a wider cheat/debug action family.
|
||||
|
||||
That means the Japanese Windows build did not merely keep some orphaned strings. It still preserves executable cheat/debug machinery.
|
||||
|
||||
## Ghidra Changes Made During This Pass
|
||||
|
||||
Renamed and commented in the active `/ja/CRUSADER.EXE` database:
|
||||
|
||||
- `00466774` -> `handle_commandline_args_jp`
|
||||
- `00415eec` -> `key_handle_option_cheats_jp`
|
||||
- `00418454` -> `key_check_jassica16_toggle_jp`
|
||||
- `00466ebc` -> `startup_apply_u_override_if_present`
|
||||
|
||||
Decompiler comments were added to each of those entry points to preserve the evidence in the live database.
|
||||
|
||||
## Open Follow-Up
|
||||
|
||||
1. Recover the remaining JP option-table handlers for `-?`, `-setver`, `-vesatest`, and `-asylum` explicitly rather than by table presence only.
|
||||
2. Re-open the JP `-warp` lane and prove whether any positional coordinate form still exists in some later startup consumer.
|
||||
3. Identify the exact key bytes in the JP `0x47b1c9` matcher table and compare them byte-for-byte against the ASCII-adjacent `JASSICA16` data.
|
||||
4. Walk the rest of `key_handle_option_cheats_jp` so the non-immortality branches are named and classified.
|
||||
5. Compare the JP cheat/debug global addresses directly against the English DOS build to see which lanes were ported verbatim versus re-laid for Win32.
|
||||
6. Runtime-test the JP build with `-laurie`, `-debug`, `-demo`, `-warp`, and `-u` to confirm the static conclusions from the Windows binary.
|
||||
200
docs/jp-remorse-windows9x-investigation.md
Normal file
200
docs/jp-remorse-windows9x-investigation.md
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
# Japanese No Remorse Windows 9x Investigation
|
||||
|
||||
## Question
|
||||
|
||||
Investigate the claim that the Japanese build of Crusader: No Remorse supports Windows 9x and can run natively on Windows 95 instead of requiring a DOS boot path.
|
||||
|
||||
Active Ghidra target for this note: `/ja/CRUSADER.EXE`.
|
||||
|
||||
## Verdict
|
||||
|
||||
Current static-analysis verdict: **strongly supports the claim**.
|
||||
|
||||
The loaded Japanese executable is not just a DOS executable with a small helper around it. It is a native flat Win32 image with Windows API startup, Windows registry configuration, DirectDraw/DirectSound initialization, and one explicit Win9x-aware compatibility branch.
|
||||
|
||||
What is proven from static analysis:
|
||||
|
||||
- the Japanese binary is a PE-style Win32 program loaded at `00400000`, with sections such as `BEGTEXT`, `DGROUP`, `.idata`, `.reloc`, and `.rsrc`
|
||||
- it creates a real top-level Windows game window and runs through Win32 startup code
|
||||
- it initializes DirectDraw and DirectSound directly
|
||||
- it reads and writes settings in the Windows registry
|
||||
- it contains Japanese/Win9x-facing support clues, including IME handling and a `GetVersion`-based compatibility branch
|
||||
|
||||
What is **not** proven by this pass:
|
||||
|
||||
- an actual runtime launch on real Windows 95 hardware or emulation
|
||||
- whether extra runtime prerequisites such as the expected DirectX version are always present on a bare Windows 95 install
|
||||
- whether every shipped JP build variant behaves the same way
|
||||
|
||||
So the safest wording is:
|
||||
|
||||
> The claim that the Japanese release has a native Windows 9x execution path is strongly supported by the code. The stricter claim that it will always run on a stock Windows 95 machine without additional runtime requirements is not fully closed by static analysis alone.
|
||||
|
||||
## Main Evidence
|
||||
|
||||
### 1. The loaded JP executable is a Win32 image, not the old DOS/NE game binary
|
||||
|
||||
Ghidra reports the active program as `/ja/CRUSADER.EXE` with entry at `004729e0`.
|
||||
|
||||
Relevant image layout recovered from Ghidra:
|
||||
|
||||
- `Headers: 00400000 - 004003ff`
|
||||
- `BEGTEXT: 00401000 - 0047afff`
|
||||
- `DGROUP: 0047b000 - 00481bff`
|
||||
- `.bss: 00482000 - 00495fff`
|
||||
- `.idata: 00496000 - 00496dff`
|
||||
- `.reloc: 00497000 - 0049e3ff`
|
||||
- `.rsrc: 0049f000 - 004a03ff`
|
||||
|
||||
That is a normal PE/Win32 layout, not the segmented NE or DOS-extender layout used by the original DOS executable.
|
||||
|
||||
### 2. The import table is decisively Windows-native
|
||||
|
||||
Recovered imports include all the major Windows subsystems expected of a native Win9x game executable:
|
||||
|
||||
- process and OS: `GetVersion`, `CreateThread`, `ExitProcess`, `TlsAlloc`, `VirtualAlloc`, `GetCommandLineA`, `GetModuleHandleA`, `GetModuleFileNameA`
|
||||
- window/UI: `CreateWindowExA`, `RegisterClassA`, `DefWindowProcA`, `DispatchMessageA`, `PeekMessageA`, `ShowWindow`, `RegisterWindowMessageA`, `MessageBoxA`
|
||||
- graphics: `DirectDrawCreate`, `CreateBitmap`, `CreateCompatibleDC`, `CreatePalette`, `TextOutA`
|
||||
- audio/timing/input: `DirectSoundCreate`, `DirectSoundEnumerateA/W`, `timeGetTime`, `joyGetDevCapsA`, `joyGetPosEx`
|
||||
- configuration/storage: `RegCreateKeyExA`, `RegOpenKeyExA`, `RegQueryValueExA`, `RegSetValueExA`, `CreateFileA`, `ReadFile`, `WriteFile`
|
||||
- Japanese text/input support: `IsDBCSLeadByte`, `GetCPInfo`, `WINNLSEnableIME`
|
||||
|
||||
This is not a DOS launcher plus one or two helper calls. It is a full Windows application stack.
|
||||
|
||||
### 3. The main window path is explicit and native
|
||||
|
||||
`00445f40` is now named `win32_create_main_window`.
|
||||
|
||||
Key behavior recovered from decompilation:
|
||||
|
||||
- registers a window class with `RegisterClassA`
|
||||
- creates a fullscreen popup window with `CreateWindowExA`
|
||||
- uses the title string `"Crusader: No Remorse"`
|
||||
- calls `ShowWindow(..., 10)`
|
||||
- calls `WINNLSEnableIME(0,0)` immediately after successful creation
|
||||
- registers `"MSWHEEL_ROLLMSG"`
|
||||
|
||||
This is direct evidence of a native Windows UI path. The IME call is especially relevant in a Japanese build because it shows the executable is consciously managing Windows-side JP text/input behavior.
|
||||
|
||||
### 4. DirectDraw and DirectSound are initialized directly by the game
|
||||
|
||||
`004459b0` is now named `video_init_directdraw_and_directsound`.
|
||||
|
||||
Recovered behavior:
|
||||
|
||||
- calls `DirectDrawCreate()`
|
||||
- sets cooperative level and display mode on the DirectDraw object
|
||||
- creates the palette and primary surface
|
||||
- calls `DirectSoundCreate()`
|
||||
- hides the cursor with `ShowCursor(0)`
|
||||
|
||||
This is a classic Windows 9x era DirectX setup path. It strongly supports the idea that this build was intended to run as a native Windows game executable.
|
||||
|
||||
### 5. Registry-backed configuration is built in
|
||||
|
||||
String at `0047b178`:
|
||||
|
||||
- `Software\Electronic Arts\Crusader: No Remorse\J1.21`
|
||||
|
||||
`004138e8` is now named `config_load_registry_and_cfg`.
|
||||
|
||||
Recovered behavior:
|
||||
|
||||
- reads `installpath`, `cdpath`, and `flicpath` from `HKEY_LOCAL_MACHINE`
|
||||
- reads user preferences such as `video`, `subtitles`, `limitblasts`, `animation`, `frameskip`, `musicvolume`, `soundvolume`, and `mousespeed` from `HKEY_CURRENT_USER`
|
||||
- falls back to parsing `crusader.cfg`
|
||||
|
||||
`00413760` is now named `config_write_registry_string` and writes string values back into the same registry branch.
|
||||
|
||||
This is strong evidence of a normal Windows-installed configuration model rather than a DOS-only setup flow.
|
||||
|
||||
### 6. There is a real Win9x-specific compatibility branch
|
||||
|
||||
`00472c41` is now named `win32_runtime_capture_process_context`.
|
||||
|
||||
Recovered behavior:
|
||||
|
||||
- captures environment strings
|
||||
- captures module filename and command line
|
||||
- calls `GetVersion()`
|
||||
- stores split version fields into globals at `0x47c09f`, `0x47c0a0`, and `0x47c0a1`
|
||||
|
||||
The more important follow-up is `00476616`, now named `win32_tls_alloc_with_win9x_guard`.
|
||||
|
||||
Recovered behavior:
|
||||
|
||||
- calls `TlsAlloc()`
|
||||
- checks the saved `GetVersion()` word
|
||||
- if the version word has the high `0x8000` bit set, it retries while the TLS slot is less than `3`
|
||||
|
||||
That is not generic Windows code. It is exactly the kind of branch you expect when a program needs to behave differently on the Win9x line, where `GetVersion()` reports the platform using the high bit and some low TLS slots are treated specially.
|
||||
|
||||
This is the strongest single code-level clue that the executable was designed with Win9x behavior in mind, not just NT-family Windows.
|
||||
|
||||
## Secondary Evidence
|
||||
|
||||
### Japanese-specific Windows integration
|
||||
|
||||
The import set and main-window path together show several JP-specific Windows integration points:
|
||||
|
||||
- `WINNLSEnableIME`
|
||||
- `IsDBCSLeadByte`
|
||||
- `GetCPInfo`
|
||||
|
||||
That does not by itself prove Windows 95 compatibility, but it does reinforce that this is a localized Windows build, not a DOS build being loosely wrapped.
|
||||
|
||||
### WinMM and joystick APIs
|
||||
|
||||
The import table also includes:
|
||||
|
||||
- `joyGetDevCapsA`
|
||||
- `joyGetPosEx`
|
||||
- `timeGetTime`
|
||||
|
||||
These are ordinary Windows multimedia/input APIs and fit the same native-executable picture.
|
||||
|
||||
## Interpretation
|
||||
|
||||
The claim can be split into two parts.
|
||||
|
||||
### Part A: "The Japanese version supports Windows 9x"
|
||||
|
||||
This is strongly supported.
|
||||
|
||||
The binary:
|
||||
|
||||
- is a Win32 image
|
||||
- uses Win32 startup and window management
|
||||
- uses DirectX-era Windows multimedia APIs
|
||||
- uses Windows registry storage
|
||||
- has a `GetVersion` branch that specifically distinguishes Win9x-style version reporting
|
||||
|
||||
### Part B: "It can run on Windows 95 natively without switching to DOS first"
|
||||
|
||||
This is also strongly supported, but with one caveat.
|
||||
|
||||
The executable is clearly built to run as a native Windows application, and the version/TLS branch is the best evidence that Win9x behavior was considered explicitly. The remaining uncertainty is practical, not architectural: an actual Windows 95 runtime may still need the expected DirectX/runtime environment installed.
|
||||
|
||||
So the architectural answer is yes; the deployment answer is very likely yes, but not fully closed until runtime-tested on a Win95 environment.
|
||||
|
||||
## Ghidra Changes Made During This Pass
|
||||
|
||||
Renamed and commented in the active `/ja/CRUSADER.EXE` database:
|
||||
|
||||
- `00472c41` -> `win32_runtime_capture_process_context`
|
||||
- `00476616` -> `win32_tls_alloc_with_win9x_guard`
|
||||
- `00445f40` -> `win32_create_main_window`
|
||||
- `004459b0` -> `video_init_directdraw_and_directsound`
|
||||
- `004138e8` -> `config_load_registry_and_cfg`
|
||||
- `00413760` -> `config_write_registry_string`
|
||||
|
||||
Each of those now has a decompiler comment explaining why it matters to the Win9x/native-Windows investigation.
|
||||
|
||||
## Remaining Follow-Up
|
||||
|
||||
1. Run the JP executable under an actual Windows 95 target or a faithful Win95 VM to confirm runtime behavior.
|
||||
2. Identify the expected DirectX version from installer files or runtime error strings.
|
||||
3. Trace the message pump and main game loop entry after window creation for a fuller WinMain reconstruction.
|
||||
4. Locate the import thunks or callsites for `GetCPInfo` and `IsDBCSLeadByte` to document the JP text path more precisely.
|
||||
5. Check whether the JP installer writes the same `J1.21` registry branch or whether the executable can self-heal missing keys.
|
||||
6. Compare the JP Windows binary against the English DOS binary to isolate which subsystems were ported versus shared.
|
||||
171
docs/regret-game-start.md
Normal file
171
docs/regret-game-start.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# No Regret Game_Start Flow
|
||||
|
||||
## Scope
|
||||
|
||||
This note documents the currently opened live `REGRET.EXE` startup lane around `Game_Start` and the later helper that performs the actual new-game mission hop.
|
||||
|
||||
The immediate goals of this pass were:
|
||||
|
||||
- name the directly involved startup helpers in Ghidra
|
||||
- annotate the startup-state globals that feed the new-game path
|
||||
- explain why No Regret appears to set the start map twice
|
||||
|
||||
## Named Functions
|
||||
|
||||
The following previously-unnamed functions are now promoted in the live `REGRET.EXE` database.
|
||||
|
||||
| Address | New name | Why the name is justified |
|
||||
|------|------|------|
|
||||
| `1058:0bbe` | `HandleCommandlineArgs` | Matches the already-recovered Remorse-side parser role. It compares argv entries against a startup-option table and writes `g_warpToLevelNoArg`, the difficulty override, map offset, egg override, and the extra startup-video flag. |
|
||||
| `1030:032d` | `Game_RunNewGameFlow` | This is the full No Regret new-game helper. It plays the ORIGIN/ANIM01 intro lane, opens the difficulty modal when not warping, rebuilds the world/avatar state, and then performs the actual mission-start teleporter hop or the debug `-warp` path. |
|
||||
| `1030:022e` | `Game_DrawCenteredStartupSplash` | Called immediately before the teleporter hop in both the normal and `-warp` startup lanes. It fades black, clears the active gameplay state, centers one startup screen object, and redraws the viewport. |
|
||||
| `11c8:0d96` | `Game_EnterFrontendMenuViewport` | Used before modal-gump dispatch. It clears the gameplay-style viewport flag, expands the mouse Y range to the taller menu height, and refreshes the screen object. |
|
||||
| `11c8:0d39` | `Game_RestoreGameplayViewport` | Used after the modal/front-end phase. It restores the gameplay-style viewport state, notifies NPC update lanes, restores the shorter mouse Y range, and refreshes the screen object again. |
|
||||
|
||||
## Game_Start Structure
|
||||
|
||||
`Game_Start` in No Regret still looks like the top-level frontend entry point.
|
||||
|
||||
High-level shape:
|
||||
|
||||
1. Freeze the current gameplay/process state enough to enter a frontend/modal lane.
|
||||
2. Enter the taller frontend viewport mode and dispatch the startup modal gump.
|
||||
3. Handle non-new-game modal results such as exit/other menu actions.
|
||||
4. For the new-game path, clear world/gameplay state, rebuild the avatar and starter items, and do an early teleporter hop to map `1`, egg `0x1e`.
|
||||
5. Fade/reset again and hand back into the wider startup framework.
|
||||
|
||||
The key early selector is still:
|
||||
|
||||
- `1008:1446 PUSH 0x1`
|
||||
- `1008:1448 PUSH 0x1e0001`
|
||||
- `1008:144e CALLF 0x1030:0000`
|
||||
|
||||
That early site is real, but it is not the only startup teleporter call that matters.
|
||||
|
||||
## Actual New-Game Mission Start
|
||||
|
||||
The function now named `Game_RunNewGameFlow` is the important second lane.
|
||||
|
||||
It is called from at least two concrete places:
|
||||
|
||||
- `CreditsProcess_Run` at `1008:0056`
|
||||
- another startup/control lane at `1058:0a2a`
|
||||
|
||||
That already shows it is not dead duplicate code. It is a standalone startup helper reused by the frontend.
|
||||
|
||||
Its normal no-warp branch does all of the following in one place:
|
||||
|
||||
1. Play the ORIGIN/ANIM01 intro-video lane.
|
||||
2. Open the difficulty-selection modal.
|
||||
3. Reinitialize NPC/world/tracker state.
|
||||
4. Apply any command-line difficulty override.
|
||||
5. Recreate starter inventory items.
|
||||
6. Draw the centered startup splash.
|
||||
7. Perform the actual mission-start teleporter hop.
|
||||
|
||||
The effective mission-start selector is:
|
||||
|
||||
- `1030:05c3 PUSH 0x1`
|
||||
- `1030:05c5 PUSH 0x1e0001`
|
||||
- `1030:05cb PUSH CS`
|
||||
- `1030:05cc CALL 0x1030:0000`
|
||||
|
||||
This is the selector that redirected real new games only after the script was updated to patch it together with the earlier `Game_Start` site.
|
||||
|
||||
## Startup Globals
|
||||
|
||||
These data roles are now annotated in the live Ghidra session.
|
||||
|
||||
| Address | Current best role |
|
||||
|------|------|
|
||||
| `1480:1453` | Startup-world transition flag. `Game_Start` clears it before the early menu-start teleporter hop. `Game_RunNewGameFlow` sets it during world/NPC rebuild and clears it again immediately before the actual mission-start teleporter hop. |
|
||||
| `1480:0ac8` | Optional command-line warp X override. `0xffff` means no direct coordinate warp is requested. |
|
||||
| `1480:0aca` | Optional command-line warp Y override. |
|
||||
| `1480:0acc` | Optional command-line warp Z override. |
|
||||
| `1480:0ace` | Command-line difficulty override parsed by `HandleCommandlineArgs`. If greater than zero, the new-game flow copies it into `g_difficultyLevel` before the teleporter hop. |
|
||||
| `1480:0ad0` | Command-line `-mapoff` additive map offset used by the debug `-warp` path. |
|
||||
| `1480:0ad2` | Command-line egg override used by the `-warp` path. When negative or `0x001e`, the code falls back to the default startup egg. |
|
||||
| `1480:0ad4` | Extra startup-video behavior flag parsed by `HandleCommandlineArgs`. Clear means the ORIGIN/ANIM01 lane is played once; set means the same lane is repeated until `1480:95f0` becomes nonzero. |
|
||||
| `1480:95f0` | Startup-video loop exit latch used by `Game_RunNewGameFlow` and `CreditsProcess_Run`. |
|
||||
|
||||
## Command-Line Warp Controls
|
||||
|
||||
The startup parser exposes more real gameplay-side command-line controls than just `-warp`.
|
||||
|
||||
Currently proven from the recovered parser strings and control flow:
|
||||
|
||||
- `-warp`
|
||||
- `-skill`
|
||||
- `-mapoff`
|
||||
- `-egg`
|
||||
- `-debug`
|
||||
- `-demo`
|
||||
- `-setver`
|
||||
- `-asylum`
|
||||
|
||||
The same executables also contain the runtime status strings:
|
||||
|
||||
- `Warping to mission %d.`
|
||||
- `Warping to mission %d @ x:%d y:%d z:%d.`
|
||||
- `Defaulting to skill level %d`
|
||||
- `Destination Egg = %d`
|
||||
|
||||
That parser behavior is now tight enough to promote the actual syntax:
|
||||
|
||||
- `-warp <mission>` = mission-only manual warp
|
||||
- `-warp <mission> <x> <y> <z>` = direct coordinate warp
|
||||
|
||||
The key parser detail is that there are no separately recovered `-x`, `-y`, or `-z` switches in this lane. After `HandleCommandlineArgs` parses the mission number, it checks the next argv token. If the next token is missing or begins with `-`, the parser takes the mission-only path and prints `Warping to mission %d.`. Otherwise it consumes the next three argv tokens as X, Y, and Z and prints `Warping to mission %d @ x:%d y:%d z:%d.`.
|
||||
|
||||
There is one important precedence rule in `Game_RunNewGameFlow`: `-egg` wins over the coordinate override path. When X is present but the egg override is nonnegative, the code still routes back into the egg-based teleporter lane. The direct `NPC_Teleport` path only runs when X/Y/Z are present and the egg override is still negative.
|
||||
|
||||
## Could Parameters Replace The Executable Hack?
|
||||
|
||||
For a normal fresh game, no.
|
||||
|
||||
The executable patch was needed because the stock new-game path still hardcodes the startup selector payload `(map = 1, egg = 0x1e)` in code. No Regret does that twice, once in `Game_Start` and once again in `Game_RunNewGameFlow`.
|
||||
|
||||
The command-line warp lane is different:
|
||||
|
||||
- it only runs when `g_warpToLevelNoArg != -1`
|
||||
- it uses a debug/manual warp flow rather than the ordinary fresh-game path
|
||||
- it computes the target map from an embedded mission-to-map table plus `-mapoff`
|
||||
- it can also change the destination egg
|
||||
- it can apply explicit X/Y/Z destination overrides through `-warp <mission> <x> <y> <z>`
|
||||
|
||||
So a command line could likely have reproduced the practical destination change for a manual debug warp session, but not the ordinary fresh-game mission-1 behavior that the patch script changed.
|
||||
|
||||
For maps with no usable teleport egg, the parameter-only workaround is now clearer too: use `-warp <mission> <x> <y> <z>` together with `-mapoff`, and do not pass `-egg`. That path bypasses the egg lookup and calls `NPC_Teleport` directly. The current inspected teleport lane does not show any automatic `snap-to-ground` rescue, so this only works if the supplied coordinate triple is already valid for the target map.
|
||||
|
||||
Current best distinction:
|
||||
|
||||
- executable patch = changes the stock fresh-game startup behavior
|
||||
- command-line warp args = invoke a separate manual/debug startup path that can land on a different mission/map/egg and likely a different coordinate triple
|
||||
|
||||
## Likely Reason Map 1 Is Set Twice
|
||||
|
||||
The most likely explanation is that No Regret preserved two valid entry paths into an in-engine gameplay state instead of refactoring them into one shared startup selector.
|
||||
|
||||
Evidence for that read:
|
||||
|
||||
- `Game_Start` contains an early teleporter hop immediately after rebuilding the avatar and starter items.
|
||||
- `Game_RunNewGameFlow` is a separate, live helper called from `CreditsProcess_Run` and another startup/control lane.
|
||||
- `Game_RunNewGameFlow` owns the full intro-video, difficulty-modal, world-reset, and real mission-start sequence.
|
||||
- Both paths call the same local teleporter wrapper with the same literal `(1, 0x1e, 1)` payload.
|
||||
|
||||
Current best model:
|
||||
|
||||
- the early `Game_Start` hop is a frontend/bootstrap entry into a playable engine state
|
||||
- the later `Game_RunNewGameFlow` hop is the authoritative mission-start step for a real new game after the intro/difficulty flow
|
||||
- both kept their own literal selector instead of sharing one startup-map global or helper constant
|
||||
|
||||
So the duplication looks more like split control-flow ownership and startup code reuse than a deliberate “set it twice just in case” pattern.
|
||||
|
||||
## Practical Patch Implication
|
||||
|
||||
For No Regret, changing the fresh-game destination map must update both hardcoded selectors:
|
||||
|
||||
- early `Game_Start` selector at `1008:1448`
|
||||
- actual mission-start selector at `1030:05c5`
|
||||
|
||||
Patching only the early site is insufficient because the later new-game helper can silently overwrite that choice during the real mission-start sequence.
|
||||
|
|
@ -104,6 +104,32 @@ That does not prove every shape-id use of `0x04D0` is literally a monster actor
|
|||
|
||||
This is the strongest reason not to read `ALARMHAT` as just a decorative siren hat sprite. The script is actively scanning for nearby `0x04D0` objects and equipping them.
|
||||
|
||||
## Follow-up: what controls immediate spawn versus waiting
|
||||
|
||||
The nearby `ALARMHAT` logic is only one half of the `0x04D0` story. The matching `MONSTER` class now adds one verified activation rule:
|
||||
|
||||
- `MONSTER::enterFastArea` only checks `0x04D0` objects when `Item.getFrame(arg_06) == 0`
|
||||
- inside that frame-0 lane it reads `Item.getMapNum(arg_06)` and only auto-runs `MONSTER.equip(pid, 0, arg_06)` when `(mapNum & 0x08) == 0`
|
||||
|
||||
Current safest read:
|
||||
|
||||
- `frame 0` = the only state that participates in the automatic `enterFastArea` spawn lane
|
||||
- `mapNum bit 0x08` = suppresses that automatic lane without changing DTABLE row selection
|
||||
- `frame 1` = skips `enterFastArea` entirely and is therefore more likely to be used in paired or externally signaled setups
|
||||
|
||||
That fits the recurring authored pairs already visible in exported map data:
|
||||
|
||||
- Remorse map 246: frame-0 `item:162` uses `mapNum = 8`, `npcNum = 0`, while colocated frame-1 `item:163` uses `mapNum = 1`, `npcNum = 8`
|
||||
- Remorse map 9: frame-0 `item:338` uses `mapNum = 0`, `npcNum = 0`, while colocated frame-1 `item:339` uses `mapNum = 7`, `npcNum = 2`
|
||||
|
||||
This follow-up also narrows one tempting overread: `quality` low byte is not the primary `spawn now vs wait` control in `MONSTER::enterFastArea`. The current Regret `ALARMHAT` body does still compare nearby `0x04D0` `Item.getQLo(...)` values against difficulty lanes `0/1/2`, so low quality remains relevant as a secondary filter, just not as the automatic-enter-area gate.
|
||||
|
||||
The wider exported corpus now supports keeping that claim local but real. Additional `0x04D0`-adjacent bodies in `ITEM.slot_2D`, `FUSPAC.slot_01`, and `MISS8.slot_20` also scan nearby `0x04D0` objects and branch on frame and/or `Item.getQLo(...)`. Current safest synthesis is therefore:
|
||||
|
||||
- `frame` plus `mapNum bit 0x08` controls the automatic `MONSTER.enterFastArea` lane
|
||||
- `quality` low byte is still a real local signal key used by several authored trigger/alarm/helper families
|
||||
- but that low byte is not yet proven to be a single universal direct-link field across every `0x04D0` interaction
|
||||
|
||||
## Comparison to the rest of the alarm family
|
||||
|
||||
`ALARMHAT` fits the broader alarm family, but it is not identical to the other alarm classes.
|
||||
|
|
|
|||
325
docs/usecode-startup-override.md
Normal file
325
docs/usecode-startup-override.md
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
# Retail `-u` USECODE Startup Override
|
||||
|
||||
## Question
|
||||
|
||||
If retail non-Japanese `CRUSADER.EXE` is started with `-u <arg>`, does that add extra usecode on top of the stock runtime, or does it replace the stock usecode root? And if it really is a replacement path, what parts of the game does that replacement actually control?
|
||||
|
||||
Active analysis target for the binary side was live `CRUSADER.EXE` in the Ghidra session.
|
||||
|
||||
## Short Answer
|
||||
|
||||
Current best answer:
|
||||
|
||||
- `-u` is a real retail startup override, not dead parser residue.
|
||||
- In retail `CRUSADER.EXE`, it behaves like a **replacement** of the live usecode runtime root, not an additive overlay.
|
||||
- The replacement root is then used by the normal usecode event/process/interpreter machinery.
|
||||
|
||||
So the practical meaning is:
|
||||
|
||||
> `-u <arg>` points startup at an alternate usecode/EUSECODE source, and subsequent scripted behavior runs against that replacement runtime instead of the stock one.
|
||||
|
||||
The strongest remaining open questions are now the precise Filespec path rules for `<arg>` and the still-unclosed live-NE stock bootstrap path that seeds the same runtime when `-u` is absent.
|
||||
|
||||
## What `<arg>` Currently Looks Like
|
||||
|
||||
The token-shape question is no longer completely open.
|
||||
|
||||
Current best retail read from the live `1420:0cdf` helper is:
|
||||
|
||||
- the parser still copies the raw argv token into `1478:065a`
|
||||
- the helper does **not** pass that token straight to the loader as the final filename
|
||||
- instead it loads a far pointer from `1478:06d6/06d8` to the mutable string at `1478:07a0`, which is `eusecode.flx`
|
||||
- it forces the first byte of that filename template to `'e'`
|
||||
- it then calls `Filespec_GetFullPath(0, s_usecode, "eusecode.flx", 0)`
|
||||
- it probes that constructed full path with `File_Exists`
|
||||
- if the path exists, it rebuilds the same full path again and passes that result into the runtime loader/constructor path
|
||||
|
||||
That makes the safest current interpretation:
|
||||
|
||||
- `<arg>` is a Filespec **path-like** argument that supplies the directory/resource-root component
|
||||
- the filename component is still the fixed retail archive name `eusecode.flx`
|
||||
|
||||
So the current evidence argues **against** these stronger interpretations:
|
||||
|
||||
- arbitrary full filename override
|
||||
- free-form alternate archive basename
|
||||
- direct one-off script/body filename
|
||||
|
||||
If the user passes a literal filename as `<arg>`, the recovered helper shape suggests the game would still try to append the fixed filename template rather than use the supplied token as the final filename.
|
||||
|
||||
The remaining uncertainty is narrower now:
|
||||
|
||||
- whether the path component can be only a filesystem directory
|
||||
- whether it can also be a loader alias/resource root that `Filespec_GetFullPath` understands
|
||||
- whether relative, absolute, and CD-backed forms are all accepted equally
|
||||
|
||||
External engine code reinforces the fixed-filename part of this read. Both Pentagram and ScummVM build the default Crusader main usecode path as `usecode/` + language letter + `usecode.flx`, and for Remorse/Regret the language-usecode letter is `'e'`, which matches the retail helper's forced `eusecode.flx` template.
|
||||
|
||||
## Why This Looks Like Replacement, Not Addition
|
||||
|
||||
### 1. There is one live runtime root, not a list of roots
|
||||
|
||||
The key global pair is:
|
||||
|
||||
- `1478:6611/6613`
|
||||
|
||||
That far pointer is read directly by all of the main recovered usecode-side consumers, including:
|
||||
|
||||
- `Usecode_ItemCallEvent` at `1420:0e3a`
|
||||
- `UsecodeProcess_CreateProcess` at `1420:0f20`
|
||||
- `Interpreter_NextUsecodeOp` at `1418:332c`
|
||||
- `Item_GetDamaged` at `10a0:277b`
|
||||
|
||||
No parallel second root pointer, linked list, or merge walk was recovered in this pass.
|
||||
|
||||
That by itself already makes additive-overlay behavior unlikely.
|
||||
|
||||
### 2. The `-u` startup helper overwrites that single root pointer
|
||||
|
||||
The retail parser case at `1048:0a46` copies the following argv token into `1478:065a`.
|
||||
|
||||
Startup later calls `startup_apply_u_override_if_present` at `1420:0cdf` from `1048:05d3`.
|
||||
|
||||
Inside `startup_apply_u_override_if_present`:
|
||||
|
||||
- if `1478:065a` is empty, it returns without changing anything
|
||||
- if `1478:065a` is non-empty, it resolves/loads an alternate source and then writes the resulting far pointer to `1478:6611/6613`
|
||||
- it sets `1478:6615 = 1`
|
||||
- it rebuilds the cumulative slot-base words at `1478:8c7c..8c82`
|
||||
|
||||
The important behavior is the direct assignment:
|
||||
|
||||
- `1420:0d49` writes `DX -> 1478:6613`
|
||||
- `1420:0d4d` writes `AX -> 1478:6611`
|
||||
|
||||
That is a swap of the live root pointer, not registration of a side table.
|
||||
|
||||
### 3. The helper rebuilds the slot-base state for the new root
|
||||
|
||||
After the root overwrite, `startup_apply_u_override_if_present` immediately recomputes the cumulative slot-base words:
|
||||
|
||||
- `1478:8c7c`
|
||||
- `1478:8c7e`
|
||||
- `1478:8c80`
|
||||
- `1478:8c82`
|
||||
|
||||
Those are the same runtime bases later used by the slot-index/category mapping and owner-row lookups.
|
||||
|
||||
That makes the replacement behavior even stronger:
|
||||
|
||||
- the code is not merely keeping an alternate archive pointer around
|
||||
- it is re-deriving the runtime indexing state that downstream usecode dispatch relies on
|
||||
|
||||
### 4. No merge step was recovered
|
||||
|
||||
This pass did **not** recover any logic that:
|
||||
|
||||
- appends records from the `-u` source onto the stock root
|
||||
- patches individual classes/events into an existing live root
|
||||
- or checks two roots in order during normal dispatch
|
||||
|
||||
Instead, the recovered shape is:
|
||||
|
||||
1. default startup path establishes the ordinary runtime state
|
||||
2. `startup_apply_u_override_if_present` runs once during startup
|
||||
3. if `-u` was supplied, it replaces the root pointer and rebuilds the slot bases
|
||||
4. later gameplay/usecode consumers all read the same replacement root
|
||||
|
||||
The stock runtime may still remain allocated in memory if it was created earlier, but the current evidence says it is no longer the one normal dispatch uses. That is replacement semantics, not additive semantics.
|
||||
|
||||
## Best Current Read Of The Stock Path
|
||||
|
||||
The default non-`-u` bootstrap is only partially closed in the live NE database.
|
||||
|
||||
What is directly closed in live `CRUSADER.EXE`:
|
||||
|
||||
- `Init_Everything` calls `startup_apply_u_override_if_present` at `1048:05d3`
|
||||
- `1478:6611/6613` starts as zero in the loaded image
|
||||
- the only currently recovered **explicit** write to `1478:6611/6613` in the live NE session is the `-u` override helper itself
|
||||
|
||||
That means the stock bootstrap still has an unresolved step in the live NE target.
|
||||
|
||||
The best current cross-reference evidence for that missing default path comes from the verified raw-side VM note in [docs/raw-000a-000d.md](docs/raw-000a-000d.md):
|
||||
|
||||
- `entity_vm_runtime_init_from_path_if_configured` (`000d:44df`) builds the external path and creates the runtime
|
||||
- `entity_vm_runtime_create` (`000d:4c99`) creates the runtime object
|
||||
- `entity_vm_runtime_owner_resource_create` (`000d:7000`) allocates and fills the owner/resource table used later by slot/context creation
|
||||
- that same raw-side init path also seeds the cumulative category bases at `0x8c7c/0x8c7e/0x8c80/0x8c82` from the four counts at `0x6608..0x660e`
|
||||
|
||||
That raw-side bootstrap is important because it matches the same state that the live retail `-u` helper rebuilds after replacement:
|
||||
|
||||
- same four category counts at `1478:6608..660e`
|
||||
- same cumulative bases at `1478:8c7c..8c82`
|
||||
- same downstream owner-row and slot-index consumers already recovered in the live NE session
|
||||
|
||||
So the safest current combined read is:
|
||||
|
||||
- the normal game also has a stock runtime bootstrap in the same VM family
|
||||
- the raw-side path is strongly evidenced as that bootstrap
|
||||
- but the exact live-NE caller that seeds `1478:6611/6613` before gameplay without `-u` is still not directly closed
|
||||
|
||||
That distinction matters for documentation quality. We can now say the stock path is strongly cross-referenced, but we should not yet claim a fully recovered live-NE writer when the current explicit xrefs do not show it.
|
||||
|
||||
## Replacement Root Layout So Far
|
||||
|
||||
The direct constructor/loader pair behind the retail `-u` path is now materially clearer in the live NE session.
|
||||
|
||||
### 1. `entity_vm_runtime_create` at `1420:1499`
|
||||
|
||||
This function is the direct constructor called by `startup_apply_u_override_if_present` after the alternate `eusecode.flx` path is resolved.
|
||||
|
||||
Current best read:
|
||||
|
||||
- allocates `0x1319` bytes when no existing object is supplied
|
||||
- clears the first `0x1300` bytes through `1420:1536`
|
||||
- stores several tail defaults in the `0x1300..0x1314` range
|
||||
- calls `entity_vm_runtime_owner_resource_create` with the resolved path
|
||||
- stores the returned far pointer at `+0x1315/+0x1317`
|
||||
|
||||
The zeroed first `0x1300` bytes are especially suggestive:
|
||||
|
||||
- `1420:1575` later walks that area as `0x80` entries of stride `0x26`
|
||||
- for each entry it frees two far-pointer pairs at offsets `+0x1e/+0x20` and `+0x22/+0x24`
|
||||
|
||||
So the safest current shape is:
|
||||
|
||||
- runtime object header/tail metadata at `0x1300..0x1318`
|
||||
- leading table of `0x80` slot/runtime records occupying `0x0000..0x12ff`
|
||||
|
||||
### 2. `entity_vm_runtime_owner_resource_create` at `1430:0000`
|
||||
|
||||
This helper is now named in the live NE session because its behavior matches the already-verified raw-side owner/resource loader closely.
|
||||
|
||||
Current best read:
|
||||
|
||||
- allocates a compact `0x14`-byte helper object
|
||||
- initializes it as a file-backed loader object
|
||||
- opens the resolved external `eusecode.flx` path through its vtable-backed file path
|
||||
- queries an entry count through vtable `+0x04`
|
||||
- allocates a backing buffer and stores its far pointer at `+0x10/+0x12`
|
||||
- materializes indexed owner/resource records through vtable `+0x0c`
|
||||
|
||||
That aligns well with the raw-side note that the owner/resource loader manages indexed file-set materialization rather than directly exposing simple class-name lookups.
|
||||
|
||||
### 3. Why This Matters For `-u`
|
||||
|
||||
This constructor/loader pair gives the override note a more concrete partial object model:
|
||||
|
||||
- `-u` is not just swapping in a raw file handle or archive pointer
|
||||
- it is constructing a full VM runtime object
|
||||
- that object owns both a slot/runtime table region and an attached owner/resource helper
|
||||
- the attached helper is what materializes the indexed owner-loaded data later consumed by event/process/interpreter paths
|
||||
|
||||
That makes the replacement semantics stronger in practice. The override is swapping the live VM runtime object graph, not merely redirecting one filename global.
|
||||
|
||||
## What The Replacement Root Actually Controls
|
||||
|
||||
### 1. Item/event dispatch
|
||||
|
||||
`Usecode_ItemCallEvent` at `1420:0e3a` reads `1478:6611`, follows the live runtime tables, checks the owner-row capability bits, and creates the event/process context from that data.
|
||||
|
||||
That means the `-u` override reaches normal item-event lanes such as the surviving Crusader event families already documented elsewhere:
|
||||
|
||||
- `use`
|
||||
- `look`
|
||||
- `hit`
|
||||
- `gotHit`
|
||||
- `equip`
|
||||
- `unequip`
|
||||
- `schedule`
|
||||
- `anim`
|
||||
- and the wider event-slot family behind class-specific behavior
|
||||
|
||||
### 2. Usecode process creation
|
||||
|
||||
`UsecodeProcess_CreateProcess` at `1420:0f20` also reads `1478:6611` and builds process/context state from the owner tables hanging off the live root.
|
||||
|
||||
That means the override is not limited to one-shot event callbacks. It reaches process-backed scripted behavior too.
|
||||
|
||||
### 3. Interpreter bytecode stepping
|
||||
|
||||
`Interpreter_NextUsecodeOp` at `1418:332c` pushes `1478:6611/6613` into its runtime helper path.
|
||||
|
||||
That is the strongest scope clue in the whole pass. The replacement is not just changing metadata or an event name table. It is feeding the same runtime used when the interpreter advances usecode bytecode.
|
||||
|
||||
### 4. Gameplay-side scripted capability checks
|
||||
|
||||
`Item_GetDamaged` at `10a0:277b` also reads the same live root and tests owner-row bit `0x40` before falling through to later behavior.
|
||||
|
||||
So the replacement root can affect gameplay-side scripted capability checks as well, not only the hidden/debugger-facing VM side.
|
||||
|
||||
## Practical Meaning For Custom USECODE
|
||||
|
||||
If we can construct a compatible replacement archive/source for the `-u` path, the current evidence says it should be able to influence the same kinds of scripted behavior the stock USECODE runtime already controls.
|
||||
|
||||
Current safest statement:
|
||||
|
||||
- a custom `-u` source should be able to replace class/event bodies that the retail interpreter and process/event creators consume
|
||||
- that makes it relevant to experimentation with event handlers, scripted item behavior, process-backed behaviors, and other VM-driven logic already present in Crusader's usecode system
|
||||
|
||||
Current important limit:
|
||||
|
||||
- this does **not** imply arbitrary native-code injection
|
||||
- the replacement still has to fit the existing Crusader usecode VM/runtime contracts
|
||||
- it is constrained by the interpreter, intrinsic tables, class layout, event table layout, and slot/body decoding the retail game already knows how to execute
|
||||
|
||||
So the safe model is:
|
||||
|
||||
> `-u` gives us a path to run **custom data for the existing usecode VM**, not a generic plug-in system for arbitrary executable code.
|
||||
|
||||
## What A Replacement Probably Needs To Contain
|
||||
|
||||
Because the recovered runtime uses a single root plus one rebuilt slot-base family, the safest current expectation is that the `-u` target is a **complete replacement source** that the loader can resolve into a full owner/class runtime.
|
||||
|
||||
This pass did **not** recover evidence for a sparse overlay format such as:
|
||||
|
||||
- `only one class`
|
||||
- `only one event body`
|
||||
- `patch this one slot onto the stock archive`
|
||||
|
||||
That does not prove such a sparse mode is impossible, but it means we should not assume it.
|
||||
|
||||
For current tooling planning, the defensive assumption should be:
|
||||
|
||||
- prepare a structurally complete replacement source that satisfies the loader's normal expectations
|
||||
- do not assume the game will merge one partial class file into the stock runtime for us
|
||||
|
||||
## Best Current Usefulness For Decompilation Work
|
||||
|
||||
This path is interesting for the current usecode-decompilation lane for two reasons.
|
||||
|
||||
### 1. It is a real in-engine execution route
|
||||
|
||||
If we can identify the accepted `-u` source format and feed it a compatible replacement archive, we get a retail-supported way to make the original game execute alternate usecode data.
|
||||
|
||||
That is far more valuable than a purely offline parser/export loop.
|
||||
|
||||
### 2. It can become a runtime validation path for reconstructed bodies
|
||||
|
||||
The current parser/export work is building toward editable and eventually round-trippable usecode.
|
||||
|
||||
If `-u` accepts a full replacement source, then a future recompiler/exporter could use it to answer questions like:
|
||||
|
||||
- does a reconstructed class/event body still execute in the retail engine?
|
||||
- which opcodes/intrinsic signatures are mandatory for a valid replacement archive?
|
||||
- which behavior changes are script-level and which still depend on native code?
|
||||
|
||||
That makes `-u` potentially useful as a **runtime validation hook** for the ongoing decompilation toolchain.
|
||||
|
||||
## Current Open Questions
|
||||
|
||||
1. Which exact Filespec forms does `<arg>` accept in practice: relative directory, absolute directory, resource alias, CD-rooted path, or more than one?
|
||||
2. Which live-NE startup or game-start function establishes the stock `1478:6611/6613` root when `-u` is not used?
|
||||
3. What exact source/container format does the `-u` loader expect on disk or in resources beyond the filename family `eusecode.flx`, and how does that map onto the `0x14`-byte owner-resource helper plus the `0x1319`-byte runtime object?
|
||||
4. Does the replacement need to be a full archive, or can the loader tolerate a narrower-but-still-valid subset?
|
||||
5. Are there any startup-visible sanity checks that reject malformed but syntactically valid usecode containers?
|
||||
6. What is the smallest safe experimental replacement that can prove the runtime path end-to-end without destabilizing unrelated startup systems?
|
||||
|
||||
## Recommended Next Steps
|
||||
|
||||
1. Close the remaining Filespec semantics for `s_usecode`: confirm which directory/alias forms `Filespec_GetFullPath` accepts in this call shape.
|
||||
2. Identify the live-NE stock-runtime initialization site that seeds `1478:6611/6613` when `-u` is absent.
|
||||
3. Continue walking `entity_vm_runtime_create` / `entity_vm_runtime_owner_resource_create` so more of the `0x26`-stride slot records and the owner/helper object fields are named concretely.
|
||||
4. Compare the resolved runtime root against known EUSECODE/USECODE container structure to decide whether `-u` expects a full FLEX-style archive or another packaging layer.
|
||||
5. Design the smallest conservative replacement experiment, ideally one structurally complete archive with one intentionally changed low-risk class/event body.
|
||||
6. Tie that future experiment back to the current parser/export pipeline so reconstructed bodies can eventually be runtime-tested through `-u`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue