Initial public source release
Split reusable rendering and format support into vectorail-core and vectorail-gc.
This commit is contained in:
+2997
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,218 @@
|
||||
#include "gc/StageCatalog.hpp"
|
||||
#include "openroller/psp/SongCatalog.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
bool readFile(const std::filesystem::path& path, std::vector<std::uint8_t>* bytes) {
|
||||
if (!bytes) return false;
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input) return false;
|
||||
input.seekg(0, std::ios::end);
|
||||
const std::streamoff size = input.tellg();
|
||||
if (size < 0) return false;
|
||||
input.seekg(0, std::ios::beg);
|
||||
bytes->resize(static_cast<std::size_t>(size));
|
||||
if (!bytes->empty()) input.read(reinterpret_cast<char*>(bytes->data()), size);
|
||||
return static_cast<bool>(input);
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
void copyDisplay(char (&output)[N], const std::string& input) {
|
||||
std::size_t written = 0;
|
||||
for (std::size_t index = 0; index < input.size();) {
|
||||
const unsigned char value = static_cast<unsigned char>(input[index++]);
|
||||
if (written + 1 >= N) break;
|
||||
if (value >= 0x20 && value < 0x7f) {
|
||||
output[written++] = static_cast<char>(value);
|
||||
continue;
|
||||
}
|
||||
output[written++] = '?';
|
||||
while (index < input.size() &&
|
||||
(static_cast<unsigned char>(input[index]) & 0xc0u) == 0x80u) ++index;
|
||||
}
|
||||
output[written] = '\0';
|
||||
}
|
||||
|
||||
const gc::StageCatalogEntry* findSong(
|
||||
const std::vector<gc::StageCatalogEntry>& entries,
|
||||
const std::string& token) {
|
||||
const std::string easy = "ac_" + token + "_easy";
|
||||
if (const auto* found = gc::FindStageCatalogEntryByChart(entries, easy)) return found;
|
||||
for (const auto& entry : entries) {
|
||||
if (entry.imageKey == token) return &entry;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string chartAt(const gc::StageCatalogEntry& entry, std::size_t difficulty) {
|
||||
std::string chart = entry.chartIds[difficulty];
|
||||
if (chart.empty()) chart = entry.chartGroup0[difficulty];
|
||||
return chart;
|
||||
}
|
||||
|
||||
struct AsciiMetadata {
|
||||
const char* token;
|
||||
const char* title;
|
||||
const char* artist;
|
||||
};
|
||||
|
||||
const AsciiMetadata* asciiMetadata(const std::string& token) {
|
||||
// The PSP port currently uses a compact built-in ASCII font. Keep the
|
||||
// original catalog as the source of gameplay data, but provide readable
|
||||
// metadata instead of locale-dependent mojibake/question marks.
|
||||
static constexpr AsciiMetadata overrides[] = {
|
||||
{"altale", "Altale", "Sakuzyo"},
|
||||
{"7days", "7 days a week", "Silver Forest feat. Aki"},
|
||||
{"mikumiku", "Miku Miku ni Shite Ageru", "ika-mo"},
|
||||
{"syositu2", "The Disappearance of Hatsune Miku", "cosMo@BousouP feat. Hatsune Miku"},
|
||||
{"world2", "World's End Dancehall", "wowaka feat. Hatsune Miku & Megurine Luka"},
|
||||
{"rollingirl2", "Rolling Girl", "wowaka feat. Hatsune Miku"},
|
||||
{"uraomote", "Two-Faced Lovers", "wowaka"},
|
||||
{"unknown", "Unknown Mother-Goose", "wowaka feat. Hatsune Miku"},
|
||||
{"redial", "Redial", "livetune feat. Hatsune Miku"},
|
||||
{"tellyour", "Tell Your World", "livetune feat. Hatsune Miku"},
|
||||
{"umiyuri", "Tale of the Deep-sea Lily", "n-buna feat. Hatsune Miku"},
|
||||
{"karakuri", "Karakuri Pierrot", "40mP feat. Hatsune Miku"},
|
||||
{"dappo", "Law-evading Rock", "Neru feat. Kagamine Len"},
|
||||
{"vampire", "The Vampire", "DECO*27 feat. Hatsune Miku"},
|
||||
{"pa3", "PaIII.SENSATION", "Yunosuke feat. Miku, GUMI & Rin"},
|
||||
};
|
||||
for (const AsciiMetadata& metadata : overrides) {
|
||||
if (token == metadata.token) return &metadata;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string resolveAudioFilename(
|
||||
const std::filesystem::path& soundDirectory,
|
||||
const std::string& authoredName) {
|
||||
const std::filesystem::path exact = soundDirectory / authoredName;
|
||||
if (std::filesystem::is_regular_file(exact)) return exact.filename().string();
|
||||
std::string lower = authoredName;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char value) {
|
||||
return static_cast<char>(std::tolower(value));
|
||||
});
|
||||
for (const auto& entry : std::filesystem::directory_iterator(soundDirectory)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
std::string candidate = entry.path().filename().string();
|
||||
std::transform(candidate.begin(), candidate.end(), candidate.begin(), [](unsigned char value) {
|
||||
return static_cast<char>(std::tolower(value));
|
||||
});
|
||||
if (candidate == lower) return entry.path().filename().string();
|
||||
}
|
||||
return authoredName;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const bool listMode = argc == 3 && std::string(argv[2]) == "--list";
|
||||
if ((!listMode && argc < 4) || argc < 3) {
|
||||
std::cerr << "usage: openroller-psp-catalog <stage_param.dat> <catalog.orpc> <song-token>...\n"
|
||||
<< " openroller-psp-catalog <stage_param.dat> --list\n";
|
||||
return 2;
|
||||
}
|
||||
std::vector<std::uint8_t> bytes;
|
||||
std::vector<gc::StageCatalogEntry> entries;
|
||||
std::string error;
|
||||
if (!readFile(argv[1], &bytes) || !gc::ParseStageCatalog(bytes, &entries, &error)) {
|
||||
std::cerr << argv[1] << ": " << (error.empty() ? "could not read catalog" : error) << '\n';
|
||||
return 1;
|
||||
}
|
||||
if (listMode) {
|
||||
for (const gc::StageCatalogEntry& entry : entries) {
|
||||
std::cout << entry.id << '\t' << entry.title << '\t'
|
||||
<< entry.artist << '\t' << entry.imageKey << '\t'
|
||||
<< entry.bgmBase;
|
||||
for (const std::string& chart : entry.chartIds) {
|
||||
std::cout << '\t' << chart;
|
||||
}
|
||||
std::cout << '\n';
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
const std::filesystem::path stageDirectory =
|
||||
std::filesystem::path(argv[1]).parent_path().parent_path() / "stage";
|
||||
const std::filesystem::path soundDirectory = stageDirectory / "sound";
|
||||
std::vector<openroller::psp::SongCatalogRecord> records;
|
||||
for (int argument = 3; argument < argc; ++argument) {
|
||||
const std::string token = argv[argument];
|
||||
if (token.size() >= 32u) {
|
||||
std::cerr << token << ": song token is too long\n";
|
||||
return 1;
|
||||
}
|
||||
const gc::StageCatalogEntry* entry = findSong(entries, token);
|
||||
if (!entry) {
|
||||
std::cerr << token << ": no stage_param.dat record\n";
|
||||
return 1;
|
||||
}
|
||||
openroller::psp::SongCatalogRecord record{};
|
||||
const AsciiMetadata* metadata = asciiMetadata(token);
|
||||
copyDisplay(record.key, token);
|
||||
copyDisplay(record.title, metadata ? metadata->title : entry->title);
|
||||
copyDisplay(record.artist, metadata ? metadata->artist : entry->artist);
|
||||
copyDisplay(record.duration, entry->duration);
|
||||
copyDisplay(record.bpm, entry->bpm);
|
||||
record.genre = entry->genre;
|
||||
std::copy(entry->difficultyRatings.begin(), entry->difficultyRatings.end(), record.ratings);
|
||||
for (std::size_t difficulty = 0; difficulty < 4; ++difficulty) {
|
||||
const std::string chart = chartAt(*entry, difficulty);
|
||||
const bool exists = !chart.empty() &&
|
||||
std::filesystem::is_regular_file(stageDirectory / (chart + ".dat"));
|
||||
if (exists) record.availableMask |= static_cast<std::uint8_t>(1u << difficulty);
|
||||
if (exists) {
|
||||
static constexpr const char* names[4] = {
|
||||
"easy", "normal", "hard", "extra",
|
||||
};
|
||||
const std::string bgmName =
|
||||
entry->bgmBase + entry->chartGroup0[difficulty] + "_BGM.wav";
|
||||
const std::string shotName =
|
||||
entry->bgmBase + entry->chartSuffixes[difficulty] + "_SHOT.wav";
|
||||
std::cout << token << '\t' << entry->imageKey << '\t'
|
||||
<< names[difficulty] << '\t' << chart << '\t'
|
||||
<< resolveAudioFilename(soundDirectory, bgmName) << '\t'
|
||||
<< resolveAudioFilename(soundDirectory, shotName) << '\t'
|
||||
<< static_cast<unsigned>(entry->bgmVolumes[difficulty]) << '\t'
|
||||
<< static_cast<unsigned>(entry->shotVolumes[difficulty]) << '\n';
|
||||
}
|
||||
}
|
||||
if (record.availableMask == 0) {
|
||||
std::cerr << token << ": catalog record has no local charts\n";
|
||||
return 1;
|
||||
}
|
||||
records.push_back(record);
|
||||
}
|
||||
if (records.size() > openroller::psp::kMaximumCatalogSongs) {
|
||||
std::cerr << "PSP catalog exceeds its song budget\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
openroller::psp::SongCatalogHeader header{};
|
||||
std::copy(std::begin(openroller::psp::kSongCatalogMagic),
|
||||
std::end(openroller::psp::kSongCatalogMagic), header.magic);
|
||||
header.version = openroller::psp::kSongCatalogVersion;
|
||||
header.headerSize = sizeof(header);
|
||||
header.songCount = static_cast<std::uint32_t>(records.size());
|
||||
header.recordSize = sizeof(openroller::psp::SongCatalogRecord);
|
||||
header.fileSize = static_cast<std::uint32_t>(
|
||||
sizeof(header) + records.size() * sizeof(openroller::psp::SongCatalogRecord));
|
||||
std::ofstream output(argv[2], std::ios::binary | std::ios::trunc);
|
||||
output.write(reinterpret_cast<const char*>(&header), sizeof(header));
|
||||
output.write(reinterpret_cast<const char*>(records.data()),
|
||||
static_cast<std::streamsize>(records.size() * sizeof(records.front())));
|
||||
if (!output) {
|
||||
std::cerr << argv[2] << ": could not write catalog\n";
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,600 @@
|
||||
#include "gc/StageDat.hpp"
|
||||
#include "gc/StagePattern.hpp"
|
||||
#include "gc/TumoModel.hpp"
|
||||
#include "openroller/psp/StagePackage.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__
|
||||
#error "openroller-psp-pack currently requires a little-endian host"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace openroller::psp;
|
||||
|
||||
struct SystemTimingConfig {
|
||||
bool missMarkOverride = false;
|
||||
float greatMinimumTimeMs = 32.0f;
|
||||
float scratchEnableTimeMs = 250.0f;
|
||||
float beatEnableTimeMs = 200.0f;
|
||||
std::array<float, 4> miss{236.0f, 202.0f, 168.0f, 168.0f};
|
||||
std::array<float, 4> unmute{202.0f, 168.0f, 134.0f, 134.0f};
|
||||
std::array<float, 4> limit{202.0f, 168.0f, 134.0f, 134.0f};
|
||||
std::array<float, 4> mute{0.0f, 0.0f, 0.0f, 0.0f};
|
||||
};
|
||||
|
||||
std::string trim(std::string value) {
|
||||
const auto first = std::find_if_not(value.begin(), value.end(),
|
||||
[](unsigned char c) { return std::isspace(c); });
|
||||
const auto last = std::find_if_not(value.rbegin(), value.rend(),
|
||||
[](unsigned char c) { return std::isspace(c); }).base();
|
||||
return first < last ? std::string(first, last) : std::string{};
|
||||
}
|
||||
|
||||
bool parseFloatTuple4(std::string value, std::array<float, 4>* output) {
|
||||
if (!output) return false;
|
||||
for (char& c : value) {
|
||||
if (c == '(' || c == ')' || c == ',') c = ' ';
|
||||
}
|
||||
std::stringstream stream(value);
|
||||
std::array<float, 4> parsed{};
|
||||
if (!(stream >> parsed[0] >> parsed[1] >> parsed[2] >> parsed[3])) return false;
|
||||
*output = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
SystemTimingConfig loadSystemTimingConfig(const std::string& sourcePath) {
|
||||
SystemTimingConfig config;
|
||||
const std::filesystem::path path =
|
||||
std::filesystem::path(sourcePath).parent_path().parent_path() / "system.cfg";
|
||||
std::ifstream file(path);
|
||||
if (!file) return config;
|
||||
std::string line;
|
||||
bool blockComment = false;
|
||||
while (std::getline(file, line)) {
|
||||
if (blockComment) {
|
||||
const std::size_t end = line.find("*/");
|
||||
if (end == std::string::npos) continue;
|
||||
line.erase(0, end + 2);
|
||||
blockComment = false;
|
||||
}
|
||||
for (;;) {
|
||||
const std::size_t begin = line.find("/*");
|
||||
if (begin == std::string::npos) break;
|
||||
const std::size_t end = line.find("*/", begin + 2);
|
||||
if (end == std::string::npos) {
|
||||
line.resize(begin);
|
||||
blockComment = true;
|
||||
break;
|
||||
}
|
||||
line.erase(begin, end + 2 - begin);
|
||||
}
|
||||
const std::size_t comment = line.find("//");
|
||||
if (comment != std::string::npos) line.resize(comment);
|
||||
const std::size_t equals = line.find('=');
|
||||
if (equals == std::string::npos) continue;
|
||||
const std::string key = trim(line.substr(0, equals));
|
||||
const std::string value = trim(line.substr(equals + 1));
|
||||
try {
|
||||
if (key == "MissMarkOverride") config.missMarkOverride = std::stoi(value) != 0;
|
||||
else if (key == "GreatMinTime") config.greatMinimumTimeMs = std::stof(value);
|
||||
else if (key == "ScratchEnableTime") config.scratchEnableTimeMs = std::stof(value);
|
||||
else if (key == "BeatEnableTime") config.beatEnableTimeMs = std::stof(value);
|
||||
else if (key == "MissTimingOverride") parseFloatTuple4(value, &config.miss);
|
||||
else if (key == "UnmuteTimingOverride") parseFloatTuple4(value, &config.unmute);
|
||||
else if (key == "LimitTimingOverride") parseFloatTuple4(value, &config.limit);
|
||||
else if (key == "MuteTimingOverride") parseFloatTuple4(value, &config.mute);
|
||||
} catch (const std::exception&) {
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
PackageDifficulty inferDifficulty(const std::string& sourcePath) {
|
||||
std::string lower = sourcePath;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char value) {
|
||||
return static_cast<char>(std::tolower(value));
|
||||
});
|
||||
if (lower.find("_extra.dat") != std::string::npos ||
|
||||
lower.find("_ex.dat") != std::string::npos) return PackageDifficulty::Extra;
|
||||
if (lower.find("_hard.dat") != std::string::npos) return PackageDifficulty::Hard;
|
||||
if (lower.find("_normal.dat") != std::string::npos) return PackageDifficulty::Normal;
|
||||
return PackageDifficulty::Easy;
|
||||
}
|
||||
|
||||
float beatDurationAt(const gc::StageConfig& config, std::uint32_t timeMs) {
|
||||
std::uint32_t bpm = 120;
|
||||
for (const gc::BpmChange& change : config.bpmChanges) {
|
||||
if (change.timeMs > timeMs) break;
|
||||
if (change.bpm != 0) bpm = change.bpm;
|
||||
}
|
||||
return 60000.0f / static_cast<float>(std::max<std::uint32_t>(1, bpm));
|
||||
}
|
||||
|
||||
float timingAt(
|
||||
const std::vector<gc::NoteSetting>& settings,
|
||||
std::uint32_t timeMs,
|
||||
float beatMs,
|
||||
float nextSpacingMs) {
|
||||
if (settings.empty()) return beatMs * 0.5f;
|
||||
const gc::NoteSetting* active = &settings.front();
|
||||
for (const gc::NoteSetting& setting : settings) {
|
||||
if (setting.timeMs > timeMs) break;
|
||||
active = &setting;
|
||||
}
|
||||
if (active->mode == 1) return active->value;
|
||||
if (active->mode == 3) return nextSpacingMs;
|
||||
return active->value * beatMs;
|
||||
}
|
||||
|
||||
std::uint8_t effectiveNoteType(const gc::StageNote& note) {
|
||||
if (note.typeOverride) return 1;
|
||||
const std::uint8_t raw = static_cast<std::uint8_t>(note.type);
|
||||
if (raw == 0x0b) return 0x0a;
|
||||
if (raw == 0x0c || raw == 0x0e) return 0x09;
|
||||
if (raw == 0x0d) return 0x04;
|
||||
return raw;
|
||||
}
|
||||
|
||||
void directionVector(const gc::StageNote& note, std::uint8_t effectiveType, float output[3]) {
|
||||
float distance = note.params25[0];
|
||||
const std::uint8_t rawType = static_cast<std::uint8_t>(note.type);
|
||||
if ((effectiveType == 2 || effectiveType == 10 || rawType == 0x10) && distance <= 0.0f) {
|
||||
distance = 1.0f;
|
||||
}
|
||||
const float pi = 3.14159265358979323846f;
|
||||
const float a = (-note.params25[2] * pi / 180.0f) * 0.5f;
|
||||
const float b = (note.params25[1] * pi / 180.0f) * 0.5f;
|
||||
const float ca = std::cos(a);
|
||||
const float cb = std::cos(b);
|
||||
const float sa = std::sin(a);
|
||||
const float sb = std::sin(b);
|
||||
const float qw = ca * cb;
|
||||
const float qx = sa * cb;
|
||||
const float qy = ca * sb;
|
||||
const float qz = sa * sb;
|
||||
output[0] = distance * 2.0f * (qz * qx + qw * qy);
|
||||
output[1] = distance * 2.0f * (qy * qz - qw * qx);
|
||||
output[2] = distance * (1.0f - 2.0f * (qx * qx + qy * qy));
|
||||
}
|
||||
|
||||
std::uint32_t rgba(const gc::Color& color) {
|
||||
return static_cast<std::uint32_t>(color.r) |
|
||||
(static_cast<std::uint32_t>(color.g) << 8) |
|
||||
(static_cast<std::uint32_t>(color.b) << 16) |
|
||||
(static_cast<std::uint32_t>(color.a) << 24);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void appendPod(std::vector<std::uint8_t>* bytes, const T& value) {
|
||||
static_assert(std::is_trivially_copyable<T>::value, "package records must be POD");
|
||||
const auto* first = reinterpret_cast<const std::uint8_t*>(&value);
|
||||
bytes->insert(bytes->end(), first, first + sizeof(T));
|
||||
}
|
||||
|
||||
void align16(std::vector<std::uint8_t>* bytes) {
|
||||
bytes->resize((bytes->size() + 15u) & ~std::size_t(15u), 0);
|
||||
}
|
||||
|
||||
template <typename Source, typename Packed, typename Convert>
|
||||
PackageSection appendSection(
|
||||
std::vector<std::uint8_t>* bytes,
|
||||
const std::vector<Source>& source,
|
||||
Convert&& convert) {
|
||||
if (source.size() > std::numeric_limits<std::uint32_t>::max()) {
|
||||
throw std::length_error("PSP package section exceeds 32-bit count");
|
||||
}
|
||||
align16(bytes);
|
||||
PackageSection section{
|
||||
static_cast<std::uint32_t>(bytes->size()),
|
||||
static_cast<std::uint32_t>(source.size()),
|
||||
};
|
||||
for (const Source& item : source) {
|
||||
const Packed packed = convert(item);
|
||||
appendPod(bytes, packed);
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
std::uint32_t stageDuration(const gc::ParsedStagePattern& stage) {
|
||||
std::uint32_t duration = 0;
|
||||
const auto includeTimes = [&duration](const auto& values) {
|
||||
for (const auto& value : values) duration = std::max(duration, value.timeMs);
|
||||
};
|
||||
includeTimes(stage.track);
|
||||
includeTimes(stage.notes);
|
||||
includeTimes(stage.cameras);
|
||||
includeTimes(stage.drawDistances);
|
||||
includeTimes(stage.backgroundColors);
|
||||
return duration;
|
||||
}
|
||||
|
||||
PackageTrackPoint packTrack(const gc::TrackPiece& source) {
|
||||
return {source.timeMs, source.x, source.y, source.z};
|
||||
}
|
||||
|
||||
PackageNote packNote(const gc::StageNote& source) {
|
||||
PackageNote output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.type = static_cast<std::uint8_t>(source.type);
|
||||
if (source.typeOverride) output.flags |= kNoteTypeOverride;
|
||||
if (source.flag24) output.flags |= kNoteFlag24;
|
||||
if (source.flag37) output.flags |= kNoteFlag37;
|
||||
if (source.flag38) output.flags |= kNoteFlag38;
|
||||
std::copy(source.params16.begin(), source.params16.end(), output.params16);
|
||||
std::copy(source.params25.begin(), source.params25.end(), output.params25);
|
||||
std::copy(source.params39.begin(), source.params39.end(), output.params39);
|
||||
std::copy(source.params55.begin(), source.params55.end(), output.params55);
|
||||
output.param67 = source.param67;
|
||||
output.param71 = source.param71;
|
||||
std::copy(source.params75.begin(), source.params75.end(), output.params75);
|
||||
output.param95 = source.param95;
|
||||
return output;
|
||||
}
|
||||
|
||||
std::vector<PackageNote> buildNotes(
|
||||
const gc::ParsedStagePattern& stage,
|
||||
const std::string& sourcePath) {
|
||||
std::vector<PackageNote> output;
|
||||
output.reserve(stage.notes.size());
|
||||
const SystemTimingConfig systemTiming = loadSystemTimingConfig(sourcePath);
|
||||
const std::size_t difficulty =
|
||||
static_cast<std::size_t>(inferDifficulty(sourcePath));
|
||||
for (std::size_t i = 0; i < stage.notes.size(); ++i) {
|
||||
const gc::StageNote& source = stage.notes[i];
|
||||
PackageNote note = packNote(source);
|
||||
note.effectiveType = effectiveNoteType(source);
|
||||
note.markEffectId = source.params16[0];
|
||||
note.beatDurationMs = beatDurationAt(stage.config, source.timeMs);
|
||||
note.appearTimeMs = std::max(
|
||||
0.0f,
|
||||
static_cast<float>(source.timeMs) -
|
||||
std::max(0.0f, source.params39[0]) * note.beatDurationMs);
|
||||
const bool durationType =
|
||||
note.effectiveType == 3 || note.effectiveType == 4 ||
|
||||
note.effectiveType == 5 || note.effectiveType == 10 ||
|
||||
note.effectiveType == 15;
|
||||
const float durationBeats = note.effectiveType == 6
|
||||
? static_cast<float>(source.param71) * std::max(0.0f, source.params75[0])
|
||||
: std::max(0.0f, source.params39[3]);
|
||||
note.endTimeMs = (durationType || note.effectiveType == 6)
|
||||
? std::max(
|
||||
static_cast<float>(source.timeMs),
|
||||
static_cast<float>(source.timeMs) + durationBeats * note.beatDurationMs)
|
||||
: static_cast<float>(source.timeMs);
|
||||
const float nextSpacingMs = i + 1 < stage.notes.size()
|
||||
? std::max(
|
||||
0.0f,
|
||||
static_cast<float>(stage.notes[i + 1].timeMs) -
|
||||
static_cast<float>(source.timeMs))
|
||||
: note.beatDurationMs * 2.0f;
|
||||
note.missTimingMs = std::max(
|
||||
0.0f,
|
||||
timingAt(stage.config.noteSettings[0], source.timeMs,
|
||||
note.beatDurationMs, nextSpacingMs));
|
||||
note.earlyTimingMs = std::max(
|
||||
0.0f,
|
||||
timingAt(stage.config.noteSettings[1], source.timeMs,
|
||||
note.beatDurationMs, nextSpacingMs));
|
||||
note.lateTimingMs = std::max(
|
||||
0.0f,
|
||||
timingAt(stage.config.noteSettings[2], source.timeMs,
|
||||
note.beatDurationMs, nextSpacingMs));
|
||||
note.muteTimingMs = std::max(
|
||||
0.0f,
|
||||
timingAt(stage.config.noteSettings[3], source.timeMs,
|
||||
note.beatDurationMs, nextSpacingMs));
|
||||
if (systemTiming.missMarkOverride) {
|
||||
note.missTimingMs = std::max(0.0f, systemTiming.miss[difficulty]);
|
||||
note.earlyTimingMs = std::max(0.0f, systemTiming.unmute[difficulty]);
|
||||
note.lateTimingMs = std::max(0.0f, systemTiming.limit[difficulty]);
|
||||
note.muteTimingMs = std::max(0.0f, systemTiming.mute[difficulty]);
|
||||
}
|
||||
if (note.effectiveType == 2 || note.type == 0x10) {
|
||||
note.earlyTimingMs += note.beatDurationMs * 0.2f;
|
||||
note.lateTimingMs += note.beatDurationMs * 0.2f;
|
||||
}
|
||||
const float fadeAnchor = (durationType || note.effectiveType == 6)
|
||||
? note.endTimeMs
|
||||
: static_cast<float>(source.timeMs) + note.lateTimingMs;
|
||||
note.markerFadeEndTimeMs = fadeAnchor + note.beatDurationMs * 4.0f;
|
||||
note.packedColor = source.params55[0];
|
||||
note.merryCount = source.param71;
|
||||
directionVector(source, note.effectiveType, note.directionVector);
|
||||
output.push_back(note);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
PackageCameraPoint packCamera(const gc::CameraPoint& source) {
|
||||
PackageCameraPoint output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.aMode = source.aMode;
|
||||
output.fMode = source.fMode;
|
||||
output.projectionType = source.projType;
|
||||
output.distance = source.dist;
|
||||
std::copy(std::begin(source.rotationA), std::end(source.rotationA), output.rotationA);
|
||||
std::copy(std::begin(source.originOff), std::end(source.originOff), output.originOffset);
|
||||
std::copy(std::begin(source.fieldFar), std::end(source.fieldFar), output.fieldFar);
|
||||
std::copy(std::begin(source.fieldNear), std::end(source.fieldNear), output.fieldNear);
|
||||
output.rotationB = source.rotationB;
|
||||
return output;
|
||||
}
|
||||
|
||||
PackageDrawDistancePoint packDrawDistance(const gc::DrawDistancePoint& source) {
|
||||
return {source.timeMs, source.distance};
|
||||
}
|
||||
|
||||
PackageBackgroundColorPoint packBackgroundColor(const gc::BackgroundColorPoint& source) {
|
||||
PackageBackgroundColorPoint output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.topRightRgba = rgba(source.topRight);
|
||||
output.topLeftRgba = rgba(source.topLeft);
|
||||
output.bottomRightRgba = rgba(source.bottomRight);
|
||||
output.bottomLeftRgba = rgba(source.bottomLeft);
|
||||
output.flags = (source.interpolateToNext ? 1u : 0u) |
|
||||
(source.audioReactive ? 2u : 0u);
|
||||
return output;
|
||||
}
|
||||
|
||||
template <typename Source, typename Packed, typename Convert>
|
||||
PackageRange appendRange(
|
||||
const std::vector<Source>& source,
|
||||
std::vector<Packed>* destination,
|
||||
Convert&& convert) {
|
||||
if (!destination || destination->size() > std::numeric_limits<std::uint32_t>::max() ||
|
||||
source.size() > std::numeric_limits<std::uint32_t>::max() - destination->size()) {
|
||||
throw std::length_error("PSP background key array exceeds 32-bit range");
|
||||
}
|
||||
PackageRange range{
|
||||
static_cast<std::uint32_t>(destination->size()),
|
||||
static_cast<std::uint32_t>(source.size()),
|
||||
};
|
||||
destination->reserve(destination->size() + source.size());
|
||||
for (const Source& item : source) destination->push_back(convert(item));
|
||||
return range;
|
||||
}
|
||||
|
||||
PackageVisibilityKey packVisibility(const gc::VisibilityPoint& source) {
|
||||
PackageVisibilityKey output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.flags = static_cast<std::uint8_t>(
|
||||
(source.fadeOut ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
||||
(source.fadeIn ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
||||
output.visible = source.visible ? 1u : 0u;
|
||||
return output;
|
||||
}
|
||||
|
||||
PackageTransformKey packTransform(const gc::TransformPoint& source) {
|
||||
PackageTransformKey output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.flags = static_cast<std::uint8_t>(
|
||||
(source.tweenTowards ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
||||
(source.tweenAway ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
||||
std::copy(std::begin(source.value), std::end(source.value), output.value);
|
||||
return output;
|
||||
}
|
||||
|
||||
PackageObjectColorKey packObjectColor(const gc::ObjectColorPoint& source) {
|
||||
PackageObjectColorKey output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.flags = static_cast<std::uint8_t>(
|
||||
(source.tweenTowards ? static_cast<std::uint8_t>(kObjectKeyLoop) : 0u) |
|
||||
(source.tweenAway ? static_cast<std::uint8_t>(kObjectKeyInterpolate) : 0u));
|
||||
output.rgba = rgba(source.color);
|
||||
return output;
|
||||
}
|
||||
|
||||
PackageParticlePoint packParticle(const gc::ParticlePoint& source) {
|
||||
PackageParticlePoint output{};
|
||||
output.timeMs = source.timeMs;
|
||||
output.enabled = source.enabled;
|
||||
output.shape = source.shape;
|
||||
output.texture = source.texture;
|
||||
output.rgba = rgba(source.color);
|
||||
std::copy(std::begin(source.velocity), std::end(source.velocity), output.velocity);
|
||||
output.repeatMeasure = source.repeatMeasure;
|
||||
output.lifespanMeasure = source.lifespanMeasure;
|
||||
output.groupShapeSize = source.groupShapeSize;
|
||||
return output;
|
||||
}
|
||||
|
||||
PackageVisualizerPoint packVisualizer(const gc::VisualizerPoint& source) {
|
||||
return {source.timeMs, source.type, rgba(source.color)};
|
||||
}
|
||||
|
||||
void appendGeometry(
|
||||
const std::vector<gc::TumoVertex>& source,
|
||||
std::vector<PackageBackgroundVertex>* vertices,
|
||||
PackageRange* range) {
|
||||
if (!vertices || !range || vertices->size() > std::numeric_limits<std::uint32_t>::max() ||
|
||||
source.size() > std::numeric_limits<std::uint32_t>::max() - vertices->size()) {
|
||||
throw std::length_error("PSP background vertex array exceeds 32-bit range");
|
||||
}
|
||||
range->first = static_cast<std::uint32_t>(vertices->size());
|
||||
range->count = static_cast<std::uint32_t>(source.size());
|
||||
vertices->reserve(vertices->size() + source.size());
|
||||
for (const gc::TumoVertex& vertex : source) {
|
||||
vertices->push_back({vertex.x, vertex.y, vertex.z});
|
||||
}
|
||||
}
|
||||
|
||||
bool writePackage(
|
||||
const std::string& sourcePath,
|
||||
const std::string& outputPath,
|
||||
std::string* error) {
|
||||
gc::StageDat dat;
|
||||
if (!gc::StageDat::LoadFromFile(sourcePath, dat, error)) return false;
|
||||
|
||||
gc::ParsedStagePattern stage;
|
||||
if (!gc::ParseStagePattern(dat, &stage, error)) return false;
|
||||
if (stage.objects.size() > kMaximumPackageBackgroundObjects) {
|
||||
if (error) *error = "stage exceeds the PSP background object budget";
|
||||
return false;
|
||||
}
|
||||
|
||||
StagePackageHeader header{};
|
||||
const SystemTimingConfig systemTiming = loadSystemTimingConfig(sourcePath);
|
||||
std::copy(std::begin(kStagePackageMagic), std::end(kStagePackageMagic), header.magic);
|
||||
header.version = kStagePackageVersion;
|
||||
header.headerSize = sizeof(StagePackageHeader);
|
||||
header.flags = static_cast<std::uint32_t>(inferDifficulty(sourcePath));
|
||||
header.durationMs = stageDuration(stage);
|
||||
header.audioOffsetRaw = stage.config.audioOffset;
|
||||
header.visualOffset = stage.config.visualOffset;
|
||||
header.greatMinimumTimeMs = std::max(0.0f, systemTiming.greatMinimumTimeMs);
|
||||
header.scratchEnableTimeMs = std::max(0.0f, systemTiming.scratchEnableTimeMs);
|
||||
header.beatEnableTimeMs = std::max(0.0f, systemTiming.beatEnableTimeMs);
|
||||
header.backwardsDrawDistance = stage.config.backwardsDrawDist;
|
||||
header.forwardDrawDistance = stage.config.forwardDrawDist;
|
||||
header.trackAheadRgba = rgba(stage.config.trackAheadColor);
|
||||
header.trackBehindRgba = rgba(stage.config.trackBehindColor);
|
||||
|
||||
std::vector<PackageBackgroundModel> backgroundModels;
|
||||
std::vector<PackageBackgroundVertex> backgroundVertices;
|
||||
backgroundModels.reserve(stage.modelNames.size());
|
||||
const std::filesystem::path modelDirectory =
|
||||
std::filesystem::path(sourcePath).parent_path().parent_path() / "model";
|
||||
for (const std::string& modelName : stage.modelNames) {
|
||||
gc::TumoGeometry geometry;
|
||||
std::string modelError;
|
||||
const std::filesystem::path modelPath = modelDirectory / (modelName + ".tumo");
|
||||
if (!gc::LoadTumoGeometry(modelPath.string(), &geometry, &modelError)) {
|
||||
if (error) *error = modelPath.string() + ": " + modelError;
|
||||
return false;
|
||||
}
|
||||
PackageBackgroundModel model{};
|
||||
appendGeometry(geometry.triangles, &backgroundVertices, &model.triangles);
|
||||
appendGeometry(geometry.solidLines, &backgroundVertices, &model.solidLines);
|
||||
appendGeometry(geometry.wireframeLines, &backgroundVertices, &model.wireframeLines);
|
||||
backgroundModels.push_back(model);
|
||||
}
|
||||
|
||||
std::vector<PackageBackgroundObject> backgroundObjects;
|
||||
std::vector<PackageVisibilityKey> visibilityKeys;
|
||||
std::vector<PackageTransformKey> transformKeys;
|
||||
std::vector<PackageObjectColorKey> objectColorKeys;
|
||||
backgroundObjects.reserve(stage.objects.size());
|
||||
for (const gc::StageObject& source : stage.objects) {
|
||||
PackageBackgroundObject object{};
|
||||
object.model = source.model;
|
||||
object.parentIndex = source.parentIndex;
|
||||
object.flags = (source.wireframe ? kBackgroundObjectWireframe : 0u) |
|
||||
(source.flashing ? kBackgroundObjectFlashing : 0u) |
|
||||
(source.unknownFlag ? kBackgroundObjectUnknown : 0u);
|
||||
object.fragmentShader = source.fragmentShader;
|
||||
std::copy(std::begin(source.position), std::end(source.position), object.position);
|
||||
std::copy(std::begin(source.scale), std::end(source.scale), object.scale);
|
||||
std::copy(std::begin(source.rotation), std::end(source.rotation), object.rotation);
|
||||
std::copy(std::begin(source.color), std::end(source.color), object.color);
|
||||
object.visibility = appendRange<gc::VisibilityPoint, PackageVisibilityKey>(
|
||||
source.visibility, &visibilityKeys, packVisibility);
|
||||
object.movement = appendRange<gc::TransformPoint, PackageTransformKey>(
|
||||
source.movement, &transformKeys, packTransform);
|
||||
object.scaling = appendRange<gc::TransformPoint, PackageTransformKey>(
|
||||
source.scaling, &transformKeys, packTransform);
|
||||
object.rotations = appendRange<gc::TransformPoint, PackageTransformKey>(
|
||||
source.rotations, &transformKeys, packTransform);
|
||||
object.colorChanges = appendRange<gc::ObjectColorPoint, PackageObjectColorKey>(
|
||||
source.colorChanges, &objectColorKeys, packObjectColor);
|
||||
backgroundObjects.push_back(object);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> bytes(sizeof(StagePackageHeader), 0);
|
||||
const std::vector<PackageNote> notes = buildNotes(stage, sourcePath);
|
||||
header.track = appendSection<gc::TrackPiece, PackageTrackPoint>(&bytes, stage.track, packTrack);
|
||||
header.notes = appendSection<PackageNote, PackageNote>(
|
||||
&bytes, notes, [](const PackageNote& item) { return item; });
|
||||
header.cameras = appendSection<gc::CameraPoint, PackageCameraPoint>(&bytes, stage.cameras, packCamera);
|
||||
header.drawDistances = appendSection<gc::DrawDistancePoint, PackageDrawDistancePoint>(
|
||||
&bytes, stage.drawDistances, packDrawDistance);
|
||||
header.backgroundColors = appendSection<gc::BackgroundColorPoint, PackageBackgroundColorPoint>(
|
||||
&bytes, stage.backgroundColors, packBackgroundColor);
|
||||
header.backgroundModels = appendSection<PackageBackgroundModel, PackageBackgroundModel>(
|
||||
&bytes, backgroundModels, [](const PackageBackgroundModel& item) { return item; });
|
||||
header.backgroundVertices = appendSection<PackageBackgroundVertex, PackageBackgroundVertex>(
|
||||
&bytes, backgroundVertices, [](const PackageBackgroundVertex& item) { return item; });
|
||||
header.backgroundObjects = appendSection<PackageBackgroundObject, PackageBackgroundObject>(
|
||||
&bytes, backgroundObjects, [](const PackageBackgroundObject& item) { return item; });
|
||||
header.visibilityKeys = appendSection<PackageVisibilityKey, PackageVisibilityKey>(
|
||||
&bytes, visibilityKeys, [](const PackageVisibilityKey& item) { return item; });
|
||||
header.transformKeys = appendSection<PackageTransformKey, PackageTransformKey>(
|
||||
&bytes, transformKeys, [](const PackageTransformKey& item) { return item; });
|
||||
header.objectColorKeys = appendSection<PackageObjectColorKey, PackageObjectColorKey>(
|
||||
&bytes, objectColorKeys, [](const PackageObjectColorKey& item) { return item; });
|
||||
header.particles = appendSection<gc::ParticlePoint, PackageParticlePoint>(
|
||||
&bytes, stage.particles, packParticle);
|
||||
header.visualizer = appendSection<gc::VisualizerPoint, PackageVisualizerPoint>(
|
||||
&bytes, stage.visualizer, packVisualizer);
|
||||
header.bpmChanges = appendSection<gc::BpmChange, PackageBpmPoint>(
|
||||
&bytes, stage.config.bpmChanges,
|
||||
[](const gc::BpmChange& item) { return PackageBpmPoint{item.timeMs, item.bpm}; });
|
||||
align16(&bytes);
|
||||
|
||||
if (bytes.size() > std::numeric_limits<std::uint32_t>::max()) {
|
||||
if (error) *error = "PSP stage package exceeds 4 GiB";
|
||||
return false;
|
||||
}
|
||||
header.fileSize = static_cast<std::uint32_t>(bytes.size());
|
||||
std::memcpy(bytes.data(), &header, sizeof(header));
|
||||
|
||||
std::ofstream output(outputPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
if (error) *error = "could not create output file";
|
||||
return false;
|
||||
}
|
||||
output.write(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<std::streamsize>(bytes.size()));
|
||||
if (!output) {
|
||||
if (error) *error = "failed while writing output file";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << outputPath << ": " << bytes.size() << " bytes"
|
||||
<< ", track=" << header.track.count
|
||||
<< ", notes=" << header.notes.count
|
||||
<< ", cameras=" << header.cameras.count
|
||||
<< ", colors=" << header.backgroundColors.count
|
||||
<< ", bgModels=" << header.backgroundModels.count
|
||||
<< ", bgObjects=" << header.backgroundObjects.count
|
||||
<< ", bgVertices=" << header.backgroundVertices.count
|
||||
<< ", particles=" << header.particles.count
|
||||
<< ", visualizer=" << header.visualizer.count
|
||||
<< ", difficulty=" << (header.flags & kStageDifficultyMask) << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3) {
|
||||
std::cerr << "usage: openroller-psp-pack <stage.dat> <stage.orps>\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
try {
|
||||
std::string error;
|
||||
if (!writePackage(argv[1], argv[2], &error)) {
|
||||
std::cerr << argv[1] << ": " << error << '\n';
|
||||
return 1;
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
std::cerr << "openroller-psp-pack: " << exception.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "StageRuntime.hpp"
|
||||
#include "Gameplay.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
bool finite(openroller::psp::Vec3 value) {
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
std::cerr << "usage: openroller-psp-runtime-probe <stage.orps>\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
openroller::psp::StageView stage{};
|
||||
char error[128]{};
|
||||
if (!openroller::psp::loadStagePackage(argv[1], &stage, error, sizeof(error))) {
|
||||
std::cerr << argv[1] << ": " << error << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
bool valid = true;
|
||||
const float samples[] = {
|
||||
0.0f,
|
||||
static_cast<float>(stage.header->durationMs) * 0.5f,
|
||||
static_cast<float>(stage.header->durationMs),
|
||||
};
|
||||
for (float timeMs : samples) {
|
||||
const auto position = openroller::psp::trackPositionAt(stage, timeMs);
|
||||
const auto camera = openroller::psp::evaluateCamera(stage, timeMs);
|
||||
valid = valid && finite(position) && finite(camera.eye) && finite(camera.target) && finite(camera.up) &&
|
||||
std::isfinite(camera.projectionBlend);
|
||||
std::cout << "t=" << timeMs
|
||||
<< " track=(" << position.x << ',' << position.y << ',' << position.z << ')'
|
||||
<< " eye=(" << camera.eye.x << ',' << camera.eye.y << ',' << camera.eye.z << ')'
|
||||
<< " target=(" << camera.target.x << ',' << camera.target.y << ',' << camera.target.z << ')'
|
||||
<< " projection=" << camera.projectionBlend << '\n';
|
||||
}
|
||||
|
||||
std::cout << "bytes=" << stage.storageSize
|
||||
<< " track=" << stage.header->track.count
|
||||
<< " notes=" << stage.header->notes.count
|
||||
<< " cameras=" << stage.header->cameras.count
|
||||
<< " bg_models=" << stage.header->backgroundModels.count
|
||||
<< " bg_objects=" << stage.header->backgroundObjects.count
|
||||
<< " bg_vertices=" << stage.header->backgroundVertices.count
|
||||
<< " duration_ms=" << stage.header->durationMs << '\n';
|
||||
|
||||
openroller::psp::GameplayState gameplay{};
|
||||
if (!openroller::psp::initializeGameplay(stage, &gameplay)) {
|
||||
std::cerr << "could not initialize PSP gameplay state\n";
|
||||
valid = false;
|
||||
} else {
|
||||
std::uint32_t tapIndex = 0;
|
||||
while (tapIndex < stage.header->notes.count &&
|
||||
stage.notes[tapIndex].effectiveType != 1 &&
|
||||
stage.notes[tapIndex].effectiveType != 2) {
|
||||
++tapIndex;
|
||||
}
|
||||
if (tapIndex == stage.header->notes.count) {
|
||||
std::cerr << "stage has no single-tap note for gameplay probe\n";
|
||||
valid = false;
|
||||
tapIndex = 0;
|
||||
}
|
||||
const float firstNoteMs = static_cast<float>(stage.notes[tapIndex].timeMs);
|
||||
const auto judgment = openroller::psp::pressGameplay(
|
||||
stage, &gameplay, firstNoteMs, 1u);
|
||||
valid = valid && judgment == openroller::psp::Judgment::Great && gameplay.combo == 1;
|
||||
std::cout << "tap_at=" << firstNoteMs
|
||||
<< " judgment=" << static_cast<int>(judgment)
|
||||
<< " combo=" << gameplay.combo << '\n';
|
||||
openroller::psp::seekGameplay(stage, &gameplay, 0.0f);
|
||||
openroller::psp::updateGameplay(
|
||||
stage, &gameplay,
|
||||
firstNoteMs + stage.notes[tapIndex].lateTimingMs + 1.0f);
|
||||
valid = valid && gameplay.missCount > 0;
|
||||
std::cout << "misses_after_window=" << gameplay.missCount << '\n';
|
||||
openroller::psp::destroyGameplay(&gameplay);
|
||||
}
|
||||
openroller::psp::unloadStagePackage(&stage);
|
||||
return valid ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user