Files
openroller/docs/re_gc_menu_exe.md
tsuki 831d96e562 Initial public source release
Split reusable rendering and format support into vectorail-core and vectorail-gc.
2026-08-02 17:05:27 +02:00

225 lines
8.8 KiB
Markdown

# Menu reverse notes: game471.exe as source of truth
This work deliberately does not derive menu layout or behavior from web
screenshots. The authoritative inputs are `GC/game_patched.exe` and the
matching files in `GC/data/2d_boost` and `GC/data/task_cfg`.
## Task flow
The global state machine at `FUN_00651440` constructs these tasks in order:
| state | task | constructor | scheduler id |
|---|---|---:|---:|
| `0x10` | `CSelectMusicTask` | `FUN_005aa900` | `0x260` |
| `0x11` | `CDifficultyTask` | `FUN_005bc750` | `0x261` |
| `0x12` | `CGameMainTask` | gameplay constructor | — |
This proves that selecting a song and selecting its difficulty are separate
screens. OpenRoller's earlier combined selector was structurally wrong.
The main vtables recovered from RTTI are:
- `CSelectMusicTask::vftable` at `0x006fbcdc`;
- `CDifficultyTask::vftable` at `0x006fb898`.
Their render callbacks are `FUN_005aca40` and `FUN_005be2d0` respectively.
## Scene files chosen by the executable
`FUN_00446a10` chooses the language-specific select-music task config. The
English configs resolve to:
```text
data/task_cfg/selectmusic_eng.cfg
data/2d_boost/selectmusic2_eng.rvb
data/2d_boost/selectmusic2_eng.mtx
data/task_cfg/difficulty_eng.cfg
data/2d_boost/selectmode2_eng.rvb
data/2d_boost/selectmode2_eng.mtx
```
The `.rvb` MOVI header identifies both scenes as `720x1280 @ 60 fps`. Its two
dimension fields are stored height-first. This ordering is confirmed by the
root placements recovered from `TIME`: `imc_title=(360,112)`,
`imc_focus=(341,695)`, and `imc_navi=(238,1136)`. The UI is authored directly
for the cabinet's portrait display.
The RVB top-level child layout is now parsed by `gc::ParseRvbScene`. For the
English music scene it is:
```text
PREP @ 0x000033 size 0x0056cf (455 action/path bindings)
REPO @ 0x005702 size 0x0017b0
DEFN @ 0x006eb2 size 0x03046e
EXPG @ 0x037320 size 0x000054
TIME @ 0x037374 size 0x00177a
```
`PREP` exposes the real animation hierarchy, including
`/imc_focus/imc_fd_jacket_anim`, the four nodes below `/imc_focus/imc_diff`,
the eight `/imc_sort/imc_sort*` nodes, `tg_decision`, and the focus in/out
actions. The companion difficulty scene exposes its own `/imc_slmode`
hierarchy.
Use the local probe to inspect all bindings:
```sh
./build/openroller-rvb-probe GC/data/2d_boost/selectmusic2_eng.rvb
./build/openroller-rvb-probe GC/data/2d_boost/selectmode2_eng.rvb
```
The probe also accepts exact MovieClip states, for example:
```sh
./build/openroller-rvb-probe --state /=jf_slmusic_start \
--state /imc_focus=jf_focus_start \
GC/data/2d_boost/selectmusic2_eng.rvb
```
## Recovered RVB/MTX runtime
Every animation object is a recursive node with a four-byte tag, total size,
local-data size, local data, and child nodes. `DEFN` supplies named `MOVC` and
`SHAP` definitions; `TIME` contains labeled `FRAM` records. Frames are display
list deltas rather than complete scenes:
- `PLC3` creates/replaces a definition at a depth or updates its transform;
- `RMOV` removes a depth;
- `TRN2` carries the 2D affine transform;
- `COLT` carries RGBA multiplication, including authored visibility fades;
- `ASRC` contains the exported `play();`, `stop();`, and target actions.
`gc::BuildRvbSnapshot` now accumulates those deltas through a selected label
and advances `play()` entry frames to their following `stop()` frame. This is
why hidden templates and transition masks no longer appear together.
MTX starts with `MTX\0`; each payload is a DDS whose first dword was replaced
by the container. Restoring `DDS ` yields a standard DDS. RVB `ImageN` maps to
MTX texture `N-1`; all 115 music resources and all 210 difficulty resources
match their declared dimensions.
## Coordinates recovered from render code
The select-music render callback walks exactly twelve neighboring song slots.
The associated catalog offsets stored at `0x006e0708` are:
```text
-5 -4 -3 -2 -1 0 0 1 2 3 4 5
```
`FUN_00447170` supplies the stable slot geometry. Its twelve vertical pairs
are:
```text
175/189 228/242 281/295 334/348 387/401 440/454
701/715 754/768 807/821 860/874 913/927 966/980
```
The fixed components in `FUN_005aca40` include positions `(39,497)`,
`(251,467)`, `(262,500)`, `(262,522)`, and `(330,594)`. These are floats read
directly from the executable's `.rdata`, not measurements from a screenshot.
`CDifficultyTask` passes sprite centres to `FUN_005b33a0`, which subtracts half
of the scaled source rectangle. The selected song fragments therefore resolve
to these destination rectangles:
```text
jacket source (1,1,196,196) -> (105,167,98,98)
title source (0,197,374,34) -> (209,178,374,34)
source source (0,232,374,24) -> (220,214,374,24)
artist source (198,180,314,16) -> (220,239,314,16)
```
The difficulty callback centers variable-length groups using the executable's
actual formulas and spacings:
- `(7 - count) * 0.5 * 68`, with the row anchored at `426 + 16`;
- `(9 - count) * 0.5 * 52`, with the row anchored at `433 + 16`.
The remaining visual work is outside this recovered static scene snapshot:
the executable-owned player/status HUD in the blank upper band, continuous
timeline interpolation, and the exact transition timing between task states.
## Common and navigator layers
The select task is not visually self-contained. Two additional original
movies are composed with it:
```text
data/2d_boost/common_eng.rvb/.mtx
data/2d_boost/navigator/navi_001_yume.rvb/.mtx
```
`common_eng` supplies the network/player icons and the three bottom controller
prompts. The select-music controller uses the `jf_ctrl_3` layout and the
`jf_ctrl_tx02`, `jf_ctrl_tx05`, and `jf_ctrl_tx08` label states (`Select`,
`Change song order`, `Confirm`).
The navigator is itself a 720x1280 MovieClip scene, not a single positioned
DDS. Its recovered opening state draws the bottom backing plate at
`(0,1000)..(720,1232)`, Yume at `(416,830)..(720,1280)`, and a separate mouth
layer. Rendering only `navigator/001_yume/base.dds` was therefore structurally
incorrect.
## Dynamically linked carousel rows
The twelve list rows do not live in the root select-music timeline. The task
creates twelve instances of each exported linkage symbol below and attaches
them to `imc_scroll_dds`:
```text
EXPG UNIQUE_71 -> mc_music_link
EXPG UNIQUE_74 -> mc_index_link
```
`gc::BuildRvbSymbolSnapshot` resolves those EXPG symbols back to their DEFN
MovieClips. `mc_music_link` contains the 520x34 row plate and its three score
cells. `FUN_00447170` places it at x=8 and the twelve y positions listed above;
`FUN_00447620` supplies row opacity (`0,.7,.8,.9,1,0,0,1,.9,.8,.7,0`), not a
geometric scale. The title atlas fragments do use the same values as scale.
`mc_index_link` is the corresponding 356x36 category plate. Entries returned
by `FUN_005aa7c0` below 50000 select `mc_music_link`; pseudo entries at or above
50000 select `mc_index_link` and therefore consume a normal carousel slot.
For the Genre sort, `FUN_005b40f0` loads
`data/2d_boost/menu/s_j[_eng].dds`. `FUN_005b3fc0` selects one of its 256x32
rows and `FUN_005aca40` draws it at the index clip position plus `(53,2)`:
with the static x=88 row position this gives label x=141. The English rows are
beginner, Anime & Pops, VOCALOID, Touhou, Rhythm Game, Game, Variety, Original.
## Sort-tab indirection
The integer stored in `DAT_007f3134` is not the left-to-right tab number. The
eight internal sort kinds map through the executable byte table `34621857`;
internal kind 0 (Genre and `s_j_eng.dds`) consequently drives visual frame
`jf_sort3_ini`. The visual tab order remains New, Monthly Theme Music, Genre,
Difficulty, Score Average, Title, Favorite, At random. Each `imc_sortN` child
also receives its independent `jf_sortN_on/off` availability frame. The small
40x18 NEW marker at `(44,181)` is a separate root child present in both
`jf_sort3_ini` and the stable `jf_sort3` frame; it is not part of `imc_sort1`.
## Executable-owned menu background
The background is a separate 3D task, not part of any RVB and not a guessed
flat colour. `FUN_00577fb0` first emits a full-screen four-vertex strip with
the exact D3D colours:
```text
top: ARGB FF30309B
bottom: ARGB FFE57386
```
It then draws two locally loaded TUMO resources. The loader table begins at
the literal `data/model/menu_obj_05.tumo`; its second 0x40-byte entry is
`data/model/obj_sphere06.tumo`. The latter is the 288-segment wire sphere seen
behind the list. Its recovered camera is LH, FOV 60 degrees, aspect 720/1280,
near/far 0.1/1000, looking along +Z. The sphere is translated to z=100, scaled
by 5, rotated equally about XYZ at `time*0.125`, and given the small authored
two-frequency vertical drift.
Finally `FUN_005b73a0` draws `data/2d_boost/menu/balloon.dds` as a 12x4 grid
of 64px cells at y=1000. This reconstructs source rectangle `(0,0,768,256)`;
the last 48 pixels are clipped by the 720px cabinet viewport. This is the
dark controller backing visible behind the common HUD and navigator.