#include "gc/StagePattern.hpp" #include #include #include namespace gc { namespace { class PatternReader { public: PatternReader(const std::vector& 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((static_cast(bytes_[pos_]) << 8) | static_cast(bytes_[pos_ + 1])); pos_ += 2; return true; } bool u32(uint32_t* out) { if (!out || !need(4)) return false; *out = (static_cast(bytes_[pos_ + 0]) << 24) | (static_cast(bytes_[pos_ + 1]) << 16) | (static_cast(bytes_[pos_ + 2]) << 8) | (static_cast(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(bytes_.data() + pos_), len); while (!out->empty() && out->back() == '\0') out->pop_back(); pos_ += len; return true; } const std::vector& 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& 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* 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* 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* names, std::vector* 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(¬e.timeMs) || !r.u8(&rawType) || !r.u8(¬e.typeOverride)) { return failAt("Note prefix", r.tell(), err); } note.type = static_cast(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(encoded); } if (!r.u8(¬e.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(¬e.flag37) || !r.u8(¬e.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(¬e.param67) || !r.u32(¬e.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(¬e.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* 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* 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* 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* 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* 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* modelNames, std::vector* fragmentShaderNames, std::vector* 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* 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(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(encoded); if (parent >= 0 && static_cast(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