summaryrefslogtreecommitdiff
path: root/src/factories/pm64
diff options
context:
space:
mode:
authorJeod <47716344+JeodC@users.noreply.github.com>2026-09-16 16:32:11 -0400
committerGitHub <noreply@github.com>2026-09-16 14:32:11 -0600
commit72960ca16f4723f96aaf4037b76072c230af8c11 (patch)
tree8f97ab5cac5a7917b1f55e605e6740e3a33774f5 /src/factories/pm64
parent2cfc71d27cb22919a12e47e2d2e691c7ba17ddd7 (diff)
PM64: Add Charset factory, de-blob bits of sprites, images and tiles for HD texpack use (#261)HEADmain
Diffstat (limited to 'src/factories/pm64')
-rw-r--r--src/factories/pm64/CharsetFactory.cpp133
-rw-r--r--src/factories/pm64/CharsetFactory.h23
-rw-r--r--src/factories/pm64/SpriteFactory.cpp136
-rw-r--r--src/factories/pm64/SpriteFactory.h14
-rw-r--r--src/factories/pm64/StoryImageFactory.cpp37
-rw-r--r--src/factories/pm64/TitleDataFactory.cpp63
6 files changed, 395 insertions, 11 deletions
diff --git a/src/factories/pm64/CharsetFactory.cpp b/src/factories/pm64/CharsetFactory.cpp
new file mode 100644
index 0000000..a076f0e
--- /dev/null
+++ b/src/factories/pm64/CharsetFactory.cpp
@@ -0,0 +1,133 @@
+#include "CharsetFactory.h"
+#include "Companion.h"
+#include <unordered_map>
+#include "utils/Decompressor.h"
+#include "utils/TextureUtils.h"
+#include "spdlog/spdlog.h"
+#include <sstream>
+
+ExportResult PM64CharsetHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ const auto symbol = GetSafeNode(node, "symbol", entryName);
+
+ if (Companion::Instance->IsOTRMode()) {
+ write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n";
+ return std::nullopt;
+ }
+
+ write << "extern u8 " << symbol << "[];\n";
+ return std::nullopt;
+}
+
+static void RegisterTexture(const std::string& name, uint32_t type, uint32_t w, uint32_t h, const uint8_t* px,
+ size_t bytes) {
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 0);
+ writer.Write(type);
+ writer.Write(w);
+ writer.Write(h);
+ writer.Write((uint32_t)bytes);
+ writer.Write((char*)px, bytes);
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ Companion::Instance->RegisterCompanionFile(name, std::vector<char>(str.begin(), str.end()));
+}
+
+static void RegisterCompanionTexture(const std::string& name, uint32_t type, uint32_t w, uint32_t h, const uint8_t* px,
+ size_t bytes) {
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 0);
+ writer.Write(type);
+ writer.Write(w);
+ writer.Write(h);
+ writer.Write((uint32_t)bytes);
+ writer.Write((char*)px, bytes);
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ Companion::Instance->RegisterCompanionFile(name, std::vector<char>(str.begin(), str.end()));
+}
+
+static void RegisterImageCompanions(YAML::Node& node, const std::string& entryName, const std::vector<uint8_t>& data) {
+ std::string base = entryName;
+ const auto slash = base.rfind('/');
+ if (slash != std::string::npos) {
+ base = base.substr(slash + 1);
+ }
+ if (node["img_format"]) {
+ static const std::unordered_map<std::string, std::pair<TextureType, double>> kFormats = {
+ { "RGBA32", { TextureType::RGBA32bpp, 4.0 } }, { "RGBA16", { TextureType::RGBA16bpp, 2.0 } },
+ { "CI4", { TextureType::Palette4bpp, 0.5 } }, { "CI8", { TextureType::Palette8bpp, 1.0 } },
+ { "I4", { TextureType::Grayscale4bpp, 0.5 } }, { "I8", { TextureType::Grayscale8bpp, 1.0 } },
+ { "IA4", { TextureType::GrayscaleAlpha4bpp, 0.5 } }, { "IA8", { TextureType::GrayscaleAlpha8bpp, 1.0 } },
+ { "IA16", { TextureType::GrayscaleAlpha16bpp, 2.0 } },
+ };
+ const auto fmt = GetSafeNode<std::string>(node, "img_format");
+ const auto w = GetSafeNode<uint32_t>(node, "img_width");
+ const auto h = GetSafeNode<uint32_t>(node, "img_height");
+ const auto it = kFormats.find(fmt);
+ if (it != kFormats.end()) {
+ const size_t bytes = (size_t)(w * h * it->second.second);
+ if (bytes <= data.size()) {
+ RegisterCompanionTexture(base + "_img", (uint32_t)it->second.first, w, h, data.data(), bytes);
+ }
+ }
+ }
+ if (node["tlut_colors"]) {
+ const auto colors = GetSafeNode<uint32_t>(node, "tlut_colors");
+ std::string img = base;
+ for (const char* suffix : { "_palette", "_pal" }) {
+ const std::string s = suffix;
+ if (img.size() > s.size() && img.compare(img.size() - s.size(), s.size(), s) == 0) {
+ img = img.substr(0, img.size() - s.size());
+ break;
+ }
+ }
+ if ((size_t)colors * 2 <= data.size()) {
+ RegisterCompanionTexture(img + "_img_tlut", (uint32_t)TextureType::RGBA16bpp, colors, 1, data.data(),
+ (size_t)colors * 2);
+ }
+ }
+}
+
+ExportResult PM64CharsetBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+
+ WriteHeader(writer, Torch::ResourceType::Blob, 0);
+ writer.Write((uint32_t)data.size());
+ writer.Write((char*)data.data(), data.size());
+ writer.Finish(write);
+
+ std::string base = entryName;
+ auto lastSlash = base.rfind('/');
+ if (lastSlash != std::string::npos) {
+ base = base.substr(lastSlash + 1);
+ }
+
+ RegisterImageCompanions(node, entryName, data);
+ if (node["glyph_width"]) {
+ const auto w = GetSafeNode<uint32_t>(node, "glyph_width");
+ const auto h = GetSafeNode<uint32_t>(node, "glyph_height");
+ const size_t bytes = (size_t)w * h / 2;
+ for (size_t i = 0; bytes > 0 && (i + 1) * bytes <= data.size(); i++) {
+ RegisterTexture(base + "_" + std::to_string(i), (uint32_t)TextureType::Palette4bpp, w, h,
+ data.data() + i * bytes, bytes);
+ }
+ } else if (node["palette_stride"]) {
+ const auto stride = GetSafeNode<uint32_t>(node, "palette_stride");
+ for (size_t i = 0; stride > 0 && i * stride + 32 <= data.size(); i++) {
+ RegisterTexture(base + "_" + std::to_string(i), (uint32_t)TextureType::RGBA16bpp, 16, 1,
+ data.data() + i * stride, 32);
+ }
+ }
+
+ return std::nullopt;
+}
+
+std::optional<std::shared_ptr<IParsedData>> PM64CharsetFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto [_, segment] = Decompressor::AutoDecode(node, buffer);
+ return std::make_shared<RawBuffer>(segment.data, segment.size);
+}
diff --git a/src/factories/pm64/CharsetFactory.h b/src/factories/pm64/CharsetFactory.h
new file mode 100644
index 0000000..197aaf0
--- /dev/null
+++ b/src/factories/pm64/CharsetFactory.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "types/RawBuffer.h"
+
+class PM64CharsetBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64CharsetHeaderExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
+};
+
+class PM64CharsetFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Header, PM64CharsetHeaderExporter)
+ REGISTER(Binary, PM64CharsetBinaryExporter)
+ };
+ }
+};
diff --git a/src/factories/pm64/SpriteFactory.cpp b/src/factories/pm64/SpriteFactory.cpp
index 14ac420..dc1dd2a 100644
--- a/src/factories/pm64/SpriteFactory.cpp
+++ b/src/factories/pm64/SpriteFactory.cpp
@@ -1,7 +1,9 @@
#include "SpriteFactory.h"
#include "Companion.h"
#include "utils/Decompressor.h"
+#include "utils/TextureUtils.h"
#include "spdlog/spdlog.h"
+#include <sstream>
#include <unordered_set>
// PM64 sprite structure (decompressed, before byte-swap):
@@ -147,6 +149,85 @@ static void ByteSwapSpriteData(uint8_t* data, size_t size) {
// so the raw ROM byte order must be preserved.
}
+static uint32_t ReadBE32(const std::vector<uint8_t>& rom, size_t offset) {
+ return (rom[offset] << 24) | (rom[offset + 1] << 16) | (rom[offset + 2] << 8) | rom[offset + 3];
+}
+
+// Walk the (already byte-swapped) blob's raster and palette lists. NPC raster
+// images sit inside the blob at imageOffset; player sprites keep their images in
+// the shared player raster data instead, located through the load descriptor
+// table, so those are read from the ROM using the offsets the yaml provides.
+static void CollectSpriteImages(PM64SpriteData& sprite, const std::vector<uint8_t>& rom, YAML::Node& node) {
+ const auto& data = sprite.mBuffer;
+ const size_t size = data.size();
+ if (size < 16) {
+ return;
+ }
+ const uint32_t rastersOffset = reinterpret_cast<const uint32_t*>(data.data())[0];
+ const uint32_t palettesOffset = reinterpret_cast<const uint32_t*>(data.data())[1];
+
+ std::vector<uint32_t> descriptors;
+ uint32_t imageDataBase = 0, imageDataRom = 0;
+ const bool player = node["raster_index"].IsDefined();
+ if (player) {
+ const auto index = GetSafeNode<uint32_t>(node, "raster_index");
+ const auto header = GetSafeNode<uint32_t>(node, "raster_header");
+ const auto sets = GetSafeNode<uint32_t>(node, "raster_sets");
+ const auto descs = GetSafeNode<uint32_t>(node, "raster_descriptors");
+ imageDataRom = GetSafeNode<uint32_t>(node, "raster_image_data");
+ imageDataBase = ReadBE32(rom, header + 8);
+ const uint32_t first = ReadBE32(rom, sets + index * 4);
+ const uint32_t last = ReadBE32(rom, sets + (index + 1) * 4);
+ for (uint32_t i = first; i < last; i++) {
+ descriptors.push_back(ReadBE32(rom, descs + i * 4));
+ }
+ }
+
+ if (rastersOffset > 0 && rastersOffset < size) {
+ const uint32_t* list = reinterpret_cast<const uint32_t*>(data.data() + rastersOffset);
+ for (size_t i = 0; list[i] != 0xFFFFFFFF && reinterpret_cast<const uint8_t*>(&list[i]) < data.data() + size;
+ i++) {
+ const uint32_t entry = list[i];
+ if (entry == 0 || entry + 8 > size) {
+ break;
+ }
+ PM64SpriteRaster raster;
+ const uint32_t imageOffset = *reinterpret_cast<const uint32_t*>(data.data() + entry);
+ raster.width = data[entry + 4];
+ raster.height = data[entry + 5];
+ const size_t bytes = (size_t)raster.width * raster.height / 2;
+ if (player) {
+ if (i >= descriptors.size()) {
+ break;
+ }
+ // upper three nibbles give size / 16, lower five the offset
+ const uint32_t romOffset = imageDataRom + ((descriptors[i] & 0xFFFFF) - imageDataBase);
+ const uint32_t descSize = (descriptors[i] >> 16) & 0xFFF0;
+ // Placeholder entries (back-facing sprites, 255x255 dummies) point at a
+ // 16-byte stub; leave those without pixels so nothing is emitted for them.
+ if (romOffset + bytes <= rom.size() && descSize >= bytes) {
+ raster.pixels.assign(rom.begin() + romOffset, rom.begin() + romOffset + bytes);
+ }
+ } else if (imageOffset + bytes <= size) {
+ raster.pixels.assign(data.begin() + imageOffset, data.begin() + imageOffset + bytes);
+ }
+ sprite.rasters.push_back(std::move(raster));
+ }
+ }
+
+ if (palettesOffset > 0 && palettesOffset < size) {
+ const uint32_t* list = reinterpret_cast<const uint32_t*>(data.data() + palettesOffset);
+ for (size_t i = 0; list[i] != 0xFFFFFFFF && reinterpret_cast<const uint8_t*>(&list[i]) < data.data() + size;
+ i++) {
+ const uint32_t offset = list[i];
+ if (offset + 32 > size) {
+ break;
+ }
+ sprite.palettes.emplace_back(data.begin() + offset, data.begin() + offset + 32);
+ }
+ }
+}
+
std::optional<std::shared_ptr<IParsedData>> PM64SpriteFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
// Get the offset from YAML
auto offset = GetSafeNode<uint32_t>(node, "offset");
@@ -154,6 +235,7 @@ std::optional<std::shared_ptr<IParsedData>> PM64SpriteFactory::parse(std::vector
// Check if this is compressed (YAY0)
auto compressionType = Decompressor::GetCompressionType(buffer, offset);
+ std::vector<uint8_t> spriteData;
if (compressionType == CompressionType::YAY0) {
// Decompress YAY0 data
auto decoded = Decompressor::Decode(buffer, offset, CompressionType::YAY0);
@@ -163,36 +245,68 @@ std::optional<std::shared_ptr<IParsedData>> PM64SpriteFactory::parse(std::vector
}
// Create a copy of decompressed data for byte-swapping
- std::vector<uint8_t> spriteData(decoded->data, decoded->data + decoded->size);
-
- // Byte-swap for little-endian
- ByteSwapSpriteData(spriteData.data(), spriteData.size());
+ spriteData.assign(decoded->data, decoded->data + decoded->size);
SPDLOG_DEBUG("PM64:SPRITE parsed at 0x{:X}, decompressed size: {}", offset, spriteData.size());
-
- return std::make_shared<RawBuffer>(spriteData);
} else {
// Uncompressed - just read raw data with size from YAML
auto size = GetSafeNode<size_t>(node, "size");
auto [_, segment] = Decompressor::AutoDecode(node, buffer, size);
- std::vector<uint8_t> spriteData(segment.data, segment.data + segment.size);
- ByteSwapSpriteData(spriteData.data(), spriteData.size());
-
- return std::make_shared<RawBuffer>(spriteData);
+ spriteData.assign(segment.data, segment.data + segment.size);
}
+
+ // Byte-swap for little-endian
+ ByteSwapSpriteData(spriteData.data(), spriteData.size());
+
+ auto sprite = std::make_shared<PM64SpriteData>(spriteData);
+ CollectSpriteImages(*sprite, buffer, node);
+ return sprite;
}
ExportResult PM64SpriteBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
std::string& entryName, YAML::Node& node, std::string* replacement) {
auto writer = LUS::BinaryWriter();
- auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+ auto sprite = std::static_pointer_cast<PM64SpriteData>(raw);
+ auto& data = sprite->mBuffer;
// Write as Blob type for now - game will load as raw binary
WriteHeader(writer, Torch::ResourceType::Blob, 0);
writer.Write(static_cast<uint32_t>(data.size()));
writer.Write(reinterpret_cast<char*>(data.data()), data.size());
writer.Finish(write);
+ std::string base = entryName;
+ auto lastSlash = base.rfind('/');
+ if (lastSlash != std::string::npos) {
+ base = base.substr(lastSlash + 1);
+ }
+
+ auto companion = [&](const std::string& name, uint32_t type, uint32_t w, uint32_t h,
+ const std::vector<uint8_t>& px) {
+ auto texWriter = LUS::BinaryWriter();
+ WriteHeader(texWriter, Torch::ResourceType::Texture, 0);
+ texWriter.Write(type);
+ texWriter.Write(w);
+ texWriter.Write(h);
+ texWriter.Write((uint32_t)px.size());
+ texWriter.Write((char*)px.data(), px.size());
+ std::stringstream ss;
+ texWriter.Finish(ss);
+ std::string str = ss.str();
+ Companion::Instance->RegisterCompanionFile(name, std::vector<char>(str.begin(), str.end()));
+ };
+
+ for (size_t i = 0; i < sprite->rasters.size(); i++) {
+ const auto& r = sprite->rasters[i];
+ if (r.pixels.empty()) {
+ continue;
+ }
+ companion(base + "_raster_" + std::to_string(i), (uint32_t)TextureType::Palette4bpp, r.width, r.height,
+ r.pixels);
+ }
+ for (size_t i = 0; i < sprite->palettes.size(); i++) {
+ companion(base + "_pal_" + std::to_string(i), (uint32_t)TextureType::RGBA16bpp, 16, 1, sprite->palettes[i]);
+ }
return std::nullopt;
}
diff --git a/src/factories/pm64/SpriteFactory.h b/src/factories/pm64/SpriteFactory.h
index 2002cce..41e712e 100644
--- a/src/factories/pm64/SpriteFactory.h
+++ b/src/factories/pm64/SpriteFactory.h
@@ -3,6 +3,20 @@
#include "factories/BaseFactory.h"
#include "types/RawBuffer.h"
+// One sprite raster split out of the blob so it can be addressed by name.
+struct PM64SpriteRaster {
+ uint8_t width;
+ uint8_t height;
+ std::vector<uint8_t> pixels; // CI4
+};
+
+struct PM64SpriteData : public RawBuffer {
+ std::vector<PM64SpriteRaster> rasters;
+ std::vector<std::vector<uint8_t>> palettes; // 16 x RGBA16, 32 bytes each
+
+ explicit PM64SpriteData(std::vector<uint8_t>& buffer) : RawBuffer(buffer) {}
+};
+
class PM64SpriteBinaryExporter : public BaseExporter {
ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, YAML::Node& node, std::string* replacement) override;
};
diff --git a/src/factories/pm64/StoryImageFactory.cpp b/src/factories/pm64/StoryImageFactory.cpp
index 79c6700..163c6b9 100644
--- a/src/factories/pm64/StoryImageFactory.cpp
+++ b/src/factories/pm64/StoryImageFactory.cpp
@@ -1,6 +1,8 @@
#include "StoryImageFactory.h"
#include "Companion.h"
#include "spdlog/spdlog.h"
+#include "utils/TextureUtils.h"
+#include <sstream>
// PM64 story image factory for intro sequence graphics
// Handles CI8 images with palettes and IA8 images without palettes
@@ -43,6 +45,21 @@ std::optional<std::shared_ptr<IParsedData>> PM64StoryImageFactory::parse(std::ve
return std::make_shared<RawBuffer>(result);
}
+static void RegisterTexture(const std::string& name, uint32_t type, uint32_t w, uint32_t h, const uint8_t* px,
+ size_t bytes) {
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 0);
+ writer.Write(type);
+ writer.Write(w);
+ writer.Write(h);
+ writer.Write((uint32_t)bytes);
+ writer.Write((char*)px, bytes);
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ Companion::Instance->RegisterCompanionFile(name, std::vector<char>(str.begin(), str.end()));
+}
+
ExportResult PM64StoryImageBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
std::string& entryName, YAML::Node& node, std::string* replacement) {
auto writer = LUS::BinaryWriter();
@@ -54,6 +71,26 @@ ExportResult PM64StoryImageBinaryExporter::Export(std::ostream& write, std::shar
writer.Write(reinterpret_cast<char*>(data.data()), data.size());
writer.Finish(write);
+ // The same image and palette as textures the game can address by name
+ // ("<page>_img", "<page>_tlut"), so replacements can be keyed to them.
+ const auto width = GetSafeNode<uint32_t>(node, "width");
+ const auto height = GetSafeNode<uint32_t>(node, "height");
+ const auto hasPalette = GetSafeNode<bool>(node, "has_palette");
+ std::string base = entryName;
+ const auto slash = base.rfind('/');
+ if (slash != std::string::npos) {
+ base = base.substr(slash + 1);
+ }
+ const size_t imageSize = (size_t)width * height;
+ if (data.size() >= imageSize) {
+ RegisterTexture(base + "_img",
+ (uint32_t)(hasPalette ? TextureType::Palette8bpp : TextureType::GrayscaleAlpha8bpp), width,
+ height, data.data(), imageSize);
+ }
+ if (hasPalette && data.size() >= imageSize + 512) {
+ RegisterTexture(base + "_tlut", (uint32_t)TextureType::RGBA16bpp, 256, 1, data.data() + imageSize, 512);
+ }
+
return std::nullopt;
}
diff --git a/src/factories/pm64/TitleDataFactory.cpp b/src/factories/pm64/TitleDataFactory.cpp
index b0ad1f9..8e2b09f 100644
--- a/src/factories/pm64/TitleDataFactory.cpp
+++ b/src/factories/pm64/TitleDataFactory.cpp
@@ -1,5 +1,8 @@
#include "TitleDataFactory.h"
#include "Companion.h"
+#include <unordered_map>
+#include <sstream>
+#include "utils/TextureUtils.h"
#include "utils/Decompressor.h"
#include "spdlog/spdlog.h"
@@ -36,6 +39,65 @@ std::optional<std::shared_ptr<IParsedData>> PM64TitleDataFactory::parse(std::vec
return std::make_shared<RawBuffer>(result);
}
+// Writes a Texture resource next to the resource being exported, so image data that
+// the game keeps as a blob can also be addressed by name (a replacement keys on it).
+static void RegisterCompanionTexture(const std::string& name, uint32_t type, uint32_t w, uint32_t h, const uint8_t* px,
+ size_t bytes) {
+ auto writer = LUS::BinaryWriter();
+ BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 0);
+ writer.Write(type);
+ writer.Write(w);
+ writer.Write(h);
+ writer.Write((uint32_t)bytes);
+ writer.Write((char*)px, bytes);
+ std::stringstream ss;
+ writer.Finish(ss);
+ std::string str = ss.str();
+ Companion::Instance->RegisterCompanionFile(name, std::vector<char>(str.begin(), str.end()));
+}
+
+static void RegisterImageCompanions(YAML::Node& node, const std::string& entryName, const std::vector<uint8_t>& data) {
+ std::string base = entryName;
+ const auto slash = base.rfind('/');
+ if (slash != std::string::npos) {
+ base = base.substr(slash + 1);
+ }
+ if (node["img_format"]) {
+ static const std::unordered_map<std::string, std::pair<TextureType, double>> kFormats = {
+ { "RGBA32", { TextureType::RGBA32bpp, 4.0 } }, { "RGBA16", { TextureType::RGBA16bpp, 2.0 } },
+ { "CI4", { TextureType::Palette4bpp, 0.5 } }, { "CI8", { TextureType::Palette8bpp, 1.0 } },
+ { "I4", { TextureType::Grayscale4bpp, 0.5 } }, { "I8", { TextureType::Grayscale8bpp, 1.0 } },
+ { "IA4", { TextureType::GrayscaleAlpha4bpp, 0.5 } }, { "IA8", { TextureType::GrayscaleAlpha8bpp, 1.0 } },
+ { "IA16", { TextureType::GrayscaleAlpha16bpp, 2.0 } },
+ };
+ const auto fmt = GetSafeNode<std::string>(node, "img_format");
+ const auto w = GetSafeNode<uint32_t>(node, "img_width");
+ const auto h = GetSafeNode<uint32_t>(node, "img_height");
+ const auto it = kFormats.find(fmt);
+ if (it != kFormats.end()) {
+ const size_t bytes = (size_t)(w * h * it->second.second);
+ if (bytes <= data.size()) {
+ RegisterCompanionTexture(base + "_img", (uint32_t)it->second.first, w, h, data.data(), bytes);
+ }
+ }
+ }
+ if (node["tlut_colors"]) {
+ const auto colors = GetSafeNode<uint32_t>(node, "tlut_colors");
+ std::string img = base;
+ for (const char* suffix : { "_palette", "_pal" }) {
+ const std::string s = suffix;
+ if (img.size() > s.size() && img.compare(img.size() - s.size(), s.size(), s) == 0) {
+ img = img.substr(0, img.size() - s.size());
+ break;
+ }
+ }
+ if ((size_t)colors * 2 <= data.size()) {
+ RegisterCompanionTexture(img + "_img_tlut", (uint32_t)TextureType::RGBA16bpp, colors, 1, data.data(),
+ (size_t)colors * 2);
+ }
+ }
+}
+
ExportResult PM64TitleDataBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
std::string& entryName, YAML::Node& node, std::string* replacement) {
auto writer = LUS::BinaryWriter();
@@ -46,6 +108,7 @@ ExportResult PM64TitleDataBinaryExporter::Export(std::ostream& write, std::share
writer.Write(reinterpret_cast<char*>(data.data()), data.size());
writer.Finish(write);
+ RegisterImageCompanions(node, entryName, data);
return std::nullopt;
}