From 22a87c77f2184ab7f21493253866e40f156f1e28 Mon Sep 17 00:00:00 2001 From: Ishan Chawla Date: Wed, 30 Oct 2024 20:46:01 -0700 Subject: [PATCH] Add Checkpoints --- source/egg/math/Vector.cc | 6 + source/egg/math/Vector.hh | 4 +- source/game/system/CourseMap.cc | 22 ++- source/game/system/CourseMap.hh | 5 + source/game/system/map/MapdataCheckPoint.cc | 184 ++++++++++++++++++++ source/game/system/map/MapdataCheckPoint.hh | 111 ++++++++++++ 6 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 source/game/system/map/MapdataCheckPoint.cc create mode 100644 source/game/system/map/MapdataCheckPoint.hh diff --git a/source/egg/math/Vector.cc b/source/egg/math/Vector.cc index 6ead008a..f798c31e 100644 --- a/source/egg/math/Vector.cc +++ b/source/egg/math/Vector.cc @@ -36,6 +36,12 @@ f32 Vector2f::normalise() { return len; } +/// @brief Constructs a Vector2f by reading 8 bytes from the stream. +void Vector2f::read(Stream &stream) { + x = stream.read_f32(); + y = stream.read_f32(); +} + Vector3f::Vector3f(f32 x_, f32 y_, f32 z_) : x(x_), y(y_), z(z_) {} Vector3f::Vector3f() : x(0.0f), y(0.0f), z(0.0f) {} diff --git a/source/egg/math/Vector.hh b/source/egg/math/Vector.hh index 71a6a4e3..2c4752fc 100644 --- a/source/egg/math/Vector.hh +++ b/source/egg/math/Vector.hh @@ -36,7 +36,9 @@ struct Vector2f { [[nodiscard]] f32 dot(const Vector2f &rhs) const; [[nodiscard]] f32 dot() const; [[nodiscard]] f32 length() const; - [[nodiscard]] f32 normalise(); + f32 normalise(); + + void read(Stream &stream); f32 x; f32 y; diff --git a/source/game/system/CourseMap.cc b/source/game/system/CourseMap.cc index f835a263..5c82c661 100644 --- a/source/game/system/CourseMap.cc +++ b/source/game/system/CourseMap.cc @@ -1,6 +1,7 @@ #include "CourseMap.hh" #include "game/system/map/MapdataCannonPoint.hh" +#include "game/system/map/MapdataCheckPoint.hh" #include "game/system/map/MapdataFileAccessor.hh" #include "game/system/map/MapdataGeoObj.hh" #include "game/system/map/MapdataStageInfo.hh" @@ -17,11 +18,13 @@ void CourseMap::init() { new MapdataFileAccessor(reinterpret_cast(buffer)); constexpr u32 CANNON_POINT_SIGNATURE = 0x434e5054; + constexpr u32 CHECK_POINT_SIGNATURE = 0x434b5054; constexpr u32 GEO_OBJ_SIGNATURE = 0x474f424a; constexpr u32 START_POINT_SIGNATURE = 0x4b545054; constexpr u32 STAGE_INFO_SIGNATURE = 0x53544749; m_startPoint = parseStartPoint(START_POINT_SIGNATURE); + m_checkPoint = parseCheckPoint(CHECK_POINT_SIGNATURE); m_geoObj = parseGeoObj(GEO_OBJ_SIGNATURE); m_cannonPoint = parseCannonPoint(CANNON_POINT_SIGNATURE); m_stageInfo = parseStageInfo(STAGE_INFO_SIGNATURE); @@ -54,6 +57,18 @@ MapdataCannonPointAccessor *CourseMap::parseCannonPoint(u32 sectionName) { return accessor; } +/// @addr{0x80513640} +MapdataCheckPointAccessor *CourseMap::parseCheckPoint(u32 sectionName) { + const MapSectionHeader *sectionPtr = m_course->findSection(sectionName); + + MapdataCheckPointAccessor *accessor = nullptr; + if (sectionPtr) { + accessor = new MapdataCheckPointAccessor(sectionPtr); + } + + return accessor; +} + /// @addr{0x805134C8} MapdataGeoObjAccessor *CourseMap::parseGeoObj(u32 sectionName) { const MapSectionHeader *sectionPtr = m_course->findSection(sectionName); @@ -90,11 +105,16 @@ MapdataStartPointAccessor *CourseMap::parseStartPoint(u32 sectionName) { return accessor; } -/// @addr{0x80518AF8} +/// @addr{0x80518AE0} MapdataCannonPoint *CourseMap::getCannonPoint(u16 i) const { return m_cannonPoint && m_cannonPoint->size() != 0 ? m_cannonPoint->get(i) : nullptr; } +/// @addr{0x80515C24} +MapdataCheckPoint *CourseMap::getCheckPoint(u16 i) const { + return m_checkPoint && m_checkPoint->size() != 0 ? m_checkPoint->get(i) : nullptr; +} + /// @addr{0x80514148} MapdataGeoObj *CourseMap::getGeoObj(u16 i) const { return i < getGeoObjCount() ? m_geoObj->get(i) : nullptr; diff --git a/source/game/system/CourseMap.hh b/source/game/system/CourseMap.hh index a50de6ed..b34ee556 100644 --- a/source/game/system/CourseMap.hh +++ b/source/game/system/CourseMap.hh @@ -8,6 +8,8 @@ namespace System { class MapdataCannonPoint; class MapdataCannonPointAccessor; +class MapdataCheckPoint; +class MapdataCheckPointAccessor; class MapdataFileAccessor; class MapdataGeoObj; class MapdataGeoObjAccessor; @@ -23,12 +25,14 @@ class CourseMap : EGG::Disposer { public: void init(); [[nodiscard]] MapdataCannonPointAccessor *parseCannonPoint(u32 sectionName); + [[nodiscard]] MapdataCheckPointAccessor *parseCheckPoint(u32 sectionName); [[nodiscard]] MapdataGeoObjAccessor *parseGeoObj(u32 sectionName); [[nodiscard]] MapdataStageInfoAccessor *parseStageInfo(u32 sectionName); [[nodiscard]] MapdataStartPointAccessor *parseStartPoint(u32 sectionName); /// @beginGetters [[nodiscard]] MapdataCannonPoint *getCannonPoint(u16 i) const; + [[nodiscard]] MapdataCheckPoint *getCheckPoint(u16 i) const; [[nodiscard]] MapdataGeoObj *getGeoObj(u16 i) const; [[nodiscard]] MapdataStageInfo *getStageInfo() const; [[nodiscard]] MapdataStartPoint *getStartPoint(u16 i) const; @@ -53,6 +57,7 @@ private: MapdataStartPointAccessor *m_startPoint; MapdataGeoObjAccessor *m_geoObj; MapdataCannonPointAccessor *m_cannonPoint; + MapdataCheckPointAccessor *m_checkPoint; MapdataStageInfoAccessor *m_stageInfo; // TODO: Better names diff --git a/source/game/system/map/MapdataCheckPoint.cc b/source/game/system/map/MapdataCheckPoint.cc new file mode 100644 index 00000000..e0acb81f --- /dev/null +++ b/source/game/system/map/MapdataCheckPoint.cc @@ -0,0 +1,184 @@ +#include "MapdataCheckPoint.hh" + +#include + +namespace System { + +/// @addr{0x805154E4} +MapdataCheckPoint::MapdataCheckPoint(const SData *data) + : m_rawData(data), m_nextCount(0), m_prevCount(0) { + u8 *unsafeData = reinterpret_cast(const_cast(data)); + EGG::RamStream stream = EGG::RamStream(unsafeData, sizeof(SData)); + read(stream); + m_midpoint = EGG::Vector2f((left().x + right().x) / 2.0f, (left().y + right().y) / 2.0f); + m_dir = EGG::Vector2f(left().y - right().y, right().x - left().x); + m_dir.normalise(); +} + +void MapdataCheckPoint::read(EGG::Stream &stream) { + m_left.read(stream); + m_right.read(stream); + m_jugemIndex = stream.read_u8(); + m_type = stream.read_s8(); + m_prevPt = stream.read_u8(); + m_nextPt = stream.read_u8(); +} + +bool MapdataCheckPoint::isPlayerFlagged(s32 /* playerIdx */) const { + return m_flag; +} + +void MapdataCheckPoint::setPlayerFlags(s32 /* playerIdx */) { + m_flag = true; +} + +void MapdataCheckPoint::resetFlags() { + m_flag = false; +} + +const EGG::Vector2f &MapdataCheckPoint::left() const { + return m_left; +} + +const EGG::Vector2f &MapdataCheckPoint::right() const { + return m_right; +} + +u8 MapdataCheckPoint::jugemIndex() const { + return m_jugemIndex; +} + +s8 MapdataCheckPoint::type() const { + return m_type; +} + +bool MapdataCheckPoint::isNormalCheckpoint() const { + return m_type == NORMAL_CHECKPOINT; +} + +bool MapdataCheckPoint::isFinishLine() const { + return m_type == FINISH_LINE; +} + +u16 MapdataCheckPoint::nextCount() const { + return m_nextCount; +} + +u16 MapdataCheckPoint::prevCount() const { + return m_prevCount; +} + +u16 MapdataCheckPoint::id() const { + return m_id; +} + +MapdataCheckPoint *MapdataCheckPoint::prevPoint(s32 i) const { + return m_prevPoints[i]; +} + +MapdataCheckPoint *MapdataCheckPoint::nextPoint(s32 i) const { + return m_nextPoints[i].checkpoint; +} + +const LinkedCheckpoint &MapdataCheckPoint::nextLinked(s32 i) const { + return m_nextPoints[i]; +} + +/// @brief computes @ref m_prevKcpId for each checkpoint. +/// @details @ref isPlayerFlagged indicates that a checkpoint has been visited, to prevent infinite +/// recursion. +/// https://decomp.me/scratch/MtF18 +/// @addr{0x80515A6C} +void MapdataCheckPoint::linkPrevKcpIds(u8 prevKcpId) { + m_prevKcpId = isNormalCheckpoint() ? prevKcpId : m_type; + + setPlayerFlags(0); + for (size_t i = 0; i < nextCount(); i++) { + MapdataCheckPoint *next = nextPoint(i); + if (next->isPlayerFlagged(0)) { + continue; + } + next->linkPrevKcpIds(m_prevKcpId); + } +} + +/// @addr{0x80512064} +f32 MapdataCheckPointAccessor::calculateMeanTotalDistanceRecursive(u16 ckptId) { + f32 sumDist = 0.0f; + MapdataCheckPoint *ckpt = get(ckptId); + u16 n = ckpt->nextCount(); + + for (size_t i = 0; i < n; i++) { + const LinkedCheckpoint &linked = ckpt->nextLinked(i); + sumDist += linked.distance; + ckpt = ckpt->nextPoint(i); + + if (m_finishLineCheckpointId == ckpt->id()) { + continue; + } + sumDist += calculateMeanTotalDistanceRecursive(ckpt->id()); + } + + return sumDist / n; +} + +s8 MapdataCheckPointAccessor::lastKcpType() const { + return m_lastKcpType; +} + +/// @addr{80512370} +f32 MapdataCheckPointAccessor::calculateMeanTotalDistance() { + return calculateMeanTotalDistanceRecursive(m_finishLineCheckpointId); +} + +/// @brief find finish line and last key checkpoint indexes; also initCheckpointLinks for all +/// checkpoints +/// @addr{Inlined in 0x80515244} fake. not real. it's not in the base game. in the base +/// game, it's inlined into `init()`. i, kooshnoo, split it out because i wanted to. +// I'm not sure if this should be split; it feels like a separate function, +// but idk if i can somehow prove nintendo wrote it as a separate function, +// and i don't know if we care what nintendo extracted to a separate function and what they +// inlined. would the PR reviewer kindly please advise, thank you +void MapdataCheckPointAccessor::findFinishAndLastKcp() { + s8 lastKcpType = -1; + s16 finishLineCheckpointId = -1; + + for (size_t ckptId = 0; ckptId < size(); ckptId++) { + MapdataCheckPoint *checkpoint = get(ckptId); + checkpoint = get(ckptId); + + if (checkpoint->isFinishLine()) { + finishLineCheckpointId = ckptId; + } + + lastKcpType = std::max(lastKcpType, checkpoint->type()); + } + + m_lastKcpType = lastKcpType; + m_finishLineCheckpointId = finishLineCheckpointId; +} + +/// @addr{0x80515244} +void MapdataCheckPointAccessor::init() { + findFinishAndLastKcp(); + MapdataCheckPoint *finishLine = get(m_finishLineCheckpointId); + finishLine->linkPrevKcpIds(0); + // CourseMap::Instance()->clearSectorChecked(); //<- unnecessary, introduces UB maybe? + // // (CourseMap::Instance()->m_checkpoint is + // uninitialized) + // // would the PR reviewer kindly please + // advise, thank you + // m_meanTotalDistance = calculateMeanTotalDistance(); //<- unused +} + +MapdataCheckPointAccessor::MapdataCheckPointAccessor(const MapSectionHeader *header) + : MapdataAccessorBase(header) { + MapdataAccessorBase::init( + reinterpret_cast(m_sectionHeader + 1), + parse(m_sectionHeader->count)); + init(); +} + +MapdataCheckPointAccessor::~MapdataCheckPointAccessor() = default; + +} // namespace System diff --git a/source/game/system/map/MapdataCheckPoint.hh b/source/game/system/map/MapdataCheckPoint.hh new file mode 100644 index 00000000..719e6685 --- /dev/null +++ b/source/game/system/map/MapdataCheckPoint.hh @@ -0,0 +1,111 @@ +#pragma once + +#include + +#include + +namespace System { + +class MapdataCheckPoint; +struct LinkedCheckpoint { + MapdataCheckPoint *checkpoint; + EGG::Vector2f p0diff; + EGG::Vector2f p1diff; + f32 distance; +}; + +class MapdataCheckPointAccessor; +class MapdataCheckPoint { +public: + static constexpr s8 NORMAL_CHECKPOINT = -1; ///< only used for picking respawn position + static constexpr s8 FINISH_LINE = 0; ///< triggers a lap change; also the starting line + + struct SData { + EGG::Vector2f left; + EGG::Vector2f right; + u8 jugemIndex; + s8 lapCheck; + u8 prevPt; + u8 nextPt; + }; + + enum class SectorOccupancy { + InsideSector, ///< if player is inside the checkpoint quad + OutsideSector, ///< if player is not between the sides of the quad (may still be between + ///< this checkpoint and next); player is likely in a different checkpoint + ///< group + OutsideSector_BetweenSides, ///< if player is between the sides of the quad, but NOT between + ///< this checkpoint and next; player is likely in the same + ///< checkpoint group + }; + + MapdataCheckPoint(const SData *data); + void read(EGG::Stream &stream); + + bool isPlayerFlagged(s32 playerIdx) const; + void setPlayerFlags(s32 playerIdx); + void resetFlags(); + + /// @beginGetters + [[nodiscard]] const EGG::Vector2f &left() const; + [[nodiscard]] const EGG::Vector2f &right() const; + [[nodiscard]] u8 jugemIndex() const; + [[nodiscard]] s8 type() const; + [[nodiscard]] bool isNormalCheckpoint() const; + [[nodiscard]] bool isFinishLine() const; + [[nodiscard]] u16 nextCount() const; + [[nodiscard]] u16 prevCount() const; + [[nodiscard]] u16 id() const; + [[nodiscard]] MapdataCheckPoint *prevPoint(s32 i) const; + [[nodiscard]] MapdataCheckPoint *nextPoint(s32 i) const; + [[nodiscard]] const LinkedCheckpoint &nextLinked(s32 i) const; + /// @endGetters + + void linkPrevKcpIds(u8 prevKcpId); + +private: + const SData *m_rawData; + EGG::Vector2f m_left; + EGG::Vector2f m_right; + s8 m_jugemIndex; ///< index of respawn point associated with this checkpoint. players who die + ///< here will be respawned at this point. + /// either: + /// - a @ref `NORMAL_CHECKPOINT` (0) used to calulate respawns, + /// - a @ref `FINISH_LINE` (-1) which updates the lap count when crossed, or + /// - a "key checkpoint" (1-127) used to ensure racers travel around the entire + /// course before proceeding to the next lap. the type value represents the index, + /// i.e. racers must pass checkpoint with @ref `m_type` 1, then 2, then 3 etc.. + s8 m_type; + u8 m_prevPt; + u8 m_nextPt; + u16 m_nextCount; + u16 m_prevCount; + EGG::Vector2f m_midpoint; + EGG::Vector2f m_dir; + bool m_flag; ///< visited flag, for recursive fucntions. + u16 m_id; + u8 m_prevKcpId; ///< @unused + MapdataCheckPoint *m_prevPoints[6]; + LinkedCheckpoint m_nextPoints[6]; +}; + +class MapdataCheckPointAccessor + : public MapdataAccessorBase { +public: + MapdataCheckPointAccessor(const MapSectionHeader *header); + ~MapdataCheckPointAccessor() override; + + [[nodiscard]] s8 lastKcpType() const; + +private: + [[nodiscard]] f32 calculateMeanTotalDistanceRecursive(u16 ckptId); + [[nodiscard]] f32 calculateMeanTotalDistance(); + void findFinishAndLastKcp(); + void init(); + + s8 m_lastKcpType; + u16 m_finishLineCheckpointId; + f32 m_meanTotalDistance; ///< @unused +}; + +} // namespace System