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);
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:
| # | Function | Address | Phase |
|---|---|---|---|
| 0 | initMapFunc | 0x1e76c0 | Map init — sets boarding state GameConditionBk=0x104, then → mode 1 (or 11 for join) |
| 1 | startMapFunc | 0x1e9968 | Station departure sequence (doors, announcement, signal) — Chapter 6 |
| 2 | runMapFunc | 0x1e940c | Active driving |
| 3 | shortReplayMapFunc | 0x1e9520 | Station result / grade / replay flow |
| 4 | endMapFunc | 0x1e7adc | Station/route transition (endmode 0–6) |
| 5 | freeMapFunc | 0x1e7724 | Resource cleanup |
| 6 | gameOverMapFunc | 0x1e774c | Game-over screen |
| 7 | pauseFunc | 0x1e7764 | Pause menu |
| 8 | fadeOutMapFunc | 0x1e8248 | Fade out (60-frame) |
| 9 | fadeOutMapWait_Func | 0x1e82e8 | Fade out with 180-frame hold (replay end) |
| 10 | ConductDoorFunc | 0x1e5f5c | Conductor door choreography (300-frame window) |
| 11 | joinMapFunc | 0x1e8a38 | Train 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;
}
mainMapMode is a field of the
GameCondition struct itself (base 0x6d74cc: condition, backup, mode,
mode-backup at +0x0/+0x4/+0x8/+0xc).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:
| Bit | Mask | Name | Meaning |
|---|---|---|---|
| 0 | 0x001 | RUNNING | Physics active; set on departure-ready, checked by ttTrainGo |
| 1 | 0x002 | PAUSE | Pause menu active; freezes time (TimeCtl mask 0x402) |
| 2 | 0x004 | EMERGENCY_BRAKE | Forces Notch=0, Brake=brakeMax every frame in setNochBrake |
| 4 | 0x010 | INHIBIT | Physics inhibit (ttTrainGo skips) |
| 6 | 0x040 | UNDEAD | Set once by ttTrainInit when launched with -undead |
| 7 | 0x080 | (unresolved) | In the suppression mask; no setter found after exhaustive sweep incl. byte-exact ORR #0x80 search |
| 8 | 0x100 | DOORS | Door/boarding flag (set by initMapFunc, cleared on departure) |
| 9 | 0x200 | GAME_OVER | Game-over trigger (set with bit 2 by gmOverCheck as 0x204) |
| 10 | 0x400 | REPLAY | Short replay active |
| 11 | 0x800 | SPEEDLIMIT/MENU | Dual-purpose (speed-limit emergency / menu key) per BLOCK_H |
| 12 | 0x1000 | JOIN_GAME | Coupling mode (joinMapFunc init: 0x1100) |
| 13 | 0x2000 | MAP_DEADLINE | Deadline-approach event in eventStopLine |
| 16 | 0x10000 | LIMIT_STOP | eventLimitStop limit-stop flag |
| 18 | 0x40000 | SEVERE_STOP | Severe stop-penalty override |
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
- Event scanning —
commonMapFunc→trEvCheckEventIwalks an event cursor along the route each frame: signal changes, tunnel lights, overtake triggers, roof events (types 0xb4–0xb7), and the taikou (opposing-train) meet events all fire from per-rail trigger records. - Area streaming —
MainGameMapCheckwatches the train's rail-tag position against the current map-area's range; near a boundary it preloads the adjacentareapacNNinto the inactive bank (GameLoadDataMap(area, MapBank^1)), waits out in-flight reads, then flips the bank (ChangeMapBank). This is why the world fades in piecewise on slow loads (screenshots in Chapter 1 show the green placeholder state. - Replay recorder — every active frame appends packed controller bits to a 6000-frame circular buffer with 0x70-byte headers; the station-result screen replays the approach from a cinematic camera (Chapter 10).
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.