From 9e69a839675d5bda5f38330a02326e30dd1bc2b7 Mon Sep 17 00:00:00 2001 From: malleoz <16770560+malleoz@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:02:16 -0500 Subject: [PATCH] Batch replay --- source/abstract/File.cc | 16 +++- source/abstract/File.hh | 4 + source/host/KReplaySystem.cc | 153 +++++++++++++++++++++++++++++------ source/host/KReplaySystem.hh | 12 +++ source/host/Option.cc | 7 ++ source/host/Option.hh | 1 + 6 files changed, 166 insertions(+), 27 deletions(-) diff --git a/source/abstract/File.cc b/source/abstract/File.cc index 173ab636..270096ec 100644 --- a/source/abstract/File.cc +++ b/source/abstract/File.cc @@ -4,17 +4,21 @@ namespace Abstract::File { -u8 *Load(const char *path, size_t &size) { +std::filesystem::path Path(const char *path) { char filepath[256]; if (path[0] == '/') { path++; } - snprintf(filepath, sizeof(filepath), "./%s", path); - std::ifstream file(filepath, std::ios::binary); + snprintf(filepath, sizeof(filepath), "%s", path); + return std::filesystem::path(filepath); +} + +u8 *Load(const std::filesystem::path &path, size_t &size) { + std::ifstream file(path, std::ios::binary); if (!file) { - PANIC("File with provided path %s was not loaded correctly!", path); + PANIC("File with provided path %s was not loaded correctly!", path.c_str()); } file.seekg(0, std::ios::end); @@ -27,6 +31,10 @@ u8 *Load(const char *path, size_t &size) { return buffer; } +u8 *Load(const char *path, size_t &size) { + return Load(Path(path), size); +} + void Append(const char *path, const char *data, size_t size) { std::ofstream stream; stream.open(path, std::ios::app | std::ios::binary); diff --git a/source/abstract/File.hh b/source/abstract/File.hh index 1e891a39..9cba11d2 100644 --- a/source/abstract/File.hh +++ b/source/abstract/File.hh @@ -2,8 +2,12 @@ #include +#include + namespace Abstract::File { +[[nodiscard]] std::filesystem::path Path(const char *path); +[[nodiscard]] u8 *Load(const std::filesystem::path &path, size_t &size); [[nodiscard]] u8 *Load(const char *path, size_t &size); void Append(const char *path, const char *data, size_t size); int Remove(const char *path); diff --git a/source/host/KReplaySystem.cc b/source/host/KReplaySystem.cc index a49446a8..b3753fa7 100644 --- a/source/host/KReplaySystem.cc +++ b/source/host/KReplaySystem.cc @@ -11,17 +11,11 @@ /// @brief Initializes the system. void KReplaySystem::init() { - ASSERT(m_currentGhostFileName); - ASSERT(m_currentRawGhost); - ASSERT(m_currentGhost); - auto *sceneCreator = new Host::SceneCreatorDynamic; m_sceneMgr = new EGG::SceneManager(sceneCreator); System::RaceConfig::RegisterInitCallback(OnInit, nullptr); Abstract::File::Remove("results.txt"); - - m_sceneMgr->changeScene(0); } /// @brief Executes a frame. @@ -33,11 +27,27 @@ void KReplaySystem::calc() { /// @details A run consists of replaying a ghost. /// @return Whether the run was successful or not. bool KReplaySystem::run() { - while (!calcEnd()) { - calc(); + bool success = true; + + while (true) { + if (m_ghostArgs.size() == 0) { + break; + } + + std::filesystem::path nextPath = m_ghostArgs.front(); + m_ghostArgs.pop(); + + if (std::filesystem::is_directory(nextPath)) { + success &= runDirectory(nextPath); + } else if (std::filesystem::is_regular_file(nextPath)) { + success &= runGhost(nextPath); + } else { + WARN("Not a valid path: %s", nextPath.string().c_str()); + continue; + } } - return success(); + return success; } /// @brief Parses non-generic command line options. @@ -60,19 +70,30 @@ void KReplaySystem::parseOptions(int argc, char **argv) { case Host::EOption::Ghost: { ASSERT(i + 1 < argc); - m_currentGhostFileName = argv[++i]; - m_currentRawGhost = Abstract::File::Load(m_currentGhostFileName, m_currentRawGhostSize); - - if (m_currentRawGhostSize < System::RKG_HEADER_SIZE || - m_currentRawGhostSize > sizeof(System::RawGhostFile)) { - PANIC("File cannot be a ghost! Check the file size."); + // Every argument (until we see another flag) should be a ghost filename or directory + while (++i < argc) { + // Decrement i if it represents some other flag + if (Host::Option::CheckFlag(argv[i])) { + --i; + break; + } + + std::filesystem::path filepath = Abstract::File::Path(argv[i]); + + if (std::filesystem::is_directory(filepath) || + std::filesystem::is_regular_file(filepath)) { + m_ghostArgs.push(filepath); + } else { + WARN("Unable to find %s. Skipping...", filepath.c_str()); + } + } + } break; + case Host::EOption::Progress: { + if (i + 1 >= argc) { + PANIC("Expected progress interval argument!"); } - // Creating the raw ghost file validates it - System::RawGhostFile file = System::RawGhostFile(m_currentRawGhost); - - m_currentGhost = new System::GhostFile(file); - ASSERT(m_currentGhost); + m_progressInterval = strtoul(argv[++i], nullptr, 0); } break; case Host::EOption::Invalid: default: @@ -96,8 +117,8 @@ void KReplaySystem::DestroyInstance() { } KReplaySystem::KReplaySystem() - : m_currentGhostFileName(nullptr), m_currentGhost(nullptr), m_currentRawGhost(nullptr), - m_currentRawGhostSize(0) {} + : m_progressInterval(0), m_replaysPlayed(0), m_replaysSynced(0), m_currentGhost(nullptr), + m_currentRawGhost(nullptr), m_currentRawGhostSize(0) {} KReplaySystem::~KReplaySystem() { if (s_instance) { @@ -130,11 +151,97 @@ bool KReplaySystem::calcEnd() const { /// @brief Reports failure to file. /// @param msg The message to report. void KReplaySystem::reportFail(const std::string &msg) const { - std::string report(m_currentGhostFileName); + std::string report(m_currentGhostPath.string()); report += "\n" + std::string(msg); Abstract::File::Append("results.txt", report.c_str(), report.size()); } +bool KReplaySystem::runDirectory(const std::filesystem::path &dirPath) { + bool success = true; + + for (const auto &dir_entry : std::filesystem::recursive_directory_iterator(dirPath)) { + const auto &entry_path = dir_entry.path(); + + // Skip directories + if (std::filesystem::is_regular_file(entry_path)) { + success &= runGhost(entry_path); + } + } + + REPORT("Progress: %llu/%llu replays synced (Error rate: %.2f%)", m_replaysSynced, + m_replaysPlayed, + static_cast(m_replaysPlayed - m_replaysSynced) / m_replaysPlayed * 100); + + return success; +} + +bool KReplaySystem::runGhost(const std::filesystem::path &ghostPath) { + if (!ghostPath.has_extension()) { + return true; + } + + if (ghostPath.extension() != ".rkg") { + WARN("Skipping %s", ghostPath.string().c_str()); + return true; + } + + loadGhost(ghostPath); + + // Has the root scene been created? + if (!m_sceneMgr->currentScene()) { + m_sceneMgr->changeScene(0); + } else { + m_sceneMgr->createScene(2, m_sceneMgr->currentScene()); + } + + while (!calcEnd()) { + calc(); + } + + bool isSuccess = success(); + + if (!isSuccess) { + WARN("DESYNC! Ghost path: %s", ghostPath.string().c_str()); + } + + m_sceneMgr->currentScene()->heap()->enableAllocation(); + m_sceneMgr->destroyScene(m_sceneMgr->currentScene()); + + delete m_currentRawGhost; + m_currentRawGhost = nullptr; + m_currentRawGhostSize = 0; + + delete m_currentGhost; + m_currentGhost = nullptr; + + ++m_replaysPlayed; + + if (isSuccess) { + ++m_replaysSynced; + } + + if (m_progressInterval > 0 && m_replaysPlayed % m_progressInterval == 0) { + REPORT("Progress: %llu/%llu replays synced (Error rate: %.2f%)", m_replaysSynced, + m_replaysPlayed, + static_cast(m_replaysPlayed - m_replaysSynced) / m_replaysPlayed * 100); + } + + return isSuccess; +} + +void KReplaySystem::loadGhost(const std::filesystem::path &ghostPath) { + m_currentGhostPath = ghostPath; + m_currentRawGhost = Abstract::File::Load(m_currentGhostPath, m_currentRawGhostSize); + if (m_currentRawGhostSize < System::RKG_HEADER_SIZE || + m_currentRawGhostSize > System::RKG_UNCOMPRESSED_INPUT_DATA_SECTION_SIZE) { + PANIC("File cannot be a ghost! Check the file size. %llu", m_currentRawGhostSize); + } + + // Creating the raw ghost file validates it + System::RawGhostFile file = System::RawGhostFile(m_currentRawGhost); + m_currentGhost = new System::GhostFile(file); +} + /// @brief Determines whether the simulation was a success or not. /// @return Whether the simulation was a success or not. bool KReplaySystem::success() const { diff --git a/source/host/KReplaySystem.hh b/source/host/KReplaySystem.hh index 29a4fadf..d81d1bdd 100644 --- a/source/host/KReplaySystem.hh +++ b/source/host/KReplaySystem.hh @@ -4,6 +4,9 @@ #include +#include +#include + /// @brief Kinoko system designed to execute replays. class KReplaySystem : public KSystem { public: @@ -30,6 +33,10 @@ private: bool calcEnd() const; void reportFail(const std::string &msg) const; + bool runDirectory(const std::filesystem::path &dirPath); + bool runGhost(const std::filesystem::path &ghostPath); + void loadGhost(const std::filesystem::path &ghostPath); + bool success() const; s32 getDesyncingTimerIdx() const; DesyncingTimerPair getDesyncingTimer(s32 i) const; @@ -38,6 +45,11 @@ private: EGG::SceneManager *m_sceneMgr; + std::queue m_ghostArgs; + size_t m_progressInterval; + size_t m_replaysPlayed; + size_t m_replaysSynced; + std::filesystem::path m_currentGhostPath; const char *m_currentGhostFileName; const System::GhostFile *m_currentGhost; const u8 *m_currentRawGhost; diff --git a/source/host/Option.cc b/source/host/Option.cc index 9d1cd9c5..7449e3d9 100644 --- a/source/host/Option.cc +++ b/source/host/Option.cc @@ -30,6 +30,10 @@ std::optional CheckFlag(const char *arg) { return EOption::TargetFrame; } + if (strcmp(verbose_arg, "progress") == 0) { + return EOption::Progress; + } + return EOption::Invalid; } else { switch (arg[1]) { @@ -45,6 +49,9 @@ std::optional CheckFlag(const char *arg) { case 'F': case 'f': return EOption::TargetFrame; + case 'P': + case 'p': + return EOption::Progress; default: return EOption::Invalid; } diff --git a/source/host/Option.hh b/source/host/Option.hh index 74076082..63028875 100644 --- a/source/host/Option.hh +++ b/source/host/Option.hh @@ -13,6 +13,7 @@ enum class EOption { Ghost, KRKG, TargetFrame, + Progress, }; namespace Option {