04 · Gameplay Systems

Game Loop & State Machine

GameTask is the driving scene's beating heart: a 60 Hz loop that snapshots world conditions, dispatches one of twelve phase functions, integrates the physics, and records what to draw. Everything else in the game hangs off its two tables and one bit-field.

4.1GameTask: the per-frame loop

When the menu hands over (setNextProc(2)), trainMain starts GameTask (@ 0x1e718c) as task 99 with a 24 KB stack; six times every other process, because this function coordinates physics, streaming, camera, effects and scoring in one frame. Its initialization runs once:

CdSyncRead();              // flush pending asset reads
SetDiaDecisionGameData();  // commit the selected diagram into game data
setJoinGame(-1);           // resolve join (multi-segment) state → station status
setStationStatus(...);     // set current/next station indices
MapInit(); initMapMainBefore(); MapRead();   // load route map data
chgMapTrain(); MapSet(); initMapMainAfter(); // place train, cameras, mode=0
startDemo(2);              // pre-departure intro cinematic (below)

Then the loop, annotated, from the decompilation:

/* GameTask @ 0x1e718c — one iteration = one 60 Hz frame */
do {
  GameCondition = GameConditionBk;      // 1. snapshot world switches
  gameMode = getGameMode();             // 0=driver 1=lecture 2/3=conductor 4=replay
  mode    = setMainMapMode(-1);         // read current phase (no change)

  // 2. game-over / replay-end detection (bit 0x200 or replay buffer end)
  if (gameMode == 4 && (replayBufferDone || GameCondition & 0x200)) { …fade out… }

  // 3. station-stop transition: running==0 while at a platform
  if (mode == 2 && !(GameCondition & 1)) {
      SaveStationStopTime();
      switch (gameMode) {
        case 1:  EndRunLecture(1); break;              // tutorial wrap
        case 2,3: setMainMapMode(10); break;           // conductor doors
        case 4:  setEndMapMode(0); setMainMapMode(9); break;
        default: setMainMapMode(3); break;             // station result screen
      }
  }

  setNochBrake();                       // 4. input → notch/brake (Chapter 7)
  if (gameMode != 4) checkPause();      // 5. Enter during driving → pause mode

  // 6. dispatch current phase; run its per-frame function
  if (setMainMapMode(-1) == 2 && mainMap() != 0) {
      // 7. ACTIVE DRIVING FRAME — the ordered pipeline:
      ttTrainGo();          // physics integration      (Chapter 7)
      moveMap();            // move along route
      procSReplay(_RunRoutePos); // record replay frame
      MainGameMapCheck(_MainRailTagID); // area streaming / bank switch
      GameViewProc();       // camera
      commonMapFunc();      // events, TimeCtl, judges, game-over check
      RendMap();            // queue world drawing
      setMapTrain(); procRilTrain();       // train position + models
      DispFallRain(); drawEffectAll();     // weather + effects
      LectDispCheck(); procLecture();      // tutorial overlays
      procGameSp();         // HUD: speed, notch, score, life, navi…
      procDispConductor(); procGradeStg(); // conductor UI, grades
      DispStationInt(); procWordMenu();    // station interior, text
      procSprMove(); procSprAttach(); procSprShade(); // 2D layers
      OldViewType = ViewType;
  }
  ttDelayTask(1);                       // yield until next tick
} while (true);
Exhibit: GameTask @ 0x1e718c, 230 lines of decompiled C condensed to its skeleton. Note the fixed ordering: physics before judgments before rendering; a reimplementation must preserve this order to reproduce behavior.

The pre-departure cinematic

Before the loop starts, startDemo(2) runs playDemo (@ 0x1e41a0), a five-state flyover showing the train and station, with the “Yamanote Line Diagram #01” title card. It ends after 269 frames, or after 79 frames if the player presses decide (checked against denkeyP+0x2c & 0x60), then cross-fades out and hands over to the loop:

case 1:                                   // hold
    cnt++;
    if (cnt > 0x10d ||                    // ~4.5 s at 60 fps
       (cnt > 0x4f && (*(uint *)((int)denkeyP + 0x2c) & 0x60) != 0)) {
        mode = 2; cnt = 0;                // → fade out
    }
    break;

4.2The 12 map modes

Every gameplay phase is one function in a 12-entry table at 0x42C348. A phase is initialized once (called with -1) and then called with 0 every frame until it reports done:

