Routes, Data & Saves
9.1The game-data pointer forest
Each of the 78 diagrams is one 0x2c-byte DGF_GameData record; an array of 78
pointers at 0x422848 (gameDataAll) indexes them by global diagram
number. Everything else hangs off pointers inside:
DGF_GameData (0x2c bytes). Multi-segment routes (e.g. the Yamanote
loop split at Ueno) chain through next_game_data with boundary stations.| Offset | Field | Notes |
|---|---|---|
| +0x00 | diagram_global_index | identity key — equals the table index for all 78 records |
| +0x04 | time_zone_policy | 0=Yamanote, 1=Chūō, 2=Osaka/Join, 3=Tōkaidō — feeds getTimeZone_direct's day-period classing |
| +0x08 | train_data_ptr | 0x20-byte DGF_TrainDataRecord array (formation, way numbers, speedometer type, sound profile) |
| +0x0c / +0x10 | stage_data_day / night | stage-data tables for the two lighting variants (_D/_N archives) |
| +0x14 | schedule_ptr | 0x28-byte DGF_StationScheduleData records — the timetable (below) |
| +0x18 | signal_control_table | 8-byte records for the AI front-train signal corrections |
| +0x1c / +0x20 | chain boundary / next segment | multi-segment routes: boundary station + linked DGF_GameData |
| +0x24 | diagram_metadata_ptr | 0x3c-byte DGF_DiagramMetadata: light no, direction, start/end/boundary stations, purchase cost (+0x38) |
9.2The timetable record
One 0x28-byte record per station visit; -1 terminates. The full field map, with
the corpus census (1,080 records across 63 arrays) where it pins semantics:
| Offset | Type | Field | Consumer |
|---|---|---|---|
| +0x00 | i16 | station lookup key (−1 terminator) | all station helpers |
| +0x02 | i16 | pass/stop flag — 1 = stop, 0 = pass through | InitStationInt, getStopStationNo |
| +0x04 | u8×3 | arrival hour / minute / departure second | InitTimeCtl: baseline = h·3600+m·60+s |
| +0x08 | i32 | time-adjust seconds | added to the game clock at schedule init |
| +0x0c | i32 | rate display value | DispStationName group/color classing |
| +0x10 | i32 | time-adjust threshold | branch selection in InitTimeCtl (below) |
| +0x14 | i32 | dwell seconds | rewound from the clock on departure (Chapter 6); corpus 6..32, 38, 1200 |
| +0x18/1c/20 | i32×3 | authored tail (uncoded) | no consumers in this build |
| +0x24 | i32 | door side (conductor mode) | CheckDoorClose/mode 10 |
/* InitTimeCtl @ 0x213630 — schedule-time correction, bounded */
scheduled = hour*3600 + minute*60 + second;
delta = current_time - scheduled;
if (threshold == 0) clock += adjust;
else if (delta < 0) clock += adjust;
else if (delta > threshold) { clock += adjust; clock -= threshold; }
else clock = scheduled + adjust;
9.3One struct, three names
The 876-byte run-state struct at 0x6d32f8 is reached through three accessor
globals that are aliases into the same allocation:
| Accessor | Alias | Content |
|---|---|---|
| getGameStatus() | game_status @ 0x6d32f8 | mode, clock (+0x314/0x318), train refs, join state (+0x338..0x340) |
| getDiaStatus() | same struct, viewed at +0x28… | diagram run data: tier (+0x14), profile (+0x20), item slots (+0x24..0x30) |
| getScoreStatus() | same struct (pool at +0x08) | life pool + per-category accumulators (+0x18·category) |
0x6d4b18 as the "life pool"; it is actually a HUD
object. The real pool is gameStatus+8 = 0x6d3300, verified live
(this report's capture sessions read and wrote both).9.4The save format
SaveReg.xml in /var/lib/save holds one Base64 string,
SaveData, exactly 0x245c characters → 0x1b44 bytes.
No checksum, no compression, no encryption. The payload is three consecutive blocks:
| Offset | Size | Block | Content |
|---|---|---|---|
| 0x0000 | 0x004c | optiondata | volume channels, vibration, controller calibration, control type, autosave flag (+0x48) |
| 0x004c | 0x036c | continueGs | continue snapshot of game_status (mode, clock, position, score) |
| 0x03b8 | 0x178c | playerdata | credits, per-diagram availability (78 × 76-byte DGF_PlayerDiagramRecord), scores, clear ranks, movie permits |
Functions: saveGameData() serializes → Base64 → TinyXML write;
loadGameData() decodes → putSaveData(). Compatibility contract for a
reimplementation: preserve both sizes exactly (0x1b44 payload, 0x245c
Base64); the game validates neither, but the UI paths assume them.
Progression, unlocks and the shop
playerdata carries credits (the coin counter in the menu) and per-diagram
records. Availability states: 0 = locked, 1 = purchased, 2 = purchasable (cost from
DGF_DiagramMetadata+0x38), 3 = always available (the eight secret diagrams:
indices 5, 10, 24, 29, 40, 42, 56, 60). Purchasing (trainbuyq_choice @
0x1f0118) requires credits ≥ cost, then subtracts exactly the cost.
Route completion folds through setClearData (@
0x1c2238): a dependency table at 0x425c08 (8-word entries) defines
which diagrams unlock which; the achieved clear rank is the weakest link across
the route's stations, capped on retry by the retry counter (setStartScore →
setClearData chain). Clearing routes also unlocks the gallery/photo movies
(movie_permit chain).
9.5Asset infrastructure (summary)
The Data/ tree is 4,975 files (~1.6 GB): 3,728 .asn sounds, 726
.dat compressed packs, 350 .aar archives, 85 .txm
textures, 38 .vma animations, 24 .mp4 movies. All container formats
(AAR/ALLZ/ALSN/ALTX/TXM/AMS) are decoded in Chapter 14. The streaming lifecycle that consumes
them: menu/line previews queue texture loads (queueTxmDatIDCB), the driving scene
streams map.dat + areapacNN.dat area packs through the double-bank
system (MainGameMapCheck → GameLoadDataMap(area, MapBank^1)), and the
green-placeholder state visible in this report's own captures is exactly "area pack not yet
decoded".