Menu System
title_play
subprocess.
5.1MenuTask: one loop, 16 states
MenuTask (@ 0x1ef81c, procID 3) initializes sprite/shading work,
calls MenuInit, then loops once per frame. A single global, menuinfo @ 0x6dca60, selects among 16 top-level states:
| menuinfo | State | Role |
|---|---|---|
| 0 | TOP | Main menu (four items) |
| 1 | Museum | Train gallery / photo mode |
| 2 | Options | Settings root |
| 3 | Intro | Menu work state machine (route select etc.) |
| 4 | Inspection | Runs menu_subProc_real(0,0) each frame — the startup inspection and friends |
| 5 | Display | The 121-frame hand-over fade (below) |
| 6–9 | Volume / Controller / Load-Save / Default | Option screens |
| 10 | Continue | Continue/new-game choice |
| 11–14 | Yamanote / Chuo / Osaka / Tokaido | Per-line diagram select |
| 15 | Conductor | Conductor mode select |
State 4's dispatch: three outcomes, encoded in one signed global
(DAT_006dca6c):
/* MenuTask, menuinfo==4 branch */
iVar3 = menu_subProc_real(0, 0); // run current sub-process
if (iVar3 != 1) goto next_frame;
DAT_006dca64 = DAT_006dca6c;
if (DAT_006dca6c == 0) { menuinfo = 3; } // back to menu work
else if (DAT_006dca6c < 0) { // negative = sub-proc id
DAT_006dca70 = -DAT_006dca6c;
menu_subProc_real(1, DAT_006dca70); // enter that sub-process
}
else { /* positive = begin the 121-frame fade-out to GameTask */ }
The 20 subprocesses
The actual screens live in menu_subprocF @ 0x42c588: 20 entries,
dispatched by DAT_006dca70 (SubProc ID, distinct numbering from menuinfo):
| ID | Handler | Purpose |
|---|---|---|
| 5 | loot_play 0x1f0bc8 | Diagram select flow (list → detail → commit) |
| 6 | title_play 0x1efed0 | Attract loop → title → push-start |
| 7 | totitle_fadeout 0x1efea8 | Fade back to title |
| 13 | exam_play 0x1efe20 | Startup inspection (below) |
| 16 | gmenuopen_play 0x1effcc | Post-load handler, gated on texture-load completion (checkTxmDat) |
(All 20 entries are decoded in the appendix; the five above carry the boot path.)
5.2Selecting a route: line → diagram → commit
Route selection is a three-layer choice: line (Yamanote/Chūō/Osaka/Tōkaidō)
→ diagram (the day's timetable pattern, 00–42 depending on line) →
commit. The list cursor lives in the 68-byte menuwk work struct
(@ 0x6f2b14); the diagram cursor is menuwk+0xa (@ 0x6f2b1e),
which Get_Diano_Nor (@ 0x219d10) maps into the internal diagram number
that the driving task consumes. Commit calls
getNewDiaListNo → setMenuDiano → setGameStatusContinue and requests
setNextProc(2) after the fade.
SetDiaDecisionGameData consumes at GameTask init.5.3The startup inspection (始業検査)
Before every run, the game runs a controller check: it asks the player to move the mascon and brake to specific detents and watches the inputs. It is the last gate between menus and driving, and a famous one: on hardware without a real controller it never completes. The whole check is one small function:
/* exam_play @ 0x1efe20 — the startup inspection (menu_subproc 13) */
undefined4 exam_play(int param_1)
{
piVar2 = get_examstates(); // state ladder @ 0x6dce20
if (param_1 != 0) { // init: show UI
inExamMenu(); initMenuPadPut(); inTitPut(2); entryExplainMsg(4);
return 0;
}
if (*piVar2 != 8) return 0; // gate: examstates must reach 8
outTitPut(); cancelExplainMsg(); outExamMenu();
*piVar2 = 0;
if ((menuwk->type == 10) && (menuwk->field_5 == 1)) {
GameGo(3); // conductor mode
} else {
GameGo(1); // driver mode
}
return 1; // MenuTask does the clean transition
}
exam_play @ 0x1efe20, Ghidra pseudo-C.The state ladder behind examstates (0→1→2→3→6→8) is driven by
procExamMenu: for the consumer controller (type 2) it demands
_Noch == 0 && _Brake == 9 (neutral plus emergency brake) before advancing. With no analog controller attached (isdencon==0, the P&P's
digital lever), the ladder auto-passes, which is why the shipped unit always boots to driving.
A reimplementation should reproduce the ladder and its gate exactly: it doubles as the
"controller connected" UX.
menuinfo=5: a 121-frame fade whose alpha is
(counter × 0x80) / 0x78, then cleanup
(IntStop → MenuFree → freeBgWord → com_freeBgPicAll → freeSe(0)),
initJoinGameData(), and setNextProc(2).5.4Menu navigation keys
Menus read getMenuKey(n): buffer mnkey @ 0x6d5580,
rebuilt by setGameKey every frame from the pad bitmaps. The navigation trigger
table (per menu type) lives at 0x616860–0x616878; the important masks:
| Mask | Meaning |
|---|---|
| 0x8000 / 0x2000 | cursor up / down |
| 0x1000 / 0x4000 | left / right |
| 0x20 | decide (confirm) |
| 0x40 | cancel |
The decide combination the game treats as "Enter" is 0x820 (bit 5 + bit 12); on hardware this is one physical button; through the port shim it is Enter/Space.
5.5Options and the optiondata block
Options persist in the first 0x4c bytes of the save payload (optiondata @
0x6e180c): volume channels (+0x20/24/26/28/2A/2C), vibration level (+0x30),
controller calibration thresholds for the analog brake (+0x34/38/3c/40; the same curve
setNochBrake's type-4 path quantizes against), and the control type byte (+0x44)
that selects __setpadcontrol's dispatch table row. Autosave is the byte at
+0x48, zero until the player enables Save/Load, which is why a fresh unit loses progress on
power-off.