Crusader_Decomp/docs/first-mission-map-selection.md

14 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 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:

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:

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:

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:

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:

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:

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:

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.