diff options
| author | Jeod <47716344+JeodC@users.noreply.github.com> | 2026-06-20 19:03:36 -0400 |
|---|---|---|
| committer | Lywx <kiritodev01@gmail.com> | 2026-06-29 18:48:27 -0600 |
| commit | d91a56479c0615231c93bb1623ca0ed8b771b831 (patch) | |
| tree | 05d60044555e2c09ca5f7167f11190087666848a /src/Companion.cpp | |
| parent | d0042dc6bfa12a6bee5702aa6b67f9a65873509c (diff) | |
Add BK64 asset extraction
Diffstat (limited to 'src/Companion.cpp')
| -rw-r--r-- | src/Companion.cpp | 243 |
1 files changed, 237 insertions, 6 deletions
diff --git a/src/Companion.cpp b/src/Companion.cpp index e449f8b..718e2db 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -12,6 +12,8 @@ #include <fstream> #include <iostream> #include <filesystem> +#include <thread> +#include <mutex> #include "factories/GenericArrayFactory.h" #include "factories/VtxFactory.h" @@ -93,6 +95,20 @@ #include "factories/fzerox/SoundFontFactory.h" #endif +#ifdef BK64_SUPPORT +#include "factories/bk64/AnimFactory.h" +#include "factories/bk64/BKAssetFactory.h" +#include "factories/bk64/DemoInputFactory.h" +#include "factories/bk64/DialogFactory.h" +#include "factories/bk64/GeoLayoutFactory.h" +#include "factories/bk64/GruntyQuestionFactory.h" +#include "factories/bk64/QuizQuestionFactory.h" +#include "factories/bk64/SpriteFactory.h" +#include "factories/bk64/ModelFactory.h" +#include "factories/bk64/MapFactory.h" +#include "factories/bk64/SoundfontTblFactory.h" +#endif + #ifdef MARIO_ARTIST_SUPPORT #include "factories/mario_artist/MA2D1Factory.h" #endif @@ -230,6 +246,20 @@ void Companion::Init(const ExportType type, std::atomic<size_t>& assetCount, boo this->RegisterFactory("FZX:SOUNDFONT", std::make_shared<FZX::SoundFontFactory>()); #endif +#ifdef BK64_SUPPORT + this->RegisterFactory("BK64:ANIM", std::make_shared<BK64::AnimFactory>()); + this->RegisterFactory("BK64:ASSET_TABLE", std::make_shared<BK64::BKAssetFactory>()); + this->RegisterFactory("BK64:DEMO", std::make_shared<BK64::DemoInputFactory>()); + this->RegisterFactory("BK64:DIALOG", std::make_shared<BK64::DialogFactory>()); + this->RegisterFactory("BK64:GEO_LAYOUT", std::make_shared<BK64::GeoLayoutFactory>()); + this->RegisterFactory("BK64:GRUNTYQ", std::make_shared<BK64::GruntyQuestionFactory>()); + this->RegisterFactory("BK64:MAP", std::make_shared<BK64::MapFactory>()); + this->RegisterFactory("BK64:QUIZQ", std::make_shared<BK64::QuizQuestionFactory>()); + this->RegisterFactory("BK64:MODEL", std::make_shared<BK64::ModelFactory>()); + this->RegisterFactory("BK64:SOUNDFONT_TBL", std::make_shared<BK64::SoundfontTblFactory>()); + this->RegisterFactory("BK64:SPRITE", std::make_shared<BK64::SpriteFactory>()); +#endif + #ifdef MARIO_ARTIST_SUPPORT this->RegisterFactory("MA:MA2D1", std::make_shared<MA::MA2D1Factory>()); #endif @@ -729,11 +759,17 @@ void Companion::ProcessExportFile() { try { switch (this->gConfig.exporterType) { case ExportType::Binary: { - stream.str(""); - stream.clear(); - exporter->get()->Export(stream, data, result.name, result.node, &result.name); - auto data = stream.str(); - this->gCurrentWrapper->AddFile(result.name, std::vector(data.begin(), data.end())); + // 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>(); + if (!noExport) { + stream.str(""); + stream.clear(); + exporter->get()->Export(stream, data, result.name, result.node, &result.name); + auto data = stream.str(); + this->gCurrentWrapper->AddFile(result.name, std::vector(data.begin(), data.end())); + } for (auto& entry : this->gCompanionFiles) { auto output = (this->gCurrentDirectory / entry.first).string(); @@ -1074,6 +1110,7 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) { // Stupid hack because the iteration broke the assets root = YAML::LoadFile(this->gCurrentFile); this->gConfig.segment.local.clear(); + this->gConfig.segment.compressed.clear(); this->gFileHeader.clear(); this->gCurrentPad = 0; this->gCurrentVram = std::nullopt; @@ -1083,7 +1120,7 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) { this->gCurrentFileOffset = 0; this->gTables.clear(); this->gCurrentExternalFiles.clear(); - this->gManualSegments.clear(); + this->gSubFileList.clear(); GFXDOverride::ClearVtx(); if (root[":config"]) { @@ -1271,6 +1308,9 @@ void Companion::Process(std::atomic<size_t>& assetCount) { } else if (key == "F3DEX_MK64") { this->gConfig.gbi.version = GBIVersion::f3dex; this->gConfig.gbi.subversion = GBIMinorVersion::Mk64; + } else if (key == "F3DEX_BK64") { + this->gConfig.gbi.version = GBIVersion::f3dex; + this->gConfig.gbi.subversion = GBIMinorVersion::BK64; } else { SPDLOG_ERROR("Invalid GBI version"); return; @@ -1340,6 +1380,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->ParseHash(); @@ -1432,6 +1473,119 @@ void Companion::Process(std::atomic<size_t>& assetCount) { if (mShouldProcess) { this->gProcessedFiles.insert(this->gCurrentFile); } + + // Sub-files were already parsed when they got created, so all that's left is export. + auto parentDir = this->gCurrentDirectory; + + if (this->gConfig.exporterType == ExportType::Modding || this->gConfig.exporterType == ExportType::XML) { + // Modding export is parallel. + std::mutex moddedPathsMutex; + std::mutex dirCreateMutex; + // Torch is already running on a worker thread, so leave a core for the parent. + const unsigned int hwThreads = std::thread::hardware_concurrency(); + const size_t numThreads = hwThreads > 1 ? hwThreads - 1 : 1u; + const size_t totalFiles = this->gSubFileList.size(); + SPDLOG_CRITICAL("Exporting {} sub-files using {} threads", totalFiles, numThreads); + + auto exportRange = [&](size_t start, size_t end) { + for (size_t si = start; si < end; si++) { + const auto subFile = this->gSubFileList[si]; + auto it = this->gParseResults.find(subFile); + if (it == this->gParseResults.end() || it->second.empty()) { + continue; + } + + auto localDir = parentDir / subFile; + + for (auto& result : it->second) { + const auto factory = this->GetFactory(result.type); + if (!factory.has_value()) + continue; + const auto impl = factory->get(); + const auto exporter = impl->GetExporter(this->gConfig.exporterType); + if (!exporter.has_value()) + continue; + + try { + std::ostringstream stream; + std::string ogname = result.name; + exporter->get()->Export(stream, result.data.value(), result.name, result.node, + &result.name); + + auto data = stream.str(); + if (data.empty()) + continue; + + std::string dpath = this->GetOutputPath() + "/" + result.name; + { + std::lock_guard<std::mutex> lock(dirCreateMutex); + if (!exists(fs::path(dpath).parent_path())) { + create_directories(fs::path(dpath).parent_path()); + } + } + + std::ofstream file(dpath, std::ios::binary); + file.write(data.c_str(), data.size()); + file.close(); + + { + std::lock_guard<std::mutex> lock(moddedPathsMutex); + this->gModdedAssetPaths[ogname] = result.name; + } + } catch (const std::exception& e) { + SPDLOG_ERROR("Sub-file export failed [{}] {}: {}", si, subFile, e.what()); + } catch (...) { SPDLOG_ERROR("Sub-file export crashed [{}] {}", si, subFile); } + } + + if (this->gAssetCounter) { + (*this->gAssetCounter)++; + } + } + }; + + std::vector<std::thread> threads; + size_t chunkSize = (totalFiles + numThreads - 1) / numThreads; + for (size_t t = 0; t < numThreads; t++) { + size_t start = t * chunkSize; + size_t end = std::min(start + chunkSize, totalFiles); + if (start < end) { + threads.emplace_back(exportRange, start, end); + } + } + for (auto& t : threads) { + t.join(); + } + SPDLOG_CRITICAL("Parallel export complete: {} modded assets", this->gModdedAssetPaths.size()); + + // gModdedAssetPaths is only filled once the threads finish, so write modding.yml here. + if (mShouldProcess) { + auto moddingPath = fs::path(this->gConfig.outputPath) / "modding.yml"; + YAML::Node modding; + for (const auto& [key, value] : this->gModdedAssetPaths) { + modding["assets"][key] = value; + } + std::ofstream moddingFile(moddingPath.string(), std::ios::binary); + moddingFile << modding; + moddingFile.close(); + } + } else { + // Binary/code/header all share wrapper state, so these have to go one at a time. + for (size_t si = 0; si < this->gSubFileList.size(); si++) { + const auto subFile = this->gSubFileList[si]; + this->gCurrentDirectory = parentDir / subFile; + this->gCurrentFile = subFile; + if (!this->gProcessedFiles.contains(subFile)) { + try { + ProcessExportFile(); + } catch (const std::exception& e) { + SPDLOG_ERROR("Sub-file export failed [{}] {}: {}", si, subFile, e.what()); + } catch (...) { SPDLOG_ERROR("Sub-file export crashed [{}] {}", si, subFile); } + if (mShouldProcess) { + this->gProcessedFiles.insert(subFile); + } + } + } + } } } @@ -1638,6 +1792,18 @@ std::optional<std::uint32_t> Companion::GetFileOffsetFromSegmentedAddr(const uin return std::nullopt; } +std::optional<std::pair<std::uint32_t, std::uint32_t>> +Companion::GetFileOffsetFromCompressedSegmentedAddr(const uint8_t segment) const { + + auto segments = this->gConfig.segment; + + if (segments.compressed[this->gCurrentFile].contains(segment)) { + return segments.compressed[this->gCurrentFile][segment]; + } + + return std::nullopt; +} + uint32_t Companion::PatchVirtualAddr(uint32_t addr) { if (addr & 0x80000000) { if (Torch::contains(gVirtualAddrMap, gCurrentFile)) { @@ -1658,6 +1824,13 @@ std::optional<std::tuple<std::string, YAML::Node>> Companion::GetNodeByAddr(uint addr = PatchVirtualAddr(addr); if (!Torch::contains(this->gAddrMap[this->gCurrentFile], addr)) { + auto realAddr = addr; + if (IS_SEGMENTED(addr) && GetCompressedSegmentOffset(&realAddr)) { + if (this->gAddrMap[this->gCurrentFile].contains(realAddr)) { + return this->gAddrMap[this->gCurrentFile][realAddr]; + } + } + for (auto& file : this->gCurrentExternalFiles) { if (!Torch::contains(this->gAddrMap, file)) { SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file); @@ -1904,6 +2077,48 @@ std::vector<char> Companion::ParseVersionString(const std::string& version) { return wv.ToVector(); } +std::optional<YAML::Node> Companion::AddSubFileAsset(YAML::Node asset, std::string newFileName, + CompressionType newCompressionType, uint32_t compressedSize) { + if (!asset["offset"] || !asset["type"]) { + return std::nullopt; + } + + if (this->gParseResults.contains(newFileName) || this->gProcessedFiles.contains(newFileName)) { + SPDLOG_WARN("File with name {} already exists, skipping..", newFileName); + return std::nullopt; + } + + auto addr = GetSafeNode<uint32_t>(asset, "offset"); + asset["offset"] = 0; + + auto oldFile = this->gCurrentFile; + auto oldVram = this->gCurrentVram; + auto oldCompressionType = this->gCurrentCompressionType; + auto oldCompressedSize = this->gCurrentCompressedSize; + this->gSubFileList.push_back(newFileName); + this->gCurrentAssetName = "Parsing: " + newFileName; + if (this->gAssetCounter) { + (*this->gAssetCounter)++; + } + + this->gCurrentFile = newFileName; + this->gCurrentVram = { 0, addr }; + this->gCurrentCompressionType = newCompressionType; + if (newCompressionType == CompressionType::BKZIP) { + this->gCurrentCompressedSize = compressedSize; + } + + auto result = this->AddAsset(asset); + + // ...and put the parent file's state back the way we found it. + this->gCurrentFile = oldFile; + this->gCurrentVram = oldVram; + this->gCurrentCompressionType = oldCompressionType; + this->gCurrentCompressedSize = oldCompressedSize; + + return result; +} + std::optional<YAML::Node> Companion::AddAsset(YAML::Node asset) { if (!asset["offset"] || !asset["type"]) { return std::nullopt; @@ -1961,3 +2176,19 @@ std::optional<YAML::Node> Companion::AddAsset(YAML::Node asset) { return std::nullopt; } + +void Companion::SetCompressedSegment(uint32_t segmentId, uint32_t compressedFileOffset, uint32_t offset) { + this->gConfig.segment.compressed[this->gCurrentFile][segmentId] = std::make_pair(compressedFileOffset, offset); +} + +bool Companion::GetCompressedSegmentOffset(uint32_t* addr) { + if (IS_SEGMENTED(*addr)) { + const auto compressedSegmentPair = + Companion::Instance->GetFileOffsetFromCompressedSegmentedAddr(SEGMENT_NUMBER(*addr)); + if (compressedSegmentPair.has_value()) { + *addr = compressedSegmentPair.value().second + SEGMENT_OFFSET(*addr); + return true; + } + } + return false; +} |
