From 6c376097d07a745864dddac1feb88151df331875 Mon Sep 17 00:00:00 2001 From: Aiden Date: Wed, 25 Dec 2024 08:00:17 -0500 Subject: [PATCH] Add a replay system --- source/game/system/GhostFile.cc | 9 ++ source/game/system/GhostFile.hh | 2 + source/game/system/RaceManager.cc | 53 ++++++++ source/game/system/RaceManager.hh | 3 + source/game/system/TimerManager.hh | 8 ++ source/host/KReplaySystem.cc | 209 +++++++++++++++++++++++++++++ source/host/KReplaySystem.hh | 40 ++++++ source/host/Option.cc | 7 + source/host/Option.hh | 1 + source/host/main.cc | 3 +- 10 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 source/host/KReplaySystem.cc create mode 100644 source/host/KReplaySystem.hh diff --git a/source/game/system/GhostFile.cc b/source/game/system/GhostFile.cc index 134f3dc7..b2d38a02 100644 --- a/source/game/system/GhostFile.cc +++ b/source/game/system/GhostFile.cc @@ -58,6 +58,15 @@ void GhostFile::read(EGG::RamStream &stream) { stream.read(m_miiData.data(), RKG_MII_DATA_SIZE); } +const Timer &GhostFile::lapTimer(size_t i) const { + ASSERT(i < m_lapTimes.size()); + return m_lapTimes[i]; +} + +const Timer &GhostFile::raceTimer() const { + return m_raceTime; +} + Character GhostFile::character() const { return m_character; } diff --git a/source/game/system/GhostFile.hh b/source/game/system/GhostFile.hh index a1ca1e36..e82e70b3 100644 --- a/source/game/system/GhostFile.hh +++ b/source/game/system/GhostFile.hh @@ -85,6 +85,8 @@ public: void read(EGG::RamStream &stream); ///< Organizes binary data into members. See RawGhostFile. /// @beginGetters + [[nodiscard]] const Timer &lapTimer(size_t i) const; + [[nodiscard]] const Timer &raceTimer() const; [[nodiscard]] Character character() const; [[nodiscard]] Vehicle vehicle() const; [[nodiscard]] Course course() const; diff --git a/source/game/system/RaceManager.cc b/source/game/system/RaceManager.cc index efb9e111..1cca414b 100644 --- a/source/game/system/RaceManager.cc +++ b/source/game/system/RaceManager.cc @@ -188,6 +188,55 @@ void RaceManager::Player::calc() { m_raceCompletion = std::min(m_raceCompletion, static_cast(m_currentLap) + 0.99999f); } +/// @brief Gets the lap split, which is the difference between the given lap and the previous one. +/// @param lap One-indexed lap. +/// @return The split timer. +Timer RaceManager::Player::getLapSplit(size_t lap) const { + ASSERT(lap <= m_lapTimers.size()); + + if (lap < 2) { + return m_lapTimers[0]; + } + + Timer split; + const Timer ¤tLap = m_lapTimers[lap - 1]; + const Timer &previousLap = m_lapTimers[lap - 2]; + if (!currentLap.valid || !previousLap.valid) { + split.min = std::numeric_limits::max(); + split.sec = 0; + split.mil = 0; + split.valid = false; + return split; + } + + s16 min = 0; + s8 sec = 0; + s16 ms = currentLap.mil - previousLap.mil; + if (ms < 0) { + sec = -1; + ms += 1000; + } + + sec += currentLap.sec - previousLap.sec; + if (sec < 0) { + min = -1; + sec += 60; + } + + min += currentLap.min - previousLap.min; + if (min < 0) { + min = 0; + sec = 0; + ms = 0; + } + + split.min = min; + split.sec = sec; + split.mil = ms; + split.valid = true; + return split; +} + u16 RaceManager::Player::checkpointId() const { return m_checkpointId; } @@ -200,6 +249,10 @@ s8 RaceManager::Player::jugemId() const { return m_jugemId; } +const std::array &RaceManager::Player::lapTimers() const { + return m_lapTimers; +} + const Timer &RaceManager::Player::lapTimer(size_t idx) const { ASSERT(idx < m_lapTimers.size()); return m_lapTimers[idx]; diff --git a/source/game/system/RaceManager.hh b/source/game/system/RaceManager.hh index 1391ca09..4512febd 100644 --- a/source/game/system/RaceManager.hh +++ b/source/game/system/RaceManager.hh @@ -25,10 +25,13 @@ public: void init(); void calc(); + [[nodiscard]] Timer getLapSplit(size_t idx) const; + /// @beginGetters [[nodiscard]] u16 checkpointId() const; [[nodiscard]] f32 raceCompletion() const; [[nodiscard]] s8 jugemId() const; + [[nodiscard]] const std::array &lapTimers() const; [[nodiscard]] const Timer &lapTimer(size_t idx) const; [[nodiscard]] const Timer &raceTimer() const; [[nodiscard]] const KPad *inputs() const; diff --git a/source/game/system/TimerManager.hh b/source/game/system/TimerManager.hh index 5883f42a..043c85bb 100644 --- a/source/game/system/TimerManager.hh +++ b/source/game/system/TimerManager.hh @@ -10,6 +10,14 @@ struct Timer { Timer(u32 data); ~Timer(); + bool operator==(const Timer &rhs) const { + return min == rhs.min && sec == rhs.sec && mil == rhs.mil && valid == rhs.valid; + } + + bool operator!=(const Timer &rhs) const { + return !(*this == rhs); + } + u16 min; u8 sec; u16 mil; ///< @todo We will likely want to expand this to a float for more precise finish times. diff --git a/source/host/KReplaySystem.cc b/source/host/KReplaySystem.cc new file mode 100644 index 00000000..d034f629 --- /dev/null +++ b/source/host/KReplaySystem.cc @@ -0,0 +1,209 @@ +#include "KReplaySystem.hh" + +#include "host/Option.hh" +#include "host/SceneCreatorDynamic.hh" + +#include + +#include + +/// @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. +void KReplaySystem::calc() { + m_sceneMgr->calc(); +} + +/// @brief Executes a run. +/// @details A run consists of replaying a ghost. +/// @return Whether the run was successful or not. +bool KReplaySystem::run() { + while (!calcEnd()) { + calc(); + } + + return success(); +} + +/// @brief Parses non-generic command line options. +/// @details The only currently accepted option is the ghost flag. +/// @param argc The number of arguments. +/// @param argv The arguments. +void KReplaySystem::parseOptions(int argc, char **argv) { + if (argc < 2) { + PANIC("Expected ghost argument!"); + } + + for (int i = 0; i < argc; ++i) { + std::optional flag = Host::Option::CheckFlag(argv[i]); + if (!flag || *flag == Host::EOption::Invalid) { + WARN("Expected a flag! Got: %s", argv[i]); + continue; + } + + switch (*flag) { + 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 > System::RKG_UNCOMPRESSED_INPUT_DATA_SECTION_SIZE) { + PANIC("File cannot be a ghost! Check the file size."); + } + + // Creating the raw ghost file validates it + System::RawGhostFile file = System::RawGhostFile(m_currentRawGhost); + + m_currentGhost = new System::GhostFile(file); + ASSERT(m_currentGhost); + } break; + case Host::EOption::Invalid: + default: + PANIC("Invalid flag!"); + break; + } + } +} + +KReplaySystem *KReplaySystem::CreateInstance() { + ASSERT(!s_instance); + s_instance = new KReplaySystem; + return static_cast(s_instance); +} + +void KReplaySystem::DestroyInstance() { + ASSERT(s_instance); + auto *instance = s_instance; + s_instance = nullptr; + delete instance; +} + +KReplaySystem *KReplaySystem::Instance() { + return static_cast(s_instance); +} + +KReplaySystem::KReplaySystem() + : m_currentGhostFileName(nullptr), m_currentGhost(nullptr), m_currentRawGhost(nullptr), + m_currentRawGhostSize(0) {} + +KReplaySystem::~KReplaySystem() { + if (s_instance) { + s_instance = nullptr; + WARN("KReplaySystem instance not explicitly handled!"); + } + + delete m_sceneMgr; + delete m_currentGhost; + delete m_currentRawGhost; +} + +bool KReplaySystem::calcEnd() const { + constexpr u16 MAX_MINUTE_COUNT = 10; + + const auto *raceManager = System::RaceManager::Instance(); + if (raceManager->stage() == System::RaceManager::Stage::FinishGlobal) { + return true; + } + + if (raceManager->timerManager().currentTimer().min >= MAX_MINUTE_COUNT) { + return true; + } + + return false; +} + +void KReplaySystem::reportFail(const char *msg) const { + std::string report(m_currentGhostFileName); + report += "\n" + std::string(msg); + Abstract::File::Append("results.txt", report.c_str(), report.size()); +} + +bool KReplaySystem::success() const { + const auto *raceManager = System::RaceManager::Instance(); + if (raceManager->stage() != System::RaceManager::Stage::FinishGlobal) { + reportFail("Race didn't finish"); + return false; + } + + s32 desyncingTimerIdx = getDesyncingTimerIdx(); + if (desyncingTimerIdx != -1) { + char msgBuffer[128]; + + const auto [correct, incorrect] = getDesyncingTimer(desyncingTimerIdx); + if (desyncingTimerIdx == 0) { + snprintf(msgBuffer, sizeof(msgBuffer), + "Final timer desync! Expected [%d:%d:%d], got [%d:%d:%d]", correct.min, + correct.sec, correct.mil, incorrect.min, incorrect.sec, incorrect.mil); + } else { + snprintf(msgBuffer, sizeof(msgBuffer), + "Lap %d timer desync! Expected [%d:%d:%d], got [%d:%d:%d]", desyncingTimerIdx, + correct.min, correct.sec, correct.mil, incorrect.min, incorrect.sec, + incorrect.mil); + } + reportFail(msgBuffer); + return false; + } + + return true; +} + +/// @brief Finds the desyncing timer index, if one exists. +/// @return -1 if there's no desync, 0 if the final timer desyncs, and 1+ if a lap timer desyncs. +s32 KReplaySystem::getDesyncingTimerIdx() const { + const auto &player = System::RaceManager::Instance()->player(); + if (m_currentGhost->raceTimer() != player.raceTimer()) { + return 0; + } + + for (size_t i = 0; i < 3; ++i) { + if (m_currentGhost->lapTimer(i) != player.getLapSplit(i + 1)) { + return i + 1; + } + } + + return -1; +} + +/// @brief Gets the desyncing timer according to the index. +/// @param i Index to the desyncing timer. Cannot be -1. +/// @return The pair of timers. The first is the correct one, and the second is the incorrect one. +std::pair KReplaySystem::getDesyncingTimer( + s32 i) const { + auto cond = i <=> 0; + ASSERT(cond != std::strong_ordering::less); + + if (cond == std::strong_ordering::equal) { + const auto &correct = m_currentGhost->raceTimer(); + const auto &incorrect = System::RaceManager::Instance()->player().raceTimer(); + ASSERT(correct != incorrect); + return std::pair(correct, incorrect); + } else if (cond == std::strong_ordering::greater) { + const auto &correct = m_currentGhost->lapTimer(i - 1); + const auto &incorrect = System::RaceManager::Instance()->player().lapTimer(i - 1); + ASSERT(correct != incorrect); + return std::pair(correct, incorrect); + } + + // This is unreachable + return std::pair(System::Timer(), System::Timer()); +} + +void KReplaySystem::OnInit(System::RaceConfig *config, void * /* arg */) { + config->setGhost(Instance()->m_currentRawGhost); + config->raceScenario().players[0].type = System::RaceConfig::Player::Type::Ghost; +} diff --git a/source/host/KReplaySystem.hh b/source/host/KReplaySystem.hh new file mode 100644 index 00000000..43c34217 --- /dev/null +++ b/source/host/KReplaySystem.hh @@ -0,0 +1,40 @@ +#include "host/KSystem.hh" + +#include + +#include + +/// @brief Kinoko system designed to execute replays. +class KReplaySystem : public KSystem { +public: + void init() override; + void calc() override; + bool run() override; + void parseOptions(int argc, char **argv) override; + + static KReplaySystem *CreateInstance(); + static void DestroyInstance(); + static KReplaySystem *Instance(); + +private: + KReplaySystem(); + KReplaySystem(const KReplaySystem &) = delete; + KReplaySystem(KReplaySystem &&) = delete; + ~KReplaySystem() override; + + bool calcEnd() const; + void reportFail(const char *msg) const; + + bool success() const; + s32 getDesyncingTimerIdx() const; + std::pair getDesyncingTimer(s32 i) const; + + static void OnInit(System::RaceConfig *config, void *arg); + + EGG::SceneManager *m_sceneMgr; + + const char *m_currentGhostFileName; + const System::GhostFile *m_currentGhost; + const u8 *m_currentRawGhost; + size_t m_currentRawGhostSize; +}; diff --git a/source/host/Option.cc b/source/host/Option.cc index a41b9061..9829988d 100644 --- a/source/host/Option.cc +++ b/source/host/Option.cc @@ -22,6 +22,10 @@ std::optional CheckFlag(const char *arg) { return EOption::Suite; } + if (strcmp(verbose_arg, "ghost") == 0) { + return EOption::Ghost; + } + return EOption::Invalid; } else { switch (arg[1]) { @@ -31,6 +35,9 @@ std::optional CheckFlag(const char *arg) { case 'S': case 's': return EOption::Suite; + case 'G': + case 'g': + return EOption::Ghost; default: return EOption::Invalid; } diff --git a/source/host/Option.hh b/source/host/Option.hh index f73883d2..f3f6dd65 100644 --- a/source/host/Option.hh +++ b/source/host/Option.hh @@ -10,6 +10,7 @@ enum class EOption { Invalid = -1, Mode, Suite, + Ghost, }; namespace Option { diff --git a/source/host/main.cc b/source/host/main.cc index c2552a79..e14398a7 100644 --- a/source/host/main.cc +++ b/source/host/main.cc @@ -1,8 +1,8 @@ +#include "host/KReplaySystem.hh" #include "host/KTestSystem.hh" #include "host/Option.hh" #include -#include #if defined(__arm64__) || defined(__aarch64__) static void FlushDenormalsToZero() { @@ -47,6 +47,7 @@ int main(int argc, char **argv) { // TODO: Allow memory initialization before any other static initializers const std::unordered_map> modeMap = { {"test", []() -> KSystem * { return KTestSystem::CreateInstance(); }}, + {"replay", []() -> KSystem * { return KReplaySystem::CreateInstance(); }}, }; if (argc < 3) {