Initial Vectorail GC format library

This commit is contained in:
2026-08-02 17:05:27 +02:00
commit 8c4e1bcacb
28 changed files with 3426 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
/build/
/cmake-build-*/
/.cache/
/.clangd/
/compile_commands.json
*.o
*.a
*.so
*.dll
*.dylib
*.exe
+93
View File
@@ -0,0 +1,93 @@
cmake_minimum_required(VERSION 3.20)
project(vectorail-gc VERSION 0.1.0 LANGUAGES CXX)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
option(VECTORAIL_GC_BUILD_TOOLS "Build format inspection tools" ON)
find_package(glm CONFIG REQUIRED)
add_library(vectorail-gc
src/EventStream.cpp
src/MtxArchive.cpp
src/RvbLayout.cpp
src/RvbScene.cpp
src/StageCatalog.cpp
src/StageDat.cpp
src/StagePattern.cpp
src/TumoModel.cpp
)
add_library(Vectorail::GC ALIAS vectorail-gc)
target_compile_features(vectorail-gc PUBLIC cxx_std_17)
target_include_directories(vectorail-gc
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
set_target_properties(vectorail-gc PROPERTIES
EXPORT_NAME GC
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
add_library(vectorail-gc-effects src/GcTargetEffect.cpp)
add_library(Vectorail::GCEffects ALIAS vectorail-gc-effects)
target_compile_features(vectorail-gc-effects PUBLIC cxx_std_17)
target_include_directories(vectorail-gc-effects
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(vectorail-gc-effects PUBLIC glm::glm)
set_target_properties(vectorail-gc-effects PROPERTIES
EXPORT_NAME GCEffects
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
)
if(VECTORAIL_GC_BUILD_TOOLS)
add_executable(vectorail-gc-stage-probe tools/stage_probe.cpp)
target_link_libraries(vectorail-gc-stage-probe PRIVATE Vectorail::GC)
add_executable(vectorail-gc-rvb-probe tools/rvb_probe.cpp)
target_link_libraries(vectorail-gc-rvb-probe PRIVATE Vectorail::GC)
add_executable(vectorail-gc-mtx-probe tools/mtx_probe.cpp)
target_link_libraries(vectorail-gc-mtx-probe PRIVATE Vectorail::GC)
add_executable(vectorail-gc-effect-probe tools/effect_probe.cpp)
target_link_libraries(vectorail-gc-effect-probe PRIVATE Vectorail::GCEffects)
endif()
install(TARGETS vectorail-gc vectorail-gc-effects
EXPORT VectorailGCTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT VectorailGCTargets
FILE VectorailGCTargets.cmake
NAMESPACE Vectorail::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC
)
configure_package_config_file(
cmake/VectorailGCConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC
)
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/VectorailGCConfigVersion.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/VectorailGC
)
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026 Kiyooru Takasaki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+30
View File
@@ -0,0 +1,30 @@
# Vectorail GC
Headless parsers and evaluators for Groove Coaster interoperability. The
library has no windowing, rendering, audio, input, or gameplay dependencies.
## Formats
- arcade stage `.dat` streams and note opcodes;
- stage catalog and pattern data;
- `.tumo` model geometry;
- MTX texture archives;
- RVB scenes and layout snapshots;
- effect and UV animation data.
No game files are distributed with this repository. Applications must point
the library at data obtained from their own installation or hardware dump.
## Build
```sh
cmake -S . -B build
cmake --build build
cmake --install build --prefix /desired/prefix
```
Consumers use `Vectorail::GC` for the base parsers and
`Vectorail::GCEffects` for the GLM-based effect evaluator.
Vectorail GC is distributed under the MIT License.
+7
View File
@@ -0,0 +1,7 @@
@PACKAGE_INIT@
include(CMakeFindDependencyMacro)
find_dependency(glm CONFIG)
include("${CMAKE_CURRENT_LIST_DIR}/VectorailGCTargets.cmake")
+46
View File
@@ -0,0 +1,46 @@
#ifndef OPENROLLER_GC_EVENTSTREAM_HPP
#define OPENROLLER_GC_EVENTSTREAM_HPP
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
struct GameEvent {
uint16_t id = 0;
float timestamp = 0.0f;
uint16_t a = 0; // unknown (often 0)
uint32_t type = 0;
float value = 0.0f;
uint16_t b = 0; // unknown (often 0)
};
struct EventStreamDecodeResult {
size_t recordSize = 16; // 16 or 12 (for now)
int alignment = 0; // best alignment within [0,recordSize-1]
size_t eventCount = 0; // decoded event count at best alignment (bounded)
double padZeroRatio = 0.0; // fraction of padding fields that are zero (0..1)
int score = 0; // heuristic score for ranking
};
EventStreamDecodeResult TryDecodeEventStream(const std::vector<uint8_t>& bytes, size_t start, size_t end);
// Like TryDecodeEventStream, but forces recordSize=12 or 16.
EventStreamDecodeResult TryDecodeEventStreamFixed(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t recordSize);
// Decodes using a fixed record size (big endian).
// recordSize=16: u16 id, f32 timestamp, u16 a, u16 type, f32 value, u16 b
// recordSize=12: f32 timestamp, u32 type, f32 value
bool DecodeEventStream(
const std::vector<uint8_t>& bytes,
size_t start,
size_t end,
size_t recordSize,
std::vector<GameEvent>* out,
std::string* err);
} // namespace gc
#endif
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include <glm/glm.hpp>
struct GcUvCell {
uint16_t x = 0;
uint16_t y = 0;
uint16_t width = 0;
uint16_t height = 0;
};
struct GcEffectSprite {
uint16_t uvRecord = 0;
int frame = 0;
glm::vec3 offsetPixels{0.0f};
glm::vec4 color{1.0f};
glm::vec2 scale{1.0f};
float rotationDegrees = 0.0f;
};
// Interpreter for the sprite subset of the original common effect format.
// It follows game471 FUN_005f2030/FUN_005f2250/FUN_005f23f0 and is enough to
// reproduce control-helper effects 61..69, including parent rotation nodes.
class GcTargetEffectBank {
public:
bool load(const std::string& efcPath, const std::string& uvPath, std::string* error = nullptr);
int lifetime(int effectId) const;
std::vector<GcEffectSprite> evaluate(int effectId, float tick, int uvRecordBase = 0) const;
const GcUvCell* uvCell(uint16_t record, int frame) const;
int uvTexture(uint16_t record) const;
private:
struct Key {
uint16_t time = 0;
uint8_t interpolation = 0;
std::vector<float> values;
};
struct Track {
uint16_t loopStart = 0;
uint16_t loopEnd = 0;
std::vector<Key> keys;
};
struct Child {
uint8_t type = 0;
uint16_t reference = 0xffff;
bool inheritParent = false;
std::array<Track, 5> tracks;
};
struct Effect {
uint16_t lifetime = 0;
std::vector<Child> children;
};
struct UvRecord {
int16_t textureIndex = -1;
std::vector<GcUvCell> cells;
};
static std::vector<float> sampleTrack(const Track& track, float tick, bool* started);
std::vector<Effect> effects_;
std::vector<UvRecord> uvRecords_;
};
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
struct MtxTexture {
uint32_t offset = 0;
uint32_t size = 0;
uint32_t width = 0;
uint32_t height = 0;
uint32_t bitsPerPixel = 0;
uint32_t fourCC = 0;
};
struct MtxArchive {
std::vector<MtxTexture> textures;
};
bool ParseMtxArchive(const std::vector<uint8_t>& bytes,
MtxArchive* archive,
std::string* error = nullptr);
// MTX stores a normal DDS surface with its four-byte magic replaced by an
// internal field. This returns the exact embedded surface with "DDS " put
// back, suitable for the existing DDS loader or external inspection tools.
bool ExtractMtxTextureDds(const std::vector<uint8_t>& bytes,
const MtxTexture& texture,
std::vector<uint8_t>* dds,
std::string* error = nullptr);
} // namespace gc
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
namespace gc {
// NOTE: these IDs are based on static analysis of a limited sample set and may
// need adjustments as we decode more charts.
enum class NoteType : uint32_t {
// --- System / Timing ---
BpmChange = 0x00000000, // Value = BPM? Or a timing/speed scalar.
Camera = 0x00000002, // Camera control (hypothesis)
MeasureLine = 0x00000004, // Measure start / marker (hypothesis)
BeatLine = 0x0000003C, // (60) Visual beat grid line (hypothesis)
// --- Basic Notes ---
Tap = 0x00000029, // (41) HIT: regular tap (hypothesis)
Critical = 0x0000002A, // (42) CRITICAL: double tap / star (hypothesis)
// --- Holds ---
HoldStart = 0x0000002B, // (43) HOLD start (hypothesis)
HoldEnd = 0x0000002C, // (44) HOLD end (hypothesis)
DualHold = 0x0000002F, // (47) Dual hold (hypothesis)
// --- Slides (Value = Angle in Radians) ---
Slide = 0x0000002D, // (45) Slide (hypothesis)
DualSlide = 0x0000002E, // (46) Dual slide (hypothesis)
SlideHold = 0x00000030, // (48) Slide + hold? (hypothesis)
// --- Special / Legacy (seen in 10pt8tion) ---
Legacy_Tap = 0x00000100,
Legacy_Slide = 0x00000A00,
// --- Unknown ---
Unknown = 0xFFFFFFFFu,
};
inline bool IsNote(uint32_t type) {
return (type >= 0x29u && type <= 0x30u) || type == 0x100u || type == 0xA00u;
}
} // namespace gc
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include "gc/RvbScene.hpp"
#include <array>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
namespace gc {
struct RvbImageDraw {
std::string imageSymbol;
std::string instancePath;
// top-left, top-right, bottom-left, bottom-right in the RVB canvas
std::array<std::array<float, 2>, 4> corners{};
std::array<float, 3> color{1.0f, 1.0f, 1.0f};
float alpha = 1.0f;
uint32_t depth = 0;
};
// A Flash-style MovieClip path mapped to the labeled frame that should be
// visible. Paths are the same absolute paths exported in PREP ("/" is the
// root timeline). Clips not mentioned here remain on their first FRAM.
struct RvbSnapshotState {
std::unordered_map<std::string, std::string> frameByPath;
bool includeRootOther = false;
};
// Builds the display list produced by the first frame of the root timeline
// and the first frame of every placed MovieClip. This is the exact static
// base of the original scene; later timeline frames and ActionScript state
// changes can be layered on top as their opcodes are recovered.
bool BuildRvbInitialSnapshot(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
std::vector<RvbImageDraw>* draws,
std::string* error = nullptr);
// Builds the display list at selected labeled frames. RVB FRAM records are
// deltas, so this applies PLC3/RMOV commands in order through each requested
// frame instead of treating a frame as a self-contained draw list.
bool BuildRvbSnapshot(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
const RvbSnapshotState& state,
std::vector<RvbImageDraw>* draws,
std::string* error = nullptr);
// Builds an exported/linkage MovieClip without requiring it to be placed on
// the root timeline. game471.exe uses this path for the twelve dynamically
// instantiated select-music rows (mc_music_link / mc_index_link).
bool BuildRvbSymbolSnapshot(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
const std::string& symbolName,
const RvbSnapshotState& state,
std::vector<RvbImageDraw>* draws,
std::string* error = nullptr);
} // namespace gc
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
struct RvbChunk {
std::string tag;
uint32_t offset = 0;
uint32_t size = 0;
};
// A PREP entry binds an animation/action name to an instance path in the
// exported menu scene. These names are what game471.exe uses to drive the
// select-music and difficulty state machines.
struct RvbBinding {
std::string action;
std::string instancePath;
};
struct RvbImageResource {
std::string fileName;
std::string symbolName;
uint32_t width = 0;
uint32_t height = 0;
};
struct RvbExport {
std::string definitionName;
std::string linkageName;
};
struct RvbNode {
std::string tag;
uint32_t offset = 0;
uint32_t size = 0;
uint32_t localDataOffset = 0;
uint32_t localDataSize = 0;
std::vector<RvbNode> children;
};
struct RvbScene {
uint8_t framesPerSecond = 0;
uint16_t sourceWidth = 0;
uint16_t sourceHeight = 0;
std::vector<RvbChunk> chunks;
std::vector<RvbBinding> bindings;
std::vector<RvbImageResource> images;
std::vector<RvbExport> exports;
std::vector<RvbNode> roots;
};
bool ParseRvbScene(const std::vector<uint8_t>& bytes,
RvbScene* scene,
std::string* error = nullptr);
} // namespace gc
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
// One 0xb4-byte runtime entry created by game471.exe's stage_param.dat loader.
// Strings in the file are byte-length-prefixed and retain the game's source
// encoding (mostly CP932 for localized text).
struct StageCatalogEntry {
uint32_t id = 0;
std::string title;
std::string imageKey;
std::string artist;
std::string source;
std::string sortKey;
uint8_t genre = 0;
std::string duration;
std::array<uint8_t, 4> difficultyRatings{};
std::string bpm;
// Per-difficulty percentage levels passed to LoadStageBGM for the two
// continuously synchronized stage stems.
std::array<uint8_t, 4> bgmVolumes{};
std::array<uint8_t, 4> shotVolumes{};
std::array<uint32_t, 3> timingValues{};
std::array<uint8_t, 2> unknown60{};
std::string bgmBase;
std::array<std::string, 4> chartGroup0{};
std::array<std::string, 4> chartSuffixes{};
std::array<std::string, 4> chartIds{};
std::string unknown9c;
uint32_t unknownA0 = 0;
std::array<uint8_t, 2> unknownA4{};
std::string unknownAc;
uint8_t unknownB0 = 0;
};
bool ParseStageCatalog(const std::vector<uint8_t>& bytes,
std::vector<StageCatalogEntry>* entries,
std::string* error = nullptr);
const StageCatalogEntry* FindStageCatalogEntryByChart(
const std::vector<StageCatalogEntry>& entries,
const std::string& chartId);
} // namespace gc
+30
View File
@@ -0,0 +1,30 @@
#ifndef OPENROLLER_GC_STAGEDAT_HPP
#define OPENROLLER_GC_STAGEDAT_HPP
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
struct Section {
size_t start = 0;
size_t end = 0;
};
struct StageDat {
std::vector<uint8_t> bytes;
uint32_t headerSize = 0;
std::vector<uint32_t> headerWords;
std::vector<size_t> offsets; // sorted unique offsets within file
std::vector<Section> sections; // derived from offsets
static bool LoadFromFile(const std::string& path, StageDat& out, std::string* err);
};
} // namespace gc
#endif
+234
View File
@@ -0,0 +1,234 @@
#ifndef OPENROLLER_GC_STAGEPATTERN_HPP
#define OPENROLLER_GC_STAGEPATTERN_HPP
#include "gc/StageDat.hpp"
#include <array>
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
struct Color {
uint8_t r = 0;
uint8_t g = 0;
uint8_t b = 0;
uint8_t a = 0;
};
struct StageHeader {
uint32_t stageCfg = 0;
uint32_t trackDrawDist = 0;
uint32_t track = 0;
uint32_t notes = 0;
uint32_t camera = 0;
uint32_t particles = 0;
uint32_t visualizer = 0;
uint32_t unk1 = 0;
uint32_t colors = 0;
uint32_t objects = 0;
uint32_t unk2 = 0;
uint32_t colors2 = 0;
uint32_t unk3 = 0;
};
struct BpmChange {
uint32_t timeMs = 0;
uint32_t bpm = 0;
};
struct NoteSetting {
uint32_t timeMs = 0;
// TuneTimingData::GetTime: 1 is an absolute millisecond value, 3 uses
// the spacing to the next note, and every other mode scales by beat_ms.
uint32_t mode = 0;
float value = 0.0f;
};
struct StageConfig {
float endTime1 = 0.0f;
float endTime2 = 0.0f;
float outroTime = 0.0f;
std::vector<BpmChange> bpmChanges;
std::array<std::vector<NoteSetting>, 4> noteSettings;
std::string chartName;
std::string chartName2;
std::string bgmName;
std::string shotName;
float backwardsDrawDist = 0.0f;
float forwardDrawDist = 0.0f;
Color trackAheadColor;
Color trackBehindColor;
uint8_t audioOffset = 0;
float visualOffset = 0.0f;
Color unkColor;
};
struct DrawDistancePoint {
uint32_t timeMs = 0;
float distance = 0.0f;
};
struct TrackPiece {
uint32_t timeMs = 0;
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
};
// Type byte used by the original arcade stage-note record. Keep this
// distinct from the legacy editor-side NoteType in include/gc/NoteTypes.hpp,
// whose numeric IDs describe another interchange format.
enum class StageNoteType : uint8_t {
None = 0,
Normal = 1,
Flick = 2,
Hold = 3,
Scratch = 4,
Beat = 5,
MerryGoRound = 6,
Hidden = 7,
Hidden2 = 8,
Critical = 9,
SlideHold = 10,
SlideCounter = 11,
Turn = 12,
Spin = 13,
Finish = 14,
DualHold = 15,
};
const char* NoteTypeName(uint8_t rawType);
// Wire layout recovered from game471.exe: FUN_005ea800 reads exactly 99 bytes
// per note. Field semantics remain neutral until their consumers are mapped.
struct StageNote {
static constexpr size_t kRecordSize = 99;
uint32_t timeMs = 0;
StageNoteType type = StageNoteType::None;
uint8_t typeOverride = 0; // original runtime forces type=1 when non-zero
std::array<int16_t, 9> params16{};
uint8_t flag24 = 0;
std::array<float, 3> params25{};
uint8_t flag37 = 0;
uint8_t flag38 = 0;
std::array<float, 4> params39{};
std::array<uint32_t, 3> params55{};
float param67 = 0.0f;
uint32_t param71 = 0;
std::array<float, 5> params75{};
uint32_t param95 = 0;
};
struct CameraPoint {
uint32_t timeMs = 0;
uint8_t aMode = 0;
uint8_t fMode = 0;
float dist = 0.0f;
float rotationA[2] = {0.0f, 0.0f};
float originOff[3] = {0.0f, 0.0f, 0.0f};
uint8_t projType = 0;
float fieldFar[3] = {0.0f, 0.0f, 0.0f};
float fieldNear[3] = {0.0f, 0.0f, 0.0f};
float rotationB = 0.0f;
};
struct ParticlePoint {
static constexpr size_t kRecordSize = 44;
uint32_t timeMs = 0;
uint32_t enabled = 0;
uint32_t shape = 0;
uint32_t texture = 0;
Color color;
float velocity[3] = {0.0f, 0.0f, 0.0f};
float repeatMeasure = 0.0f;
float lifespanMeasure = 0.0f;
uint32_t groupShapeSize = 0;
};
struct VisualizerPoint {
static constexpr size_t kRecordSize = 12;
uint32_t timeMs = 0;
uint32_t type = 0;
Color color;
};
struct BackgroundColorPoint {
static constexpr size_t kRecordSize = 22;
uint32_t timeMs = 0;
Color topRight;
Color topLeft;
Color bottomRight;
Color bottomLeft;
bool interpolateToNext = false;
bool audioReactive = false;
};
struct VisibilityPoint {
uint32_t timeMs = 0;
bool fadeOut = false;
bool fadeIn = false;
bool visible = false;
};
struct TransformPoint {
uint32_t timeMs = 0;
bool tweenTowards = false;
bool tweenAway = false;
float value[3] = {0.0f, 0.0f, 0.0f};
};
struct ObjectColorPoint {
uint32_t timeMs = 0;
bool tweenTowards = false;
bool tweenAway = false;
Color color;
};
struct StageObject {
uint32_t model = 0;
uint32_t fragmentShader = 0;
bool wireframe = false;
bool flashing = false;
bool unknownFlag = false;
float position[3] = {0.0f, 0.0f, 0.0f};
float scale[3] = {1.0f, 1.0f, 1.0f};
float rotation[3] = {0.0f, 0.0f, 0.0f};
float color[4] = {1.0f, 1.0f, 1.0f, 1.0f};
float unknownVector[3] = {0.0f, 0.0f, 0.0f};
// game471 supplemental object table (stage format > 0x29ce). A child is
// rendered with parent * child transform and component-wise parent color.
int32_t parentIndex = -1;
std::vector<VisibilityPoint> visibility;
std::vector<TransformPoint> movement;
std::vector<TransformPoint> scaling;
std::vector<TransformPoint> rotations;
std::vector<ObjectColorPoint> colorChanges;
};
struct ParsedStagePattern {
StageHeader header;
StageConfig config;
std::vector<DrawDistancePoint> drawDistances;
std::vector<TrackPiece> track;
std::vector<std::string> noteNames;
std::vector<StageNote> notes;
std::vector<CameraPoint> cameras;
std::vector<ParticlePoint> particles;
std::vector<VisualizerPoint> visualizer;
std::vector<BackgroundColorPoint> backgroundColors;
std::vector<std::string> modelNames;
std::vector<std::string> fragmentShaderNames;
std::vector<StageObject> objects;
};
bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string* err);
} // namespace gc
#endif
+29
View File
@@ -0,0 +1,29 @@
#ifndef OPENROLLER_GC_TUMOMODEL_HPP
#define OPENROLLER_GC_TUMOMODEL_HPP
#include <cstdint>
#include <string>
#include <vector>
namespace gc {
struct TumoVertex {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
};
struct TumoGeometry {
std::vector<TumoVertex> triangles;
std::vector<TumoVertex> solidLines;
std::vector<TumoVertex> wireframeLines;
};
// Decodes the common one-mesh TUMO layout used by gameplay stage objects.
// Polygon fans are expanded on the host so the PSP only has to submit flat
// GU_TRIANGLES/GU_LINES arrays.
bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* error);
} // namespace gc
#endif
+261
View File
@@ -0,0 +1,261 @@
#include "gc/EventStream.hpp"
#include <cmath>
#include <cstring>
#include <limits>
namespace gc {
static uint16_t u16be(const std::vector<uint8_t>& b, size_t off) {
return static_cast<uint16_t>((static_cast<uint16_t>(b[off + 0]) << 8) |
(static_cast<uint16_t>(b[off + 1]) << 0));
}
static uint32_t u32be(const std::vector<uint8_t>& b, size_t off) {
return (static_cast<uint32_t>(b[off + 0]) << 24) |
(static_cast<uint32_t>(b[off + 1]) << 16) |
(static_cast<uint32_t>(b[off + 2]) << 8) |
(static_cast<uint32_t>(b[off + 3]) << 0);
}
static float f32be(const std::vector<uint8_t>& b, size_t off) {
const uint32_t u = u32be(b, off);
float f = 0.0f;
static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
std::memcpy(&f, &u, sizeof(float));
return f;
}
static bool isPlausibleTs(float ts) {
// Timestamps appear to be a monotonic progress/beat count float.
return std::isfinite(ts) && ts >= -1.0f && ts <= 1.0e6f;
}
static bool isPlausibleVal(float v) {
return std::isfinite(v) && std::fabs(v) <= 1.0e7f;
}
static int scoreStream16(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t* outEventCount, double* outPadZeroRatio) {
if (end <= start) return 0;
size_t len = end - start;
len -= (len % 16);
if (len < 16 * 10) return 0;
// Score only first N events for speed and to avoid false confidence on huge blocks.
const size_t n = std::min<size_t>(len / 16, 200);
int score = 0;
size_t padFields = 0;
size_t padZeros = 0;
size_t plausible = 0;
size_t typeNonZero = 0;
size_t tsNonZero = 0;
size_t tsChanges = 0;
size_t tsBackwards = 0;
float prevTs = 0.0f;
bool havePrevTs = false;
for (size_t i = 0; i < n; i++) {
const size_t off = start + i * 16;
const uint16_t id = u16be(bytes, off + 0);
const float ts = f32be(bytes, off + 2);
const uint16_t a = u16be(bytes, off + 6);
const uint16_t type = u16be(bytes, off + 8);
const float val = f32be(bytes, off + 10);
const uint16_t b = u16be(bytes, off + 14);
padFields += 2;
if (a == 0) padZeros++;
if (b == 0) padZeros++;
if (id == 0) score += 1;
if (a == 0) score += 2;
if (b == 0) score += 2;
if (type != 0 && type != 0xFFFF) score += 1;
if (type > 0 && type < 0x4000) score += 1;
if (type != 0 && type != 0xFFFF) typeNonZero++;
if (isPlausibleTs(ts)) {
score += 2;
plausible++;
}
if (isPlausibleVal(val)) {
score += 1;
}
if (ts != 0.0f) tsNonZero++;
if (havePrevTs) {
if (ts < prevTs) tsBackwards++;
if (std::fabs(ts - prevTs) > 1.0e-6f) tsChanges++;
} else {
havePrevTs = true;
}
prevTs = ts;
}
if (outEventCount) *outEventCount = n;
if (outPadZeroRatio) *outPadZeroRatio = padFields ? (static_cast<double>(padZeros) / static_cast<double>(padFields)) : 0.0;
// Penalize if almost nothing looks plausible.
if (plausible < (n / 4)) score /= 2;
// Prefer streams that look like a real timeline (changing, mostly monotonic),
// and avoid giant zero-filled blocks that otherwise look "plausible".
if (typeNonZero < (n / 4)) score /= 2;
if (tsNonZero < (n / 4)) score /= 2;
if (tsChanges < (n / 8)) score /= 2;
if (tsBackwards > (n / 20)) score /= 2;
return score;
}
static int scoreStream12(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t* outEventCount) {
if (end <= start) return 0;
size_t len = end - start;
len -= (len % 12);
if (len < 12 * 10) return 0;
const size_t n = std::min<size_t>(len / 12, 200);
int score = 0;
size_t plausible = 0;
size_t typeNonZero = 0;
size_t tsNonZero = 0;
size_t tsChanges = 0;
size_t tsBackwards = 0;
float prevTs = 0.0f;
bool havePrevTs = false;
for (size_t i = 0; i < n; i++) {
const size_t off = start + i * 12;
const float ts = f32be(bytes, off + 0);
const uint32_t type = u32be(bytes, off + 4);
const float val = f32be(bytes, off + 8);
if (type != 0 && type != 0xFFFFFFFFu) score += 3;
if (type != 0 && type != 0xFFFFFFFFu) typeNonZero++;
if (isPlausibleTs(ts)) {
score += 2;
plausible++;
}
if (isPlausibleVal(val)) score += 1;
if (ts != 0.0f) tsNonZero++;
if (havePrevTs) {
if (ts < prevTs) tsBackwards++;
if (std::fabs(ts - prevTs) > 1.0e-6f) tsChanges++;
} else {
havePrevTs = true;
}
prevTs = ts;
}
if (outEventCount) *outEventCount = n;
if (plausible < (n / 4)) score /= 2;
if (typeNonZero < (n / 4)) score /= 2;
if (tsNonZero < (n / 4)) score /= 2;
if (tsChanges < (n / 8)) score /= 2;
if (tsBackwards > (n / 20)) score /= 2;
return score;
}
EventStreamDecodeResult TryDecodeEventStreamFixed(const std::vector<uint8_t>& bytes, size_t start, size_t end, size_t recordSize) {
EventStreamDecodeResult best;
best.recordSize = recordSize;
best.alignment = 0;
best.score = 0;
best.eventCount = 0;
best.padZeroRatio = 0.0;
if (end <= start) return best;
if (recordSize != 12 && recordSize != 16) return best;
const size_t maxAlign = recordSize;
for (size_t align = 0; align < maxAlign; align++) {
if (start + align >= end) break;
if (recordSize == 16) {
size_t eventCount = 0;
double padZeroRatio = 0.0;
const int s = scoreStream16(bytes, start + align, end, &eventCount, &padZeroRatio);
if (s > best.score) {
best.recordSize = 16;
best.score = s;
best.alignment = static_cast<int>(align);
best.eventCount = eventCount;
best.padZeroRatio = padZeroRatio;
}
} else { // 12
size_t eventCount = 0;
const int s = scoreStream12(bytes, start + align, end, &eventCount);
if (s > best.score) {
best.recordSize = 12;
best.score = s;
best.alignment = static_cast<int>(align);
best.eventCount = eventCount;
best.padZeroRatio = 0.0;
}
}
}
return best;
}
EventStreamDecodeResult TryDecodeEventStream(const std::vector<uint8_t>& bytes, size_t start, size_t end) {
EventStreamDecodeResult best16 = TryDecodeEventStreamFixed(bytes, start, end, 16);
EventStreamDecodeResult best12 = TryDecodeEventStreamFixed(bytes, start, end, 12);
return (best12.score > best16.score) ? best12 : best16;
}
bool DecodeEventStream(
const std::vector<uint8_t>& bytes,
size_t start,
size_t end,
size_t recordSize,
std::vector<GameEvent>* out,
std::string* err) {
if (!out) return false;
out->clear();
if (end < start) {
if (err) *err = "end < start";
return false;
}
if (recordSize != 12 && recordSize != 16) {
if (err) *err = "unsupported recordSize";
return false;
}
size_t len = end - start;
len -= (len % recordSize);
if (len == 0) return true;
if (start + len > bytes.size()) {
if (err) *err = "range out of bounds";
return false;
}
const size_t n = len / recordSize;
out->reserve(n);
for (size_t i = 0; i < n; i++) {
const size_t off = start + i * recordSize;
GameEvent e;
if (recordSize == 16) {
e.id = u16be(bytes, off + 0);
e.timestamp = f32be(bytes, off + 2);
e.a = u16be(bytes, off + 6);
e.type = static_cast<uint32_t>(u16be(bytes, off + 8));
e.value = f32be(bytes, off + 10);
e.b = u16be(bytes, off + 14);
} else { // 12
e.id = 0;
e.timestamp = f32be(bytes, off + 0);
e.a = 0;
e.type = u32be(bytes, off + 4);
e.value = f32be(bytes, off + 8);
e.b = 0;
}
out->push_back(e);
}
return true;
}
} // namespace gc
+216
View File
@@ -0,0 +1,216 @@
#include "gc/GcTargetEffect.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <fstream>
namespace {
bool readFile(const std::string& path, std::vector<uint8_t>* out) {
std::ifstream file(path, std::ios::binary);
if (!file) return false;
file.seekg(0, std::ios::end);
const std::streamoff length = file.tellg();
if (length < 0) return false;
file.seekg(0, std::ios::beg);
out->assign(static_cast<size_t>(length), 0);
if (!out->empty()) file.read(reinterpret_cast<char*>(out->data()), length);
return static_cast<bool>(file) || file.eof();
}
uint16_t u16be(const std::vector<uint8_t>& data, size_t offset) {
return static_cast<uint16_t>((static_cast<uint16_t>(data[offset]) << 8) | data[offset + 1]);
}
uint32_t u32be(const std::vector<uint8_t>& data, size_t offset) {
return (static_cast<uint32_t>(data[offset]) << 24) |
(static_cast<uint32_t>(data[offset + 1]) << 16) |
(static_cast<uint32_t>(data[offset + 2]) << 8) |
static_cast<uint32_t>(data[offset + 3]);
}
float f32be(const std::vector<uint8_t>& data, size_t offset) {
const uint32_t bits = u32be(data, offset);
float value = 0.0f;
std::memcpy(&value, &bits, sizeof(value));
return value;
}
} // namespace
std::vector<float> GcTargetEffectBank::sampleTrack(const Track& track, float tick, bool* started) {
if (started) *started = false;
if (track.keys.empty()) return {};
float sample = tick;
const int loopLength = static_cast<int>(track.loopEnd) - static_cast<int>(track.loopStart);
if (loopLength > 0 && sample >= track.loopStart) {
sample = std::fmod(sample - track.loopStart, static_cast<float>(loopLength)) + track.loopStart;
}
if (sample < track.keys.front().time) return track.keys.front().values;
if (started) *started = true;
size_t index = 0;
while (index + 1 < track.keys.size() && sample >= track.keys[index + 1].time) ++index;
const auto& current = track.keys[index];
if (index + 1 >= track.keys.size() || current.interpolation == 0) return current.values;
const auto& next = track.keys[index + 1];
const float span = static_cast<float>(next.time - current.time);
const float amount = span > 0.0f ? std::clamp((sample - current.time) / span, 0.0f, 1.0f) : 0.0f;
std::vector<float> result = current.values;
for (size_t i = 0; i < result.size() && i < next.values.size(); ++i) {
result[i] = current.values[i] + (next.values[i] - current.values[i]) * amount;
}
return result;
}
bool GcTargetEffectBank::load(const std::string& efcPath, const std::string& uvPath, std::string* error) {
std::vector<uint8_t> uv;
std::vector<uint8_t> efc;
if (!readFile(uvPath, &uv) || uv.size() < 6) {
if (error) *error = "could not read uvdata.dat";
return false;
}
if (!readFile(efcPath, &efc) || efc.size() < 6) {
if (error) *error = "could not read efcdata.dat";
return false;
}
const uint16_t uvCount = u16be(uv, 4);
if (6u + static_cast<size_t>(uvCount) * 4u > uv.size()) {
if (error) *error = "invalid uvdata.dat offset table";
return false;
}
uvRecords_.assign(uvCount, {});
for (uint16_t record = 0; record < uvCount; ++record) {
const size_t begin = u32be(uv, 6 + record * 4);
const size_t end = record + 1 < uvCount ? u32be(uv, 6 + (record + 1) * 4) : uv.size();
if (begin + 4 > end || end > uv.size()) continue;
const uint16_t cellCount = u16be(uv, begin + 2);
if (begin + 4u + static_cast<size_t>(cellCount) * 8u > end) continue;
UvRecord& decoded = uvRecords_[record];
decoded.textureIndex = static_cast<int16_t>(u16be(uv, begin));
auto& cells = decoded.cells;
cells.reserve(cellCount);
for (uint16_t cell = 0; cell < cellCount; ++cell) {
const size_t at = begin + 4 + cell * 8;
cells.push_back({u16be(uv, at), u16be(uv, at + 2), u16be(uv, at + 4), u16be(uv, at + 6)});
}
}
static constexpr int firstTrackDimensions[] = {1, 0, 7, 4, 13, 3, 2};
static constexpr int otherTrackDimensions[] = {0, 3, 4, 2, 1};
const uint16_t effectCount = u16be(efc, 4);
if (6u + static_cast<size_t>(effectCount) * 4u > efc.size()) {
if (error) *error = "invalid efcdata.dat offset table";
return false;
}
effects_.assign(effectCount, {});
for (uint16_t effectIndex = 0; effectIndex < effectCount; ++effectIndex) {
const size_t begin = u32be(efc, 6 + effectIndex * 4);
const size_t end = effectIndex + 1 < effectCount ? u32be(efc, 6 + (effectIndex + 1) * 4) : efc.size();
if (begin + 3 > end || end > efc.size()) continue;
Effect& effect = effects_[effectIndex];
effect.lifetime = u16be(efc, begin);
const uint8_t childCount = efc[begin + 2];
if (begin + 3u + static_cast<size_t>(childCount) * 2u > end) continue;
effect.children.reserve(childCount);
for (uint8_t childIndex = 0; childIndex < childCount; ++childIndex) {
const size_t child = begin + u16be(efc, begin + 3 + childIndex * 2);
if (child + 15 > end) continue;
Child decoded;
decoded.type = efc[child];
decoded.reference = u16be(efc, child + 1);
decoded.inheritParent = efc[child + 4] != 0;
for (int trackIndex = 0; trackIndex < 5; ++trackIndex) {
const size_t trackAt = child + u16be(efc, child + 5 + trackIndex * 2);
if (trackAt + 6 > end) continue;
Track& track = decoded.tracks[trackIndex];
const uint16_t keyCount = u16be(efc, trackAt);
track.loopStart = u16be(efc, trackAt + 2);
track.loopEnd = u16be(efc, trackAt + 4);
const int dimensions = trackIndex == 0
? (decoded.type < sizeof(firstTrackDimensions) / sizeof(firstTrackDimensions[0])
? firstTrackDimensions[decoded.type] : 0)
: otherTrackDimensions[trackIndex];
const size_t stride = 3u + static_cast<size_t>(dimensions) * 4u;
if (dimensions <= 0 || trackAt + 6u + static_cast<size_t>(keyCount) * stride > end) continue;
track.keys.reserve(keyCount);
for (uint16_t keyIndex = 0; keyIndex < keyCount; ++keyIndex) {
const size_t keyAt = trackAt + 6 + keyIndex * stride;
Key key;
key.time = u16be(efc, keyAt);
key.interpolation = efc[keyAt + 2];
key.values.reserve(dimensions);
for (int value = 0; value < dimensions; ++value) {
key.values.push_back(f32be(efc, keyAt + 3 + value * 4));
}
track.keys.push_back(std::move(key));
}
}
effect.children.push_back(std::move(decoded));
}
}
return true;
}
int GcTargetEffectBank::lifetime(int effectId) const {
return effectId >= 0 && static_cast<size_t>(effectId) < effects_.size() ? effects_[effectId].lifetime : 0;
}
std::vector<GcEffectSprite> GcTargetEffectBank::evaluate(int effectId, float tick, int uvRecordBase) const {
std::vector<GcEffectSprite> result;
if (effectId < 0 || static_cast<size_t>(effectId) >= effects_.size()) return result;
float parentRadians = 0.0f;
bool haveParent = false;
for (const Child& child : effects_[effectId].children) {
bool firstTrackStarted = false;
const std::vector<float> first = sampleTrack(child.tracks[0], tick, &firstTrackStarted);
if (child.type == 0) {
parentRadians = !first.empty() ? first[0] : 0.0f;
haveParent = firstTrackStarted;
continue;
}
if (child.type != 2 || child.reference == 0xffff || first.size() < 1 || !firstTrackStarted) continue;
GcEffectSprite sprite;
const int resolvedRecord = static_cast<int>(child.reference) + uvRecordBase;
if (resolvedRecord < 0 || resolvedRecord > 0xffff) continue;
sprite.uvRecord = static_cast<uint16_t>(resolvedRecord);
sprite.frame = static_cast<int>(std::lround(first[0]));
const std::vector<float> position = sampleTrack(child.tracks[1], tick, nullptr);
const std::vector<float> color = sampleTrack(child.tracks[2], tick, nullptr);
const std::vector<float> scale = sampleTrack(child.tracks[3], tick, nullptr);
const std::vector<float> rotation = sampleTrack(child.tracks[4], tick, nullptr);
if (position.size() >= 3) sprite.offsetPixels = {position[0], position[1], position[2]};
if (color.size() >= 4) sprite.color = {color[1], color[2], color[3], color[0]};
if (scale.size() >= 2) sprite.scale = {scale[0], scale[1]};
if (!rotation.empty()) sprite.rotationDegrees = rotation[0];
if (child.inheritParent && haveParent) {
const float c = std::cos(parentRadians);
const float s = std::sin(parentRadians);
const glm::vec2 p(sprite.offsetPixels.x, sprite.offsetPixels.y);
sprite.offsetPixels.x = c * p.x - s * p.y;
sprite.offsetPixels.y = s * p.x + c * p.y;
sprite.rotationDegrees += glm::degrees(parentRadians);
}
result.push_back(sprite);
}
return result;
}
const GcUvCell* GcTargetEffectBank::uvCell(uint16_t record, int frame) const {
if (record >= uvRecords_.size() || frame < 0 ||
static_cast<size_t>(frame) >= uvRecords_[record].cells.size()) {
return nullptr;
}
return &uvRecords_[record].cells[frame];
}
int GcTargetEffectBank::uvTexture(uint16_t record) const {
return record < uvRecords_.size() ? uvRecords_[record].textureIndex : -1;
}
+100
View File
@@ -0,0 +1,100 @@
#include "gc/MtxArchive.hpp"
#include <cstring>
#include <limits>
namespace gc {
namespace {
uint16_t readU16Le(const std::vector<uint8_t>& bytes, size_t offset) {
return static_cast<uint16_t>(bytes[offset]) |
static_cast<uint16_t>(bytes[offset + 1] << 8);
}
uint32_t readU32Le(const std::vector<uint8_t>& bytes, size_t offset) {
return static_cast<uint32_t>(bytes[offset]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
}
bool fail(std::string* error, const std::string& message) {
if (error) *error = message;
return false;
}
} // namespace
bool ParseMtxArchive(const std::vector<uint8_t>& bytes,
MtxArchive* archive,
std::string* error) {
if (!archive) return fail(error, "null MTX archive output");
*archive = {};
if (bytes.size() < 32 || std::memcmp(bytes.data(), "MTX\0", 4) != 0) {
return fail(error, "missing MTX header");
}
const uint32_t headerOffset = readU32Le(bytes, 8);
if (headerOffset > bytes.size() - 16) return fail(error, "invalid MTX header offset");
const uint32_t count = readU32Le(bytes, headerOffset);
const uint32_t tableOffset = readU32Le(bytes, headerOffset + 4);
const uint32_t stride = readU32Le(bytes, headerOffset + 8);
const uint16_t nameBytes = readU16Le(bytes, headerOffset + 12);
if (stride < 16 || count > std::numeric_limits<uint32_t>::max() / stride) {
return fail(error, "invalid MTX entry table shape");
}
const uint64_t tableStart = static_cast<uint64_t>(headerOffset) + tableOffset;
const uint64_t tableBytes = static_cast<uint64_t>(count) * stride;
const uint64_t dataStart64 = tableStart + tableBytes + nameBytes;
if (tableStart > bytes.size() || dataStart64 > bytes.size()) {
return fail(error, "MTX entry table exceeds the file");
}
size_t dataCursor = static_cast<size_t>(dataStart64);
archive->textures.reserve(count);
for (uint32_t index = 0; index < count; ++index) {
const size_t entry = static_cast<size_t>(tableStart) + static_cast<size_t>(index) * stride;
const uint32_t size = readU32Le(bytes, entry);
if (size < 128 || size > bytes.size() - dataCursor) {
return fail(error, "MTX texture payload exceeds the file");
}
// Apart from the first dword, each embedded payload is a standard
// little-endian DDS_HEADER. game471.exe passes it directly to its DDS
// reader after locating it with this same cumulative-size algorithm.
if (readU32Le(bytes, dataCursor + 4) != 124 ||
readU32Le(bytes, dataCursor + 76) != 32) {
return fail(error, "MTX texture does not contain a DDS header");
}
MtxTexture texture;
texture.offset = static_cast<uint32_t>(dataCursor);
texture.size = size;
texture.height = readU32Le(bytes, dataCursor + 12);
texture.width = readU32Le(bytes, dataCursor + 16);
texture.fourCC = readU32Le(bytes, dataCursor + 84);
texture.bitsPerPixel = readU32Le(bytes, dataCursor + 88);
archive->textures.push_back(texture);
dataCursor += size;
}
if (dataCursor != bytes.size()) return fail(error, "MTX has trailing texture data");
return true;
}
bool ExtractMtxTextureDds(const std::vector<uint8_t>& bytes,
const MtxTexture& texture,
std::vector<uint8_t>* dds,
std::string* error) {
if (!dds) return fail(error, "null DDS output");
dds->clear();
if (texture.size < 128 || texture.offset > bytes.size() ||
texture.size > bytes.size() - texture.offset) {
return fail(error, "MTX texture range is invalid");
}
dds->assign(bytes.begin() + texture.offset,
bytes.begin() + texture.offset + texture.size);
(*dds)[0] = 'D';
(*dds)[1] = 'D';
(*dds)[2] = 'S';
(*dds)[3] = ' ';
return true;
}
} // namespace gc
+413
View File
@@ -0,0 +1,413 @@
#include "gc/RvbLayout.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <unordered_map>
namespace gc {
namespace {
uint32_t readU32Le(const std::vector<uint8_t>& bytes, size_t offset) {
return static_cast<uint32_t>(bytes[offset]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
}
float readFloatLe(const std::vector<uint8_t>& bytes, size_t offset) {
const uint32_t value = readU32Le(bytes, offset);
float result = 0.0f;
std::memcpy(&result, &value, sizeof(result));
return result;
}
bool readCString(const std::vector<uint8_t>& bytes, size_t* cursor,
size_t end, std::string* value) {
if (!cursor || !value || *cursor >= end || end > bytes.size()) return false;
const auto begin = bytes.begin() + static_cast<std::ptrdiff_t>(*cursor);
const auto last = bytes.begin() + static_cast<std::ptrdiff_t>(end);
const auto zero = std::find(begin, last, uint8_t{0});
if (zero == last) return false;
value->assign(reinterpret_cast<const char*>(&*begin),
static_cast<size_t>(zero - begin));
*cursor += value->size() + 1;
return true;
}
struct Matrix {
float xx = 1.0f;
float yy = 1.0f;
float xy = 0.0f;
float yx = 0.0f;
float tx = 0.0f;
float ty = 0.0f;
};
Matrix multiply(const Matrix& a, const Matrix& b) {
Matrix out;
out.xx = a.xx * b.xx + a.xy * b.yx;
out.xy = a.xx * b.xy + a.xy * b.yy;
out.tx = a.xx * b.tx + a.xy * b.ty + a.tx;
out.yx = a.yx * b.xx + a.yy * b.yx;
out.yy = a.yx * b.xy + a.yy * b.yy;
out.ty = a.yx * b.tx + a.yy * b.ty + a.ty;
return out;
}
std::array<float, 2> transform(const Matrix& m, float x, float y) {
return {m.xx * x + m.xy * y + m.tx, m.yx * x + m.yy * y + m.ty};
}
Matrix nodeTransform(const std::vector<uint8_t>& bytes, const RvbNode& node) {
for (const RvbNode& child : node.children) {
if (child.tag != "TRN2" && child.tag != "TRAN") continue;
if (child.localDataSize < 24) continue;
const size_t p = child.localDataOffset;
Matrix m;
m.xx = readFloatLe(bytes, p + 0);
m.yy = readFloatLe(bytes, p + 4);
m.xy = readFloatLe(bytes, p + 8);
m.yx = readFloatLe(bytes, p + 12);
m.tx = readFloatLe(bytes, p + 16);
m.ty = readFloatLe(bytes, p + 20);
return m;
}
return {};
}
bool hasNodeTransform(const RvbNode& node) {
for (const RvbNode& child : node.children) {
if ((child.tag == "TRN2" || child.tag == "TRAN") && child.localDataSize >= 24) {
return true;
}
}
return false;
}
struct ColorTransform {
float red = 1.0f;
float green = 1.0f;
float blue = 1.0f;
float alpha = 1.0f;
};
ColorTransform multiply(const ColorTransform& a, const ColorTransform& b) {
return {a.red * b.red, a.green * b.green, a.blue * b.blue, a.alpha * b.alpha};
}
bool nodeColor(const std::vector<uint8_t>& bytes, const RvbNode& node,
ColorTransform* color) {
if (!color) return false;
for (const RvbNode& child : node.children) {
if (child.tag != "COLT" || child.localDataSize < 20) continue;
// RGBAColorTransform::read (game471.exe FUN_004d64b0) reads four
// floats followed by four bytes. The animation node adds a leading
// four-byte field, so alpha multiplication is the fourth float at
// local + 16. Alpha zero is how authored clips are made invisible.
color->red = readFloatLe(bytes, child.localDataOffset + 4);
color->green = readFloatLe(bytes, child.localDataOffset + 8);
color->blue = readFloatLe(bytes, child.localDataOffset + 12);
color->alpha = readFloatLe(bytes, child.localDataOffset + 16);
return true;
}
return false;
}
const RvbNode* firstChild(const RvbNode& node, const char* tag) {
for (const RvbNode& child : node.children) {
if (child.tag == tag) return &child;
}
return nullptr;
}
std::string definitionName(const std::vector<uint8_t>& bytes, const RvbNode& node) {
size_t cursor = node.localDataOffset;
std::string name;
readCString(bytes, &cursor, node.localDataOffset + node.localDataSize, &name);
return name;
}
std::string frameActionSource(const std::vector<uint8_t>& bytes, const RvbNode& frame) {
for (const RvbNode& child : frame.children) {
if (child.tag != "ASRC") continue;
size_t cursor = child.localDataOffset;
std::string source;
if (readCString(bytes, &cursor, child.localDataOffset + child.localDataSize, &source)) {
return source;
}
}
return {};
}
std::string findImageSymbol(const std::vector<uint8_t>& bytes, const RvbNode& node) {
if (node.tag == "IMGF" && node.localDataSize >= 2) {
size_t cursor = node.localDataOffset + 1;
std::string symbol;
if (readCString(bytes, &cursor, node.localDataOffset + node.localDataSize, &symbol) &&
!symbol.empty()) {
return symbol;
}
}
for (const RvbNode& child : node.children) {
std::string symbol = findImageSymbol(bytes, child);
if (!symbol.empty()) return symbol;
}
return {};
}
struct Context {
const std::vector<uint8_t>& bytes;
std::unordered_map<std::string, const RvbNode*> definitions;
std::vector<RvbImageDraw>* draws = nullptr;
std::string* error = nullptr;
const RvbSnapshotState* state = nullptr;
bool renderDefinition(const std::string& name, const Matrix& matrix,
const ColorTransform& color, uint32_t depth, unsigned recursion,
const std::string& path) {
if (recursion > 64) {
if (error) *error = "RVB MovieClip recursion limit exceeded";
return false;
}
const auto found = definitions.find(name);
if (found == definitions.end()) return true;
const RvbNode& definition = *found->second;
if (definition.tag == "SHAP") {
size_t cursor = definition.localDataOffset;
std::string ignored;
if (!readCString(bytes, &cursor,
definition.localDataOffset + definition.localDataSize, &ignored) ||
cursor + 16 > definition.localDataOffset + definition.localDataSize) {
return true;
}
const std::string image = findImageSymbol(bytes, definition);
if (image.empty()) return true;
const float left = readFloatLe(bytes, cursor + 0);
const float top = readFloatLe(bytes, cursor + 4);
const float right = readFloatLe(bytes, cursor + 8);
const float bottom = readFloatLe(bytes, cursor + 12);
RvbImageDraw draw;
draw.imageSymbol = image;
draw.instancePath = path;
draw.corners[0] = transform(matrix, left, top);
draw.corners[1] = transform(matrix, right, top);
draw.corners[2] = transform(matrix, left, bottom);
draw.corners[3] = transform(matrix, right, bottom);
draw.depth = depth;
draw.color = {color.red, color.green, color.blue};
draw.alpha = color.alpha;
draws->push_back(std::move(draw));
return true;
}
if (definition.tag == "MOVC") {
const RvbNode* timeline = firstChild(definition, "TIME");
return !timeline || renderTimeline(*timeline, matrix, color, recursion + 1, path);
}
return true;
}
bool renderTimeline(const RvbNode& timeline, const Matrix& parent,
const ColorTransform& parentColor,
unsigned recursion, const std::string& path) {
const RvbNode* targetFrame = nullptr;
std::string wantedLabel;
if (state) {
const auto found = state->frameByPath.find(path);
if (found != state->frameByPath.end()) wantedLabel = found->second;
}
for (const RvbNode& child : timeline.children) {
if (child.tag != "FRAM") continue;
if (!targetFrame) targetFrame = &child;
if (wantedLabel.empty()) break;
size_t cursor = child.localDataOffset;
std::string label;
if (readCString(bytes, &cursor, child.localDataOffset + child.localDataSize,
&label) && label == wantedLabel) {
targetFrame = &child;
break;
}
}
if (!targetFrame) return true;
// A label whose script calls play() is an animation entry point, not
// the visible steady state. The original player advances until the
// following stop(); doing the same resolves authored fades such as
// lf_title_selectmusic_start and jf_focusds_start.
if (frameActionSource(bytes, *targetFrame).find("play();") != std::string::npos) {
bool afterTarget = false;
for (const RvbNode& child : timeline.children) {
if (child.tag != "FRAM") continue;
if (&child == targetFrame) {
afterTarget = true;
continue;
}
if (!afterTarget) continue;
targetFrame = &child;
if (frameActionSource(bytes, child).find("stop();") != std::string::npos) break;
}
}
struct Placement {
uint32_t depth = 0;
std::string definition;
std::string instance;
Matrix matrix;
ColorTransform color;
};
std::unordered_map<uint32_t, Placement> displayList;
bool reachedTarget = false;
for (const RvbNode& frame : timeline.children) {
if (frame.tag != "FRAM") continue;
for (const RvbNode& command : frame.children) {
if (command.tag == "RMOV") {
if (command.localDataSize >= 5) {
displayList.erase(readU32Le(bytes, command.localDataOffset + 1));
}
continue;
}
if (command.tag != "PLC3") continue;
size_t cursor = command.localDataOffset;
const size_t end = command.localDataOffset + command.localDataSize;
std::string definition;
std::string instance;
if (!readCString(bytes, &cursor, end, &definition) ||
!readCString(bytes, &cursor, end, &instance) || cursor + 8 > end) {
continue;
}
cursor += 4;
const uint32_t depth = readU32Le(bytes, cursor);
const bool hasTransform = hasNodeTransform(command);
ColorTransform color;
const bool hasColor = nodeColor(bytes, command, &color);
if (!definition.empty()) {
Placement placement;
placement.depth = depth;
placement.definition = std::move(definition);
placement.instance = std::move(instance);
if (hasTransform) placement.matrix = nodeTransform(bytes, command);
if (hasColor) placement.color = color;
displayList[depth] = std::move(placement);
} else {
const auto found = displayList.find(depth);
if (found == displayList.end()) continue;
if (!instance.empty()) found->second.instance = std::move(instance);
if (hasTransform) found->second.matrix = nodeTransform(bytes, command);
if (hasColor) found->second.color = color;
}
}
if (&frame == targetFrame) {
reachedTarget = true;
break;
}
}
if (!reachedTarget) return true;
std::vector<Placement> placements;
placements.reserve(displayList.size());
for (auto& [_, placement] : displayList) placements.push_back(std::move(placement));
std::stable_sort(placements.begin(), placements.end(),
[](const Placement& a, const Placement& b) { return a.depth < b.depth; });
for (const Placement& placement : placements) {
// imc_other is the game's reusable off-focus song template. The
// CSelectMusicTask renderer moves and draws it once per carousel
// entry; leaving its authored root placement visible produces a
// spurious duplicate focus panel in a static snapshot.
if (recursion == 0 && placement.instance == "imc_other" &&
(!state || !state->includeRootOther)) {
continue;
}
const Matrix placed = multiply(parent, placement.matrix);
const ColorTransform color = multiply(parentColor, placement.color);
if (color.alpha <= 0.0001f) continue;
const std::string childName = placement.instance.empty()
? placement.definition
: placement.instance;
const std::string childPath = path == "/" ? "/" + childName
: path + "/" + childName;
if (!renderDefinition(placement.definition, placed, color, placement.depth,
recursion, childPath)) {
return false;
}
}
return true;
}
};
void collectDefinitions(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
Context* context,
const RvbNode** rootTimeline = nullptr) {
if (rootTimeline) *rootTimeline = nullptr;
for (const RvbNode& root : scene.roots) {
if (root.tag == "DEFN") {
for (const RvbNode& definition : root.children) {
const std::string name = definitionName(bytes, definition);
if (!name.empty()) context->definitions.emplace(name, &definition);
}
} else if (root.tag == "TIME" && rootTimeline) {
*rootTimeline = &root;
}
}
}
} // namespace
bool BuildRvbInitialSnapshot(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
std::vector<RvbImageDraw>* draws,
std::string* error) {
if (!draws) {
if (error) *error = "null RVB snapshot output";
return false;
}
draws->clear();
const RvbSnapshotState state;
return BuildRvbSnapshot(bytes, scene, state, draws, error);
}
bool BuildRvbSnapshot(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
const RvbSnapshotState& state,
std::vector<RvbImageDraw>* draws,
std::string* error) {
if (!draws) {
if (error) *error = "null RVB snapshot output";
return false;
}
draws->clear();
Context context{bytes, {}, draws, error, &state};
const RvbNode* rootTimeline = nullptr;
collectDefinitions(bytes, scene, &context, &rootTimeline);
if (!rootTimeline) {
if (error) *error = "RVB has no root TIME timeline";
return false;
}
return context.renderTimeline(*rootTimeline, {}, {}, 0, "/");
}
bool BuildRvbSymbolSnapshot(const std::vector<uint8_t>& bytes,
const RvbScene& scene,
const std::string& symbolName,
const RvbSnapshotState& state,
std::vector<RvbImageDraw>* draws,
std::string* error) {
if (!draws) {
if (error) *error = "null RVB symbol snapshot output";
return false;
}
draws->clear();
Context context{bytes, {}, draws, error, &state};
collectDefinitions(bytes, scene, &context);
std::string definitionName = symbolName;
for (const RvbExport& exported : scene.exports) {
if (exported.linkageName == symbolName) {
definitionName = exported.definitionName;
break;
}
}
if (context.definitions.find(definitionName) == context.definitions.end()) {
if (error) *error = "RVB symbol is not present in DEFN: " + symbolName;
return false;
}
return context.renderDefinition(definitionName, {}, {}, 0, 0, "/");
}
} // namespace gc
+200
View File
@@ -0,0 +1,200 @@
#include "gc/RvbScene.hpp"
#include <algorithm>
#include <cstring>
#include <sstream>
namespace gc {
namespace {
uint16_t readU16Le(const std::vector<uint8_t>& bytes, size_t offset) {
return static_cast<uint16_t>(bytes[offset]) |
static_cast<uint16_t>(bytes[offset + 1] << 8);
}
uint32_t readU32Le(const std::vector<uint8_t>& bytes, size_t offset) {
return static_cast<uint32_t>(bytes[offset]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
}
bool hasTag(const std::vector<uint8_t>& bytes, size_t offset, const char tag[5]) {
return offset + 4 <= bytes.size() &&
std::memcmp(bytes.data() + offset, tag, 4) == 0;
}
bool readCString(const std::vector<uint8_t>& bytes, size_t* cursor,
size_t end, std::string* value) {
if (!cursor || !value || *cursor >= end || end > bytes.size()) return false;
const auto first = bytes.begin() + static_cast<std::ptrdiff_t>(*cursor);
const auto last = bytes.begin() + static_cast<std::ptrdiff_t>(end);
const auto zero = std::find(first, last, uint8_t{0});
if (zero == last) return false;
value->assign(reinterpret_cast<const char*>(&*first),
static_cast<size_t>(zero - first));
*cursor += value->size() + 1;
return true;
}
bool fail(std::string* error, const std::string& message) {
if (error) *error = message;
return false;
}
bool parseNode(const std::vector<uint8_t>& bytes, size_t offset, size_t limit,
RvbNode* node, std::string* error) {
if (!node || offset + 12 > limit || limit > bytes.size()) {
return fail(error, "truncated RVB animation node");
}
const uint32_t size = readU32Le(bytes, offset + 4);
const uint32_t localSize = readU32Le(bytes, offset + 8);
if (size < 12 || size > limit - offset || localSize > size - 12) {
std::ostringstream message;
message << "invalid RVB animation node at 0x" << std::hex << offset;
return fail(error, message.str());
}
node->tag.assign(reinterpret_cast<const char*>(bytes.data() + offset), 4);
node->offset = static_cast<uint32_t>(offset);
node->size = size;
node->localDataOffset = static_cast<uint32_t>(offset + 12);
node->localDataSize = localSize;
size_t childCursor = offset + 12 + localSize;
const size_t nodeEnd = offset + size;
while (childCursor < nodeEnd) {
RvbNode child;
if (!parseNode(bytes, childCursor, nodeEnd, &child, error)) return false;
childCursor += child.size;
node->children.push_back(std::move(child));
}
return childCursor == nodeEnd;
}
} // namespace
bool ParseRvbScene(const std::vector<uint8_t>& bytes,
RvbScene* scene,
std::string* error) {
if (!scene) return fail(error, "null RVB scene output");
*scene = {};
if (bytes.size() < 0x43) return fail(error, "RVB file is shorter than its MOVI header");
if (!hasTag(bytes, 0, "RVB_")) return fail(error, "missing RVB_ root tag");
if (readU32Le(bytes, 4) != bytes.size()) {
return fail(error, "RVB_ root size does not match the file size");
}
if (!hasTag(bytes, 0x10, "MOVI")) return fail(error, "missing MOVI chunk at 0x10");
const uint32_t movieSize = readU32Le(bytes, 0x14);
if (movieSize < 0x23 || static_cast<uint64_t>(0x10) + movieSize != bytes.size()) {
return fail(error, "invalid MOVI chunk size");
}
// Recovered from the MOVI reader layout used by game471.exe. The file
// stores height before width; root placements such as imc_title=(360,112)
// confirm the authored scene is the cabinet's native portrait canvas.
scene->framesPerSecond = bytes[0x1d];
scene->sourceHeight = readU16Le(bytes, 0x2a);
scene->sourceWidth = readU16Le(bytes, 0x2e);
// The first child begins at the unaligned 0x33 offset. Each child size
// includes its four-byte tag and size field; walking the sizes lands
// exactly on the end of MOVI.
size_t cursor = 0x33;
while (cursor < bytes.size()) {
if (cursor + 8 > bytes.size()) return fail(error, "truncated RVB child chunk header");
const uint32_t chunkSize = readU32Le(bytes, cursor + 4);
if (chunkSize < 8 || chunkSize > bytes.size() - cursor) {
std::ostringstream message;
message << "invalid RVB child chunk size at 0x" << std::hex << cursor;
return fail(error, message.str());
}
RvbChunk chunk;
chunk.tag.assign(reinterpret_cast<const char*>(bytes.data() + cursor), 4);
chunk.offset = static_cast<uint32_t>(cursor);
chunk.size = chunkSize;
scene->chunks.push_back(chunk);
if (chunk.tag == "PREP") {
if (chunkSize < 16) return fail(error, "PREP chunk is too short");
const uint32_t payloadSize = readU32Le(bytes, cursor + 8);
const uint32_t count = readU32Le(bytes, cursor + 12);
if (payloadSize != chunkSize - 12) {
return fail(error, "PREP payload size does not match its chunk size");
}
size_t stringCursor = cursor + 16;
const size_t chunkEnd = cursor + chunkSize;
scene->bindings.reserve(count);
for (uint32_t index = 0; index < count; ++index) {
RvbBinding binding;
if (!readCString(bytes, &stringCursor, chunkEnd, &binding.action) ||
!readCString(bytes, &stringCursor, chunkEnd, &binding.instancePath)) {
std::ostringstream message;
message << "truncated PREP binding " << index;
return fail(error, message.str());
}
scene->bindings.push_back(std::move(binding));
}
if (stringCursor != chunkEnd) {
return fail(error, "PREP bindings do not consume the complete chunk");
}
} else if (chunk.tag == "REPO") {
if (chunkSize < 12) return fail(error, "REPO chunk is too short");
const size_t chunkEnd = cursor + chunkSize;
const uint32_t headerSize = readU32Le(bytes, cursor + 8);
size_t resourceCursor = cursor + 12 + headerSize;
if (resourceCursor > chunkEnd) return fail(error, "REPO header exceeds its chunk");
while (resourceCursor < chunkEnd) {
if (resourceCursor + 12 > chunkEnd) {
return fail(error, "truncated REPO resource header");
}
const std::string resourceTag(
reinterpret_cast<const char*>(bytes.data() + resourceCursor), 4);
const uint32_t resourceSize = readU32Le(bytes, resourceCursor + 4);
if (resourceSize < 12 || resourceSize > chunkEnd - resourceCursor) {
return fail(error, "invalid REPO resource size");
}
if (resourceTag == "IMAG") {
const size_t resourceEnd = resourceCursor + resourceSize;
size_t fieldCursor = resourceCursor + 16;
RvbImageResource image;
if (!readCString(bytes, &fieldCursor, resourceEnd, &image.fileName) ||
!readCString(bytes, &fieldCursor, resourceEnd, &image.symbolName) ||
fieldCursor + 13 != resourceEnd) {
return fail(error, "malformed IMAG repository resource");
}
fieldCursor += 4; // resource flags, zero in the dumped menu assets
image.height = readU32Le(bytes, fieldCursor);
image.width = readU32Le(bytes, fieldCursor + 4);
scene->images.push_back(std::move(image));
}
resourceCursor += resourceSize;
}
} else if (chunk.tag == "EXPG") {
RvbNode exports;
if (!parseNode(bytes, cursor, cursor + chunkSize, &exports, error)) return false;
for (const RvbNode& child : exports.children) {
if (child.tag != "EXPS") continue;
size_t fieldCursor = child.localDataOffset;
const size_t fieldEnd = child.localDataOffset + child.localDataSize;
RvbExport exported;
if (!readCString(bytes, &fieldCursor, fieldEnd, &exported.definitionName) ||
!readCString(bytes, &fieldCursor, fieldEnd, &exported.linkageName) ||
fieldCursor != fieldEnd) {
return fail(error, "malformed EXPS linkage record");
}
scene->exports.push_back(std::move(exported));
}
}
if (chunk.tag != "PREP") {
RvbNode root;
if (!parseNode(bytes, cursor, cursor + chunkSize, &root, error)) return false;
scene->roots.push_back(std::move(root));
}
cursor += chunkSize;
}
if (scene->chunks.empty() || scene->chunks.front().tag != "PREP") {
return fail(error, "MOVI has no leading PREP binding chunk");
}
return true;
}
} // namespace gc
+146
View File
@@ -0,0 +1,146 @@
#include "gc/StageCatalog.hpp"
#include <cstddef>
#include <sstream>
namespace gc {
namespace {
class CatalogReader {
public:
explicit CatalogReader(const std::vector<uint8_t>& bytes) : bytes_(bytes) {}
size_t tell() const { return pos_; }
bool ok() const { return ok_; }
uint8_t u8() {
if (pos_ >= bytes_.size()) {
ok_ = false;
return 0;
}
return bytes_[pos_++];
}
uint16_t u16be() {
const uint16_t a = u8();
const uint16_t b = u8();
return static_cast<uint16_t>((a << 8) | b);
}
uint32_t u32be() {
const uint32_t a = u8();
const uint32_t b = u8();
const uint32_t c = u8();
const uint32_t d = u8();
return (a << 24) | (b << 16) | (c << 8) | d;
}
std::string string8() {
const size_t size = u8();
if (!ok_ || size > bytes_.size() - pos_) {
ok_ = false;
return {};
}
const char* begin = reinterpret_cast<const char*>(bytes_.data() + pos_);
pos_ += size;
return std::string(begin, size);
}
private:
const std::vector<uint8_t>& bytes_;
size_t pos_ = 0;
bool ok_ = true;
};
template <typename T, size_t N, typename Read>
void readArray(std::array<T, N>& values, Read&& read) {
for (T& value : values) value = read();
}
} // namespace
bool ParseStageCatalog(const std::vector<uint8_t>& bytes,
std::vector<StageCatalogEntry>* entries,
std::string* error) {
if (!entries) {
if (error) *error = "null stage catalog output";
return false;
}
entries->clear();
if (bytes.size() < 2) {
if (error) *error = "stage_param.dat is shorter than its u16 count";
return false;
}
CatalogReader r(bytes);
const uint16_t count = r.u16be();
entries->reserve(count);
for (uint16_t index = 0; index < count; ++index) {
const size_t recordOffset = r.tell();
StageCatalogEntry entry;
entry.id = r.u32be();
entry.title = r.string8();
entry.imageKey = r.string8();
entry.artist = r.string8();
entry.source = r.string8();
entry.sortKey = r.string8();
entry.genre = r.u8();
entry.duration = r.string8();
readArray(entry.difficultyRatings, [&] { return r.u8(); });
entry.bpm = r.string8();
readArray(entry.bgmVolumes, [&] { return r.u8(); });
readArray(entry.shotVolumes, [&] { return r.u8(); });
readArray(entry.timingValues, [&] { return r.u32be(); });
readArray(entry.unknown60, [&] { return r.u8(); });
entry.bgmBase = r.string8();
readArray(entry.chartGroup0, [&] { return r.string8(); });
readArray(entry.chartSuffixes, [&] { return r.string8(); });
readArray(entry.chartIds, [&] { return r.string8(); });
entry.unknown9c = r.string8();
entry.unknownA0 = r.u32be();
readArray(entry.unknownA4, [&] { return r.u8(); });
entry.unknownAc = r.string8();
entry.unknownB0 = r.u8();
if (!r.ok()) {
if (error) {
std::ostringstream message;
message << "truncated stage_param.dat record " << index
<< " at file offset 0x" << std::hex << recordOffset;
*error = message.str();
}
entries->clear();
return false;
}
entries->push_back(std::move(entry));
}
if (r.tell() != bytes.size()) {
if (error) {
std::ostringstream message;
message << "stage_param.dat has " << (bytes.size() - r.tell())
<< " trailing bytes after " << count << " records";
*error = message.str();
}
entries->clear();
return false;
}
return true;
}
const StageCatalogEntry* FindStageCatalogEntryByChart(
const std::vector<StageCatalogEntry>& entries,
const std::string& chartId) {
for (const StageCatalogEntry& entry : entries) {
for (const std::string& candidate : entry.chartIds) {
if (candidate == chartId) return &entry;
}
// Older/alternate data can use either of the two preceding chart groups.
for (const std::string& candidate : entry.chartGroup0) {
if (candidate == chartId) return &entry;
}
}
return nullptr;
}
} // namespace gc
+95
View File
@@ -0,0 +1,95 @@
#include "gc/StageDat.hpp"
#include <algorithm>
#include <fstream>
#include <sstream>
namespace gc {
static bool readFile(const std::string& path, std::vector<uint8_t>* out, std::string* err) {
std::ifstream f(path, std::ios::binary);
if (!f.is_open()) {
if (err) *err = "could not open file";
return false;
}
f.seekg(0, std::ios::end);
std::streampos end = f.tellg();
if (end < 0) {
if (err) *err = "tellg failed";
return false;
}
const size_t size = static_cast<size_t>(end);
f.seekg(0, std::ios::beg);
out->assign(size, 0);
if (size > 0) f.read(reinterpret_cast<char*>(out->data()), static_cast<std::streamsize>(size));
if (!f) {
if (err) *err = "read failed";
return false;
}
return true;
}
static uint32_t u32be(const std::vector<uint8_t>& b, size_t off) {
return (static_cast<uint32_t>(b[off + 0]) << 24) |
(static_cast<uint32_t>(b[off + 1]) << 16) |
(static_cast<uint32_t>(b[off + 2]) << 8) |
(static_cast<uint32_t>(b[off + 3]) << 0);
}
bool StageDat::LoadFromFile(const std::string& path, StageDat& out, std::string* err) {
StageDat tmp;
if (!readFile(path, &tmp.bytes, err)) return false;
if (tmp.bytes.size() < 4) {
if (err) *err = "file too small";
return false;
}
const size_t size = tmp.bytes.size();
tmp.headerSize = u32be(tmp.bytes, 0);
if (tmp.headerSize < 4 || tmp.headerSize > size) {
if (err) {
std::ostringstream oss;
oss << "invalid headerSize=" << tmp.headerSize << " for file size=" << size;
*err = oss.str();
}
return false;
}
if ((tmp.headerSize % 4) != 0) {
if (err) *err = "headerSize is not multiple of 4";
return false;
}
// The first headerSize bytes are typically a u32 table (big endian) containing section offsets.
tmp.headerWords.reserve(tmp.headerSize / 4);
for (size_t off = 0; off < tmp.headerSize; off += 4) {
tmp.headerWords.push_back(u32be(tmp.bytes, off));
}
// Collect plausible offsets from the header table.
// We keep only offsets that point past the header and within the file.
tmp.offsets.clear();
tmp.offsets.reserve(tmp.headerWords.size());
for (uint32_t w : tmp.headerWords) {
if (w >= tmp.headerSize && w < size) tmp.offsets.push_back(static_cast<size_t>(w));
}
tmp.offsets.push_back(static_cast<size_t>(tmp.headerSize));
tmp.offsets.push_back(size);
std::sort(tmp.offsets.begin(), tmp.offsets.end());
tmp.offsets.erase(std::unique(tmp.offsets.begin(), tmp.offsets.end()), tmp.offsets.end());
// Sections are just adjacent offset pairs.
tmp.sections.clear();
for (size_t i = 0; i + 1 < tmp.offsets.size(); i++) {
const size_t a = tmp.offsets[i];
const size_t b = tmp.offsets[i + 1];
if (a >= b) continue;
tmp.sections.push_back(Section{a, b});
}
out = std::move(tmp);
return true;
}
} // namespace gc
+589
View File
@@ -0,0 +1,589 @@
#include "gc/StagePattern.hpp"
#include <cstring>
#include <limits>
#include <sstream>
namespace gc {
namespace {
class PatternReader {
public:
PatternReader(const std::vector<uint8_t>& bytes, size_t pos, size_t end)
: bytes_(bytes), pos_(pos), end_(end) {}
size_t tell() const { return pos_; }
bool u8(uint8_t* out) {
if (!out || !need(1)) return false;
*out = bytes_[pos_++];
return true;
}
bool u16(uint16_t* out) {
if (!out || !need(2)) return false;
*out = static_cast<uint16_t>((static_cast<uint16_t>(bytes_[pos_]) << 8) |
static_cast<uint16_t>(bytes_[pos_ + 1]));
pos_ += 2;
return true;
}
bool u32(uint32_t* out) {
if (!out || !need(4)) return false;
*out = (static_cast<uint32_t>(bytes_[pos_ + 0]) << 24) |
(static_cast<uint32_t>(bytes_[pos_ + 1]) << 16) |
(static_cast<uint32_t>(bytes_[pos_ + 2]) << 8) |
(static_cast<uint32_t>(bytes_[pos_ + 3]));
pos_ += 4;
return true;
}
bool f32(float* out) {
uint32_t u = 0;
if (!u32(&u) || !out) return false;
static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
std::memcpy(out, &u, sizeof(float));
return true;
}
bool color(Color* out) {
if (!out || !need(4)) return false;
out->r = bytes_[pos_ + 0];
out->g = bytes_[pos_ + 1];
out->b = bytes_[pos_ + 2];
out->a = bytes_[pos_ + 3];
pos_ += 4;
return true;
}
bool string8(std::string* out) {
uint8_t len = 0;
if (!u8(&len)) return false;
return sizedString(len, out);
}
bool string16(std::string* out) {
uint16_t len = 0;
if (!u16(&len)) return false;
return sizedString(len, out);
}
private:
bool need(size_t n) const {
return pos_ <= end_ && n <= end_ - pos_ && pos_ + n <= bytes_.size();
}
bool sizedString(size_t len, std::string* out) {
if (!out || !need(len)) return false;
out->assign(reinterpret_cast<const char*>(bytes_.data() + pos_), len);
while (!out->empty() && out->back() == '\0') out->pop_back();
pos_ += len;
return true;
}
const std::vector<uint8_t>& bytes_;
size_t pos_ = 0;
size_t end_ = 0;
};
bool failAt(const char* what, size_t off, std::string* err) {
if (err) {
std::ostringstream oss;
oss << "failed to parse " << what << " at 0x" << std::hex << off;
*err = oss.str();
}
return false;
}
bool validOffset(const StageDat& dat, uint32_t off) {
return off < dat.bytes.size();
}
bool validEndOffset(const StageDat& dat, uint32_t off) {
return off <= dat.bytes.size();
}
bool parseHeader(const StageDat& dat, StageHeader* out, std::string* err) {
if (!out) return false;
if (dat.headerWords.size() < 10) {
if (err) *err = "file too small for stage header";
return false;
}
out->stageCfg = dat.headerWords[0];
out->trackDrawDist = dat.headerWords[1];
out->track = dat.headerWords[2];
out->notes = dat.headerWords[3];
out->camera = dat.headerWords[4];
out->particles = dat.headerWords[5];
out->visualizer = dat.headerWords[6];
out->unk1 = dat.headerWords[7];
out->colors = dat.headerWords[8];
out->objects = dat.headerWords[9];
// Older stage revisions have an 11-word header and place ColorTable2 in
// slot 10. The 13-word revision used by game471 inserts a scalar at 10,
// moves ColorTable2 to 11, then appends another scalar.
if (dat.headerWords.size() == 11) {
out->colors2 = dat.headerWords[10];
} else if (dat.headerWords.size() >= 13) {
out->unk2 = dat.headerWords[10];
out->colors2 = dat.headerWords[11];
out->unk3 = dat.headerWords[12];
}
return true;
}
bool parseStageConfig(const StageDat& dat, uint32_t start, uint32_t next, StageConfig* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
PatternReader r(dat.bytes, start, next);
uint16_t bpmCount = 0;
if (!r.f32(&out->endTime1) ||
!r.f32(&out->endTime2) ||
!r.f32(&out->outroTime) ||
!r.u16(&bpmCount)) {
return failAt("StageConfig header", r.tell(), err);
}
out->bpmChanges.clear();
out->bpmChanges.reserve(bpmCount);
for (uint16_t i = 0; i < bpmCount; i++) {
BpmChange bp;
if (!r.u32(&bp.timeMs) || !r.u32(&bp.bpm)) return failAt("BpmChange", r.tell(), err);
out->bpmChanges.push_back(bp);
}
for (std::vector<NoteSetting>& list : out->noteSettings) {
uint16_t count = 0;
if (!r.u16(&count)) return failAt("NoteSetting count", r.tell(), err);
list.clear();
list.reserve(count);
for (uint16_t i = 0; i < count; ++i) {
NoteSetting ns;
if (!r.u32(&ns.timeMs) || !r.u32(&ns.mode) || !r.f32(&ns.value)) {
return failAt("NoteSetting", r.tell(), err);
}
list.push_back(ns);
}
}
if (!r.string16(&out->chartName) ||
!r.string16(&out->chartName2) ||
!r.string16(&out->bgmName) ||
!r.string16(&out->shotName) ||
!r.f32(&out->backwardsDrawDist) ||
!r.f32(&out->forwardDrawDist) ||
!r.color(&out->trackAheadColor) ||
!r.color(&out->trackBehindColor) ||
!r.u8(&out->audioOffset) ||
!r.f32(&out->visualOffset) ||
!r.color(&out->unkColor)) {
return failAt("StageConfig tail", r.tell(), err);
}
return true;
}
bool readCount(uint32_t count, size_t recordSize, size_t start, size_t end, const char* what, std::string* err) {
const size_t maxCount = (end > start) ? ((end - start) / recordSize) : 0;
if (count > maxCount) {
if (err) {
std::ostringstream oss;
oss << what << " count " << count << " exceeds section capacity " << maxCount;
*err = oss.str();
}
return false;
}
return true;
}
bool parseDrawDistances(const StageDat& dat, uint32_t start, uint32_t next, std::vector<DrawDistancePoint>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t count = 0;
if (!r.u32(&count)) return failAt("TrackDrawDist count", r.tell(), err);
if (!readCount(count, 8, r.tell(), next, "TrackDrawDist", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; i++) {
DrawDistancePoint p;
if (!r.u32(&p.timeMs) || !r.f32(&p.distance)) return failAt("DrawDistance", r.tell(), err);
out->push_back(p);
}
return true;
}
bool parseTrack(const StageDat& dat, uint32_t start, uint32_t next, std::vector<TrackPiece>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t count = 0;
if (!r.u32(&count)) return failAt("TrackPieceArray count", r.tell(), err);
if (!readCount(count, 16, r.tell(), next, "TrackPieceArray", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; i++) {
TrackPiece p;
if (!r.u32(&p.timeMs) || !r.f32(&p.x) || !r.f32(&p.y) || !r.f32(&p.z)) {
return failAt("TrackPiece", r.tell(), err);
}
out->push_back(p);
}
return true;
}
bool parseNotes(
const StageDat& dat,
uint32_t start,
uint32_t next,
std::vector<std::string>* names,
std::vector<StageNote>* out,
std::string* err) {
if (!names || !out || !validOffset(dat, start)) return false;
names->clear();
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t nameCount = 0;
uint32_t count = 0;
if (!r.u32(&nameCount)) return failAt("NoteArray name count", r.tell(), err);
names->reserve(nameCount);
for (uint32_t i = 0; i < nameCount; ++i) {
std::string name;
if (!r.string8(&name)) return failAt("NoteArray name", r.tell(), err);
names->push_back(std::move(name));
}
if (!r.u32(&count)) return failAt("NoteArray note count", r.tell(), err);
if (!readCount(count, StageNote::kRecordSize, r.tell(), next, "NoteArray", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
StageNote note;
uint8_t rawType = 0;
if (!r.u32(&note.timeMs) ||
!r.u8(&rawType) ||
!r.u8(&note.typeOverride)) {
return failAt("Note prefix", r.tell(), err);
}
note.type = static_cast<StageNoteType>(rawType);
for (int16_t& value : note.params16) {
uint16_t encoded = 0;
if (!r.u16(&encoded)) return failAt("Note s16 fields", r.tell(), err);
value = static_cast<int16_t>(encoded);
}
if (!r.u8(&note.flag24)) return failAt("Note flag24", r.tell(), err);
for (float& value : note.params25) if (!r.f32(&value)) return failAt("Note float fields at +25", r.tell(), err);
if (!r.u8(&note.flag37) || !r.u8(&note.flag38)) return failAt("Note flags at +37", r.tell(), err);
for (float& value : note.params39) if (!r.f32(&value)) return failAt("Note float fields at +39", r.tell(), err);
for (uint32_t& value : note.params55) if (!r.u32(&value)) return failAt("Note u32 fields at +55", r.tell(), err);
if (!r.f32(&note.param67) || !r.u32(&note.param71)) return failAt("Note fields at +67", r.tell(), err);
for (float& value : note.params75) if (!r.f32(&value)) return failAt("Note float fields at +75", r.tell(), err);
if (!r.u32(&note.param95)) return failAt("Note field at +95", r.tell(), err);
out->push_back(note);
}
if (r.tell() != next) {
if (err) {
std::ostringstream oss;
oss << "NoteArray leaves " << (next - r.tell()) << " trailing bytes";
*err = oss.str();
}
return false;
}
return true;
}
bool parseCameras(const StageDat& dat, uint32_t start, uint32_t next, std::vector<CameraPoint>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t count = 0;
if (!r.u32(&count)) return failAt("CameraArray count", r.tell(), err);
if (!readCount(count, 59, r.tell(), next, "CameraArray", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; i++) {
CameraPoint c;
if (!r.u32(&c.timeMs) ||
!r.u8(&c.aMode) ||
!r.u8(&c.fMode) ||
!r.f32(&c.dist) ||
!r.f32(&c.rotationA[0]) ||
!r.f32(&c.rotationA[1]) ||
!r.f32(&c.originOff[0]) ||
!r.f32(&c.originOff[1]) ||
!r.f32(&c.originOff[2]) ||
!r.u8(&c.projType) ||
!r.f32(&c.fieldFar[0]) ||
!r.f32(&c.fieldFar[1]) ||
!r.f32(&c.fieldFar[2]) ||
!r.f32(&c.fieldNear[0]) ||
!r.f32(&c.fieldNear[1]) ||
!r.f32(&c.fieldNear[2]) ||
!r.f32(&c.rotationB)) {
return failAt("Camera", r.tell(), err);
}
out->push_back(c);
}
return true;
}
bool parseParticles(const StageDat& dat, uint32_t start, uint32_t next,
std::vector<ParticlePoint>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t count = 0;
if (!r.u32(&count)) return failAt("ParticleArray count", r.tell(), err);
if (!readCount(count, ParticlePoint::kRecordSize, r.tell(), next, "ParticleArray", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
ParticlePoint p;
if (!r.u32(&p.timeMs) || !r.u32(&p.enabled) || !r.u32(&p.shape) || !r.u32(&p.texture) ||
!r.color(&p.color) || !r.f32(&p.velocity[0]) || !r.f32(&p.velocity[1]) ||
!r.f32(&p.velocity[2]) || !r.f32(&p.repeatMeasure) || !r.f32(&p.lifespanMeasure) ||
!r.u32(&p.groupShapeSize)) {
return failAt("Particle", r.tell(), err);
}
out->push_back(p);
}
if (r.tell() != next) return failAt("ParticleArray trailing bytes", r.tell(), err);
return true;
}
bool parseVisualizer(const StageDat& dat, uint32_t start, uint32_t next,
std::vector<VisualizerPoint>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t count = 0;
if (!r.u32(&count)) return failAt("VisualizerArray count", r.tell(), err);
if (!readCount(count, VisualizerPoint::kRecordSize, r.tell(), next, "VisualizerArray", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
VisualizerPoint v;
if (!r.u32(&v.timeMs) || !r.u32(&v.type) || !r.color(&v.color)) {
return failAt("Visualizer", r.tell(), err);
}
out->push_back(v);
}
if (r.tell() != next) return failAt("VisualizerArray trailing bytes", r.tell(), err);
return true;
}
bool parseBackgroundColors(const StageDat& dat, uint32_t start, uint32_t next,
std::vector<BackgroundColorPoint>* out, std::string* err) {
if (!out || !validOffset(dat, start)) return false;
out->clear();
PatternReader r(dat.bytes, start, next);
uint32_t count = 0;
if (!r.u32(&count)) return failAt("ColorTable count", r.tell(), err);
if (!readCount(count, BackgroundColorPoint::kRecordSize, r.tell(), next, "ColorTable", err)) return false;
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
BackgroundColorPoint c;
uint8_t interpolateToNext = 0;
uint8_t audioReactive = 0;
if (!r.u32(&c.timeMs) || !r.color(&c.topRight) || !r.color(&c.topLeft) ||
!r.color(&c.bottomRight) || !r.color(&c.bottomLeft) ||
!r.u8(&interpolateToNext) || !r.u8(&audioReactive)) {
return failAt("ColorTable entry", r.tell(), err);
}
c.interpolateToNext = interpolateToNext != 0;
c.audioReactive = audioReactive != 0;
out->push_back(c);
}
if (r.tell() != next) return failAt("ColorTable trailing bytes", r.tell(), err);
return true;
}
bool parseTransformArray(PatternReader& r, std::vector<TransformPoint>* out,
const char* what, std::string* err) {
uint32_t count = 0;
if (!r.u32(&count)) return failAt(what, r.tell(), err);
out->clear();
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
TransformPoint p;
uint8_t towards = 0;
uint8_t away = 0;
if (!r.u32(&p.timeMs) || !r.u8(&towards) || !r.u8(&away) ||
!r.f32(&p.value[0]) || !r.f32(&p.value[1]) || !r.f32(&p.value[2])) {
return failAt(what, r.tell(), err);
}
p.tweenTowards = towards != 0;
p.tweenAway = away != 0;
out->push_back(p);
}
return true;
}
bool parseObjects(const StageDat& dat, uint32_t start, uint32_t next,
std::vector<std::string>* modelNames,
std::vector<std::string>* fragmentShaderNames,
std::vector<StageObject>* out, std::string* err) {
if (!modelNames || !fragmentShaderNames || !out || !validOffset(dat, start)) return false;
PatternReader r(dat.bytes, start, next);
modelNames->clear();
fragmentShaderNames->clear();
out->clear();
uint32_t count = 0;
if (!r.u32(&count)) return failAt("ObjectArray model name count", r.tell(), err);
modelNames->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
std::string name;
if (!r.string8(&name)) return failAt("ObjectArray model name", r.tell(), err);
modelNames->push_back(std::move(name));
}
if (!r.u32(&count)) return failAt("ObjectArray shader name count", r.tell(), err);
fragmentShaderNames->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
std::string name;
if (!r.string8(&name)) return failAt("ObjectArray shader name", r.tell(), err);
fragmentShaderNames->push_back(std::move(name));
}
if (!r.u32(&count)) return failAt("ObjectArray count", r.tell(), err);
out->reserve(count);
for (uint32_t i = 0; i < count; ++i) {
StageObject object;
uint8_t wireframe = 0;
uint8_t flashing = 0;
uint8_t unknown = 0;
if (!r.u32(&object.model) || !r.u32(&object.fragmentShader) ||
!r.u8(&wireframe) || !r.u8(&flashing) || !r.u8(&unknown)) {
return failAt("Object prefix", r.tell(), err);
}
object.wireframe = wireframe != 0;
object.flashing = flashing != 0;
object.unknownFlag = unknown != 0;
for (float& value : object.position) if (!r.f32(&value)) return failAt("Object position", r.tell(), err);
for (float& value : object.scale) if (!r.f32(&value)) return failAt("Object scale", r.tell(), err);
for (float& value : object.rotation) if (!r.f32(&value)) return failAt("Object rotation", r.tell(), err);
for (float& value : object.color) if (!r.f32(&value)) return failAt("Object color", r.tell(), err);
for (float& value : object.unknownVector) if (!r.f32(&value)) return failAt("Object vector", r.tell(), err);
uint32_t keyCount = 0;
if (!r.u32(&keyCount)) return failAt("Object visibility count", r.tell(), err);
object.visibility.reserve(keyCount);
for (uint32_t key = 0; key < keyCount; ++key) {
VisibilityPoint p;
uint8_t fadeOut = 0;
uint8_t fadeIn = 0;
uint8_t visible = 0;
if (!r.u32(&p.timeMs) || !r.u8(&fadeOut) || !r.u8(&fadeIn) || !r.u8(&visible)) {
return failAt("Object visibility", r.tell(), err);
}
p.fadeOut = fadeOut != 0;
p.fadeIn = fadeIn != 0;
p.visible = visible != 0;
object.visibility.push_back(p);
}
if (!parseTransformArray(r, &object.movement, "Object movement", err) ||
!parseTransformArray(r, &object.scaling, "Object scaling", err) ||
!parseTransformArray(r, &object.rotations, "Object rotation keys", err)) {
return false;
}
if (!r.u32(&keyCount)) return failAt("Object color count", r.tell(), err);
object.colorChanges.reserve(keyCount);
for (uint32_t key = 0; key < keyCount; ++key) {
ObjectColorPoint p;
uint8_t towards = 0;
uint8_t away = 0;
if (!r.u32(&p.timeMs) || !r.u8(&towards) || !r.u8(&away) || !r.color(&p.color)) {
return failAt("Object color key", r.tell(), err);
}
p.tweenTowards = towards != 0;
p.tweenAway = away != 0;
object.colorChanges.push_back(p);
}
out->push_back(std::move(object));
}
if (r.tell() != next) return failAt("ObjectArray trailing bytes", r.tell(), err);
return true;
}
bool parseObjectParents(const StageDat& dat, uint32_t extensionBase, uint32_t version,
std::vector<StageObject>* objects, std::string* err) {
if (!objects || objects->empty() || version <= 0x29ce) return true;
if (extensionBase > dat.bytes.size() || dat.bytes.size() - extensionBase < 12) {
return true;
}
// The extension begins with relative offsets. game471 seeks base+8 for
// the parent stream, then reads objectCount followed by signed BE int16s.
PatternReader table(dat.bytes, extensionBase + 8, dat.bytes.size());
uint32_t relative = 0;
if (!table.u32(&relative) || relative > dat.bytes.size() - extensionBase) {
return true;
}
const size_t parentStart = static_cast<size_t>(extensionBase) + relative;
PatternReader parents(dat.bytes, parentStart, dat.bytes.size());
uint32_t count = 0;
if (!parents.u32(&count) || count != objects->size()) return true;
for (size_t i = 0; i < objects->size(); ++i) {
uint16_t encoded = 0;
if (!parents.u16(&encoded)) return failAt("ObjectArray parent index", parents.tell(), err);
const int32_t parent = static_cast<int16_t>(encoded);
if (parent >= 0 && static_cast<size_t>(parent) >= objects->size()) {
continue;
}
(*objects)[i].parentIndex = parent;
}
return true;
}
} // namespace
const char* NoteTypeName(uint8_t rawType) {
static constexpr const char* names[] = {
"NONE", "NORMAL", "FLICK", "HOLD", "SCRATCH", "BEAT", "MERRY GO ROUND", "HIDDEN",
"HIDDEN2", "CRITICAL", "SLIDE HOLD", "SLIDE COUNTER", "TURN", "SPIN", "FINISH", "DUAL HOLD",
};
return rawType < (sizeof(names) / sizeof(names[0])) ? names[rawType] : "UNKNOWN";
}
bool ParseStagePattern(const StageDat& dat, ParsedStagePattern* out, std::string* err) {
if (!out) return false;
ParsedStagePattern tmp;
if (!parseHeader(dat, &tmp.header, err)) return false;
if (!validOffset(dat, tmp.header.stageCfg) ||
!validOffset(dat, tmp.header.trackDrawDist) ||
!validOffset(dat, tmp.header.track) ||
!validOffset(dat, tmp.header.camera)) {
if (err) *err = "stage header contains out-of-file offsets";
return false;
}
if (!parseStageConfig(dat, tmp.header.stageCfg, tmp.header.trackDrawDist, &tmp.config, err)) return false;
if (!parseDrawDistances(dat, tmp.header.trackDrawDist, tmp.header.track, &tmp.drawDistances, err)) return false;
if (!parseTrack(dat, tmp.header.track, tmp.header.notes, &tmp.track, err)) return false;
if (!parseNotes(dat, tmp.header.notes, tmp.header.camera, &tmp.noteNames, &tmp.notes, err)) return false;
if (!validOffset(dat, tmp.header.particles) || tmp.header.particles < tmp.header.camera) {
if (err) *err = "invalid camera/particle section order";
return false;
}
if (!parseCameras(dat, tmp.header.camera, tmp.header.particles, &tmp.cameras, err)) return false;
if (!validOffset(dat, tmp.header.visualizer) || tmp.header.visualizer < tmp.header.particles ||
!validOffset(dat, tmp.header.unk1) || tmp.header.unk1 < tmp.header.visualizer ||
!validOffset(dat, tmp.header.colors) || tmp.header.colors < tmp.header.unk1 ||
!validOffset(dat, tmp.header.objects) || tmp.header.objects < tmp.header.colors ||
!validEndOffset(dat, tmp.header.colors2) || tmp.header.colors2 < tmp.header.objects) {
if (err) *err = "invalid background section order";
return false;
}
if (!parseParticles(dat, tmp.header.particles, tmp.header.visualizer, &tmp.particles, err) ||
!parseVisualizer(dat, tmp.header.visualizer, tmp.header.unk1, &tmp.visualizer, err) ||
!parseBackgroundColors(dat, tmp.header.colors, tmp.header.objects, &tmp.backgroundColors, err) ||
!parseObjects(dat, tmp.header.objects, tmp.header.colors2,
&tmp.modelNames, &tmp.fragmentShaderNames, &tmp.objects, err) ||
!parseObjectParents(dat, tmp.header.colors2, tmp.header.unk3, &tmp.objects, err)) {
return false;
}
*out = std::move(tmp);
return true;
}
} // namespace gc
+163
View File
@@ -0,0 +1,163 @@
#include "gc/TumoModel.hpp"
#include <cstddef>
#include <cstring>
#include <fstream>
#include <utility>
#include <vector>
namespace gc {
namespace {
class Reader {
public:
explicit Reader(std::vector<std::uint8_t> bytes) : bytes_(std::move(bytes)) {}
bool u32(std::uint32_t* value) {
if (!value || offset_ + 4 > bytes_.size()) return false;
*value = (static_cast<std::uint32_t>(bytes_[offset_]) << 24) |
(static_cast<std::uint32_t>(bytes_[offset_ + 1]) << 16) |
(static_cast<std::uint32_t>(bytes_[offset_ + 2]) << 8) |
static_cast<std::uint32_t>(bytes_[offset_ + 3]);
offset_ += 4;
return true;
}
bool f32(float* value) {
std::uint32_t encoded = 0;
if (!u32(&encoded) || !value) return false;
std::memcpy(value, &encoded, sizeof(encoded));
return true;
}
bool string8() {
if (offset_ >= bytes_.size()) return false;
const std::size_t length = bytes_[offset_++];
if (length > bytes_.size() - offset_) return false;
offset_ += length;
return true;
}
private:
std::vector<std::uint8_t> bytes_;
std::size_t offset_ = 0;
};
bool readFile(const std::string& path, std::vector<std::uint8_t>* bytes) {
std::ifstream stream(path, std::ios::binary);
if (!stream || !bytes) return false;
stream.seekg(0, std::ios::end);
const std::streamoff size = stream.tellg();
if (size < 0) return false;
stream.seekg(0, std::ios::beg);
bytes->resize(static_cast<std::size_t>(size));
if (!bytes->empty()) {
stream.read(reinterpret_cast<char*>(bytes->data()), size);
}
return static_cast<bool>(stream);
}
bool malformed(std::string* error, const char* message) {
if (error) *error = message;
return false;
}
} // namespace
bool LoadTumoGeometry(const std::string& path, TumoGeometry* out, std::string* error) {
if (!out) return malformed(error, "missing TUMO output");
*out = {};
std::vector<std::uint8_t> bytes;
if (!readFile(path, &bytes)) return malformed(error, "could not read TUMO file");
Reader reader(std::move(bytes));
std::uint32_t meshCount = 0;
if (!reader.u32(&meshCount) || meshCount != 1) {
return malformed(error, "unsupported TUMO mesh layout");
}
std::uint32_t nameCount = 0;
if (!reader.u32(&nameCount) || nameCount > 4096) return malformed(error, "invalid TUMO names");
for (std::uint32_t i = 0; i < nameCount; ++i) {
if (!reader.string8()) return malformed(error, "truncated TUMO name table");
}
std::uint32_t vertexCount = 0;
if (!reader.u32(&vertexCount) || vertexCount > 10000000) {
return malformed(error, "invalid TUMO vertex count");
}
std::vector<TumoVertex> vertices(vertexCount);
for (TumoVertex& vertex : vertices) {
if (!reader.f32(&vertex.x) || !reader.f32(&vertex.y) || !reader.f32(&vertex.z)) {
return malformed(error, "truncated TUMO vertices");
}
}
float ignoredBound = 0.0f;
for (int i = 0; i < 8; ++i) {
if (!reader.f32(&ignoredBound)) return malformed(error, "truncated TUMO bounds");
}
std::uint32_t partCount = 0;
if (!reader.u32(&partCount) || partCount > 100000) {
return malformed(error, "invalid TUMO part count");
}
for (std::uint32_t part = 0; part < partCount; ++part) {
std::uint32_t primitiveType = 0;
std::uint32_t ignoredMaterial = 0;
std::uint32_t ignoredIndexCount = 0;
std::uint32_t polygonCount = 0;
if (!reader.u32(&primitiveType) || !reader.u32(&ignoredMaterial) ||
!reader.u32(&ignoredIndexCount) || !reader.u32(&polygonCount) ||
polygonCount > 1000000) {
return malformed(error, "invalid TUMO part");
}
for (std::uint32_t polygon = 0; polygon < polygonCount; ++polygon) {
std::uint32_t cornerCount = 0;
if (!reader.u32(&cornerCount) || cornerCount > 1000000) {
return malformed(error, "invalid TUMO polygon");
}
std::vector<std::uint32_t> corners;
corners.reserve(cornerCount);
for (std::uint32_t corner = 0; corner < cornerCount; ++corner) {
std::uint32_t index = 0;
float ignoredU = 0.0f;
float ignoredV = 0.0f;
if (!reader.u32(&index) || !reader.f32(&ignoredU) || !reader.f32(&ignoredV) ||
index >= vertices.size()) {
return malformed(error, "invalid TUMO polygon corner");
}
corners.push_back(index);
}
if (primitiveType == 1) {
for (std::size_t corner = 0; corner + 1 < corners.size(); corner += 2) {
out->solidLines.push_back(vertices[corners[corner]]);
out->solidLines.push_back(vertices[corners[corner + 1]]);
}
} else {
for (std::size_t corner = 1; corner + 1 < corners.size(); ++corner) {
out->triangles.push_back(vertices[corners[0]]);
out->triangles.push_back(vertices[corners[corner]]);
out->triangles.push_back(vertices[corners[corner + 1]]);
}
}
}
}
std::uint32_t lineCount = 0;
if (!reader.u32(&lineCount) || lineCount > 10000000) {
return malformed(error, "invalid TUMO edge count");
}
out->wireframeLines.reserve(static_cast<std::size_t>(lineCount) * 2);
for (std::uint64_t line = 0; line < static_cast<std::uint64_t>(lineCount) * 2; ++line) {
std::uint32_t index = 0;
if (!reader.u32(&index) || index >= vertices.size()) {
return malformed(error, "invalid TUMO edge");
}
out->wireframeLines.push_back(vertices[index]);
}
if (out->triangles.empty() && out->solidLines.empty() && out->wireframeLines.empty()) {
return malformed(error, "TUMO model contains no geometry");
}
return true;
}
} // namespace gc
+49
View File
@@ -0,0 +1,49 @@
#include "gc/GcTargetEffect.hpp"
#include <cstdlib>
#include <iostream>
#include <set>
#include <string>
int main(int argc, char** argv) {
if (argc < 3) {
std::cerr << "usage: opencoaster-effect-probe EFC_DATA UV_DATA [first [last]]\n";
return 2;
}
GcTargetEffectBank bank;
std::string error;
if (!bank.load(argv[1], argv[2], &error)) {
std::cerr << error << '\n';
return 1;
}
const int first = argc > 3 ? std::atoi(argv[3]) : 0;
const int last = argc > 4 ? std::atoi(argv[4]) : first;
const int uvBase = argc > 5 ? std::atoi(argv[5]) : 0;
for (int effect = first; effect <= last; ++effect) {
std::cout << "effect " << effect << " lifetime=" << bank.lifetime(effect) << '\n';
const int lifetime = bank.lifetime(effect);
const int sampleTicks[] = {0, 1, lifetime / 2, lifetime > 0 ? lifetime - 1 : 0};
std::set<int> seenTicks;
for (const int tick : sampleTicks) {
if (!seenTicks.insert(tick).second) continue;
const auto sprites = bank.evaluate(effect, static_cast<float>(tick), uvBase);
std::cout << " tick " << tick << " sprites=" << sprites.size() << '\n';
for (const auto& sprite : sprites) {
const GcUvCell* cell = bank.uvCell(sprite.uvRecord, sprite.frame);
std::cout << " uv=" << sprite.uvRecord << " tex="
<< bank.uvTexture(sprite.uvRecord) << " frame=" << sprite.frame;
if (cell) {
std::cout << " cell=" << cell->x << ',' << cell->y << ' '
<< cell->width << 'x' << cell->height;
}
std::cout << " pos=" << sprite.offsetPixels.x << ',' << sprite.offsetPixels.y
<< " scale=" << sprite.scale.x << ',' << sprite.scale.y
<< " rot=" << sprite.rotationDegrees
<< " rgba=" << sprite.color.r << ',' << sprite.color.g << ','
<< sprite.color.b << ',' << sprite.color.a << '\n';
}
}
}
}
+99
View File
@@ -0,0 +1,99 @@
#include "gc/MtxArchive.hpp"
#include "gc/RvbScene.hpp"
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <limits>
#include <string>
#include <vector>
namespace fs = std::filesystem;
namespace {
bool readFile(const fs::path& path, std::vector<uint8_t>* bytes) {
std::ifstream file(path, std::ios::binary);
if (!file) return false;
bytes->assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
return true;
}
size_t textureIndex(const gc::RvbImageResource& image) {
static constexpr char prefix[] = "Image";
if (image.symbolName.rfind(prefix, 0) != 0 || image.symbolName.size() <= 5) {
return std::numeric_limits<size_t>::max();
}
size_t number = 0;
for (size_t i = 5; i < image.symbolName.size(); ++i) {
const char c = image.symbolName[i];
if (c < '0' || c > '9') return std::numeric_limits<size_t>::max();
number = number * 10 + static_cast<size_t>(c - '0');
}
return number == 0 ? std::numeric_limits<size_t>::max() : number - 1;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 3 || argc > 4) {
std::cerr << "Usage: " << argv[0] << " <scene.rvb> <textures.mtx> [output-dir]\n";
return 2;
}
std::vector<uint8_t> rvbBytes;
std::vector<uint8_t> mtxBytes;
if (!readFile(argv[1], &rvbBytes) || !readFile(argv[2], &mtxBytes)) {
std::cerr << "could not read RVB/MTX input\n";
return 1;
}
gc::RvbScene scene;
gc::MtxArchive archive;
std::string error;
if (!gc::ParseRvbScene(rvbBytes, &scene, &error) ||
!gc::ParseMtxArchive(mtxBytes, &archive, &error)) {
std::cerr << "parse failed: " << error << '\n';
return 1;
}
if (scene.images.size() != archive.textures.size()) {
std::cerr << "resource mismatch: RVB images=" << scene.images.size()
<< " MTX textures=" << archive.textures.size() << '\n';
return 1;
}
const bool extract = argc == 4;
const fs::path output = extract ? fs::path(argv[3]) : fs::path();
if (extract) fs::create_directories(output);
for (const gc::RvbImageResource& image : scene.images) {
const size_t index = textureIndex(image);
if (index >= archive.textures.size()) {
std::cerr << "invalid MTX symbol index: " << image.symbolName << '\n';
return 1;
}
const gc::MtxTexture& texture = archive.textures[index];
if (image.width != texture.width || image.height != texture.height) {
std::cerr << "dimension mismatch at " << index << ": " << image.symbolName
<< " RVB=" << image.width << 'x' << image.height
<< " MTX=" << texture.width << 'x' << texture.height << '\n';
return 1;
}
std::cout << index << ' ' << image.symbolName << ' ' << texture.width << 'x'
<< texture.height << " bytes=" << texture.size << '\n';
if (extract) {
std::vector<uint8_t> dds;
if (!gc::ExtractMtxTextureDds(mtxBytes, texture, &dds, &error)) {
std::cerr << "extract failed: " << error << '\n';
return 1;
}
std::ofstream file(output / (image.symbolName + ".dds"), std::ios::binary);
file.write(reinterpret_cast<const char*>(dds.data()),
static_cast<std::streamsize>(dds.size()));
if (!file) {
std::cerr << "could not write extracted DDS\n";
return 1;
}
}
}
return 0;
}
+117
View File
@@ -0,0 +1,117 @@
#include "gc/RvbScene.hpp"
#include "gc/RvbLayout.hpp"
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <string>
#include <unordered_map>
#include <vector>
namespace {
void printNode(const gc::RvbNode& node, unsigned depth) {
std::cout << " " << std::string(depth * 2, ' ') << node.tag
<< " @0x" << std::hex << node.offset << "/0x" << node.size
<< std::dec << " local=" << node.localDataSize << '\n';
for (const gc::RvbNode& child : node.children) printNode(child, depth + 1);
}
bool probe(const std::string& path, bool tree, const std::string& symbol,
const gc::RvbSnapshotState& state) {
std::ifstream file(path, std::ios::binary);
if (!file) {
std::cerr << path << ": could not open\n";
return false;
}
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
gc::RvbScene scene;
std::string error;
if (!gc::ParseRvbScene(bytes, &scene, &error)) {
std::cerr << path << ": parse failed: " << error << '\n';
return false;
}
std::vector<gc::RvbImageDraw> snapshot;
const bool built = symbol.empty()
? gc::BuildRvbSnapshot(bytes, scene, state, &snapshot, &error)
: gc::BuildRvbSymbolSnapshot(bytes, scene, symbol, state, &snapshot, &error);
if (!built) {
std::cerr << path << ": snapshot failed: " << error << '\n';
return false;
}
std::cout << path << "\n movie=" << scene.sourceWidth << 'x' << scene.sourceHeight
<< " @ " << static_cast<unsigned>(scene.framesPerSecond) << " fps"
<< " bindings=" << scene.bindings.size()
<< " images=" << scene.images.size()
<< " initialDraws=" << snapshot.size() << "\n chunks:";
for (const gc::RvbChunk& chunk : scene.chunks) {
std::cout << ' ' << chunk.tag << "@0x" << std::hex << chunk.offset
<< "/0x" << chunk.size << std::dec;
}
std::cout << "\n PREP bindings:\n";
for (const gc::RvbBinding& binding : scene.bindings) {
std::cout << " " << std::left << std::setw(28) << binding.action
<< ' ' << binding.instancePath << '\n';
}
if (tree) {
std::cout << " animation tree:\n";
for (const gc::RvbNode& root : scene.roots) printNode(root, 0);
std::cout << " initial draws:\n";
for (const gc::RvbImageDraw& draw : snapshot) {
std::cout << " " << draw.imageSymbol << " depth=" << draw.depth
<< " path=" << draw.instancePath
<< " rgba=(" << draw.color[0] << ',' << draw.color[1] << ','
<< draw.color[2] << ',' << draw.alpha << ')'
<< " tl=(" << draw.corners[0][0] << ',' << draw.corners[0][1]
<< ") br=(" << draw.corners[3][0] << ',' << draw.corners[3][1]
<< ")\n";
}
}
return true;
}
} // namespace
int main(int argc, char** argv) {
bool tree = false;
std::string symbol;
gc::RvbSnapshotState state;
int firstPath = 1;
while (firstPath < argc) {
const std::string option = argv[firstPath];
if (option == "--tree") {
tree = true;
++firstPath;
} else if (option == "--include-other") {
state.includeRootOther = true;
++firstPath;
} else if (option == "--symbol" && firstPath + 1 < argc) {
symbol = argv[firstPath + 1];
firstPath += 2;
} else if (option == "--state" && firstPath + 1 < argc) {
const std::string value = argv[firstPath + 1];
const size_t equals = value.find('=');
if (equals == std::string::npos) {
std::cerr << "--state expects /path=frame_label\n";
return 2;
}
state.frameByPath[value.substr(0, equals)] = value.substr(equals + 1);
firstPath += 2;
} else {
break;
}
}
if (argc <= firstPath) {
std::cerr << "Usage: " << argv[0]
<< " [--tree] [--include-other] [--symbol name]"
" [--state /path=frame] <scene.rvb> [scene.rvb ...]\n";
return 2;
}
bool ok = true;
for (int i = firstPath; i < argc; ++i) ok = probe(argv[i], tree, symbol, state) && ok;
return ok ? 0 : 1;
}
+163
View File
@@ -0,0 +1,163 @@
#include "gc/StageDat.hpp"
#include "gc/StagePattern.hpp"
#include <algorithm>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <map>
#include <string>
namespace {
bool probe(const std::string& path) {
gc::StageDat dat;
std::string err;
if (!gc::StageDat::LoadFromFile(path, dat, &err)) {
std::cerr << path << ": load failed: " << err << '\n';
return false;
}
gc::ParsedStagePattern stage;
if (!gc::ParseStagePattern(dat, &stage, &err)) {
std::cerr << path << ": parse failed: " << err << '\n';
return false;
}
std::map<uint8_t, size_t> types;
for (const gc::StageNote& note : stage.notes) ++types[static_cast<uint8_t>(note.type)];
std::map<uint8_t, size_t> cameraAnchorModes;
std::map<uint8_t, size_t> cameraFadeModes;
std::map<uint8_t, size_t> cameraProjectionTypes;
for (const gc::CameraPoint& camera : stage.cameras) {
++cameraAnchorModes[camera.aMode];
++cameraFadeModes[camera.fMode];
++cameraProjectionTypes[camera.projType];
}
size_t parentedObjects = 0;
size_t wireframeObjects = 0;
size_t flashingObjects = 0;
size_t noGlobalFadeObjects = 0;
size_t translucentObjects = 0;
for (const gc::StageObject& object : stage.objects) {
if (object.parentIndex >= 0) ++parentedObjects;
if (object.wireframe) ++wireframeObjects;
if (object.flashing) ++flashingObjects;
if (object.unknownFlag) ++noGlobalFadeObjects;
if (object.color[3] < 0.999f) ++translucentObjects;
}
std::cout << path
<< "\n chart=" << stage.config.chartName
<< " bgm=" << stage.config.bgmName
<< "\n track=" << stage.track.size()
<< " drawDistanceKeys=" << stage.drawDistances.size()
<< " drawBehind=" << stage.config.backwardsDrawDist
<< " drawAhead=" << stage.config.forwardDrawDist
<< " audioOffset=" << static_cast<unsigned>(stage.config.audioOffset)
<< " visualOffset=" << stage.config.visualOffset
<< " trackColors=(" << static_cast<unsigned>(stage.config.trackAheadColor.r) << ','
<< static_cast<unsigned>(stage.config.trackAheadColor.g) << ','
<< static_cast<unsigned>(stage.config.trackAheadColor.b) << ")->("
<< static_cast<unsigned>(stage.config.trackBehindColor.r) << ','
<< static_cast<unsigned>(stage.config.trackBehindColor.g) << ','
<< static_cast<unsigned>(stage.config.trackBehindColor.b) << ')'
<< " notes=" << stage.notes.size()
<< " cameras=" << stage.cameras.size()
<< " noteNames=" << stage.noteNames.size()
<< "\n background: particles=" << stage.particles.size()
<< " visualizerKeys=" << stage.visualizer.size()
<< " colorKeys=" << stage.backgroundColors.size()
<< " models=" << stage.modelNames.size()
<< " objects=" << stage.objects.size()
<< " parented=" << parentedObjects
<< " wire=" << wireframeObjects
<< " flashing=" << flashingObjects
<< " noGlobalFade=" << noGlobalFadeObjects
<< " translucent=" << translucentObjects;
if (!stage.notes.empty()) {
const auto [lo, hi] = std::minmax_element(
stage.notes.begin(), stage.notes.end(),
[](const gc::StageNote& a, const gc::StageNote& b) { return a.timeMs < b.timeMs; });
std::cout << " noteTimeMs=" << lo->timeMs << ".." << hi->timeMs;
}
if (!stage.particles.empty()) {
std::cout << "\n particleKeys:";
for (const auto& particle : stage.particles) {
std::cout << ' ' << particle.timeMs << ":on=" << particle.enabled
<< ",shape=" << particle.shape << ",tex=" << particle.texture
<< ",repeat=" << particle.repeatMeasure
<< ",life=" << particle.lifespanMeasure
<< ",group=" << particle.groupShapeSize;
}
}
if (!stage.visualizer.empty()) {
std::map<std::uint32_t, std::size_t> visualizerTypes;
for (const auto& key : stage.visualizer) ++visualizerTypes[key.type];
std::cout << "\n visualizerTypes:";
for (const auto& [type, count] : visualizerTypes) std::cout << ' ' << type << '=' << count;
}
if (!stage.drawDistances.empty()) {
std::cout << "\n drawDistance=" << stage.drawDistances.front().timeMs << ':'
<< stage.drawDistances.front().distance << ".."
<< stage.drawDistances.back().timeMs << ':'
<< stage.drawDistances.back().distance;
}
if (!stage.track.empty()) {
const auto& p = stage.track.front();
std::cout << "\n firstTrack=" << p.timeMs << ":(" << p.x << ',' << p.y << ',' << p.z << ')';
if (stage.track.size() > 1) {
const auto& q = stage.track[1];
std::cout << " nextTrack=" << q.timeMs << ":(" << q.x << ',' << q.y << ',' << q.z << ')';
}
}
if (!stage.cameras.empty()) {
const auto& c = stage.cameras.front();
std::cout << " firstCamera=" << c.timeMs << " dist=" << c.dist
<< " rot=(" << c.rotationA[0] << ',' << c.rotationA[1] << ',' << c.rotationB << ')'
<< " off=(" << c.originOff[0] << ',' << c.originOff[1] << ',' << c.originOff[2] << ')';
std::cout << "\n cameraModes: aMode";
for (const auto& [mode, count] : cameraAnchorModes) {
std::cout << ' ' << static_cast<unsigned>(mode) << '=' << count;
}
std::cout << " fMode";
for (const auto& [mode, count] : cameraFadeModes) {
std::cout << ' ' << static_cast<unsigned>(mode) << '=' << count;
}
std::cout << " projType";
for (const auto& [mode, count] : cameraProjectionTypes) {
std::cout << ' ' << static_cast<unsigned>(mode) << '=' << count;
}
}
std::cout << "\n noteTypes:";
for (const auto& [type, count] : types) {
std::cout << " 0x" << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<unsigned>(type) << std::dec << '/' << gc::NoteTypeName(type) << '=' << count;
}
if (!stage.fragmentShaderNames.empty()) {
std::cout << "\n fragmentShaders:";
for (size_t i = 0; i < stage.fragmentShaderNames.size(); ++i) {
std::cout << ' ' << i << '=' << stage.fragmentShaderNames[i];
}
}
if (!stage.modelNames.empty()) {
std::cout << "\n modelNames:";
for (size_t i = 0; i < stage.modelNames.size(); ++i) {
std::cout << ' ' << i << '=' << stage.modelNames[i];
}
}
std::cout << '\n';
return true;
}
} // namespace
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <stage.dat> [stage.dat ...]\n";
return 2;
}
bool ok = true;
for (int i = 1; i < argc; ++i) ok = probe(argv[i]) && ok;
return ok ? 0 : 1;
}