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