summaryrefslogtreecommitdiff
path: root/src/factories/pm64/EntityGfxFactory.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/factories/pm64/EntityGfxFactory.cpp')
-rw-r--r--src/factories/pm64/EntityGfxFactory.cpp569
1 files changed, 569 insertions, 0 deletions
diff --git a/src/factories/pm64/EntityGfxFactory.cpp b/src/factories/pm64/EntityGfxFactory.cpp
new file mode 100644
index 0000000..23f90b6
--- /dev/null
+++ b/src/factories/pm64/EntityGfxFactory.cpp
@@ -0,0 +1,569 @@
+#include "EntityGfxFactory.h"
+#include "Companion.h"
+#include "spdlog/spdlog.h"
+#include <unordered_set>
+#include <unordered_map>
+#include <sstream>
+#include "n64/gbi-otr.h"
+#include "strhash64/StrHash64.h"
+
+// F3DEX2 GBI opcodes
+#define F3DEX2_G_ENDDL 0xDF
+#define F3DEX2_G_VTX 0x01
+#define F3DEX2_G_DL 0xDE
+#define F3DEX2_G_SETTIMG 0xFD
+#define F3DEX2_G_MOVEMEM 0xDC
+#define F3DEX2_G_MTX 0xDA
+
+// Walk a display list at the given offset, byte-swap commands from BE to native,
+// collect them, and recursively process nested display lists.
+// Buffer is const — overlapping display lists share tail commands,
+// so we must never modify the shared ROM data.
+static void WalkDisplayList(const uint8_t* data, uint32_t offset, size_t bufferSize,
+ std::unordered_set<uint32_t>& visited,
+ std::vector<PM64EntityDisplayListInfo>& collected) {
+ if (offset >= bufferSize - 8) return;
+ if (visited.count(offset)) return;
+ visited.insert(offset);
+
+ const uint8_t* ptr = data + offset;
+ const uint8_t* endPtr = data + bufferSize;
+
+ PM64EntityDisplayListInfo dlInfo;
+ dlInfo.offset = offset;
+
+ while (ptr + 8 <= endPtr) {
+ const uint32_t* words = reinterpret_cast<const uint32_t*>(ptr);
+ uint32_t w0 = BSWAP32(words[0]);
+ uint32_t w1 = BSWAP32(words[1]);
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ // G_VTX: w1 is a segment 0xA address, strip segment byte
+ if (opcode == F3DEX2_G_VTX) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_SETTIMG: w1 is a segment 0xA address, strip segment byte
+ if (opcode == F3DEX2_G_SETTIMG) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_MOVEMEM: w1 is a segment 0xA address
+ if (opcode == F3DEX2_G_MOVEMEM) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_MTX: w1 is a segment 0xA address
+ if (opcode == F3DEX2_G_MTX) {
+ w1 = w1 & 0x00FFFFFF;
+ }
+
+ // G_DL: w1 is a segment 0xA address, strip and recurse
+ if (opcode == F3DEX2_G_DL) {
+ uint32_t nestedOffset = w1 & 0x00FFFFFF;
+ w1 = nestedOffset;
+ WalkDisplayList(data, nestedOffset, bufferSize, visited, collected);
+ }
+
+ dlInfo.commands.push_back(w0);
+ dlInfo.commands.push_back(w1);
+
+ if (opcode == F3DEX2_G_ENDDL) {
+ break;
+ }
+ ptr += 8;
+ }
+
+ if (!dlInfo.commands.empty()) {
+ collected.push_back(std::move(dlInfo));
+ }
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64EntityGfxFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto size = GetSafeNode<uint32_t>(node, "size");
+ auto dlistsNode = node["dlists"];
+
+ if (offset + size > buffer.size()) {
+ SPDLOG_ERROR("PM64:ENTITY_GFX: Data at offset 0x{:X} exceeds buffer (need {}, have {})",
+ offset, size, buffer.size() - offset);
+ return std::nullopt;
+ }
+
+ // Copy entity graphics data from ROM (initial size from YAML)
+ std::vector<uint8_t> entityData(buffer.data() + offset, buffer.data() + offset + size);
+
+ // Walk each display list specified in the YAML
+ std::unordered_set<uint32_t> visited;
+ std::vector<PM64EntityDisplayListInfo> collectedDLs;
+
+ if (dlistsNode && dlistsNode.IsSequence()) {
+ for (size_t i = 0; i < dlistsNode.size(); i++) {
+ uint32_t dlOffset = dlistsNode[i].as<uint32_t>();
+ if (dlOffset < size) {
+ WalkDisplayList(entityData.data(), dlOffset, entityData.size(), visited, collectedDLs);
+ }
+ }
+ }
+
+ // Find maximum offset referenced by any DL command (textures, vertices, matrices
+ // may live beyond the declared size in shared ROM space)
+ uint32_t maxReferencedEnd = size;
+ for (const auto& dl : collectedDLs) {
+ for (size_t i = 0; i < dl.commands.size(); i += 2) {
+ uint32_t w0 = dl.commands[i];
+ uint32_t w1 = dl.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_VTX) {
+ uint32_t n = (w0 >> 12) & 0xFF;
+ uint32_t end = w1 + n * 16;
+ if (end > maxReferencedEnd) maxReferencedEnd = end;
+ } else if (opcode == F3DEX2_G_SETTIMG || opcode == F3DEX2_G_MTX || opcode == F3DEX2_G_MOVEMEM) {
+ // We don't know exact sizes yet, but the offset itself must be in-bounds
+ // Add a generous margin (textures can be large)
+ if (w1 >= maxReferencedEnd) maxReferencedEnd = w1 + 0x800;
+ }
+ }
+ }
+
+ // Expand buffer from ROM if DLs reference data beyond the declared size
+ if (maxReferencedEnd > size) {
+ uint32_t expandedSize = maxReferencedEnd;
+ if (offset + expandedSize > buffer.size()) {
+ expandedSize = buffer.size() - offset;
+ }
+ entityData.assign(buffer.data() + offset, buffer.data() + offset + expandedSize);
+ }
+
+ // Collect standalone Mtx offsets from YAML (not reachable by DL walking)
+ std::vector<uint32_t> standaloneMtx;
+ auto standaloneMtxNode = node["standalone_mtx"];
+ if (standaloneMtxNode && standaloneMtxNode.IsSequence()) {
+ for (size_t i = 0; i < standaloneMtxNode.size(); i++) {
+ standaloneMtx.push_back(standaloneMtxNode[i].as<uint32_t>());
+ }
+ }
+
+ auto data = std::make_shared<PM64EntityGfxData>(std::move(entityData), std::move(collectedDLs));
+ data->mStandaloneMtx = std::move(standaloneMtx);
+ return data;
+}
+
+// Export a display list as an OTR DisplayList resource
+static void ExportEntityDisplayList(const std::string& entityName, const PM64EntityDisplayListInfo& dlInfo) {
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/dlist_%X", entityName.c_str(), dlInfo.offset);
+ std::string path = pathBuf;
+ std::string fullPath = Companion::Instance->RelativePath(path);
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::DisplayList, 0);
+
+ // GBI version byte (F3DEX2)
+ writer.Write(static_cast<int8_t>(GBIVersion::f3dex2));
+
+ // Pad to 8-byte alignment
+ while (writer.GetBaseAddress() % 8 != 0)
+ writer.Write(static_cast<int8_t>(0xFF));
+
+ // G_MARKER with resource hash
+ uint64_t hash = CRC64(fullPath.c_str());
+ writer.Write(static_cast<uint32_t>(G_MARKER << 24));
+ writer.Write(static_cast<uint32_t>(0xBEEFBEEF));
+ writer.Write(static_cast<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(hash & 0xFFFFFFFF));
+
+ // Write commands, converting to OTR format
+ for (size_t i = 0; i < dlInfo.commands.size(); i += 2) {
+ uint32_t w0 = dlInfo.commands[i];
+ uint32_t w1 = dlInfo.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_SETTIMG) {
+ // Convert to G_SETTIMG_OTR_HASH
+ char texPath[256];
+ snprintf(texPath, sizeof(texPath), "%s/tex_%X", entityName.c_str(), w1);
+ std::string fullTexPath = Companion::Instance->RelativePath(texPath);
+ uint64_t texHash = CRC64(fullTexPath.c_str());
+
+ uint32_t newW0 = (G_SETTIMG_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(texHash >> 32));
+ writer.Write(static_cast<uint32_t>(texHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_VTX) {
+ // Convert to G_VTX_OTR_HASH
+ char vtxPath[256];
+ snprintf(vtxPath, sizeof(vtxPath), "%s/vtx_%X", entityName.c_str(), w1);
+ std::string fullVtxPath = Companion::Instance->RelativePath(vtxPath);
+ uint64_t vtxHash = CRC64(fullVtxPath.c_str());
+
+ uint32_t newW0 = (G_VTX_OTR_HASH << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(vtxHash >> 32));
+ writer.Write(static_cast<uint32_t>(vtxHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_MTX) {
+ // Convert to G_MTX_OTR (hash-based)
+ char mtxPath[256];
+ snprintf(mtxPath, sizeof(mtxPath), "%s/mtx_%X", entityName.c_str(), w1);
+ std::string fullMtxPath = Companion::Instance->RelativePath(mtxPath);
+ uint64_t mtxHash = CRC64(fullMtxPath.c_str());
+
+ // Preserve flags from original w0 (push/nopush, load/mul, projection/modelview)
+ uint32_t newW0 = (G_MTX_OTR << 24) | (w0 & 0x00FFFFFF);
+ writer.Write(newW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(mtxHash >> 32));
+ writer.Write(static_cast<uint32_t>(mtxHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_MOVEMEM) {
+ // Convert to G_MOVEMEM_OTR_HASH
+ char mmPath[256];
+ snprintf(mmPath, sizeof(mmPath), "%s/mm_%X", entityName.c_str(), w1);
+ std::string fullMmPath = Companion::Instance->RelativePath(mmPath);
+ uint64_t mmHash = CRC64(fullMmPath.c_str());
+
+ uint8_t index = w0 & 0xFF;
+ uint8_t mmOffset = ((w0 >> 8) & 0xFF) * 8;
+
+ writer.Write(static_cast<uint32_t>(G_MOVEMEM_OTR_HASH << 24));
+ writer.Write(static_cast<uint32_t>((index << 24) | (mmOffset << 16)));
+ writer.Write(static_cast<uint32_t>(mmHash >> 32));
+ writer.Write(static_cast<uint32_t>(mmHash & 0xFFFFFFFF));
+ } else if (opcode == F3DEX2_G_DL) {
+ // Convert to G_DL_OTR_HASH
+ char nestedPath[256];
+ snprintf(nestedPath, sizeof(nestedPath), "%s/dlist_%X", entityName.c_str(), w1);
+ std::string fullNestedPath = Companion::Instance->RelativePath(nestedPath);
+ uint64_t nestedHash = CRC64(fullNestedPath.c_str());
+
+ uint8_t pushFlag = (w0 >> 16) & 0x01;
+ uint32_t otrW0 = (0x31u << 24) | (pushFlag << 16);
+ writer.Write(otrW0);
+ writer.Write(static_cast<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(nestedHash >> 32));
+ writer.Write(static_cast<uint32_t>(nestedHash & 0xFFFFFFFF));
+ } else {
+ // Standard 8-byte command
+ writer.Write(w0);
+ writer.Write(w1);
+ }
+ }
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> data(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, data);
+}
+
+// Export vertex data as a Vertex resource (V1 format with float ob[])
+static void ExportVertexResource_Entity(const std::string& entityName, const uint8_t* data,
+ uint32_t offset, uint32_t size, uint32_t totalSize) {
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/vtx_%X", entityName.c_str(), offset);
+ std::string path = pathBuf;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Vertex, 0);
+
+ // Write vertex count and per-vertex data (read from raw BE ROM data)
+ uint32_t count = size / 16;
+ writer.Write(count);
+ for (uint32_t i = 0; i < count; i++) {
+ const uint8_t* src = data + offset + i * 16;
+ writer.Write(static_cast<int16_t>((src[0] << 8) | src[1])); // ob[0]
+ writer.Write(static_cast<int16_t>((src[2] << 8) | src[3])); // ob[1]
+ writer.Write(static_cast<int16_t>((src[4] << 8) | src[5])); // ob[2]
+ writer.Write(static_cast<uint16_t>((src[6] << 8) | src[7])); // flag
+ writer.Write(static_cast<int16_t>((src[8] << 8) | src[9])); // tc[0]
+ writer.Write(static_cast<int16_t>((src[10] << 8) | src[11])); // tc[1]
+ writer.Write(src[12]); writer.Write(src[13]); writer.Write(src[14]); writer.Write(src[15]); // cn[4]
+ }
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+// Map N64 fmt/siz to Torch TextureType enum value
+static uint32_t N64FmtSizToTextureType(uint32_t fmt, uint32_t siz) {
+ switch (fmt) {
+ case 0: // G_IM_FMT_RGBA
+ return (siz == 3) ? 1 : 2; // RGBA32bpp or RGBA16bpp
+ case 2: // G_IM_FMT_CI
+ return (siz == 0) ? 3 : 4; // Palette4bpp or Palette8bpp
+ case 4: // G_IM_FMT_I
+ return (siz == 0) ? 5 : 6; // Grayscale4bpp or Grayscale8bpp
+ case 3: // G_IM_FMT_IA
+ if (siz == 0) return 7; // GrayscaleAlpha4bpp
+ if (siz == 1) return 8; // GrayscaleAlpha8bpp
+ return 9; // GrayscaleAlpha16bpp
+ default:
+ return 2; // Default to RGBA16bpp
+ }
+}
+
+// Export texture/palette data as a Texture resource (V1 format)
+// Fast3D interpreter reads pixel/palette data as BE byte pairs — keep raw ROM byte order.
+static void ExportTextureResource(const std::string& entityName, const uint8_t* data,
+ uint32_t offset, uint32_t size, const char* prefix,
+ uint32_t settimgW0) {
+ if (offset + size > 0x100000) return; // Sanity check
+
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/%s_%X", entityName.c_str(), prefix, offset);
+ std::string path = pathBuf;
+
+ // Extract format info from the G_SETTIMG w0 word
+ uint32_t fmt = (settimgW0 >> 21) & 0x7;
+ uint32_t siz = (settimgW0 >> 19) & 0x3;
+ uint32_t width = (settimgW0 & 0xFFF) + 1;
+
+ // Compute height from data size and pixel format
+ uint32_t bitsPerPixel;
+ switch (siz) {
+ case 0: bitsPerPixel = 4; break;
+ case 1: bitsPerPixel = 8; break;
+ case 2: bitsPerPixel = 16; break;
+ case 3: bitsPerPixel = 32; break;
+ default: bitsPerPixel = 16; break;
+ }
+ uint32_t bytesPerRow = (width * bitsPerPixel + 7) / 8;
+ uint32_t height = (bytesPerRow > 0) ? (size / bytesPerRow) : 1;
+ if (height == 0) height = 1;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 1);
+ writer.Write(N64FmtSizToTextureType(fmt, siz)); // Type
+ writer.Write(width); // Width
+ writer.Write(height); // Height
+ writer.Write(static_cast<uint32_t>(0)); // Flags
+ writer.Write(1.0f); // HByteScale
+ writer.Write(1.0f); // VPixelScale
+ writer.Write(static_cast<uint32_t>(size)); // ImageDataSize
+ writer.Write(const_cast<char*>(reinterpret_cast<const char*>(data + offset)), size);
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+// Export matrix data as a Blob resource — convert N64 fixed-point to float[4][4]
+static void ExportMatrixBlob(const std::string& entityName, const uint8_t* data,
+ uint32_t offset) {
+ const uint32_t MTX_SIZE = 64; // N64 Mtx is 64 bytes (s15.16 interleaved)
+ std::vector<uint8_t> mtxData(data + offset, data + offset + MTX_SIZE);
+
+ // Byte-swap 32-bit words from BE
+ for (uint32_t i = 0; i + 4 <= MTX_SIZE; i += 4) {
+ uint32_t* v = reinterpret_cast<uint32_t*>(mtxData.data() + i);
+ *v = BSWAP32(*v);
+ }
+
+ // Decode interleaved integer/fraction parts to float[4][4]
+ int32_t* addr = reinterpret_cast<int32_t*>(mtxData.data());
+ float matrix[4][4];
+ for (int i = 0; i < 4; i++) {
+ for (int j = 0; j < 2; j++) {
+ int32_t int_part = addr[i * 2 + j];
+ uint32_t frac_part = addr[8 + i * 2 + j];
+ matrix[i][j * 2] = (int32_t)((int_part & 0xFFFF0000) | (frac_part >> 16)) / 65536.0f;
+ matrix[i][j * 2 + 1] = (int32_t)((int_part << 16) | (frac_part & 0xFFFF)) / 65536.0f;
+ }
+ }
+
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/mtx_%X", entityName.c_str(), offset);
+ std::string path = pathBuf;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(MTX_SIZE));
+ writer.Write(reinterpret_cast<char*>(matrix), MTX_SIZE);
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+// Export G_MOVEMEM data (lights, viewports) as a Blob resource
+static void ExportMovememBlob(const std::string& entityName, const uint8_t* data,
+ uint32_t offset, uint32_t size, uint8_t index) {
+ std::vector<uint8_t> mmData(data + offset, data + offset + size);
+
+ // Viewport data has s16 fields that need byte-swap
+ if (index == 0x08) { // G_MV_VIEWPORT
+ for (uint32_t i = 0; i + 2 <= size; i += 2) {
+ uint16_t* v = reinterpret_cast<uint16_t*>(mmData.data() + i);
+ *v = BSWAP16(*v);
+ }
+ }
+
+ char pathBuf[256];
+ snprintf(pathBuf, sizeof(pathBuf), "%s/mm_%X", entityName.c_str(), offset);
+ std::string path = pathBuf;
+
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(size));
+ writer.Write(reinterpret_cast<char*>(mmData.data()), size);
+
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ std::vector<char> fileData(str.begin(), str.end());
+ Companion::Instance->RegisterCompanionFile(path, fileData);
+}
+
+ExportResult PM64EntityGfxBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto entityData = std::static_pointer_cast<PM64EntityGfxData>(raw);
+
+ // Extract entity name from entry path
+ std::string entityName = entryName;
+ size_t lastSlash = entryName.rfind('/');
+ if (lastSlash != std::string::npos) {
+ entityName = entryName.substr(lastSlash + 1);
+ }
+
+ // Collect all vertex, texture, matrix, and movemem offsets from display lists
+ std::unordered_set<uint32_t> vtxOffsets;
+ std::unordered_map<uint32_t, uint32_t> texInfo; // offset → G_SETTIMG w0
+ std::unordered_set<uint32_t> mtxOffsets;
+ std::unordered_map<uint32_t, uint32_t> mmInfo; // offset → w0
+
+ for (const auto& dl : entityData->mDisplayLists) {
+ for (size_t i = 0; i < dl.commands.size(); i += 2) {
+ uint32_t w0 = dl.commands[i];
+ uint32_t w1 = dl.commands[i + 1];
+ uint8_t opcode = (w0 >> 24) & 0xFF;
+
+ if (opcode == F3DEX2_G_VTX) {
+ vtxOffsets.insert(w1);
+ } else if (opcode == F3DEX2_G_SETTIMG) {
+ if (texInfo.find(w1) == texInfo.end()) {
+ texInfo[w1] = w0;
+ }
+ } else if (opcode == F3DEX2_G_MTX) {
+ mtxOffsets.insert(w1);
+ } else if (opcode == F3DEX2_G_MOVEMEM) {
+ if (mmInfo.find(w1) == mmInfo.end()) {
+ mmInfo[w1] = w0;
+ }
+ }
+ }
+ }
+
+ // Export vertex blobs
+ std::vector<uint32_t> sortedVtxOffsets(vtxOffsets.begin(), vtxOffsets.end());
+ std::sort(sortedVtxOffsets.begin(), sortedVtxOffsets.end());
+
+ for (size_t i = 0; i < sortedVtxOffsets.size(); i++) {
+ uint32_t vtxOff = sortedVtxOffsets[i];
+ // Find max vertex count from G_VTX commands referencing this offset
+ uint32_t vtxSize = 0;
+ for (const auto& dl : entityData->mDisplayLists) {
+ for (size_t j = 0; j < dl.commands.size(); j += 2) {
+ uint32_t w0 = dl.commands[j];
+ uint32_t w1 = dl.commands[j + 1];
+ uint8_t op = (w0 >> 24) & 0xFF;
+ if (op == F3DEX2_G_VTX && w1 == vtxOff) {
+ uint32_t n = (w0 >> 12) & 0xFF;
+ uint32_t candidateSize = n * 16;
+ if (candidateSize > vtxSize) vtxSize = candidateSize;
+ }
+ }
+ }
+ if (vtxSize == 0) vtxSize = 256;
+ if (vtxOff + vtxSize <= entityData->mBuffer.size()) {
+ ExportVertexResource_Entity(entityName, entityData->mBuffer.data(), vtxOff, vtxSize, entityData->mBuffer.size());
+ }
+ }
+
+ // Export texture resources
+ std::vector<uint32_t> sortedTexOffsets;
+ for (const auto& [off, w0] : texInfo) {
+ sortedTexOffsets.push_back(off);
+ }
+ std::sort(sortedTexOffsets.begin(), sortedTexOffsets.end());
+
+ for (size_t i = 0; i < sortedTexOffsets.size(); i++) {
+ uint32_t texOff = sortedTexOffsets[i];
+ uint32_t texSize;
+ if (i + 1 < sortedTexOffsets.size()) {
+ texSize = sortedTexOffsets[i + 1] - texOff;
+ } else {
+ // Last texture - find next known structure after it
+ uint32_t nextOff = entityData->mBuffer.size();
+ for (const auto& dl : entityData->mDisplayLists) {
+ if (dl.offset > texOff && dl.offset < nextOff) {
+ nextOff = dl.offset;
+ }
+ }
+ for (uint32_t vo : sortedVtxOffsets) {
+ if (vo > texOff && vo < nextOff) {
+ nextOff = vo;
+ }
+ }
+ for (uint32_t mo : mtxOffsets) {
+ if (mo > texOff && mo < nextOff) {
+ nextOff = mo;
+ }
+ }
+ texSize = nextOff - texOff;
+ }
+ if (texOff + texSize <= entityData->mBuffer.size()) {
+ ExportTextureResource(entityName, entityData->mBuffer.data(), texOff, texSize, "tex", texInfo[texOff]);
+ }
+ }
+
+ // Export matrix blobs (from DL references)
+ for (uint32_t mtxOff : mtxOffsets) {
+ if (mtxOff + 64 <= entityData->mBuffer.size()) {
+ ExportMatrixBlob(entityName, entityData->mBuffer.data(), mtxOff);
+ }
+ }
+
+ // Export standalone matrix blobs (not reachable by DL walking, e.g. Chest lid, Padlock shackle)
+ for (uint32_t mtxOff : entityData->mStandaloneMtx) {
+ if (mtxOffsets.count(mtxOff) == 0 && mtxOff + 64 <= entityData->mBuffer.size()) {
+ ExportMatrixBlob(entityName, entityData->mBuffer.data(), mtxOff);
+ }
+ }
+
+ // Export movemem data blobs
+ for (const auto& [mmOff, mmW0] : mmInfo) {
+ uint32_t sizeField = (mmW0 >> 19) & 0x1F;
+ uint32_t dataSize = (sizeField + 1) * 8;
+ uint8_t index = mmW0 & 0xFF;
+ if (mmOff + dataSize <= entityData->mBuffer.size()) {
+ ExportMovememBlob(entityName, entityData->mBuffer.data(), mmOff, dataSize, index);
+ }
+ }
+
+ // Export each display list
+ for (const auto& dl : entityData->mDisplayLists) {
+ ExportEntityDisplayList(entityName, dl);
+ }
+
+ // Write main blob (entire entity data)
+ auto writer = LUS::BinaryWriter();
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write(static_cast<uint32_t>(entityData->mBuffer.size()));
+ writer.Write(reinterpret_cast<char*>(entityData->mBuffer.data()), entityData->mBuffer.size());
+ writer.Finish(write);
+
+ return std::nullopt;
+}
+
+ExportResult PM64EntityGfxHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, std::string* replacement) {
+ // Header generation handled by tools/extract-entity-offsets.py
+ return std::nullopt;
+}