summaryrefslogtreecommitdiff
path: root/src/factories/bk64/BKAssetFactory.cpp
diff options
context:
space:
mode:
authorLywx <kiritodev01@gmail.com>2026-07-02 20:42:38 -0600
committerGitHub <noreply@github.com>2026-07-02 20:42:38 -0600
commit58527d7bf8778bc67179454a19ad8c84a74aff13 (patch)
tree37df9df3eb558977b84b5a33b8143f3859c5d057 /src/factories/bk64/BKAssetFactory.cpp
parentb42a4fa16f7a75e2a96a7c107e6359c80b685863 (diff)
Implemented UI Framework (#225)
* Implemented base renderer among main UI framework implementation * Implemented basic geo layout rendering * Implemented animation support among fixing lightning among other things * Implemented multiple ui factories for sm64 * Implemented first iteration of the custom sequence driver * Added export buttons on multiple factories * More sequence player fixes among f3dex2 working correctly * Implemented pm64 viewers * Implemented audio support for pm64 * Fully implemented pm64 factories * Implemented more sm64 factories * Added tons of missing sf64 factories * Implemented all assets among sf64 wrong padding * Run formatting * Fixed wrong spacing on gfx too * Fixed CI * Fixed bad pipeline
Diffstat (limited to 'src/factories/bk64/BKAssetFactory.cpp')
-rw-r--r--src/factories/bk64/BKAssetFactory.cpp440
1 files changed, 218 insertions, 222 deletions
diff --git a/src/factories/bk64/BKAssetFactory.cpp b/src/factories/bk64/BKAssetFactory.cpp
index b109c17..6a0d903 100644
--- a/src/factories/bk64/BKAssetFactory.cpp
+++ b/src/factories/bk64/BKAssetFactory.cpp
@@ -191,8 +191,7 @@ static void RegisterLangInfo(YAML::Node& node) {
if (langNode && langNode.IsSequence()) {
for (auto entry : langNode) {
- langs.emplace_back(GetSafeNode<std::string>(entry, "name"),
- GetSafeNode<uint32_t>(entry, "index", 0),
+ langs.emplace_back(GetSafeNode<std::string>(entry, "name"), GetSafeNode<uint32_t>(entry, "index", 0),
GetSafeNode<uint32_t>(entry, "script", 0));
}
} else if (cfg.dialogPack) {
@@ -259,8 +258,7 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
RegisterLangInfo(node);
// Resolve the pack's language folder.
- const std::string langRegion =
- Companion::Instance->GetConfig().dialogPack ? ResolveLangRegion() : std::string();
+ const std::string langRegion = Companion::Instance->GetConfig().dialogPack ? ResolveLangRegion() : std::string();
// Romhack o2rs overlay the base bk.o2r, whose manifest already lives there: walk the
// table for side effects (BB config, modified-slot registration) but don't output a
@@ -362,246 +360,245 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
size_t parseFailures = 0;
for (uint32_t i = 0; i < assetCount - 1; i++) {
- try {
- auto assetInfo = assetTableInfo.at(i);
-
- // Size is the gap to the next asset's offset.
- uint32_t assetSize = assetTableInfo.at(i + 1).offset - assetInfo.offset;
-
- // Same offset means an empty slot sits in between; skip ahead until the
- // offset actually changes to find the real boundary.
- if (assetSize == 0) {
- for (uint32_t j = i + 2; j < assetCount; j++) {
- if (assetTableInfo.at(j).offset != assetInfo.offset) {
- assetSize = assetTableInfo.at(j).offset - assetInfo.offset;
- break;
+ try {
+ auto assetInfo = assetTableInfo.at(i);
+
+ // Size is the gap to the next asset's offset.
+ uint32_t assetSize = assetTableInfo.at(i + 1).offset - assetInfo.offset;
+
+ // Same offset means an empty slot sits in between; skip ahead until the
+ // offset actually changes to find the real boundary.
+ if (assetSize == 0) {
+ for (uint32_t j = i + 2; j < assetCount; j++) {
+ if (assetTableInfo.at(j).offset != assetInfo.offset) {
+ assetSize = assetTableInfo.at(j).offset - assetInfo.offset;
+ break;
+ }
}
}
- }
- auto assetOffset = dataStartRomOffset + assetInfo.offset;
- BKAssetType assetType;
+ auto assetOffset = dataStartRomOffset + assetInfo.offset;
+ BKAssetType assetType;
- if (assetInfo.tFlag == 4) {
- continue;
- }
+ if (assetInfo.tFlag == 4) {
+ continue;
+ }
- if (assetOffset + assetSize > buffer.size()) {
- SPDLOG_ERROR("Asset {} offset 0x{:X} + size 0x{:X} = 0x{:X} exceeds ROM "
- "buffer 0x{:X}",
- assetInfo.index, assetOffset, assetSize, assetOffset + assetSize, buffer.size());
- continue;
- }
+ if (assetOffset + assetSize > buffer.size()) {
+ SPDLOG_ERROR("Asset {} offset 0x{:X} + size 0x{:X} = 0x{:X} exceeds ROM "
+ "buffer 0x{:X}",
+ assetInfo.index, assetOffset, assetSize, assetOffset + assetSize, buffer.size());
+ continue;
+ }
- SPDLOG_TRACE("Parsing asset {} mode={} offset=0x{:X} size=0x{:X} comp={}", assetInfo.index, assetInfo.assetMode,
- assetOffset, assetSize, assetInfo.compressionFlag);
-
- if (romhackMode && assetSize > 0) {
- const std::string& baselineHash = GetBaselineAssetHash(assetInfo.index);
- if (!baselineHash.empty()) {
- if (assetOffset + assetSize <= rom.size()) {
- sha1::SHA1 s;
- s.processBytes(rom.data() + assetOffset, assetSize);
- uint32_t digest[5];
- s.getDigest(digest);
- char buf[41];
- std::snprintf(buf, sizeof(buf), "%08x%08x%08x%08x%08x", digest[0], digest[1], digest[2], digest[3],
- digest[4]);
- if (baselineHash == buf) {
- skipHits++;
- if ((skipHits % 500) == 0) {
- SPDLOG_WARN("Romhack hash-skip progress: {} hits, {} misses so far", skipHits, skipMisses);
+ SPDLOG_TRACE("Parsing asset {} mode={} offset=0x{:X} size=0x{:X} comp={}", assetInfo.index,
+ assetInfo.assetMode, assetOffset, assetSize, assetInfo.compressionFlag);
+
+ if (romhackMode && assetSize > 0) {
+ const std::string& baselineHash = GetBaselineAssetHash(assetInfo.index);
+ if (!baselineHash.empty()) {
+ if (assetOffset + assetSize <= rom.size()) {
+ sha1::SHA1 s;
+ s.processBytes(rom.data() + assetOffset, assetSize);
+ uint32_t digest[5];
+ s.getDigest(digest);
+ char buf[41];
+ std::snprintf(buf, sizeof(buf), "%08x%08x%08x%08x%08x", digest[0], digest[1], digest[2],
+ digest[3], digest[4]);
+ if (baselineHash == buf) {
+ skipHits++;
+ if ((skipHits % 500) == 0) {
+ SPDLOG_WARN("Romhack hash-skip progress: {} hits, {} misses so far", skipHits,
+ skipMisses);
+ }
+ continue;
}
- continue;
+ skipMisses++;
}
- skipMisses++;
}
}
- }
- switch (assetInfo.assetMode) {
- case 0:
- assetType = BKAssetType::Animation;
- break;
- case 1:
- case 3:
- case 7: {
- uint8_t* dataBuf;
- if (assetInfo.compressionFlag != 0) {
- DataChunk* uncompressedData =
- Decompressor::Decode(buffer, assetOffset, CompressionType::BKZIP, assetSize);
- dataBuf = uncompressedData->data;
- } else {
- dataBuf = buffer.data() + assetOffset;
+ switch (assetInfo.assetMode) {
+ case 0:
+ assetType = BKAssetType::Animation;
+ break;
+ case 1:
+ case 3:
+ case 7: {
+ uint8_t* dataBuf;
+ if (assetInfo.compressionFlag != 0) {
+ DataChunk* uncompressedData =
+ Decompressor::Decode(buffer, assetOffset, CompressionType::BKZIP, assetSize);
+ dataBuf = uncompressedData->data;
+ } else {
+ dataBuf = buffer.data() + assetOffset;
+ }
+ if (dataBuf[0] == 0 && dataBuf[1] == 0 && dataBuf[2] == 0 && dataBuf[3] == 11) {
+ assetType = BKAssetType::Model;
+ } else {
+ assetType = BKAssetType::Sprite;
+ }
+ break;
}
- if (dataBuf[0] == 0 && dataBuf[1] == 0 && dataBuf[2] == 0 && dataBuf[3] == 11) {
- assetType = BKAssetType::Model;
- } else {
- assetType = BKAssetType::Sprite;
+ case 2:
+ assetType = BKAssetType::Map;
+ break;
+ case 4: {
+ uint8_t* dataBuf;
+ if (assetInfo.compressionFlag != 0) {
+ DataChunk* uncompressedData =
+ Decompressor::Decode(buffer, assetOffset, CompressionType::BKZIP, assetSize);
+ dataBuf = uncompressedData->data;
+ } else {
+ dataBuf = buffer.data() + assetOffset;
+ }
+ // US headers
+ if (dataBuf[0] == 1 && dataBuf[1] == 1 && dataBuf[2] == 2 && dataBuf[3] == 5 && dataBuf[4] == 0) {
+ assetType = BKAssetType::QuizQuestion;
+ } else if (dataBuf[0] == 1 && dataBuf[1] == 3 && dataBuf[2] == 0 && dataBuf[3] == 5 &&
+ dataBuf[4] == 0) {
+ assetType = BKAssetType::GruntyQuestion;
+ } else if (dataBuf[0] == 1 && dataBuf[1] == 3 && dataBuf[2] == 0) {
+ assetType = BKAssetType::Dialog;
+ // PAL headers
+ } else if (dataBuf[0] == 3 && dataBuf[1] == 1 && dataBuf[2] == 2) {
+ assetType = BKAssetType::QuizQuestion;
+ } else if (dataBuf[0] == 3 && dataBuf[1] == 3 && dataBuf[2] == 0) {
+ assetType = BKAssetType::GruntyQuestion;
+ } else if (dataBuf[0] == 3 && dataBuf[1] == 7 && dataBuf[2] == 0) {
+ assetType = BKAssetType::Dialog;
+ } else {
+ assetType = BKAssetType::DemoInput;
+ }
+ break;
}
- break;
+ case 5:
+ assetType = BKAssetType::Model;
+ break;
+ case 6:
+ assetType = BKAssetType::Midi;
+ break;
+ default:
+ assetType = BKAssetType::Binary;
+ break;
}
- case 2:
- assetType = BKAssetType::Map;
- break;
- case 4: {
- uint8_t* dataBuf;
- if (assetInfo.compressionFlag != 0) {
- DataChunk* uncompressedData =
- Decompressor::Decode(buffer, assetOffset, CompressionType::BKZIP, assetSize);
- dataBuf = uncompressedData->data;
- } else {
- dataBuf = buffer.data() + assetOffset;
+
+ // 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);
}
- // US headers
- if (dataBuf[0] == 1 && dataBuf[1] == 1 && dataBuf[2] == 2 && dataBuf[3] == 5 && dataBuf[4] == 0) {
- assetType = BKAssetType::QuizQuestion;
- } else if (dataBuf[0] == 1 && dataBuf[1] == 3 && dataBuf[2] == 0 && dataBuf[3] == 5 &&
- dataBuf[4] == 0) {
- assetType = BKAssetType::GruntyQuestion;
- } else if (dataBuf[0] == 1 && dataBuf[1] == 3 && dataBuf[2] == 0) {
- assetType = BKAssetType::Dialog;
- // PAL headers
- } else if (dataBuf[0] == 3 && dataBuf[1] == 1 && dataBuf[2] == 2) {
- assetType = BKAssetType::QuizQuestion;
- } else if (dataBuf[0] == 3 && dataBuf[1] == 3 && dataBuf[2] == 0) {
- assetType = BKAssetType::GruntyQuestion;
- } else if (dataBuf[0] == 3 && dataBuf[1] == 7 && dataBuf[2] == 0) {
- assetType = BKAssetType::Dialog;
- } else {
- assetType = BKAssetType::DemoInput;
+ if (!isText && !isLangAsset) {
+ continue;
}
- break;
}
- case 5:
- assetType = BKAssetType::Model;
- break;
- case 6:
- assetType = BKAssetType::Midi;
- break;
- default:
- assetType = BKAssetType::Binary;
- 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);
+ std::string assetSymbol;
+ std::string assetIndexStr = std::to_string(assetInfo.index);
- if (symbolMapExists && node["symbol_map"][assetIndexStr]) {
- assetSymbol = node["symbol_map"][assetIndexStr].as<std::string>();
- } else {
- std::stringstream assetStream;
+ if (symbolMapExists && node["symbol_map"][assetIndexStr]) {
+ assetSymbol = node["symbol_map"][assetIndexStr].as<std::string>();
+ } else {
+ std::stringstream assetStream;
- assetStream << "D_" << sAssetSymbolPrefixes.at(assetType) << "_" << std::to_string(assetInfo.index);
+ assetStream << "D_" << sAssetSymbolPrefixes.at(assetType) << "_" << std::to_string(assetInfo.index);
- assetSymbol = assetStream.str();
- }
+ assetSymbol = assetStream.str();
+ }
- if (Companion::Instance->GetConfig().dialogPack) {
- assetSymbol = "lang/" + langRegion + "/" + assetSymbol;
- }
+ if (Companion::Instance->GetConfig().dialogPack) {
+ assetSymbol = "lang/" + langRegion + "/" + assetSymbol;
+ }
- symbolMap[assetInfo.index] = assetSymbol;
+ symbolMap[assetInfo.index] = assetSymbol;
- YAML::Node bkAssetNode;
- bkAssetNode["offset"] = assetOffset;
- bkAssetNode["symbol"] = assetSymbol;
- CompressionType compressionType =
- (assetInfo.compressionFlag != 0) ? CompressionType::BKZIP : CompressionType::None;
+ YAML::Node bkAssetNode;
+ bkAssetNode["offset"] = assetOffset;
+ bkAssetNode["symbol"] = assetSymbol;
+ CompressionType compressionType =
+ (assetInfo.compressionFlag != 0) ? CompressionType::BKZIP : CompressionType::None;
- switch (assetType) {
- case BKAssetType::Animation:
- bkAssetNode["type"] = "BK64:ANIM";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::Binary:
- bkAssetNode["type"] = "BLOB";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::DemoInput:
- bkAssetNode["type"] = "BK64:DEMO";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::Dialog:
- bkAssetNode["type"] = "BK64:DIALOG";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::GruntyQuestion:
- bkAssetNode["type"] = "BK64:GRUNTYQ";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::Map:
- bkAssetNode["type"] = "BK64:MAP";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::Midi:
- bkAssetNode["type"] = "BLOB";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::Model:
- bkAssetNode["type"] = "BK64:MODEL";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::QuizQuestion:
- bkAssetNode["type"] = "BK64:QUIZQ";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- case BKAssetType::Sprite:
- bkAssetNode["type"] = "BK64:SPRITE";
- Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
- break;
- default:
- // We assigned assetType ourselves, so getting here means a bug.
- throw std::runtime_error("Invalid BKAsset Type Found");
+ switch (assetType) {
+ case BKAssetType::Animation:
+ bkAssetNode["type"] = "BK64:ANIM";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::Binary:
+ bkAssetNode["type"] = "BLOB";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::DemoInput:
+ bkAssetNode["type"] = "BK64:DEMO";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::Dialog:
+ bkAssetNode["type"] = "BK64:DIALOG";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::GruntyQuestion:
+ bkAssetNode["type"] = "BK64:GRUNTYQ";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::Map:
+ bkAssetNode["type"] = "BK64:MAP";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::Midi:
+ bkAssetNode["type"] = "BLOB";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::Model:
+ bkAssetNode["type"] = "BK64:MODEL";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::QuizQuestion:
+ bkAssetNode["type"] = "BK64:QUIZQ";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ case BKAssetType::Sprite:
+ bkAssetNode["type"] = "BK64:SPRITE";
+ Companion::Instance->AddSubFileAsset(bkAssetNode, assetSymbol, compressionType, assetSize);
+ break;
+ default:
+ // We assigned assetType ourselves, so getting here means a bug.
+ throw std::runtime_error("Invalid BKAsset Type Found");
+ }
+ } catch (const std::exception& e) {
+ parseFailures++;
+ const auto& bad = assetTableInfo.at(i);
+ SPDLOG_ERROR("[BKAssetFactory] skipping slot {} (compFlag={} tFlag={} mode={} offset=0x{:X}): {}", i,
+ bad.compressionFlag, bad.tFlag, bad.assetMode, dataStartRomOffset + bad.offset, e.what());
}
- } catch (const std::exception& e) {
- parseFailures++;
- const auto& bad = assetTableInfo.at(i);
- SPDLOG_ERROR("[BKAssetFactory] skipping slot {} (compFlag={} tFlag={} mode={} offset=0x{:X}): {}", i,
- bad.compressionFlag, bad.tFlag, bad.assetMode,
- dataStartRomOffset + bad.offset, e.what());
- }
}
if (romhackMode) {
@@ -662,13 +659,12 @@ std::optional<std::shared_ptr<IParsedData>> BKAssetFactory::parse(std::vector<ui
}
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);
+ 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;
- }
+ } catch (const std::exception&) { continue; }
if (symbolMap.count(id)) {
continue; // the base table already covers this slot
}