Initial public source release
Split reusable rendering and format support into vectorail-core and vectorail-gc.
This commit is contained in:
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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]];
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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;
|
||||
Reference in New Issue
Block a user