Documentation improvements
This commit is contained in:
parent
d78808d6b5
commit
c34f481c3a
34 changed files with 2800 additions and 20 deletions
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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue