490 lines
No EOL
18 KiB
Markdown
490 lines
No EOL
18 KiB
Markdown
# 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.
|
|
|
|
### 3a. No Regret cross-check: the live `REGRET.EXE` table is the same 17-word sequence at `1480:075c`
|
|
|
|
The currently opened `REGRET.EXE` session now closes the missing cross-check directly.
|
|
|
|
In `Game_RunNewGameFlow`, the debug/manual warp lane computes:
|
|
|
|
```c
|
|
mapno = *(int *)(g_warpToLevelNoArg * 2 + 0x75c) + DAT_1480_0ad0;
|
|
```
|
|
|
|
So the live No Regret table base is:
|
|
|
|
- `1480:075c`
|
|
|
|
The retail bytes at that address are:
|
|
|
|
```text
|
|
1480:075c: 00 00 01 00 03 00 05 00 07 00 09 00 0b 00 0d 00
|
|
1480:076c: 0f 00 11 00 13 00 15 00 17 00 19 00 1b 00 1d 00
|
|
1480:077c: 28 00
|
|
```
|
|
|
|
Interpreted as little-endian words, that is the same 17-entry base-map sequence already recovered in No Remorse:
|
|
|
|
- `0, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 40`
|
|
|
|
The next two words after the last entry are `0x0000, 0x0000`, so the retail table also has a clean double-zero terminator immediately after the `40` entry.
|
|
|
|
Current best cross-game read is therefore tighter than the earlier one-sided Remorse note:
|
|
|
|
- No Remorse and No Regret both keep the debug `-warp mission` base-map table in executable data
|
|
- the two currently checked retail tables carry the same 17-word payload
|
|
- the practical game-to-game difference in this startup area is not the mission table itself, but the surrounding startup control flow and data addresses
|
|
|
|
### 3b. Renderer-side extracted JSON now records both retail tables
|
|
|
|
The public renderer project now has a dedicated extractor for this table instead of keeping the mission mapping only in prose notes.
|
|
|
|
Current generator and output:
|
|
|
|
- script: `Crusader_Decomp_Public/map_renderer/src/generate-mission-map-data.js`
|
|
- generated cache file: `Crusader_Decomp_Public/map_renderer/.cache/mission-map-data.generated.json`
|
|
|
|
That JSON captures both retail tables, their data-segment addresses, the per-mission base-map entries, and the reverse `map -> missions` lookup used by the renderer metadata.
|
|
|
|
The renderer's scene metadata builder now consumes that generated table so usage notes can distinguish:
|
|
|
|
- maps that are real base mission entries in the executable `-warp` table
|
|
- maps that are only reachable through `-mapoff` or some other non-table selector path
|
|
|
|
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.
|
|
|
|
### 3c. 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.
|
|
|
|
# Maps mapping by Candy
|
|
|
|
These entries might be shifted by 1
|
|
|
|
28: Rebel base
|
|
29: destroyed Rebel base
|
|
|
|
Mission 1:
|
|
0: level 1, looks complete
|
|
34: level 1, most complete
|
|
56: level 1
|
|
|
|
Mission 2:
|
|
2: mission 2
|
|
3: mission 2 incomplete
|
|
|
|
Mission 3:
|
|
4: thermatron manufacturing plant
|
|
38: thermatron manufacturing plant
|
|
43: thermatron manuf plant
|
|
45: thermatron manuf plant
|
|
5: thermatron plant broken
|
|
|
|
Mission 4:
|
|
6: blow up comms
|
|
44: incomplete mission 5
|
|
|
|
Mission 5:
|
|
8: first half
|
|
9: second half
|
|
|
|
Mission 6:
|
|
10:
|
|
|
|
Mission 7:
|
|
11: Blow up nerve gas stockpile
|
|
12: Blow up nerve gas stockpile
|
|
|
|
Mission 8: Save prof Willmar who dies on telepad
|
|
13,14: first mission with extra protection stuff, 5?
|
|
|
|
Mission 9:
|
|
15:
|
|
36:
|
|
|
|
Mission 10:
|
|
16: mission 10
|
|
|
|
Mission 11: Download plans for platform
|
|
17: first half
|
|
35: no idea, looks mostly complete level
|
|
37: incomplete cipher chip level
|
|
31: incomplete mission 6
|
|
20: Looks like part of a mission but incomplete. Has that big computer thing
|
|
|
|
Mission 12:
|
|
19: ????? Third done mission. Start matches.
|
|
|
|
Mission 13:
|
|
21: mission 13
|
|
22: incomplete mission 13
|
|
39: mission 13 incomplete
|
|
40: mission 13 very incomplete
|
|
|
|
Mission 14:
|
|
23: mission 14
|
|
24: mission 14 with cheese fart
|
|
|
|
Mission 15:
|
|
25: mission 15 |