Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() : m_isCTGP(false), m_is200cc(false) {}

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

u8 *streamPtr = const_cast<u8 *>(reinterpret_cast<const u8 *>(data));
EGG::RamStream stream(streamPtr, sizeof(CTGPGhostFooter));
Comment thread
vabold marked this conversation as resolved.
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
46 changes: 45 additions & 1 deletion source/game/system/GhostFile.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
vabold marked this conversation as resolved.
Comment thread
vabold marked this conversation as resolved.
u8 trackSHA1[20];
u64 ghostDBPlayerID;
f32 trueFinishTime;
u8 _20[0x27 - 0x20];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add @unused doxygen comment I guess?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an example of what that looks like?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f32 m_angVel0YFactor; ///< Scalar for damping angular velocity.
bool m_forceUpright;  ///< Specifies if we should return the vehicle to upwards orientation.
bool m_noGravity;     ///< @unused

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

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; ///< 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
///------------- | ------------- | -------------
Expand Down Expand Up @@ -68,8 +110,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
6 changes: 6 additions & 0 deletions source/game/system/RaceConfig.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
vabold marked this conversation as resolved.
if (m_ctgpMetadata.m_isCTGP) {
ASSERT(!m_ctgpMetadata.m_is200cc);
}

GhostFile ghost(m_ghost);

m_raceScenario.course = ghost.course();
Expand Down
7 changes: 6 additions & 1 deletion source/game/system/RaceConfig.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t>::max()) {
Comment thread
vabold marked this conversation as resolved.
m_ghost = rkg;
m_ctgpMetadata.read(RawGhostFile::FindCTGPFooter(rkg, size));
}

static void RegisterInitCallback(const InitCallback &callback, void *arg);
Expand All @@ -73,6 +77,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 @@ -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;
Expand Down