diff options
| author | Jeod <47716344+JeodC@users.noreply.github.com> | 2026-06-21 21:56:17 -0400 |
|---|---|---|
| committer | Lywx <kiritodev01@gmail.com> | 2026-06-29 18:48:27 -0600 |
| commit | ec17e8fe49b7bcd9b181e5d0feb05da660c6f061 (patch) | |
| tree | 793d639c88b08191156a41fea23efa3dc8b8ed93 /src | |
| parent | d91a56479c0615231c93bb1623ca0ed8b771b831 (diff) | |
BK64: Add dialog pack support
Diffstat (limited to 'src')
| -rw-r--r-- | src/Companion.cpp | 61 | ||||
| -rw-r--r-- | src/Companion.h | 3 | ||||
| -rw-r--r-- | src/factories/BaseFactory.h | 4 | ||||
| -rw-r--r-- | src/factories/TextureFactory.cpp | 56 | ||||
| -rw-r--r-- | src/factories/bk64/BKAssetFactory.cpp | 292 | ||||
| -rw-r--r-- | src/factories/bk64/BKAssetFactory.h | 6 | ||||
| -rw-r--r-- | src/factories/bk64/SpriteFactory.cpp | 109 | ||||
| -rw-r--r-- | src/factories/bk64/SpriteFactory.h | 1 |
8 files changed, 494 insertions, 38 deletions
diff --git a/src/Companion.cpp b/src/Companion.cpp index 718e2db..e8ca2c0 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -709,6 +709,17 @@ void Companion::ProcessParseFile(YAML::Node root, std::atomic<size_t>& assetCoun continue; } + if (this->gConfig.dialogPack) { + bool isRoot = false; + if (assetNode["type"]) { + const auto factory = this->GetFactory(GetTypeNode(assetNode)); + isRoot = factory.has_value() && factory->get()->IsDialogPackRoot(); + } + if (!isRoot) { + continue; + } + } + this->gCurrentAssetName = "Parsing: " + entryName; if (gCurrentFileOffset && assetNode["offset"]) { @@ -762,7 +773,8 @@ void Companion::ProcessExportFile() { // no_export: still parse it for the side effects — VTX sub-refs, // companion-file registration — just don't write the body itself. // Whatever companion files it spun up still need to go out. - const bool noExport = result.node["no_export"] && result.node["no_export"].as<bool>(); + const bool noExport = (result.node["no_export"] && result.node["no_export"].as<bool>()) || + (this->gConfig.dialogPack && impl->IsDialogPackRoot()); if (!noExport) { stream.str(""); stream.clear(); @@ -1229,6 +1241,11 @@ void Companion::Process(std::atomic<size_t>& assetCount) { } auto cfg = rom["config"]; + // Let factories rewrite the config before processing (e.g. dialog-pack output + // routing). Each guards on its own keys, so this is a no-op for unrelated roms. + for (auto& [type, factory] : this->gFactories) { + factory->PreprocessConfig(cfg, this->gCartridge.get()); + } if (!cfg) { SPDLOG_ERROR("No config found for {}", !isDirectoryMode ? this->gCartridge->GetHash() : GetSafeNode<std::string>(config, "folder")); @@ -1288,6 +1305,9 @@ void Companion::Process(std::atomic<size_t>& assetCount) { } } this->gConfig.outputPath = output_path.string(); + if (auto outParent = output_path.parent_path(); !outParent.empty() && !exists(outParent)) { + create_directories(outParent); + } if (gbi) { auto key = gbi.as<std::string>(); @@ -1381,6 +1401,7 @@ void Companion::Process(std::atomic<size_t>& assetCount) { this->gConfig.textureDefines = cfg["textures"] && (cfg["textures"].as<std::string>() == "ADDITIONAL_DEFINES"); this->gConfig.includeAutogen = cfg["include_autogen"] && cfg["include_autogen"].as<bool>(); + this->gConfig.dialogPack = cfg["dialog_pack"] && cfg["dialog_pack"].as<bool>(); this->ParseHash(); @@ -1615,6 +1636,11 @@ void Companion::Process(std::atomic<size_t>& assetCount) { wrapper->AddFile("version", vWriter.ToVector()); vWriter.Close(); + // Extra top-level files a factory asked us to drop at the archive root. + for (const auto& [name, fileData] : this->gArchiveFiles) { + wrapper->AddFile(name, fileData); + } + this->gCurrentAssetName = "Writing archive to disk..."; wrapper->Close(); } @@ -1633,6 +1659,7 @@ void Companion::Process(std::atomic<size_t>& assetCount) { spdlog::set_pattern(regular); Decompressor::ClearCache(); + this->gArchiveFiles.clear(); this->gCartridge = nullptr; } @@ -1945,25 +1972,34 @@ std::optional<ParseResultData> Companion::GetParseDataByAddr(uint32_t addr) { } std::optional<ParseResultData> Companion::GetParseDataBySymbol(const std::string& symbol) { - if (CONTAINS(this->gParseResults, this->gCurrentFile)) { - for (auto& result : this->gParseResults[this->gCurrentFile]) { + auto searchBucket = [&](const std::string& file) -> std::optional<ParseResultData> { + auto it = this->gParseResults.find(file); + if (it == this->gParseResults.end()) { + return std::nullopt; + } + for (auto& result : it->second) { auto sym = GetNode<std::string>(result.node, "symbol"); - if (result.data.has_value() && sym.has_value() && sym.value() == symbol) { return result; } } - } + return std::nullopt; + }; + // Fast path: the current file and its declared externals. + if (auto r = searchBucket(this->gCurrentFile)) { + return r; + } for (auto& file : this->gCurrentExternalFiles) { - if (!CONTAINS(this->gParseResults, this->gCurrentFile)) { - SPDLOG_INFO("GetParseDataBySymbol: External File {} Not Found.", file); - continue; + if (auto r = searchBucket(file)) { + return r; } + } - for (auto& result : this->gParseResults[file]) { + // Global fallback to prevent racy lookups. + for (auto& [file, results] : this->gParseResults) { + for (auto& result : results) { auto sym = GetNode<std::string>(result.node, "symbol"); - if (result.data.has_value() && sym.has_value() && sym.value() == symbol) { return result; } @@ -2019,6 +2055,11 @@ void Companion::RegisterCompanionFile(const std::string path, std::vector<char> SPDLOG_TRACE("Registered companion file {}", path); } +void Companion::RegisterArchiveFile(const std::string& name, std::vector<char> data) { + this->gArchiveFiles.emplace_back(name, std::move(data)); + SPDLOG_TRACE("Registered archive root file {}", name); +} + std::string Companion::NormalizeAsset(const std::string& name) const { auto path = fs::path(this->gCurrentFile).stem().string() + "_" + name; return path; diff --git a/src/Companion.h b/src/Companion.h index f8bcd68..59d1940 100644 --- a/src/Companion.h +++ b/src/Companion.h @@ -209,6 +209,7 @@ public: std::string RelativePathToSrcDir(const std::string& path) const; std::string RelativePathToDestDir(const std::string& path) const; void RegisterCompanionFile(const std::string path, std::vector<char> data); + void RegisterArchiveFile(const std::string& name, std::vector<char> data); void SetAdditionalFiles(const std::vector<std::string>& files) { this->gAdditionalFiles = files; } void SetVersion(const std::string& version) { this->gVersion = version; } @@ -219,6 +220,7 @@ public: void SetProcess(bool shouldProcess); TorchConfig& GetConfig() { return this->gConfig; } BinaryWrapper* GetCurrentWrapper() { return this->gCurrentWrapper; } + const std::unordered_map<std::string, std::string>& GetModdedAssetPaths() const { return this->gModdedAssetPaths; } std::optional<std::tuple<std::string, YAML::Node>> RegisterAsset(const std::string& name, YAML::Node& node); std::optional<YAML::Node> AddSubFileAsset(YAML::Node asset, std::string newFileName, CompressionType newCompressionType, uint32_t compressedSize = 0); @@ -267,6 +269,7 @@ private: std::unordered_set<std::string> gProcessedFiles; std::unordered_map<std::string, std::vector<char>> gCompanionFiles; + std::vector<std::pair<std::string, std::vector<char>>> gArchiveFiles; std::unordered_map<std::string, std::vector<ParseResultData>> gParseResults; std::vector<std::string> gAdditionalFiles; 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) }; |
