#include "gc/EventStream.hpp" #include "gc/NoteTypes.hpp" #include "gc/StageCatalog.hpp" #include "gc/StageDat.hpp" #include "gc/StagePattern.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum class RenderTimeMode { Auto, Time, Fit, Index, }; enum class ExportAtMode { Auto, // pick bars if time looks meaningful, else index Beats, // raw timestamp as beats (string) Bars, // bar:fracOfBar (string) Seconds, // not implemented yet (needs BPM map); falls back to auto Index, // uniform by index }; static bool parseExportAtMode(const std::string& s, ExportAtMode* out) { if (!out) return false; if (s == "auto") { *out = ExportAtMode::Auto; return true; } if (s == "beats") { *out = ExportAtMode::Beats; return true; } if (s == "bars") { *out = ExportAtMode::Bars; return true; } if (s == "seconds") { *out = ExportAtMode::Seconds; return true; } if (s == "index") { *out = ExportAtMode::Index; return true; } return false; } static bool parseRenderTimeMode(const std::string& s, RenderTimeMode* out) { if (!out) return false; if (s == "auto") { *out = RenderTimeMode::Auto; return true; } if (s == "time") { *out = RenderTimeMode::Time; return true; } if (s == "fit") { *out = RenderTimeMode::Fit; return true; } if (s == "index") { *out = RenderTimeMode::Index; return true; } return false; } static void printUsage(const char* argv0) { std::cerr << "Usage:\n" << " " << argv0 << " [--dump N] [--section IDX] [--align N] [--svg OUT.svg] [--svg-y value|type]\n" << " [--type-max N] [--no-filter] [--stats] [--stats-top N] [--stats-all] [--stats-keep-zero] [--find-align]\n" << " [--survey] [--strings] [--strings-section IDX] [--strings-min N] [--strings-max N] [--strings-all]\n" << " [--meta] [--meta-section IDX] (scan u16-length ASCII tokens; useful for backgrounds/ids)\n" << " [--stats-section IDX] [--stats-rs auto|12|16]\n" << " [--raw] (treat input as a single section, for files like *_ext.dat)\n" << " [--rs auto|12|16] (force event record size for --dump/--svg section picking)\n" << " [--find-align-section IDX] [--find-align-rs 12|16]\n" << " [--render OUT.svg] [--track-section IDX] [--note-section IDX] [--note-align N]\n" << " [--render-time auto|time|fit|index] [--render-keep-type0] [--render-notes-only] [--render-max-notes N]\n" << " " << argv0 << " --track-info \n" << " " << argv0 << " --play [--play-what bgm|shot] [--play-tool auto|ffplay|aplay|paplay|mpv]\n" << " " << argv0 << " --viz [--viz-what bgm|shot] [--viz-notes-only]\n" << " " << argv0 << " --export-json \n" << " [--export-at auto|bars|beats|index|seconds] [--export-relative] [--export-notes-only]\n" << " " << argv0 << " --export-gcsim \n" << " [--gcsim-what bgm|shot] [--gcsim-bpm N] [--gcsim-title TITLE]\n" << " " << argv0 << " --export-gcsim-project \n" << " [--stage-param PATH] [--ac-id ID] [--music WAV] [--gcsim-what bgm|shot] [--gcsim-bpm N] [--gcsim-title TITLE]\n" << " " << argv0 << " --export-vectomapper \n" << " [--note-section IDX] [--note-align N] (track/camera from docs/stage.pat; notes still heuristic)\n" << "\n" << "Example:\n" << " " << argv0 << " GC/data/stage/ac_10pt8tion_easy.dat --dump 20\n" << " " << argv0 << " GC/data/stage/ac_10pt8tion_easy.dat --dump 20 --section 4\n" << " " << argv0 << " GC/data/stage/ac_10pt8tion_easy.dat --svg out.svg\n" << " " << argv0 << " GC/data/stage/ac_10pt8tion_hard.dat --stats\n" << " " << argv0 << " GC/data/stage/ac_10pt8tion_hard.dat --find-align\n" << " " << argv0 << " GC/data/stage/ac_10pt8tion_hard.dat --render out.svg\n" << " " << argv0 << " GC/data/boot/stage_param.dat --track-info ac_10pt8tion_hard\n" << " " << argv0 << " GC/data/boot/stage_param.dat --play ac_10pt8tion_hard\n" << " " << argv0 << " GC/data/boot/stage_param.dat --viz ac_10pt8tion_hard /tmp/viz.html\n" << " " << argv0 << " GC/data/boot/stage_param.dat --export-json ac_10pt8tion_hard /tmp/track.json\n"; } static bool isFiniteF(float f) { return std::isfinite(static_cast(f)); } static std::string rgbHexForType(uint32_t type) { if (type == 0 || type == 0xFFFFFFFFu) return "#999999"; // Deterministic hash -> RGB, trying to avoid too-dark colors. const uint32_t x = static_cast(type) * 0x9e37u + 0x7f4a7c15u; const uint8_t r = static_cast(64 + ((x >> 0) & 0x7F)); const uint8_t g = static_cast(64 + ((x >> 8) & 0x7F)); const uint8_t b = static_cast(64 + ((x >> 16) & 0x7F)); std::ostringstream oss; oss << "#" << std::hex << std::setw(2) << std::setfill('0') << static_cast(r) << std::hex << std::setw(2) << std::setfill('0') << static_cast(g) << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); return oss.str(); } static bool isGarbage12(const gc::GameEvent& e) { if (!isFiniteF(e.timestamp)) return true; if (e.timestamp < -1.0f || e.timestamp > 1.0e6f) return true; if (e.type == 0xFFFFFFFFu) return true; // value may legitimately be NaN for some opcodes, but as a default filter it helps a lot. if (!isFiniteF(e.value)) return true; if (std::fabs(static_cast(e.value)) > 1.0e7) return true; return false; } static bool parseU32(const char* s, uint32_t* out) { if (!s || !out) return false; errno = 0; char* end = nullptr; unsigned long v = std::strtoul(s, &end, 0); // accepts 123 or 0x7b if (errno != 0 || end == s || *end != '\0') return false; if (v > 0xFFFFFFFFul) return false; *out = static_cast(v); return true; } static uint32_t u32be_bytes(const std::vector& b, size_t off) { return (static_cast(b[off + 0]) << 24) | (static_cast(b[off + 1]) << 16) | (static_cast(b[off + 2]) << 8) | (static_cast(b[off + 3]) << 0); } static float f32be_bytes(const std::vector& b, size_t off) { const uint32_t u = u32be_bytes(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 readWholeFile(const std::string& path, std::vector* out, std::string* err) { if (!out) return false; out->clear(); std::ifstream in(path, std::ios::binary); if (!in.is_open()) { if (err) *err = "could not open file"; return false; } in.seekg(0, std::ios::end); std::streamoff sz = in.tellg(); in.seekg(0, std::ios::beg); if (sz < 0) { if (err) *err = "could not stat file size"; return false; } out->resize(static_cast(sz)); if (!out->empty()) in.read(reinterpret_cast(out->data()), static_cast(out->size())); if (!in.good() && !in.eof()) { if (err) *err = "read failed"; return false; } return true; } static bool isAsciiPrintable(uint8_t c) { return (c >= 0x20 && c <= 0x7E); } struct AsciiStringHit { size_t off = 0; // absolute offset in file std::string s; }; static bool isLikelyInterestingToken(const std::string& s) { if (s.size() < 4) return false; if (s.rfind("ac_", 0) == 0) return true; if (s.rfind("bgm_", 0) == 0) return true; if (s.find('/') != std::string::npos) return true; if (s.find('\\') != std::string::npos) return true; if (s.find(".wav") != std::string::npos) return true; if (s.find(".dat") != std::string::npos) return true; if (s.find(".png") != std::string::npos) return true; if (s.find(".dds") != std::string::npos) return true; if (s.find(".tga") != std::string::npos) return true; if (s.find("shader") != std::string::npos) return true; if (s.find("tex") != std::string::npos) return true; if (s.find("bg") != std::string::npos && s.size() <= 32) return true; return false; } static bool isSaneAsciiToken(const std::string& s) { if (s.size() < 4) return false; size_t good = 0; size_t alnum = 0; for (unsigned char c : s) { if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) { good++; alnum++; continue; } // Common separators in file ids / paths / keys. if (c == '_' || c == '-' || c == '.' || c == '/' || c == '\\' || c == ':' || c == ' ') { good++; continue; } } const double ratio = static_cast(good) / static_cast(s.size()); // Avoid random punctuation soup: require mostly "path-like" chars and at least some alnum. return ratio >= 0.85 && alnum >= 3; } static std::vector scanAsciiStrings( const std::vector& bytes, size_t start, size_t end, size_t minLen, size_t maxHits, bool keepAll) { std::vector out; if (start >= end || start >= bytes.size()) return out; end = std::min(end, bytes.size()); if (minLen < 2) minLen = 2; if (maxHits == 0) maxHits = 1; std::unordered_set seen; size_t i = start; while (i < end) { while (i < end && !isAsciiPrintable(bytes[i])) i++; size_t j = i; while (j < end && isAsciiPrintable(bytes[j])) j++; const size_t len = (j > i) ? (j - i) : 0; if (len >= minLen) { std::string s(reinterpret_cast(&bytes[i]), len); if (s.size() > 512) s.resize(512); if (!keepAll && !isSaneAsciiToken(s)) { i = (j > i) ? j : (i + 1); continue; } if (keepAll || isLikelyInterestingToken(s)) { if (seen.insert(s).second) { out.push_back(AsciiStringHit{i, std::move(s)}); if (out.size() >= maxHits) break; } } } i = (j > i) ? j : (i + 1); } return out; } static double shannonEntropyBytes(const std::vector& bytes, size_t start, size_t end) { if (start >= end || start >= bytes.size()) return 0.0; end = std::min(end, bytes.size()); const size_t n = end - start; if (n == 0) return 0.0; uint32_t hist[256]; std::memset(hist, 0, sizeof(hist)); for (size_t i = start; i < end; i++) hist[bytes[i]]++; double ent = 0.0; for (size_t c = 0; c < 256; c++) { if (!hist[c]) continue; const double p = static_cast(hist[c]) / static_cast(n); ent -= p * (std::log(p) / std::log(2.0)); } return ent; // bits per byte (0..8) } static uint64_t countVec3TriplesF32BE(const std::vector& bytes, size_t start, size_t end) { if (start >= end || start >= bytes.size()) return 0; end = std::min(end, bytes.size()); if (end - start < 12) return 0; uint64_t hits = 0; // Scan in 4-byte steps; count a hit when 3 consecutive floats are finite and not absurd. for (size_t off = start; off + 12 <= end; off += 4) { const float a = f32be_bytes(bytes, off + 0); const float b = f32be_bytes(bytes, off + 4); const float c = f32be_bytes(bytes, off + 8); if (!isFiniteF(a) || !isFiniteF(b) || !isFiniteF(c)) continue; if (std::fabs(static_cast(a)) > 1.0e5) continue; if (std::fabs(static_cast(b)) > 1.0e5) continue; if (std::fabs(static_cast(c)) > 1.0e5) continue; hits++; } return hits; } static std::filesystem::path guessGcRootFromStageParamPath(const std::filesystem::path& stageParamPath) { // stageParam.dat usually lives at: /data/boot/stage_param.dat std::filesystem::path p = stageParamPath; if (p.has_filename()) p = p.parent_path(); for (int i = 0; i < 8; i++) { if (p.filename() == "data") return p.parent_path(); if (!p.has_parent_path()) break; p = p.parent_path(); } return std::filesystem::current_path(); } static std::string shEscapeSingleQuotes(const std::string& s) { // POSIX-ish /bin/sh escaping using single quotes. // abc'd -> 'abc'"'"'d' std::string out; out.reserve(s.size() + 8); out.push_back('\''); for (char c : s) { if (c == '\'') out += "'\"'\"'"; else out.push_back(c); } out.push_back('\''); return out; } static bool tryReadWavDurationSec(const std::filesystem::path& wavPath, double* outSec, std::string* err) { if (outSec) *outSec = 0.0; std::ifstream in(wavPath, std::ios::binary); if (!in.is_open()) { if (err) *err = "could not open wav"; return false; } auto readU32le = [&](uint32_t* v) -> bool { uint8_t b[4]; if (!in.read(reinterpret_cast(b), 4)) return false; *v = (static_cast(b[0]) << 0) | (static_cast(b[1]) << 8) | (static_cast(b[2]) << 16) | (static_cast(b[3]) << 24); return true; }; auto readU16le = [&](uint16_t* v) -> bool { uint8_t b[2]; if (!in.read(reinterpret_cast(b), 2)) return false; *v = static_cast((static_cast(b[0]) << 0) | (static_cast(b[1]) << 8)); return true; }; auto readFourCC = [&](char out[5]) -> bool { char c[4]; if (!in.read(c, 4)) return false; out[0] = c[0]; out[1] = c[1]; out[2] = c[2]; out[3] = c[3]; out[4] = '\0'; return true; }; char riff[5] = {0}, wave[5] = {0}; uint32_t riffSize = 0; if (!readFourCC(riff) || std::string(riff) != "RIFF") { if (err) *err = "not RIFF"; return false; } if (!readU32le(&riffSize)) { if (err) *err = "short file"; return false; } if (!readFourCC(wave) || std::string(wave) != "WAVE") { if (err) *err = "not WAVE"; return false; } uint16_t fmtAudioFormat = 0; uint16_t fmtNumChannels = 0; uint32_t fmtSampleRate = 0; uint32_t fmtByteRate = 0; uint16_t fmtBlockAlign = 0; uint16_t fmtBitsPerSample = 0; uint32_t dataSize = 0; bool haveFmt = false; bool haveData = false; // Walk chunks. while (in.good() && (!haveFmt || !haveData)) { char id[5] = {0}; uint32_t sz = 0; if (!readFourCC(id)) break; if (!readU32le(&sz)) break; const std::string sid(id); std::streamoff payloadStart = in.tellg(); if (sid == "fmt ") { if (sz < 16) { if (err) *err = "fmt chunk too small"; return false; } if (!readU16le(&fmtAudioFormat)) return false; if (!readU16le(&fmtNumChannels)) return false; if (!readU32le(&fmtSampleRate)) return false; if (!readU32le(&fmtByteRate)) return false; if (!readU16le(&fmtBlockAlign)) return false; if (!readU16le(&fmtBitsPerSample)) return false; haveFmt = true; } else if (sid == "data") { dataSize = sz; haveData = true; } // Seek to end of chunk (account for what we already read in fmt) in.seekg(payloadStart + static_cast(sz), std::ios::beg); if (sz & 1) in.seekg(1, std::ios::cur); // pad byte } if (!haveFmt || !haveData) { if (err) *err = "missing fmt/data chunk"; return false; } if (fmtSampleRate == 0 || fmtNumChannels == 0 || fmtBitsPerSample == 0) { if (err) *err = "invalid fmt"; return false; } const double bytesPerSec = static_cast(fmtSampleRate) * static_cast(fmtNumChannels) * static_cast(fmtBitsPerSample / 8.0); if (bytesPerSec <= 0.0) { if (err) *err = "invalid bytes/sec"; return false; } if (outSec) *outSec = static_cast(dataSize) / bytesPerSec; return true; } static uint32_t gcdU32(uint32_t a, uint32_t b) { while (b) { uint32_t t = a % b; a = b; b = t; } return a; } static bool approxToRational(double x, const std::vector& dens, uint32_t* outNum, uint32_t* outDen, double tol = 1.0e-4) { if (!outNum || !outDen) return false; if (!std::isfinite(x)) return false; if (x < 0.0) x = -x; uint32_t bestN = 0, bestD = 1; double bestErr = 1e100; for (uint32_t d : dens) { if (d == 0) continue; const double n = std::round(x * static_cast(d)); const double v = n / static_cast(d); const double e = std::fabs(v - x); if (e < bestErr) { bestErr = e; bestN = static_cast(std::max(0.0, n)); bestD = d; } } if (bestErr > tol) return false; const uint32_t g = gcdU32(bestN, bestD); *outNum = bestN / (g ? g : 1); *outDen = bestD / (g ? g : 1); return true; } static std::string fmtFixed(double x, int digits) { std::ostringstream oss; oss << std::fixed << std::setprecision(digits) << x; return oss.str(); } static std::string jsonEscape(const std::string& s) { std::ostringstream oss; for (unsigned char c : s) { switch (c) { case '\\': oss << "\\\\"; break; case '"': oss << "\\\""; break; case '\n': oss << "\\n"; break; case '\r': oss << "\\r"; break; case '\t': oss << "\\t"; break; default: if (c < 0x20) { oss << "\\u" << std::hex << std::setw(4) << std::setfill('0') << static_cast(c) << std::dec << std::setfill(' '); } else { oss << static_cast(c); } } } return oss.str(); } static std::string typeToExportString(uint32_t type) { switch (static_cast(type)) { case gc::NoteType::Tap: return "hit"; case gc::NoteType::Critical: return "critical"; case gc::NoteType::HoldStart: return "hold"; case gc::NoteType::HoldEnd: return "hold"; case gc::NoteType::DualHold: return "dualhold"; case gc::NoteType::Slide: return "slide"; case gc::NoteType::DualSlide: return "dualslide"; case gc::NoteType::SlideHold: return "slidehold"; default: break; } // Legacy / unknown in our hypothesis if (type == 0x100u) return "hit2"; if (type == 0xA00u) return "slide"; return "event"; } static std::string directionFromAngleRad(float a) { if (!std::isfinite(static_cast(a))) return {}; // Normalize to [-pi, pi) const double pi = 3.14159265358979323846; double x = static_cast(a); while (x >= pi) x -= 2.0 * pi; while (x < -pi) x += 2.0 * pi; // 8-way const double deg = x * 180.0 / pi; auto sector = [&](double d) -> int { // center sectors at multiples of 45 degrees int s = static_cast(std::floor((d + 22.5) / 45.0)); s %= 8; if (s < 0) s += 8; return s; }; switch (sector(deg)) { case 0: return "right"; case 1: return "up_right"; case 2: return "up"; case 3: return "up_left"; case 4: return "left"; case 5: return "down_left"; case 6: return "down"; case 7: return "down_right"; default: return {}; } } struct TrackAssets { std::filesystem::path gcRoot; std::string trackId; // ac_... std::string title; std::string imageKey; std::string artist; std::string duration; std::string bpm; std::array difficultyRatings{}; std::array chartIds{}; std::string bgmBase; // bgm_... std::filesystem::path stageDat; std::filesystem::path stageExt; std::filesystem::path stageClip; std::filesystem::path menuDds; std::filesystem::path menuDdsEnglish; std::filesystem::path startDds; std::filesystem::path startDdsEnglish; std::filesystem::path wavBgm; std::filesystem::path wavShot; std::filesystem::path vibCsv; }; static bool resolveTrackAssetsFromStageParam( const std::string& stageParamPath, const std::string& trackId, TrackAssets* out, std::string* err) { if (!out) return false; *out = TrackAssets{}; std::vector bytes; std::string ferr; if (!readWholeFile(stageParamPath, &bytes, &ferr)) { if (err) *err = "failed to read stage_param: " + ferr; return false; } std::vector entries; if (!gc::ParseStageCatalog(bytes, &entries, &ferr)) { if (err) *err = "failed to parse stage_param: " + ferr; return false; } const gc::StageCatalogEntry* entry = gc::FindStageCatalogEntryByChart(entries, trackId); if (!entry) { if (err) *err = "chart id not found in stage_param: " + trackId; return false; } const std::filesystem::path root = guessGcRootFromStageParamPath(std::filesystem::path(stageParamPath)); const std::filesystem::path stageSoundDir = root / "data" / "stage" / "sound"; const std::filesystem::path stageDir = root / "data" / "stage"; const std::filesystem::path stage2dDir = stageDir / "2d"; out->gcRoot = root; out->trackId = trackId; out->title = entry->title; out->imageKey = entry->imageKey; out->artist = entry->artist; out->duration = entry->duration; out->bpm = entry->bpm; out->difficultyRatings = entry->difficultyRatings; out->chartIds = entry->chartIds; out->bgmBase = entry->bgmBase; out->stageDat = stageDir / (trackId + ".dat"); out->stageExt = stageDir / (trackId + "_ext.dat"); out->stageClip = stageDir / (trackId + "_clip.dat"); out->menuDds = stage2dDir / (entry->imageKey + "_menu.dds"); out->menuDdsEnglish = stage2dDir / "eng" / (entry->imageKey + "_menu.dds"); out->startDds = stage2dDir / (entry->imageKey + "_start.dds"); out->startDdsEnglish = stage2dDir / "eng" / (entry->imageKey + "_start.dds"); size_t difficultyIndex = 0; for (size_t i = 0; i < entry->chartIds.size(); ++i) { if (entry->chartIds[i] == trackId) { difficultyIndex = i; break; } } out->wavBgm = stageSoundDir / (entry->bgmBase + entry->chartGroup0[difficultyIndex] + "_BGM.wav"); out->wavShot = stageSoundDir / (entry->bgmBase + entry->chartSuffixes[difficultyIndex] + "_SHOT.wav"); out->vibCsv = stageSoundDir / (entry->bgmBase + "_vib.csv"); return true; } struct TrackPt { float t = 0.0f; float v = 0.0f; }; struct NoteEv { float t = 0.0f; uint32_t type = 0; float value = 0.0f; }; static float lerp(float a, float b, float t) { return a + (b - a) * t; } static float clamp01(float t) { if (t < 0.0f) return 0.0f; if (t > 1.0f) return 1.0f; return t; } static float trackValueAt(const std::vector& pts, float t) { if (pts.empty()) return 0.0f; if (t <= pts.front().t) return pts.front().v; if (t >= pts.back().t) return pts.back().v; // upper_bound by time size_t lo = 0, hi = pts.size(); while (lo + 1 < hi) { const size_t mid = lo + (hi - lo) / 2; if (pts[mid].t <= t) lo = mid; else hi = mid; } const TrackPt& a = pts[lo]; const TrackPt& b = pts[std::min(lo + 1, pts.size() - 1)]; const float dt = (b.t - a.t); const float u = (dt > 1.0e-6f) ? clamp01((t - a.t) / dt) : 0.0f; return lerp(a.v, b.v, u); } static std::string colorForNote(uint32_t type) { switch (static_cast(type)) { case gc::NoteType::Tap: return "#1f77b4"; case gc::NoteType::Critical: return "#ffbf00"; case gc::NoteType::HoldStart: return "#ff7f0e"; case gc::NoteType::HoldEnd: return "#ff7f0e"; case gc::NoteType::DualHold: return "#d62728"; case gc::NoteType::Slide: return "#2ca02c"; case gc::NoteType::DualSlide: return "#2ca02c"; case gc::NoteType::SlideHold: return "#9467bd"; default: break; } // fallback return "#444444"; } static bool buildTrackFromSection16( const gc::StageDat& dat, size_t sectionIndex, std::vector* outPts, std::string* err) { if (!outPts) return false; outPts->clear(); if (sectionIndex >= dat.sections.size()) { if (err) *err = "track section index out of range"; return false; } const auto& sec = dat.sections[sectionIndex]; // Force 16-byte decoding and alignment search. const auto res = gc::TryDecodeEventStreamFixed(dat.bytes, sec.start, sec.end, 16); const size_t start = sec.start + static_cast(res.alignment); std::vector events; std::string tmpErr; if (!gc::DecodeEventStream(dat.bytes, start, sec.end, 16, &events, &tmpErr)) { if (err) *err = "DecodeEventStream(track) failed: " + tmpErr; return false; } // Interpret (timestamp,value) as a 2D curve for now. outPts->reserve(events.size()); for (const auto& e : events) { if (!isFiniteF(e.timestamp) || !isFiniteF(e.value)) continue; if (e.timestamp < -1.0f || e.timestamp > 1.0e6f) continue; if (std::fabs(static_cast(e.value)) > 1.0e7) continue; outPts->push_back(TrackPt{e.timestamp, e.value}); } std::sort(outPts->begin(), outPts->end(), [](const TrackPt& a, const TrackPt& b) { return a.t < b.t; }); // drop duplicates with same t to stabilize interpolation outPts->erase(std::unique(outPts->begin(), outPts->end(), [](const TrackPt& a, const TrackPt& b) { return std::fabs(static_cast(a.t - b.t)) < 1.0e-6; }), outPts->end()); if (outPts->size() < 2) { if (err) *err = "not enough track points decoded"; return false; } return true; } static int pickBestNoteAlign12(const gc::StageDat& dat, const gc::Section& sec) { // Score shifts by a mixture of markers and note-like opcodes. const size_t rs = 12; int bestShift = 0; uint64_t bestScore = 0; for (int shift = 0; shift < 12; shift++) { const size_t start = sec.start + static_cast(shift); const size_t end = sec.end; const size_t span = (end > start) ? (end - start) - ((end - start) % rs) : 0; uint64_t noteLike = 0; uint64_t bpmMarkers = 0; uint64_t plausible = 0; for (size_t rel = 0; rel + rs <= span; rel += rs) { const size_t off = start + rel; if (off + rs > dat.bytes.size()) break; const float ts = f32be_bytes(dat.bytes, off + 0); const uint32_t type = u32be_bytes(dat.bytes, off + 4); const float val = f32be_bytes(dat.bytes, off + 8); if (!isFiniteF(ts) || ts < -1.0f || ts > 1.0e6f) continue; if (type == 0xFFFFFFFFu) continue; if (!isFiniteF(val) || std::fabs(static_cast(val)) > 1.0e7) continue; if (type <= 0xFFFFu && gc::IsNote(type)) noteLike++; if (type == 0 && std::fabs(static_cast(val - 1.0f)) < 1.0e-6) bpmMarkers++; plausible++; } const uint64_t score = noteLike * 100000 + bpmMarkers * 1000 + plausible; if (score > bestScore) { bestScore = score; bestShift = shift; } } return bestShift; } static bool buildNotesFromSection12( const gc::StageDat& dat, size_t sectionIndex, int forcedAlign, bool keepType0, bool notesOnly, std::vector* outNotes, std::string* err) { if (!outNotes) return false; outNotes->clear(); if (sectionIndex >= dat.sections.size()) { if (err) *err = "note section index out of range"; return false; } const auto& sec = dat.sections[sectionIndex]; const int align = (forcedAlign >= 0) ? forcedAlign : pickBestNoteAlign12(dat, sec); const size_t rs = 12; const size_t start = sec.start + static_cast(align); const size_t end = sec.end; const size_t span = (end > start) ? (end - start) - ((end - start) % rs) : 0; outNotes->reserve(span / rs); for (size_t rel = 0; rel + rs <= span; rel += rs) { const size_t off = start + rel; if (off + rs > dat.bytes.size()) break; const float ts = f32be_bytes(dat.bytes, off + 0); const uint32_t type = u32be_bytes(dat.bytes, off + 4); const float val = f32be_bytes(dat.bytes, off + 8); if (!isFiniteF(ts) || ts < -1.0f || ts > 1.0e6f) continue; if (type == 0xFFFFFFFFu) continue; if (!isFiniteF(val) || std::fabs(static_cast(val)) > 1.0e7) continue; if (type > 0xFFFFu) continue; if (!keepType0 && type == 0) continue; if (notesOnly && !gc::IsNote(type)) continue; outNotes->push_back(NoteEv{ts, type, val}); } std::sort(outNotes->begin(), outNotes->end(), [](const NoteEv& a, const NoteEv& b) { return a.t < b.t; }); if (outNotes->empty()) { if (err) *err = "no events decoded from note section (try --note-align 0..11, or --render-keep-type0)"; return false; } return true; } static std::string mapperNoteType(uint32_t type) { switch (static_cast(type)) { case gc::NoteType::Tap: case gc::NoteType::Legacy_Tap: return "tap"; case gc::NoteType::Critical: return "critical"; case gc::NoteType::HoldStart: case gc::NoteType::HoldEnd: return "hold"; case gc::NoteType::DualHold: return "dualhold"; case gc::NoteType::Slide: case gc::NoteType::Legacy_Slide: return "slide"; case gc::NoteType::DualSlide: return "dualslide"; case gc::NoteType::SlideHold: return "slidehold"; default: break; } return "event"; } static double dist2d(const gc::TrackPiece& a, const gc::TrackPiece& b) { const double dx = static_cast(b.x) - static_cast(a.x); const double dz = static_cast(b.z) - static_cast(a.z); return std::sqrt(dx * dx + dz * dz); } static bool exportVectoMapperProject( const std::filesystem::path& stageDatPath, const gc::ParsedStagePattern& stage, const std::vector& notes, const std::filesystem::path& outDir, std::string* err) { if (stage.track.size() < 2) { if (err) *err = "stage pattern track has fewer than two points"; return false; } std::error_code ec; std::filesystem::create_directories(outDir / "track", ec); if (ec) { if (err) *err = "failed to create track directory: " + ec.message(); return false; } std::filesystem::create_directories(outDir / "charts", ec); if (ec) { if (err) *err = "failed to create charts directory: " + ec.message(); return false; } std::filesystem::create_directories(outDir / "bg", ec); if (ec) { if (err) *err = "failed to create bg directory: " + ec.message(); return false; } const std::string title = !stage.config.chartName.empty() ? stage.config.chartName : stageDatPath.stem().string(); std::vector cumulative; cumulative.reserve(stage.track.size()); cumulative.push_back(0.0); for (size_t i = 1; i < stage.track.size(); i++) { cumulative.push_back(cumulative.back() + dist2d(stage.track[i - 1], stage.track[i])); } { std::ofstream out(outDir / "track" / "track_graph.json", std::ios::binary); if (!out.is_open()) { if (err) *err = "failed to write track_graph.json"; return false; } out << "{\n"; out << " \"schema\": 1,\n"; out << " \"source\": {\"format\": \"gc_stage_dat\", \"file\": \"" << jsonEscape(stageDatPath.filename().string()) << "\"},\n"; out << " \"nodes\": [\n"; for (size_t i = 0; i < stage.track.size(); i++) { const auto& p = stage.track[i]; out << " {\"id\": \"N" << i << "\", \"x\": " << fmtFixed(p.x, 6) << ", \"z\": " << fmtFixed(p.z, 6) << ", \"gc_time_ms\": " << p.timeMs << ", \"gc_y\": " << fmtFixed(p.y, 6) << "}"; out << (i + 1 < stage.track.size() ? "," : "") << "\n"; } out << " ],\n"; out << " \"segments\": [\n"; for (size_t i = 0; i + 1 < stage.track.size(); i++) { const auto& a = stage.track[i]; const auto& b = stage.track[i + 1]; const double c1x = static_cast(a.x) + (static_cast(b.x) - static_cast(a.x)) / 3.0; const double c1z = static_cast(a.z) + (static_cast(b.z) - static_cast(a.z)) / 3.0; const double c2x = static_cast(a.x) + 2.0 * (static_cast(b.x) - static_cast(a.x)) / 3.0; const double c2z = static_cast(a.z) + 2.0 * (static_cast(b.z) - static_cast(a.z)) / 3.0; out << " {\"id\": \"S" << i << "\", \"type\": \"bezier\", \"mode\": \"smooth\", " << "\"a\": \"N" << i << "\", \"b\": \"N" << (i + 1) << "\", " << "\"c1\": [" << fmtFixed(c1x, 6) << ", " << fmtFixed(c1z, 6) << "], " << "\"c2\": [" << fmtFixed(c2x, 6) << ", " << fmtFixed(c2z, 6) << "], " << "\"visible\": 1}"; out << (i + 2 < stage.track.size() ? "," : "") << "\n"; } out << " ],\n"; out << " \"pieces\": [],\n"; out << " \"paths\": {\"main\": ["; for (size_t i = 0; i + 1 < stage.track.size(); i++) { out << (i ? ", " : "") << "\"S" << i << "\""; } out << "]},\n"; out << " \"active_path\": \"main\"\n"; out << "}\n"; } { std::ofstream out(outDir / "track" / "camera_timeline.txt", std::ios::binary); if (!out.is_open()) { if (err) *err = "failed to write camera_timeline.txt"; return false; } const double lastMs = std::max(1.0, static_cast(stage.track.back().timeMs)); const double maxParam = static_cast(stage.track.size() - 1); for (const auto& c : stage.cameras) { const double t = std::max(0.0, std::min(maxParam, (static_cast(c.timeMs) / lastMs) * maxParam)); const double fov = (std::isfinite(c.fieldNear[0]) && c.fieldNear[0] > 1.0f && c.fieldNear[0] < 179.0f) ? static_cast(c.fieldNear[0]) : 90.0; const double camY = (std::isfinite(c.originOff[2]) && std::fabs(static_cast(c.originOff[2])) > 1.0e-6) ? static_cast(c.originOff[2]) : 3.5; const double camZ = (std::isfinite(c.dist) && std::fabs(static_cast(c.dist)) > 1.0e-6) ? static_cast(c.dist) : 3.5; out << fmtFixed(t, 6) << " fov " << fmtFixed(fov, 6) << "\n"; out << fmtFixed(t, 6) << " cam_y " << fmtFixed(camY, 6) << "\n"; out << fmtFixed(t, 6) << " cam_z " << fmtFixed(camZ, 6) << "\n"; } } { std::ofstream out(outDir / "charts" / "normal.json", std::ios::binary); if (!out.is_open()) { if (err) *err = "failed to write charts/normal.json"; return false; } out << "{\n"; out << " \"schema\": 1,\n"; out << " \"name\": \"normal\",\n"; out << " \"source\": {\"format\": \"gc_stage_dat\", \"note_decode\": \"heuristic_section12\"},\n"; out << " \"avatar_move\": [\n"; for (size_t i = 0; i < stage.track.size(); i++) { const double t = stage.track[i].timeMs ? (static_cast(stage.track[i].timeMs) / 1000.0) : (cumulative[i] / 30.0); out << " {\"t\": " << fmtFixed(t, 6) << ", \"d\": " << fmtFixed(cumulative[i], 6) << "}"; out << (i + 1 < stage.track.size() ? "," : "") << "\n"; } out << " ],\n"; out << " \"notes\": [\n"; for (size_t i = 0; i < notes.size(); i++) { const auto& n = notes[i]; out << " {\"t\": " << fmtFixed(n.t, 6) << ", \"type\": \"" << jsonEscape(mapperNoteType(n.type)) << "\"" << ", \"raw_type\": \"0x" << std::hex << std::setw(8) << std::setfill('0') << n.type << std::dec << std::setfill(' ') << "\"" << ", \"value\": " << fmtFixed(n.value, 6) << "}"; out << (i + 1 < notes.size() ? "," : "") << "\n"; } out << " ]\n"; out << "}\n"; } { std::ofstream out(outDir / "bg" / "bg.json", std::ios::binary); if (!out.is_open()) { if (err) *err = "failed to write bg/bg.json"; return false; } out << "{\n"; out << " \"schema\": 1,\n"; out << " \"type\": \"color\",\n"; out << " \"params\": {\"color\": \"#0f1116\"}\n"; out << "}\n"; } { std::ofstream out(outDir / "level.json", std::ios::binary); if (!out.is_open()) { if (err) *err = "failed to write level.json"; return false; } out << "{\n"; out << " \"schema\": 1,\n"; out << " \"title\": \"" << jsonEscape(title) << "\",\n"; out << " \"audio\": \"\",\n"; out << " \"track\": \"track/track_graph.json\",\n"; out << " \"camera\": \"track/camera_timeline.txt\",\n"; out << " \"background\": \"bg/bg.json\",\n"; out << " \"charts\": {\"normal\": \"charts/normal.json\"},\n"; out << " \"timing\": {\"base_dps\": 30.0, \"playback_rate\": 1.0},\n"; out << " \"gc\": {\n"; out << " \"stage_file\": \"" << jsonEscape(stageDatPath.string()) << "\",\n"; out << " \"chart_name\": \"" << jsonEscape(stage.config.chartName) << "\",\n"; out << " \"bgm_name\": \"" << jsonEscape(stage.config.bgmName) << "\",\n"; out << " \"shot_name\": \"" << jsonEscape(stage.config.shotName) << "\",\n"; out << " \"track_points\": " << stage.track.size() << ",\n"; out << " \"camera_points\": " << stage.cameras.size() << ",\n"; out << " \"draw_distance_points\": " << stage.drawDistances.size() << "\n"; out << " }\n"; out << "}\n"; } return true; } int main(int argc, char** argv) { if (argc < 2) { printUsage(argv[0]); return 2; } std::string path = argv[1]; bool rawMode = false; int dumpN = 0; int dumpSection = -1; int forcedAlign = -1; std::string dumpRs = "auto"; // auto|12|16 (affects --dump/--svg candidate selection) std::string svgOut; std::string svgY = "value"; int svgMaxPoints = 6000; bool svgKeepAll = false; bool filterGarbage = true; uint32_t typeMax = 0xFFFFFFFFu; // for rs=12, defaulted later to 0xFFFF when filtering bool statsMode = false; int statsTop = 50; bool statsAll = false; bool statsSkipZero = true; int statsSection = -1; std::string statsRs = "12"; // auto|12|16 bool findAlignMode = false; int findAlignSection = -1; std::string findAlignRs = "12"; // 12|16 bool surveyMode = false; bool stringsMode = false; int stringsSection = -1; int stringsMinLen = 4; int stringsMax = 250; bool stringsAll = false; bool metaMode = false; int metaSection = 0; std::string renderOut; int trackSection = 2; int noteSection = 4; int noteAlign = -1; RenderTimeMode renderTimeMode = RenderTimeMode::Auto; bool renderKeepType0 = false; bool renderNotesOnly = false; int renderMaxNotes = 8000; bool trackInfoMode = false; std::string trackInfoId; bool playMode = false; std::string playId; std::string playWhat = "bgm"; // bgm|shot std::string playTool = "auto"; // auto|ffplay|aplay|paplay|mpv bool vizMode = false; std::string vizId; std::string vizOutHtml; std::string vizWhat = "bgm"; bool vizNotesOnly = false; bool exportJsonMode = false; std::string exportId; std::string exportOutJson; ExportAtMode exportAtMode = ExportAtMode::Auto; bool exportRelative = false; bool exportNotesOnly = false; bool exportGcsimMode = false; std::string gcsimId; std::string gcsimOutDir; std::string gcsimWhat = "bgm"; int gcsimBpm = 120; std::string gcsimTitle; bool exportGcsimProjectMode = false; std::string gcsimProjectOutDir; std::string gcsimProjectStageParam; std::string gcsimProjectAcId; std::string gcsimProjectMusic; bool exportVectoMapperMode = false; std::string exportVectoMapperOutDir; for (int i = 2; i < argc; i++) { std::string a = argv[i]; if (a == "--dump") { if (i + 1 >= argc) { std::cerr << "--dump requires an integer\n"; return 2; } dumpN = std::atoi(argv[i + 1]); i++; } else if (a == "--section") { if (i + 1 >= argc) { std::cerr << "--section requires an integer\n"; return 2; } dumpSection = std::atoi(argv[i + 1]); i++; } else if (a == "--align") { if (i + 1 >= argc) { std::cerr << "--align requires an integer\n"; return 2; } forcedAlign = std::atoi(argv[i + 1]); if (forcedAlign < 0) forcedAlign = 0; i++; } else if (a == "--rs") { if (i + 1 >= argc) { std::cerr << "--rs requires one of: auto, 12, 16\n"; return 2; } dumpRs = argv[i + 1]; if (dumpRs != "auto" && dumpRs != "12" && dumpRs != "16") { std::cerr << "--rs requires one of: auto, 12, 16\n"; return 2; } i++; } else if (a == "--svg") { if (i + 1 >= argc) { std::cerr << "--svg requires a path\n"; return 2; } svgOut = argv[i + 1]; i++; } else if (a == "--svg-y") { if (i + 1 >= argc) { std::cerr << "--svg-y requires one of: value, type\n"; return 2; } svgY = argv[i + 1]; if (svgY != "value" && svgY != "type") { std::cerr << "--svg-y requires one of: value, type\n"; return 2; } i++; } else if (a == "--type-max") { if (i + 1 >= argc) { std::cerr << "--type-max requires an integer (e.g. 65535 or 0xffff)\n"; return 2; } uint32_t v = 0; if (!parseU32(argv[i + 1], &v)) { std::cerr << "--type-max parse failed: " << argv[i + 1] << "\n"; return 2; } typeMax = v; i++; } else if (a == "--svg-max") { if (i + 1 >= argc) { std::cerr << "--svg-max requires an integer\n"; return 2; } svgMaxPoints = std::atoi(argv[i + 1]); if (svgMaxPoints < 10) svgMaxPoints = 10; i++; } else if (a == "--svg-all") { svgKeepAll = true; } else if (a == "--no-filter") { filterGarbage = false; } else if (a == "--stats") { statsMode = true; } else if (a == "--stats-section") { if (i + 1 >= argc) { std::cerr << "--stats-section requires an integer\n"; return 2; } statsSection = std::atoi(argv[i + 1]); i++; } else if (a == "--stats-rs") { if (i + 1 >= argc) { std::cerr << "--stats-rs requires one of: auto, 12, 16\n"; return 2; } statsRs = argv[i + 1]; if (statsRs != "auto" && statsRs != "12" && statsRs != "16") { std::cerr << "--stats-rs requires one of: auto, 12, 16\n"; return 2; } i++; } else if (a == "--stats-top") { if (i + 1 >= argc) { std::cerr << "--stats-top requires an integer\n"; return 2; } statsTop = std::atoi(argv[i + 1]); if (statsTop < 1) statsTop = 1; i++; } else if (a == "--stats-all") { statsAll = true; } else if (a == "--stats-keep-zero") { statsSkipZero = false; } else if (a == "--find-align") { findAlignMode = true; } else if (a == "--find-align-section") { if (i + 1 >= argc) { std::cerr << "--find-align-section requires an integer\n"; return 2; } findAlignSection = std::atoi(argv[i + 1]); i++; } else if (a == "--find-align-rs") { if (i + 1 >= argc) { std::cerr << "--find-align-rs requires one of: 12, 16\n"; return 2; } findAlignRs = argv[i + 1]; if (findAlignRs != "12" && findAlignRs != "16") { std::cerr << "--find-align-rs requires one of: 12, 16\n"; return 2; } i++; } else if (a == "--survey") { surveyMode = true; } else if (a == "--strings") { stringsMode = true; } else if (a == "--strings-section") { if (i + 1 >= argc) { std::cerr << "--strings-section requires an integer\n"; return 2; } stringsSection = std::atoi(argv[i + 1]); i++; } else if (a == "--strings-min") { if (i + 1 >= argc) { std::cerr << "--strings-min requires an integer\n"; return 2; } stringsMinLen = std::atoi(argv[i + 1]); if (stringsMinLen < 2) stringsMinLen = 2; i++; } else if (a == "--strings-max") { if (i + 1 >= argc) { std::cerr << "--strings-max requires an integer\n"; return 2; } stringsMax = std::atoi(argv[i + 1]); if (stringsMax < 1) stringsMax = 1; i++; } else if (a == "--strings-all") { stringsAll = true; } else if (a == "--meta") { metaMode = true; } else if (a == "--meta-section") { if (i + 1 >= argc) { std::cerr << "--meta-section requires an integer\n"; return 2; } metaSection = std::atoi(argv[i + 1]); i++; } else if (a == "--raw") { rawMode = true; } else if (a == "--render") { if (i + 1 >= argc) { std::cerr << "--render requires a path\n"; return 2; } renderOut = argv[i + 1]; i++; } else if (a == "--render-time") { if (i + 1 >= argc) { std::cerr << "--render-time requires one of: auto, time, fit, index\n"; return 2; } if (!parseRenderTimeMode(argv[i + 1], &renderTimeMode)) { std::cerr << "--render-time requires one of: auto, time, fit, index\n"; return 2; } i++; } else if (a == "--render-keep-type0") { renderKeepType0 = true; } else if (a == "--render-notes-only") { renderNotesOnly = true; } else if (a == "--render-max-notes") { if (i + 1 >= argc) { std::cerr << "--render-max-notes requires an integer\n"; return 2; } renderMaxNotes = std::atoi(argv[i + 1]); if (renderMaxNotes < 100) renderMaxNotes = 100; i++; } else if (a == "--track-info") { if (i + 1 >= argc) { std::cerr << "--track-info requires an id like: ac_10pt8tion_hard\n"; return 2; } trackInfoMode = true; trackInfoId = argv[i + 1]; i++; } else if (a == "--play") { if (i + 1 >= argc) { std::cerr << "--play requires an id like: ac_10pt8tion_hard\n"; return 2; } playMode = true; playId = argv[i + 1]; i++; } else if (a == "--play-what") { if (i + 1 >= argc) { std::cerr << "--play-what requires one of: bgm, shot\n"; return 2; } playWhat = argv[i + 1]; if (playWhat != "bgm" && playWhat != "shot") { std::cerr << "--play-what requires one of: bgm, shot\n"; return 2; } i++; } else if (a == "--play-tool") { if (i + 1 >= argc) { std::cerr << "--play-tool requires one of: auto, ffplay, aplay, paplay, mpv\n"; return 2; } playTool = argv[i + 1]; if (playTool != "auto" && playTool != "ffplay" && playTool != "aplay" && playTool != "paplay" && playTool != "mpv") { std::cerr << "--play-tool requires one of: auto, ffplay, aplay, paplay, mpv\n"; return 2; } i++; } else if (a == "--viz") { if (i + 2 >= argc) { std::cerr << "--viz requires: \n"; return 2; } vizMode = true; vizId = argv[i + 1]; vizOutHtml = argv[i + 2]; i += 2; } else if (a == "--viz-what") { if (i + 1 >= argc) { std::cerr << "--viz-what requires one of: bgm, shot\n"; return 2; } vizWhat = argv[i + 1]; if (vizWhat != "bgm" && vizWhat != "shot") { std::cerr << "--viz-what requires one of: bgm, shot\n"; return 2; } i++; } else if (a == "--viz-notes-only") { vizNotesOnly = true; } else if (a == "--export-json") { if (i + 2 >= argc) { std::cerr << "--export-json requires: \n"; return 2; } exportJsonMode = true; exportId = argv[i + 1]; exportOutJson = argv[i + 2]; i += 2; } else if (a == "--export-at") { if (i + 1 >= argc) { std::cerr << "--export-at requires one of: auto, bars, beats, index, seconds\n"; return 2; } if (!parseExportAtMode(argv[i + 1], &exportAtMode)) { std::cerr << "--export-at requires one of: auto, bars, beats, index, seconds\n"; return 2; } i++; } else if (a == "--export-relative") { exportRelative = true; } else if (a == "--export-notes-only") { exportNotesOnly = true; } else if (a == "--export-gcsim") { if (i + 2 >= argc) { std::cerr << "--export-gcsim requires: \n"; return 2; } exportGcsimMode = true; gcsimId = argv[i + 1]; gcsimOutDir = argv[i + 2]; i += 2; } else if (a == "--gcsim-what") { if (i + 1 >= argc) { std::cerr << "--gcsim-what requires one of: bgm, shot\n"; return 2; } gcsimWhat = argv[i + 1]; if (gcsimWhat != "bgm" && gcsimWhat != "shot") { std::cerr << "--gcsim-what requires one of: bgm, shot\n"; return 2; } i++; } else if (a == "--gcsim-bpm") { if (i + 1 >= argc) { std::cerr << "--gcsim-bpm requires an integer\n"; return 2; } gcsimBpm = std::atoi(argv[i + 1]); if (gcsimBpm < 1) gcsimBpm = 1; i++; } else if (a == "--gcsim-title") { if (i + 1 >= argc) { std::cerr << "--gcsim-title requires a string\n"; return 2; } gcsimTitle = argv[i + 1]; i++; } else if (a == "--export-gcsim-project") { if (i + 1 >= argc) { std::cerr << "--export-gcsim-project requires: \n"; return 2; } exportGcsimProjectMode = true; gcsimProjectOutDir = argv[i + 1]; i++; } else if (a == "--export-vectomapper") { if (i + 1 >= argc) { std::cerr << "--export-vectomapper requires: \n"; return 2; } exportVectoMapperMode = true; exportVectoMapperOutDir = argv[i + 1]; i++; } else if (a == "--stage-param") { if (i + 1 >= argc) { std::cerr << "--stage-param requires a path to stage_param.dat\n"; return 2; } gcsimProjectStageParam = argv[i + 1]; i++; } else if (a == "--ac-id") { if (i + 1 >= argc) { std::cerr << "--ac-id requires an id like: ac_10pt8tion_hard\n"; return 2; } gcsimProjectAcId = argv[i + 1]; i++; } else if (a == "--music") { if (i + 1 >= argc) { std::cerr << "--music requires a wav path\n"; return 2; } gcsimProjectMusic = argv[i + 1]; i++; } else if (a == "--track-section") { if (i + 1 >= argc) { std::cerr << "--track-section requires an integer\n"; return 2; } trackSection = std::atoi(argv[i + 1]); i++; } else if (a == "--note-section") { if (i + 1 >= argc) { std::cerr << "--note-section requires an integer\n"; return 2; } noteSection = std::atoi(argv[i + 1]); i++; } else if (a == "--note-align") { if (i + 1 >= argc) { std::cerr << "--note-align requires an integer (0..11)\n"; return 2; } noteAlign = std::atoi(argv[i + 1]); if (noteAlign < 0) noteAlign = 0; if (noteAlign > 11) noteAlign = 11; i++; } else if (a == "--help" || a == "-h") { printUsage(argv[0]); return 0; } else { std::cerr << "Unknown arg: " << a << "\n"; return 2; } } if (exportVectoMapperMode) { const std::filesystem::path stageDatPath(path); gc::StageDat dat; std::string eerr; if (!gc::StageDat::LoadFromFile(stageDatPath.string(), dat, &eerr)) { std::cerr << "VectoMapper export: failed to load stage dat: " << stageDatPath.string() << "\n"; if (!eerr.empty()) std::cerr << eerr << "\n"; return 1; } gc::ParsedStagePattern stage; if (!gc::ParseStagePattern(dat, &stage, &eerr)) { std::cerr << "VectoMapper export: stage.pat parse failed: " << eerr << "\n"; return 1; } std::vector notes; if (!buildNotesFromSection12(dat, static_cast(noteSection), noteAlign, /*keepType0*/false, /*notesOnly*/true, ¬es, &eerr)) { std::cerr << "VectoMapper export: note decode warning: " << eerr << "\n"; notes.clear(); } if (!exportVectoMapperProject(stageDatPath, stage, notes, std::filesystem::path(exportVectoMapperOutDir), &eerr)) { std::cerr << "VectoMapper export: write failed: " << eerr << "\n"; return 1; } std::cout << "Wrote VectoMapper project: " << exportVectoMapperOutDir << "\n"; std::cout << " track points: " << stage.track.size() << "\n"; std::cout << " camera points: " << stage.cameras.size() << "\n"; std::cout << " notes: " << notes.size() << " (heuristic)\n"; return 0; } if (playMode) { TrackAssets ta; std::string perr; if (!resolveTrackAssetsFromStageParam(path, playId, &ta, &perr)) { std::cerr << "Play: resolve failed: " << perr << "\n"; return 1; } std::filesystem::path wav; if (playWhat == "bgm") wav = ta.wavBgm; else wav = ta.wavShot; if (!std::filesystem::exists(wav)) { std::cerr << "Play: missing wav: " << wav.string() << "\n"; return 1; } // Choose a tool. auto existsInPath = [&](const std::string& exe) -> bool { const char* p = std::getenv("PATH"); if (!p) return false; std::string cmd = "command -v " + exe + " >/dev/null 2>&1"; return std::system(cmd.c_str()) == 0; }; std::string tool = playTool; if (tool == "auto") { if (existsInPath("ffplay")) tool = "ffplay"; else if (existsInPath("aplay")) tool = "aplay"; else if (existsInPath("paplay")) tool = "paplay"; else if (existsInPath("mpv")) tool = "mpv"; else tool.clear(); } if (tool.empty()) { std::cerr << "Play: no audio tool found. Install one of: ffplay, aplay(alsa-utils), paplay(pulseaudio), mpv\n"; return 1; } const std::string wavEsc = shEscapeSingleQuotes(wav.string()); std::string cmd; if (tool == "ffplay") { cmd = "ffplay -nodisp -autoexit -hide_banner -loglevel warning " + wavEsc; } else if (tool == "aplay") { cmd = "aplay " + wavEsc; } else if (tool == "paplay") { cmd = "paplay " + wavEsc; } else if (tool == "mpv") { cmd = "mpv --no-video --really-quiet " + wavEsc; } else { std::cerr << "Play: unsupported tool: " << tool << "\n"; return 1; } std::cout << "Track: " << ta.trackId << "\n"; std::cout << "BGM base: " << ta.bgmBase << "\n"; std::cout << "WAV (" << playWhat << "): " << wav.string() << "\n"; if (std::filesystem::exists(ta.vibCsv)) std::cout << "VIB: " << ta.vibCsv.string() << "\n"; std::cout << "Player: " << tool << "\n"; std::cout << "Running: " << cmd << "\n"; return std::system(cmd.c_str()); } if (vizMode) { TrackAssets ta; std::string verr; if (!resolveTrackAssetsFromStageParam(path, vizId, &ta, &verr)) { std::cerr << "Viz: resolve failed: " << verr << "\n"; return 1; } std::filesystem::path wav; if (vizWhat == "bgm") wav = ta.wavBgm; else wav = ta.wavShot; if (!std::filesystem::exists(wav)) { std::cerr << "Viz: missing wav: " << wav.string() << "\n"; return 1; } double durSec = 0.0; std::string werr; if (!tryReadWavDurationSec(wav, &durSec, &werr)) { std::cerr << "Viz: could not read wav duration: " << werr << "\n"; return 1; } // Load stage dat and decode. gc::StageDat dat; if (!gc::StageDat::LoadFromFile(ta.stageDat.string(), dat, &verr)) { std::cerr << "Viz: failed to load stage dat: " << ta.stageDat.string() << "\n"; if (!verr.empty()) std::cerr << verr << "\n"; return 1; } std::vector track; std::vector notes; if (!buildTrackFromSection16(dat, static_cast(trackSection), &track, &verr)) { std::cerr << "Viz: track decode failed: " << verr << "\n"; return 1; } if (!buildNotesFromSection12(dat, static_cast(noteSection), noteAlign, /*keepType0*/false, /*notesOnly*/vizNotesOnly, ¬es, &verr)) { std::cerr << "Viz: note decode failed: " << verr << "\n"; return 1; } // Downsample for DOM size. const int maxDomNotes = 6000; if (static_cast(notes.size()) > maxDomNotes) { const size_t stride = std::max(1, notes.size() / static_cast(maxDomNotes)); std::vector ds; ds.reserve(static_cast(maxDomNotes)); for (size_t i = 0; i < notes.size(); i += stride) ds.push_back(notes[i]); notes.swap(ds); } // Map note times to audio seconds: if note time range looks meaningful, fit it; else spread by index. float nMinT = notes.front().t; float nMaxT = notes.back().t; const float nRange = (nMaxT > nMinT) ? (nMaxT - nMinT) : 0.0f; uint64_t zeroTs = 0; for (const auto& n : notes) if (std::fabs(static_cast(n.t)) < 1.0e-9) zeroTs++; const double pZero = notes.empty() ? 0.0 : (100.0 * static_cast(zeroTs) / static_cast(notes.size())); const bool useIndex = (nRange <= 2.0f) || (pZero >= 80.0); auto noteAudioSec = [&](const NoteEv& n, size_t idx, size_t total) -> double { if (durSec <= 0.0) return 0.0; if (!useIndex && nRange > 1.0e-6f) { const double u = clamp01((n.t - nMinT) / nRange); return u * durSec; } if (total <= 1) return 0.0; const double u = static_cast(idx) / static_cast(total - 1); return u * durSec; }; // Prepare SVG coordinate mapping using track time. float tMin = track.front().t; float tMax = track.back().t; float vMin = std::numeric_limits::infinity(); float vMax = -std::numeric_limits::infinity(); for (const auto& p : track) { vMin = std::min(vMin, p.v); vMax = std::max(vMax, p.v); } const float tRange = (tMax > tMin) ? (tMax - tMin) : 1.0f; const float vRange = (vMax > vMin) ? (vMax - vMin) : 1.0f; const int W = 1600; const int H = 900; const int M = 70; auto mapX = [&](float t) -> float { const float u = (t - tMin) / tRange; return static_cast(M) + clamp01(u) * static_cast(W - 2 * M); }; auto mapY = [&](float v) -> float { const float u = (v - vMin) / vRange; return static_cast(H - M) - clamp01(u) * static_cast(H - 2 * M); }; auto mapAudioToTrackT = [&](double sec) -> float { const double u = (durSec > 1.0e-6) ? std::max(0.0, std::min(1.0, sec / durSec)) : 0.0; return tMin + static_cast(u) * tRange; }; std::ofstream out(vizOutHtml, std::ios::binary); if (!out.is_open()) { std::cerr << "Viz: failed to open for write: " << vizOutHtml << "\n"; return 1; } // Use absolute file path for browser