summaryrefslogtreecommitdiff
path: root/src/factories/bk64/BKAssetFactory.cpp
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/bk64/BKAssetFactory.cpp
parentd91a56479c0615231c93bb1623ca0ed8b771b831 (diff)
BK64: Add dialog pack support
Diffstat (limited to 'src/factories/bk64/BKAssetFactory.cpp')
-rw-r--r--src/factories/bk64/BKAssetFactory.cpp292
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