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