#FunctionAddressPhase
0initMapFunc0x1e76c0Map init — sets boarding state GameConditionBk=0x104, then → mode 1 (or 11 for join)
1startMapFunc0x1e9968Station departure sequence (doors, announcement, signal) — Chapter 6
2runMapFunc0x1e940cActive driving
3shortReplayMapFunc0x1e9520Station result / grade / replay flow
4endMapFunc0x1e7adcStation/route transition (endmode 0–6)
5freeMapFunc0x1e7724Resource cleanup
6gameOverMapFunc0x1e774cGame-over screen
7pauseFunc0x1e7764Pause menu
8fadeOutMapFunc0x1e8248Fade out (60-frame)
9fadeOutMapWait_Func0x1e82e8Fade out with 180-frame hold (replay end)
10ConductDoorFunc0x1e5f5cConductor door choreography (300-frame window)
11joinMapFunc0x1e8a38Train coupling sequence — Chapter 6
/* setMainMapMode @ 0x1e7974 — the whole dispatch mechanism */
int setMainMapMode(int param_1)
{
  if ((GameCondition.mainMapMode != param_1) && (-1 < param_1)) {
    GameCondition.mainMapModeBk = GameCondition.mainMapMode;
    GameCondition.mainMapMode = param_1;
    (*(code *)(&mainFunc)[param_1])(0xffffffff);   // init new phase
    return GameCondition.mainMapMode;
  }
  return GameCondition.mainMapMode;
}
Exhibit: note that mainMapMode is a field of the GameCondition struct itself (base 0x6d74cc: condition, backup, mode, mode-backup at +0x0/+0x4/+0x8/+0xc).
State diagram of the 12 mainFunc map modes
The map-mode lifecycle for one station visit. initMap → startMap → run → (stop) → result/conductor → endMap → free → next station. Game-over and pause are side exits from run.

4.3GameCondition: the world's switchboard

Two globals form a snapshot pair: game logic flips bits in GameConditionBk (@ 0x6d74d0); once per frame the loop copies it into GameCondition (@ 0x6d74cc) so every subsystem reads a consistent snapshot. The decompiled bit table:

BitMaskNameMeaning
00x001RUNNINGPhysics active; set on departure-ready, checked by ttTrainGo
10x002PAUSEPause menu active; freezes time (TimeCtl mask 0x402)
20x004EMERGENCY_BRAKEForces Notch=0, Brake=brakeMax every frame in setNochBrake
40x010INHIBITPhysics inhibit (ttTrainGo skips)
60x040UNDEADSet once by ttTrainInit when launched with -undead
70x080(unresolved)In the suppression mask; no setter found after exhaustive sweep incl. byte-exact ORR #0x80 search
80x100DOORSDoor/boarding flag (set by initMapFunc, cleared on departure)
90x200GAME_OVERGame-over trigger (set with bit 2 by gmOverCheck as 0x204)
100x400REPLAYShort replay active
110x800SPEEDLIMIT/MENUDual-purpose (speed-limit emergency / menu key) per BLOCK_H
120x1000JOIN_GAMECoupling mode (joinMapFunc init: 0x1100)
130x2000MAP_DEADLINEDeadline-approach event in eventStopLine
160x10000LIMIT_STOPeventLimitStop limit-stop flag
180x40000SEVERE_STOPSevere stop-penalty override
Key compound masks 0x7C2 = game-over suppression (pause, undead, bit7, doors, game-over, replay; gmOverCheck refuses to trigger while any is set). 0x204 = game-over + emergency brake, set together. 0x104 = boarding initial state (emergency brake + doors). 0x402 = time-stop (pause or replay).

4.4Time and the timetable

Game time is an integer second counter at gameStatus+0x314 (@ 0x6d360c), advanced by TimeCtl once per active frame (masked off by pause/replay: GameCondition & 0x402). The scheduled arrival second for the current station sits at gameStatus+0x318 (@ 0x6d3610), and the difference drives the late-arrival judgment (Chapter 8). Departure rewinds the clock by the station's dwell time (startMapFunc mode 3: gameStatus+0x314 -= schedule.dwell_seconds_14), so the timetable stays coherent across stops.

4.5Events, streaming, and the replay recorder

Debug leftovers in the loop A four-state secFlg "section event" machine with a jump table at 0x425288 exists but its arming functions have zero callers (dead code). mapViewer3 (a free-drive train previewer) and testProcess (procID 1) are orphaned debug harnesses, reachable only by poking procman directly.
← 03 · Aqualead Engine Core