Game Startup Flow
How a skirmish goes from the Start Game button through MSG_NEW_GAME, GameLogic::startNewGame, and into the first frame.
Field notes on what actually happens between clicking Start and seeing the first logic frame tick. The path is long. It leaves the shell, hops through the message stream into GameLogic, punches through map loading, warms the AI, then finally drops the load screen. Order matters at every step and one miswiring corrupts the world state silently.
From the menu to MSG_NEW_GAME
The skirmish menu owns the slot roster. When the host clicks Start, SkirmishGameOptionsMenu.cpp:661 finalizes everything and hands off:
TheWritableGlobalData->m_mapName = TheSkirmishGameInfo->getMap();
TheSkirmishGameInfo->startGame(0);
// ...
GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME );
msg->appendIntegerArgument(GAME_SKIRMISH);
msg->appendIntegerArgument(DIFFICULTY_NORMAL);
msg->appendIntegerArgument(0);
msg->appendIntegerArgument(maxFPS);
startGame(0) locks in the slot table (colors, factions, starting positions) so downstream code can read it authoritatively. The four integer arguments are mode, difficulty, rank-start points, and an FPS cap. After this, the menu is done. Everything else happens when the message stream drains.
Dispatch and preparation
The message lands in GameLogic::logicMessageDispatcher in GameLogicDispatch.cpp:431. The MSG_NEW_GAME case splits the work in two halves:
// prepare for new game prepareNewGame( gameMode, diff, rankPoints ); // start new game startNewGame( FALSE );
prepareNewGame at GameLogicDispatch.cpp:307 owns the UI-adjacent setup: it calls TheScriptEngine->setGlobalDifficulty(diff), creates the Menus/BlankWindow.wnd background layout, hides the shell, stashes m_rankPointsToAddAtGameStart, and crucially sets m_startNewGame = FALSE.
startNewGame at GameLogic.cpp:1143 is the heavy lifter. It resolves TheGameInfo, builds players and teams, drives terrain, walks the map, and arms the pathfinder.
startNewGame: the big switch
The thing to internalize: load order is not flexible. Several subsystems assume their dependencies are already live.
- Terrain before pathfinder. Pathfinder cells read ground height and cliff data.
- Sides before players.
ThePlayerList->newGame()expects side entries to exist. - ThingFactory needs TerrainLogic. Object spawn positions sample
getGroundHeight. - Bridges before the main MapObject sweep. Bridge objects feed the terrain landmark list the pathfinder consumes.
The source has this ordering flattened into one big function. The skeleton:
TheTerrainLogic->newMap( loadingSaveGame ); // ~line 1771 // bridge-only MapObject pass (isBridgeLike) ~line 1784 TheRadar->refreshTerrain( TheTerrainLogic ); // ~line 1829 TheAI->pathfinder()->newMap(); // ~line 1833 // full MapObject pass (units, props, trees) ~line 1920 // ... start buildings, relationships, scripts ... ThePlayerList->newMap(); // ~line 2220
Spawning initial objects
Every non-road, non-bridge entity on the map is a MapObject in a linked list. startNewGame walks it twice: once for bridges, then once for everything else. Lights have no template and fall out early. Trees flagged KINDOF_OPTIMIZED_TREE skip ThingFactory and become batched draw entries. Props and fluff (when forceFluffToProp is on) go to TheTerrainVisual->addProp instead of becoming full logic objects. The rest flow through the factory:
AsciiString originalOwner =
pMapObj->getProperties()->getAsciiString(TheKey_originalOwner);
Team *team = ThePlayerList->validateTeam(originalOwner);
Object *obj = TheThingFactory->newObject( thingTemplate, team );
if( obj )
{
Coord3D pos = *pMapObj->getLocation();
pos.z += TheTerrainLogic->getGroundHeight( pos.x, pos.y );
obj->setOrientation(normalizeAngle(pMapObj->getAngle()));
obj->setPosition( &pos );
obj->updateObjValuesFromMapProperties( pMapObj->getProperties() );
}
Note that pos.z always adds ground height. The map stores local Z offsets from the terrain surface, never absolute world Z.
Quirks
startNewGamereturns early the first time. The call fromMSG_NEW_GAMEhitsGameLogic.cpp:1182, seesm_startNewGame == FALSE(cleared byprepareNewGame), creates the load screen for single-player, setsm_startNewGame = TRUE, and bails at line 1206. The real work happens the next timestartNewGameis called fromupdate(). So the dispatcher triggers setup; the game loop does the actual load.GameSlot::m_coloris raw 0x00RRGGBB now, not a palette index. Any path that wants the display color must go throughMultiplayerSettings::resolveSlotColor, nevergetColor(int). Bounds checks againstgetNumColors()are wrong everywhere they appear.ThePlayeris a placeholder used by challenge campaign maps. It gets resolved atGameLogic.cpp:2233afterThePlayerList->newMap(), and the local skirmish player inherits its enemy relationships.- Network start buildings are placed per-player from start positions — only in multiplayer, not generic skirmish vs. AI. They're distinct from the normal MapObject sweep.
- AI init is staged:
clearGameDataresets,TheAI->pathfinder()->newMap()runs at line 1833, and the firstAIPlayer::updateonly fires after the first logic frame. Nothing AI-side runs during load. - The load screen is the last thing torn down.
deleteLoadScreen()is called near line 2333, after the fade transition. If an assert fires earlier, the load screen stays live as a zombie overlay on top of whatever crash dialog appears — easy to mistake for a freeze.