From f8d31ef7ec0577b37b1c1583d5e72cb24aea792f Mon Sep 17 00:00:00 2001 From: Phillip Stephens Date: Thu, 21 May 2026 12:17:57 -0700 Subject: [PATCH] Add Decal uncooking and HINT --- lib/CMakeLists.txt | 1 + lib/include/PrimeDep/FourCC.hpp | 4 + .../Resources/MetroidPrime/DecalData.hpp | 40 ++++- .../PrimeDep/Resources/MetroidPrime/Hints.hpp | 54 ++++++ .../Resources/MetroidPrime/DecalData.cpp | 57 ++++++- .../PrimeDep/Resources/MetroidPrime/Hints.cpp | 155 ++++++++++++++++++ uncooker/src/main.cpp | 2 + 7 files changed, 311 insertions(+), 2 deletions(-) create mode 100644 lib/include/PrimeDep/Resources/MetroidPrime/Hints.hpp create mode 100644 lib/src/PrimeDep/Resources/MetroidPrime/Hints.cpp diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index fb7139b..80f35bc 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -49,6 +49,7 @@ add_library( src/PrimeDep/Resources/MetroidPrime/SkinRules.cpp src/PrimeDep/Resources/MetroidPrime/StringTable.cpp src/PrimeDep/Resources/MetroidPrime/Texture.cpp + src/PrimeDep/Resources/MetroidPrime/Hints.cpp src/PrimeDep/texture_decode.cpp src/PrimeDep/WorldCommon/MetroidPrime/MappableObject.cpp src/PrimeDep/Particles/ParticleDataFactory.cpp diff --git a/lib/include/PrimeDep/FourCC.hpp b/lib/include/PrimeDep/FourCC.hpp index 98f1549..c765314 100644 --- a/lib/include/PrimeDep/FourCC.hpp +++ b/lib/include/PrimeDep/FourCC.hpp @@ -49,6 +49,10 @@ public: static constexpr FourCC kInvalidFourCC = {}; +static constexpr FourCC FOURCCByChar(const char a, const char b, const char c, const char d) { + const char fcc[4] = {a, b, c, d}; + return FourCC(fcc); +} } // namespace axdl::primedep #define FOURCC(fcc) axdl::primedep::FourCC(SBIG(fcc)) diff --git a/lib/include/PrimeDep/Resources/MetroidPrime/DecalData.hpp b/lib/include/PrimeDep/Resources/MetroidPrime/DecalData.hpp index 478b15f..8bc09dd 100644 --- a/lib/include/PrimeDep/Resources/MetroidPrime/DecalData.hpp +++ b/lib/include/PrimeDep/Resources/MetroidPrime/DecalData.hpp @@ -1,17 +1,55 @@ #pragma once #include "PrimeDep/IResource.hpp" + +#include "PrimeDep/Particles/ParticleProperty.hpp" + namespace axdl::primedep::MetroidPrime { -class DecalData final : public TypedResource('DPSC', ".dpsm", ".dpsm.dpsc", DESCRIPTION("Decal Particle System")) { + +class DecalData final : public TypedResource('DPSC', ".dpsm", ".dpsm.dpsc", DESCRIPTION("Decal Particle System")), + particles::IPropertyContainer { public: + template + struct QuadDescription { + QuadDescription(IPropertyContainer* parent) + : m_lifetime(FOURCCByChar(Num, 'L', 'F', 'T'), std::format("Quad{}Lifetime", Num), parent) + , m_size(FOURCCByChar(Num, 'S', 'Z', 'E'), std::format("Quad{}Size", Num), parent) + , m_rotation(FOURCCByChar(Num, 'R', 'O', 'T'), std::format("Quad{}Rotation", Num) + "Rotation", parent) + , m_offset(FOURCCByChar(Num, 'O', 'F', 'F'), std::format("Quad{}Offset", Num) + "Offset", parent) + , m_color(FOURCCByChar(Num, 'C', 'L', 'R'), std::format("Quad{}Color", Num), parent) + , m_texture(FOURCCByChar(Num, 'T', 'E', 'X'), std::format("Quad{}Texture", Num), parent) + , m_additiveBlend(false, FOURCCByChar(Num, 'A', 'D', 'D'), std::format("Quad{}AdditiveBlend", Num), parent) {} + particles::IntElementProperty m_lifetime; + particles::RealElementProperty m_size; + particles::RealElementProperty m_rotation; + particles::VectorElementProperty m_offset; + particles::ColorElementProperty m_color; + particles::UVElementProperty m_texture; + particles::BoolElementProperty m_additiveBlend; + }; + DecalData(); DecalData(const char* ptr, std::size_t size); static std::shared_ptr loadCooked(const char* ptr, std::size_t size); + bool writeUncooked(const std::string_view path) const override; + static bool canIngest(const nlohmann::ordered_json& metadata) { return metadata["ResourceType"] == ResourceType().toString(); } static std::shared_ptr ingest(const nlohmann::ordered_json& metadata, std::string_view path) { return nullptr; } + +private: + QuadDescription<'1'> m_quad1; + QuadDescription<'2'> m_quad2; + particles::AssetID32BigElementProperty m_model; + particles::IntElementProperty m_lifetime; + particles::VectorElementProperty m_modelOffset; + particles::VectorElementProperty m_modelRotation; + particles::VectorElementProperty m_modelScale; + particles::ColorElementProperty m_modelColor; + particles::BoolElementProperty m_modelAdditiveBlend; + particles::BoolElementProperty m_modelRotationGlobal; }; } // namespace axdl::primedep::MetroidPrime \ No newline at end of file diff --git a/lib/include/PrimeDep/Resources/MetroidPrime/Hints.hpp b/lib/include/PrimeDep/Resources/MetroidPrime/Hints.hpp new file mode 100644 index 0000000..e509e3d --- /dev/null +++ b/lib/include/PrimeDep/Resources/MetroidPrime/Hints.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "PrimeDep/IResource.hpp" + +namespace axdl::primedep::MetroidPrime { +class Hints final +: public TypedResource('HINT', ".hintr", ".hints", DESCRIPTION("Hint definitions for in-game events")) { +public: + static constexpr uint32_t kMagic = 0x00BADBAD; + static constexpr uint32_t kVersion = 1; + struct HintLocation { + explicit HintLocation(athena::io::IStreamReader& in); + explicit HintLocation(const nlohmann::ordered_json& in); + + void PutTo(athena::io::IStreamWriter& out) const; + void PutTo(nlohmann::ordered_json& out) const; + + AssetId32Big m_worldId; + AssetId32Big m_areaId; + uint32_t m_roomId; + AssetId32Big m_stringId; + }; + + struct Hint { + explicit Hint(athena::io::IStreamReader& in); + explicit Hint(const nlohmann::ordered_json& in); + + void PutTo(athena::io::IStreamWriter& out) const; + void PutTo(nlohmann::ordered_json& out) const; + + std::string m_name; + float m_immediateTime; + float m_normalTime; + AssetId32Big m_stringId; + uint32_t m_textTime; + std::vector m_locations; + }; + Hints(const char* ptr, std::size_t size); + explicit Hints(const nlohmann::ordered_json& in); + + bool writeCooked(std::string_view path) const override; + bool writeUncooked(std::string_view path) const override; + + static std::shared_ptr loadCooked(const char* ptr, std::size_t size); + + static bool canIngest(const nlohmann::ordered_json& metadata) { + return metadata["ResourceType"] == ResourceType().toString(); + } + static std::shared_ptr ingest(const nlohmann::ordered_json& metadata, std::string_view path); + +private: + std::vector m_hints; +}; +} // namespace axdl::primedep::MetroidPrime \ No newline at end of file diff --git a/lib/src/PrimeDep/Resources/MetroidPrime/DecalData.cpp b/lib/src/PrimeDep/Resources/MetroidPrime/DecalData.cpp index 6a23912..e199725 100644 --- a/lib/src/PrimeDep/Resources/MetroidPrime/DecalData.cpp +++ b/lib/src/PrimeDep/Resources/MetroidPrime/DecalData.cpp @@ -2,8 +2,63 @@ #include "athena/MemoryReader.hpp" +#include + namespace axdl::primedep::MetroidPrime { -DecalData::DecalData(const char* ptr, const std::size_t size) { athena::io::MemoryReader mr(ptr, size, true); } +DecalData::DecalData() +: m_quad1(this) +, m_quad2(this) +, m_model(FOURCC('CMDL'), FOURCC('DMDL'), "Model", this) +, m_lifetime(FOURCC('DLFT'), "Lifetime", this) +, m_modelOffset(FOURCC('DMOP'), "ModelOffset", this) +, m_modelRotation(FOURCC('DMRT'), "ModelRotation", this) +, m_modelScale(FOURCC('DMSC'), "ModelScale", this) +, m_modelColor(FOURCC('DMCL'), "ModelColor", this) +, m_modelAdditiveBlend(false, FOURCC('DMAB'), "ModelAdditiveBlend", this) +, m_modelRotationGlobal(false, FOURCC('DMOO'), "ModelRotationGlobal", this) {} + +DecalData::DecalData(const char* ptr, const std::size_t size) : DecalData() { + athena::io::MemoryReader in(ptr, size, true); + + if (particles::ParticleDataFactory::GetClassID(in) != FOURCC('DPSM')) { + return; + } + + auto classId = particles::ParticleDataFactory::GetClassID(in); + + int loadOrder = 0; + while (classId != FOURCC('_END') && !in.hasError()) { + if (auto* cls = propertyForClass(classId)) { + cls->loadValue(in); + cls->setLoadOrder(loadOrder); + ++loadOrder; + } else { + std::cout << "Unhandled class " << classId.toString() << std::endl; + } + classId = particles::ParticleDataFactory::GetClassID(in); + } + + sortProperties(); +} + +[[nodiscard]] bool DecalData::writeUncooked(const std::string_view path) const { + const auto p = rawPath(path); + + nlohmann::ordered_json data = nlohmann::ordered_json::object(); + for (const auto& property : m_properties) { + if (property->loadOrder() == -1) { + continue; + } + + property->PutTo(data); + } + + athena::io::FileWriter writer(p.generic_string()); + const auto js = data.dump(4) + "\n"; + writer.writeString(js, js.length()); + return !writer.hasError(); +} + std::shared_ptr DecalData::loadCooked(const char* ptr, std::size_t size) { return std::make_shared(ptr, size); } diff --git a/lib/src/PrimeDep/Resources/MetroidPrime/Hints.cpp b/lib/src/PrimeDep/Resources/MetroidPrime/Hints.cpp new file mode 100644 index 0000000..b883c58 --- /dev/null +++ b/lib/src/PrimeDep/Resources/MetroidPrime/Hints.cpp @@ -0,0 +1,155 @@ +#include "PrimeDep/Resources/MetroidPrime/Hints.hpp" + +#include "PrimeDep/Resources/MetroidPrime/AnimPOIData.hpp" + +#include +#include + +namespace axdl::primedep::MetroidPrime { + +Hints::HintLocation::HintLocation(athena::io::IStreamReader& in) +: m_worldId(in, FOURCC('MLVL')) +, m_areaId(in, FOURCC('MREA')) +, m_roomId(in.readUint32Big()) +, m_stringId(in, FOURCC('STRG')) {} + +Hints::HintLocation::HintLocation(const nlohmann::ordered_json& in) +: m_worldId(in.value("World", nlohmann::ordered_json()), FOURCC('MLVL')) +, m_areaId(in.value("Area", nlohmann::ordered_json()), FOURCC('MREA')) +, m_roomId(in.value("World", -1)) +, m_stringId(in.value("String", nlohmann::ordered_json()), FOURCC('STRG')) {} + +void Hints::HintLocation::PutTo(athena::io::IStreamWriter& out) const { + m_worldId.PutTo(out); + m_areaId.PutTo(out); + out.writeUint32Big(m_roomId); + m_stringId.PutTo(out); +} +void Hints::HintLocation::PutTo(nlohmann::ordered_json& out) const { + m_worldId.PutTo(out["World"], FOURCC('MLVL')); + m_areaId.PutTo(out["Area"], FOURCC('MREA')); + out["Room"] = m_roomId; + m_stringId.PutTo(out["String"], FOURCC('STRG')); +} + +Hints::Hint::Hint(athena::io::IStreamReader& in) +: m_name(in.readString()) +, m_immediateTime(in.readFloatBig()) +, m_normalTime(in.readFloatBig()) +, m_stringId(in, FOURCC('STRG')) +, m_textTime(in.readInt32Big()) { + uint32_t count = in.readUint32Big(); + while (count--) { + m_locations.emplace_back(in); + } +} + +Hints::Hint::Hint(const nlohmann::ordered_json& in) +: m_name(in.value("Name", "")) +, m_immediateTime(in.value("ImmediateTime", 0.f)) +, m_normalTime(in.value("NormalTime", 0.f)) +, m_stringId(in.value("String", nlohmann::ordered_json()), FOURCC('STRG')) +, m_textTime(in.value("TextTime", 0)) { + + if (in.contains("Locations")) { + for (const auto& locations = in["Locations"]; const auto& location : locations) { + m_locations.emplace_back(location); + } + } +} + +void Hints::Hint::PutTo(athena::io::IStreamWriter& out) const { + out.writeString(m_name); + out.writeFloatBig(m_immediateTime); + out.writeFloatBig(m_normalTime); + m_stringId.PutTo(out); + out.writeInt32Big(m_textTime); + out.writeUint32Big(m_locations.size()); + for (const auto& location : m_locations) { + location.PutTo(out); + } +} + +void Hints::Hint::PutTo(nlohmann::ordered_json& out) const { + out["Name"] = m_name; + out["ImmediateTime"] = m_immediateTime; + out["NormalTime"] = m_normalTime; + m_stringId.PutTo(out["String"], FOURCC('STRG')); + out["TextTime"] = m_textTime; + auto& locations = out["Locations"]; + for (const auto& location : m_locations) { + location.PutTo(locations.emplace_back()); + } +} + +Hints::Hints(const char* ptr, std::size_t size) { + athena::io::MemoryReader in(ptr, size); + const auto magic = in.readUint32Big(); + if (magic != kMagic) { + printf("Unexpected Hint magic, got %08X, expected %08X\n", magic, kMagic); + return; + } + + const auto version = in.readUint32Big(); + if (version != kVersion) { + printf("Unexpected Hint version, got %i, expected %i\n", version, kVersion); + return; + } + + auto count = in.readUint32Big(); + + while (count--) { + m_hints.emplace_back(in); + } +} + +Hints::Hints(const nlohmann::ordered_json& in) { + if (in.contains("Hints")) { + const auto hints = in["Hints"]; + for (const auto& hint : hints) { + m_hints.emplace_back(hint); + } + } +} + +bool Hints::writeCooked(const std::string_view path) const { + const auto p = cookedPath(path); + athena::io::FileWriter out(p.generic_string()); + out.writeUint32Big(kMagic); + out.writeUint32Big(kVersion); + + out.writeUint32Big(m_hints.size()); + for (const auto& hint : m_hints) { + hint.PutTo(out); + } + + return !out.hasError(); +} + +bool Hints::writeUncooked(const std::string_view path) const { + const auto p = rawPath(path); + + nlohmann::ordered_json j; + auto& hints = j["Hints"]; + hints = nlohmann::ordered_json::array(); + for (const auto& hint : m_hints) { + hint.PutTo(hints.emplace_back()); + } + + athena::io::FileWriter writer(p.generic_string()); + const auto js = j.dump(4) + "\n"; + writer.writeString(js, js.length()); + return !writer.hasError(); +} + +std::shared_ptr Hints::loadCooked(const char* ptr, std::size_t size) { + return std::make_shared(ptr, size); +} + +std::shared_ptr Hints::ingest([[maybe_unused]] const nlohmann::ordered_json& metadata, + const std::string_view path) { + athena::io::FileReader in(path); + auto js = nlohmann::ordered_json::parse(in.readString()); + return std::make_shared(js); +} +} // namespace axdl::primedep::MetroidPrime \ No newline at end of file diff --git a/uncooker/src/main.cpp b/uncooker/src/main.cpp index d101907..1e9f4ee 100644 --- a/uncooker/src/main.cpp +++ b/uncooker/src/main.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -100,6 +101,7 @@ void addFactories(axdl::primedep::ResourceFactory& factory) { axdl::primedep::RegisterFactory(factory); axdl::primedep::RegisterFactory(factory); axdl::primedep::RegisterFactory(factory); + axdl::primedep::RegisterFactory(factory); axdl::primedep::RegisterSpecialFactory(factory); }