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