Skip to content

Commit

Permalink
Add support for reading 200cc
Browse files Browse the repository at this point in the history
  • Loading branch information
vabold committed Dec 15, 2024
1 parent ed8483f commit 954dddc
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 4 deletions.
61 changes: 60 additions & 1 deletion source/game/system/GhostFile.cc
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,67 @@ T RawGhostFile::parseAt(size_t offset) const {
return parse<T>(*reinterpret_cast<const T *>(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<size_t>::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<const CTGPGhostFooter *>(pFooter);
if (parse<u32>(footer->magic) != CTGPGhostFooter::CTGP_FOOTER_SIGNATURE) {
return nullptr;
}

return footer;
}

bool RawGhostFile::compressed(const u8 *rkg) {
return ((*(rkg + 0xC) >> 3) & 1) == 1;
}

CTGPMetadata::CTGPMetadata() : isCTGP(false), is200cc(false) {}

void CTGPMetadata::read(const CTGPGhostFooter *data) {
if (!data) {
isCTGP = false;
return;
}

u8 *streamPtr = const_cast<u8 *>(reinterpret_cast<const u8 *>(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);
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) {
is200cc = tasCategory >= 4 && tasCategory <= 6;
} else {
is200cc = category >= 4 && category <= 7;
}
}

} // namespace System
42 changes: 41 additions & 1 deletion source/game/system/GhostFile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,44 @@ 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;

struct __attribute__((packed)) CTGPGhostFooter {
u8 trackSHA1[20];
u64 ghostDBPlayerID;
f32 trueFinishTime;
u8 _20[0x27 - 0x20];
u8 regionLetter;
u8 _28[0x38 - 0x28];
std::array<f32, 10> 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; // 4-6 determine if a ghost is 200cc if category is 3
u8 category : 4; // 3 determines TAS, 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 isCTGP;
bool is200cc;
};

/// @brief The binary data of a ghost saved to a file.
/// Offset | Size | Description
///------------- | ------------- | -------------
Expand Down Expand Up @@ -68,8 +106,10 @@ public:
template <typename T>
[[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];
};
Expand Down
5 changes: 5 additions & 0 deletions source/game/system/RaceConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ void RaceConfig::initControllers() {
/// @brief Initializes the ghost.
/// @details This is normally scoped within RaceConfig::Scenario, but Kinoko doesn't support menus.
void RaceConfig::initGhost() {
// 200cc isn't supported yet, so we simply check that it's not present
if (m_ctgpMetadata.isCTGP) {
ASSERT(!m_ctgpMetadata.is200cc);
}

GhostFile ghost(m_ghost);

m_raceScenario.course = ghost.course();
Expand Down
4 changes: 3 additions & 1 deletion source/game/system/RaceConfig.hh
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ public:
return m_raceScenario;
}

void setGhost(const u8 *rkg) {
void setGhost(const u8 *rkg, size_t size = std::numeric_limits<size_t>::max()) {
m_ghost = rkg;
m_ctgpMetadata.read(RawGhostFile::FindCTGPFooter(rkg, size));
}

static void RegisterInitCallback(const InitCallback &callback, void *arg);
Expand All @@ -73,6 +74,7 @@ private:

Scenario m_raceScenario;
RawGhostFile m_ghost;
CTGPMetadata m_ctgpMetadata;

static RaceConfig *s_instance; ///< @addr{0x809BD728}
static InitCallback s_onInitCallback;
Expand Down
2 changes: 1 addition & 1 deletion source/test/TestDirector.cc
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,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;
Expand Down

0 comments on commit 954dddc

Please sign in to comment.