summaryrefslogtreecommitdiff
path: root/src/factories
diff options
context:
space:
mode:
authorJeod <47716344+JeodC@users.noreply.github.com>2026-06-21 21:56:17 -0400
committerLywx <kiritodev01@gmail.com>2026-06-29 18:48:27 -0600
commitec17e8fe49b7bcd9b181e5d0feb05da660c6f061 (patch)
tree793d639c88b08191156a41fea23efa3dc8b8ed93 /src/factories
parentd91a56479c0615231c93bb1623ca0ed8b771b831 (diff)
BK64: Add dialog pack support
Diffstat (limited to 'src/factories')
-rw-r--r--src/factories/BaseFactory.h4
-rw-r--r--src/factories/TextureFactory.cpp56
-rw-r--r--src/factories/bk64/BKAssetFactory.cpp292
-rw-r--r--src/factories/bk64/BKAssetFactory.h6
-rw-r--r--src/factories/bk64/SpriteFactory.cpp109
-rw-r--r--src/factories/bk64/SpriteFactory.h1
6 files changed, 440 insertions, 28 deletions
diff --git a/src/factories/BaseFactory.h b/src/factories/BaseFactory.h
index 3fdb1cf..630f2cd 100644
--- a/src/factories/BaseFactory.h
+++ b/src/factories/BaseFactory.h
@@ -113,6 +113,10 @@ public:
virtual uint32_t GetAlignment() {
return 4;
}
+ virtual bool IsDialogPackRoot() const {
+ return false;
+ }
+ virtual void PreprocessConfig(YAML::Node& cfg, N64::Cartridge* cart) {}
virtual std::optional<std::shared_ptr<IParsedData>> CreateDataPointer() {
return std::nullopt;
}
diff --git a/src/factories/TextureFactory.cpp b/src/factories/TextureFactory.cpp
index 80e0fa9..c8d94ee 100644
--- a/src/factories/TextureFactory.cpp
+++ b/src/factories/TextureFactory.cpp
@@ -420,34 +420,34 @@ std::optional<std::shared_ptr<IParsedData>> TextureFactory::parse_modding(std::v
}
case TextureType::Palette8bpp:
case TextureType::Palette4bpp: {
- // This implementation is not correct.
- // The process should be:
- // png2rgba --> imgpal2rawci
-
- // todo: Add wheel palette input
- // Implement so that it works.
-
- // auto tlut = GetSafeNode<std::string>(node,"tlut_symbol");
- // auto tlutTextureMap = Companion::Instance->GetTlutTextureMap();
- // auto palettePtr = tlutTextureMap[tlut];
-
- // if (palettePtr) {
-
- // auto imgi = png2rgba(buffer.data(), buffer.size(), &width, &height);
- // auto pal = png2rgba(palettePtr->mBuffer.data(), (palettePtr->mWidth * palettePtr->mWidth *
- // palettePtr->mFormat.depth * 2), &width, &height);
-
- // size = width * height * fmt.depth / 8;
- // raw = new uint8_t[size];
-
- // if(imgpal2rawci(raw, imgi, pal, 0, 0, width, height, fmt.depth) <= 0){
- // throw std::runtime_error("Failed to convert PNG to texture");
- // }
- // } else {
-
- // }
- SPDLOG_ERROR("Unsupported texture format for modding: {}", format);
- return std::nullopt;
+ // Re-index the edited PNG against this texture's palette to rebuild the
+ // CI4/CI8 data.
+ rgba* img = png2rgba(buffer.data(), buffer.size(), &width, &height);
+ if (img == nullptr) {
+ throw std::runtime_error("Failed to read PNG for CI texture");
+ }
+ rgba* pal = nullptr;
+ int palColors = (fmt.depth == 4) ? 16 : 256;
+ if (node["tlut_symbol"]) {
+ const auto tlut = GetSafeNode<std::string>(node, "tlut_symbol");
+ if (auto palette = Companion::Instance->GetParseDataBySymbol(tlut); palette.has_value()) {
+ auto palTex = std::static_pointer_cast<TextureData>(palette.value().data.value());
+ palColors = static_cast<int>(palTex->mWidth);
+ pal = raw2rgba(palTex->mBuffer.data(), palColors, 1, palTex->mFormat.depth);
+ }
+ }
+ if (pal == nullptr) {
+ SPDLOG_ERROR("CI texture '{}': could not resolve palette '{}' for re-encode",
+ GetSafeNode<std::string>(node, "symbol", ""),
+ GetSafeNode<std::string>(node, "tlut_symbol", ""));
+ return std::nullopt;
+ }
+ size = width * height * fmt.depth / 8;
+ raw = new uint8_t[size];
+ if (imgpal2rawci(raw, img, pal, nullptr, size, fmt.depth, width * height, palColors) <= 0) {
+ throw std::runtime_error("Failed to convert PNG to CI texture");
+ }
+ break;
}
case TextureType::Grayscale8bpp:
case TextureType::Grayscale4bpp: {
diff --git a/src/factories/bk64/BKAssetFactory.cpp b/src/factories/bk64/BKAssetFactory.cpp
index 8891541..5aa1d8f 100644
--- a/src/factories/bk64/BKAssetFactory.cpp
+++ b/src/factories/bk64/BKAssetFactory.cpp
@@ -115,6 +115,134 @@ ExportResult BKAssetBinaryExporter::Export(std::ostream& write, std::shared_ptr<
return OffsetEntry{ 0 };
}
+// Pack the language list + optional English->translation string table into the binary
+// "langinfo" at the archive root. Layout (LE):
+// u32 version (1)
+// u32 langCount, per language { u32 dialogIndex, u32 script, u32 nameLen, char name[] }
+// u32 stringCount, per string { u32 keyLen, char key[], u32 valLen, char val[] }
+// script: 0 = latin, 1 = japanese.
+static void EmitLangInfo(const std::vector<std::tuple<std::string, uint32_t, uint32_t>>& langs,
+ const std::vector<std::pair<std::string, std::string>>& strings = {}) {
+ if (langs.empty()) {
+ return;
+ }
+ std::vector<char> data;
+ auto putU32 = [&data](uint32_t v) {
+ data.push_back(static_cast<char>(v & 0xFF));
+ data.push_back(static_cast<char>((v >> 8) & 0xFF));
+ data.push_back(static_cast<char>((v >> 16) & 0xFF));
+ data.push_back(static_cast<char>((v >> 24) & 0xFF));
+ };
+ auto putStr = [&data, &putU32](const std::string& s) {
+ putU32(static_cast<uint32_t>(s.size()));
+ data.insert(data.end(), s.begin(), s.end());
+ };
+ putU32(1); // format version
+ putU32(static_cast<uint32_t>(langs.size()));
+ for (const auto& [name, index, script] : langs) {
+ putU32(index);
+ putU32(script);
+ putStr(name);
+ }
+ putU32(static_cast<uint32_t>(strings.size()));
+ for (const auto& [key, val] : strings) {
+ putStr(key);
+ putStr(val);
+ }
+ Companion::Instance->RegisterArchiveFile("langinfo", data);
+}
+
+// Source the language list (+ string table) for langinfo: a pack's langinfo.yml next
+// to modding.yml, else a `langinfo` key on the asset-table node, else (under
+// dialog_pack) names defaulted from the cartridge region.
+static void RegisterLangInfo(YAML::Node& node) {
+ std::vector<std::tuple<std::string, uint32_t, uint32_t>> langs;
+ std::vector<std::pair<std::string, std::string>> strings;
+ const auto& cfg = Companion::Instance->GetConfig();
+
+ // Prefer a pack-supplied langinfo.yml from the modding source dir; fall back
+ // to a langinfo key on the asset-table node.
+ YAML::Node langNode;
+ YAML::Node stringNode;
+ if (cfg.modding && !cfg.moddingPath.empty()) {
+ const auto path = fs::path(cfg.moddingPath) / "langinfo.yml";
+ if (fs::exists(path)) {
+ auto loaded = YAML::LoadFile(path.string());
+ langNode = loaded["langinfo"] ? loaded["langinfo"] : loaded;
+ stringNode = loaded["strings"];
+ }
+ }
+ if (!langNode && node["langinfo"]) {
+ langNode = node["langinfo"];
+ }
+ if (!stringNode && node["strings"]) {
+ stringNode = node["strings"];
+ }
+ // A localized-string table: { "<English>": "<translation>", ... }. Lets a pack
+ // override hardcoded UI strings (world names, parade credits) the asset pipeline
+ // can't reach; the runtime falls back to the English key when a string is absent.
+ if (stringNode && stringNode.IsMap()) {
+ for (const auto& kv : stringNode) {
+ strings.emplace_back(kv.first.as<std::string>(), kv.second.as<std::string>());
+ }
+ }
+
+ if (langNode && langNode.IsSequence()) {
+ for (auto entry : langNode) {
+ langs.emplace_back(GetSafeNode<std::string>(entry, "name"),
+ GetSafeNode<uint32_t>(entry, "index", 0),
+ GetSafeNode<uint32_t>(entry, "script", 0));
+ }
+ } else if (cfg.dialogPack) {
+ if (auto* cart = Companion::Instance->GetCartridge()) {
+ switch (cart->GetCountry()) {
+ case N64::CountryCode::Europe:
+ langs = { { "English (UK)", 0, 0 }, { "French", 1, 0 }, { "German", 2, 0 } };
+ break;
+ case N64::CountryCode::Japan:
+ langs = { { "Japanese", 0, 1 } };
+ break;
+ case N64::CountryCode::NorthAmerica:
+ langs = { { "English (US)", 0, 0 } };
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ EmitLangInfo(langs, strings);
+}
+
+// The pack's internal language folder (assets/lang/<region>/...). A custom pack sets a
+// top-level `region` key in langinfo.yml so its paths don't collide with other packs;
+// retail packs omit it and fall back to the cartridge region.
+static std::string ResolveLangRegion() {
+ const auto& cfg = Companion::Instance->GetConfig();
+ if (cfg.modding && !cfg.moddingPath.empty()) {
+ const auto path = fs::path(cfg.moddingPath) / "langinfo.yml";
+ if (fs::exists(path)) {
+ auto loaded = YAML::LoadFile(path.string());
+ if (loaded["region"]) {
+ return loaded["region"].as<std::string>();
+ }
+ }
+ }
+ if (auto* cart = Companion::Instance->GetCartridge()) {
+ switch (cart->GetCountry()) {
+ case N64::CountryCode::Europe:
+ return "pal";
+ case N64::CountryCode::Japan:
+ return "jp";
+ case N64::CountryCode::NorthAmerica:
+ return "us";
+ default:
+ break;
+ }
+ }
+ return "pack";
+}
+
std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
auto [_, segment] = Decompressor::AutoDecode(node, buffer);
LUS::BinaryReader reader(segment.data, segment.size);
@@ -124,6 +252,14 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
symbolMapExists = true;
}
+ // Emit the pack-level langinfo once, from the asset table: a yaml-declared
+ // langinfo key if present, otherwise the cartridge-region fallback under dialogPack.
+ RegisterLangInfo(node);
+
+ // Resolve the pack's language folder.
+ const std::string langRegion =
+ Companion::Instance->GetConfig().dialogPack ? ResolveLangRegion() : std::string();
+
reader.SetEndianness(Torch::Endianness::Big);
uint32_t assetCount = reader.ReadUInt32();
@@ -312,6 +448,47 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
break;
}
+ // Dialog-pack mode only wants what actually differs by language: the text
+ // assets, plus the assets a pack may redraw for a new script. The font
+ // masks and the in-world text/sign models apply to any region, so custom
+ // packs (not just the retail JP cart) can carry and replace them.
+ if (Companion::Instance->GetConfig().dialogPack) {
+ const bool isText = assetType == BKAssetType::Dialog ||
+ assetType == BKAssetType::GruntyQuestion ||
+ assetType == BKAssetType::QuizQuestion;
+ // Font masks + text-bearing models / signs / overlays a language pack
+ // may relocalize.
+ static const std::unordered_set<uint32_t> kLangAssets = {
+ 0x6EB, // SPRITE_DIALOG_FONT_ALPHAMASK (dialog/quiz/grunty text)
+ 0x6EC, // SPRITE_BOLD_FONT_LETTERS_ALPHAMASK (world names, headers)
+ 0x2EE, // ON_VACATIOIN_SIGN
+ 0x46C, // JIGSAW_PUZZLE
+ 0x486, // XMAS_TREE_SWITCH
+ 0x48B, // JIGGY_PODIUM
+ 0x4EA, // RACE_BANNER_FINISH
+ 0x4EB, // RACE_BANNER_START
+ 0x50A, // SHARKFOOD_ISLAND (model with sign)
+ 0x54C, // GAME OVER
+ 0x54D, // BANJO_KAZOOIE_SIGN
+ 0x54E, // COPYRIGHT_OVERLAY
+ 0x55C, // PRESS_START_OVERLAY
+ 0x55D, // NO_CONTROLLER_OVERLAY
+ 0x563, // LEVEL_ENTRY_SIGNS
+ 0x56C, // THE_END_SIGN
+ };
+ const uint32_t idx = assetInfo.index;
+ bool isLangAsset = kLangAssets.count(idx) != 0;
+ // The JP cart additionally carries the kana dialog font and the
+ // pre-rendered kana pause-menu world-name banners.
+ if (auto* cart = Companion::Instance->GetCartridge();
+ cart != nullptr && cart->GetCountry() == N64::CountryCode::Japan) {
+ isLangAsset = isLangAsset || idx == 0x6EA || (idx >= 0xE2C && idx <= 0xE38);
+ }
+ if (!isText && !isLangAsset) {
+ continue;
+ }
+ }
+
std::string assetSymbol;
std::string assetIndexStr = std::to_string(assetInfo.index);
@@ -325,6 +502,10 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
assetSymbol = assetStream.str();
}
+ if (Companion::Instance->GetConfig().dialogPack) {
+ assetSymbol = "lang/" + langRegion + "/" + assetSymbol;
+ }
+
symbolMap[assetInfo.index] = assetSymbol;
YAML::Node bkAssetNode;
@@ -391,7 +572,118 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
SPDLOG_WARN("[BKAssetFactory] {} slot(s) failed to parse and were skipped", parseFailures);
}
+ // Additive lang assets: slots the base ROM leaves empty but a pack fills via an
+ // ASSET_<id>_* yaml in modding.yml (e.g. region-specific dialog or 0x1600+ banners).
+ // parse_modding builds these from the yaml alone, so the offset below is a placeholder.
+ // No-op outside modding import (gModdedAssetPaths is empty on export).
+ if (Companion::Instance->GetConfig().dialogPack) {
+ size_t additive = 0;
+ for (const auto& [name, yamlPath] : Companion::Instance->GetModdedAssetPaths()) {
+ auto langPos = name.find("lang/");
+ if (langPos == std::string::npos) {
+ continue;
+ }
+ // Only an asset's own yaml is additive; its texture sub-assets (PNGs, e.g. a
+ // sprite's chunks) are created by the factory's parse_modding, so skip them.
+ if (yamlPath.size() < 5 || yamlPath.compare(yamlPath.size() - 5, 5, ".yaml") != 0) {
+ continue;
+ }
+ const std::string sym = name.substr(langPos); // lang/<region>/<folder>/ASSET_<id>_<rest>
+ // Split into [lang, region, folder, stem].
+ std::vector<std::string> parts;
+ size_t start = 0;
+ while (start <= sym.size()) {
+ size_t slash = sym.find('/', start);
+ if (slash == std::string::npos) {
+ parts.emplace_back(sym.substr(start));
+ break;
+ }
+ parts.emplace_back(sym.substr(start, slash - start));
+ start = slash + 1;
+ }
+ if (parts.size() < 4) {
+ continue;
+ }
+ const std::string& folder = parts[2];
+ const char* assetTypeName = nullptr;
+ if (folder == "dialog") {
+ assetTypeName = "BK64:DIALOG";
+ } else if (folder == "quizq") {
+ assetTypeName = "BK64:QUIZQ";
+ } else if (folder == "gruntyq") {
+ assetTypeName = "BK64:GRUNTYQ";
+ } else if (folder == "sprite") {
+ assetTypeName = "BK64:SPRITE"; // e.g. world-name banners at 0x1600+
+ } else {
+ continue;
+ }
+ const std::string& stem = parts[3];
+ if (stem.rfind("ASSET_", 0) != 0) {
+ continue;
+ }
+ size_t idStart = 6; // strlen("ASSET_")
+ size_t idEnd = stem.find('_', idStart);
+ std::string idHex = (idEnd == std::string::npos) ? stem.substr(idStart) : stem.substr(idStart, idEnd - idStart);
+ uint32_t id = 0;
+ try {
+ id = static_cast<uint32_t>(std::stoul(idHex, nullptr, 16));
+ } catch (const std::exception&) {
+ continue;
+ }
+ if (symbolMap.count(id)) {
+ continue; // the base table already covers this slot
+ }
+ YAML::Node addNode;
+ addNode["offset"] = id; // synthetic; AddSubFileAsset zeroes it, parse_modding ignores it
+ addNode["symbol"] = sym;
+ addNode["type"] = assetTypeName;
+ addNode["additive"] = true; // no ROM asset at this id — build entirely from the yaml
+ if (Companion::Instance->AddSubFileAsset(addNode, sym, CompressionType::None, 0)) {
+ symbolMap[id] = sym;
+ additive++;
+ SPDLOG_INFO("[BKAssetFactory] additive lang asset 0x{:X} -> {}", id, sym);
+ }
+ }
+ if (additive > 0) {
+ SPDLOG_INFO("[BKAssetFactory] emitted {} additive lang asset(s)", additive);
+ }
+ }
+
return std::make_shared<BKAssetData>(assetTableInfo, symbolMap);
}
+// Route a dialog-pack build to mods/lang/bk<region>.o2r when no explicit binary name is
+// given. Inert unless the rom config sets dialog_pack, so it's safe to call for any rom.
+void BKAssetFactory::PreprocessConfig(YAML::Node& cfg, N64::Cartridge* cart) {
+ if (!cfg || !cfg["dialog_pack"] || !cfg["dialog_pack"].as<bool>()) {
+ return;
+ }
+
+ std::string name;
+ if (cfg["output"] && cfg["output"]["binary"]) {
+ name = fs::path(cfg["output"]["binary"].as<std::string>()).filename().string();
+ }
+ if (name.empty() || name == "bk.o2r") {
+ std::string region = "pack";
+ if (cart != nullptr) {
+ switch (cart->GetCountry()) {
+ case N64::CountryCode::Europe:
+ region = "pal";
+ break;
+ case N64::CountryCode::Japan:
+ region = "jp";
+ break;
+ case N64::CountryCode::NorthAmerica:
+ region = "us";
+ break;
+ default:
+ break;
+ }
+ }
+ name = "bk" + region + ".o2r";
+ }
+
+ cfg["output"]["binary"] = "mods/lang/" + name;
+}
+
} // namespace BK64
diff --git a/src/factories/bk64/BKAssetFactory.h b/src/factories/bk64/BKAssetFactory.h
index 9061807..3786dc4 100644
--- a/src/factories/bk64/BKAssetFactory.h
+++ b/src/factories/bk64/BKAssetFactory.h
@@ -79,6 +79,12 @@ class BKAssetFactory : public BaseFactory {
bool HasModdedDependencies() override {
return true;
}
+
+ bool IsDialogPackRoot() const override {
+ return true;
+ }
+
+ void PreprocessConfig(YAML::Node& cfg, N64::Cartridge* cart) override;
};
} // namespace BK64
diff --git a/src/factories/bk64/SpriteFactory.cpp b/src/factories/bk64/SpriteFactory.cpp
index 6a14b6a..1313d03 100644
--- a/src/factories/bk64/SpriteFactory.cpp
+++ b/src/factories/bk64/SpriteFactory.cpp
@@ -351,4 +351,113 @@ std::optional<std::shared_ptr<IParsedData>> SpriteFactory::parse(std::vector<uin
unkA, animSpeed, animType, animDirection, animFlip);
}
+std::optional<std::shared_ptr<IParsedData>> SpriteFactory::parse_modding(std::vector<uint8_t>& buffer,
+ YAML::Node& node) {
+ YAML::Node root;
+ try {
+ root = YAML::Load(std::string(reinterpret_cast<char*>(buffer.data()), buffer.size()));
+ } catch (const YAML::ParserException& e) {
+ SPDLOG_ERROR("Failed to parse sprite modding yaml: {}", e.what());
+ return std::nullopt;
+ }
+ auto content = root.begin()->second;
+ auto frames = content["Frames"];
+ if (!frames || !frames.IsSequence() || frames.size() != 1) {
+ // Only single-frame sprites (fonts, banners) support yaml-driven (re)build;
+ // leave multi-frame sprites to the ROM parse.
+ auto base = this->parse(Companion::Instance->GetRomData(), node);
+ return base.has_value() ? std::optional(base.value()) : std::nullopt;
+ }
+ auto frame0 = frames[0];
+ const auto newCount = GetSafeNode<uint16_t>(frame0, "ChunkCount");
+ std::vector<std::pair<int16_t, int16_t>> positions;
+ auto chunks = frame0["Chunks"];
+ for (std::size_t i = 0; i < chunks.size(); i++) {
+ YAML::Node chunk = chunks[i];
+ positions.emplace_back(GetSafeNode<int16_t>(chunk, "X"), GetSafeNode<int16_t>(chunk, "Y"));
+ }
+ if (positions.empty() || positions.size() != newCount) {
+ SPDLOG_ERROR("Sprite modding: ChunkCount {} != Chunks listed {}", newCount, positions.size());
+ return std::nullopt;
+ }
+
+ const auto frameCount = GetSafeNode<int16_t>(content, "FrameCount", 1);
+ const auto formatCode = GetSafeNode<int16_t>(content, "FormatCode", static_cast<int16_t>(0x100));
+
+ // Header/anim/frame fields and the existing chunk count come from the ROM sprite — but
+ // an *additive* sprite (an id the ROM lacks, e.g. a language-pack world-name banner at
+ // 0x1600+) has no ROM sprite to read, so default them and supply every chunk from PNGs.
+ int16_t unk4 = 0, unk6 = 0, unk8 = 0, unkA = 0;
+ uint8_t animSpeed = 0, animType = 0, animDirection = 0, animFlip = 0;
+ std::vector<SpriteFrameHeader> frameHeaders;
+ uint16_t romCount = 0;
+ const bool additive = node["additive"] && node["additive"].as<bool>();
+ if (!additive) {
+ auto base = this->parse(Companion::Instance->GetRomData(), node);
+ if (!base.has_value()) {
+ return std::nullopt;
+ }
+ auto sprite = std::static_pointer_cast<SpriteData>(base.value());
+ unk4 = sprite->mUnk4;
+ unk6 = sprite->mUnk6;
+ unk8 = sprite->mUnk8;
+ unkA = sprite->mUnkA;
+ animSpeed = sprite->mAnimSpeed;
+ animType = sprite->mAnimType;
+ animDirection = sprite->mAnimDirection;
+ animFlip = sprite->mAnimFlip;
+ frameHeaders = sprite->mFrameHeaders;
+ romCount = static_cast<uint16_t>(sprite->mPositions.size());
+ } else {
+ // Additive: no ROM sprite, so take the header/frame fields from the yaml (a
+ // banner mirrors the JP layout); fields default to a frame sized to the chunk.
+ const auto i16 = [](int v) { return static_cast<int16_t>(v); };
+ unk4 = GetSafeNode<int16_t>(content, "Unk4", i16(0));
+ unk6 = GetSafeNode<int16_t>(content, "Unk6", i16(0));
+ unk8 = GetSafeNode<int16_t>(content, "Unk8", positions[0].first);
+ unkA = GetSafeNode<int16_t>(content, "UnkA", positions[0].second);
+ YAML::Node fh = frame0["FrameHeader"];
+ if (fh) {
+ frameHeaders.push_back({ GetSafeNode<int16_t>(fh, "x", i16(0)), GetSafeNode<int16_t>(fh, "y", i16(0)),
+ GetSafeNode<int16_t>(fh, "w", positions[0].first),
+ GetSafeNode<int16_t>(fh, "h", positions[0].second),
+ GetSafeNode<int16_t>(fh, "unkA", i16(0)), GetSafeNode<int16_t>(fh, "unkC", i16(0)),
+ GetSafeNode<int16_t>(fh, "unkE", i16(0)), GetSafeNode<int16_t>(fh, "unk10", i16(0)),
+ GetSafeNode<int16_t>(fh, "unk12", i16(0)) });
+ } else {
+ frameHeaders.push_back({ 0, 0, positions[0].first, positions[0].second, 0, 0, 0, 0, 0 });
+ }
+ }
+
+ std::string format;
+ switch (formatCode) {
+ case 0x1: format = "CI4"; break;
+ case 0x4: format = "CI8"; break;
+ case 0x20: format = "I4"; break;
+ case 0x40: format = "I8"; break;
+ case 0x80: format = "IA4"; break;
+ case 0x100: format = "IA8"; break;
+ case 0x400: format = "RGBA16"; break;
+ case 0x800: format = "RGBA32"; break;
+ default: format = "IA8"; break;
+ }
+
+ const auto symbol = GetSafeNode<std::string>(node, "symbol");
+ for (uint16_t i = romCount; i < newCount; i++) {
+ YAML::Node texture;
+ texture["type"] = "TEXTURE";
+ texture["offset"] = 0xF0000000u + i;
+ texture["format"] = format;
+ texture["ctype"] = "u16";
+ texture["width"] = additive ? frameHeaders[0].w : positions[i].first;
+ texture["height"] = additive ? frameHeaders[0].h : positions[i].second;
+ texture["symbol"] = symbol + "_0_" + std::to_string(i);
+ Companion::Instance->AddAsset(texture);
+ }
+
+ return std::make_shared<SpriteData>(frameCount, formatCode, std::vector<uint16_t>{ newCount }, positions,
+ frameHeaders, unk4, unk6, unk8, unkA, animSpeed, animType, animDirection,
+ animFlip);
+}
+
} // namespace BK64
diff --git a/src/factories/bk64/SpriteFactory.h b/src/factories/bk64/SpriteFactory.h
index a56a306..d00b7a4 100644
--- a/src/factories/bk64/SpriteFactory.h
+++ b/src/factories/bk64/SpriteFactory.h
@@ -79,6 +79,7 @@ class SpriteModdingExporter : public BaseExporter {
class SpriteFactory : public BaseFactory {
public:
std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::optional<std::shared_ptr<IParsedData>> parse_modding(std::vector<uint8_t>& buffer, YAML::Node& data) override;
inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
return { REGISTER(Header, SpriteHeaderExporter) REGISTER(Binary, SpriteBinaryExporter)
REGISTER(Code, SpriteCodeExporter) REGISTER(Modding, SpriteModdingExporter) };