18 KiB
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 0x1e00011020:0249 CALLF 0x1090:04ce
No Regret also hardcodes the same values, but there are two relevant selectors:
- an early menu-start selector in
Game_Startat1008:1448 - the actual mission-start selector later in
FUN_1030_032dat1030: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 ing_warpToLevelNoArg-mapoff-> stores an additive map offset ing_mapoffArgValue-egg-> stores an egg override ing_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 override1480:0aca= warp Y override1480: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_warpToLevelNoArg1478:0854=g_mapoffArgValue1478:0856=g_eggArgValue
2. Normal new-game startup is hardcoded in Game_Start
Game_Start has two distinct startup branches:
- normal new game: no
-warpargument - debug/manual warp:
g_warpToLevelNoArg != -1
In the normal new-game branch, the game does this directly:
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 0x11020:0243 PUSH 0x1e00011020: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:
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],0x01008:1446 PUSH 0x11008:1448 PUSH 0x1e00011008: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:
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 0x11030:05c5 PUSH 0x1e00011030:05cb PUSH CS1030: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:
iVar5 = *(int *)(g_warpToLevelNoArg * 2 + 0x488) + g_mapoffArgValue;
Disassembly:
1020:0254 MOV BX,word ptr [0x84a]1020:0258 SHL BX,0x11020:025a MOV AX,word ptr [BX + 0x488]1020:025e ADD AX,word ptr [0x854]
Static bytes at 1478:0488:
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:
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:
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 missionbase-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
-warptable - maps that are only reachable through
-mapoffor 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,-warpplus-mapoffalready 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-eggomitted so the code uses the directNPC_Teleportpath
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 = 504itemCount = 1080terrain = 902egg = 149editor = 10npcLinkedItems = 16invalidItemCount = 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 = 28670appears repeatedly with y-values such as19454,20478,21502,22526,23550,24574, and25598x = 30718appears repeatedly with the same lower/mid y-bands such as19454,20478,21502,22526,23550,24574, and25598y = 30718is also real on map246, but in a different band; one cached pair there isx = 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_Teleportpath is working - map
246itself is present and populated - but
28670,30718,0is 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,030718,25598,022526,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::mapnoat offset0x32
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:
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.DATcontains 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:
irqdmaportsoundmusicfullinstallbigvideosubtitlescdletterflicpath
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
248instead of map1
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, egg0x1e - or a nearby wrapper rewrite that substitutes the startup map before
Teleporter_CreateProcessDirectruns
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.EXEfor No RemorseREGRET.EXEfor 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:
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, egg0x1einGame_Start. - No Regret hardcodes the same values twice: once in
Game_Start, and again in the laterFUN_1030_032dmission-start path that actually controls a real new game. - The debug
-warp missionpath uses an executable-embedded mission-to-map word table at1478:0488, plus-mapoff. - External files such as
FIXED.DAThold the actual map contents. CRUSADER.CFGdoes 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