Add Decal uncooking and HINT

This commit is contained in:
Phillip Stephens
2026-05-21 12:17:57 -07:00
parent 4c3e808eca
commit f8d31ef7ec
7 changed files with 311 additions and 2 deletions
+1
View File
@@ -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
+4
View File
@@ -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))
@@ -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 <char Num>
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<IResource> 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<IResource> 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
@@ -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<HintLocation> 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<IResource> 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<IResource> ingest(const nlohmann::ordered_json& metadata, std::string_view path);
private:
std::vector<Hint> m_hints;
};
} // namespace axdl::primedep::MetroidPrime
@@ -2,8 +2,63 @@
#include "athena/MemoryReader.hpp"
#include <iostream>
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<IResource> DecalData::loadCooked(const char* ptr, std::size_t size) {
return std::make_shared<DecalData>(ptr, size);
}
@@ -0,0 +1,155 @@
#include "PrimeDep/Resources/MetroidPrime/Hints.hpp"
#include "PrimeDep/Resources/MetroidPrime/AnimPOIData.hpp"
#include <athena/FileReader.hpp>
#include <athena/MemoryReader.hpp>
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<IResource> Hints::loadCooked(const char* ptr, std::size_t size) {
return std::make_shared<Hints>(ptr, size);
}
std::shared_ptr<IResource> 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<Hints>(js);
}
} // namespace axdl::primedep::MetroidPrime
+2
View File
@@ -17,6 +17,7 @@
#include <PrimeDep/Resources/MetroidPrime/DecalData.hpp>
#include <PrimeDep/Resources/MetroidPrime/DependencyGroup.hpp>
#include <PrimeDep/Resources/MetroidPrime/GuiFrame.hpp>
#include <PrimeDep/Resources/MetroidPrime/Hints.hpp>
#include <PrimeDep/Resources/MetroidPrime/MapArea.hpp>
#include <PrimeDep/Resources/MetroidPrime/MapUniverse.hpp>
#include <PrimeDep/Resources/MetroidPrime/MapWorld.hpp>
@@ -100,6 +101,7 @@ void addFactories(axdl::primedep::ResourceFactory& factory) {
axdl::primedep::RegisterFactory<axdl::primedep::MetroidPrime::SaveWorld>(factory);
axdl::primedep::RegisterFactory<axdl::primedep::MetroidPrime::MetroidWorld>(factory);
axdl::primedep::RegisterFactory<axdl::primedep::MetroidPrime::DependencyGroup>(factory);
axdl::primedep::RegisterFactory<axdl::primedep::MetroidPrime::Hints>(factory);
axdl::primedep::RegisterSpecialFactory<axdl::primedep::MetroidPrime::ITweak>(factory);
}