Skip to content

Commit

Permalink
Add a replay system
Browse files Browse the repository at this point in the history
  • Loading branch information
vabold committed Dec 28, 2024
1 parent a023a1f commit 2d9eb6d
Show file tree
Hide file tree
Showing 10 changed files with 334 additions and 1 deletion.
9 changes: 9 additions & 0 deletions source/game/system/GhostFile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions source/game/system/GhostFile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
53 changes: 53 additions & 0 deletions source/game/system/RaceManager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,55 @@ void RaceManager::Player::calc() {
m_raceCompletion = std::min(m_raceCompletion, static_cast<f32>(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 &currentLap = m_lapTimers[lap - 1];
const Timer &previousLap = m_lapTimers[lap - 2];
if (!currentLap.valid || !previousLap.valid) {
split.min = std::numeric_limits<u16>::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;
}
Expand All @@ -200,6 +249,10 @@ s8 RaceManager::Player::jugemId() const {
return m_jugemId;
}

const std::array<Timer, 3> &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];
Expand Down
3 changes: 3 additions & 0 deletions source/game/system/RaceManager.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<Timer, 3> &lapTimers() const;
[[nodiscard]] const Timer &lapTimer(size_t idx) const;
[[nodiscard]] const Timer &raceTimer() const;
[[nodiscard]] const KPad *inputs() const;
Expand Down
8 changes: 8 additions & 0 deletions source/game/system/TimerManager.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
209 changes: 209 additions & 0 deletions source/host/KReplaySystem.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#include "KReplaySystem.hh"

#include "host/Option.hh"
#include "host/SceneCreatorDynamic.hh"

#include <abstract/File.hh>

#include <game/system/RaceManager.hh>

/// @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<Host::EOption> 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<KReplaySystem *>(s_instance);
}

void KReplaySystem::DestroyInstance() {
ASSERT(s_instance);
auto *instance = s_instance;
s_instance = nullptr;
delete instance;
}

KReplaySystem *KReplaySystem::Instance() {
return static_cast<KReplaySystem *>(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<const System::Timer &, const System::Timer &> 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<const System::Timer &, const System::Timer &>(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<const System::Timer &, const System::Timer &>(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;
}
40 changes: 40 additions & 0 deletions source/host/KReplaySystem.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include "host/KSystem.hh"

#include <egg/core/SceneManager.hh>

#include <game/system/RaceConfig.hh>

/// @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<const System::Timer &, const System::Timer &> 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;
};
7 changes: 7 additions & 0 deletions source/host/Option.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ std::optional<EOption> CheckFlag(const char *arg) {
return EOption::Suite;
}

if (strcmp(verbose_arg, "ghost") == 0) {
return EOption::Ghost;
}

return EOption::Invalid;
} else {
switch (arg[1]) {
Expand All @@ -31,6 +35,9 @@ std::optional<EOption> CheckFlag(const char *arg) {
case 'S':
case 's':
return EOption::Suite;
case 'G':
case 'g':
return EOption::Ghost;
default:
return EOption::Invalid;
}
Expand Down
Loading

0 comments on commit 2d9eb6d

Please sign in to comment.