Initial public source release

Split reusable rendering and format support into vectorail-core and vectorail-gc.
This commit is contained in:
2026-08-02 17:05:27 +02:00
commit 831d96e562
109 changed files with 20558 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct Item {
u32 id;
string texture;
string unk2; // Name?
u8 unk3;
string descriptionJP;
string descriptionEN;
};
u16 itemCount @ 0x00;
Item items[itemCount] @ 0x02 [[inline]];
+32
View File
@@ -0,0 +1,32 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct Message {
u32 id;
string texture;
string unk1;
u8 unk2;
string unk3; // Contents
string unk4; // Contents
bool unk5;
bool unk6;
};
u16 messageCount @ 0x00;
Message messages[messageCount] @ 0x02 [[inline]];
+46
View File
@@ -0,0 +1,46 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct Navigator {
u32 id;
string texture; // /data/2d_boost/navigator/<name>.dds
string unk2;
string unk3;
string unk4;
string unk5;
string unk6;
string unk7;
string unk8;
string unk9;
string unk10;
u8 unk11;
u8 unk12;
u8 unk13;
u8 unk14;
u32 unk15;
u32 unk16;
u8 unk17;
u8 unk18;
u8 unk19;
string descriptionJP;
string descriptionEN;
};
u16 navigatorCount @ 0x00;
Navigator navigators[navigatorCount] @ 0x02 [[inline]];
+47
View File
@@ -0,0 +1,47 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct Player {
u32 id;
string texture; // /data/2d_boost/avatar/<name>.dds
string unk2;
string unk3;
string unk4;
string unk5;
string unk6;
string unk7;
u8 unk8;
string unk9; // /data/model/<name> - pngs only
string unk10; // /data/model/<name> - uvb/tumo files
string unk11; // /data/model/<name> - tusc/efcb2 files
u32 unk12;
u32 unk13;
u8 unk14;
u32 unk15;
u32 unk16;
u32 unk17;
u32 unk18;
u8 unk19;
string descriptionJP;
string descriptionEN;
u32 unk22;
};
u16 playerCount @ 0x00;
Player players[playerCount] @ 0x02 [[inline]];
+35
View File
@@ -0,0 +1,35 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct SE {
u32 id;
string texture;
string unk2; // Name long?
string unk3; // Name short?
string unk4;
string se_name1; // /data/sound/<name>.wav
string se_name2;
string se_name3;
u8 unk8;
string descriptionJP;
string descriptionEN;
};
u16 seCount @ 0x00;
SE ses[seCount] @ 0x02 [[inline]];
+31
View File
@@ -0,0 +1,31 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct Skin {
u32 id;
string texture;
string unk2;
string skinFolder;
u8 unk4;
string descriptionJP;
string descriptionEN;
};
u16 skinCount @ 0x00;
Skin skins[skinCount] @ 0x02 [[inline]];
+197
View File
@@ -0,0 +1,197 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
enum Genre_New : u8 {
GENRE_UNKNOWN = 0,
GENRE_ANIME_POP,
GENRE_VOCALOID,
GENRE_OTOGE,
GENRE_GAME,
GENRE_VARIETY,
GENRE_ORIGINAL,
GENRE_TOUHOU
};
enum Genre_GC2 : u8 {
GENRE_GC2_UNKNOWN = 0,
GENRE_GC2_POPS,
GENRE_GC2_VOCALOID,
GENRE_GC2_GAME,
GENRE_GC2_VARIETY,
GENRE_GC2_ORIGINAL,
GENRE_GC2_ANIME,
GENRE_GC2_TOUHOU
};
enum Genre_GC1 : u8 {
GENRE_GC1_UNKNOWN = 0,
GENRE_GC1_ANIME_POP,
GENRE_GC1_VOCALOID,
GENRE_GC1_GAME, // Rhythm games and touhou are merged into this in GC1
GENRE_GC1_VARIETY,
GENRE_GC1_ORIGINAL
};
enum SKIN : u8 {
SKIN_NONE = 0,
SKIN_BASIC,
SKIN_FLOWER,
SKIN_FIREWORKS,
SKIN_MARBLES,
SKIN_MIDCENTURY,
SKIN_INFINITYGENE,
SKIN_MOLECULE,
SKIN_BUTTERFLY,
SKIN_PAISLEY,
SKIN_8BIT,
SKIN_BRIGHTNESS,
SKIN_HOLOGRAM,
SKIN_HEAVEN,
SKIN_SNOW,
SKIN_FEATHER,
SKIN_NOTE,
SKIN_STAR,
SKIN_HORROR,
SKIN_HEART,
SKIN_SYNCHRONICA
};
enum SongFlag : u8 {
NONE = 0,
TUTORIAL = 1,
GC1_CREDITS = 2,
SHOW_CATEGORY = 3,
GC2_CREDITS = 4,
TUTORIAL_JP = 5,
TUTORIAL_EN_RV = 6,
TUTORIAL_EN_GC = 7,
GC3_CREDITS = 8,
GC3EX_CREDITS = 9,
GC4_CREDITS = 10,
};
struct Song {
u32 id;
string name;
string ident;
string artist;
if (PARSE_VER >= Version::GC2)
string extraInfo;
string yomigana;
if (PARSE_VER <= Version::GC1EX)
Genre_GC1 genre;
else if (PARSE_VER == Version::GC2)
Genre_GC2 genre;
else
Genre_New genre;
string duration;
u8 diffSimple;
u8 diffNormal;
u8 diffHard;
if (PARSE_VER > Version::GC1) u8 diffExtra;
string bpm;
if (PARSE_VER == Version::GC1) {
u32 unk1; // TODO: Maybe look what this is?
} else if (PARSE_VER == Version::GC1EX) {
// TODO: for 1EX one of these needs to have [4] otherise shit gets fucked
u8 bgmVolume[4];
u8 shotVolume;
} else {
u8 bgmVolume[4];
u8 shotVolume[4];
}
if (PARSE_VER <= Version::GC1EX) u32 unk2;
if (PARSE_VER >= Version::GC2) {
u16 unk3;
u16 unk4;
}
u32 previewSeekStartMillis;
u32 previewSeekEndMillis;
SKIN defaultSkin;
SongFlag specialFlags; // TODO: Implement the enum
string bgmFilePrefix;
// TODO: Rework this a bit
if (PARSE_VER == Version::GC1) {
string stageFilenameSimple;
string stageFilenameNormal;
string stageFilenameHard;
string bgmFilenameSuffixSimple;
string bgmFilenameSuffixNormal;
string bgmFilenameSuffixHard;
} else if (PARSE_VER == Version::GC1EX) {
if (id != 999) {
string shotFilenameSuffixSimple;
string shotFilenameSuffixNormal;
string shotFilenameSuffixHard;
string shotFilenameSuffixExtra;
}
string stageFilenameSimple;
string stageFilenameNormal;
string stageFilenameHard;
string stageFilenameExtra;
string bgmFilenameSuffixSimple;
string bgmFilenameSuffixNormal;
string bgmFilenameSuffixHard;
string bgmFilenameSuffixExtra;
} else {
string bgmFilenameSuffixSimple;
string bgmFilenameSuffixNormal;
string bgmFilenameSuffixHard;
string bgmFilenameSuffixExtra;
string shotFilenameSuffixSimple;
string shotFilenameSuffixNormal;
string shotFilenameSuffixHard;
string shotFilenameSuffixExtra;
string stageFilenameSimple;
string stageFilenameNormal;
string stageFilenameHard;
string stageFilenameExtra;
}
if (PARSE_VER >= Version::GC2) {
string stageRegionSuffix;
u32 judgeDelay; // TODO: Confirm
}
if (PARSE_VER == Version::GC1EX) u8 musicPanelCost;
u8 unlockable; // definitely a bool, seems to dictate whether server unlocks work
u8 songEnabled;
u8 unk7;
if (PARSE_VER == Version::GC4) u8 isRecommended;
} [[format("song_formatter")]];
fn song_formatter(ref Song s) {
return std::format("{} {}: ({})", s.id, s.ident, s.name);
};
Version PARSE_VER = Version::GC1EX;
u16 songCount @ 0x00;
Song songs[songCount] @ 0x02 [[inline]];
+40
View File
@@ -0,0 +1,40 @@
#pragma endian big
import std.io;
import std.string;
using string = std::string::SizedString<u8> [[format("string_formatter")]];
fn string_formatter(ref string s) {
return std::format("\"{:s}\"", s);
};
enum Version : u8 {
GC1 = 1,
GC1EX = 2,
GC2 = 3,
GC3 = 4,
GC4 = 5,
};
Version PARSE_VER = Version::GC4;
struct Title {
u32 id;
string texture;
string textureSuffixEn;
string textJP;
string textEN;
u8 unk5;
string descriptionJP;
string descriptionEN;
string unk6; // Also descriptions, maybe for next title?
string unk7; // ^
u8 unk8;
u8 unk9;
u8 unk10;
u8 unk11;
u8 unk12;
u8 unk13;
};
u16 titleCount @ 0x00;
Title titles[titleCount] @ 0x02 [[inline]];
+211
View File
@@ -0,0 +1,211 @@
# OpenRoller PSP port
The first hardware target is a PSP-1004 (PSP-1000/Fat). The runtime must fit
the original model's memory limits and must not depend on the extra application
memory available on later PSP models.
## Display contract
The arcade game uses a 720x1280 portrait canvas. OpenRoller keeps those logical
coordinates on PSP and rotates the final scene into the physical 480x272 panel:
- scale: 3/8;
- resulting image: 270x480;
- physical border: one pixel on each long side;
- `SELECT`: swap clockwise/counter-clockwise tate orientation;
- `START`: pause/resume once a stage is loaded.
This transform must remain above both the gameplay renderer and the song-select
renderer. Content code should not contain PSP screen coordinates.
Gameplay geometry is submitted to the native PSP Geometry Engine as
`GU_TRANSFORM_3D` vertices. The 720x1280 portrait projection is preserved in
tate mode by swapping its horizontal/vertical projection extents and rolling
the camera basis by 90 degrees. The GU performs perspective division,
near/far clipping, and depth testing; CPU projection is not part of the runtime
renderer except for the original screen-relative directional-note arrow and
lower-right helper overlays.
## Memory rules for PSP-1000
- Use RGB565 double framebuffers and a 16-bit depth buffer. Together they use
0xcc000 bytes of VRAM, leaving about 1.2 MiB of VRAM for resident textures.
- Never load a whole song into RAM. Audio is decoded from a bounded streaming
ring and the decoded sample count is the gameplay clock.
- Keep only the current jacket resident; replace it when selection moves.
- Convert DDS/MTX textures on the host to RGB565, RGBA4444, or indexed T8 data.
- Parse arcade formats on the host. PSP consumes little-endian, aligned binary
arrays and never instantiates the reverse-engineering object graph.
- Generate/copy only the visible rail window and visible notes each frame.
- Background meshes require culling and a fixed resident-memory budget.
## Build
The local machine does not need a permanent PSPDEV installation. The build
script uses the official PSPDEV Docker image:
```sh
tools/build_psp.sh
```
The result is `psp/EBOOT.PBP`. Copy the runtime directory to:
```text
ms0:/PSP/GAME/OpenRoller/
```
When `catalog.orpc` is present next to `EBOOT.PBP`, the runtime opens the
portrait song-select screen and lazily loads the selected jacket, chart and
audio stream. It renders the real track, camera, notes, draw window, four-corner color
background, and the stage-authored `.tumo` objects. Object translation, scale,
rotation, color and visibility channels are evaluated on PSP; one-level parent
composition, wireframe selection, the fixed 250 ms visibility fade and sorted
alpha rendering follow the recovered `game471.exe` path.
Particle repeat/lifetime, spawn shape, direction, velocity and opacity are now
evaluated against the authored timeline and current BPM. The sprite resource
lookup is not ported yet, so particle instances use a small geometric marker.
Visualizer type, color and timeline are also carried by `.orps`; the current GE
renderer maps those types to rings, rays, bars and scanlines, but still uses a
deterministic pulse rather than the original audio analyser/FFT path. TUMO
geometry is position/color only at this stage; its material textures remain the
next background fidelity layer.
Without a catalog, the legacy `stage.orps`/`audio.mp3` pair still works. Without
either format, the executable falls back to the synthetic smoke scene.
The host preparation script applies both per-difficulty volumes from
`stage_param.dat`, pre-mixes the authored BGM and SHOT WAVs with a
latency-compensated limiter, and emits one CBR 128 kbit/s, 44.1 kHz stereo MP3
per chart. The PSP runtime decodes that stream incrementally through one handle
of the native MP3 module and submits it through one SRC channel. This is
deliberately single-stream: simultaneous MP3 handles stutter on PSP-1000 while
the 3D stage is active. Reactive SHOT muting is therefore temporarily disabled
in the PSP build. The number of samples accepted by audio output is the gameplay
clock; rendering framerate does not advance song time. `SE_ARRANGE`, `TAP_SE1` and
`TAP_SE2` are host-converted to small 44.1 kHz PCM effects mixed by the same
thread.
The host-side chart normalizer is built with the regular desktop CMake build:
```sh
cmake -S . -B build
cmake --build build --target openroller-psp-pack
build/openroller-psp-pack GC/data/stage/ac_example_easy.dat stage.orps
```
The version-4 `.orps` file is a little-endian, 16-byte-aligned snapshot containing the
track, notes, camera, draw distance, background colors, pre-triangulated TUMO
vertices, stage-object records, flattened animation keys, particles,
visualizer keys and BPM changes. Each note also carries the effective runtime
type, appearance/end times, directional vector, per-note early/late/miss/mute
windows, colour and effect selector produced by the recovered
`BuildTimingDataSub` path. It deliberately
retains unknown note fields so discoveries made later in the reverse do not
invalidate imported charts. For `ac_10pt8tion_easy.dat` the full package is
about 493 KiB; the 36 source models contribute about 356 KiB of position-only
vertices and are submitted to GU directly from the loaded package.
For a ready-to-copy Memory Stick directory using the default test chart:
```sh
tools/prepare_psp_demo.sh
```
This creates `dist/PSP/GAME/OpenRoller/`. Copy that `OpenRoller` directory to
`ms0:/PSP/GAME/`. Runtime controls are:
For the 30-song library build used by the PSP-1004 port:
```sh
tools/prepare_psp_library.sh
```
This produces 97 difficulty packages for `10pt8ion`, `Oshama Scramble!`,
`Bonetrousle`, `Shadow`, `Planet connection`, `Departure`, `Journey`,
`SPACE ARCADIAN`, `Altale`, `Analysis Division`, `Thrash Beat`, `Agent Angels`,
`FLOWER`, `ADRENA`, and `7 days a week`, plus `Satisfiction` and fourteen
VOCALOID tracks: `MikuMiku`, `The Disappearance of Hatsune Miku`,
`World's End Dancehall`, `Rolling Girl`, `Uraomote Lovers`,
`Unknown Mother-Goose`, `Redial`, `Tell Your World`, `Umiyuri`,
`Karakuri Pierrot`, `Dappo Rock`, `ECHO`, `Vampire`, and
`PaIII.SENSATION`. Audio is CBR 128 kbit/s MP3 and jackets are 128x128
RGBA4444. Only the selected song's resources are resident.
The song selector has a continuously scrolling public-test notice. Its build
identifier is compiled as `tsuki@kagebaito-yyyymmdd-hhmmss` by
`tools/build_psp.sh`; each release build therefore exposes its exact timestamp
on screen.
Song-select controls are:
- D-pad up/down: previous/next song;
- `L` / `R`: move five songs;
- `CIRCLE`: enter difficulty selection, then start the selected chart;
- D-pad in difficulty selection: choose an available mode;
- `CROSS`: back (or exit from the song list);
- `SELECT`: switch tate side.
The physical D-pad is rotated into the portrait coordinate system. In the
default clockwise orientation physical left/up/right/down becomes logical
down/left/up/right; counter-clockwise tate uses the inverse mapping.
Gameplay controls are:
- D-pad or `CROSS` / `CIRCLE` / `TRIANGLE` / `SQUARE`: tap;
- `SELECT`: switch tate side;
- `START`: open the pause menu;
- `L` / `R`: seek backward/forward five seconds;
- `START` + `SELECT`: exit (the PSP `HOME` menu also remains available).
The pause menu contains `CONTINUE`, `RESTART`, and `BACK TO MENU`; `CIRCLE`
confirms and `CROSS` cancels. At chart end the current song is unloaded and the
runtime returns to song select automatically. The Linux runtime performs the
same transition after the authored track duration and reopens its selector.
Every newly pressed D-pad or face-button bit is retained as an independent
input, so simultaneous buttons can judge CRITICAL and start DUAL HOLD.
Releases terminate HOLD/DUAL HOLD, while alternating/repeated presses keep
SCRATCH and BEAT active. The recovered `game471.exe` judge routine uses
25% / 50% / 100% of the full early or late input-window width for GREAT / COOL /
GOOD, with `GreatMinTime = 32` ms as the lower GREAT bound. Windows come from
the chart timing lists or the per-difficulty `system.cfg` override, including
the recovered FLICK extension. Long-note ranks use the original held-percentage
thresholds and short-tail correction. SLIDE HOLD and MERRY GO ROUND rendering
data is packaged, but their final input state machines remain unresolved in
the desktop reverse as well.
Notes fade in/out on authored beat-relative times and are submitted after the
rail without depth testing. HOLD/SCRATCH/BEAT/SLIDE/DUAL bodies and matching end
markers are generated as PSP GE geometry; directional arrows are fixed in
screen orientation using the camera at the note timestamp, and the control
helper stays in the lower-right HUD slot. The original note/effect atlases are
not yet converted for PSP, so these marks remain geometric rather than using
the arcade textures.
## Runtime package
```text
OpenRoller/
EBOOT.PBP
catalog.orpc
songs/
<song-id>/
easy.orps
normal.orps
hard.orps
extra.orps # when present
easy_bgm.mp3
normal_bgm.mp3
...
jacket.orpj
sounds/
adlib.pcm
tap1.pcm
tap2.pcm
```
`catalog.orpc` is a fixed-record index generated from the original
`stage_param.dat`; it binds display title, artist, BPM, duration, genre,
difficulty ratings and availability to a stable song key. The menu uses that
same key to resolve the chart package, per-difficulty mix and jacket paths.
+537
View File
@@ -0,0 +1,537 @@
# game471.exe: Notes (Static Analysis)
Goal: understand file formats and runtime behavior for a clean-room reimplementation.
This is based on static inspection (strings + disassembly). No patching, no runtime hooking.
## Quick Map
- Stage file path templates referenced in code:
- `data/stage/%s.dat`
- `data/stage/%s_ext.dat`
- `data/stage/%s_clip.dat`
- The game uses a background file reader (a small work queue + thread) that:
1. Opens a file path stored in a request object.
2. Reads the file into a buffer in 64 KiB chunks.
3. Calls a callback function provided when the queue was created.
## Background File Reader
### Worker thread proc
- `0x004ca320` is used as the thread entry function (passed as a function pointer).
- It receives a small argument struct, extracts two pointers from it, then calls `0x004ca070`.
### File read loop
- `0x004ca070` implements:
- open file (`CreateFileA`), then `ReadFile` in a loop
- destination buffer is `request->buf` at offset `+0x204`
- expected size is `request->size` at offset `+0x208`
At several points it calls a callback:
- `queue->callback_fn` stored at offset `queue + 0x120`
- `queue->callback_ctx` stored at offset `queue + 0x124` (passed as last argument)
### Queue initialization
- `0x004ca5f0` looks like the queue constructor/initializer.
- It stores its arguments into `this+0x120` and `this+0x124`.
So, to find the actual parsing logic for a specific file type, you typically follow the callback function pointer passed into the queue creation.
## Stage Loader Entry
There is code that formats the stage file path strings and attempts to open:
- `data/stage/<name>.dat`
- `data/stage/<name>_ext.dat`
- `data/stage/<name>_clip.dat`
This is a good anchor when looking for where the game requests reads for stage containers.
Static addresses in the 4.71 executable:
- `0x0063ea70`: formats all three stage paths and requests each resource.
- `0x00427730`: resource request wrapper.
- `0x004279d0`: creates the 0x4c-byte resource object (Ghidra mislabels it as an MFC `CreateObject`).
- `0x0063ecc0`: completion/cleanup callback which unwraps three resources before passing the result onward.
## Main stage header
The file is big-endian. The initial words are not all section offsets. Confirmed section slots are:
| Index | Meaning |
| ---: | --- |
| 0 | stage config |
| 1 | track draw distances |
| 2 | track points |
| 3 | notes |
| 4 | camera |
| 5 | particles |
| 6 | visualizer |
| 7 | unknown section |
| 8 | first color table |
| 9 | objects |
| 10 | scalar/unknown; often `0x30`, not an offset |
| 11 | second color table |
| 12 | scalar/unknown; can accidentally look like an in-file offset |
This matters because treating every plausible header word as an offset can split the notes or camera section at a false boundary.
Older 11-word stage headers instead store the second color-table offset in
slot 10. The scalar/offset/scalar arrangement in slots 10..12 belongs to the
newer 13-word format used by the 4.71-era charts.
## Note array: confirmed wire layout
The note section starts at header word 3 and ends exactly at header word 4. Its layout is:
```text
u32be name_count // observed: 0 in all 2,968 dumped main charts
repeat name_count times:
string8 name // u8 length followed by bytes
u32be count
repeat count times:
u32be time_ms
u8 raw_type
u8 type_override
s16be params16[9]
u8 flag24
f32be params25[3]
u8 flag37
u8 flag38
f32be params39[4]
u32be params55[3]
f32be param67
u32be param71
f32be params75[5]
u32be param95
```
Thus each note record is exactly 99 bytes and the invariant is:
```text
camera_offset - notes_offset == 8 + encoded_names_size + count * 99
```
The invariant holds on tested charts of very different sizes. For example,
`ac_10pt8tion_hard.dat` has 669 records from 6162 through 125568 ms.
Its raw-type histogram is `01:366, 02:231, 03:1, 04:3, 05:3, 09:63, 0f:2`.
Observed raw types over the parsed corpus are `00, 01, 02, 03, 04, 05, 06, 09, 0a, 0f`.
The payload field meanings remain unnamed until backed by code references or
controlled runtime observations.
The exact read order above comes from the game's stage constructor `0x005ed4c0`
and its note-section parser `0x005ea800`. The latter allocates a 0x11c-byte
runtime object for every compact 99-byte wire record. It also proves one behavior:
if `type_override` is non-zero, the runtime replaces `raw_type` with type 1.
The post-load pass at `0x005ebaa0` additionally proves that runtime types 11,
12, 13 and 14 are compatibility aliases: 11 becomes 10, 12/14 become 9, and
13 becomes 4. Other game modes can deliberately simplify types (for example
2 to 1, and 4/5/10/15 to 3), so a clean player should preserve both the raw
wire type and the effective runtime type.
Full-dump validation with `tools/scan_gc_notes.py GC/data/stage` covers 2,968
main stage files and 1,080,259 notes with zero size mismatches. All of those
files currently have zero note-name strings. The aggregate raw-type counts are:
```text
00:6 01:719892 02:163462 03:67753 04:8651
05:5115 06:193 09:102699 0a:6360 0f:6128
```
`type_override` is non-zero in 52,506 records, so it cannot be discarded.
## Confirmed type names
The executable contains a direct pointer table at `0x0077d480`; its array index
is the numeric note type. This yields the complete original enum:
| Value | Original name |
| ---: | --- |
| 0 | NONE |
| 1 | NORMAL |
| 2 | FLICK |
| 3 | HOLD |
| 4 | SCRATCH |
| 5 | BEAT |
| 6 | MERRY GO ROUND |
| 7 | HIDDEN |
| 8 | HIDDEN2 |
| 9 | CRITICAL |
| 10 | SLIDE HOLD |
| 11 | SLIDE COUNTER |
| 12 | TURN |
| 13 | SPIN |
| 14 | FINISH |
| 15 | DUAL HOLD |
The same 16-entry sequence is repeated in several embedded reflection/debug
tables, which independently confirms the ordering.
## Confirmed runtime classifications
With the exact names assigned, several behavioral groups are also explicit in code:
- `0x005e9480` classifies types 3, 4, 5, 10 and 15 as duration/path notes.
- `0x005ebaa0` generates sampled paths for those types. Type 4 additionally
builds two derived point arrays; types 3/10/15 share one path branch.
- `0x005e94c0` singles out type 6. During gameplay setup it expands that note
into several runtime judge objects using the runtime field originating from
wire offset `+71`.
- Types 1, 2 and 9 follow the non-duration branches in the preview/gameplay
render path at `0x006492f0`.
The enum names are now exact. The remaining reverse target is the meaning of
the compact payload fields and the type-specific hold/slide behavior.
## Appearance, duration and control-helper effects
The post-load pass at `0x005ebaa0` converts several compact fields using the
BPM active at the note timestamp. With `beat_ms = 60000 / bpm`:
| Wire field | Runtime meaning |
| ---: | --- |
| `+6` | signed marker-effect/UV selector; the note-head draw passes `value - 1` to effect 3 |
| `+39` | appearance lead in beats; runtime `+0xbc = max(time - value * beat_ms, 0)` |
| `+51` | duration in beats for types 3/4/5/10/15; runtime `+0x58` and `+0xac = time + value * beat_ms` |
| `+55` | packed authored `RRGGBBAA` colour used by duration-target geometry |
| `+71` | number of generated MERRY GO ROUND targets |
| `+75` | MERRY GO ROUND spacing in beats |
`0x005efe40` confirms that MERRY's span is
`count * spacing * beat_ms`. Gameplay setup allocates three visual handles per
generated target, which explains why treating type 6 as one marker loses most
of the authored pattern.
The four blocks following the BPM table in `StageConfig` are variable-length
timing lists, not four fixed records. Each list is `u16be count`, followed by
`count` entries of `u32be time_ms, u32be mode, f32be value`.
`TuneTimingData::GetTime` selects the last entry at or before the note: mode 1
returns `value` as milliseconds, mode 3 returns the spacing to the next note,
and the other modes return `value * beat_ms`. The third list supplies the late
boundary used as runtime `+0xac` for ordinary targets. FLICK (and raw type
`0x10`) receives an additional literal `0.2 * beat_ms`.
Marker opacity is independent of the fixed judgment-window overrides below.
`0x0063ff90` stores `beat_ms` at runtime `+0x20`; `0x0064ab80` divides it by
the literal `2.0`, while `0x005ebaa0` sets runtime `+0xc4` to
`+0xac + 4 * beat_ms`. With `start = +0xbc`, `finish = +0xc4`, and
`fade = beat_ms / 2`, the exact unclamped shape is:
```text
time < start + fade : alpha = (time - start) / fade
time <= finish - fade : alpha = 1
otherwise : alpha = (finish - time) / fade
```
The renderer clamps this to `[0, 1]`. Duration targets use their authored end
as `+0xac`; MERRY GO ROUND first adds its generated-target span. The Linux
player now carries the per-note BPM and `+0xc4` equivalent instead of using
the former guessed 236 ms marker lifetime.
The lower-right control helpers are not note-head effects. `0x00661680` reads the 16-entry
table at `0x006ea040`, loads the indicated `efcdata.dat` record, and stores it
in runtime slot `0xb1 + type`:
```text
type: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
effect id: -1 61 66 62 63 64 61 -1 67 65 69 -1 -1 -1 -1 68
```
Thus NONE and HIDDEN have no helper resource. FLICK and SLIDE HOLD also draw
runtime slot `0xb9` as a direction overlay in `0x006492f0`. The leading words
of effect records 61--69 give animation lengths of 16, 38, 34, 13, 16, 14,
14, 38 and 38 update ticks respectively.
The common-effect interpreter is now mapped far enough to reproduce those
records exactly. `0x005f2800` dispatches the child nodes, while
`0x005f2030`, `0x005f2250` and `0x005f23f0` implement looping, key lookup and
step/linear interpolation. Every child has five relative track offsets. A
track begins with `u16be key_count, loop_start, loop_end`, followed by keys of
`u16be time, u8 interpolation, f32be values[]`. For a sprite child (type 2),
the tracks are:
| Track | Values |
| ---: | --- |
| 0 | UV frame plus six unused/secondary values |
| 1 | XYZ position |
| 2 | alpha, red, green, blue |
| 3 | XY scale |
| 4 | rotation in degrees |
Type 0 is a parent rotation node in radians. A sprite whose child flag at
`+4` is set inherits that rotation, including the rotated XY offset. Effects
61--69 use UV record 13 from `uvdata.dat`, whose image is `img13.bin`: target
rings, balls, hand prompts, arrows, action bursts and the type-specific help
caption are separate animated layers. `0x006492f0` advances the records with
an integer 60 Hz tick, places the selected composite at the literal virtual-
screen coordinate `(580, 788, 0)` and applies the literal global XY scale
`1.3`. `0x006e9ee4` supplies a 1000 ms look-ahead, so only the current helper
is selected; the effect is not cloned onto every world-space note. The Linux
player implements this format
in `vectorail-gc/src/GcTargetEffect.cpp`; it does not substitute a
single guessed icon for the composite any more.
## Cross-version confirmation of note-head rendering
The Android ARM64 build preserves C++ symbols which are stripped from the
arcade executable. Its `GameScene::DrawGameStageCharacter` establishes the
same ordering seen in game471: it disables depth writes, calls `DrawWay`,
then calls `DrawMark` without disabling the depth test. The route therefore
cannot punch holes through marker sprites drawn afterwards.
`TuneGameData::BuildTimingDataSub` (`0x00196a74`) fixes the note head's world
position before gameplay by calling `GetWayPosition(note_time)` directly and
storing the returned XYZ at runtime `+0xe0`. Long-note endpoints likewise use
`GetWayPosition(end_time)` at `+0xf8`. There is no conversion through physical
track distance and no note-time lead added in this path. `DrawMark` consumes
these stored positions. The separate one-frame `-16.6667 ms` lookup in
`GameScene::SetCommonParam` is used for the moving player segment, not for note
placement.
The Linux player now performs the same direct timestamp lookup for heads,
endpoint heads, generated BEAT/MERRY targets, and approach circles. Its shared
physical-distance LUT is still needed for duration-body sampling; it now
samples each authored segment independently and includes every exact corner.
The previous cumulative `t += 0.02` loop could jump across a segment boundary,
measure a shortcut chord through the corner, and progressively place later
distance-based geometry ahead of the authored route position.
`GameScene::DrawMark` creates each head with effect manager group 1,
effect 3, and the final selector `wire_mark_effect_id - 1`. Group 1 is bound
by `GameScene::LoadSkinData` to the selected skin's `uv.dat` and its
texture list. The first signed word at stage-note wire offset `+6` contains
the selector source. Examples from `ac_10pt8tion_hard.dat` are:
| Note | Wire `+6` | Resolved UV record |
| --- | ---: | ---: |
| NORMAL | 1 | 1 |
| HOLD | 9 | 9 |
| FLICK | 10 | 10 |
| SCRATCH | 11 | 11 |
| BEAT | 12 | 12 |
| CRITICAL | 32 | 32 |
| DUAL HOLD | 37 | 37 |
Effect 3 has a 15-tick loop. Records 1--12 use 32x32 cells from the common
skin atlas, while records 32 and 37 use 64x64 cells from the second common
atlas. The effect instance uses the literal XY scale `0.025`; consequently
the authored 32px and 64px cells occupy 0.8 and 1.6 route-world units.
Directional heads have a second world-space layer. `DrawMark` plays effect
39 over FLICK with UV base 0 and over SLIDE HOLD with UV base 10. The final
root angle is not wire `+29` used directly: wire `+25` is a vector length and
`+29/+33` are HPB angles. `BuildTimingDataSub` converts these through
`RotateHPB::ToVector_Deg`; `GameScene::buildGameData` projects the route point
and the displaced point with the camera evaluated at the note timestamp, then
uses `RotateHPB::SetVector` (`atan2(screen_dx, screen_dy_down)`) and stores the
resulting screen angle. It is computed before gameplay and remains fixed while
the live camera moves. Effect 39 resolves to a 128x32 animated strip whose
arrows extend to the sides of the 32x32 head; it uses the same `0.025` scale,
colour and marker alpha. This is independent of the lower-right control
helper, even though that helper also has a direction overlay.
The marker animation clock is beat-synchronized, not a free-running 60 Hz
counter. Android `GameScene::SetCommonParam` computes
`phase = 1 - (((time - bpm_change_time) / beat_ms * 2000) % 2000) / 2000`
and `DrawMark` writes `phase * EffectData::GetFrameMax()` to effect field
`+8`. Thus effect 3 runs backwards from frame 15 toward frame 0 once per beat
and resets at every beat; BPM changes also reset the phase origin. BEAT adds
`sample_index * 2` to this base frame.
The approach ring is geometry, not another atlas layer.
`GameScene::drawHitTimingCircle` draws a white 16-segment circle with additive
blending, `radius = 0.2 + progress * 0.5`, and alpha `marker_alpha * 128/255`.
For ordinary notes `progress = (late_boundary - time) / (2 * (late_boundary - early_boundary))`,
capped at 1 and only shown before
the target time. The arcade MERRY branch instead evaluates the analogous
progress separately at each generated target's shifted timestamp.
Both that circle and the effect-3 head are camera-facing billboards. The
mobile renderer calls `SetTransposeMatrix(world_position, camera_matrix)` for
the circle and leaves the effect sprite in billboard mode; neither is laid in
the route tangent plane. Duration bodies remain authored 3D route geometry.
The Linux player now decodes this selector, interprets effect 3, loads
`skin/common/common.png`, `skin/common/common2.png`, the selected
`skinN/img.dat` list and `skinN/uv.dat`, and renders the selected animated
UV record. The former guessed `effect/game/img4.bin` glyph mapping and
synthetic pulsing rings are no longer used for decoded GC stages.
Duration bodies are also type-specific in the executable:
- HOLD calls `0x00647cf0` and SLIDE HOLD calls `0x00641fd0`; both emit ribbon
triangles rather than an OpenGL-style line.
- SCRATCH is sampled every `0.15` world units. `0x005ebaa0` derives two
opposing paths with radius `0.2` and rotates the offset by 45 degrees per
sample; `0x00641d50` emits six vertices per segment for each path.
- BEAT is sampled every `0.55` world units and `0x0064ab80` creates a separate
effect at every point. There is no continuous BEAT body. Its case-5 loop
draws only points whose sampled timestamp has not passed, and writes
`base_frame + sample_index * 2` into effect field `+8`. Android's symbolized
`EffectData` access confirms this field is the animation frame; the authored
UV-list selector remains unchanged.
- MERRY GO ROUND computes the `+0xbc…+0xc4` alpha once outside its generated-
target loop and multiplies every copied effect by that same parent alpha.
Individual offsets from `0x005efe40` change target time and position, not
the fade interval.
- DUAL HOLD uses `0x00646d70` to build two ribbon bands. Its literal inner and
outer offsets are `0.2` and `0.5`.
- Android `GameScene::drawLongShotEffect` (matching arcade `0x0064ab80`)
draws an effect-3 marker at the authored endpoint as well as the starting
marker. The fixed endpoint UV bases are 29 for HOLD, 30 for SCRATCH, 41 for
SLIDE HOLD, and 42 for DUAL HOLD. SLIDE HOLD additionally layers effect 39
with UV base 10 at the endpoint and rotates its root by the precomputed
screen direction. Both endpoint layers inherit the long note's colour, fade alpha,
billboard transform, beat-synchronised animation and `0.025` sprite scale.
## Tap judgment timing
The system-config loader at `0x00635c90` lays out the timing overrides in four
per-difficulty arrays. The stage loader applies them in `0x005ed4c0`, and the
post-load pass at `0x005ebaa0` derives these runtime note boundaries:
| Runtime field | Boundary | Easy override |
| ---: | --- | ---: |
| `+0x98` | note time minus `MissTimingOverride` | 236 ms |
| `+0x9c` | note time minus `UnmuteTimingOverride` | 202 ms |
| `+0xa0` | note time plus `LimitTimingOverride` | 202 ms |
| `+0xa4` | note time plus `MuteTimingOverride` | 0 ms |
`0x005d1690` is the tap-grade function. It selects `+0x9c` for an early tap or
`+0xa0` for a late tap and computes the absolute distance `W` from the note
time to that boundary. It then builds thresholds using the literal table
`{100000, 100, 50, 25}` at `0x006e8bc0`, divided by 100. The grade loop checks
indices 3 down to 1, which matches the adjacent original enum/string order
`MISS, GOOD, COOL, GREAT`.
For an error magnitude `e` and the selected outer input distance `W`, the
unclamped comparisons are therefore:
```text
GREAT: e < 0.25 * W
COOL: e < 0.50 * W
GOOD: e < 1.00 * W
```
`GreatMinTime` is copied into the gameplay object at `+0x104`; if the computed
GREAT threshold is below that value, it is clamped and COOL is redistributed
by the same routine to `great + (good - great) / 3`, capped at GOOD. With the
shipped Easy `W = 202`, the absolute-error thresholds are 50.5 ms for GREAT,
101 ms for COOL, and 202 ms for GOOD. HARD/EXTRA use `W = 134`, producing
33.5/67/134 ms. The shipped `GreatMinTime = 32` therefore does not clamp these
four standard difficulty windows, but the branch matters for custom configs.
`MissMarkOverride=1` in the shipped `data/system.cfg` makes the stage loader
replace the timing-list results with these four Simple/Normal/Hard/Extra arrays:
```text
MissTimingOverride = (236,202,168,168)
UnmuteTimingOverride = (202,168,134,134)
LimitTimingOverride = (202,168,134,134)
MuteTimingOverride = (0,0,0,0)
```
## SHOT miss gating
The two stage WAVs remain sample-synchronized, but they are not mixed at a
fixed level. `FUN_00640790` reads each player's `IsMute` state after the
judgment update. For player zero, `FUN_00611920`/`FUN_00611940` pass `1.0` or
`0.0` through `FUN_00611800` to the third volume multiplier of channel 13
(SHOT); channel 12 (BGM) is untouched. The write is immediate rather than a
timed SoundManager fade.
An unresolved target closes the SHOT gate at runtime `+0xa4`, which is note
time plus `MuteTimingOverride` (zero in the shipped configuration). The note
remains judgeable through `+0xa0`; a successful late judgment opens SHOT again
immediately. This is why a miss removes the chart-performance layer without
stopping or seeking either WAV.
## Gameplay tap and AD-LIB sound effects
The hit sounds are a separate three-WAV sound set; they are not baked into
either stage stem. `FUN_00613430` resolves the selected entry from
`data/boot/se.dat` and loads its three names from record fields `+0x14`,
`+0x18`, and `+0x1c`. The default `se0000` ("Ver.3 Set") maps them to:
| Slot | WAV | `SEList.csv` volume | Playback channels |
| ---: | --- | ---: | --- |
| 0 | `SE_ARRANGE.wav` | 87 | 6, 7 |
| 1 | `TAP_SE1.wav` | 80 | 10, 11 |
| 2 | `TAP_SE2.wav` | 77 | 8, 9 |
Every slot is deliberately double-buffered. `FUN_00611fc0` round-robins slot
0 over channels 6/7. `FUN_00611e70` round-robins the two booster sounds over
10/11 and 8/9. Consequently consecutive taps overlap instead of cutting off
the previous sample. Gameplay update `FUN_00640790` consumes the two booster
edge flags at player fields `+0xed/+0xee` and the separate AD-LIB flag at
`+0xaa`; the booster edge is what starts TP1/TP2, before the resulting note
rank is known. A mistimed press still produces its tap sound. The AD-LIB flag
selects slot 0 independently.
The other `se.dat` entries preserve the same three-slot layout, for example
`ALB_06/TP1_06/TP2_06` for the Scratch set. The chosen set id comes from the
player customization state (`Afx` state `+0xce0` in the gameplay loader), not
from the stage chart.
The Linux player currently loads the default Ver.3 set at the exact CSV gains
and reproduces the original two voices per slot. It decodes serialized marker
byte `+5` into the runtime `+8` AD-LIB flag and starts ALB after a successful
rank on such a marker. D-pad inputs are treated as booster 1 and face-button
inputs as booster 2; this also matches the PSP-style control grouping.
## Ranked hit effects
`setRankedEffect` records the rank and gameplay timestamp. The later
`DrawGameStageCharacter` pass selects group-1 effect `rank + 0x1c`, so the
effect IDs are 29 MISS, 30 GOOD, 31 COOL, and 32 GREAT. Their decoded lifetimes
are respectively 20, 12, 12, and 12 frames. The effect advances from the hit
timestamp at 60 Hz, uses additive blending, a global XY scale of `0.03`, a
camera-facing matrix, and a stored random root rotation from `rand() % 360`.
## Hit-type dispatch and long-shot ranks
The arcade dispatcher at `0x005d5fb0` selects the handler from the effective
marker type: 1 TAP, 2 FLICK, 3 HOLD, 4 SCRATCH, 5 BEAT, 6 MERRY, 9 CRITICAL,
10 SLIDE HOLD, and 15 DUAL HOLD. Android's symbolized
`GameScene::checkHitMark` independently confirms the same mapping and names
type 9 `checkHitMarkDualTap`.
Dual tap stores the timestamp of its first independent input, consumes a
second independent input, then grades using the first timestamp. DUAL HOLD
also waits for two independent inputs, but begins its measured hold duration
when the second arrives; releasing either side ends it.
Arcade `FUN_005d0d80` grades HOLD and DUAL HOLD by the percentage of the
authored body actually covered after clamping press/release to its start/end:
```text
GREAT: held > 80%
COOL: held > 60%
GOOD: held > 40%
MISS: otherwise
```
The comparisons are strict. Before computing the percentage it also applies
the original short-tail compensation: if 20% of the body is below
`66.666664 ms`, that difference is added to covered time. `FUN_005d0eb0`
finalizes a still-pressed hold at the authored end; an early release grades it
immediately.
BEAT (`FUN_005d41b0`) uses the same long-shot percentage grader, but records
the first tap and continually replaces the covered endpoint with each later
tap. If no fresh trigger arrives for `BeatEnableTime` (200 ms in the shipped
config), it finalizes at the last trigger. Thus it measures how far through the
body the repeated taps continue, rather than merely counting taps.
SCRATCH (`FUN_005d44f0`) listens to four directional triggers. The first one
starts coverage; each later trigger extends it only when its direction differs
from the previous accepted direction. It finalizes after `ScratchEnableTime`
(250 ms) without a direction change or at the body end, then uses the same
40/60/80-percent long-shot grader.
+374
View File
@@ -0,0 +1,374 @@
# GC Camera Reverse Notes
Status: work in progress. This file records facts extracted from `game471.exe`
and `docs/stage.pat` so the Vectorail camera port does not keep accumulating
guesswork.
## Stage Camera Records
`docs/stage.pat` defines a 59-byte camera key:
```c
struct Camera {
u32 timeMs;
u8 aMode;
u8 fMode;
float dist;
float rotationA[2];
float originOff[3];
u8 projType;
float fieldFar[3];
float fieldNear[3];
float rotationB[1];
};
```
Observed across stage files:
- `fieldFar[]` and `fieldNear[]` are almost always zero.
- Therefore `fieldNear[0]` is not the gameplay FOV. The current Vectorail GC
loader uses it as `fov`; that is almost certainly wrong.
- Useful camera fields are `dist`, `rotationA[0]`, `rotationA[1]`,
`originOff[0..2]`, `rotationB`, and the mode bytes.
- Common defaults seen in real stages:
- `projType` is usually `1`.
- `fMode` is usually `1` or `2`, with some `0`.
- `aMode` is usually `0`, but `1..6` also appear.
Example from `ac_10pt8tion_hard.dat`:
```text
time=0 aMode=0 fMode=0 dist=22 rotA=(0,90) originOff=(0,0,9) projType=1 rotB=0
```
## Runtime Matrix Path
The apparent gameplay matrix slots in the tune object are not where the camera
is calculated:
- `tune + 0x150`: saved D3D `VIEW` matrix.
- `tune + 0x110`: saved D3D `PROJECTION` matrix.
- `tune + 0x0d0`: alternate saved D3D `VIEW` matrix.
- `tune + 0x090`: alternate saved D3D `PROJECTION` matrix.
Functions confirmed:
- `FUN_006449f0` calls `GetTransform(2, tune+0x150)` and
`GetTransform(3, tune+0x110)`.
- `FUN_00645e00` does the same after setting a 2D ortho projection.
- `FUN_00648d40` also snapshots `VIEW/PROJECTION` at render start.
- `FUN_0064da90` snapshots `VIEW/PROJECTION` into `+0xd0/+0x90`.
So these offsets are consumers/snapshots, not the camera mechanics.
The D3D device calls use the normal Direct3D 9 transform ids:
```text
0xb0 on IDirect3DDevice9 vtable = SetTransform
+0xe4 on IDirect3DDevice9 vtable = GetRenderState
+0xb4 on IDirect3DDevice9 vtable = GetTransform
2 = D3DTS_VIEW
3 = D3DTS_PROJECTION
0x100 = D3DTS_WORLD
```
`FUN_004e72c0` / `FUN_004e5af0` are a central D3D state cache, not camera
math. The cache fields are:
```text
+0x3a8 cached WORLD
+0x3e8 cached VIEW
+0x428 cached PROJECTION
+0x390 cached viewport
```
Wrapper setters identified:
```text
FUN_004e3c10 = set view matrix, copies 16 floats to +0x310 and calls SetTransform(2)
FUN_004e3bc0 = set projection matrix, copies 16 floats to +0x350 and calls SetTransform(3)
FUN_004e3c60 = copy 16-float matrix to +0x180, no direct D3D SetTransform
```
These are useful for tracing the render path, but current xrefs show most
gameplay code uses the global D3D device helpers and snapshots rather than
directly calling these wrappers.
## Common 3D Camera
`CCommon3DCamera` is identified around `0063d820..0063d9d0`.
Struct layout:
```text
+0x04 eye vec3
+0x10 target vec3
+0x1c up vec3
+0x28 base fov, radians
+0x2c camera mode
```
Projection:
```c
D3DXMatrixPerspectiveFovLH(
out,
camera->fovRad * fovMul,
viewportWidth / viewportHeight,
nearZ,
farZ);
```
Important difference from Vectorail: the original uses the runtime viewport
aspect, not a hardcoded `1280.0 / 720.0`.
View:
```c
D3DXMatrixLookAtLH(out, eye, target, up);
```
when camera mode is `0`.
Known refs:
```text
FUN_0063d820 projection, caller FUN_00577840
FUN_0063d9d0 view/look-at, caller FUN_00577840
FUN_0063d8a0 constructor, caller FUN_00578570
```
Only one `D3DXMatrixPerspectiveFovLH` call is present in the binary, and it is
in `FUN_0063d820`. Only two `D3DXMatrixLookAtLH` calls are present:
`FUN_0063d9d0` and `FUN_0065b1f0`. This makes `CCommon3DCamera` the strongest
candidate for the gameplay camera path, but its stage-game caller has not been
found yet.
## Stage Object Transform Detour (Corrected)
`FUN_00643570` is not a camera or stage-object evaluator. It belongs to the
temporary rail-strip construction path. The actual scene-object loop is
`FUN_006445b0`; it iterates `0xdc`-byte runtime objects from
`stageResource + 0x140` and calls `FUN_00643b20` for their transforms and
`FUN_00643fd0` for their colors.
Inside `FUN_00643b20`:
```text
+0xa4/+0xa8/+0xac count/times/vec3 track-like stream
+0xb0/+0xb4/+0xb8 count/times/vec3 stream
+0xbc/+0xc0/+0xc4 count/times/vec3 stream
```
It samples every keyframe array through `FUN_005e9100`. Movement, scale, and
rotation are linearly interpolated vec3 values. The final Euler rotation is
then converted to a quaternion using the engine's `(-Y, X, Z)` convention and
converted to a matrix; there is no quaternion slerp. Base and
animated translations, rotations and scales are separate matrices which are
multiplied in this exact sequence:
```text
T(base) * T(move) * R(base) * R(animated) * S(base) * S(animated)
```
The color evaluator selects a keyed RGBA value in
place of the base color and linearly interpolates it when enabled.
The recovered helper operations include:
```text
FUN_005df6a0 = vec3 subtract
FUN_005df660 = vec3 scale
FUN_005df6f0 = vec3 add
```
and builds matrices through the local matrix stack:
```text
FUN_005e0650 = translation matrix
FUN_005e0610 = scale matrix
FUN_005e0330 = Euler-degrees-to-rotation-matrix entry
FUN_005e0220 = quaternion from the (-Y, X, Z) half angles
FUN_005df790 = quaternion-to-column-major-matrix
FUN_005e0680 = in-place right matrix multiply
```
The first boolean in transform/color key records becomes runtime metadata byte
`+5` and identifies repeating key ranges. The second becomes byte `+4` and
enables interpolation from the active key to the next. Visibility uses a
separate fixed fade window: `_DAT_006fcae8` is `250.0f` milliseconds.
The renderer also checks runtime object `+0xd4`, populated from the newer stage
supplemental parent-index stream. When present, it evaluates the parent and
child independently, multiplies their matrices, and multiplies their RGBA via
`FUN_0043a540`. It does not recursively walk more than one parent level.
## Stage Camera Evaluator (Confirmed)
`FUN_005e9e20(stage, result, timeMs, interpolate)` is the stage-camera
evaluator. The output is 44 bytes:
```text
+0x00 eye vec3
+0x0c target vec3
+0x18 up vec3
+0x24 projType byte
+0x28 projBlend float (0 or 1 outside an fMode=1 transition)
```
`FUN_005e0ca0` passes those first three vectors to `FUN_005e0800`, the game's
look-at matrix builder. This proves the vector order and bypasses the earlier
`CCommon3DCamera` false lead.
The 59-byte wire key becomes this 0x44-byte runtime key:
```text
+0x00 aMode (int)
+0x04 fMode (int)
+0x08 dist
+0x0c rotationA.x
+0x10 rotationA.y
+0x14 rotationA.z = 0
+0x18 originOff vec3
+0x24 projType byte
+0x28 fieldFar vec3
+0x34 fieldNear vec3
+0x40 rotationB
```
`FUN_005ed4c0` reads every serialized field directly and does not reject
non-finite floats. This matters for `ac_comet_{easy,normal,hard}.dat`: their
first key at 0 ms intentionally has NaN in both `rotationA` components, and the
next key starts at 1316 ms. The Linux loader must retain that first key; dropping
it activates the second camera too early and changes the intro.
Let `T(t)` be the linearly interpolated track position and `R` be the orbit
vector calculated from `rotationA` and `dist`. The seven `aMode` branches are:
```text
aMode 0: target = T(t) + originOff; eye = target + R
aMode 1: target = T(keyTime) + originOff; eye = target + R
aMode 2: for camera index > 1, return camera(keyTime - 1ms, no interpolation);
otherwise the same as mode 0
aMode 3: target = T(t) + originOff;
eye = T(keyTime) + originOff + R
aMode 4: target = fieldFar; eye = fieldNear
aMode 5: target = T(t) + originOff; eye = fieldNear
aMode 6: target = fieldFar; eye = T(t) + R
```
This confirms that `originOff` is a world-space addition, not a rail-local
offset.
### Orbit axis convention
`FUN_005dfaa0` creates a quaternion from the Euler tuple
`(-rotationA.y, rotationA.x, rotationA.z)` in degrees. `FUN_005e01d0` then
transforms `(0, 0, dist)` by its matrix. In particular, the first 10pt8tion
key `rotationA=(0,90), dist=22` produces `R=(0,22,0)`: the camera is directly
above the rail, not behind it.
### Interpolation modes
Interpolation only happens strictly between the active key and the following
key:
```text
fMode 0: evaluate the active key directly
fMode 1: evaluate camera states at both key timestamps, remove endpoint roll,
linearly interpolate eye/target/up, normalize up, then apply the
linearly interpolated rotationB
fMode 2: linearly interpolate all raw float/vector fields, retain the active
key's aMode/fMode/projType, then evaluate that mixed key
```
There is no shortest-angle interpolation in `fMode=2`; the rotation floats are
mixed as ordinary scalars.
### Up vector and roll
`FUN_005e0ad0` rebuilds `up` by projecting world-up `(0,1,0)` onto the plane
normal to `target-eye`. If the two directions are collinear it falls back to
`(0,0,1)`. `rotationB` then rotates that up vector around the normalized view
axis.
### Gameplay projection
`FUN_0063fd60(cameraState, fovMul)` consumes the complete 44-byte result of
the stage-camera evaluator. It selects or blends two projections:
```text
projBlend <= 0: orthographic
projBlend >= 1: perspective
0 < projBlend < 1:
ortho + (perspective - ortho) * clamp(projBlend^3, 0, 1)
```
The perspective matrix uses vertical FOV `75 * fovMul`, the live viewport
aspect, near `1` and far `1000`. The orthographic half-height is
`distance(eye,target) * tan(FOV/2)` and its half-width is that value times the
viewport aspect. Consequently objects on the target plane keep the same
screen size while the projection changes, but depth shrinking disappears in
orthographic sections.
The distance is not clamped to the near plane. A zero-length camera therefore
also produces zero orthographic extents in the original. Likewise, its vector
normalizer returns `(0,0,0)` for a zero-length input instead of propagating a
NaN as `glm::normalize` does. The Linux port mirrors both edge cases and uses
explicit left-handed view/projection builders; the `NO` depth variant is the
OpenGL backend adaptation of the original D3D left-handed matrices.
The evaluator sets `projBlend` to `0` for `projType=0` and `1` for
`projType=1`. An `fMode=1` transition linearly interpolates the endpoint
blend values before the projection builder applies the cubic curve.
The gameplay owner initializes its `fovMul` field at `+0x238` to `1.0` and the
normal update path passes it unchanged to `FUN_0063fd60`; therefore the normal
stage camera uses a 75-degree vertical FOV. This scalar is runtime state rather
than part of the 59-byte camera record. The same is true of viewport aspect and
the fixed near/far planes.
### Gameplay item projection modifiers
The value checked at global gameplay-state offset `+0xcd8` is the selected item
ID from `data/boot/item.dat`, not a camera or screen-mode enum:
- `4`, `MIRROR`: flip the stage horizontally;
- `6`, `REVERSE`: flip the stage horizontally and vertically.
At the end of `FUN_0063ff90`, both items negate the first column of the 3D
projection at gameplay-camera offset `+0x90` and the 2D orthographic projection
at `+0x110`. `REVERSE` also negates their second columns. The view matrix at
`+0xd0` is not changed. Course geometry switches from `D3DCULL_CCW` to
`D3DCULL_CW` only for `MIRROR`; the two-axis `REVERSE` transform preserves
triangle winding.
## Vectorail Port Status
The player now imports the raw camera keys (including non-finite sentinel
values) and ports the confirmed `aMode`,
`fMode`, orbit, up/roll, projection type/blend, FOV and clipping-plane
behavior. GC cameras are evaluated directly without the legacy follow-camera
smoothing. Remaining camera work is validation against captured original
frames.
## Switch and Android cross-version validation
The base Switch executable retains `CTuneGameData` RTTI and the original GC
source filenames. Its stripped `CTuneGameData::GetWayPosition` and
`CTuneGameData::GetCameraData` implementations were matched to the named
Android ARM64 functions by class layout, control flow, and camera-mode
behavior.
Switch stores camera timestamps separately from a 0x44-byte runtime payload,
where Android uses an interleaved 0x48-byte record. The evaluator itself still
implements the same `aMode` branches `0..6`, `fMode` 1/2 interpolation, up and
roll construction, projection type/blend, and the mode-2 one-millisecond
hold. This provides an independent confirmation of the arcade reconstruction
above.
FOV is platform policy rather than a stage field. Android chooses between
`60.0`, `68.5`, and `75.0` degrees for its 3:2, 16:9, and iPhone-X layout
modes. That mobile-only aspect switch does not override the arcade
executable's confirmed 75-degree gameplay FOV used by the Linux arcade target.
See `re_gc_switch.md` for the Switch addresses and asset inventory.
+107
View File
@@ -0,0 +1,107 @@
# GC 4.71 song catalog reverse notes
The Windows build loads its master song catalog from
`data/boot/stage_param.dat`. `LevelList.dat` is a different fixed-record table:
its 9002 bytes are a big-endian `u16` count of 1000 followed by 1000 records of
9 bytes, and it is not the song/asset relation table.
## Confirmed loader
`FUN_005e66a0` opens `data/boot/stage_param.dat`. It reads a big-endian `u16`
record count, allocates `count * 0xb4` bytes, and deserializes every variable-size
disk record into one `0xb4`-byte runtime entry. The dumped file contains 924
records. IDs are explicit and can have gaps; they are not array indices.
The game's primitives used by this loader are:
- `FUN_005d8bc0`: big-endian `u32`;
- `FUN_005d8cc0`: `u8`;
- `FUN_005d97d0`: `u8 byteLength` followed by that many string bytes;
- `FUN_005d96d0`: CP932-to-current-Windows-codepage conversion, used for display
strings but not asset identifiers.
The recovered runtime entry is:
```text
offset type confirmed/current meaning
0x00 u32 numeric song ID
0x04 string* display title
0x08 string* image/asset key
0x0c string* artist
0x10 string* source/subtitle
0x14 string* normalized sort key
0x18 u8 genre ID (1 anime, 2 Vocaloid, 3 rhythm game, 4 game,
5 variety, 6 original, 7 Touhou)
0x1c string* duration, e.g. "2:03"
0x20 u8[4] EASY/NORMAL/HARD/EXTRA ratings
0x30 string* BPM text
0x34 u8[4] per-difficulty BGM volume percentage
0x44 u8[4] per-difficulty SHOT volume percentage
0x54 u32[3] timing values (exact roles not yet named)
0x60 u8[2] not yet named
0x68 string* BGM base name
0x6c string*[4] alternate chart group (mostly empty in current songs)
0x7c string*[4] auxiliary chart suffixes
0x8c string*[4] EASY/NORMAL/HARD/EXTRA chart IDs
0x9c string* not yet named
0xa0 u32 not yet named
0xa4 u8[2] not yet named
0xac string* not yet named
0xb0 u8 not yet named
```
The clean-room implementation is in `src/gc/StageCatalog.cpp`; the old
nearby-string search used by `--track-info` has been replaced by this exact
record parser.
## Asset relation
For `Oshama Scramble!`, the catalog record contains:
```text
title Oshama Scramble!
image key oshama
artist t+pazolite
ratings 1, 7, 13, 0
BGM base bgm_b-879_oshama
charts ac_oshama_easy
ac_oshama_normal
ac_oshama_hard
```
The executable derives paths rather than storing full paths in the catalog:
```text
chart ID -> data/stage/<chart ID>.dat
data/stage/<chart ID>_ext.dat
data/stage/<chart ID>_clip.dat
BGM base -> data/stage/sound/<BGM base><BGM difficulty suffix>_BGM.wav
data/stage/sound/<BGM base><SHOT difficulty suffix>_SHOT.wav
data/stage/sound/<BGM base>_VIB.csv
image key -> data/stage/2d/<image key>_menu.dds
data/stage/2d/<image key>_start.dds
```
`FUN_0063ea70` confirms the three chart formats. `FUN_005b3980` and
`FUN_005b3850` build the `_menu.dds` path from runtime offset `+0x08`.
For English UI they first try `data/stage/2d/eng/<key>_menu.dds` and fall back to
the non-language directory. `_start.dds` follows the same rule.
`FUN_00613710` prefixes stage audio with `data/stage/sound/` and appends the
suffixes. If the stage-specific BGM cannot be found, it also has a legacy
fallback under `data/sound/`.
The stage `.dat` then supplies the playable geometry, notes, camera, authored
background color table, particles, visualizer data and object/model scene. The
`*_menu.dds` image is selection/game UI artwork, not the gameplay background.
## Inspecting a record
```sh
./build/openroller GC/data/boot/stage_param.dat --track-info ac_oshama_hard
```
This prints metadata, all difficulty chart IDs, and every derived stage, sound,
menu and start-image path with a missing-file marker where applicable.
+224
View File
@@ -0,0 +1,224 @@
# 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.
+61
View File
@@ -0,0 +1,61 @@
# GC-style song selection
Running the stage player without a stage path opens the graphical song
selector backed by `data/boot/stage_param.dat`:
```sh
./tools/run_stage_player.sh
```
Passing a stage remains the direct reverse/debug path:
```sh
./tools/run_stage_player.sh GC/data/stage/ac_oshama_hard.dat
```
The selector renders the original English `selectmusic2` and `selectmode2`
RVB/MTX scenes. Exact task flow, coordinates, animation bindings, and the
container reverse are recorded in `docs/re_gc_menu_exe.md` from
`game471.exe` itself.
What is already wired:
- the original portrait 720x1280 `SELECT MUSIC` and `SELECT MODE` scenes;
- original MovieClip frame labels, display-list depth updates, transforms,
alpha fades, and RGB color transforms;
- the original `common_eng` controller HUD and animated Yume navigator scenes;
- the executable's purple-to-pink background strip, rotating wire sphere,
translucent menu geometry, and `balloon.dds` lower backing layer;
- genre filtering from the catalog genre byte;
- the twelve dynamically linked `mc_music_link`/`mc_index_link` carousel
slots with their executable positions and opacity table, including original
`s_j_eng.dds` genre headings as real scrolling entries;
- title rows, jacket, source and artist cut directly from each original
`data/stage/2d/<imageKey>_menu.dds` atlas;
- SIMPLE/NORMAL/HARD/EXTRA availability and selected-state MovieClips from the
same catalog;
- separate select-music and difficulty states, matching the task transition
proven in the executable;
- launch through the selected difficulty's exact `ac_*` stage ID.
Entries are included only when both their menu atlas and at least one local
stage `.dat` exist. This dump currently yields 887 playable songs from the 924
master catalog records. Only the visible carousel window is uploaded to the
GPU, rather than all jackets at once.
Controls:
```text
Up / Down, W / S previous / next song
PageUp / PageDown jump by eight songs
Q / E or Tab genre
Enter or Space enter difficulty screen
Escape close selector
Difficulty state:
```text
Arrows, W / S, A / D difficulty
Enter or Space play
Escape back to music selection
```
+154
View File
@@ -0,0 +1,154 @@
# Groove Coaster Wai Wai Party Switch reverse notes
Status: work in progress. These notes describe the base title
`0100EB500D92E000`; update and DLC content have not yet been merged into the
analysis corpus.
## Executable and engine lineage
The program NCA contains an AArch64 NSO (`main`). Converting it to ELF and
importing it into Ghidra exposes a stripped executable with relocations, RTTI,
and exception unwind data.
This is the same custom GC engine lineage as the arcade and Android builds,
not a Unity rewrite. Direct evidence in `main` includes:
- RTTI name `13CTuneGameData`;
- assertion/source paths under
`D:/project/GC/svn/latest/Program/Main/Src/GCMain/Tune/Functions/`;
- `TuneGameData.cpp`, `TuneGameManager.cpp`, and
`TuneGameManager_Draw.cpp` source names;
- the assertion label `LoadStageData`;
- stage paths `stage/data_gz/%s.dat.gz`, `%s_ext.dat.gz`, and
`%s_clip.dat.gz`.
Initial function mapping in the base Switch executable (Ghidra image
addresses):
```text
001456a0 CTuneGameData constructor
001457f0 CTuneGameData destructor
00145f60 CTuneGameData deleting destructor
0014a3d0 CTuneGameData::GetWayPosition
0014a560 CTuneGameData::GetCameraData
00134af0 asynchronous stage .dat/.ext/.clip loader
00155b50 TuneGameManager::LoadStageData owner/assert site
```
The names after the destructors are cross-matched against the symbol-bearing
Android ARM64 build and then checked from their decompiled behavior.
## RomFS inventory
The base RomFS contains 8,976 files (about 1.6 GiB). Important families are:
```text
3421 .tumo scene models
2136 .gz compressed stage data
1790 .opus audio, normally named *.wav.opus
1052 .bntx Switch textures
104 .tusc
88 .bnvib
85 .dat boot/catalog tables
69 .efcb2
65 .uvb
57 .rvb
57 .mtx
19 .bnsh shaders
```
Gameplay audio names explicitly distinguish tap, slide-hold, scratch,
critical, beat, and adlib hit effects. This makes the Switch assets useful for
validating the note-type-to-sound mapping even where the arcade catalog uses
numeric IDs.
## Stage data compatibility
Each chart still consists of the familiar three files, now gzip-compressed:
```text
stage/data_gz/<chart>.dat.gz
stage/data_gz/<chart>_ext.dat.gz
stage/data_gz/<chart>_clip.dat.gz
```
After gzip decompression the normal `.dat` is accepted directly by
OpenRoller's arcade `StageDat` and `StagePattern` parsers. For example,
`sw_adr_hard_1.dat` decodes as:
```text
track points: 240
notes: 529
camera keys: 67
draw-distance keys: 21
background models: 8
background objects: 199
```
The parser also recovers all expected note types, authored camera modes,
particle/visualizer keys, colors, and object animation. This is strong
structural confirmation that the arcade field interpretations are not merely
heuristics.
The Switch `_clip.dat` can be much larger than the main chart because it
contains baked per-frame object visibility/animation data, just like the
arcade clip stream.
All 712 main chart files in the extracted base-game RomFS parse successfully
with the current OpenRoller stage parser. A chart can be staged and launched
directly by ID:
```sh
tools/run_switch_stage.sh /path/to/decoded/romfs sw_adr_hard_1
```
List the available chart IDs with:
```sh
tools/run_switch_stage.sh /path/to/decoded/romfs --list
```
The launcher decompresses the chart, extension, and clip streams into
`build/nsw_runtime`, links the common model directory, converts the matching
Nintendo Switch OPUS stream with `vgmstream-cli`, and starts the Linux player.
Set `VGMSTREAM_CLI=/path/to/vgmstream-cli` if it is not on `PATH` or in the
repository build directory.
This currently provides chart, camera, note, clip, model geometry, and BGM
loading. The `.bntx` texture/material pipeline and the newer Switch song
catalog are not implemented yet, so a successfully running stage is not yet a
pixel-identical Switch presentation and there is no Switch-native song menu.
The BGM resolver finds a shipped audio stream for 711 of the 712 chart files,
including charts whose internal authoring-time BGM name differs from the
release filename. The sole exception is `sw_shoukon_hard_1`: it is absent from
the base `stage_param.dat` catalog and the base RomFS contains no matching BGM,
so this orphan chart can only be launched silently from that data set.
`boot/stage_param.dat` is a newer catalog revision. The current arcade catalog
parser does not consume it completely and must not be treated as compatible
until its extra fields are mapped from the Switch loader.
## Camera cross-check
The Switch `CTuneGameData::GetCameraData` stores key timestamps in a separate
array and uses a 0x44-byte runtime camera payload rather than Android's
interleaved 0x48-byte record. Despite that storage change, its behavior is the
same:
- the active camera key is selected by timestamp;
- `fMode` 1 evaluates both endpoint cameras, rebuilds endpoint up vectors,
then interpolates the complete camera states;
- `fMode` 2 interpolates raw key fields before evaluation;
- `aMode` has the same seven branches `0..6`;
- projection type and projection blend are separate results;
- the mode-2 hold branch samples the state one millisecond before the key.
This independently validates the camera evaluator currently documented in
`re_gc_camera.md` and implemented in the Linux player.
The projection/FOV policy remains platform-specific. The Android build calls
`SwitchAspectValue(60.0, 68.5, 75.0)` and selects the value for its 3:2,
16:9, or iPhone-X layout. The arcade executable instead establishes a normal
75-degree gameplay FOV. Therefore Android's aspect presets must not replace
the arcade target behavior in the Linux player.
+310
View File
@@ -0,0 +1,310 @@
# game471 test mode
This note describes the operator/test mode in the arcade `game471.exe`
(4.74.00ENG). The structural and layout observations below come from the
executable and its shipped resources, not from screenshots.
## Live entry
The game exposes twenty logical cabinet inputs through `FUN_00634060`
(`0x00634060`). Logical input 0 is the TEST switch.
During normal operation, the main task at `FUN_006396f0` samples input 0 on
every update. Holding it for at least three updates starts the test-mode
transition. The game then:
1. stops the normal game/audio/render tasks;
2. waits 45 updates;
3. constructs the test-mode text renderer, sprite renderer, input adapter,
SE adapter, and BGM adapter;
4. initializes the test-mode form system and opens the main form.
The already generated `game471_bootskip.exe` can reach the original menu under
Wine. Once the `GameWare` window is active, press and briefly hold Caps Lock.
Caps Lock is the executable's built-in DirectInput fallback for the TEST
switch; no test-mode-specific patch is needed.
The original executable's keyboard fallback table starts at `0x007840f0`:
| Logical input | Cabinet control | FAST I/O mask | DirectInput fallback |
| --- | --- | ---: | --- |
| 0 | Test switch | `0x00000040` | `DIK_CAPITAL` (Caps Lock) |
| 1 | Service switch | `0x00000001` | `DIK_F1` |
| 2 | Coin switch | `0x00000004` | `DIK_F2` |
| 3 | Select switch | `0x00000010` | `DIK_F3` |
| 4 | Enter switch | `0x00000020` | `DIK_RBRACKET` (`]`) |
| 5 | Left booster up | `0x00000100` | `DIK_Q` |
| 6 | Left booster down | `0x00000200` | `DIK_A` |
| 7 | Left booster left | `0x00000400` | `DIK_LCONTROL` |
| 8 | Left booster right | `0x00000800` | `DIK_S` |
| 9 | Left booster button | `0x00100000` | `DIK_LMENU` (left Alt) |
| 10..14 | Right booster controls | `0x00010000` through `0x00200000` | raw scan codes `78,7d,7a,7b,6a` |
`FUN_00633d00` obtains the FAST I/O bitfield and masks it with the first table
above. `FUN_00633de0` applies the per-input active-high/active-low table.
`FUN_00634060` ORs that result with the DirectInput fallback.
## Test-mode input adapter
`GWTestModeInput_GW` is constructed by `FUN_00569d00`. Its update method,
`FUN_00569d20`, translates cabinet inputs to the generic test-mode flags:
- Test switch -> `0x20` (back/exit)
- Select switch -> `0x02`
- Enter switch -> `0x58`
- left booster up/down/left/right/button -> `0x01`, `0x02`, `0x84`, `0x48`,
`0x10`
The language table describes the intended behavior:
- booster up/down or Select moves the cursor;
- booster buttons or Enter confirms;
- booster left/right or Enter changes a setting;
- Test returns/backtracks.
Short synthetic X11 key taps may be sampled for several game updates because
the original runs uncapped. For automated navigation, inject an explicit
cabinet state for one update instead of relying on `xdotool key`.
## Resources
Test mode does not use the normal RVB/MTX menu assets. Its strings come from:
- `data/TestModeLaungage/Laungage_eng_sjis.csv`
- `data/TestModeLaungage/Laungage_jpn_sjis.csv`
Both are CP932/Shift-JIS. `Laungage` is the spelling used by the shipped game.
The glyphs themselves come from `data/font/Font.mtf` and the
`data/font/FontXXXXXXXX.mfi` atlas pages. `GWTestModeRenderText_GW`
constructs a pool of 256 regular GameWare text objects backed by that font.
It does not use a test-mode-specific bitmap font or GDI text.
`Font.mtf` starts with a 16-way Unicode radix table. A resolved entry stores
the MFI page in its low 24 bits and the high nibble of the atlas cell in bits
28..31; the low cell nibble comes from the Unicode codepoint. Each MFI contains
a 512x512 DXT5 atlas split into 16x16 cells. This is confirmed by
`FUN_00485f90`, which creates GameWare image format 3, and by
`GWPCImage2D::Create`, whose format table maps index 3 to `D3DFMT_DXT5`.
The font shader uses the DXT5 alpha channel as glyph coverage. The English
ASCII glyphs use page 0 and occupy the left half of each cell, producing 8x16
glyphs. The selection marker is U+2192 (`→`), resolved by the MTF to page 27,
cell `0xb2`, and is 16x16.
The renderer is a separate, mostly text-based framework represented by the
following RTTI classes:
- `GWTestModeWindowText`, `GWTestModeWindow`, `GWTestModeWindowList`
- `GWTestModeForm`, `GWTestModeSelectForm`
- `GWTestModeForm_YesNo`, `GWTestModeForm_ProcYesNo`
- `GWTestModeInput_GW`
- `GWTestModeRenderText_GW`, `GWTestModeRenderSprite_GW`
- `GWTestModeBGM_GW`, `GWTestModeSE_GW`
The framework screen is 720x1280 on black. `FUN_00577940` installs a logical
text size of 16x16, pure red `(1,0,0,1)` for the selected/help color, and pure
cyan `(0,1,1,1)` for normal selectable entries. The three centered common
title windows use normalized y coordinates `-0.95`, `-0.925`, and `-0.9`,
which map to pixel y positions 32, 48, and 64.
The main list is anchored at normalized y `-0.725` (176 px). Its default row
gap is `0.01` of the 640-pixel half-height, so rows advance by
`16 + 6.4 = 22.4` pixels. The selected row is red and has a separately drawn
`→`; unselected rows are cyan. The help form is centered at normalized
y `0.2` (768 px), while the main five-line build/machine/time information list
is centered at y `0.4` (896 px).
## Main menu in 4.74.00ENG
The live build displays:
1. Monitor Test
2. Input/Output Test
3. LED Test
4. Card Test
5. Audio Settings
6. Game Settings
7. Network Info
8. Bookkeeping
9. System Info
10. Restore factory settings
11. Exit Test Mode
The CSV also contains `Check Input Count`, `System Settings`, `Delete High
Scores`, and `Machine Connection Test`, but prefixes those entries with `#`.
It contains `Update Online` without `#`, although that item is still filtered
out by this build's runtime conditions.
## Forms and data exposed
- Monitor Test: color bars, white, red, green, blue, and cross-hatch patterns.
- Input/Output Test: all cabinet switches, both five-input boosters, headphone
volume/jack state, and Groove Stage connection. Booster buttons drive their
lamps; the coin switch changes lockout.
- LED Test: all lights and individual title, side, and booster light groups.
- Card Test: card-reader status and card ID.
- Audio Settings: test BGM, master/headphone/demo levels, five speaker channels,
Groove Stage status, and normal/demo vibration intensity.
- Game Settings: coin/song price, per-day operating hours, and score-attack
mode.
- Network Info: location identity/address/IP, cabinet IP/MAC, NESYS versions,
and relay server.
- Bookkeeping: uptime/play totals, free plays, service-switch count, player
statistics, 30-week and hourly histograms, play logs, the last twenty errors,
and service-switch history.
- System Info: program/system configuration values.
- Factory Settings: confirmation form followed by persistent-data reset.
Persistent test-mode data has separate RTTI classes for system settings, high
scores, play/weekly/daily/error/service-switch logs, and game-unique data. Log
paths embedded in the executable live under `TestModeFile\...\Log`.
### Coin/song presets
`CTestModeForm_GameSetting` chooses its coin/song table from the Type X
`HKLM\SOFTWARE\taito\typex\Country` registry value. `Country == 0` uses eight
entries, in this exact order:
1. 1 coin, 2 songs
2. 1 coin, 3 songs
3. 2 coins, 2 songs
4. 2 coins, 3 songs
5. Free play, 1 song
6. Free play, 2 songs
7. Free play, 3 songs
8. 1 coin, 1 song
For a non-zero country value it instead exposes 39 entries: every combination
of 1 through 12 coins with 1 through 3 songs, followed by the three free-play
variants. `FUN_00570a70` and `FUN_00570810` wrap the selected index forward and
backward respectively.
OpenRoller mirrors the registry value with `Country` in `openroller.cfg` beside
the executable. CMake creates the file on the first build but leaves an
existing copy untouched. The value is reread whenever test mode is entered, so
the application does not need to be rebuilt after changing it.
## Useful code addresses
| Address | Role |
| ---: | --- |
| `0x006396f0` | top-level boot/game/test-mode state machine |
| `0x00634060` | logical cabinet input + DirectInput fallback |
| `0x00633d00` | mask current FAST I/O input bitfield |
| `0x00633de0` | apply active-level table |
| `0x00569d00` | construct `GWTestModeInput_GW` |
| `0x00569d20` | translate game inputs to generic test-mode input flags |
| `0x00569610` | construct `GWTestModeRenderText_GW` |
| `0x00569ad0` | construct `GWTestModeRenderSprite_GW` |
| `0x00569440` | construct `GWTestModeSE_GW` |
| `0x005693f0` | construct `GWTestModeBGM_GW` |
| `0x00577940` | initialize test-mode form/render state |
| `0x00634fd0` | credit/service-credit handling and `SERVICE-SW LOCKED` |
`SERVICE-SW LOCKED` is unrelated to entering test mode. It is emitted by the
credit controller after repeated Service-switch credit pulses and holds the
service-credit lock for 180 updates.
## LED output pipeline
The LED test form's main methods are:
| Address | Role |
| ---: | --- |
| `0x0056e160` | construct `CTestModeForm_LEDTest` |
| `0x0056e390` | update the selected LED test pattern |
| `0x0056e6c0` | handle the seven LED-test menu items |
| `0x0062b0e0` | set logical LED `index` to `R,G,B,alpha` |
| `0x0062d4d0` | construct all logical LED objects and their hardware mapping |
| `0x0062a4d0` | update one `CLedDevice` and place its value in the cabinet output buffers |
| `0x004b3b40` | write one RGB pixel into the shared board buffer |
| `0x004b4c50` | pack shared LED buffers into the FIO transfer blocks |
| `0x004b55f0` | exchange registers and transfer blocks with `iDmacDrv32` |
| `0x004b6700` | wrapper for `iDmacDrvRegisterBufferWrite` |
### Logical groups
There are 118 logical LED objects:
| Logical indices | Test-mode group | Hardware coordinates |
| ---: | --- | --- |
| `0` | left booster button | simple output 4 |
| `1..8` | left booster RGB group 8 | board 0, group 8, positions 0..7 |
| `9..40` | left booster RGB groups 0..7 | board 0, groups 0..7, positions 0..3 |
| `41` | right booster button | simple output 5 |
| `42..49` | right booster RGB group 8 | board 1, group 8, positions 0..7 |
| `50..81` | right booster RGB groups 0..7 | board 1, groups 0..7, positions 0..3 |
| `82..93` | title | board 2, strips 0..1, pixels 0..5 |
| `94..105` | left side | board 2, strips 2..3, pixels 0..5 |
| `106..117` | right side | board 2, strips 4..5, pixels 0..5 |
The menu implements the groups directly:
- item 0 cycles all 118 LEDs through off, white, red, green, and blue;
- items 1, 2, and 3 fill the twelve title/left/right-side LEDs white one
address at a time, then clear them in the same order;
- items 4 and 5 perform the same fill/clear sequence across 41 LEDs on the
corresponding booster;
- item 6 starts the form-exit sequence.
The LED screen itself is only the generic seven-row `GWTestModeWindowList`,
anchored at y `-0.725` with an explicit row gap of `0.05` (48 px row
advance). It does not draw a schematic of the cabinet or booster rings.
The all-light loop special-cases logical LEDs 0 and 41 because the booster
button lamps are single-channel. For those two, it ORs `R|G|B` and writes the
same intensity as a simple lamp.
The executable establishes 40 independently controlled RGB positions plus one
simple button lamp per booster. The `group/position` coordinates above describe
the software/FIO mapping only; they do not establish the physical arrangement
of those RGB positions inside the arcade booster's plastic assembly.
### Color representation
`FUN_004b3b40(board, strip, pixel, R, G, B)` stores each full-color LED as
three bytes in this order:
```text
B, R, G
```
Each board reserves `9 * 8 * 3 = 0xd8` bytes even where fewer than eight
pixels are connected. Before placing a value in the hardware buffer,
`CLedDevice` applies the executable's integer gamma curve independently to
each channel:
```text
wire_channel = min(channel * channel / 255, 255)
```
The alpha/intensity field is applied by the LED object before this final
conversion. Test mode always passes alpha `0xff`.
### iDmac/FIO transfers for board type 0x825c
The current shim reports FIO type `0x825c`; the game selects its `0x25c`
packing branch. Each hardware update eventually makes these calls:
| Register-buffer address | Bytes | LED content |
| ---: | ---: | --- |
| `0x5000` | `0x1b0` | general FIO block; title bytes at offset `0x168`, button lamps at offsets 4 and 5 |
| `0x5200` | `0x1e0` | general FIO block; left/right-side bytes at offset `0x168` |
| `0x5400` | `0x0d8` | raw board-0 buffer: left booster |
| `0x5600` | `0x0d8` | raw board-1 buffer: right booster |
The board-2 source buffer is split as follows:
- its first `0x30` bytes (two 8-pixel strip slots) go to
`0x5000 + 0x168`;
- its following `0x60` bytes (four 8-pixel strip slots) go to
`0x5200 + 0x168`.
The unused bytes in those strip slots remain zero. This was confirmed both in
the decompiled `GWInputDeviceXioFio_BOOST` update path and in the live
`idmac_shim.log`: normal steady-state writes have sizes `1b0`, `1e0`, `d8`,
and `d8` respectively. Initial device negotiation first sends four `0x200`
byte blocks, then switches to the exact steady-state lengths above.
The game also contains a `0x23c` packing branch and generic transfer banks
through `0x5e00`, but those are not selected by the current emulated hardware
ID.
+183
View File
@@ -0,0 +1,183 @@
# GC Track and Background Reverse Notes
Status: linear track evaluation and the base background color table are
implemented in the Vectorail player. Camera mode bytes, fade modes and stage
objects still need deeper mapping.
## Track wire data
The main stage file stores track keys as big-endian records:
```text
u32 count
repeat count times:
u32 time_ms
f32 x
f32 y
f32 z
```
The stage constructor `FUN_005ed4c0` loads these into 16-byte runtime entries.
The fourth float is not read from disk: the constructor computes the physical
length to the next point and stores it there. It also accumulates the complete
track length at stage-object offset `+0xb0`.
## Confirmed evaluation
`FUN_005e9690` evaluates a track position at an integer timestamp. It finds the
adjacent `time_ms` keys and computes:
```text
u = (time_ms - key[i].time_ms) / (key[i+1].time_ms - key[i].time_ms)
position = key[i] + (key[i+1] - key[i]) * u
```
There is no Catmull-Rom or other spline interpolation in this path.
The Android symbol `GameScene::DrawWay` (`0x001b3b68`) establishes how the
visible route itself is rendered. It clips the authored point list to the
requested first and last timestamps, inserts linearly interpolated points at
both exact boundaries, interpolates an RGBA color over that time interval, and
submits the result as primitive mode 3: a line strip. It does not generate a
camera-facing ribbon, resample by physical distance, or cull triangles.
`FUN_005e9990` is a separate physical-distance sampler used while constructing
duration-note paths. It carries leftover distance between segments so those
generated samples remain uniform over corners; it is not the normal route
renderer.
The player now mirrors `DrawWay`: it uploads the original authored points plus
the two exact timestamp intersections and draws separate behind/current and
current/ahead line strips with endpoint color gradients. The stage values
`backwardsDrawDist` and `forwardDrawDist` behave as seconds and are converted
to milliseconds before comparison with track and note timestamps.
Dynamic `TrackDrawDist` records are step changes, not interpolation keys. The
StageConfig forward distance remains active until the timestamp of the first
matching record, then each record replaces it. This matters for Knight Rider:
its only record is `128400 ms: 0`, which hides the route at the end of the song;
using the first record before its timestamp incorrectly hid the entire future
route from the beginning.
For `ac_10pt8tion_hard.dat`:
```text
first track key: 0 ms, (0, 0, 0)
second track key: 6486 ms, (0, 0, 199.985)
draw behind: 10 s
draw ahead: 7 s
```
Treating 7/10 as world units collapses the visible rail to a tiny fraction of
the first segment; timestamp clipping produces the expected visible range.
## Track colors
The stage config stores two RGBA colors directly after the draw-range values.
They are used for the forward and already-travelled portions of the rail. For
`10pt8tion_hard` they are `(255,0,128)` ahead and `(255,255,255)` behind.
## Base background
The main `.dat` contains a `ColorTable` section. Each 22-byte record is:
```text
u32 time_ms
rgba top_right
rgba top_left
rgba bottom_right
rgba bottom_left
u8 interpolate_to_next
u8 audio_reactive_color
```
`FUN_00642390` holds the active colors unless `interpolate_to_next` is set; in
that case it linearly interpolates all four RGBA values to the following key.
`audio_reactive_color` applies `FUN_005d9650` to each active color using the
runtime analyser value. The Linux player now implements the exact hold versus
interpolate selection and keeps the second mode at its neutral color factor
until the analyser feeding `stage renderer +0x24` is ported.
`data/stage/2d/<song>_menu.dds` is not the gameplay background. It is a
512x256 UI atlas whose top-left 197x197 cell is the song jacket. The player has
a small uncompressed DDS loader, but the jacket is disabled by default and is
only an explicit debug comparison layer (`B`). The inherited procedural
blue/black square grid has also been removed: the base layer is now only the
stage-authored color table before particles, visualizers and objects are drawn.
The remaining original scene is produced by the stage `particles`,
`visualizer`, and `objects` sections (plus `.tumo` models), not by a single
background bitmap. These sections are now decoded completely by
`StagePattern`: particle records are 44 bytes, visualizer records are 12 bytes,
and every variable-length object record is consumed through its visibility,
movement, scaling, rotation and color-key arrays. Oshama contains 5 particle
keys, 19 visualizer keys, 46 model names and 316 object instances; 10pt8tion
contains 2, 28, 37 and 352 respectively. The Vectorail level data retains this
decoded scene for the model-rendering pass.
The PSP package now retains all three timelines. The particle constructor at
`FUN_005f0940` creates a 64-instance pool for every configured particle key.
`FUN_005f0130` scales both `repeatMeasure` and `lifespanMeasure` by the active
beat duration. Recovered spawn layouts are: type 1, a deterministic/random
point; type 2, a screen/grid group using `groupShapeSize`; type 3, six points
at 60-degree intervals around a circle. The PSP implementation follows these
timing and layout rules, but substitutes geometry for the original particle
texture resource until that resource binding is mapped.
The common `.tumo` container is also big-endian. Its outer count is followed,
for each mesh, by resource names, an XYZ vertex table, eight bound floats,
render parts, and polygon lists. A polygon corner is `u32 vertexIndex, f32 u,
f32 v`; a separate block contains explicit line-index pairs. The player now
triangulates polygon fans, retains line primitives, and renders stage objects
with the original five animation channels: visibility, movement, scaling,
linearly interpolated Euler rotation, and RGBA color. The final rotation uses
the recovered `(-Y, X, Z)` quaternion-builder convention. Base and animated transform
components are composed as separate matrices, matching `FUN_00643b20`;
visibility fades use the original fixed 250 ms window. All 46 unique Oshama
models and all 37 unique 10pt8tion models decode. `O` toggles this object layer
for comparison.
For stage format versions newer than `0x29ce`, the supplemental table beginning
at the header's `ColorTable2` offset has a relative pointer at `base + 8`. Its
stream is `u32 objectCount` followed by one signed big-endian `int16`
`parentIndex` per object (`-1` means no parent). `FUN_006445b0` evaluates one
parent level and renders `parentTransform * childTransform`; parent and child
RGBA are multiplied component-wise. Oshama uses this on 80 of 316 objects and
10pt8tion on 87 of 352, so omitting it visibly loses grouped scale, placement,
and opacity.
The object's `wireframe` byte also selects mutually exclusive model paths:
`FUN_005dd8e0` draws polygon parts, while `FUN_005dd7f0` draws the explicit TUMO
edge buffer. The player previously drew both. Polygon/edge selection now
matches the flag. `FUN_00648c30` enables `D3DRS_ZENABLE` and
`D3DRS_ZWRITEENABLE` for the complete authored object loop, then disables both
before the rail and markers are submitted. Objects therefore depth-test one
another in file order, but never hide gameplay geometry drawn afterwards.
### Per-frame object clipping
The stage loader also opens `data/stage/<chart>_clip.dat`. `FUN_005ed4c0`
decodes it as:
```text
u32be object_count
u32be frame_count
u8 visible[object_count][frame_count]
```
The payload is object-major. `FUN_006445b0` selects frame
`round(time_ms / (1000 / 60))` and skips the object when the byte is zero;
when the table is absent or the requested frame is past its end, it falls
back to the normal authored-visibility path. Knight Rider has 178 objects and
7909 clip frames. Ignoring this table submits many objects which the original
never draws in that camera frame and turns its background into overlapping
full-screen geometry. The Linux player now applies the same test before object
animation and submission.
`FUN_005dd8e0` itself supports two TUMO part types. Type `0` is a triangle
list; type `1` is a line list. Type-1 source polygons contain exactly two
corners. Oshama's models contain ten such parts, including one with 1498 line
vertices; treating them as polygon fans silently discarded all of them. The
player now keeps solid line parts separate from both triangle parts and the
wireframe edge buffer. All Oshama and 10pt8tion models end with a zero node
count, so an internal model hierarchy is not responsible for their transforms.
+332
View File
@@ -0,0 +1,332 @@
// Common
using string8 = std::string::SizedString<u8> [[format("string_formatter8")]];
using string16 = std::string::SizedString<u16> [[format("string_formatter16")]];
fn string_formatter8(ref string8 s) {
return std::format("\"{:s}\"", s);
};
fn string_formatter16(ref string16 s) {
return std::format("\"{:s}\"", s);
};
struct color {
u8 r;
u8 g;
u8 b;
u8 a;
} [[format("color_formatter")]];
fn color_formatter(ref color c) {
return std::format("#{:02x}{:02x}{:02x}{:02x}", c.r, c.g, c.b, c.a);
};
struct fcolor {
float r;
float g;
float b;
float a;
};
// Header
struct Header {
u32 stageCfg;
u32 trackDrawDist;
u32 track;
u32 notes;
u32 camera;
u32 particles;
u32 visualizer;
u32 unk1;
u32 colors;
u32 objects;
u32 unk2;
u32 colors2;
u32 unk3;
};
// Stage Config
struct BpmChange {
u32 timeMs;
u32 bpm;
} [[single_color]];
struct NoteTimingEntry {
u32 timeMs;
u32 mode;
float value;
} [[single_color]];
struct NoteTimingList {
u16 size;
NoteTimingEntry entries[size];
} [[single_color]];
struct StageConfig {
float endTime1;
float endTime2;
float outroTime;
u16 bpmChangeSz [[hidden]];
BpmChange bpmChanges[bpmChangeSz];
NoteTimingList noteTimings[4];
string16 chartName;
string16 chartName2;
string16 bgmName;
string16 shotName;
float backwardsDrawDist;
float forwardDrawDist;
color trackAheadColor;
color trackBehindColor;
u8 audioOffset;
float visualOffset;
color unk;
};
// Track Draw Distance
struct DrawDistance {
u32 timeMs;
float distance;
} [[single_color]];
struct TrackDrawDist {
u32 sz;
DrawDistance points[sz];
};
// Track
struct TrackPiece {
u32 timeMs;
float x;
float y;
float z;
} [[single_color]];
struct TrackPieceArray {
u32 sz;
TrackPiece pieces[sz];
};
// Notes
enum NoteType : u8 {
NONE = 0,
NORMAL = 1,
FLICK = 2,
HOLD = 3,
SCRATCH = 4,
BEAT = 5,
MERRY_GO_ROUND = 6,
HIDDEN = 7,
HIDDEN2 = 8,
CRITICAL = 9,
SLIDE_HOLD = 10,
SLIDE_COUNTER = 11,
TURN = 12,
SPIN = 13,
FINISH = 14,
DUAL_HOLD = 15,
};
struct Note {
u32 timeMs;
NoteType type;
u8 typeOverride;
s16 params16[9];
u8 flag24;
float params25[3];
u8 flag37;
u8 flag38;
float params39[4];
u32 params55[3];
float param67;
u32 param71;
float params75[5];
u32 param95;
} [[single_color]];
struct NoteArray {
u32 namesSz;
string8 names[namesSz];
u32 sz;
Note entries[sz];
};
// Camera
struct Camera {
u32 timeMs;
u8 aMode;
u8 fMode;
float dist;
float rotationA[2];
float originOff[3];
u8 projType;
float fieldFar[3];
float fieldNear[3];
float rotationB[1];
} [[single_color]];
struct CameraArray {
u32 sz;
Camera points[sz];
};
// Particles
struct Particle {
u32 timeMs;
u32 unk;
u32 shape;
u32 texture;
color color;
float velocity[3];
float repeatMeasure;
float lifespanMeasure;
u32 groupShapeSize;
} [[single_color]];
struct ParticleArray {
u32 sz;
Particle particles[sz];
};
// Visualizer
struct Visualizer {
u32 timeMs;
u32 type;
color color;
} [[same_color]];
struct VisualizerArray {
u32 sz;
Visualizer entries[sz];
};
// Color Table 1
struct ColorTable {
u32 timeMs;
color topRight;
color topLeft;
color bottomRight;
color bottomLeft;
bool fadeOut;
bool fadeIn;
} [[same_color]];
struct ColorTableArray {
u32 sz;
ColorTable entries[sz];
};
// Objects
struct Visibility {
u32 timeMs;
bool fadeOut;
bool fadeIn;
bool visible;
};
struct VisibilityArray {
u32 sz;
Visibility entries[sz];
};
struct Movement {
u32 timeMs;
bool tweenTowards;
bool tweenAway;
float pos[3];
};
struct MovementArray {
u32 sz;
Movement entries[sz];
};
struct Scaling {
u32 timeMs;
bool tweenTowards;
bool tweenAway;
float scale[3];
};
struct ScalingArray {
u32 sz;
Scaling entries[sz];
};
struct Rotation {
u32 timeMs;
bool tweenTowards;
bool tweenAway;
float rotation[3];
};
struct RotationArray {
u32 sz;
Rotation entries[sz];
};
struct ColorChange {
u32 timeMs;
bool tweenTowards;
bool tweenAway;
color new;
};
struct ColorChangeArray {
u32 sz;
ColorChange entries[sz];
};
struct Object {
u32 model;
u32 fs;
bool wireframe;
bool flashing;
bool unk;
float position[3];
float scale[3];
float rotation[3];
fcolor color;
float unk2[3];
VisibilityArray vis;
MovementArray mov;
ScalingArray scaling;
RotationArray rotations;
ColorChangeArray colorChanges;
} [[same_color]];
struct ObjectArray {
u32 namesSz;
string8 names[namesSz];
u32 names2Sz;
string8 names2[names2Sz];
u32 sz;
Object entries[sz];
};
// Color Table 2
struct ColorTable2Entry1 {
color color;
u8 unk;
};
struct ColorTable2Entry2 {
bool visible;
color center;
color top;
color bottom;
color left;
color right;
};
struct ColorTable2Entry4 {
u8 unk;
color color;
float unk2;
u8 unk3;
u8 unk4;
};
struct ColorTable2Array {
u32 off1; // after offsets
u32 off2; // after arr1
u32 off3; // after arr2
u32 off4; // after arr3
u32 off5; // after arr4
u32 sz1;
ColorTable2Entry1 arr1[sz1];
u32 sz2;
ColorTable2Entry2 arr2[sz2];
u32 sz3;
u16 arr3[sz3];
u32 sz4;
ColorTable2Entry4 arr4[sz4];
};
Header hdr @ 0x00;
StageConfig cfg @ hdr.stageCfg;
TrackDrawDist trackDrawDist @ hdr.trackDrawDist;
TrackPieceArray track @ hdr.track;
NoteArray notes @ hdr.notes;
CameraArray camera @ hdr.camera;
ParticleArray particles @ hdr.particles;
VisualizerArray visualizer @ hdr.visualizer;
ColorTableArray colorTable @ hdr.colors;
ObjectArray objects @ hdr.objects;
ColorTable2Array colorTable2 @ hdr.colors2;