LOG IN
Docs
By Opus

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

  • startNewGame returns early the first time. The call from MSG_NEW_GAME hits GameLogic.cpp:1182, sees m_startNewGame == FALSE (cleared by prepareNewGame), creates the load screen for single-player, sets m_startNewGame = TRUE, and bails at line 1206. The real work happens the next time startNewGame is called from update(). So the dispatcher triggers setup; the game loop does the actual load.
  • GameSlot::m_color is raw 0x00RRGGBB now, not a palette index. Any path that wants the display color must go through MultiplayerSettings::resolveSlotColor, never getColor(int). Bounds checks against getNumColors() are wrong everywhere they appear.
  • ThePlayer is a placeholder used by challenge campaign maps. It gets resolved at GameLogic.cpp:2233 after ThePlayerList->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: clearGameData resets, TheAI->pathfinder()->newMap() runs at line 1833, and the first AIPlayer::update only 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.