From c841dd17e3d5a50fd2de5f99cf67655fbdb07049 Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 1 Nov 2024 15:41:55 -0400 Subject: [PATCH] Add support for reading 200cc --- source/game/system/GhostFile.cc | 61 +++++++++++++++++++++++++++++++- source/game/system/GhostFile.hh | 46 +++++++++++++++++++++++- source/game/system/RaceConfig.cc | 6 ++++ source/game/system/RaceConfig.hh | 7 +++- source/test/TestDirector.cc | 2 +- 5 files changed, 118 insertions(+), 4 deletions(-) diff --git a/source/game/system/GhostFile.cc b/source/game/system/GhostFile.cc index 134f3dc7..2d14f7e7 100644 --- a/source/game/system/GhostFile.cc +++ b/source/game/system/GhostFile.cc @@ -181,8 +181,67 @@ T RawGhostFile::parseAt(size_t offset) const { return parse(*reinterpret_cast(m_buffer + offset)); } -bool RawGhostFile::compressed(const u8 *rkg) const { +const CTGPGhostFooter *RawGhostFile::FindCTGPFooter(const u8 *rkg, size_t size) { + // If there is no ghost, there is no footer + if (!rkg) { + return nullptr; + } + + // We don't know the size - assume there is no CTGP footer + if (size == std::numeric_limits::max()) { + return nullptr; + } + + // All CTGP ghosts are compressed + if (!compressed(rkg)) { + return nullptr; + } + + const u8 *pFooter = (rkg + size) - sizeof(CTGPGhostFooter); + const CTGPGhostFooter *footer = reinterpret_cast(pFooter); + if (parse(footer->magic) != CTGPGhostFooter::CTGP_FOOTER_SIGNATURE) { + return nullptr; + } + + return footer; +} + +bool RawGhostFile::compressed(const u8 *rkg) { return ((*(rkg + 0xC) >> 3) & 1) == 1; } +CTGPMetadata::CTGPMetadata() : m_isCTGP(false), m_is200cc(false) {} + +void CTGPMetadata::read(const CTGPGhostFooter *data) { + if (!data) { + m_isCTGP = false; + return; + } + + u8 *streamPtr = const_cast(reinterpret_cast(data)); + EGG::RamStream stream(streamPtr, sizeof(CTGPGhostFooter)); + read(stream); +} + +void CTGPMetadata::read(EGG::RamStream &stream) { + // Check if it's CTGP + // This is always expected to be the case if we reach this point + stream.jump(offsetof(CTGPGhostFooter, magic)); + ASSERT(stream.read_u32() == CTGPGhostFooter::CTGP_FOOTER_SIGNATURE); + m_isCTGP = true; + + // Check if it's 200cc + // We cannot jump directly into a bitfield, so we jump to the member behind it and add 1 + stream.jump(offsetof(CTGPGhostFooter, ghostActionFlags) + 1); + u8 categoryInfo = stream.read_u8(); + u8 tasCategory = categoryInfo >> 4 & 0xf; + u8 category = categoryInfo & 0xf; + + if (category == 3) { + m_is200cc = tasCategory >= 4 && tasCategory <= 6; + } else { + m_is200cc = category >= 4 && category <= 7; + } +} + } // namespace System diff --git a/source/game/system/GhostFile.hh b/source/game/system/GhostFile.hh index 2e92d56a..7d37495e 100644 --- a/source/game/system/GhostFile.hh +++ b/source/game/system/GhostFile.hh @@ -20,6 +20,48 @@ static constexpr size_t RKG_USER_DATA_SIZE = 0x14; static constexpr size_t RKG_MII_DATA_OFFSET = 0x3C; static constexpr size_t RKG_MII_DATA_SIZE = 0x4A; +/// @brief C++ implementation of the CTGP ghost footer, as documented by MrBean and Chadderz. +/// @details This footer was not created with C/C++ interfacing in mind. +/// In practice, the goal for the footer was to generate load instructions with negative offsets, +/// relative to the end of the file. +struct __attribute__((packed)) CTGPGhostFooter { + u8 trackSHA1[20]; + u64 ghostDBPlayerID; + f32 trueFinishTime; + u8 _20[0x27 - 0x20]; + u8 regionLetter; + u8 _28[0x38 - 0x28]; + std::array trueLapTimes; ///< Indexed via 10 - i, such that i is one-indexed. + u64 rtcTimeEnd; + u64 rtcTimeStart; + u64 rtcTimePaused; + u8 ghostConsoleFlags; + u8 shroom3Lap; + u8 shroom2Lap; + u8 shroom1Lap; + u8 shortcutDefinitionVer; + u8 ghostActionFlags; + u8 tasCategory : 4; ///< Values 4-6 determine if a ghost is 200cc, if the category is 3. + u8 category : 4; ///< Value 3 determines TAS. Values 4-7 determine if a ghost is 200cc. + u8 footerVersion; + u32 footerLen; + u32 magic; + u32 checksum; + + static constexpr u32 CTGP_FOOTER_SIGNATURE = 0x434b4744; // CKGD +}; +STATIC_ASSERT(sizeof(CTGPGhostFooter) == 0x8c); + +struct CTGPMetadata { + CTGPMetadata(); + + void read(const CTGPGhostFooter *data); + void read(EGG::RamStream &stream); + + bool m_isCTGP; + bool m_is200cc; +}; + /// @brief The binary data of a ghost saved to a file. /// Offset | Size | Description ///------------- | ------------- | ------------- @@ -68,8 +110,10 @@ public: template [[nodiscard]] T parseAt(size_t offset) const; + [[nodiscard]] static const CTGPGhostFooter *FindCTGPFooter(const u8 *rkg, size_t size); + private: - [[nodiscard]] bool compressed(const u8 *rkg) const; + [[nodiscard]] static bool compressed(const u8 *rkg); u8 m_buffer[RKG_UNCOMPRESSED_FILE_SIZE]; }; diff --git a/source/game/system/RaceConfig.cc b/source/game/system/RaceConfig.cc index 26297bfa..4026ef9a 100644 --- a/source/game/system/RaceConfig.cc +++ b/source/game/system/RaceConfig.cc @@ -44,7 +44,13 @@ void RaceConfig::initControllers() { /// @addr{0x8052EEF0} /// @brief Initializes the ghost. /// @details This is normally scoped within RaceConfig::Scenario, but Kinoko doesn't support menus. +/// @todo Implement full support for 200cc. void RaceConfig::initGhost() { + // 200cc isn't supported yet, so we simply check that it's not present + if (m_ctgpMetadata.m_isCTGP) { + ASSERT(!m_ctgpMetadata.m_is200cc); + } + GhostFile ghost(m_ghost); m_raceScenario.course = ghost.course(); diff --git a/source/game/system/RaceConfig.hh b/source/game/system/RaceConfig.hh index 2f66f365..39e6bf8f 100644 --- a/source/game/system/RaceConfig.hh +++ b/source/game/system/RaceConfig.hh @@ -57,8 +57,12 @@ public: return m_raceScenario; } - void setGhost(const u8 *rkg) { + /// @brief Sets the ghost, and attempts to gather CTGP metadata if available. + /// @param rkg Pointer to the ghost buffer, before decompression. + /// @param size The optional size of the ghost buffer. Required for CTGP parsing. + void setGhost(const u8 *rkg, size_t size = std::numeric_limits::max()) { m_ghost = rkg; + m_ctgpMetadata.read(RawGhostFile::FindCTGPFooter(rkg, size)); } static void RegisterInitCallback(const InitCallback &callback, void *arg); @@ -73,6 +77,7 @@ private: Scenario m_raceScenario; RawGhostFile m_ghost; + CTGPMetadata m_ctgpMetadata; static RaceConfig *s_instance; ///< @addr{0x809BD728} static InitCallback s_onInitCallback; diff --git a/source/test/TestDirector.cc b/source/test/TestDirector.cc index 23741f5a..066aa110 100644 --- a/source/test/TestDirector.cc +++ b/source/test/TestDirector.cc @@ -256,7 +256,7 @@ void TestDirector::OnInit(System::RaceConfig *config, void * /* arg */) { size_t size; const auto *testDirector = Host::KSystem::Instance().testDirector(); u8 *rkg = Abstract::File::Load(testDirector->testCase().rkgPath.data(), size); - config->setGhost(rkg); + config->setGhost(rkg, size); delete[] rkg; config->raceScenario().players[0].type = System::RaceConfig::Player::Type::Ghost;