09 · Rules & Flow

Routes, Data & Saves

Seventy-eight diagrams, four lines, one timetable per station, and a 6,980-byte save, all wired through a compact pointer forest that this chapter maps end to end.

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 pointer forest
DGF_GameData (0x2c bytes). Multi-segment routes (e.g. the Yamanote loop split at Ueno) chain through next_game_data with boundary stations.
OffsetFieldNotes
+0x00diagram_global_indexidentity key — equals the table index for all 78 records
+0x04time_zone_policy0=Yamanote, 1=Chūō, 2=Osaka/Join, 3=Tōkaidō — feeds getTimeZone_direct's day-period classing
+0x08train_data_ptr0x20-byte DGF_TrainDataRecord array (formation, way numbers, speedometer type, sound profile)
+0x0c / +0x10stage_data_day / nightstage-data tables for the two lighting variants (_D/_N archives)
+0x14schedule_ptr0x28-byte DGF_StationScheduleData records — the timetable (below)
+0x18signal_control_table8-byte records for the AI front-train signal corrections
+0x1c / +0x20chain boundary / next segmentmulti-segment routes: boundary station + linked DGF_GameData
+0x24diagram_metadata_ptr0x3c-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:

OffsetTypeFieldConsumer
+0x00i16station lookup key (−1 terminator)all station helpers
+0x02i16pass/stop flag — 1 = stop, 0 = pass throughInitStationInt, getStopStationNo
+0x04u8×3arrival hour / minute / departure secondInitTimeCtl: baseline = h·3600+m·60+s
+0x08i32time-adjust secondsadded to the game clock at schedule init
+0x0ci32rate display valueDispStationName group/color classing
+0x10i32time-adjust thresholdbranch selection in InitTimeCtl (below)
+0x14i32dwell secondsrewound from the clock on departure (Chapter 6); corpus 6..32, 38, 1200
+0x18/1c/20i32×3authored tail (uncoded)no consumers in this build
+0x24i32door 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;
Exhibit: how the game re-syncs its clock to the published timetable at each stop: small drift is absorbed, large drift is clamped back to the schedule.

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:

AccessorAliasContent
getGameStatus()game_status @ 0x6d32f8mode, 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)
A debunked address The older RE notes treated 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:

OffsetSizeBlockContent
0x00000x004coptiondatavolume channels, vibration, controller calibration, control type, autosave flag (+0x48)
0x004c0x036ccontinueGscontinue snapshot of game_status (mode, clock, position, score)
0x03b80x178cplayerdatacredits, 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".

World streaming with placeholder quads
Area-pack streaming mid-load: geometry present, textures pending; the loader's visible seam.
Diagram title card
The diagram title card shown while the first area packs load.
← 08 · Scoring & Penalties