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/factories/bk64/BKAssetFactory.cpp | |
| parent | d91a56479c0615231c93bb1623ca0ed8b771b831 (diff) | |
BK64: Add dialog pack support
Diffstat (limited to 'src/factories/bk64/BKAssetFactory.cpp')
| -rw-r--r-- | src/factories/bk64/BKAssetFactory.cpp | 292 |
1 files changed, 292 insertions, 0 deletions
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 |
