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
+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