forked from tsuki/openroller
Add portable Windows build and update GC runtime
This commit is contained in:
@@ -37,7 +37,9 @@ workspace/
|
|||||||
└── openroller/
|
└── openroller/
|
||||||
```
|
```
|
||||||
|
|
||||||
Install SDL3, OpenGL, libpng and GLM, then build:
|
Install CMake, a C++ compiler, and OpenGL development files, then build. SDL3,
|
||||||
|
GLM, and the PNG decoder are fetched automatically when no installed package
|
||||||
|
is available:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cmake -S openroller -B openroller/build -DCMAKE_BUILD_TYPE=Release
|
cmake -S openroller -B openroller/build -DCMAKE_BUILD_TYPE=Release
|
||||||
@@ -51,6 +53,33 @@ with `VECTORAIL_CORE_SOURCE_DIR` and `VECTORAIL_GC_SOURCE_DIR`.
|
|||||||
|
|
||||||
The raw stage inspection utility is built as `openroller-stage-probe`.
|
The raw stage inspection utility is built as `openroller-stage-probe`.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
To cross-build a self-contained x86-64 package from Linux with MinGW-w64:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S openroller -B openroller/build-windows \
|
||||||
|
-DCMAKE_TOOLCHAIN_FILE=openroller/cmake/toolchains/mingw-x86_64.cmake \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build openroller/build-windows --parallel
|
||||||
|
cmake --install openroller/build-windows \
|
||||||
|
--prefix openroller/build-windows/package
|
||||||
|
```
|
||||||
|
|
||||||
|
For a native MSYS2 CLANG64 build, install CMake, Ninja, Clang, SDL3, GLM, and
|
||||||
|
OpenGL packages in the CLANG64 shell, then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S openroller -B openroller/build-windows -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release
|
||||||
|
cmake --build openroller/build-windows --parallel
|
||||||
|
cmake --install openroller/build-windows \
|
||||||
|
--prefix openroller/build-windows/package
|
||||||
|
```
|
||||||
|
|
||||||
|
The package directory contains `OpenRoller.exe`, `SDL3.dll`,
|
||||||
|
`openroller.cfg`, and the shaders needed at runtime.
|
||||||
|
|
||||||
## PSP build
|
## PSP build
|
||||||
|
|
||||||
The PSP port is built with the `pspdev/pspdev` container:
|
The PSP port is built with the `pspdev/pspdev` container:
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ target_link_libraries(openroller-desktop
|
|||||||
Vectorail::GCEffects
|
Vectorail::GCEffects
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(MINGW AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||||
|
target_link_options(openroller-desktop PRIVATE
|
||||||
|
-static -static-libgcc -static-libstdc++)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(openroller-desktop PRIVATE /W4)
|
target_compile_options(openroller-desktop PRIVATE /W4)
|
||||||
else()
|
else()
|
||||||
@@ -25,3 +30,27 @@ endif()
|
|||||||
|
|
||||||
configure_file(openroller.cfg openroller.cfg COPYONLY)
|
configure_file(openroller.cfg openroller.cfg COPYONLY)
|
||||||
file(COPY shaders DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
file(COPY shaders DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
|
||||||
|
add_custom_command(TARGET openroller-desktop POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/shaders"
|
||||||
|
"$<TARGET_FILE_DIR:openroller-desktop>/shaders"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/openroller.cfg"
|
||||||
|
"$<TARGET_FILE_DIR:openroller-desktop>/openroller.cfg"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
if(WIN32 AND TARGET SDL3::SDL3-shared)
|
||||||
|
add_custom_command(TARGET openroller-desktop POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"$<TARGET_FILE:SDL3::SDL3-shared>"
|
||||||
|
"$<TARGET_FILE_DIR:openroller-desktop>"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
install(FILES "$<TARGET_FILE:SDL3::SDL3-shared>" DESTINATION .)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
install(TARGETS openroller-desktop RUNTIME DESTINATION .)
|
||||||
|
install(DIRECTORY shaders DESTINATION .)
|
||||||
|
install(FILES openroller.cfg DESTINATION .)
|
||||||
|
|||||||
@@ -686,12 +686,12 @@ struct GcKeySample {
|
|||||||
float sampledTimeMs = 0.0f;
|
float sampledTimeMs = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
bool gcLoopFlag(const gc::TransformPoint& key) { return key.tweenTowards; }
|
bool gcLoopFlag(const gc::TransformPoint& key) { return key.repeat; }
|
||||||
bool gcLoopFlag(const gc::ObjectColorPoint& key) { return key.tweenTowards; }
|
bool gcLoopFlag(const gc::ObjectColorPoint& key) { return key.repeat; }
|
||||||
bool gcLoopFlag(const gc::VisibilityPoint& key) { return key.fadeOut; }
|
bool gcLoopFlag(const gc::VisibilityPoint& key) { return key.repeat; }
|
||||||
bool gcInterpolateFlag(const gc::TransformPoint& key) { return key.tweenAway; }
|
bool gcInterpolateFlag(const gc::TransformPoint& key) { return key.interpolate; }
|
||||||
bool gcInterpolateFlag(const gc::ObjectColorPoint& key) { return key.tweenAway; }
|
bool gcInterpolateFlag(const gc::ObjectColorPoint& key) { return key.interpolate; }
|
||||||
bool gcInterpolateFlag(const gc::VisibilityPoint& key) { return key.fadeIn; }
|
bool gcInterpolateFlag(const gc::VisibilityPoint& key) { return key.interpolate; }
|
||||||
|
|
||||||
// FUN_005e9100 is shared by all five object-animation channels. The first
|
// FUN_005e9100 is shared by all five object-animation channels. The first
|
||||||
// flag marks a repeat block; the second enables interpolation to the next key.
|
// flag marks a repeat block; the second enables interpolation to the next key.
|
||||||
@@ -807,7 +807,7 @@ GcVisibilityState gcObjectVisibility(const gc::StageObject& object, float timeMs
|
|||||||
if (active + 1 < static_cast<int>(keys.size())) {
|
if (active + 1 < static_cast<int>(keys.size())) {
|
||||||
const auto& next = keys[active + 1];
|
const auto& next = keys[active + 1];
|
||||||
const float untilNext = static_cast<float>(next.timeMs) - sample.sampledTimeMs;
|
const float untilNext = static_cast<float>(next.timeMs) - sample.sampledTimeMs;
|
||||||
if (next.visible != visible && next.fadeIn &&
|
if (next.visible != visible && next.interpolate &&
|
||||||
untilNext >= 0.0f && untilNext < kGcVisibilityFadeMs) {
|
untilNext >= 0.0f && untilNext < kGcVisibilityFadeMs) {
|
||||||
const float remaining = std::clamp(untilNext / kGcVisibilityFadeMs, 0.0f, 1.0f);
|
const float remaining = std::clamp(untilNext / kGcVisibilityFadeMs, 0.0f, 1.0f);
|
||||||
alpha = next.visible ? 1.0f - remaining : remaining;
|
alpha = next.visible ? 1.0f - remaining : remaining;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||||
|
|
||||||
|
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||||
|
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||||
|
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||||
|
|
||||||
|
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||||
+49
-10
@@ -70,20 +70,21 @@ The file is big-endian. The initial words are not all section offsets. Confirmed
|
|||||||
| 2 | track points |
|
| 2 | track points |
|
||||||
| 3 | notes |
|
| 3 | notes |
|
||||||
| 4 | camera |
|
| 4 | camera |
|
||||||
| 5 | particles |
|
| 5 | `TuneBGEffectData` / FlowItem keys |
|
||||||
| 6 | visualizer |
|
| 6 | visualizer |
|
||||||
| 7 | unknown section |
|
| 7 | background texture names and image keys |
|
||||||
| 8 | first color table |
|
| 8 | first color table |
|
||||||
| 9 | objects |
|
| 9 | objects |
|
||||||
| 10 | scalar/unknown; often `0x30`, not an offset |
|
| 10 | scalar/unknown; often `0x30`, not an offset |
|
||||||
| 11 | second color table |
|
| 11 | points into the post-object extension offset table |
|
||||||
| 12 | scalar/unknown; can accidentally look like an in-file offset |
|
| 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.
|
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
|
The post-object extension base is the reader position after the variable
|
||||||
slot 10. The scalar/offset/scalar arrangement in slots 10..12 belongs to the
|
object stream. Relative offset slot 1 extends background colors and slot 2
|
||||||
newer 13-word format used by the 4.71-era charts.
|
stores object parents. Deriving that base from consumed bytes, as Android
|
||||||
|
`LoadBGData` does, also handles the small mobile chart revision correctly.
|
||||||
|
|
||||||
## Note array: confirmed wire layout
|
## Note array: confirmed wire layout
|
||||||
|
|
||||||
@@ -199,8 +200,15 @@ BPM active at the note timestamp. With `beat_ms = 60000 / bpm`:
|
|||||||
| ---: | --- |
|
| ---: | --- |
|
||||||
| `+6` | signed marker-effect/UV selector; the note-head draw passes `value - 1` to effect 3 |
|
| `+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)` |
|
| `+39` | appearance lead in beats; runtime `+0xbc = max(time - value * beat_ms, 0)` |
|
||||||
|
| `+37` | enable positional fly-in |
|
||||||
|
| `+38` | draw the additive fly-in trail |
|
||||||
|
| `+43` | fly-in interpolation start, in beats after appearance |
|
||||||
|
| `+47` | fly-in interpolation end, in beats before the hit time |
|
||||||
| `+51` | duration in beats for types 3/4/5/10/15; runtime `+0x58` and `+0xac = time + value * beat_ms` |
|
| `+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 |
|
| `+55` | primary packed authored `RRGGBBAA` target colour; also used by duration geometry |
|
||||||
|
| `+59` | secondary packed `RRGGBBAA` colour |
|
||||||
|
| `+63` | integer fly-in oscillation count |
|
||||||
|
| `+67` | fly-in oscillation endpoint/phase scalar |
|
||||||
| `+71` | number of generated MERRY GO ROUND targets |
|
| `+71` | number of generated MERRY GO ROUND targets |
|
||||||
| `+75` | MERRY GO ROUND spacing in beats |
|
| `+75` | MERRY GO ROUND spacing in beats |
|
||||||
|
|
||||||
@@ -333,7 +341,7 @@ 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
|
uses `RotateHPB::SetVector` (`atan2(screen_dx, screen_dy_down)`) and stores the
|
||||||
resulting screen angle. It is computed before gameplay and remains fixed while
|
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
|
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,
|
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
|
colour and marker alpha. This is independent of the lower-right control
|
||||||
helper, even though that helper also has a direction overlay.
|
helper, even though that helper also has a direction overlay.
|
||||||
|
|
||||||
@@ -366,8 +374,13 @@ synthetic pulsing rings are no longer used for decoded GC stages.
|
|||||||
|
|
||||||
Duration bodies are also type-specific in the executable:
|
Duration bodies are also type-specific in the executable:
|
||||||
|
|
||||||
- HOLD calls `0x00647cf0` and SLIDE HOLD calls `0x00641fd0`; both emit ribbon
|
- Android HOLD does not submit the prebuilt ribbon array. Its normal gameplay
|
||||||
triangles rather than an OpenGL-style line.
|
branch calls `DrawWay(max(current_ms, appear_ms), end_ms, color, color)` with
|
||||||
|
additive blending and a beat-reactive width from 3 to 5 pixels. This exact
|
||||||
|
moving timestamp boundary is why dropping whole prebuilt samples produces a
|
||||||
|
visibly stepped disappearance. Arcade still constructs HOLD helper geometry,
|
||||||
|
but it is not evidence for the mobile draw path.
|
||||||
|
- SLIDE HOLD calls its prebuilt triangle-ribbon path.
|
||||||
- SCRATCH is sampled every `0.15` world units. `0x005ebaa0` derives two
|
- 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
|
opposing paths with radius `0.2` and rotates the offset by 45 degrees per
|
||||||
sample; `0x00641d50` emits six vertices per segment for each path.
|
sample; `0x00641d50` emits six vertices per segment for each path.
|
||||||
@@ -391,6 +404,32 @@ Duration bodies are also type-specific in the executable:
|
|||||||
screen direction. Both endpoint layers inherit the long note's colour, fade alpha,
|
screen direction. Both endpoint layers inherit the long note's colour, fade alpha,
|
||||||
billboard transform, beat-synchronised animation and `0.025` sprite scale.
|
billboard transform, beat-synchronised animation and `0.025` sprite scale.
|
||||||
|
|
||||||
|
### Positional fly-in
|
||||||
|
|
||||||
|
`GameScene::DrawMark` converts wire `+25/+29/+33` into direction vector `D` and
|
||||||
|
starts from the authored route position `P`. With:
|
||||||
|
|
||||||
|
```text
|
||||||
|
start = max(0, appear_ms + field_43 * beat_ms)
|
||||||
|
end = max(0, note_ms - field_47 * beat_ms)
|
||||||
|
u = 0 before start, (now-start)/(end-start) in between, 1 at/after end
|
||||||
|
```
|
||||||
|
|
||||||
|
`0x005ebaa0` first stores runtime `+0xc0 = ROUND(appear_ms / frame_ms)`.
|
||||||
|
`0x0064ab80` then computes the start from that stored integer, but computes the
|
||||||
|
end from the still-fractional `note_ms / frame_ms`; each final boundary passes
|
||||||
|
through another nearest-integer conversion at `0x0050a4a0`. The live clock is
|
||||||
|
quantized the same way.
|
||||||
|
Flag `+37` selects positional interpolation. With no oscillations the displayed
|
||||||
|
position is `P + D*(1-u)`. When integer `cycles = +63` is positive, let
|
||||||
|
`c = +67` for values up to 1 and `c = 2-(+67)` otherwise; then position is
|
||||||
|
`P + D*(c - abs(sin(u*(asin(c) + cycles*2*pi))))`. Integer cycles guarantee
|
||||||
|
that the marker lands back on `P` at `u=1`. The approach circle follows the
|
||||||
|
same moved position. Flag `+38` draws an additive line from the initial external
|
||||||
|
anchor to the moving marker before the hit time. The player preserves that
|
||||||
|
nearest-frame 60 Hz quantization rather than smoothing the movement on the
|
||||||
|
audio clock.
|
||||||
|
|
||||||
## Tap judgment timing
|
## Tap judgment timing
|
||||||
|
|
||||||
The system-config loader at `0x00635c90` lays out the timing overrides in four
|
The system-config loader at `0x00635c90` lays out the timing overrides in four
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# Groove Coaster Android Runtime Reverse
|
||||||
|
|
||||||
|
This is a clean-room interoperability record for the offline Android package
|
||||||
|
`gc2offlinev4.xapk`. It documents facts used to validate the GC stage loader
|
||||||
|
and Vectorail runtime. Addresses below are ELF virtual addresses in
|
||||||
|
`lib/arm64-v8a/libtune.so`; the Ghidra project uses an additional `0x100000`
|
||||||
|
image-base offset.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
```text
|
||||||
|
XAPK c6c394f7a1cc65331edc98aac94014668f9d5277ce17262fecf9aeeb1a2a3003
|
||||||
|
APK 62c9e739dc9b8c1bcbcb4b5234d78f154b20d1a930329b9c21a3c9236b92a0dc
|
||||||
|
libtune 689b2e4c0bc4479a3f309944b5070796878c66c0dfdbd283c11ec0bbeeea9efa
|
||||||
|
```
|
||||||
|
|
||||||
|
Stage and audio ZIPs use the password returned by
|
||||||
|
`mtxc::ObbFile::getZipPassword`: `eiprblFFv69R83J5`.
|
||||||
|
|
||||||
|
The Android `ac_10pt8tion_{easy,normal,hard}{,_ext}.dat` payloads are binary
|
||||||
|
identical to the corresponding files in `GC/data/stage`. For example both
|
||||||
|
copies of `ac_10pt8tion_easy.dat` have SHA-256
|
||||||
|
`86a75c86b91bbbe25cae78bcb51d4f7842b3fc4946e4b091f2ec5ba084497765`.
|
||||||
|
The Android executable is therefore a valid independent specification for the
|
||||||
|
arcade files consumed by Vectorail.
|
||||||
|
|
||||||
|
## Stage Selection And Containers
|
||||||
|
|
||||||
|
`TuneAppMain::LoadStageData` (`0x9da24`) loads six chart ids per song. The
|
||||||
|
first three are serialized mobile ids. The other three are synthesized by
|
||||||
|
prefixing those ids with `ac_`. `GameScene::makeFilenameStageDat` (`0xc4bac`)
|
||||||
|
then appends `.dat` or `_ext.dat`.
|
||||||
|
|
||||||
|
The sampled offline package contains one-note `placeholder_bgm` mobile charts,
|
||||||
|
while its `ac_` charts contain the complete arcade route, camera, notes and
|
||||||
|
background scene. `GameScene::LoadStageData` (`0xc607c`) performs this order:
|
||||||
|
|
||||||
|
1. Load the selected main DAT from `<stage-pack>.zip`.
|
||||||
|
2. Call `TuneGameData::LoadGameData` (`0x933a0`).
|
||||||
|
3. Call `TuneGameData::LoadBGData` (`0x93aa4`) on the same bytes.
|
||||||
|
4. Load `_ext.dat` only for an extra/arcade difficulty when arrange mode is off.
|
||||||
|
5. Build runtime data and load stage resources.
|
||||||
|
|
||||||
|
For every selected `ac_` chart, `LoadExtData` starts at byte 6 of the matching
|
||||||
|
`_ext.dat`, replaces the four timing lists, reads another array of 99-byte
|
||||||
|
notes, and links each ext note to a same-time main note. If both effective
|
||||||
|
types are FLICK, the main runtime type becomes `0x10` (dual flick).
|
||||||
|
|
||||||
|
The corpus contains 2927 valid sidecars with 18965 ext notes. Every sidecar
|
||||||
|
parses with the layout described by `docs/stage_ext.pat`, and all ext note
|
||||||
|
records are FLICK entries. One unusual main chart is itself named
|
||||||
|
`SW_marianne_hard_ext.dat`, so its sidecar is
|
||||||
|
`SW_marianne_hard_ext_ext.dat`. File discovery must check whether removing the
|
||||||
|
suffix names an existing main chart instead of excluding every `_ext.dat`.
|
||||||
|
|
||||||
|
## Parsed Stage Sections
|
||||||
|
|
||||||
|
The parser now follows every section consumed by Android `LoadGameData` and
|
||||||
|
`LoadBGData`:
|
||||||
|
|
||||||
|
| Header | Runtime data |
|
||||||
|
| ---: | --- |
|
||||||
|
| 0 | stage config, BPM and four timing tables |
|
||||||
|
| 1 | route draw-distance keys |
|
||||||
|
| 2 | route points |
|
||||||
|
| 3 | 99-byte note records |
|
||||||
|
| 4 | 59-byte camera records |
|
||||||
|
| 5 | 44-byte `TuneBGEffectData` / FlowItem keys |
|
||||||
|
| 6 | 12-byte visualizer keys |
|
||||||
|
| 7 | background texture names and 20-byte image keys |
|
||||||
|
| 8 | background color keys |
|
||||||
|
| 9 | model/shader names and animated stage objects |
|
||||||
|
|
||||||
|
After the variable-length object stream, Android records the current read
|
||||||
|
position and treats it as the base of four relative extension offsets. The
|
||||||
|
third extension contains signed object parent indices. Header word 11 points
|
||||||
|
inside this table on current charts; it is not the object-stream end. The
|
||||||
|
parser now derives the base from the consumed object stream exactly as Android
|
||||||
|
does, which also fixes the small mobile DAT revision.
|
||||||
|
|
||||||
|
## Timing, Route And Long Elements
|
||||||
|
|
||||||
|
`TuneTimingData::GetTime` selects the latest timing key at or before the note.
|
||||||
|
Mode 1 is absolute milliseconds, mode 3 is next-note spacing, and other modes
|
||||||
|
multiply the authored value by `60000 / BPM`.
|
||||||
|
|
||||||
|
`TuneGameData::GetWayPosition` (`0x953bc`) linearly interpolates route points
|
||||||
|
by timestamp and clamps before/after the route. `WaySplitCheck` (`0x95c4c`)
|
||||||
|
clips the polyline to the requested time range, emits samples at a fixed world
|
||||||
|
distance, carries the unused distance across authored segments, and stores the
|
||||||
|
true interpolated timestamp for each sample.
|
||||||
|
|
||||||
|
Android spacing is `0.20` for HOLD/SLIDE/DUAL, `0.15` for SCRATCH and `0.40`
|
||||||
|
for BEAT. Arcade `game471.exe` uses `0.55` for BEAT; Vectorail intentionally
|
||||||
|
keeps the arcade value when playing arcade DATs.
|
||||||
|
|
||||||
|
`LoadTuneMarkDataOne` (`0x967d8`) confirms the wire-field translation and
|
||||||
|
compatibility remaps. Runtime types 7/8 become 1, 11 becomes 10, 12/14 become
|
||||||
|
9, and 13 becomes 4. Marker effects are forced to 35 for type 10, 32 for type
|
||||||
|
9, 37 for type 15 and 11 for raw type 13. Duration types are 3, 4, 5, 10 and
|
||||||
|
15. MERRY type 6 expands to `count` targets separated by the authored beat
|
||||||
|
spacing.
|
||||||
|
|
||||||
|
## Camera
|
||||||
|
|
||||||
|
`TuneGameData::GetCameraData` (`0x95588`) independently confirms the seven
|
||||||
|
anchor modes, the two interpolation paths and the orthographic/perspective
|
||||||
|
blend documented in `re_gc_camera.md`. Android uses right-handed OpenGL
|
||||||
|
look-at/projection matrices. The arcade executable uses the corresponding D3D
|
||||||
|
left-handed path; Vectorail converts the evaluated eye/target/up state to its
|
||||||
|
OpenGL renderer and uses the arcade fixed 75 degree FOV.
|
||||||
|
|
||||||
|
No camera wire fields are discarded, including non-finite values in the three
|
||||||
|
`ac_comet_*` intros.
|
||||||
|
|
||||||
|
## Background Runtime
|
||||||
|
|
||||||
|
`DrawBGColor` (`0xb3fc8`) selects the active color key, optionally cross-fades
|
||||||
|
to the next key, and applies BPM-derived HSV brightness modulation when its
|
||||||
|
rhythm-reactive flag is set. `DrawBGImage` (`0xb49b0`) supports tiled atlas
|
||||||
|
images, a built-in centered image and external texture entries.
|
||||||
|
`DrawBGVisualizer` (`0xb6118`) selects timed visualizer keys and calls the
|
||||||
|
8572-byte procedural `DrawVisualizer` path. The conventional FFT routine at
|
||||||
|
`0x104ec0` is not used to drive this stage visualizer.
|
||||||
|
|
||||||
|
The 44-byte table previously called particles is `TuneBGEffectData`.
|
||||||
|
`GameScene::ExecFlowItem` (`0xc0610`) uses it to spawn and update authored
|
||||||
|
flowing background elements. Shape 1 spawns a random screen point, shape 2 a
|
||||||
|
screen-space grid and shape 3 an eight-point ring. Repeat and lifetime are
|
||||||
|
scaled by the active beat before points are unprojected to route depth. It is
|
||||||
|
separate from hit-effect particles.
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
Stage audio is stored as encrypted `*_bgm.ogg.zip` and `*_shot.ogg.zip` files.
|
||||||
|
`MtxSoundBuffer::LoadData` (`0x10a614`) rewrites `.m4a` resource names to
|
||||||
|
`.ogg`. The sampled Android 10pt8tion pair is Vorbis, 44.1 kHz stereo, and both
|
||||||
|
streams are 129.621859 seconds. The corresponding arcade BGM WAV is PCM s16le,
|
||||||
|
44.1 kHz stereo, with the same duration.
|
||||||
|
|
||||||
|
`GameScene::ExecGameStage` (`0xaf8ac`) maintains a 60 Hz logical count, reads
|
||||||
|
`MtxSoundSource::GetPlaySecTime` (`0x10af3c`) from BGM, and replaces the logical
|
||||||
|
count when they differ by more than two frames. BGM and SHOT are prepared and
|
||||||
|
started together. If either source is no longer playing, both are stopped,
|
||||||
|
repositioned and restarted; SHOT is also periodically aligned to the BGM time.
|
||||||
|
|
||||||
|
Vectorail now derives gameplay time from consumed BGM source bytes instead of
|
||||||
|
wall time. Its two SDL streams start together, retain their source PCM and are
|
||||||
|
cleared/requeued at the BGM position when their source clocks differ by more
|
||||||
|
than two frames. Corpus verification found 114 of 1674 unique arcade pairs
|
||||||
|
with different PCM frame counts; exhausted SHOT tails remain silent when the
|
||||||
|
BGM position is already beyond the shorter stream.
|
||||||
|
|
||||||
|
## Verification Status
|
||||||
|
|
||||||
|
`opencoaster-stage-verify` currently validates all 2970 main DATs recursively,
|
||||||
|
including the nested `stage/sound/ac_dontfight_ex.dat` and the chart whose
|
||||||
|
actual name ends in `_ext`. It
|
||||||
|
compares the retained wire model to the runtime route, all camera fields, note
|
||||||
|
type/effect/color/timing/distance translation and clip dimensions. It also
|
||||||
|
validates resolved BGM/SHOT containers and PCM formats.
|
||||||
|
|
||||||
|
The final aggregate result is `2970/2970` main charts, 2927 loaded sidecars,
|
||||||
|
18965 ext notes and 18948 linked dual-flicks. Section coverage is 16869 FlowItem
|
||||||
|
keys, 54152 visualizer keys, 79 background image keys, 417451 background color
|
||||||
|
keys and 1171999 stage objects. Audio resolution succeeds for 2918 charts;
|
||||||
|
2768 have paired BGM/SHOT and 150 intentionally resolve only BGM. Gameplay
|
||||||
|
coverage includes 193 MERRY records expanded to 676 timed targets and 6364
|
||||||
|
SLIDE HOLD records.
|
||||||
|
|
||||||
|
Implemented in the player:
|
||||||
|
|
||||||
|
- route timing and draw windows;
|
||||||
|
- camera modes, interpolation and projection;
|
||||||
|
- note heads, directional overlays, long paths, BEAT samples and MERRY layout;
|
||||||
|
- ext timing tables and dual-flick pairing, including both direction vectors;
|
||||||
|
- stage model transforms, visibility, colors and one-level parents;
|
||||||
|
- exact background color cross-fade and BPM brightness;
|
||||||
|
- timed background image selection and external DDS loading;
|
||||||
|
- timed visualizer execution for types 1 through 7;
|
||||||
|
- FlowItem spawning, screen layouts, route-depth unprojection and motion;
|
||||||
|
- MERRY target timing and desktop SLIDE HOLD duration judging;
|
||||||
|
- BGM/SHOT playback, hit SE, source-position game clock and pair resync.
|
||||||
|
|
||||||
|
Remaining asset/input fidelity gaps:
|
||||||
|
|
||||||
|
- the seven visualizer types execute as GLSL procedural equivalents; the
|
||||||
|
original Android vertex generators have not been copied constant-for-constant;
|
||||||
|
- FlowItem timing/layout/motion executes, but original texture selectors
|
||||||
|
34/35 are represented by colored billboards;
|
||||||
|
- mode-3 background loading is implemented, but `stage_back10` is referenced
|
||||||
|
by the corpus and absent from the supplied game dump; built-in modes 1/2
|
||||||
|
still need their original atlas resources;
|
||||||
|
- dual flick and directional SLIDE HOLD use desktop multi-input/hold semantics
|
||||||
|
rather than Android touch lines;
|
||||||
|
- Android touch-line gesture semantics (the desktop player maps controls to
|
||||||
|
keyboard/gamepad inputs).
|
||||||
|
|
||||||
|
This distinction is intentional: a green parser/runtime translation result
|
||||||
|
does not claim pixel-identical procedural meshes or unavailable textures.
|
||||||
+69
-8
@@ -288,9 +288,11 @@ mixed as ordinary scalars.
|
|||||||
### Up vector and roll
|
### Up vector and roll
|
||||||
|
|
||||||
`FUN_005e0ad0` rebuilds `up` by projecting world-up `(0,1,0)` onto the plane
|
`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
|
normal to `target-eye`. The Android `camera3D::BuildUpVector` uses `(0,0,1)`
|
||||||
`(0,0,1)`. `rotationB` then rotates that up vector around the normalized view
|
when the normalized view direction's L1 distance from either Y pole is below
|
||||||
axis.
|
`0.001`; otherwise it uses `(0,1,0)`. `rotationB` then rotates that up vector
|
||||||
|
around the normalized view axis. This is not equivalent to a generic
|
||||||
|
dot-product parallelism threshold on near-vertical authored cameras.
|
||||||
|
|
||||||
### Gameplay projection
|
### Gameplay projection
|
||||||
|
|
||||||
@@ -314,9 +316,13 @@ orthographic sections.
|
|||||||
The distance is not clamped to the near plane. A zero-length camera therefore
|
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
|
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
|
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
|
NaN as `glm::normalize` does. The Linux port mirrors both edge cases.
|
||||||
explicit left-handed view/projection builders; the `NO` depth variant is the
|
|
||||||
OpenGL backend adaptation of the original D3D left-handed matrices.
|
The matrix convention is backend-specific. Arcade emits D3D matrices, while
|
||||||
|
Android's `matrix44::LookAt` uses `eye-target` and its perspective matrix has
|
||||||
|
`-1` at `m[2][3]`, the OpenGL right-handed/no-depth-remap convention. Because
|
||||||
|
Vectorail also renders through OpenGL, its GC path follows the Android RH/NO
|
||||||
|
builders rather than feeding D3D handedness directly to GLM.
|
||||||
|
|
||||||
The evaluator sets `projBlend` to `0` for `projType=0` and `1` for
|
The evaluator sets `projBlend` to `0` for `projType=0` and `1` for
|
||||||
`projType=1`. An `fMode=1` transition linearly interpolates the endpoint
|
`projType=1`. An `fMode=1` transition linearly interpolates the endpoint
|
||||||
@@ -349,11 +355,66 @@ The player now imports the raw camera keys (including non-finite sentinel
|
|||||||
values) and ports the confirmed `aMode`,
|
values) and ports the confirmed `aMode`,
|
||||||
`fMode`, orbit, up/roll, projection type/blend, FOV and clipping-plane
|
`fMode`, orbit, up/roll, projection type/blend, FOV and clipping-plane
|
||||||
behavior. GC cameras are evaluated directly without the legacy follow-camera
|
behavior. GC cameras are evaluated directly without the legacy follow-camera
|
||||||
smoothing. Remaining camera work is validation against captured original
|
smoothing. The runtime uses Android's `a + (b-a)*u` float operation order and
|
||||||
frames.
|
builds the GC view matrix without an extra host-side up-vector fallback.
|
||||||
|
|
||||||
|
An independent verifier reimplements the Android evaluator from decoded wire
|
||||||
|
records instead of calling the player's camera helpers. Across the current
|
||||||
|
2970-chart corpus it checked 171333 keys and 9113837 sampled times, including
|
||||||
|
every track vertex and the interior of every camera-key interval. All seven
|
||||||
|
`aMode` branches and `fMode` 0/1/2 were present:
|
||||||
|
|
||||||
|
```text
|
||||||
|
eye/target max error: 0
|
||||||
|
up max error: 0
|
||||||
|
view max error: 0
|
||||||
|
projection max error: 4.76837e-7
|
||||||
|
```
|
||||||
|
|
||||||
|
This pass found and corrected a sign error in the expanded quaternion's `qz`
|
||||||
|
term and replaced the previous approximate vertical-up threshold with the
|
||||||
|
exact Android `0.001` L1 test.
|
||||||
|
|
||||||
## Switch and Android cross-version validation
|
## Switch and Android cross-version validation
|
||||||
|
|
||||||
|
The Android reference used here comes from
|
||||||
|
`/home/au/Downloads/gc2offlinev4.xapk`. The outer package is an offline
|
||||||
|
installer; the original game is its nested `assets/groovecoaster.apk`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
package: jp.co.taito.groovecoasterzero
|
||||||
|
version: 1.0.18 (versionCode 76)
|
||||||
|
native ABI: arm64-v8a
|
||||||
|
native code: lib/arm64-v8a/libtune.so
|
||||||
|
|
||||||
|
XAPK SHA-256: c6c394f7a1cc65331edc98aac94014668f9d5277ce17262fecf9aeeb1a2a3003
|
||||||
|
nested APK SHA-256: 62c9e739dc9b8c1bcbcb4b5234d78f154b20d1a930329b9c21a3c9236b92a0dc
|
||||||
|
libtune.so SHA-256: 689b2e4c0bc4479a3f309944b5070796878c66c0dfdbd283c11ec0bbeeea9efa
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike the arcade executable, this `libtune.so` retains C++ symbols. Relevant
|
||||||
|
ELF virtual addresses (before Ghidra's `+0x100000` image base) are:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0x0933a0 TuneGameData::LoadGameData(bytearray*, bool)
|
||||||
|
0x0953bc TuneGameData::GetWayPosition(int)
|
||||||
|
0x095588 TuneGameData::GetCameraData(int, bool)
|
||||||
|
0x0ad1a4 GameScene::SetCommonParam()
|
||||||
|
0x0ad524 GameScene::CalcGameProjectionMatrix(camera3D&)
|
||||||
|
0x0b8410 GameScene::CalcGamePerspectiveMatrix(camera3D&)
|
||||||
|
0x0b847c GameScene::CalcGameOrthoMatrix(camera3D&)
|
||||||
|
0x102c78 matrix44::Perspective(float, float, float, float)
|
||||||
|
0x102d34 matrix44::LookAt(vector3 const&, vector3 const&, vector3 const&)
|
||||||
|
0x1031f0 camera3D::BuildUpVector(float)
|
||||||
|
0x103e48 RotateHPB::ToVector_Deg(float)
|
||||||
|
```
|
||||||
|
|
||||||
|
`TuneGameData::LoadGameData` independently confirms every wire read and the
|
||||||
|
59-byte camera record order. It expands each record to an aligned 0x48-byte
|
||||||
|
runtime entry. `GameScene::SetCommonParam` then calls `GetCameraData` for the
|
||||||
|
current chart time, builds the projection, and passes `eye`, `target`, and
|
||||||
|
`up` to `matrix44::LookAt`.
|
||||||
|
|
||||||
The base Switch executable retains `CTuneGameData` RTTI and the original GC
|
The base Switch executable retains `CTuneGameData` RTTI and the original GC
|
||||||
source filenames. Its stripped `CTuneGameData::GetWayPosition` and
|
source filenames. Its stripped `CTuneGameData::GetWayPosition` and
|
||||||
`CTuneGameData::GetCameraData` implementations were matched to the named
|
`CTuneGameData::GetCameraData` implementations were matched to the named
|
||||||
|
|||||||
+40
-26
@@ -47,10 +47,22 @@ generated samples remain uniform over corners; it is not the normal route
|
|||||||
renderer.
|
renderer.
|
||||||
|
|
||||||
The player now mirrors `DrawWay`: it uploads the original authored points plus
|
The player now mirrors `DrawWay`: it uploads the original authored points plus
|
||||||
the two exact timestamp intersections and draws separate behind/current and
|
the two exact timestamp intersections and draws separate past/current and
|
||||||
current/ahead line strips with endpoint color gradients. The stage values
|
current/future line strips with endpoint color gradients. Android
|
||||||
`backwardsDrawDist` and `forwardDrawDist` behave as seconds and are converted
|
`DrawGameStageCharacter` proves that the active `forwardDrawDist` value is a
|
||||||
to milliseconds before comparison with track and note timestamps.
|
count of beats, not seconds. If `beat_ms = 60000 / bpm`, it computes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
beat_start = floor(current_ms / beat_ms) * beat_ms
|
||||||
|
future_ms = forwardDrawDist * beat_ms
|
||||||
|
first_ms = max(0, beat_start - max(beat_ms, future_ms))
|
||||||
|
last_ms = min(song_end, beat_start + future_ms)
|
||||||
|
```
|
||||||
|
|
||||||
|
The same active value drives both sides, but only the past side has a minimum
|
||||||
|
one-beat span. Thus a dynamic value of zero removes the future route while
|
||||||
|
retaining one past beat. The first `backwardsDrawDist` config float is retained
|
||||||
|
while parsing but is not consumed by this normal Android rendering path.
|
||||||
|
|
||||||
Dynamic `TrackDrawDist` records are step changes, not interpolation keys. The
|
Dynamic `TrackDrawDist` records are step changes, not interpolation keys. The
|
||||||
StageConfig forward distance remains active until the timestamp of the first
|
StageConfig forward distance remains active until the timestamp of the first
|
||||||
@@ -64,18 +76,22 @@ For `ac_10pt8tion_hard.dat`:
|
|||||||
```text
|
```text
|
||||||
first track key: 0 ms, (0, 0, 0)
|
first track key: 0 ms, (0, 0, 0)
|
||||||
second track key: 6486 ms, (0, 0, 199.985)
|
second track key: 6486 ms, (0, 0, 199.985)
|
||||||
draw behind: 10 s
|
first range field: 10
|
||||||
draw ahead: 7 s
|
active range: 7 beats
|
||||||
```
|
```
|
||||||
|
|
||||||
Treating 7/10 as world units collapses the visible rail to a tiny fraction of
|
The rail brightness is also chart-clock driven. Over a two-beat triangle wave,
|
||||||
the first segment; timestamp clipping produces the expected visible range.
|
`pulse = triangle * 0.6 + 0.4`; the past segment grades from `0.7*pulse` to
|
||||||
|
`pulse` alpha and the future segment from `pulse` to `0.5*pulse`. Both are
|
||||||
|
submitted with additive blending.
|
||||||
|
|
||||||
## Track colors
|
## Track colors
|
||||||
|
|
||||||
The stage config stores two RGBA colors directly after the draw-range values.
|
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
|
Android uses the first for the past/current call and the second for the
|
||||||
`10pt8tion_hard` they are `(255,0,128)` ahead and `(255,255,255)` behind.
|
current/future call. For `10pt8tion_hard` they are `(255,0,128)` and
|
||||||
|
`(255,255,255)` respectively; the older ahead/behind field names were inferred
|
||||||
|
backwards.
|
||||||
|
|
||||||
## Base background
|
## Base background
|
||||||
|
|
||||||
@@ -88,15 +104,14 @@ rgba top_left
|
|||||||
rgba bottom_right
|
rgba bottom_right
|
||||||
rgba bottom_left
|
rgba bottom_left
|
||||||
u8 interpolate_to_next
|
u8 interpolate_to_next
|
||||||
u8 audio_reactive_color
|
u8 rhythm_reactive_color
|
||||||
```
|
```
|
||||||
|
|
||||||
`FUN_00642390` holds the active colors unless `interpolate_to_next` is set; in
|
`FUN_00642390` holds the active colors unless `interpolate_to_next` is set.
|
||||||
that case it linearly interpolates all four RGBA values to the following key.
|
The transition uses `CalcCrossFadeColor`'s 25% overlap rather than a plain
|
||||||
`audio_reactive_color` applies `FUN_005d9650` to each active color using the
|
linear mix. `rhythm_reactive_color` multiplies HSV brightness by a 0.5..0.75
|
||||||
runtime analyser value. The Linux player now implements the exact hold versus
|
triangle wave derived from chart time and the active BPM in `SetCommonParam`;
|
||||||
interpolate selection and keeps the second mode at its neutral color factor
|
it is not sourced from an audio analyser. The Linux player implements both.
|
||||||
until the analyser feeding `stage renderer +0x24` is ported.
|
|
||||||
|
|
||||||
`data/stage/2d/<song>_menu.dds` is not the gameplay background. It is a
|
`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
|
512x256 UI atlas whose top-left 197x197 cell is the song jacket. The player has
|
||||||
@@ -105,7 +120,7 @@ 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
|
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.
|
stage-authored color table before particles, visualizers and objects are drawn.
|
||||||
|
|
||||||
The remaining original scene is produced by the stage `particles`,
|
The remaining original scene is produced by the stage FlowItem,
|
||||||
`visualizer`, and `objects` sections (plus `.tumo` models), not by a single
|
`visualizer`, and `objects` sections (plus `.tumo` models), not by a single
|
||||||
background bitmap. These sections are now decoded completely by
|
background bitmap. These sections are now decoded completely by
|
||||||
`StagePattern`: particle records are 44 bytes, visualizer records are 12 bytes,
|
`StagePattern`: particle records are 44 bytes, visualizer records are 12 bytes,
|
||||||
@@ -115,14 +130,13 @@ 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
|
contains 2, 28, 37 and 352 respectively. The Vectorail level data retains this
|
||||||
decoded scene for the model-rendering pass.
|
decoded scene for the model-rendering pass.
|
||||||
|
|
||||||
The PSP package now retains all three timelines. The particle constructor at
|
Android `GameScene::ExecFlowItem` scales both `repeatMeasure` and
|
||||||
`FUN_005f0940` creates a 64-instance pool for every configured particle key.
|
`lifespanMeasure` by the active beat duration. Recovered spawn layouts are:
|
||||||
`FUN_005f0130` scales both `repeatMeasure` and `lifespanMeasure` by the active
|
type 1, a random screen point; type 2, a screen/grid group using
|
||||||
beat duration. Recovered spawn layouts are: type 1, a deterministic/random
|
`groupShapeSize`; type 3, eight points at 45-degree intervals around a circle.
|
||||||
point; type 2, a screen/grid group using `groupShapeSize`; type 3, six points
|
The player follows those timing/layout rules, unprojects the points to route
|
||||||
at 60-degree intervals around a circle. The PSP implementation follows these
|
depth and applies the authored velocity, but substitutes colored billboards
|
||||||
timing and layout rules, but substitutes geometry for the original particle
|
for original texture selectors 34/35.
|
||||||
texture resource until that resource binding is mapped.
|
|
||||||
|
|
||||||
The common `.tumo` container is also big-endian. Its outer count is followed,
|
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,
|
for each mesh, by resource names, an XYZ vertex table, eight bound floats,
|
||||||
|
|||||||
+18
-1
@@ -186,6 +186,21 @@ struct VisualizerArray {
|
|||||||
Visualizer entries[sz];
|
Visualizer entries[sz];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Background Images
|
||||||
|
struct BackgroundImage {
|
||||||
|
u32 timeMs;
|
||||||
|
u32 mode;
|
||||||
|
u32 atlasIndex;
|
||||||
|
s32 textureIndex;
|
||||||
|
color color;
|
||||||
|
} [[same_color]];
|
||||||
|
struct BackgroundImageArray {
|
||||||
|
u32 textureCount;
|
||||||
|
string8 textureNames[textureCount];
|
||||||
|
u32 sz;
|
||||||
|
BackgroundImage entries[sz];
|
||||||
|
};
|
||||||
|
|
||||||
// Color Table 1
|
// Color Table 1
|
||||||
struct ColorTable {
|
struct ColorTable {
|
||||||
u32 timeMs;
|
u32 timeMs;
|
||||||
@@ -327,6 +342,8 @@ NoteArray notes @ hdr.notes;
|
|||||||
CameraArray camera @ hdr.camera;
|
CameraArray camera @ hdr.camera;
|
||||||
ParticleArray particles @ hdr.particles;
|
ParticleArray particles @ hdr.particles;
|
||||||
VisualizerArray visualizer @ hdr.visualizer;
|
VisualizerArray visualizer @ hdr.visualizer;
|
||||||
|
BackgroundImageArray backgroundImages @ hdr.unk1;
|
||||||
ColorTableArray colorTable @ hdr.colors;
|
ColorTableArray colorTable @ hdr.colors;
|
||||||
ObjectArray objects @ hdr.objects;
|
ObjectArray objects @ hdr.objects;
|
||||||
ColorTable2Array colorTable2 @ hdr.colors2;
|
// Header word 11 points to offset slot 2, one word into this table.
|
||||||
|
ColorTable2Array colorTable2 @ hdr.colors2 - 4;
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Groove Coaster Android/arcade *_ext.dat
|
||||||
|
// LoadExtData starts reading at byte 6. Bytes after notes are not consumed by
|
||||||
|
// the recovered Android routine and are intentionally left untyped.
|
||||||
|
|
||||||
|
struct NoteTimingEntry {
|
||||||
|
u32 timeMs;
|
||||||
|
u32 mode;
|
||||||
|
float value;
|
||||||
|
} [[single_color]];
|
||||||
|
struct NoteTimingList {
|
||||||
|
u16 size;
|
||||||
|
NoteTimingEntry entries[size];
|
||||||
|
} [[single_color]];
|
||||||
|
|
||||||
|
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 StageExtension {
|
||||||
|
NoteTimingList noteTimings[4];
|
||||||
|
u32 unknown;
|
||||||
|
u32 noteCount;
|
||||||
|
Note notes[noteCount];
|
||||||
|
};
|
||||||
|
|
||||||
|
u8 prefix[6] @ 0x00;
|
||||||
|
StageExtension extension @ 0x06;
|
||||||
+7
-7
@@ -347,7 +347,7 @@ PackageBackgroundColorPoint packBackgroundColor(const gc::BackgroundColorPoint&
|
|||||||
output.bottomRightRgba = rgba(source.bottomRight);
|
output.bottomRightRgba = rgba(source.bottomRight);
|
||||||
output.bottomLeftRgba = rgba(source.bottomLeft);
|
output.bottomLeftRgba = rgba(source.bottomLeft);
|
||||||
output.flags = (source.interpolateToNext ? 1u : 0u) |
|
output.flags = (source.interpolateToNext ? 1u : 0u) |
|
||||||
(source.audioReactive ? 2u : 0u);
|
(source.rhythmReactive ? 2u : 0u);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,8 +373,8 @@ PackageVisibilityKey packVisibility(const gc::VisibilityPoint& source) {
|
|||||||
PackageVisibilityKey output{};
|
PackageVisibilityKey output{};
|
||||||
output.timeMs = source.timeMs;
|
output.timeMs = source.timeMs;
|
||||||
output.flags = static_cast<std::uint8_t>(
|
output.flags = static_cast<std::uint8_t>(
|
||||||
(source.fadeOut ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
(source.repeat ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
||||||
(source.fadeIn ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
(source.interpolate ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
||||||
output.visible = source.visible ? 1u : 0u;
|
output.visible = source.visible ? 1u : 0u;
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -383,8 +383,8 @@ PackageTransformKey packTransform(const gc::TransformPoint& source) {
|
|||||||
PackageTransformKey output{};
|
PackageTransformKey output{};
|
||||||
output.timeMs = source.timeMs;
|
output.timeMs = source.timeMs;
|
||||||
output.flags = static_cast<std::uint8_t>(
|
output.flags = static_cast<std::uint8_t>(
|
||||||
(source.tweenTowards ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
(source.repeat ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
||||||
(source.tweenAway ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
(source.interpolate ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
||||||
std::copy(std::begin(source.value), std::end(source.value), output.value);
|
std::copy(std::begin(source.value), std::end(source.value), output.value);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -393,8 +393,8 @@ PackageObjectColorKey packObjectColor(const gc::ObjectColorPoint& source) {
|
|||||||
PackageObjectColorKey output{};
|
PackageObjectColorKey output{};
|
||||||
output.timeMs = source.timeMs;
|
output.timeMs = source.timeMs;
|
||||||
output.flags = static_cast<std::uint8_t>(
|
output.flags = static_cast<std::uint8_t>(
|
||||||
(source.tweenTowards ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
(source.repeat ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
||||||
(source.tweenAway ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
(source.interpolate ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
||||||
output.rgba = rgba(source.color);
|
output.rgba = rgba(source.color);
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// GhidraScript: DecompileBySymbol.java
|
||||||
|
// Usage (headless):
|
||||||
|
// analyzeHeadless <projDir> <projName> -process <program> -noanalysis -readOnly \
|
||||||
|
// -scriptPath <path> -postScript DecompileBySymbol.java <name-fragment>...
|
||||||
|
|
||||||
|
import ghidra.app.decompiler.DecompInterface;
|
||||||
|
import ghidra.app.decompiler.DecompileResults;
|
||||||
|
import ghidra.app.script.GhidraScript;
|
||||||
|
import ghidra.program.model.listing.Function;
|
||||||
|
import ghidra.program.model.listing.FunctionIterator;
|
||||||
|
|
||||||
|
public class DecompileBySymbol extends GhidraScript {
|
||||||
|
@Override
|
||||||
|
public void run() throws Exception {
|
||||||
|
String[] args = getScriptArgs();
|
||||||
|
if (args == null || args.length == 0) {
|
||||||
|
printerr("DecompileBySymbol: needs one or more symbol-name fragments");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DecompInterface decomp = new DecompInterface();
|
||||||
|
decomp.openProgram(currentProgram);
|
||||||
|
|
||||||
|
for (String query : args) {
|
||||||
|
boolean found = false;
|
||||||
|
FunctionIterator functions = currentProgram.getFunctionManager().getFunctions(true);
|
||||||
|
while (functions.hasNext()) {
|
||||||
|
Function function = functions.next();
|
||||||
|
String name = function.getName(true);
|
||||||
|
if (!name.contains(query)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
found = true;
|
||||||
|
println("================================================================================");
|
||||||
|
println("query: " + query);
|
||||||
|
println("function: " + name + " @ " + function.getEntryPoint());
|
||||||
|
|
||||||
|
DecompileResults result = decomp.decompileFunction(function, 60, monitor);
|
||||||
|
if (!result.decompileCompleted()) {
|
||||||
|
println("(decompile failed)");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
println(result.getDecompiledFunction().getC());
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
println("No function matched: " + query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,9 @@ def main() -> int:
|
|||||||
override_count = 0
|
override_count = 0
|
||||||
overrides = collections.Counter()
|
overrides = collections.Counter()
|
||||||
note_count = 0
|
note_count = 0
|
||||||
|
fly_in_count = 0
|
||||||
|
fly_trail_count = 0
|
||||||
|
fly_bounce_count = 0
|
||||||
samples = {}
|
samples = {}
|
||||||
failures = []
|
failures = []
|
||||||
for path in paths:
|
for path in paths:
|
||||||
@@ -89,6 +92,12 @@ def main() -> int:
|
|||||||
if note[2] != 0:
|
if note[2] != 0:
|
||||||
overrides[(raw_type, note[2])] += 1
|
overrides[(raw_type, note[2])] += 1
|
||||||
samples.setdefault(raw_type, (path, note))
|
samples.setdefault(raw_type, (path, note))
|
||||||
|
flag37 = note[16]
|
||||||
|
flag38 = note[17]
|
||||||
|
cycles = note[24]
|
||||||
|
fly_in_count += flag37 != 0
|
||||||
|
fly_trail_count += flag38 != 0
|
||||||
|
fly_bounce_count += flag37 != 0 and cycles != 0
|
||||||
|
|
||||||
print(f"record_size={NOTE.size}")
|
print(f"record_size={NOTE.size}")
|
||||||
print(f"files={len(paths)} valid={len(paths) - len(failures)} invalid={len(failures)} notes={note_count}")
|
print(f"files={len(paths)} valid={len(paths) - len(failures)} invalid={len(failures)} notes={note_count}")
|
||||||
@@ -98,6 +107,7 @@ def main() -> int:
|
|||||||
for key, value in sorted(types.items())
|
for key, value in sorted(types.items())
|
||||||
))
|
))
|
||||||
print(f"type_override_nonzero={override_count}")
|
print(f"type_override_nonzero={override_count}")
|
||||||
|
print(f"fly_in={fly_in_count} fly_trail={fly_trail_count} fly_bounce={fly_bounce_count}")
|
||||||
print("overrides=" + " ".join(
|
print("overrides=" + " ".join(
|
||||||
f"0x{raw:02x}/0x{override:02x}:{count}"
|
f"0x{raw:02x}/0x{override:02x}:{count}"
|
||||||
for (raw, override), count in sorted(overrides.items())
|
for (raw, override), count in sorted(overrides.items())
|
||||||
|
|||||||
Reference in New Issue
Block a user