Skip to content

Commit 055bfb8

Browse files
committed
Add a replay system
1 parent 5d988f3 commit 055bfb8

10 files changed

+334
-1
lines changed

source/game/system/GhostFile.cc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ void GhostFile::read(EGG::RamStream &stream) {
5858
stream.read(m_miiData.data(), RKG_MII_DATA_SIZE);
5959
}
6060

61+
const Timer &GhostFile::lapTimer(size_t i) const {
62+
ASSERT(i < m_lapTimes.size());
63+
return m_lapTimes[i];
64+
}
65+
66+
const Timer &GhostFile::raceTimer() const {
67+
return m_raceTime;
68+
}
69+
6170
Character GhostFile::character() const {
6271
return m_character;
6372
}

source/game/system/GhostFile.hh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ public:
8585
void read(EGG::RamStream &stream); ///< Organizes binary data into members. See RawGhostFile.
8686

8787
/// @beginGetters
88+
[[nodiscard]] const Timer &lapTimer(size_t i) const;
89+
[[nodiscard]] const Timer &raceTimer() const;
8890
[[nodiscard]] Character character() const;
8991
[[nodiscard]] Vehicle vehicle() const;
9092
[[nodiscard]] Course course() const;

source/game/system/RaceManager.cc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,55 @@ void RaceManager::Player::calc() {
188188
m_raceCompletion = std::min(m_raceCompletion, static_cast<f32>(m_currentLap) + 0.99999f);
189189
}
190190

191+
/// @brief Gets the lap split, which is the difference between the given lap and the previous one.
192+
/// @param lap One-indexed lap.
193+
/// @return The split timer.
194+
Timer RaceManager::Player::getLapSplit(size_t lap) const {
195+
ASSERT(lap <= m_lapTimers.size());
196+
197+
if (lap < 2) {
198+
return m_lapTimers[0];
199+
}
200+
201+
Timer split;
202+
const Timer &currentLap = m_lapTimers[lap - 1];
203+
const Timer &previousLap = m_lapTimers[lap - 2];
204+
if (!currentLap.valid || !previousLap.valid) {
205+
split.min = std::numeric_limits<u16>::max();
206+
split.sec = 0;
207+
split.mil = 0;
208+
split.valid = false;
209+
return split;
210+
}
211+
212+
s16 min = 0;
213+
s8 sec = 0;
214+
s16 ms = currentLap.mil - previousLap.mil;
215+
if (ms < 0) {
216+
sec = -1;
217+
ms += 1000;
218+
}
219+
220+
sec += currentLap.sec - previousLap.sec;
221+
if (sec < 0) {
222+
min = -1;
223+
sec += 60;
224+
}
225+
226+
min += currentLap.min - previousLap.min;
227+
if (min < 0) {
228+
min = 0;
229+
sec = 0;
230+
ms = 0;
231+
}
232+
233+
split.min = min;
234+
split.sec = sec;
235+
split.mil = ms;
236+
split.valid = true;
237+
return split;
238+
}
239+
191240
u16 RaceManager::Player::checkpointId() const {
192241
return m_checkpointId;
193242
}
@@ -200,6 +249,10 @@ s8 RaceManager::Player::jugemId() const {
200249
return m_jugemId;
201250
}
202251

