05 · Gameplay Systems

Menu System

Everything the player does outside the cab (title, attract mode, line and diagram selection, the startup inspection, options, saves) is one loop, one state variable, and one 20-entry subprocess table.
Title screen, photo variant
The title screen alternates between a live-action video backdrop and this still photo (E231 Kehin-Tōhoku livery); both are menu assets over the same title_play subprocess.
Top menu with video attract background
The top menu (Driver's work / Conductor's work / Museum / Option) plays over the attract video. The coin counter bottom-left is the credit system carried over from the arcade lineage.

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:

menuinfoStateRole
0TOPMain menu (four items)
1MuseumTrain gallery / photo mode
2OptionsSettings root
3IntroMenu work state machine (route select etc.)
4InspectionRuns menu_subProc_real(0,0) each frame — the startup inspection and friends
5DisplayThe 121-frame hand-over fade (below)
6–9Volume / Controller / Load-Save / DefaultOption screens
10ContinueContinue/new-game choice
11–14Yamanote / Chuo / Osaka / TokaidoPer-line diagram select
15ConductorConductor 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):

IDHandlerPurpose
5loot_play 0x1f0bc8Diagram select flow (list → detail → commit)
6title_play 0x1efed0Attract loop → title → push-start
7totitle_fadeout 0x1efea8Fade back to title
13exam_play 0x1efe20Startup inspection (below)
16gmenuopen_play 0x1effccPost-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.

Yamanote diagram select list
Yamanote diagram list: 00 is the tutorial (★), 01 the E231 loop run; 03 shows the shop price (25,000 coins) and weather/night variants.
Diagram detail preview
The diagram detail card: formation, segments and rank before commit.
Yamanote line select screen
The line select stage: choosing the line enters its diagram list. The line-select intro is skippable with decide.
Normal mode briefing screen
The commit preview: mode name, formation (E231-500), the three loop segments and the diagram rank: what 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
}
Exhibit: 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.

Diagram title card during load
Commit → the diagram title card while the route streams in. The hand-over sequence itself is 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:

MaskMeaning
0x8000 / 0x2000cursor up / down
0x1000 / 0x4000left / right
0x20decide (confirm)
0x40cancel

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.

← 04 · Game Loop & State Machine