A reimplementation of Mario Kart Wii's physics engine in C++
Loading...
Searching...
No Matches
KTestSystem.cc
1#include "KTestSystem.hh"
2
3#include "host/SceneCreatorDynamic.hh"
4
5#include <egg/core/Heap.hh>
6
7#include <game/kart/KartObjectManager.hh>
8#include <game/system/RaceManager.hh>
9
10#include <abstract/File.hh>
11
12namespace Kinoko {
13
14// We use an unscoped enum to avoid static_casting in all usecases
15// This is defined in the source due to its lack of scoping
16enum Changelog {
17 Initial = 1,
18 AddedExtVel = 2,
19 AddedIntVel = 3,
20 AddedSpeed = 4,
21 AddedRotation = 5,
22 AddedCheckpoints = 6,
23};
24
25struct TestHeader {
26 u32 signature;
27 u16 byteOrderMark;
28 u16 frameCount;
29 u16 versionMajor;
30 u16 versionMinor;
31 u32 dataOffset;
32};
33
36 auto *sceneCreator = EGG::egg_new<Host::SceneCreatorDynamic>();
37 m_sceneMgr = EGG::egg_new<EGG::SceneManager>(sceneCreator);
38
39 System::RaceConfig::RegisterInitCallback(OnInit, nullptr);
40 Abstract::File::Remove("results.txt");
41
42 if (m_testMode == Host::EOption::Suite) {
43 initSuite();
44 }
45
47 m_sceneMgr->changeScene(0);
48}
49
53 constexpr u32 TEST_HEADER_SIGNATURE = 0x54535448; // TSTH
54 constexpr u32 TEST_FOOTER_SIGNATURE = 0x54535446; // TSTF
55 constexpr u16 SUITE_MAJOR_VER = 1;
56 constexpr u16 SUITE_MAX_MINOR_VER = 0;
57
58 u16 numTestCases = m_stream.read_u16();
59 u16 testMajorVer = m_stream.read_u16();
60 u16 testMinorVer = m_stream.read_u16();
61
62 if (testMajorVer != SUITE_MAJOR_VER || testMinorVer > SUITE_MAX_MINOR_VER) {
63 PANIC("Version not supported! Provided file is %d.%d while Kinoko supports up to %d.%d",
64 testMajorVer, testMinorVer, SUITE_MAJOR_VER, SUITE_MAX_MINOR_VER);
65 }
66
67 for (u16 i = 0; i < numTestCases; ++i) {
68 // Validate alignment
69 if (m_stream.read_u32() != TEST_HEADER_SIGNATURE) {
70 PANIC("Invalid binary data for test case!");
71 }
72
73 u16 totalSize = m_stream.read_u16();
74 TestCase testCase;
75
76 u16 nameLen = m_stream.read_u16();
77 testCase.name = m_stream.read_string();
78 if (nameLen != testCase.name.size() + 1) {
79 PANIC("Test case name length mismatch!");
80 }
81
82 u16 rkgPathLen = m_stream.read_u16();
83 testCase.rkgPath = m_stream.read_string();
84 if (rkgPathLen != testCase.rkgPath.size() + 1) {
85 PANIC("Test case RKG Path length mismatch!");
86 }
87
88 u16 krkgPathLen = m_stream.read_u16();
89 testCase.krkgPath = m_stream.read_string();
90 if (krkgPathLen != testCase.krkgPath.size() + 1) {
91 PANIC("Test case KRKG Path length mismatch!");
92 }
93
94 testCase.targetFrame = m_stream.read_u16();
95
96 // Validate alignment
97 if (m_stream.read_u32() != TEST_FOOTER_SIGNATURE) {
98 PANIC("Invalid binary data for test case!");
99 }
100
101 if (totalSize != sizeof(u16) * 4 + nameLen + rkgPathLen + krkgPathLen) {
102 PANIC("Unexpected bytes in test case");
103 }
104
105 m_testCases.push(testCase);
106 }
107}
108
111 m_sceneMgr->calc();
112}
113
118 bool success = true;
119
120 while (true) {
121 success &= runTest();
122
123 if (!popTestCase()) {
124 break;
125 }
126
127 // TODO: Use a system heap! We currently have a dependency on the scene heap
128 m_sceneMgr->destroyScene(m_sceneMgr->currentScene());
130 m_sceneMgr->createScene(2, m_sceneMgr->currentScene());
131 }
132
133 return success;
134}
135
140void KTestSystem::parseOptions(int argc, char **argv) {
141 if (argc < 2) {
142 PANIC("Expected suite/ghost/krkg argument!");
143 }
144
145 std::optional<char *> rkgPath;
146 std::optional<char *> krkgPath;
147 std::optional<u16> target;
148
149 for (int i = 0; i < argc; ++i) {
150 std::optional<Host::EOption> flag = Host::Option::CheckFlag(argv[i]);
151 if (!flag || *flag == Host::EOption::Invalid) {
152 WARN("Expected a flag! Got: %s", argv[i]);
153 continue;
154 }
155
156 switch (*flag) {
157 case Host::EOption::Suite: {
158 if (m_testMode != Host::EOption::Invalid) {
159 PANIC("Mode was already set!");
160 }
161
162 m_testMode = Host::EOption::Suite;
163
164 ASSERT(i + 1 < argc);
165
166 size_t size;
167 u8 *data = Abstract::File::Load(argv[++i], size);
168
169 if (size == 0) {
170 PANIC("Failed to load suite data!");
171 }
172
173 m_stream = EGG::RamStream(data, size);
174 m_stream.setEndian(std::endian::big);
175
176 } break;
177 case Host::EOption::Ghost:
178 if (m_testMode != Host::EOption::Invalid && m_testMode != Host::EOption::Ghost) {
179 PANIC("Mode was already set!");
180 }
181
182 m_testMode = Host::EOption::Ghost;
183 ASSERT(i + 1 < argc);
184 rkgPath = argv[++i];
185
186 break;
187 case Host::EOption::KRKG:
188 if (m_testMode != Host::EOption::Invalid && m_testMode != Host::EOption::Ghost) {
189 PANIC("Mode was already set!");
190 }
191
192 m_testMode = Host::EOption::Ghost;
193 ASSERT(i + 1 < argc);
194 krkgPath = argv[++i];
195
196 break;
197 case Host::EOption::TargetFrame:
198 ASSERT(i + 1 < argc);
199 {
200 if (strlen(argv[++i]) > 5) {
201 PANIC("Target has too many digits");
202 }
203 target = atoi(argv[i]);
204 if (target < 0 || target > std::numeric_limits<u16>::max()) {
205 PANIC("Target is out of bounds (expected 0-65535), got %d\n", *target);
206 }
207 }
208
209 break;
210 case Host::EOption::Invalid:
211 default:
212 PANIC("Invalid flag!");
213 break;
214 }
215 }
216
217 if (target && m_testMode != Host::EOption::Ghost) {
218 PANIC("'--framecount' is only supported in a single ghost test");
219 }
220
221 if (m_testMode == Host::EOption::Ghost) {
222 if (!rkgPath) {
223 PANIC("Missing ghost argument!");
224 }
225
226 if (!krkgPath) {
227 PANIC("Missing KRKG argument!");
228 }
229
230 if (!target) {
231 target = 0;
232 }
233
234 m_testCases.emplace(*rkgPath, *rkgPath, *krkgPath, *target);
235 }
236}
237
238KTestSystem *KTestSystem::CreateInstance() {
239 ASSERT(!s_instance);
240 s_instance = EGG::egg_new<KTestSystem>();
241 return static_cast<KTestSystem *>(s_instance);
242}
243
244void KTestSystem::DestroyInstance() {
245 ASSERT(s_instance);
246 auto *instance = s_instance;
247 s_instance = nullptr;
248 EGG::egg_delete(instance);
249}
250
251KTestSystem::KTestSystem() : m_testMode(Host::EOption::Invalid) {}
252
253KTestSystem::~KTestSystem() {
254 if (s_instance) {
255 s_instance = nullptr;
256 WARN("KTestSystem instance not explicitly handled!");
257 }
258}
259
262 constexpr u32 KRKG_SIGNATURE = 0x4b524b47; // KRKG
263
264 size_t size;
265 u8 *krkg = Abstract::File::Load(getCurrentTestCase().krkgPath.data(), size);
266 m_stream = EGG::RamStream(krkg, static_cast<u32>(size));
267 m_currentFrame = -1;
268 m_sync = true;
269
270 // Initialize endianness for the RAM stream
271 u16 mark = reinterpret_cast<TestHeader *>(krkg)->byteOrderMark;
272 std::endian endian = parse<u16>(mark) == 0xfeff ? std::endian::big : std::endian::little;
273 m_stream.setEndian(endian);
274
275 ASSERT(m_stream.read_u32() == KRKG_SIGNATURE);
276 m_stream.skip(2);
277 m_frameCount = m_stream.read_u16();
278 m_versionMajor = m_stream.read_u16();
279 m_versionMinor = m_stream.read_u16();
280
281 ASSERT(m_stream.read_u32() == m_stream.index());
282
283 // If we're in Ghost mode instead of Suite mode and framecount not specified, then target the
284 // total framecount of the KRKG.
285 if (m_testMode == Host::EOption::Ghost) {
286 ASSERT(m_testCases.size() == 1);
287 auto &front = m_testCases.front();
288 if (front.targetFrame == 0) {
289 front.targetFrame = m_frameCount;
290 }
291
292 front.targetFrame = std::min(front.targetFrame, m_frameCount);
293 }
294}
295
299 ASSERT(m_testCases.size() > 0);
300 m_testCases.pop();
301 EGG::egg_free(m_stream.data());
302
303 return !m_testCases.empty();
304}
305
309 ++m_currentFrame;
310
311 // Check if we're out of frames
312 u16 targetFrame = getCurrentTestCase().targetFrame;
313 ASSERT(targetFrame <= m_frameCount);
314 if (m_currentFrame > targetFrame) {
315 REPORT("Test Case Passed: %s [%d / %d]", getCurrentTestCase().name.c_str(), targetFrame,
316 m_frameCount);
317 return false;
318 }
319
320 // Test the current frame
322 return m_sync;
323}
324
328 EGG::Vector3f pos;
329 EGG::Quatf fullRot;
330 EGG::Vector3f extVel;
331 EGG::Vector3f intVel;
332 f32 speed = 0.0f;
333 f32 acceleration = 0.0f;
334 f32 softSpeedLimit = 0.0f;
335 EGG::Quatf mainRot = EGG::Quatf::ident; // Initialize to avoid maybe-uninitialized warning
336 EGG::Vector3f angVel2;
337 f32 raceCompletion = 0.0f;
338 u16 checkpointId = 0;
339 u8 jugemId = 0;
340
341 pos.read(m_stream);
342 fullRot.read(m_stream);
343
344 if (m_versionMinor >= Changelog::AddedExtVel) {
345 extVel.read(m_stream);
346 }
347
348 if (m_versionMinor >= Changelog::AddedIntVel) {
349 intVel.read(m_stream);
350 }
351
352 if (m_versionMinor >= Changelog::AddedSpeed) {
353 speed = m_stream.read_f32();
354 acceleration = m_stream.read_f32();
355 softSpeedLimit = m_stream.read_f32();
356 }
357
358 if (m_versionMinor >= Changelog::AddedRotation) {
359 mainRot.read(m_stream);
360 angVel2.read(m_stream);
361 }
362
363 if (m_versionMinor >= Changelog::AddedCheckpoints) {
364 raceCompletion = m_stream.read_f32();
365 checkpointId = m_stream.read_u16();
366 jugemId = m_stream.read_u8();
367 m_stream.skip(1);
368 }
369
370 TestData data;
371 data.pos = pos;
372 data.fullRot = fullRot;
373 data.extVel = extVel;
374 data.intVel = intVel;
375 data.speed = speed;
376 data.acceleration = acceleration;
377 data.softSpeedLimit = softSpeedLimit;
378 data.mainRot = mainRot;
379 data.angVel2 = angVel2;
380 data.raceCompletion = raceCompletion;
381 data.checkpointId = checkpointId;
382 data.jugemId = jugemId;
383 return data;
384}
385
389 auto *object = Kart::KartObjectManager::Instance()->object(0);
390 const auto &pos = object->pos();
391 const auto &fullRot = object->fullRot();
392 const auto &extVel = object->extVel();
393 const auto &intVel = object->intVel();
394 f32 speed = object->speed();
395 f32 acceleration = object->acceleration();
396 f32 softSpeedLimit = object->softSpeedLimit();
397 const auto &mainRot = object->mainRot();
398 const auto &angVel2 = object->angVel2();
399
400 const auto &player = System::RaceManager::Instance()->player();
401 f32 raceCompletion = player.raceCompletion();
402 u16 checkpointId = player.checkpointId();
403 u8 jugemId = player.jugemId();
404
405 switch (m_versionMinor) {
406 case Changelog::AddedCheckpoints:
407 checkDesync(data.raceCompletion, raceCompletion, "raceCompletion");
408 checkDesync(data.checkpointId, checkpointId, "checkpointId");
409 checkDesync(data.jugemId, jugemId, "jugemId");
410 [[fallthrough]];
411 case Changelog::AddedRotation:
412 checkDesync(data.mainRot, mainRot, "mainRot");
413 checkDesync(data.angVel2, angVel2, "angVel2");
414 [[fallthrough]];
415 case Changelog::AddedSpeed:
416 checkDesync(data.speed, speed, "speed");
417 checkDesync(data.acceleration, acceleration, "acceleration");
418 checkDesync(data.softSpeedLimit, softSpeedLimit, "softSpeedLimit");
419 [[fallthrough]];
420 case Changelog::AddedIntVel:
421 checkDesync(data.intVel, intVel, "intVel");
422 [[fallthrough]];
423 case Changelog::AddedExtVel:
424 checkDesync(data.extVel, extVel, "extVel");
425 [[fallthrough]];
426 default:
427 checkDesync(data.pos, pos, "pos");
428 checkDesync(data.fullRot, fullRot, "fullRot");
429 }
430}
431
436 while (calcTest()) {
437 calc();
438 }
439
440 // TODO: Use a system heap! std::string relies on heap allocation
441 // The heap is destroyed after this and there is no further allocation, so it's not re-disabled
442 m_sceneMgr->currentScene()->heap()->enableAllocation();
444 return m_sync;
445}
446
450 std::string outStr(getCurrentTestCase().name.data());
451 outStr += "\n" + std::string(m_sync ? "1" : "0") + "\n";
452 outStr += std::to_string(getCurrentTestCase().targetFrame) + "\n";
453 outStr += std::to_string(m_frameCount) + "\n";
454 Abstract::File::Append("results.txt", outStr.c_str(), outStr.size());
455}
456
461 ASSERT(!m_testCases.empty());
462 return m_testCases.front();
463}
464
468void KTestSystem::OnInit(System::RaceConfig *config, void * /* arg */) {
469 size_t size;
470 u8 *rkg = Abstract::File::Load(Instance()->getCurrentTestCase().rkgPath.data(), size);
471 config->setGhost(rkg);
472 EGG::egg_free(rkg);
473
474 config->raceScenario().players[0].type = System::RaceConfig::Player::Type::Ghost;
475}
476
477} // namespace Kinoko
A stream of data stored in memory.
Definition Stream.hh:81
Kinoko system designed to execute tests.
bool popTestCase()
Pops the current test case and frees the KRKG buffer.
Host::EOption m_testMode
Differentiates between test suite and ghost+krkg.
bool run() override
Executes a run.
TestData findCurrentFrameEntry()
Finds the test data of the current frame.
void parseOptions(int argc, char **argv) override
Parses non-generic command line options.
void startNextTestCase()
Starts the next test case.
void writeTestOutput() const
Writes details about the current test to file.
bool calcTest()
Checks one frame in the test.
const TestCase & getCurrentTestCase() const
Gets the current test case.
void testFrame(const TestData &data)
Tests the frame against the provided test data.
bool runTest()
Runs a single test case, and ends when the test is finished or when a desync is found.
static void OnInit(System::RaceConfig *config, void *arg)
Initializes the race configuration as needed for test cases.
void calc() override
Executes a frame.
void init() override
Initializes the system.
Initializes the player with parameters specified in the provided ghost file.
Definition RaceConfig.hh:23
Represents the host application.
Definition HeapCommon.hh:11
A quaternion, used to represent 3D rotation.
Definition Quat.hh:12
A 3D float vector.
Definition Vector.hh:107
void read(Stream &stream)
Initializes a Vector3f by reading 12 bytes from the stream.
Definition Vector.hh:365