252+
const std::array<Timer, 3> &RaceManager::Player::lapTimers() const {
253+
return m_lapTimers;
254+
}
255+
203256
const Timer &RaceManager::Player::lapTimer(size_t idx) const {
204257
ASSERT(idx < m_lapTimers.size());
205258
return m_lapTimers[idx];

source/game/system/RaceManager.hh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ public:
2525
void init();
2626
void calc();
2727

28+
[[nodiscard]] Timer getLapSplit(size_t idx) const;
29+
2830
/// @beginGetters
2931
[[nodiscard]] u16 checkpointId() const;
3032
[[nodiscard]] f32 raceCompletion() const;
3133
[[nodiscard]] s8 jugemId() const;
34+
[[nodiscard]] const std::array<Timer, 3> &lapTimers() const;
3235
[[nodiscard]] const Timer &lapTimer(size_t idx) const;
3336
[[nodiscard]] const Timer &raceTimer() const;
3437
[[nodiscard]] const KPad *inputs() const;

source/game/system/TimerManager.hh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ struct Timer {
1010
Timer(u32 data);
1111
~Timer();
1212

13+
bool operator==(const Timer &rhs) const {
14+
return min == rhs.min && sec == rhs.sec && mil == rhs.mil && valid == rhs.valid;
15+
}
16+
17+
bool operator!=(const Timer &rhs) const {
18+
return !(*this == rhs);
19+
}
20+
1321
u16 min;
1422
u8 sec;
1523
u16 mil; ///< @todo We will likely want to expand this to a float for more precise finish times.

source/host/KReplaySystem.cc

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#include "KReplaySystem.hh"
2+
3+
#include "host/Option.hh"
4+
#include "host/SceneCreatorDynamic.hh"
5+
6+
#include <abstract/File.hh>
7+
8+
#include <game/system/RaceManager.hh>
9+
10+
/// @brief Initializes the system.
11+
void KReplaySystem::init() {
12+
ASSERT(m_currentGhostFileName);
13+
ASSERT(m_currentRawGhost);
14+
ASSERT(m_currentGhost);
15+
16+
auto *sceneCreator = new Host::SceneCreatorDynamic;
17+
m_sceneMgr = new EGG::SceneManager(sceneCreator);
18+
19+
System::RaceConfig::RegisterInitCallback(OnInit, nullptr);
20+
Abstract::File::Remove("results.txt");
21+
22+
m_sceneMgr->changeScene(0);
23+
}
24+
25+
/// @brief Executes a frame.
26+
void KReplaySystem::calc() {
27+
m_sceneMgr->calc();
28+
}
29+
30+
/// @brief Executes a run.
31+
/// @details A run consists of replaying a ghost.
32+
/// @return Whether the run was successful or not.
33+
bool KReplaySystem::run() {
34+
while (!calcEnd()) {
35+
calc();
36+
}
37+
38+
return success();
39+
}
40+
41+
/// @brief Parses non-generic command line options.
42+
/// @details The only currently accepted option is the ghost flag.
43+
/// @param argc The number of arguments.
44+
/// @param argv The arguments.
45+
void KReplaySystem::parseOptions(int argc, char **argv) {
46+
if (argc < 2) {
47+
PANIC("Expected ghost argument!");
48+
}
49+
50+
for (int i = 0; i < argc; ++i) {
51+
std::optional<Host::EOption> flag = Host::Option::CheckFlag(argv[i]);
52+
if (!flag || *flag == Host::EOption::Invalid) {
53+
WARN("Expected a flag! Got: %s", argv[i]);
54+
continue;
55+
}
56+
57+
switch (*flag) {
58+
case Host::EOption::Ghost: {
59+
ASSERT(i + 1 < argc);
60+
61+
m_currentGhostFileName = argv[++i];
62+
m_currentRawGhost = Abstract::File::Load(m_currentGhostFileName, m_currentRawGhostSize);
63+
64+
if (m_currentRawGhostSize < System::RKG_HEADER_SIZE ||
65+
m_currentRawGhostSize > System::RKG_UNCOMPRESSED_INPUT_DATA_SECTION_SIZE) {
66+
PANIC("File cannot be a ghost! Check the file size.");
67+
}
68+
69+
// Creating the raw ghost file validates it
70+
System::RawGhostFile file = System::RawGhostFile(m_currentRawGhost);
71+
72+
m_currentGhost = new System::GhostFile(file);
73+
ASSERT(m_currentGhost);
74+
} break;
75+
case Host::EOption::Invalid:
76+
default:
77+
PANIC("Invalid flag!");
78+
break;
79+
}
80+
}
81+
}
82+
83+
KReplaySystem *KReplaySystem::CreateInstance() {
84+
ASSERT(!s_instance);
85+
s_instance = new KReplaySystem;
86+
return static_cast<KReplaySystem *>(s_instance);
87+
}
88+
89+
void KReplaySystem::DestroyInstance() {
90+
ASSERT(s_instance);
91+
auto *instance = s_instance;
92+
s_instance = nullptr;
93+
delete instance;
94+
}
95+
96+
KReplaySystem *KReplaySystem::Instance() {
97+
return static_cast<KReplaySystem *>(s_instance);
98+
}
99+
100+
KReplaySystem::KReplaySystem()
101+
: m_currentGhostFileName(nullptr), m_currentGhost(nullptr), m_currentRawGhost(nullptr),
102+
m_currentRawGhostSize(0) {}
103+
104+
KReplaySystem::~KReplaySystem() {
105+
if (s_instance) {
106+
s_instance = nullptr;
107+
WARN("KReplaySystem instance not explicitly handled!");
108+
}
109+
110+
delete m_sceneMgr;
111+
delete m_currentGhost;
112+
delete m_currentRawGhost;
113+
}
114+
115+
bool KReplaySystem::calcEnd() const {
116+
constexpr u16 MAX_MINUTE_COUNT = 10;
117+
118+
const auto *raceManager = System::RaceManager::Instance();
119+
if (raceManager->stage() == System::RaceManager::Stage::FinishGlobal) {
120+
return true;
121+
}
122+
123+
if (raceManager->timerManager().currentTimer().min >= MAX_MINUTE_COUNT) {
124+
return true;
125+
}
126+
127+
return false;
128+
}
129+
130+
void KReplaySystem::reportFail(const char *msg) const {
131+
std::string report(m_currentGhostFileName);
132+
report += "\n" + std::string(msg);
133+
Abstract::File::Append("results.txt", report.c_str(), report.size());
134+
}
135+
136+
bool KReplaySystem::success() const {
137+
const auto *raceManager = System::RaceManager::Instance();
138+
if (raceManager->stage() != System::RaceManager::Stage::FinishGlobal) {
139+
reportFail("Race didn't finish");
140+
return false;
141+
}
142+
143+
s32 desyncingTimerIdx = getDesyncingTimerIdx();
144+
if (desyncingTimerIdx != -1) {
145+
char msgBuffer[128];
146+
147+
const auto [correct, incorrect] = getDesyncingTimer(desyncingTimerIdx);
148+
if (desyncingTimerIdx == 0) {
149+
snprintf(msgBuffer, sizeof(msgBuffer),
150+
"Final timer desync! Expected [%d:%d:%d], got [%d:%d:%d]", correct.min,
151+
correct.sec, correct.mil, incorrect.min, incorrect.sec, incorrect.mil);
152+
} else {
153+
snprintf(msgBuffer, sizeof(msgBuffer),
154+
"Lap %d timer desync! Expected [%d:%d:%d], got [%d:%d:%d]", desyncingTimerIdx,
155+
correct.min, correct.sec, correct.mil, incorrect.min, incorrect.sec,
156+
incorrect.mil);
157+
}
158+
reportFail(msgBuffer);
159+
return false;
160+
}
161+
162+
return true;
163+
}
164+
165+
/// @brief Finds the desyncing timer index, if one exists.
166+
/// @return -1 if there's no desync, 0 if the final timer desyncs, and 1+ if a lap timer desyncs.
167+
s32 KReplaySystem::getDesyncingTimerIdx() const {
168+
const auto &player = System::RaceManager::Instance()->player();
169+
if (m_currentGhost->raceTimer() != player.raceTimer()) {
170+
return 0;
171+
}
172+
173+
for (size_t i = 0; i < 3; ++i) {
174+
if (m_currentGhost->lapTimer(i) != player.getLapSplit(i + 1)) {
175+
return i + 1;
176+
}
177+
}
178+
179+
return -1;
180+
}
181+
182+
/// @brief Gets the desyncing timer according to the index.
183+
/// @param i Index to the desyncing timer. Cannot be -1.
184+
/// @return The pair of timers. The first is the correct one, and the second is the incorrect one.
185+
std::pair<const System::Timer &, const System::Timer &> KReplaySystem::getDesyncingTimer(
186+
s32 i) const {
187+
auto cond = i <=> 0;
188+
ASSERT(cond != std::strong_ordering::less);
189+
190+
if (cond == std::strong_ordering::equal) {
191+
const auto &correct = m_currentGhost->raceTimer();
192+
const auto &incorrect = System::RaceManager::Instance()->player().raceTimer();
193+
ASSERT(correct != incorrect);
194+
return std::pair<const System::Timer &, const System::Timer &>(correct, incorrect);
195+
} else if (cond == std::strong_ordering::greater) {
196+
const auto &correct = m_currentGhost->lapTimer(i - 1);
197+
const auto &incorrect = System::RaceManager::Instance()->player().lapTimer(i - 1);
198+
ASSERT(correct != incorrect);
199+
return std::pair<const System::Timer &, const System::Timer &>(correct, incorrect);
200+
}
201+
202+
// This is unreachable
203+
return std::pair(System::Timer(), System::Timer());
204+
}
205+
206+
void KReplaySystem::OnInit(System::RaceConfig *config, void * /* arg */) {
207+
config->setGhost(Instance()->m_currentRawGhost);
208+
config->raceScenario().players[0].type = System::RaceConfig::Player::Type::Ghost;
209+
}

source/host/KReplaySystem.hh

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#include "host/KSystem.hh"
2+
3+
#include <egg/core/SceneManager.hh>
4+
5+
#include <game/system/RaceConfig.hh>
6+
7+
/// @brief Kinoko system designed to execute replays.
8+
class KReplaySystem : public KSystem {
9+
public:
10+
void init() override;
11+
void calc() override;
12+
bool run() override;
13+
void parseOptions(int argc, char **argv) override;
14+
15+
static KReplaySystem *CreateInstance();
16+
static void DestroyInstance();
17+
static KReplaySystem *Instance();
18+
19+
private:
20+
KReplaySystem();
21+
KReplaySystem(const KReplaySystem &) = delete;
22+
KReplaySystem(KReplaySystem &&) = delete;
23+
~KReplaySystem() override;
24+
25+
bool calcEnd() const;
26+
void reportFail(const char *msg) const;
27+
28+
bool success() const;
29+
s32 getDesyncingTimerIdx() const;
30+
std::pair<const System::Timer &, const System::Timer &> getDesyncingTimer(s32 i) const;
31+
32+
static void OnInit(System::RaceConfig *config, void *arg);
33+
34+
EGG::SceneManager *m_sceneMgr;
35+
36+
const char *m_currentGhostFileName;
37+
const System::GhostFile *m_currentGhost;
38+
const u8 *m_currentRawGhost;
39+
size_t m_currentRawGhostSize;
40+
};

source/host/Option.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ std::optional<EOption> CheckFlag(const char *arg) {
2222
return EOption::Suite;
2323
}
2424

25+
if (strcmp(verbose_arg, "ghost") == 0) {
26+
return EOption::Ghost;
27+
}
28+
2529
return EOption::Invalid;
2630
} else {
2731
switch (arg[1]) {
@@ -31,6 +35,9 @@ std::optional<EOption> CheckFlag(const char *arg) {
3135
case 'S':
3236
case 's':
3337
return EOption::Suite;
38+
case 'G':
39+
case 'g':
40+
return EOption::Ghost;
3441
default:
3542
return EOption::Invalid;
3643
}

0 commit comments

Comments
 (0)