diff options
Diffstat (limited to 'src')
27 files changed, 6339 insertions, 22 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; +} diff --git a/src/archive/ZWrapper.cpp b/src/archive/ZWrapper.cpp index 330d35e..480dafc 100644 --- a/src/archive/ZWrapper.cpp +++ b/src/archive/ZWrapper.cpp @@ -34,7 +34,13 @@ bool ZWrapper::AddFile(const std::string& path, std::vector<char> data) { stream.close(); } - this->mZip->writebytes(path, data); + try { + this->mZip->writebytes(path, data); + } catch (const std::exception& e) { + // miniz's own error says nothing useful, so log which entry blew up and how big. + SPDLOG_ERROR("[ZWrapper] Failed to add '{}' ({} bytes): {}", path, fileSize, e.what()); + throw; + } return true; } diff --git a/src/factories/DisplayListFactory.cpp b/src/factories/DisplayListFactory.cpp index 60344f6..144d666 100644 --- a/src/factories/DisplayListFactory.cpp +++ b/src/factories/DisplayListFactory.cpp @@ -4,6 +4,7 @@ #include "spdlog/spdlog.h" #include "Companion.h" #include <fstream> +#include <tuple> #include "n64/gbi-otr.h" #ifdef STANDALONE @@ -195,21 +196,32 @@ void DebugDisplayList(uint32_t w0, uint32_t w1) { #endif std::optional<std::tuple<std::string, YAML::Node>> SearchVtx(uint32_t ptr) { - auto decs = Companion::Instance->GetNodesByType("VTX"); - - if (!decs.has_value()) { + const auto* decs = Companion::Instance->GetNodesByTypeRef("VTX", Companion::Instance->GetConfig().includeAutogen); + if (decs == nullptr) { return std::nullopt; } - for (auto& dec : decs.value()) { - auto [name, node] = dec; + auto realAddr = ptr; + bool hasRealAddr = false; + if (IS_SEGMENTED(realAddr) && Companion::Instance->GetCompressedSegmentOffset(&realAddr)) { + hasRealAddr = true; + } + + for (const auto& dec : *decs) { + const auto& [name, node] = dec; - auto offset = GetSafeNode<uint32_t>(node, "offset"); - auto count = GetSafeNode<uint32_t>(node, "count"); + auto offset = GetSafeNode<uint32_t>(const_cast<YAML::Node&>(node), "offset"); + auto count = GetSafeNode<uint32_t>(const_cast<YAML::Node&>(node), "count"); auto end = ALIGN16((count * sizeof(N64Vtx_t))); - if (ptr > offset && ptr < offset + end) { - return std::make_tuple(GetSafeNode<std::string>(node, "symbol", name), node); + // ptr == offset just means the block's first vertex — still a valid hit. + // The old strict > missed it, and that's what spawned every bogus + // "_seg1_vtx_0" autogen. + if (ptr >= offset && ptr < offset + end) { + return std::make_tuple(GetSafeNode<std::string>(const_cast<YAML::Node&>(node), "symbol", name), node); + } + if (hasRealAddr && realAddr >= offset && realAddr < offset + end) { + return std::make_tuple(GetSafeNode<std::string>(const_cast<YAML::Node&>(node), "symbol", name), node); } } @@ -263,6 +275,14 @@ ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr<IP auto ptr = Companion::Instance->PatchVirtualAddr(w1); auto overlap = GFXDOverride::GetVtxOverlap(ptr); + if (!overlap.has_value() && IS_SEGMENTED(ptr)) { + uint32_t flatPtr = ptr; + if (Companion::Instance->GetCompressedSegmentOffset(&flatPtr)) { + overlap = GFXDOverride::GetVtxOverlap(flatPtr); + if (overlap.has_value()) + ptr = flatPtr; + } + } if (overlap.has_value()) { auto ovnode = std::get<1>(overlap.value()); auto path = Companion::Instance->RelativePath(std::get<0>(overlap.value())); @@ -325,9 +345,15 @@ ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr<IP auto branch = (w0 >> 16) & G_DL_NO_PUSH; // Export displaylist segment addresses as an index into a buffer of gfx - value = gsSPDisplayListOTRHash(ptr); - w0 = value.words.w0; - w1 = value.words.w1; + if ((Companion::Instance->GetGBIMinorVersion() == GBIMinorVersion::Mk64) && (SEGMENT_NUMBER(w1) == 0x07)) { + value = gsSPDisplayListOTRIndex(w1); + w0 = value.words.w0; + w1 = value.words.w1; + } else { + value = gsSPDisplayListOTRHash(ptr); + w0 = value.words.w0; + w1 = value.words.w1; + } writer.Write(w0); writer.Write(w1); @@ -413,6 +439,19 @@ ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr<IP uint32_t newW0 = (G_SETTIMG_OTR_HASH << 24) | (w0 & 0x00FFFFFF); writer.Write(newW0); writer.Write(ptr); + } else if ((Companion::Instance->GetGBIMinorVersion() == GBIMinorVersion::Mk64) && + ((SEGMENT_NUMBER(w1) == 0x03) || (SEGMENT_NUMBER(w1) == 0x05))) { + // Export texture segment addresses as segmented addresses + w1 |= 1; + writer.Write(w0); + writer.Write(w1); + // Segment 4 and 0x0B-0x0F are BK64's runtime animated-texture slots. + // Nothing static to resolve, so the segmented address just passes through. + } else if ((Companion::Instance->GetGBIMinorVersion() == GBIMinorVersion::BK64) && + (SEGMENT_NUMBER(w1) == 0x04 || (SEGMENT_NUMBER(w1) >= 0x0B && SEGMENT_NUMBER(w1) <= 0x0F))) { + w1 |= 1; + writer.Write(w0); + writer.Write(w1); } else { // Export texture segment addresses as segmented addresses N64Gfx value = gsDPSetTextureOTRImage(C0(21, 3), C0(19, 2), C0(0, 10), ptr); @@ -592,6 +631,13 @@ std::optional<std::shared_ptr<IParsedData>> DListFactory::parse(std::vector<uint SPDLOG_INFO("Found vtx at 0x{:X} matching last vtx at 0x{:X}", adjPtr, lOffset); GFXDOverride::RegisterVTXOverlap(adjPtr, search.value()); } + + if (IS_SEGMENTED(adjPtr) && Companion::Instance->GetCompressedSegmentOffset(&adjPtr)) { + if (adjPtr > lOffset && adjPtr < lOffset + lSize) { + SPDLOG_INFO("Found vtx at 0x{:X} matching last vtx at 0x{:X}", adjPtr, lOffset); + GFXDOverride::RegisterVTXOverlap(adjPtr, search.value()); + } + } } else { YAML::Node vtx; vtx["type"] = "VTX"; @@ -600,7 +646,7 @@ std::optional<std::shared_ptr<IParsedData>> DListFactory::parse(std::vector<uint Companion::Instance->AddAsset(vtx); } } else { - SPDLOG_WARN("Found vtx at 0x{:X}", w1); + SPDLOG_INFO("Found registered vtx at 0x{:X}", w1); } } diff --git a/src/factories/DisplayListOverrides.cpp b/src/factories/DisplayListOverrides.cpp index 45bd1a9..2c82481 100644 --- a/src/factories/DisplayListOverrides.cpp +++ b/src/factories/DisplayListOverrides.cpp @@ -73,6 +73,26 @@ int Vtx(uint32_t ptr, int32_t num) { return 1; } + if (IS_SEGMENTED(ptr) && Companion::Instance->GetCompressedSegmentOffset(&ptr)) { + vtx = GetVtxOverlap(ptr); + if (vtx.has_value()) { + auto symbol = std::get<0>(vtx.value()); + auto node = std::get<1>(vtx.value()); + + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto count = GetSafeNode<uint32_t>(node, "count"); + auto idx = (ptr - offset) / sizeof(N64Vtx_t); + + SPDLOG_INFO("Replaced Vtx Overlapped: 0x{:X} Symbol: {}", ptr, symbol); + gfxd_puts("&"); + gfxd_puts(symbol.c_str()); + gfxd_puts("["); + gfxd_puts(std::to_string(idx).c_str()); + gfxd_puts("]"); + return 1; + } + } + auto dec = Companion::Instance->GetSafeNodeByAddr(ptr, "VTX"); if (dec.has_value()) { diff --git a/src/factories/TextureFactory.cpp b/src/factories/TextureFactory.cpp index c436ca6..80e0fa9 100644 --- a/src/factories/TextureFactory.cpp +++ b/src/factories/TextureFactory.cpp @@ -243,8 +243,8 @@ ExportResult TextureModdingExporter::Export(std::ostream& write, std::shared_ptr palTexture->mFormat.depth); } else { auto symbol = GetSafeNode<std::string>(node, "symbol"); - throw std::runtime_error("Could not convert ci8 '" + symbol + - "' the tlut symbol name is probably wrong for tlut_symbol node"); + throw std::runtime_error("Could not convert ci8 '" + symbol + "' the tlut symbol name " + tlut + + " is probably wrong for tlut_symbol node"); } break; } diff --git a/src/factories/bk64/AnimFactory.cpp b/src/factories/bk64/AnimFactory.cpp new file mode 100644 index 0000000..b45e342 --- /dev/null +++ b/src/factories/bk64/AnimFactory.cpp @@ -0,0 +1,212 @@ +#include "AnimFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" + +namespace BK64 { + +ExportResult AnimHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult AnimCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto anim = std::static_pointer_cast<AnimData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "AnimationFile " << symbol << "_File = { " << anim->mStartFrame << ", " << anim->mEndFrame << ", " + << anim->mFiles.size() << " };\n\n"; + + write << "AnimationFileElement " << symbol << "_Data[] = {\n"; + for (const auto& file : anim->mFiles) { + write << fourSpaceTab << "{\n"; + write << fourSpaceTab << fourSpaceTab << file.mBoneIndex << ", " << file.mTransformType << ", " + << file.mData.size() << ",\n"; + for (const auto& fileData : file.mData) { + write << fourSpaceTab << fourSpaceTab << "{ "; + write << (uint32_t)fileData.unk0_15 << ", " << (uint32_t)fileData.unk0_14 << ", " << fileData.unk0_13 + << ", " << fileData.unk2; + write << " },\n"; + } + write << fourSpaceTab << "},\n"; + } + + write << "};\n\n"; + + return offset; +} + +ExportResult BK64::AnimBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto anim = std::static_pointer_cast<AnimData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKAnimation, 0); + + writer.Write(anim->mStartFrame); + writer.Write(anim->mEndFrame); + writer.Write((uint32_t)anim->mFiles.size()); + + for (const auto& file : anim->mFiles) { + writer.Write(file.mBoneIndex); + writer.Write(file.mTransformType); + writer.Write((uint32_t)file.mData.size()); + for (const auto& fileData : file.mData) { + writer.Write(static_cast<uint8_t>(fileData.unk0_15)); + writer.Write(static_cast<uint8_t>(fileData.unk0_14)); + writer.Write(static_cast<uint16_t>(fileData.unk0_13)); + writer.Write(fileData.unk2); + } + } + + writer.Finish(write); + return std::nullopt; +} + +ExportResult BK64::AnimModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto anim = std::static_pointer_cast<AnimData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "StartFrame"; + out << YAML::Value << anim->mStartFrame; + out << YAML::Key << "EndFrame"; + out << YAML::Value << anim->mEndFrame; + out << YAML::Key << "Elements"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto& file : anim->mFiles) { + out << YAML::BeginMap; + out << YAML::Key << "BoneIndex"; + out << YAML::Value << file.mBoneIndex; + out << YAML::Key << "TransformType"; + out << YAML::Value << file.mTransformType; + out << YAML::Key << "Data"; + out << YAML::Value; + out << YAML::BeginSeq; + for (const auto& fileData : file.mData) { + out << YAML::Flow << YAML::BeginSeq; + out << YAML::Value << (uint32_t)fileData.unk0_15; + out << YAML::Value << (uint32_t)fileData.unk0_14; + out << YAML::Value << fileData.unk0_13; + out << YAML::Value << fileData.unk2; + out << YAML::EndSeq; + } + out << YAML::EndSeq; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> AnimFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + + int16_t startFrame = reader.ReadInt16(); + int16_t endFrame = reader.ReadInt16(); + int16_t fileCount = reader.ReadInt16(); + reader.ReadInt16(); + + std::vector<AnimationFile> animFiles; + for (int16_t i = 0; i < fileCount; i++) { + int16_t fileElementBitField = reader.ReadInt16(); + int16_t dataCount = reader.ReadInt16(); + int16_t boneIndex = (fileElementBitField & 0xFFF0) >> 4; + int16_t transformType = fileElementBitField & 0xF; + + std::vector<AnimationFileData> fileData; + + for (int16_t j = 0; j < dataCount; j++) { + AnimationFileData data; + int16_t fileDataBitField = reader.ReadInt16(); + + data.unk0_15 = (fileDataBitField & 0b1000000000000000) >> 15; + data.unk0_14 = (fileDataBitField & 0b0100000000000000) >> 14; + data.unk0_13 = fileDataBitField & 0b0011111111111111; + data.unk2 = reader.ReadInt16(); + + fileData.emplace_back(data); + } + + animFiles.emplace_back(boneIndex, transformType, fileData); + } + + return std::make_shared<AnimData>(startFrame, endFrame, animFiles); +} + +std::optional<std::shared_ptr<IParsedData>> AnimFactory::parse_modding(std::vector<uint8_t>& buffer, YAML::Node& node) { + YAML::Node assetNode; + + try { + std::string text((char*)buffer.data(), buffer.size()); + assetNode = YAML::Load(text.c_str()); + } catch (YAML::ParserException& e) { + SPDLOG_ERROR("Failed to parse message data: {}", e.what()); + SPDLOG_ERROR("{}", (char*)buffer.data()); + return std::nullopt; + } + + const auto info = assetNode.begin()->second; + + auto startFrame = info["StartFrame"].as<int16_t>(); + auto endFrame = info["EndFrame"].as<int16_t>(); + + auto elements = info["Elements"]; + + std::vector<AnimationFile> animFiles; + + for (YAML::iterator it = elements.begin(); it != elements.end(); ++it) { + auto elem = *it; + + auto boneIndex = elem["BoneIndex"].as<int16_t>(); + auto transformType = elem["TransformType"].as<int16_t>(); + auto data = elem["Data"]; + + std::vector<AnimationFileData> fileData; + for (YAML::iterator jt = data.begin(); jt != data.end(); ++jt) { + AnimationFileData animFileData; + + animFileData.unk0_15 = (*jt)[0].as<uint32_t>(); + animFileData.unk0_14 = (*jt)[1].as<uint32_t>(); + animFileData.unk0_13 = (*jt)[2].as<uint16_t>(); + animFileData.unk2 = (*jt)[3].as<int16_t>(); + + fileData.push_back(animFileData); + } + animFiles.emplace_back(boneIndex, transformType, fileData); + } + + return std::make_shared<AnimData>(startFrame, endFrame, animFiles); +} + +} // namespace BK64 diff --git a/src/factories/bk64/AnimFactory.h b/src/factories/bk64/AnimFactory.h new file mode 100644 index 0000000..2748433 --- /dev/null +++ b/src/factories/bk64/AnimFactory.h @@ -0,0 +1,71 @@ +#pragma once + +#include <factories/BaseFactory.h> + +namespace BK64 { + +typedef union { // transform structure [xx xx] -> [DE FF FF FF FF FF FF FF] + struct { + uint8_t unk0_15 : 1; // bit flag 1 [D] + uint8_t unk0_14 : 1; // bit flag 2 [E] + uint16_t unk0_13 : 14; // frame of transformation [FF FF FF FF FF FF FF]; + // ends up 87.59375 (0x15E6) after the bitwise ops + int16_t unk2; + }; +} AnimationFileData; + +class AnimationFile { + public: + int16_t mBoneIndex; + int16_t mTransformType; + std::vector<AnimationFileData> mData; + + AnimationFile(int16_t boneIndex, int16_t transformType, std::vector<AnimationFileData> data) + : mBoneIndex(boneIndex), mTransformType(transformType), mData(std::move(data)) { + } +}; + +class AnimData : public IParsedData { + public: + int16_t mStartFrame; + int16_t mEndFrame; + std::vector<AnimationFile> mFiles; + + AnimData(int16_t startFrame, int16_t endFrame, std::vector<AnimationFile> files) + : mStartFrame(startFrame), mEndFrame(endFrame), mFiles(std::move(files)) { + } +}; + +class AnimHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class AnimBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class AnimCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class AnimModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class AnimFactory : 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(Code, AnimCodeExporter) REGISTER(Header, AnimHeaderExporter) + REGISTER(Binary, AnimBinaryExporter) REGISTER(Modding, AnimModdingExporter) }; + } + bool SupportModdedAssets() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/BKAssetFactory.cpp b/src/factories/bk64/BKAssetFactory.cpp new file mode 100644 index 0000000..8891541 --- /dev/null +++ b/src/factories/bk64/BKAssetFactory.cpp @@ -0,0 +1,397 @@ +#include "BKAssetFactory.h" +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "utils/Decompressor.h" +#include <cstring> +#include <iomanip> +#include <thread> +#include <mutex> +#include <unordered_set> +#include <yaml-cpp/yaml.h> + +namespace BK64 { + +static const std::unordered_map<BKAssetType, std::string> sAssetSymbolPrefixes = { + { BKAssetType::Animation, "ANIM" }, + { BKAssetType::Binary, "BIN" }, + { BKAssetType::DemoInput, "DEMO" }, + { BKAssetType::Dialog, "DIALOG" }, + { BKAssetType::GruntyQuestion, "GRUNTYQ" }, + { BKAssetType::Map, "MAP" }, + { BKAssetType::Midi, "MIDI" }, + { BKAssetType::Model, "MODEL" }, + { BKAssetType::QuizQuestion, "QUIZQ" }, + { BKAssetType::Sprite, "SPRITE" }, +}; + +ExportResult BKAssetHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + write << "extern BKAssetTableEntry " << symbol << "[];\n"; + return std::nullopt; +} + +ExportResult BKAssetCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto assetTable = std::static_pointer_cast<BKAssetData>(raw); + const auto offset = GetSafeNode<uint32_t>(node, "offset"); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "BKAssetTableEntry " << symbol << "[] = {\n"; + + for (const auto& assetInfo : assetTable->mAssetTableInfo) { + write << fourSpaceTab << "{ "; + write << "/* index */ " << assetInfo.index << ", "; + write << "/* offset */ " << assetInfo.offset << ", "; + write << "/* compressed */ " << assetInfo.compressionFlag << ", "; + write << "/* tFlag */ " << assetInfo.tFlag; + write << " }, // mode: " << assetInfo.assetMode; + + if (assetInfo.tFlag == 4) { + write << " (empty slot)"; + } + + write << "\n"; + } + + write << "};\n\n"; + + // count + padding + entries + size_t size = 4 + 4 + (assetTable->mAssetTableInfo.size() * 8); + + return offset + size; +} + +ExportResult BKAssetBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto assetTable = std::static_pointer_cast<BKAssetData>(raw); + + WriteHeader(writer, Torch::ResourceType::Blob, 0); + + // Pull the o2r path prefix off our own replacement path, e.g. + // "assets/aBKAssetTable" -> "assets/" + std::string prefix; + if (replacement) { + auto lastSlash = replacement->rfind('/'); + if (lastSlash != std::string::npos) { + prefix = replacement->substr(0, lastSlash + 1); + } + } + + // Manifest mapping each asset ID to its full o2r path. + // Format: u32 count, then per entry: u32 assetId, s32 pathLen, char + // path[pathLen] + std::vector<std::pair<uint32_t, std::string>> entries; + entries.reserve(assetTable->mSymbolMap.size() + assetTable->mAssetTableInfo.size()); + size_t payloadSize = 4; + for (const auto& [id, symbol] : assetTable->mSymbolMap) { + std::string fullPath = prefix + symbol; + payloadSize += 4 + 4 + fullPath.size(); + entries.emplace_back(id, std::move(fullPath)); + } + for (const auto& assetInfo : assetTable->mAssetTableInfo) { + if (assetInfo.tFlag != 4) { + continue; // only the empty slots; real assets are already in mSymbolMap + } + payloadSize += 4 + 4; // id + zero-length path + entries.emplace_back(static_cast<uint32_t>(assetInfo.index), std::string()); + } + writer.Write(static_cast<uint32_t>(payloadSize)); + + writer.Write(static_cast<uint32_t>(entries.size())); + for (const auto& [id, fullPath] : entries) { + writer.Write(id); + writer.Write(fullPath); + } + + writer.Finish(write); + return OffsetEntry{ 0 }; +} + +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); + const auto offset = GetSafeNode<uint32_t>(node, "offset"); + bool symbolMapExists = false; + if (node["symbol_map"]) { + symbolMapExists = true; + } + + reader.SetEndianness(Torch::Endianness::Big); + + uint32_t assetCount = reader.ReadUInt32(); + reader.ReadUInt32(); + + uint32_t dataStartRomOffset = offset + 8 + assetCount * 8; + int16_t prevTFlag = 3; + int32_t assetMode = 0; + + std::vector<BKAssetInfo> assetTableInfo; + std::unordered_map<uint32_t, std::string> symbolMap; + + for (uint32_t i = 0; i < assetCount; i++) { + BKAssetInfo assetInfo; + assetInfo.index = i; + assetInfo.offset = reader.ReadUInt32(); + assetInfo.compressionFlag = reader.ReadInt16(); + assetInfo.tFlag = reader.ReadInt16(); + + if (assetInfo.tFlag == 4) { + // Empty slot. Keep it anyway. We still need its offset to size + // the asset before it. + assetTableInfo.emplace_back(assetInfo); + continue; + } + + if (assetInfo.tFlag != 2 && (prevTFlag & 2) != (assetInfo.tFlag & 2)) { + assetMode++; + prevTFlag = assetInfo.tFlag; + } + + assetInfo.assetMode = assetMode; + + assetTableInfo.emplace_back(assetInfo); + } + + int count = 0; + + // Warm the decompressor cache up front, in parallel. The serial parse + // pass below then mostly hits already-decoded data. + struct DecompJob { + uint32_t offset; + uint32_t size; + }; + std::vector<DecompJob> decompJobs; + for (uint32_t i = 0; i < assetCount - 1; i++) { + auto& ai = assetTableInfo.at(i); + if (ai.tFlag == 4 || ai.compressionFlag == 0) + continue; + uint32_t sz = assetTableInfo.at(i + 1).offset - ai.offset; + if (sz == 0) { + for (uint32_t j = i + 2; j < assetCount; j++) { + if (assetTableInfo.at(j).offset != ai.offset) { + sz = assetTableInfo.at(j).offset - ai.offset; + break; + } + } + } + uint32_t off = dataStartRomOffset + ai.offset; + if (off + sz <= buffer.size()) { + decompJobs.push_back({ off, sz }); + } + } + + // Torch already runs us 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; + SPDLOG_INFO("Pre-decompressing {} assets using {} threads", decompJobs.size(), numThreads); + auto decompRange = [&](size_t start, size_t end) { + for (size_t j = start; j < end; j++) { + try { + Decompressor::Decode(buffer, decompJobs[j].offset, CompressionType::BKZIP, decompJobs[j].size); + } catch (...) {} + } + }; + std::vector<std::thread> decompThreads; + size_t decompChunk = (decompJobs.size() + numThreads - 1) / numThreads; + for (size_t t = 0; t < numThreads; t++) { + size_t s = t * decompChunk, e = std::min(s + decompChunk, decompJobs.size()); + if (s < e) + decompThreads.emplace_back(decompRange, s, e); + } + for (auto& t : decompThreads) + t.join(); + SPDLOG_INFO("Pre-decompression complete"); + + 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; + } + } + } + + auto assetOffset = dataStartRomOffset + assetInfo.offset; + BKAssetType assetType; + + 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; + } + + SPDLOG_TRACE("Parsing asset {} mode={} offset=0x{:X} size=0x{:X} comp={}", assetInfo.index, assetInfo.assetMode, + assetOffset, assetSize, assetInfo.compressionFlag); + + 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; + } + 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; + } + case 5: + assetType = BKAssetType::Model; + break; + case 6: + assetType = BKAssetType::Midi; + break; + default: + assetType = BKAssetType::Binary; + break; + } + + 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; + + assetStream << "D_" << sAssetSymbolPrefixes.at(assetType) << "_" << std::to_string(assetInfo.index); + + assetSymbol = assetStream.str(); + } + + symbolMap[assetInfo.index] = assetSymbol; + + 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"); + } + } 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 (parseFailures > 0) { + SPDLOG_WARN("[BKAssetFactory] {} slot(s) failed to parse and were skipped", parseFailures); + } + + return std::make_shared<BKAssetData>(assetTableInfo, symbolMap); +} + +} // namespace BK64 diff --git a/src/factories/bk64/BKAssetFactory.h b/src/factories/bk64/BKAssetFactory.h new file mode 100644 index 0000000..9061807 --- /dev/null +++ b/src/factories/bk64/BKAssetFactory.h @@ -0,0 +1,84 @@ +#pragma once + +#include "factories/BaseFactory.h" +#include <string> +#include <types/RawBuffer.h> +#include <unordered_map> +#include <vector> + +namespace BK64 { + +/** + * One asset-table entry: where an asset lives and what it is. + * + * Fields: + * - offset: ROM address or file offset (BKZIP compressed if compressionFlag != 0) + * - compressionFlag: 0 = uncompressed, 1 = BKZIP compressed, 2 = MIO0, 3 = YAY0 + * - tFlag: Type discriminator (0=model, 1=sprite, 2=animation, etc.) + * - assetMode: Loading mode (0=immediate, 1=deferred, 2=cached) + * - index: Original asset index in source data (debugging aid) + * + * How a lookup flows: + * getModel3d(0x2d5) → assetTable[0x2d5] → offset=0x1A2400, compression=1 + * -> Decompressor::BKZIP(ROM + 0x1A2400) → ModelFactory::parse() + */ +typedef struct BKAssetInfo { + uint32_t offset; + int16_t compressionFlag; + int16_t tFlag; + int32_t assetMode; + int32_t index; +} BKAssetInfo; + +enum class BKAssetType { + Animation, // 0x000 - 0x0FF + Binary, // 0x100 - 0x1FF + DemoInput, // 0x200 - 0x2CF + Dialog, // 0x2D0 - 0x2D0 + Model, // 0x2D1 - 0x571 + Sprite, // 0x572 - 0x6FF + Map, // 0x700 - 0x7FF + Midi, // 0x800 - 0x8FF + GruntyQuestion, // 0x900 - 0x9FF + QuizQuestion, // 0xA00 - 0xAFF +}; + +class BKAssetData : public IParsedData { + public: + std::vector<BKAssetInfo> mAssetTableInfo; + std::unordered_map<uint32_t, std::string> mSymbolMap; + + BKAssetData(std::vector<BKAssetInfo> assetTableInfo, std::unordered_map<uint32_t, std::string> symbolMap) + : mAssetTableInfo(std::move(assetTableInfo)), mSymbolMap(std::move(symbolMap)) { + } +}; + +class BKAssetHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class BKAssetBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class BKAssetCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class BKAssetFactory : public BaseFactory { + public: + std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override; + inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override { + return { REGISTER(Header, BKAssetHeaderExporter) REGISTER(Binary, BKAssetBinaryExporter) + REGISTER(Code, BKAssetCodeExporter) }; + } + + bool HasModdedDependencies() override { + return true; + } +}; + +} // namespace BK64 diff --git a/src/factories/bk64/DemoInputFactory.cpp b/src/factories/bk64/DemoInputFactory.cpp new file mode 100644 index 0000000..702aec9 --- /dev/null +++ b/src/factories/bk64/DemoInputFactory.cpp @@ -0,0 +1,172 @@ +#include "DemoInputFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" + +#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec +#define FORMAT_HEX(x, w) \ + std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec + +namespace BK64 { + +ExportResult DemoInputHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult DemoInputCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto demoInput = std::static_pointer_cast<DemoInputData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "DemoFileHeader " << symbol << " = {\n"; + + write << fourSpaceTab << demoInput->mInputs.size() * sizeof(ControllerInput) << ",\n"; + + for (const auto& input : demoInput->mInputs) { + write << fourSpaceTab << "{ "; + write << (int32_t)input.stickX << ", " << (int32_t)input.stickY << ", 0x" << FORMAT_HEX(input.buttons, 4) + << ", " << (uint32_t)input.frames << ", " << (uint32_t)input.unkFlag; + write << " },\n"; + } + + write << "};\n\n"; + + return offset; +} + +ExportResult BK64::DemoInputBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto demoInput = std::static_pointer_cast<DemoInputData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKDemoInput, 0); + + writer.Write((uint32_t)demoInput->mInputs.size()); + for (const auto& input : demoInput->mInputs) { + writer.Write(input.stickX); + writer.Write(input.stickY); + writer.Write(input.buttons); + writer.Write(input.frames); + writer.Write(input.unkFlag); + } + + writer.Finish(write); + return std::nullopt; +} + +ExportResult BK64::DemoInputModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + const auto demoInput = std::static_pointer_cast<DemoInputData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "ControllerInputs" << YAML::Value; + + out << YAML::BeginSeq; + for (const auto& input : demoInput->mInputs) { + out << YAML::BeginMap; + out << YAML::Key << "StickX" << YAML::Value << (int32_t)input.stickX; + out << YAML::Key << "StickY" << YAML::Value << (int32_t)input.stickY; + out << YAML::Key << "Buttons" << YAML::Value << YAML_HEX(input.buttons); + out << YAML::Key << "Frames" << YAML::Value << (uint32_t)input.frames; + out << YAML::Key << "UnkFlag" << YAML::Value << (uint32_t)input.unkFlag; + out << YAML::EndMap; + } + out << YAML::EndSeq; + out << YAML::EndMap; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> DemoInputFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + + SPDLOG_INFO("START SYMBOL {}", symbol); + + if (segment.size < 4) { + return std::make_shared<DemoInputData>(std::vector<ControllerInput>()); + } + + auto size = reader.ReadUInt32(); + + std::vector<ControllerInput> inputs; + + for (uint32_t i = 0; i < size / sizeof(ControllerInput); i++) { + ControllerInput input; + + input.stickX = reader.ReadInt8(); + input.stickY = reader.ReadInt8(); + input.buttons = reader.ReadUInt16(); + input.frames = reader.ReadUByte(); + input.unkFlag = reader.ReadUByte(); + + inputs.push_back(input); + } + + return std::make_shared<DemoInputData>(inputs); +} + +std::optional<std::shared_ptr<IParsedData>> DemoInputFactory::parse_modding(std::vector<uint8_t>& buffer, + YAML::Node& node) { + YAML::Node assetNode; + + try { + std::string text((char*)buffer.data(), buffer.size()); + assetNode = YAML::Load(text.c_str()); + } catch (YAML::ParserException& e) { + SPDLOG_ERROR("Failed to parse message data: {}", e.what()); + SPDLOG_ERROR("{}", (char*)buffer.data()); + return std::nullopt; + } + + const auto info = assetNode.begin()->second; + + auto controllerInfo = info["ControllerInputs"]; + + std::vector<ControllerInput> inputs; + + for (YAML::iterator it = controllerInfo.begin(); it != controllerInfo.end(); ++it) { + ControllerInput input; + + auto inputInfo = *it; + + input.stickX = inputInfo["StickX"].as<int32_t>(); + input.stickY = inputInfo["StickY"].as<int32_t>(); + input.buttons = inputInfo["Buttons"].as<uint16_t>(); + input.frames = inputInfo["Frames"].as<uint32_t>(); + input.unkFlag = inputInfo["UnkFlag"].as<uint32_t>(); + + inputs.push_back(input); + } + + return std::make_shared<DemoInputData>(inputs); +} + +} // namespace BK64 diff --git a/src/factories/bk64/DemoInputFactory.h b/src/factories/bk64/DemoInputFactory.h new file mode 100644 index 0000000..7b6efcc --- /dev/null +++ b/src/factories/bk64/DemoInputFactory.h @@ -0,0 +1,55 @@ +#pragma once + +#include <factories/BaseFactory.h> + +namespace BK64 { + +typedef struct ControllerInput { + int8_t stickX; + int8_t stickY; + uint16_t buttons; + uint8_t frames; + uint8_t unkFlag; +} ControllerInput; + +class DemoInputData : public IParsedData { + public: + std::vector<ControllerInput> mInputs; + + DemoInputData(std::vector<ControllerInput> inputs) : mInputs(std::move(inputs)) { + } +}; + +class DemoInputHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DemoInputBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DemoInputCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DemoInputModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DemoInputFactory : 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(Code, DemoInputCodeExporter) REGISTER(Header, DemoInputHeaderExporter) + REGISTER(Binary, DemoInputBinaryExporter) REGISTER(Modding, DemoInputModdingExporter) }; + } + bool SupportModdedAssets() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/DialogFactory.cpp b/src/factories/bk64/DialogFactory.cpp new file mode 100644 index 0000000..a128504 --- /dev/null +++ b/src/factories/bk64/DialogFactory.cpp @@ -0,0 +1,276 @@ +#include "DialogFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" + +#define DIALOG_HEADER_1 0x01 +#define DIALOG_HEADER_2 0x03 +#define DIALOG_HEADER_3 0x00 + +#define FORMAT_HEX(x, w) \ + std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec +#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec + +namespace BK64 { + +ExportResult DialogHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult DialogCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto dialog = std::static_pointer_cast<DialogData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "u8 " << symbol << "[] = {\n"; + + write << fourSpaceTab << "DIALOG_HEADER_1" + << ", " + << "DIALOG_HEADER_2" + << ", " + << "DIALOG_HEADER_3" + << ",\n"; + write << fourSpaceTab << "/* Bottom Dialog */\n"; + write << fourSpaceTab << dialog->mBottom.size() << ",\n"; + for (const auto [cmd, str] : dialog->mBottom) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } + write << ",\n"; + } + write << fourSpaceTab << "/* Top Dialog */\n"; + write << fourSpaceTab << dialog->mTop.size() << ",\n"; + for (const auto [cmd, str] : dialog->mTop) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } + write << ",\n"; + } + + write << "};\n\n"; + + return offset; +} + +static void WriteLangBlock(LUS::BinaryWriter& writer, const std::vector<DialogString>& bottom, + const std::vector<DialogString>& top) { + writer.Write((uint32_t)bottom.size()); + for (const auto& dialogString : bottom) { + writer.Write(dialogString.cmd); + writer.Write((uint32_t)dialogString.str.length()); + writer.Write((char*)dialogString.str.data(), dialogString.str.size()); + } + + writer.Write((uint32_t)top.size()); + for (const auto& dialogString : top) { + writer.Write(dialogString.cmd); + writer.Write((uint32_t)dialogString.str.length()); + writer.Write((char*)dialogString.str.data(), dialogString.str.size()); + } +} + +ExportResult BK64::DialogBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto dialog = std::static_pointer_cast<DialogData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKDialog, 0); + + // 1 for US/JP, 3 for PAL (EN + FR + DE) + uint32_t langCount = 1 + static_cast<uint32_t>(dialog->mExtraLangs.size()); + writer.Write(langCount); + + // English always goes first + WriteLangBlock(writer, dialog->mBottom, dialog->mTop); + + // PAL only: French then German + for (const auto& lang : dialog->mExtraLangs) { + WriteLangBlock(writer, lang.bottom, lang.top); + } + + writer.Finish(write); + return std::nullopt; +} + +ExportResult BK64::DialogModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto dialog = std::static_pointer_cast<DialogData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "Bottom"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto [cmd, str] : dialog->mBottom) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + out << str.c_str(); + out << YAML::EndSeq; + } + out << YAML::EndSeq; + + out << YAML::Key << "Top"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto [cmd, str] : dialog->mTop) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + out << str.c_str(); + out << YAML::EndSeq; + } + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +// One language: bottom box strings, then top box strings. +static DialogLang ParseLangBlock(LUS::BinaryReader& reader) { + DialogLang lang; + + auto bottomSize = reader.ReadUByte(); + for (uint8_t i = 0; i < bottomSize; i++) { + DialogString dialogString; + dialogString.cmd = reader.ReadUByte(); + auto strLen = reader.ReadUByte(); + dialogString.str = reader.ReadString(strLen); + lang.bottom.push_back(dialogString); + } + + auto topSize = reader.ReadUByte(); + for (uint8_t i = 0; i < topSize; i++) { + DialogString dialogString; + dialogString.cmd = reader.ReadUByte(); + auto strLen = reader.ReadUByte(); + dialogString.str = reader.ReadString(strLen); + lang.top.push_back(dialogString); + } + + return lang; +} + +std::optional<std::shared_ptr<IParsedData>> DialogFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + + auto header1 = reader.ReadInt8(); + auto header2 = reader.ReadInt8(); + auto header3 = reader.ReadInt8(); + + if (header1 == DIALOG_HEADER_1 && header2 == DIALOG_HEADER_2 && header3 == DIALOG_HEADER_3) { + // US/JP: 01 03 00, dialog data follows immediately + auto lang = ParseLangBlock(reader); + return std::make_shared<DialogData>(std::move(lang.bottom), std::move(lang.top)); + } + + if (header1 == 0x03 && header2 == 0x07 && header3 == 0x00) { + // PAL: 03 07 00, then two LE u16 offsets (French, German), then the + // EN/FR/DE blocks. + uint16_t frenchOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); + uint16_t germanOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); + + // EN sits right here at byte 7; FR and DE we seek to. + auto english = ParseLangBlock(reader); + + reader.Seek(frenchOffset, LUS::SeekOffsetType::Start); + auto french = ParseLangBlock(reader); + + reader.Seek(germanOffset, LUS::SeekOffsetType::Start); + auto german = ParseLangBlock(reader); + + std::vector<DialogLang> extraLangs; + extraLangs.push_back(std::move(french)); + extraLangs.push_back(std::move(german)); + + return std::make_shared<DialogData>(std::move(english.bottom), std::move(english.top), std::move(extraLangs)); + } + + SPDLOG_ERROR("Invalid Header For BK64 Dialog {}: {:02X} {:02X} {:02X}", symbol, header1, header2, header3); + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> DialogFactory::parse_modding(std::vector<uint8_t>& buffer, + YAML::Node& node) { + YAML::Node assetNode; + + try { + std::string text((char*)buffer.data(), buffer.size()); + assetNode = YAML::Load(text.c_str()); + } catch (YAML::ParserException& e) { + SPDLOG_ERROR("Failed to parse message data: {}", e.what()); + SPDLOG_ERROR("{}", (char*)buffer.data()); + return std::nullopt; + } + + const auto info = assetNode.begin()->second; + + std::vector<DialogString> bottom; + std::vector<DialogString> top; + + auto bottomNode = info["Bottom"]; + auto topNode = info["Top"]; + + for (YAML::iterator it = bottomNode.begin(); it != bottomNode.end(); ++it) { + DialogString dialogString; + dialogString.cmd = (*it)[0].as<uint32_t>(); + dialogString.str = (*it)[1].as<std::string>(); + dialogString.str += '\0'; + bottom.push_back(dialogString); + } + + for (YAML::iterator it = topNode.begin(); it != topNode.end(); ++it) { + DialogString dialogString; + dialogString.cmd = (*it)[0].as<uint32_t>(); + dialogString.str = (*it)[1].as<std::string>(); + dialogString.str += '\0'; + top.push_back(dialogString); + } + + return std::make_shared<DialogData>(bottom, top); +} + +} // namespace BK64 diff --git a/src/factories/bk64/DialogFactory.h b/src/factories/bk64/DialogFactory.h new file mode 100644 index 0000000..9398d64 --- /dev/null +++ b/src/factories/bk64/DialogFactory.h @@ -0,0 +1,68 @@ +#pragma once + +#include <factories/BaseFactory.h> + +namespace BK64 { + +typedef struct DialogString { + uint8_t cmd; + std::string str; +} DialogString; + +// One language's dialog: the bottom text box plus the top one. +typedef struct DialogLang { + std::vector<DialogString> bottom; + std::vector<DialogString> top; +} DialogLang; + +class DialogData : public IParsedData { + public: + // Always set. English on PAL/JP, the lone language on US. + std::vector<DialogString> mBottom; + std::vector<DialogString> mTop; + + // PAL only: index 0=French, 1=German + std::vector<DialogLang> mExtraLangs; + + DialogData(std::vector<DialogString> bottom, std::vector<DialogString> top) + : mBottom(std::move(bottom)), mTop(std::move(top)) { + } + + DialogData(std::vector<DialogString> bottom, std::vector<DialogString> top, std::vector<DialogLang> extraLangs) + : mBottom(std::move(bottom)), mTop(std::move(top)), mExtraLangs(std::move(extraLangs)) { + } +}; + +class DialogHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DialogBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DialogCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DialogModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class DialogFactory : 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(Code, DialogCodeExporter) REGISTER(Header, DialogHeaderExporter) + REGISTER(Binary, DialogBinaryExporter) REGISTER(Modding, DialogModdingExporter) }; + } + bool SupportModdedAssets() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/GeoLayoutFactory.cpp b/src/factories/bk64/GeoLayoutFactory.cpp new file mode 100644 index 0000000..e1e08fa --- /dev/null +++ b/src/factories/bk64/GeoLayoutFactory.cpp @@ -0,0 +1,715 @@ +#include "GeoLayoutFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" +#include <cstring> +#include <deque> + +#define ALIGN8(val) (((val) + 7) & ~7) +#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec + +namespace BK64 { + +ExportResult GeoLayoutHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult GeoLayoutCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto geo = std::static_pointer_cast<GeoLayoutData>(raw); + + return offset; +} + +// Serialized size of one geo command, 8-byte header included. +static uint32_t GetGeoCommandByteSize(const GeoLayoutCommand& cmd) { + uint32_t bodySize = 0; + switch (cmd.opCode) { + case GeoLayoutOpCode::UnknownCmd0: + bodySize = 16; + break; // 2+2+4+4+4 + case GeoLayoutOpCode::Sort: + bodySize = 32; + break; // 4*6+2+2+4 (matches GeoCmd1) + case GeoLayoutOpCode::Bone: + bodySize = 4; + break; // 1+1+2 + case GeoLayoutOpCode::LoadDL: + bodySize = 4; + break; // 2+2 + case GeoLayoutOpCode::Skinning: + // 2 per arg + 2 for terminator + bodySize = static_cast<uint32_t>(cmd.args.size()) * 2 + 2; + break; + case GeoLayoutOpCode::Branch: + bodySize = 4; + break; // 4 + case GeoLayoutOpCode::UnknownCmd7: + bodySize = 4; + break; // 2+2 + case GeoLayoutOpCode::LOD: + bodySize = 24; + break; // 4*5+4 + case GeoLayoutOpCode::ReferencePoint: + bodySize = 16; + break; // 2+2+4+4+4 + case GeoLayoutOpCode::Selector: + // 2+2 + (args.size()-2)*4 + bodySize = 4 + static_cast<uint32_t>(cmd.args.size() - 2) * 4; + break; + case GeoLayoutOpCode::DrawDistance: + bodySize = 16; + break; // 2*8 + case GeoLayoutOpCode::UnknownCmdE: + bodySize = 12; + break; // 2*6 + case GeoLayoutOpCode::UnknownCmdF: + bodySize = 16; + break; // 2+1+1+12 + case GeoLayoutOpCode::UnknownCmd10: + bodySize = 4; + break; // 4 + default: + break; + } + return 8 + bodySize; // 8 = opcode(4) + cmdLength(4) +} + +ExportResult BK64::GeoLayoutBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto geo = std::static_pointer_cast<GeoLayoutData>(raw); + + // Size the buffer from each command's original offset plus its length. + uint32_t totalSize = 0; + for (const auto& cmd : geo->mCmds) { + uint32_t end = cmd.originalOffset + GetGeoCommandByteSize(cmd); + if (end > totalSize) + totalSize = end; + } + + // Zero-fill, then drop each command back at the offset it came from. + std::vector<uint8_t> buffer(totalSize, 0); + + for (const auto& cmd : geo->mCmds) { + uint32_t pos = cmd.originalOffset; + const auto& arguments = cmd.args; + + // Little helpers: write at pos, bump pos + auto writeU8 = [&](uint8_t v) { buffer[pos++] = v; }; + auto writeU16 = [&](uint16_t v) { + memcpy(&buffer[pos], &v, 2); + pos += 2; + }; + auto writeS16 = [&](int16_t v) { + memcpy(&buffer[pos], &v, 2); + pos += 2; + }; + auto writeU32 = [&](uint32_t v) { + memcpy(&buffer[pos], &v, 4); + pos += 4; + }; + auto writeS32 = [&](int32_t v) { + memcpy(&buffer[pos], &v, 4); + pos += 4; + }; + auto writeF32 = [&](float v) { + memcpy(&buffer[pos], &v, 4); + pos += 4; + }; + + // Header is opcode then cmdLength + writeU32(static_cast<uint32_t>(cmd.opCode)); + writeU32(cmd.cmdLength); + + switch (cmd.opCode) { + case GeoLayoutOpCode::UnknownCmd0: + writeU16(std::get<uint16_t>(arguments[0])); + writeU16(std::get<uint16_t>(arguments[1])); + writeF32(std::get<float>(arguments[2])); + writeF32(std::get<float>(arguments[3])); + writeF32(std::get<float>(arguments[4])); + break; + case GeoLayoutOpCode::Sort: + writeF32(std::get<float>(arguments[0])); + writeF32(std::get<float>(arguments[1])); + writeF32(std::get<float>(arguments[2])); + writeF32(std::get<float>(arguments[3])); + writeF32(std::get<float>(arguments[4])); + writeF32(std::get<float>(arguments[5])); + // [port] Decomp reads unk20 as s16, unk22 as s16, unk24 as s32, so + // we match the GeoCmd1 struct layout here, not the N64 BE byte layout. + writeS16(static_cast<int16_t>(std::get<uint8_t>(arguments[6]))); // unk20 (layoutOrder) + writeS16(static_cast<int16_t>(std::get<uint16_t>(arguments[7]))); // unk22 (firstChildOffset) + writeS32(static_cast<int32_t>(std::get<uint16_t>(arguments[8]))); // unk24 (secondChildOffset) + break; + case GeoLayoutOpCode::Bone: + writeU8(std::get<uint8_t>(arguments[0])); + writeU8(std::get<uint8_t>(arguments[1])); + writeU16(std::get<uint16_t>(arguments[2])); + break; + case GeoLayoutOpCode::LoadDL: + writeU16(std::get<uint16_t>(arguments[0])); + writeU16(std::get<uint16_t>(arguments[1])); + break; + case GeoLayoutOpCode::Skinning: + writeU16(std::get<uint16_t>(arguments[0])); + for (size_t i = 1; i < arguments.size(); i++) + writeU16(std::get<uint16_t>(arguments[i])); + writeU16(0); // terminator + break; + case GeoLayoutOpCode::Branch: + writeU32(std::get<uint32_t>(arguments[0])); + break; + case GeoLayoutOpCode::UnknownCmd7: + writeU16(0); // pad + writeU16(std::get<uint16_t>(arguments[0])); + break; + case GeoLayoutOpCode::LOD: + writeF32(std::get<float>(arguments[0])); + writeF32(std::get<float>(arguments[1])); + writeF32(std::get<float>(arguments[2])); + writeF32(std::get<float>(arguments[3])); + writeF32(std::get<float>(arguments[4])); + writeU32(std::get<uint32_t>(arguments[5])); + break; + case GeoLayoutOpCode::ReferencePoint: + writeU16(std::get<uint16_t>(arguments[0])); + writeU16(std::get<uint16_t>(arguments[1])); + writeF32(std::get<float>(arguments[2])); + writeF32(std::get<float>(arguments[3])); + writeF32(std::get<float>(arguments[4])); + break; + case GeoLayoutOpCode::Selector: + writeU16(std::get<uint16_t>(arguments[0])); + writeU16(std::get<uint16_t>(arguments[1])); + for (size_t i = 2; i < arguments.size(); i++) + writeU32(std::get<uint32_t>(arguments[i])); + break; + case GeoLayoutOpCode::DrawDistance: + writeS16(std::get<int16_t>(arguments[0])); + writeS16(std::get<int16_t>(arguments[1])); + writeS16(std::get<int16_t>(arguments[2])); + writeS16(std::get<int16_t>(arguments[3])); + writeS16(std::get<int16_t>(arguments[4])); + writeS16(std::get<int16_t>(arguments[5])); + writeS16(std::get<int16_t>(arguments[6])); + writeS16(std::get<int16_t>(arguments[7])); + break; + case GeoLayoutOpCode::UnknownCmdE: + writeS16(std::get<int16_t>(arguments[0])); + writeS16(std::get<int16_t>(arguments[1])); + writeS16(std::get<int16_t>(arguments[2])); + writeS16(std::get<int16_t>(arguments[3])); + writeS16(std::get<int16_t>(arguments[4])); + writeS16(std::get<int16_t>(arguments[5])); + break; + case GeoLayoutOpCode::UnknownCmdF: + writeU16(std::get<uint16_t>(arguments[0])); + writeU8(std::get<uint8_t>(arguments[1])); + writeU8(std::get<uint8_t>(arguments[2])); + for (size_t i = 3; i < arguments.size(); i++) + writeU8(std::get<uint8_t>(arguments[i])); + break; + case GeoLayoutOpCode::UnknownCmd10: + writeS32(std::get<int32_t>(arguments[0])); + break; + default: + throw std::runtime_error("BK64::GeoLayoutBinaryExporter: Unknown OpCode Found " + + std::to_string(static_cast<uint32_t>(cmd.opCode))); + } + } + + LUS::BinaryWriter output = LUS::BinaryWriter(); + WriteHeader(output, Torch::ResourceType::Blob, 0); + + output.Write(static_cast<uint32_t>(buffer.size())); + output.Write(reinterpret_cast<char*>(buffer.data()), buffer.size()); + output.Finish(write); + output.Close(); + + return std::nullopt; +} + +ExportResult GeoLayoutModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto geo = std::static_pointer_cast<GeoLayoutData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + out << YAML::BeginSeq; + std::deque<std::tuple<uint32_t, uint32_t, uint32_t>> childrenStack; + + for (auto& [opCode, cmdLength, arguments, origOff_] : geo->mCmds) { + uint32_t numChildren = 0; + uint32_t i = 0; + + out << YAML::Value; + out << YAML::BeginMap; + + switch (opCode) { + case GeoLayoutOpCode::UnknownCmd0: + out << YAML::Key << "UnknownCmd0"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "childOffset" << YAML::Value << YAML_HEX(std::get<uint16_t>(arguments.at(0))); + out << YAML::Key << "shouldRotatePitch" << YAML::Value << (bool)std::get<uint16_t>(arguments.at(1)); + out << YAML::Key << "x" << YAML::Value << std::get<float>(arguments.at(2)); + out << YAML::Key << "y" << YAML::Value << std::get<float>(arguments.at(3)); + out << YAML::Key << "z" << YAML::Value << std::get<float>(arguments.at(4)); + break; + case GeoLayoutOpCode::Sort: + out << YAML::Key << "Sort"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "x1" << YAML::Value << std::get<float>(arguments.at(0)); + out << YAML::Key << "y1" << YAML::Value << std::get<float>(arguments.at(1)); + out << YAML::Key << "z1" << YAML::Value << std::get<float>(arguments.at(2)); + out << YAML::Key << "x2" << YAML::Value << std::get<float>(arguments.at(3)); + out << YAML::Key << "y2" << YAML::Value << std::get<float>(arguments.at(4)); + out << YAML::Key << "z2" << YAML::Value << std::get<float>(arguments.at(5)); + out << YAML::Key << "layoutOrder" << YAML::Value << (uint32_t)std::get<uint8_t>(arguments.at(6)); + out << YAML::Key << "firstChildOffset" << YAML::Value << YAML_HEX(std::get<uint16_t>(arguments.at(7))); + out << YAML::Key << "secondChildOffset" << YAML::Value << YAML_HEX(std::get<uint16_t>(arguments.at(8))); + + if (std::get<uint16_t>(arguments.at(7)) != 0) { + numChildren++; + } + if (std::get<uint16_t>(arguments.at(8)) != 0) { + numChildren++; + } + break; + case GeoLayoutOpCode::Bone: + out << YAML::Key << "Bone"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "childOffset" << YAML::Value + << YAML_HEX((uint32_t)std::get<uint8_t>(arguments.at(0))); + out << YAML::Key << "boneId" << YAML::Value << (uint32_t)std::get<uint8_t>(arguments.at(1)); + out << YAML::Key << "unkBoneInfo" << YAML::Value << std::get<uint16_t>(arguments.at(2)); + if (std::get<uint8_t>(arguments.at(0)) != 0) { + numChildren++; + } + break; + case GeoLayoutOpCode::LoadDL: + out << YAML::Key << "LoadDL"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "dlIndex" << YAML::Value << std::get<uint16_t>(arguments.at(0)); + out << YAML::Key << "triCount" << YAML::Value << std::get<uint16_t>(arguments.at(1)); + break; + case GeoLayoutOpCode::Skinning: + out << YAML::Key << "Skinning"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "dlOffsetPreviousBone" << YAML::Value << std::get<uint16_t>(arguments.at(0)); + out << YAML::Key << "dlOffsets" << YAML::Value; + out << YAML::BeginSeq; + for (size_t j = 1; j < arguments.size(); j++) { + out << YAML::Value << std::get<uint16_t>(arguments.at(j)); + } + out << YAML::EndSeq; + break; + case GeoLayoutOpCode::Branch: + out << YAML::Key << "Branch"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "cmdTargetOffset" << YAML::Value << std::get<uint32_t>(arguments.at(0)); + break; + case GeoLayoutOpCode::UnknownCmd7: + out << YAML::Key << "UnknownCmd7"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "dlIndex" << YAML::Value << std::get<uint16_t>(arguments.at(0)); + break; + case GeoLayoutOpCode::LOD: + out << YAML::Key << "LOD"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "maxDistance" << YAML::Value << std::get<float>(arguments.at(0)); + out << YAML::Key << "minDistance" << YAML::Value << std::get<float>(arguments.at(1)); + out << YAML::Key << "x" << YAML::Value << std::get<float>(arguments.at(2)); + out << YAML::Key << "y" << YAML::Value << std::get<float>(arguments.at(3)); + out << YAML::Key << "z" << YAML::Value << std::get<float>(arguments.at(4)); + out << YAML::Key << "childOffset" << YAML::Value << YAML_HEX(std::get<uint32_t>(arguments.at(5))); + if (std::get<uint32_t>(arguments.at(5)) != 0) { + numChildren++; + } + break; + case GeoLayoutOpCode::ReferencePoint: + out << YAML::Key << "ReferencePoint"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "referencePointIndex" << YAML::Value << std::get<uint16_t>(arguments.at(0)); + out << YAML::Key << "boneIndex" << YAML::Value << std::get<uint16_t>(arguments.at(1)); + out << YAML::Key << "boneOffsetX" << YAML::Value << std::get<float>(arguments.at(2)); + out << YAML::Key << "boneOffsetY" << YAML::Value << std::get<float>(arguments.at(3)); + out << YAML::Key << "boneOffsetZ" << YAML::Value << std::get<float>(arguments.at(4)); + break; + case GeoLayoutOpCode::Selector: + out << YAML::Key << "Selector"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "childCount" << YAML::Value << std::get<uint16_t>(arguments.at(0)); + out << YAML::Key << "selectorIndex" << YAML::Value << std::get<uint16_t>(arguments.at(1)); + out << YAML::Key << "childOffsets" << YAML::Value; + out << YAML::BeginSeq; + for (size_t j = 2; j < arguments.size(); j++) { + out << YAML::Value << YAML_HEX(std::get<uint32_t>(arguments.at(j))); + if (std::get<uint32_t>(arguments.at(j)) != 0) { + numChildren++; + } + } + out << YAML::EndSeq; + break; + case GeoLayoutOpCode::DrawDistance: + out << YAML::Key << "DrawDistance"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "negX" << YAML::Value << std::get<int16_t>(arguments.at(0)); + out << YAML::Key << "negY" << YAML::Value << std::get<int16_t>(arguments.at(1)); + out << YAML::Key << "negZ" << YAML::Value << std::get<int16_t>(arguments.at(2)); + out << YAML::Key << "posX" << YAML::Value << std::get<int16_t>(arguments.at(3)); + out << YAML::Key << "posY" << YAML::Value << std::get<int16_t>(arguments.at(4)); + out << YAML::Key << "posZ" << YAML::Value << std::get<int16_t>(arguments.at(5)); + out << YAML::Key << "unk14" << YAML::Value << std::get<int16_t>(arguments.at(6)); + out << YAML::Key << "unk16" << YAML::Value << std::get<int16_t>(arguments.at(7)); + break; + case GeoLayoutOpCode::UnknownCmdE: + out << YAML::Key << "UnknownCmdE"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "coords1X" << YAML::Value << std::get<int16_t>(arguments.at(0)); + out << YAML::Key << "coords1Y" << YAML::Value << std::get<int16_t>(arguments.at(1)); + out << YAML::Key << "coords1Z" << YAML::Value << std::get<int16_t>(arguments.at(2)); + out << YAML::Key << "coords2X" << YAML::Value << std::get<int16_t>(arguments.at(3)); + out << YAML::Key << "coords2Y" << YAML::Value << std::get<int16_t>(arguments.at(4)); + out << YAML::Key << "coords2Z" << YAML::Value << std::get<int16_t>(arguments.at(5)); + break; + case GeoLayoutOpCode::UnknownCmdF: + out << YAML::Key << "UnknownCmdF"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "childOffset" << YAML::Value << YAML_HEX(std::get<uint16_t>(arguments.at(0))); + out << YAML::Key << "unkA" << YAML::Value << (uint32_t)std::get<uint8_t>(arguments.at(1)); + out << YAML::Key << "unkB" << YAML::Value << (uint32_t)std::get<uint8_t>(arguments.at(2)); + out << YAML::Key << "unkCBuf" << YAML::Value; + out << YAML::BeginSeq; + for (uint32_t j = 0; j < 12; j++) { + out << YAML::Value << (uint32_t)std::get<uint8_t>(arguments.at(j + 3)); + } + out << YAML::EndSeq; + if (std::get<uint16_t>(arguments.at(0)) != 0) { + numChildren++; + } + break; + case GeoLayoutOpCode::UnknownCmd10: + out << YAML::Key << "UnknownCmd10"; + out << YAML::Value << YAML::BeginMap; + out << YAML::Key << "wrapMode" << YAML::Value << std::get<int32_t>(arguments.at(0)); + break; + default: + throw std::runtime_error("BK64::GeoLayoutModdingExporter: Unknown OpCode Found " + + std::to_string(static_cast<uint32_t>(opCode))); + } + + out << YAML::Key << "CMD_LEN" << YAML::Value << cmdLength; + + if (numChildren > 0) { + childrenStack.emplace_back(0, numChildren, cmdLength); + out << YAML::Key << "Children" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "Child0" << YAML::Value << YAML::BeginSeq; + continue; + } + + out << YAML::EndMap; + out << YAML::EndMap; + + while (cmdLength == 0 && !childrenStack.empty()) { + auto& [childrenProcessed, totalChildren, parentCmdLength] = childrenStack.back(); + if (++childrenProcessed >= totalChildren) { + out << YAML::EndSeq; + out << YAML::EndMap; + out << YAML::EndMap; + out << YAML::EndMap; + cmdLength = parentCmdLength; + childrenStack.pop_back(); + // Exit Child + } else { + // Go To Next Child + out << YAML::EndSeq; + out << YAML::Key << ("Child" + std::to_string(childrenProcessed)) << YAML::Value << YAML::BeginSeq; + break; + } + } + } + + out << YAML::EndSeq; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> GeoLayoutFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + const auto offset = GetSafeNode<uint32_t>(node, "offset"); + + std::vector<GeoLayoutCommand> cmds; + + std::deque<uint32_t> offsetStack; + + offsetStack.push_back(0); + + while (true) { + std::vector<GeoLayoutArg> args; + auto localOffset = offsetStack.back(); + if (localOffset + 8 > segment.size) { + break; + } + reader.Seek(localOffset, LUS::SeekOffsetType::Start); + auto opCode = reader.ReadUInt32(); + auto cmdLength = reader.ReadUInt32(); + + offsetStack.back() += cmdLength; + + if (cmdLength == 0) { + offsetStack.pop_back(); + } + + switch (static_cast<GeoLayoutOpCode>(opCode)) { + case GeoLayoutOpCode::UnknownCmd0: { + auto childOffset = reader.ReadUInt16(); + auto shouldRotatePitch = reader.ReadUInt16(); + auto x = reader.ReadFloat(); + auto y = reader.ReadFloat(); + auto z = reader.ReadFloat(); + + args.emplace_back(childOffset); + args.emplace_back(shouldRotatePitch); + args.emplace_back(x); + args.emplace_back(y); + args.emplace_back(z); + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + break; + } + case GeoLayoutOpCode::Sort: { + auto x1 = reader.ReadFloat(); + auto y1 = reader.ReadFloat(); + auto z1 = reader.ReadFloat(); + auto x2 = reader.ReadFloat(); + auto y2 = reader.ReadFloat(); + auto z2 = reader.ReadFloat(); + + reader.ReadUByte(); // pad + auto layoutOrder = reader.ReadUByte(); + auto firstChildOffset = reader.ReadUInt16(); + reader.ReadUInt16(); // pad + auto secondChildOffset = reader.ReadUInt16(); + + args.emplace_back(x1); + args.emplace_back(y1); + args.emplace_back(z1); + args.emplace_back(x2); + args.emplace_back(y2); + args.emplace_back(z2); + args.emplace_back(layoutOrder); + args.emplace_back(firstChildOffset); + args.emplace_back(secondChildOffset); + + if (firstChildOffset != 0) { + offsetStack.push_back(localOffset + firstChildOffset); + } + if (secondChildOffset != 0) { + offsetStack.push_back(localOffset + secondChildOffset); + } + break; + } + case GeoLayoutOpCode::Bone: { + auto childOffset = reader.ReadUByte(); + auto boneId = reader.ReadUByte(); + auto unkBoneInfo = reader.ReadUInt16(); + + args.emplace_back(childOffset); + args.emplace_back(boneId); + args.emplace_back(unkBoneInfo); + + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + break; + } + case GeoLayoutOpCode::LoadDL: { + auto dlIndex = reader.ReadUInt16(); + auto triCount = reader.ReadUInt16(); + args.emplace_back(dlIndex); + args.emplace_back(triCount); + break; + } + case GeoLayoutOpCode::Skinning: { + auto dlOffsetPreviousBone = reader.ReadUInt16(); + + args.emplace_back(dlOffsetPreviousBone); + + while (true) { + auto dlOffset = reader.ReadUInt16(); + if (dlOffset == 0) { + break; + } + args.emplace_back(dlOffset); + } + break; + } + case GeoLayoutOpCode::Branch: { + auto cmdTargetOffset = reader.ReadUInt32(); + + args.emplace_back(cmdTargetOffset); + break; + } + case GeoLayoutOpCode::UnknownCmd7: { + reader.ReadUInt16(); // pad + auto dlIndex = reader.ReadUInt16(); + + args.emplace_back(dlIndex); + break; + } + case GeoLayoutOpCode::LOD: { + auto maxDistance = reader.ReadFloat(); + auto minDistance = reader.ReadFloat(); + auto x = reader.ReadFloat(); + auto y = reader.ReadFloat(); + auto z = reader.ReadFloat(); + auto childOffset = reader.ReadUInt32(); + args.emplace_back(maxDistance); + args.emplace_back(minDistance); + args.emplace_back(x); + args.emplace_back(y); + args.emplace_back(z); + args.emplace_back(childOffset); + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + break; + } + case GeoLayoutOpCode::ReferencePoint: { + auto referencePointIndex = reader.ReadUInt16(); + auto boneIndex = reader.ReadUInt16(); + auto boneOffsetX = reader.ReadFloat(); + auto boneOffsetY = reader.ReadFloat(); + auto boneOffsetZ = reader.ReadFloat(); + + args.emplace_back(referencePointIndex); + args.emplace_back(boneIndex); + args.emplace_back(boneOffsetX); + args.emplace_back(boneOffsetY); + args.emplace_back(boneOffsetZ); + break; + } + case GeoLayoutOpCode::Selector: { + auto childCount = reader.ReadUInt16(); + auto selectorIndex = reader.ReadUInt16(); + + args.emplace_back(childCount); + args.emplace_back(selectorIndex); + + for (uint16_t i = 0; i < childCount; i++) { + auto childOffset = reader.ReadUInt32(); + + args.emplace_back(childOffset); + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + } + break; + } + case GeoLayoutOpCode::DrawDistance: { + auto negX = reader.ReadInt16(); + auto negY = reader.ReadInt16(); + auto negZ = reader.ReadInt16(); + auto posX = reader.ReadInt16(); + auto posY = reader.ReadInt16(); + auto posZ = reader.ReadInt16(); + auto childOffset = reader.ReadInt16(); + auto unk16 = reader.ReadInt16(); + + args.emplace_back(negX); + args.emplace_back(negY); + args.emplace_back(negZ); + args.emplace_back(posX); + args.emplace_back(posY); + args.emplace_back(posZ); + args.emplace_back(childOffset); + args.emplace_back(unk16); + // [port] unk14 is a child offset; the renderer follows it to recurse + // into child geo commands + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + break; + } + case GeoLayoutOpCode::UnknownCmdE: { + auto coords1X = reader.ReadInt16(); + auto coords1Y = reader.ReadInt16(); + auto coords1Z = reader.ReadInt16(); + auto unkE = reader.ReadInt16(); + auto childOffset = reader.ReadInt16(); + auto unk12 = reader.ReadInt16(); + + args.emplace_back(coords1X); + args.emplace_back(coords1Y); + args.emplace_back(coords1Z); + args.emplace_back(unkE); + args.emplace_back(childOffset); + args.emplace_back(unk12); + // [port] unk10 is a child offset; the renderer follows it to recurse + // into child geo commands + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + break; + } + case GeoLayoutOpCode::UnknownCmdF: { + auto childOffset = reader.ReadUInt16(); + auto unkA = reader.ReadUByte(); + auto unkB = reader.ReadUByte(); + args.emplace_back(childOffset); + args.emplace_back(unkA); + args.emplace_back(unkB); + for (int32_t i = 0; i < 12; i++) { + auto unkCBuf = reader.ReadUByte(); + args.emplace_back(unkCBuf); + } + if (childOffset != 0) { + offsetStack.push_back(localOffset + childOffset); + } + break; + } + case GeoLayoutOpCode::UnknownCmd10: { + auto wrapMode = reader.ReadInt32(); + args.emplace_back(wrapMode); + break; + } + default: + throw std::runtime_error("BK64::GeoLayoutFactory: Unknown OpCode Found " + std::to_string(opCode)); + } + cmds.emplace_back(static_cast<GeoLayoutOpCode>(opCode), cmdLength, args, localOffset); + + if (offsetStack.size() == 0) { + break; + } + } + + return std::make_shared<GeoLayoutData>(cmds); +} + +} // namespace BK64 diff --git a/src/factories/bk64/GeoLayoutFactory.h b/src/factories/bk64/GeoLayoutFactory.h new file mode 100644 index 0000000..703f0e1 --- /dev/null +++ b/src/factories/bk64/GeoLayoutFactory.h @@ -0,0 +1,82 @@ +#pragma once + +#include <factories/BaseFactory.h> + +namespace BK64 { + +typedef std::variant<uint8_t, int8_t, uint16_t, int16_t, uint32_t, int32_t, float> GeoLayoutArg; + +enum class GeoLayoutArgType { U8, S8, U16, S16, U32, S32, FLOAT }; + +enum class GeoLayoutOpCode { + UnknownCmd0, + Sort, + Bone, + LoadDL, + Skinning = 5, + Branch, + UnknownCmd7, + LOD, + ReferencePoint = 10, + Selector = 12, + DrawDistance, + UnknownCmdE, + UnknownCmdF, + UnknownCmd10, +}; + +class GeoLayoutCommand { + public: + GeoLayoutOpCode opCode; + uint32_t cmdLength; + std::vector<GeoLayoutArg> args; + uint32_t originalOffset; // where this command sat in the original N64 binary + + GeoLayoutCommand(GeoLayoutOpCode opCode, uint32_t cmdLength, std::vector<GeoLayoutArg> args, + uint32_t originalOffset = 0) + : opCode(opCode), cmdLength(cmdLength), args(std::move(args)), originalOffset(originalOffset) { + } +}; + +class GeoLayoutData : public IParsedData { + public: + std::vector<GeoLayoutCommand> mCmds; + + GeoLayoutData(std::vector<GeoLayoutCommand> cmds) : mCmds(std::move(cmds)) { + } +}; + +class GeoLayoutHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GeoLayoutBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GeoLayoutCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GeoLayoutModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GeoLayoutFactory : 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(Code, GeoLayoutCodeExporter) REGISTER(Header, GeoLayoutHeaderExporter) + REGISTER(Binary, GeoLayoutBinaryExporter) REGISTER(Modding, GeoLayoutModdingExporter) }; + } + bool SupportModdedAssets() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/GruntyQuestionFactory.cpp b/src/factories/bk64/GruntyQuestionFactory.cpp new file mode 100644 index 0000000..95af785 --- /dev/null +++ b/src/factories/bk64/GruntyQuestionFactory.cpp @@ -0,0 +1,283 @@ +#include "GruntyQuestionFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" + +#define GRUNTY_QUESTION_HEADER_1 0x01 +#define GRUNTY_QUESTION_HEADER_2 0x03 +#define GRUNTY_QUESTION_HEADER_3 0x00 +#define GRUNTY_QUESTION_HEADER_4 0x05 +#define GRUNTY_QUESTION_HEADER_5 0x00 + +#define FORMAT_HEX(x, w) \ + std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec +#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec + +namespace BK64 { + +ExportResult GruntyQuestionHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult GruntyQuestionCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto gruntyQuestion = std::static_pointer_cast<GruntyQuestionData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "u8 " << symbol << "[] = {\n"; + + write << fourSpaceTab << "GRUNTY_QUESTION_HEADER_1" + << ", " + << "GRUNTY_QUESTION_HEADER_2" + << ", " + << "GRUNTY_QUESTION_HEADER_3" + << ", " + << "GRUNTY_QUESTION_HEADER_4" + << ", " + << "GRUNTY_QUESTION_HEADER_5" + << ",\n"; + write << fourSpaceTab << "/* GruntyQuestion */\n"; + write << fourSpaceTab << gruntyQuestion->mText.size() << ",\n"; + for (const auto [cmd, str] : gruntyQuestion->mText) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } + write << ",\n"; + } + write << fourSpaceTab << "/* Options */\n"; + write << fourSpaceTab << gruntyQuestion->mOptions.size() << ",\n"; + for (const auto [cmd, unk0, unk1, str] : gruntyQuestion->mOptions) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + write << ", 0x" << FORMAT_HEX((uint32_t)unk0, 2) << ", 0x" << FORMAT_HEX((uint32_t)unk1, 2); + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } + write << ",\n"; + } + + write << "};\n\n"; + + return offset; +} + +ExportResult BK64::GruntyQuestionBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto gruntyQuestion = std::static_pointer_cast<GruntyQuestionData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKGruntyQuestion, 0); + + writer.Write((uint32_t)gruntyQuestion->mText.size()); + for (const auto& dialogString : gruntyQuestion->mText) { + writer.Write(dialogString.cmd); + writer.Write((uint32_t)dialogString.str.length()); + writer.Write((char*)dialogString.str.data(), + dialogString.str.size()); // [port] Write(string) would prefix the length twice + } + + writer.Write((uint32_t)gruntyQuestion->mOptions.size()); + for (const auto& optionString : gruntyQuestion->mOptions) { + writer.Write(optionString.cmd); + writer.Write(optionString.unk0); + writer.Write(optionString.unk1); + writer.Write((uint32_t)optionString.str.length()); + writer.Write((char*)optionString.str.data(), + optionString.str.size()); // [port] Write(string) would prefix the length twice + } + + writer.Finish(write); + return std::nullopt; +} + +ExportResult BK64::GruntyQuestionModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + const auto gruntyQuestion = std::static_pointer_cast<GruntyQuestionData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "Text"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto [cmd, str] : gruntyQuestion->mText) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + out << str.c_str(); + out << YAML::EndSeq; + } + out << YAML::EndSeq; + + out << YAML::Key << "Options"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto [cmd, unk0, unk1, str] : gruntyQuestion->mOptions) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + out << YAML_HEX((uint32_t)unk0); + out << YAML_HEX((uint32_t)unk1); + out << str.c_str(); + out << YAML::EndSeq; + } + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +// The actual question data. Same shape whether we got here via US or PAL English. +static std::shared_ptr<GruntyQuestionData> ParseGruntyBlock(LUS::BinaryReader& reader) { + std::vector<DialogString> text; + std::vector<OptionString> options; + + auto textSize = reader.ReadUByte(); + + for (uint8_t i = 0; i < textSize - 3; i++) { + DialogString dialogString; + dialogString.cmd = reader.ReadUByte(); + auto strLen = reader.ReadUByte(); + dialogString.str = reader.ReadString(strLen); + text.push_back(dialogString); + } + + for (uint8_t i = textSize - 3; i < textSize; i++) { + OptionString optionString; + optionString.cmd = reader.ReadUByte(); + auto strLen = reader.ReadUByte(); + optionString.unk0 = reader.ReadUByte(); + optionString.unk1 = reader.ReadUByte(); + optionString.str = reader.ReadString(strLen - 2); + options.push_back(optionString); + } + + return std::make_shared<GruntyQuestionData>(text, options); +} + +std::optional<std::shared_ptr<IParsedData>> GruntyQuestionFactory::parse(std::vector<uint8_t>& buffer, + YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + + auto header1 = reader.ReadInt8(); + auto header2 = reader.ReadInt8(); + auto header3 = reader.ReadInt8(); + + if (header1 == GRUNTY_QUESTION_HEADER_1 && header2 == GRUNTY_QUESTION_HEADER_2 && + header3 == GRUNTY_QUESTION_HEADER_3) { + // US: 01 03 00 05 00, then the question data + reader.ReadInt8(); // header4 (0x05) + reader.ReadInt8(); // header5 (0x00) + return ParseGruntyBlock(reader); + } + + if (header1 == 0x03 && header2 == 0x03 && header3 == 0x00) { + // PAL: 03 03 00, then 3 x LE u16 offsets (EN/FR/DE start positions) + uint16_t enOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); + reader.ReadUByte(); + reader.ReadUByte(); // skip FR offset + reader.ReadUByte(); + reader.ReadUByte(); // skip DE offset + // We're now at byte 9 = enOffset, so just read the English block. + return ParseGruntyBlock(reader); + } + + SPDLOG_ERROR("Invalid Header For BK64 GruntyQuestion {}: {:02X} {:02X} {:02X}", symbol, header1, header2, header3); + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> GruntyQuestionFactory::parse_modding(std::vector<uint8_t>& buffer, + YAML::Node& node) { + YAML::Node assetNode; + + try { + std::string text((char*)buffer.data(), buffer.size()); + assetNode = YAML::Load(text.c_str()); + } catch (YAML::ParserException& e) { + SPDLOG_ERROR("Failed to parse message data: {}", e.what()); + SPDLOG_ERROR("{}", (char*)buffer.data()); + return std::nullopt; + } + + const auto info = assetNode.begin()->second; + + std::vector<DialogString> text; + std::vector<OptionString> options; + + auto textNode = info["Text"]; + auto optionsNode = info["Options"]; + + for (YAML::iterator it = textNode.begin(); it != textNode.end(); ++it) { + DialogString dialogString; + dialogString.cmd = (*it)[0].as<uint32_t>(); + dialogString.str = (*it)[1].as<std::string>(); + dialogString.str += '\0'; + text.push_back(dialogString); + } + + uint32_t i = 0; + for (YAML::iterator it = optionsNode.begin(); it != optionsNode.end(); ++it) { + if (i >= 3) { + SPDLOG_WARN("BK64 GruntyQuestion: Only 3 Options Allowed; extra options ignored"); + break; + } + OptionString optionString; + optionString.cmd = (*it)[0].as<uint32_t>(); + optionString.unk0 = (*it)[1].as<uint32_t>(); + optionString.unk1 = (*it)[2].as<uint32_t>(); + optionString.str = (*it)[3].as<std::string>(); + optionString.str += '\0'; + options.push_back(optionString); + i++; + } + + if (i != 3) { + throw std::runtime_error("BK64 GruntyQuestion: Requires Exactly 3 Options"); + } + + return std::make_shared<GruntyQuestionData>(text, options); +} + +} // namespace BK64 diff --git a/src/factories/bk64/GruntyQuestionFactory.h b/src/factories/bk64/GruntyQuestionFactory.h new file mode 100644 index 0000000..da490ea --- /dev/null +++ b/src/factories/bk64/GruntyQuestionFactory.h @@ -0,0 +1,57 @@ +#pragma once + +#include "DialogFactory.h" +#include <factories/BaseFactory.h> + +namespace BK64 { + +typedef struct OptionString { + uint8_t cmd; + uint8_t unk0; + uint8_t unk1; + std::string str; +} OptionString; + +class GruntyQuestionData : public IParsedData { + public: + std::vector<DialogString> mText; + std::vector<OptionString> mOptions; + + GruntyQuestionData(std::vector<DialogString> text, std::vector<OptionString> options) + : mText(std::move(text)), mOptions(std::move(options)) { + } +}; + +class GruntyQuestionHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GruntyQuestionBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GruntyQuestionCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GruntyQuestionModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class GruntyQuestionFactory : 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(Code, GruntyQuestionCodeExporter) REGISTER(Header, GruntyQuestionHeaderExporter) + REGISTER(Binary, GruntyQuestionBinaryExporter) REGISTER(Modding, GruntyQuestionModdingExporter) }; + } + bool SupportModdedAssets() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/MapFactory.cpp b/src/factories/bk64/MapFactory.cpp new file mode 100644 index 0000000..5c48568 --- /dev/null +++ b/src/factories/bk64/MapFactory.cpp @@ -0,0 +1,1029 @@ +#include "MapFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" +#include <iomanip> +#include <sstream> + +namespace BK64 { + +// Prop type names, keyed by the flag-derived type code +static const std::unordered_map<uint8_t, std::string> sPropTypeNames = { + { 0x0, "Sprite" }, // is_actor=0, is_3d=0 + { 0x1, "Actor" }, // is_actor=1 + { 0x2, "Model" }, // is_actor=0, is_3d=1 +}; + +// What the bit6 category field means +static const std::unordered_map<uint8_t, std::string> sNodePropCategories = { + { 0x6, "ActorSpawn" }, + { 0x7, "Warp" }, + { 0x9, "Trigger" }, + { 0xA, "Event" }, +}; + +// Chunk parsers, defined further down +static void ParseCubeSection(LUS::BinaryReader& reader, std::shared_ptr<MapData>& map, size_t totalSize, + const std::string& symbol); +static void ParseCameraSection(LUS::BinaryReader& reader, std::shared_ptr<MapData>& map, const std::string& symbol); +static void ParseLightingSection(LUS::BinaryReader& reader, std::shared_ptr<MapData>& map, const std::string& symbol); + +// Flag bits in the prop discriminator byte +static constexpr uint8_t PROP_FLAG_ACTOR = 0x01; // is_actor bit +static constexpr uint8_t PROP_FLAG_3D = 0x02; // is_3d bit +static constexpr uint8_t PROP_FLAG_VISIBLE = 0x10; // visibility bit +static constexpr uint8_t PROP_FLAG_COLLISION = 0x20; // collision bit (ModelProps) + +// Actor wins over 3D wins over sprite +static inline uint8_t GetPropType(uint8_t flags) { + if (flags & PROP_FLAG_ACTOR) + return 0x1; // Actor + if (flags & PROP_FLAG_3D) + return 0x2; // Model + return 0x0; // Sprite +} + +static inline const char* GetPropTypeName(uint8_t flags) { + auto type = GetPropType(flags); + auto it = sPropTypeNames.find(type); + return (it != sPropTypeNames.end()) ? it->second.c_str() : "Unknown"; +} + +static inline const char* GetNodePropCategoryName(uint8_t bit6) { + auto it = sNodePropCategories.find(bit6); + return (it != sNodePropCategories.end()) ? it->second.c_str() : "Unknown"; +} + +ExportResult MapHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + auto map = std::static_pointer_cast<MapData>(raw); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + // Each cube's NodeProp/Prop arrays need an extern decl before the Cube array references them + for (size_t cubeIdx = 0; cubeIdx < map->mCubes.size(); cubeIdx++) { + const auto& cube = map->mCubes[cubeIdx]; + + if (!cube.nodeProps.empty()) { + write << "extern NodeProp " << symbol << "_Cube" << cubeIdx << "_NodeProps[" << cube.nodeProps.size() + << "];\n"; + } + + if (!cube.props.empty()) { + write << "extern Prop " << symbol << "_Cube" << cubeIdx << "_Props[" << cube.props.size() << "];\n"; + } + } + + write << "extern Cube " << symbol << "[" << map->mCubes.size() << "];\n"; + return std::nullopt; +} + +ExportResult MapCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto map = std::static_pointer_cast<MapData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + // OTR mode: just the cube array, prop pointers stay NULL (resolved from the OTR at load) + if (Companion::Instance->IsOTRMode()) { + write << "Cube " << symbol << "[] = {\n"; + for (size_t cubeIdx = 0; cubeIdx < map->mCubes.size(); cubeIdx++) { + const auto& cube = map->mCubes[cubeIdx]; + write << fourSpaceTab << "{\n"; + write << fourSpaceTab << fourSpaceTab << "/* coord */ " << cube.x << ", " << cube.y << ", " << cube.z + << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* prop1Cnt */ " << cube.prop1Cnt << ", /* prop2Cnt */ " + << cube.prop2Cnt << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* unk0_4 */ " << cube.unk0_4 << ",\n"; + + write << fourSpaceTab << fourSpaceTab << "/* prop1Ptr */ NULL,\n"; + write << fourSpaceTab << fourSpaceTab << "/* prop2Ptr */ NULL\n"; + + write << fourSpaceTab << "},\n"; + } + write << "};\n\n"; + return offset; + } + + // Export NodeProps for each cube + for (size_t cubeIdx = 0; cubeIdx < map->mCubes.size(); cubeIdx++) { + const auto& cube = map->mCubes[cubeIdx]; + + if (!cube.nodeProps.empty()) { + write << "NodeProp " << symbol << "_Cube" << cubeIdx << "_NodeProps[] = {\n"; + for (const auto& nodeProp : cube.nodeProps) { + write << fourSpaceTab << "{\n"; + write << fourSpaceTab << fourSpaceTab << "/* pos */ " << nodeProp.position[0] << ", " + << nodeProp.position[1] << ", " << nodeProp.position[2] << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* radius */ " << nodeProp.radius << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* type */ " << (uint32_t)nodeProp.bit6; + + if (nodeProp.bit6 == 6) { + write << ", /* actor */ " << std::hex << "0x" << nodeProp.unk8 << std::dec; + } else if (nodeProp.bit6 == 7) { + write << ", /* warp */ " << std::hex << "0x" << nodeProp.unk8 << std::dec; + } else if (nodeProp.bit6 == 9) { + write << ", /* trigger */ " << std::hex << "0x" << nodeProp.unk8 << std::dec; + } else if (nodeProp.bit6 == 0xA) { + write << ", /* event */ " << std::hex << "0x" << nodeProp.unk8 << std::dec; + } else { + write << ", " << std::hex << "0x" << nodeProp.unk8 << std::dec; + } + + write << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* yaw */ " << nodeProp.yaw << ", /* scale */ " + << nodeProp.scale << "\n"; + write << fourSpaceTab << "},\n"; + } + write << "};\n\n"; + } + + if (!cube.props.empty()) { + write << "Prop " << symbol << "_Cube" << cubeIdx << "_Props[] = {\n"; + for (const auto& prop : cube.props) { + const uint8_t flags = prop.raw[10]; + const char* typeName = GetPropTypeName(flags); + + write << fourSpaceTab << "{ ." << typeName << " = { "; + + if (flags & PROP_FLAG_ACTOR) { + // ActorProp: marker is always NULL in ROM (engine fills it in), + // position and flags are the real data + write << "NULL, { " << prop.actor.position[0] << ", " << prop.actor.position[1] << ", " + << prop.actor.position[2] << " }, "; + write << std::hex << "0x" << prop.actor.flags << std::dec; + } else if (flags & PROP_FLAG_3D) { + // ModelProp field order: unk0(2), yaw(1), roll(1), position[3](6), + // scale(1), flags(1) + write << std::hex << "0x" << prop.model.unk0 << std::dec << ", "; + write << (int)prop.model.yaw << ", " << (int)prop.model.roll << ", "; + write << "{ " << prop.model.position[0] << ", " << prop.model.position[1] << ", " + << prop.model.position[2] << " }, "; + write << (int)prop.model.scale << ", "; + write << std::hex << "0x" << (int)prop.model.flags << std::dec; + } else { + // SpriteProp field order: word0(4), unk4[3](6), wordA(2) + write << std::hex << "0x" << prop.sprite.word0 << std::dec << ", "; + write << "{ " << prop.sprite.unk4[0] << ", " << prop.sprite.unk4[1] << ", " << prop.sprite.unk4[2] + << " }, "; + write << std::hex << "0x" << prop.sprite.wordA << std::dec; + } + + write << " } },\n"; + } + write << "};\n\n"; + } + } + + // Export cube array + write << "Cube " << symbol << "[] = {\n"; + for (size_t cubeIdx = 0; cubeIdx < map->mCubes.size(); cubeIdx++) { + const auto& cube = map->mCubes[cubeIdx]; + write << fourSpaceTab << "{\n"; + write << fourSpaceTab << fourSpaceTab << "/* coord */ " << cube.x << ", " << cube.y << ", " << cube.z << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* prop1Cnt */ " << cube.prop1Cnt << ", /* prop2Cnt */ " + << cube.prop2Cnt << ",\n"; + write << fourSpaceTab << fourSpaceTab << "/* unk0_4 */ " << cube.unk0_4 << ",\n"; + + if (!cube.nodeProps.empty()) { + write << fourSpaceTab << fourSpaceTab << "/* prop1Ptr */ " << symbol << "_Cube" << cubeIdx + << "_NodeProps,\n"; + } else { + write << fourSpaceTab << fourSpaceTab << "/* prop1Ptr */ NULL,\n"; + } + + if (!cube.props.empty()) { + write << fourSpaceTab << fourSpaceTab << "/* prop2Ptr */ " << symbol << "_Cube" << cubeIdx << "_Props\n"; + } else { + write << fourSpaceTab << fourSpaceTab << "/* prop2Ptr */ NULL\n"; + } + + write << fourSpaceTab << "},\n"; + } + write << "};\n\n"; + + return offset; +} + +ExportResult MapBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto map = std::static_pointer_cast<MapData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKMap, 0); + + // --- Cube section --- + writer.Write((uint32_t)map->mCubes.size()); + writer.Write(map->mCubeMin[0]); + writer.Write(map->mCubeMin[1]); + writer.Write(map->mCubeMin[2]); + writer.Write(map->mCubeMax[0]); + writer.Write(map->mCubeMax[1]); + writer.Write(map->mCubeMax[2]); + + for (const auto& cube : map->mCubes) { + uint32_t cubeHeader = ((cube.x & 0x1F) << 27) | ((cube.y & 0x1F) << 22) | ((cube.z & 0x1F) << 17) | + ((cube.prop1Cnt & 0x3F) << 11) | ((cube.prop2Cnt & 0x3F) << 5) | + ((cube.unk0_4 & 0x1F) << 0); + writer.Write(cubeHeader); + + // NodeProps inline + writer.Write((uint32_t)cube.nodeProps.size()); + for (const auto& np : cube.nodeProps) { + writer.Write(np.position[0]); + writer.Write(np.position[1]); + writer.Write(np.position[2]); + writer.Write(static_cast<uint16_t>(np.radius)); + writer.Write(static_cast<uint8_t>(np.bit6)); + writer.Write(static_cast<uint8_t>(np.bit0)); + writer.Write(np.unk8); + writer.Write(np.unkA); + writer.Write(np.padB); + writer.Write(static_cast<uint16_t>(np.yaw)); + writer.Write(np.scale); + writer.Write(static_cast<uint16_t>(np.unk10_31)); + writer.Write(static_cast<uint16_t>(np.unk10_19)); + writer.Write(static_cast<uint8_t>(np.pad10_7)); + writer.Write(static_cast<uint8_t>(np.unk10_6)); + writer.Write(static_cast<uint8_t>(np.pad10_5)); + writer.Write(static_cast<uint8_t>(np.unk10_0)); + } + + // Props inline (raw 12-byte structs) + writer.Write((uint32_t)cube.props.size()); + for (const auto& prop : cube.props) { + writer.Write((char*)prop.raw, 12); + } + } + + // --- Camera section --- + writer.Write((uint32_t)map->mCameraNodes.size()); + for (const auto& cam : map->mCameraNodes) { + writer.Write(cam.index); + writer.Write(cam.type); + switch (cam.type) { + case 1: + writer.Write(cam.data.type1.position[0]); + writer.Write(cam.data.type1.position[1]); + writer.Write(cam.data.type1.position[2]); + writer.Write(cam.data.type1.horizontalSpeed); + writer.Write(cam.data.type1.verticalSpeed); + writer.Write(cam.data.type1.rotation); + writer.Write(cam.data.type1.accelaration); + writer.Write(cam.data.type1.pitchYawRoll[0]); + writer.Write(cam.data.type1.pitchYawRoll[1]); + writer.Write(cam.data.type1.pitchYawRoll[2]); + writer.Write(cam.data.type1.unknownFlag); + break; + case 2: + writer.Write(cam.data.type2.position[0]); + writer.Write(cam.data.type2.position[1]); + writer.Write(cam.data.type2.position[2]); + writer.Write(cam.data.type2.pitchYawRoll[0]); + writer.Write(cam.data.type2.pitchYawRoll[1]); + writer.Write(cam.data.type2.pitchYawRoll[2]); + break; + case 3: + writer.Write(cam.data.type3.position[0]); + writer.Write(cam.data.type3.position[1]); + writer.Write(cam.data.type3.position[2]); + writer.Write(cam.data.type3.horizontalSpeed); + writer.Write(cam.data.type3.verticalSpeed); + writer.Write(cam.data.type3.rotation); + writer.Write(cam.data.type3.accelaration); + writer.Write(cam.data.type3.closeDistance); + writer.Write(cam.data.type3.farDistance); + writer.Write(cam.data.type3.pitchYawRoll[0]); + writer.Write(cam.data.type3.pitchYawRoll[1]); + writer.Write(cam.data.type3.pitchYawRoll[2]); + writer.Write(cam.data.type3.unknownFlag); + break; + case 4: + writer.Write(cam.data.type4.unknownFlag); + break; + case 0: + // Type 0 is legitimately empty + break; + default: + SPDLOG_WARN("[BK64:MAP] Binary export: unknown camera type {}", cam.type); + break; + } + } + + // --- Lighting section --- + writer.Write((uint32_t)map->mLightingVectors.size()); + for (const auto& light : map->mLightingVectors) { + writer.Write(light.position[0]); + writer.Write(light.position[1]); + writer.Write(light.position[2]); + writer.Write(light.fadeRadii[0]); + writer.Write(light.fadeRadii[1]); + writer.Write(light.rgb[0]); + writer.Write(light.rgb[1]); + writer.Write(light.rgb[2]); + } + + writer.Finish(write); + return OffsetEntry{ 0 }; +} + +ExportResult MapModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + const auto map = std::static_pointer_cast<MapData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "CubeCount"; + out << YAML::Value << map->mCubes.size(); + out << YAML::Key << "Cubes"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (size_t cubeIdx = 0; cubeIdx < map->mCubes.size(); cubeIdx++) { + const auto& cube = map->mCubes[cubeIdx]; + + out << YAML::BeginMap; + out << YAML::Key << "Position"; + out << YAML::Value; + out << YAML::Flow; + out << YAML::BeginMap; + out << YAML::Key << "X" << YAML::Value << cube.x; + out << YAML::Key << "Y" << YAML::Value << cube.y; + out << YAML::Key << "Z" << YAML::Value << cube.z; + out << YAML::EndMap; + + out << YAML::Key << "Unknown"; + out << YAML::Value << cube.unk0_4; + + // Export NodeProps + if (!cube.nodeProps.empty()) { + out << YAML::Key << "NodeProps"; + out << YAML::Value; + out << YAML::BeginSeq; + + for (const auto& nodeProp : cube.nodeProps) { + out << YAML::BeginMap; + out << YAML::Key << "Position"; + out << YAML::Value; + out << YAML::Flow; + out << YAML::BeginMap; + out << YAML::Key << "X" << YAML::Value << nodeProp.position[0]; + out << YAML::Key << "Y" << YAML::Value << nodeProp.position[1]; + out << YAML::Key << "Z" << YAML::Value << nodeProp.position[2]; + out << YAML::EndMap; + + out << YAML::Key << "Radius" << YAML::Value << nodeProp.radius; + out << YAML::Key << "Category" << YAML::Value << GetNodePropCategoryName(nodeProp.bit6); + out << YAML::Key << "Type" << YAML::Value << (uint32_t)nodeProp.bit6; + out << YAML::Key << "ActorID" << YAML::Value << YAML::Hex << nodeProp.unk8 << YAML::Dec; + out << YAML::Key << "Yaw" << YAML::Value << nodeProp.yaw; + out << YAML::Key << "Scale" << YAML::Value << nodeProp.scale; + + out << YAML::EndMap; + } + + out << YAML::EndSeq; + } + + // Export Props (one of ModelProp / SpriteProp / ActorProp) + if (!cube.props.empty()) { + out << YAML::Key << "Props"; + out << YAML::Value; + out << YAML::BeginSeq; + + for (const auto& prop : cube.props) { + // Type comes from the discriminator flags at offset 0xA (byte 10) + const uint8_t flags = prop.raw[10]; + const char* typeName = GetPropTypeName(flags); + bool is_visible = (flags & PROP_FLAG_VISIBLE) != 0; + + out << YAML::BeginMap; + out << YAML::Key << "Type" << YAML::Value << typeName; + + if (flags & PROP_FLAG_ACTOR) { + // ActorProp: marker is NULL in ROM; position and flags are the + // real ROM data + out << YAML::Key << "Position"; + out << YAML::Value; + out << YAML::Flow; + out << YAML::BeginMap; + out << YAML::Key << "X" << YAML::Value << prop.actor.position[0]; + out << YAML::Key << "Y" << YAML::Value << prop.actor.position[1]; + out << YAML::Key << "Z" << YAML::Value << prop.actor.position[2]; + out << YAML::EndMap; + out << YAML::Key << "Flags" << YAML::Value << YAML::Hex << prop.actor.flags << YAML::Dec; + } else if (flags & PROP_FLAG_3D) { + // ModelProp - static 3D model + uint16_t model_index = prop.model.unk0 & 0xFFF; + out << YAML::Key << "ModelIndex" << YAML::Value << model_index; + out << YAML::Key << "AssetID" << YAML::Value << YAML::Hex << (model_index + 0x2d1) << YAML::Dec; + out << YAML::Key << "Position"; + out << YAML::Value; + out << YAML::Flow; + out << YAML::BeginMap; + out << YAML::Key << "X" << YAML::Value << prop.model.position[0]; + out << YAML::Key << "Y" << YAML::Value << prop.model.position[1]; + out << YAML::Key << "Z" << YAML::Value << prop.model.position[2]; + out << YAML::EndMap; + out << YAML::Key << "Yaw" << YAML::Value << (int)prop.model.yaw * 2; + out << YAML::Key << "Roll" << YAML::Value << (int)prop.model.roll * 2; + out << YAML::Key << "Scale" << YAML::Value << (float)prop.model.scale / 100.0f; + } else { + // SpriteProp - 2D billboard. word0 bit layout (32-bit big-endian): + // sprite_id[31:20], pad[19], r[18:16], g[15:13], b[12:10], + // scale[9:2], mirror[1], pad[0] + uint16_t sprite_index = (prop.sprite.word0 >> 20) & 0xFFF; + out << YAML::Key << "SpriteIndex" << YAML::Value << sprite_index; + out << YAML::Key << "AssetID" << YAML::Value << YAML::Hex << (sprite_index + 0x572) << YAML::Dec; + uint8_t r = (prop.sprite.word0 >> 16) & 0x7; + uint8_t g = (prop.sprite.word0 >> 13) & 0x7; + uint8_t b = (prop.sprite.word0 >> 10) & 0x7; + out << YAML::Key << "Color"; + out << YAML::Value; + out << YAML::Flow; + out << YAML::BeginMap; + out << YAML::Key << "R" << YAML::Value << (int)r; + out << YAML::Key << "G" << YAML::Value << (int)g; + out << YAML::Key << "B" << YAML::Value << (int)b; + out << YAML::EndMap; + uint8_t scale = (prop.sprite.word0 >> 2) & 0xFF; + out << YAML::Key << "Scale" << YAML::Value << (float)scale / 100.0f; + bool mirrored = (prop.sprite.word0 >> 1) & 0x1; + out << YAML::Key << "Mirrored" << YAML::Value << (mirrored ? "true" : "false"); + // Frame is bits [15:11] of wordA (16-bit big-endian at offset 0x0A) + uint8_t frame = (prop.sprite.wordA >> 11) & 0x1F; + out << YAML::Key << "Frame" << YAML::Value << (int)frame; + } + + out << YAML::Key << "Visible" << YAML::Value << (is_visible ? "true" : "false"); + + out << YAML::EndMap; + } + + out << YAML::EndSeq; + } + + out << YAML::EndMap; + } + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + write << out.c_str(); + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> MapFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + std::vector<uint8_t> decodedData; + + try { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + if (!segment.data || segment.size == 0) { + SPDLOG_ERROR("Decompression returned null for symbol: {}", symbol); + return std::nullopt; + } + + decodedData.assign(segment.data, segment.data + segment.size); + + // BK64 map format, post-decompression. A run of chunks, each led by a + // type marker (see gsworld_load): + // Type 0x00: End of file + // Type 0x01: Cube data section (grid bounds + cube definitions) + // Type 0x02: Reserved/empty + // Type 0x03: Camera node section + // Type 0x04: Lighting vector section + // + // Cube Section Format (type 0x01): + // - Min cube position (s32[3]) + // - Max cube position (s32[3]) + // - For each cube in grid: CubeHeader + NodeProps + Props + // - CubeHeader: 4 bytes (x:5, y:5, z:5, prop1Cnt:6, prop2Cnt:6, unk0_4:5) + // - NodeProp: 20 bytes each + // - Prop: 12 bytes each + + LUS::BinaryReader reader(decodedData.data(), decodedData.size()); + reader.SetEndianness(Torch::Endianness::Big); + + auto map = std::make_shared<MapData>(); + + // Walk chunks until the 0x00 end marker (or we run out of data) + while (reader.GetBaseAddress() < decodedData.size()) { + uint8_t chunkType = reader.ReadUByte(); + + if (chunkType == 0x00) { + // End of file + break; + } else if (chunkType == 0x01) { + ParseCubeSection(reader, map, decodedData.size(), symbol); + } else if (chunkType == 0x02) { + // Reserved/empty in the decomp; nothing to read + continue; + } else if (chunkType == 0x03) { + ParseCameraSection(reader, map, symbol); + } else if (chunkType == 0x04) { + ParseLightingSection(reader, map, symbol); + } else { + SPDLOG_WARN("[BK64:MAP] Unknown chunk type 0x{:02X} at offset 0x{:X} " + "in asset {}", + chunkType, reader.GetBaseAddress() - 1, symbol); + break; + } + } + + return map; + + } catch (const std::exception& e) { + SPDLOG_ERROR("MapFactory parse error for {}: {}", symbol, e.what()); + return std::nullopt; + } +} + +// Reads the inner contents of one cube (this is code7AF80_initCubeFromFile). +// Reached when the per-cube wrapper below hits marker 0x03. +static void ReadCubeContent(LUS::BinaryReader& reader, CubeData& cube, const std::string& symbol) { + // Layout from the decomp (code7AF80_initCubeFromFile in + // actor_cubepropsystem.c): + // Optional NodeProps (new): 0x0A + count (u8) + 0x0B + NodeProp data (20 + // bytes each) Optional NodeProps (old): 0x06 + count (u8) + 0x07 + + // OtherNode data (12 bytes each) Optional Props: 0x08 + count + // (u8) + 0x09 + Prop data (12 bytes each) + // All optional — a cube may carry nothing at all. + + // NodeProps + size_t peekPos = reader.GetBaseAddress(); + uint8_t marker = reader.ReadUByte(); + reader.Seek(peekPos, LUS::SeekOffsetType::Start); + + if (marker == 0x0A || marker == 0x06) { + marker = reader.ReadUByte(); // consume 0x0A or 0x06 + uint8_t nodeCount = reader.ReadUByte(); + + if (nodeCount > 0) { + uint8_t dataMarker = reader.ReadUByte(); // expect 0x0B or 0x07 + if ((marker == 0x0A && dataMarker != 0x0B) || (marker == 0x06 && dataMarker != 0x07)) { + SPDLOG_WARN("[BK64:MAP] Expected data marker {} after 0x{:02X}, got " + "0x{:02X} at offset 0x{:X} in asset {}", + marker == 0x0A ? "0x0B" : "0x07", marker, dataMarker, reader.GetBaseAddress() - 1, symbol); + } + } + + cube.prop1Cnt = nodeCount; + cube.unk0_4 = nodeCount; + + for (uint32_t j = 0; j < nodeCount; j++) { + if (marker == 0x0A) { + NodeProp nodeProp; + nodeProp.position[0] = reader.ReadInt16(); + nodeProp.position[1] = reader.ReadInt16(); + nodeProp.position[2] = reader.ReadInt16(); + + uint16_t f1 = reader.ReadUInt16(); + nodeProp.radius = (f1 >> 7) & 0x1FF; + nodeProp.bit6 = (f1 >> 1) & 0x3F; + nodeProp.bit0 = (f1 >> 0) & 0x01; + + nodeProp.unk8 = reader.ReadUInt16(); + nodeProp.unkA = reader.ReadUByte(); + nodeProp.padB = reader.ReadUByte(); + + uint32_t f2 = reader.ReadUInt32(); + nodeProp.yaw = (f2 >> 23) & 0x1FF; + nodeProp.scale = (f2 >> 0) & 0x7FFFFF; + + uint32_t f3 = reader.ReadUInt32(); + nodeProp.unk10_31 = (f3 >> 20) & 0xFFF; + nodeProp.unk10_19 = (f3 >> 8) & 0xFFF; + nodeProp.pad10_7 = (f3 >> 7) & 0x01; + nodeProp.unk10_6 = (f3 >> 6) & 0x01; + nodeProp.pad10_5 = (f3 >> 2) & 0x0F; + nodeProp.unk10_0 = (f3 >> 0) & 0x03; + + cube.nodeProps.push_back(nodeProp); + } else { + // OtherNode (0x06 format): 12 bytes. Never appears in a retail ROM; + // we only handle it so a corrupt/modded ROM doesn't desync the reader. + uint32_t word0 = reader.ReadUInt32(); + uint32_t word4 = reader.ReadUInt32(); + uint32_t word8 = reader.ReadUInt32(); + + // Push a zeroed NodeProp so the array count still lines up + NodeProp nodeProp = {}; + cube.nodeProps.push_back(nodeProp); + + SPDLOG_ERROR("[BK64:MAP] Encountered OtherNode (0x06 format) at offset " + "0x{:X} in asset {}. " + "This format is NEVER used in released ROMs and indicates " + "ROM corruption or modification. " + "Converted to zero-initialized NodeProp, but level data " + "is likely broken.", + reader.GetBaseAddress() - 12, symbol); + } + } + } + + // Props + peekPos = reader.GetBaseAddress(); + marker = reader.ReadUByte(); + reader.Seek(peekPos, LUS::SeekOffsetType::Start); + + if (marker == 0x08) { + reader.ReadUByte(); // consume 0x08 + uint8_t propCount = reader.ReadUByte(); + + if (propCount > 0) { + uint8_t dataMarker = reader.ReadUByte(); // expect 0x09 + if (dataMarker != 0x09) { + SPDLOG_WARN("[BK64:MAP] Expected data marker 0x09 after 0x08, got " + "0x{:02X} at offset 0x{:X} in asset {}", + dataMarker, reader.GetBaseAddress() - 1, symbol); + } + } + + cube.prop2Cnt = propCount; + for (uint32_t j = 0; j < propCount; j++) { + Prop prop; + reader.Read((char*)prop.raw, 12); + cube.props.push_back(prop); + } + } +} + +// Reads one cube's slot. The gccube wrapper (__code7AF80_initCubeFromFile) loops +// until it hits 0x01, which terminates the cube. Markers seen along the way: +// 0x03 → real cube content (calls code7AF80_initCubeFromFile) +// 0x00 → skip 6 words (two 3-word padding groups) +// 0x02 → skip 3 words +static void ReadCubeData(LUS::BinaryReader& reader, CubeData& cube, const std::string& symbol) { + while (reader.GetBaseAddress() < reader.GetLength()) { + uint8_t marker = reader.ReadUByte(); + + if (marker == 0x01) { + // Per-cube terminator; on to the next grid position + break; + } else if (marker == 0x03) { + ReadCubeContent(reader, cube, symbol); + } else if (marker == 0x00) { + // Padding: two groups of 3 words (6 × s32 = 24 bytes) + reader.Seek(reader.GetBaseAddress() + 24, LUS::SeekOffsetType::Start); + } else if (marker == 0x02) { + // One group of 3 words + reader.Seek(reader.GetBaseAddress() + 12, LUS::SeekOffsetType::Start); + } else { + SPDLOG_WARN("[BK64:MAP] Unexpected per-cube marker 0x{:02X} at offset " + "0x{:X} in asset {}", + marker, reader.GetBaseAddress() - 1, symbol); + break; + } + } +} + +static void ParseCubeSection(LUS::BinaryReader& reader, std::shared_ptr<MapData>& map, size_t totalSize, + const std::string& symbol) { + // The section opens with its own 0x01 sub-marker — not the chunk-type 0x01 the + // outer loop already ate. Source: file_getNWords_ifExpected(fp, 1, from, 3) + uint8_t innerMarker = reader.ReadUByte(); + if (innerMarker != 0x01) { + SPDLOG_WARN("[BK64:MAP] Expected inner marker 0x01 at cube section start, " + "got 0x{:02X} at offset 0x{:X} in asset {}", + innerMarker, reader.GetBaseAddress() - 1, symbol); + return; + } + + // from[0..2] then to[0..2], back to back — to[] has no marker in front of it + // (file_getNWords reads it unconditionally) + int32_t from[3], to[3]; + from[0] = reader.ReadInt32(); + from[1] = reader.ReadInt32(); + from[2] = reader.ReadInt32(); + to[0] = reader.ReadInt32(); + to[1] = reader.ReadInt32(); + to[2] = reader.ReadInt32(); + + map->mCubeMin[0] = from[0]; + map->mCubeMin[1] = from[1]; + map->mCubeMin[2] = from[2]; + map->mCubeMax[0] = to[0]; + map->mCubeMax[1] = to[1]; + map->mCubeMax[2] = to[2]; + + int32_t countX = to[0] - from[0] + 1; + int32_t countY = to[1] - from[1] + 1; + int32_t countZ = to[2] - from[2] + 1; + + if (countX <= 0 || countY <= 0 || countZ <= 0) { + SPDLOG_WARN("[BK64:MAP] Invalid cube bounds: from ({},{},{}) to ({},{},{}) " + "in asset {}", + from[0], from[1], from[2], to[0], to[1], to[2], symbol); + return; + } + + int32_t totalCubes = countX * countY * countZ; + + SPDLOG_INFO("[BK64:MAP] {} cube section: from ({},{},{}) to ({},{},{}) = {} cubes", symbol, from[0], from[1], + from[2], to[0], to[1], to[2], totalCubes); + + // Same nesting as the source: X outer, Z inner. Order matters — cubes are + // stored in this sequence, so don't reshuffle the loops. + for (int32_t x = from[0]; x <= to[0]; x++) { + for (int32_t y = from[1]; y <= to[1]; y++) { + for (int32_t z = from[2]; z <= to[2]; z++) { + CubeData cube; + cube.x = x; + cube.y = y; + cube.z = z; + cube.prop1Cnt = cube.prop2Cnt = cube.unk0_4 = 0; + + // Runs until it eats the 0x01 per-cube terminator + ReadCubeData(reader, cube, symbol); + + SPDLOG_INFO("[BK64:MAP] {} cube ({},{},{}) nodeProps={} props={}", symbol, x, y, z, + cube.nodeProps.size(), cube.props.size()); + + map->mCubes.push_back(cube); + } + } + } + + // Section closes with a 0x00 (file_isNextByteExpected(fp, 0) in cubeList_fromFile) + size_t peekPos = reader.GetBaseAddress(); + uint8_t endMarker = reader.ReadUByte(); + reader.Seek(peekPos, LUS::SeekOffsetType::Start); + + if (endMarker != 0x00) { + SPDLOG_WARN("[BK64:MAP] Expected end marker 0x00 after {} cubes, got " + "0x{:02X} at offset 0x{:X} in asset {}", + totalCubes, endMarker, reader.GetBaseAddress(), symbol); + } else { + reader.ReadUByte(); // consume 0x00 + } +} + +// Camera node section (chunk type 0x03) +static void ParseCameraSection(LUS::BinaryReader& reader, std::shared_ptr<MapData>& map, const std::string& symbol) { + // ncCameraNodeList_fromFile format: + // while(next byte != 0x00): + // file_getShort_ifExpected(fp, 0x01, &index) → marker 0x01 + s16 + // file_getByte_ifExpected(fp, 0x02, &type) → marker 0x02 + u8 + // cameraNodeTypeN_fromFile(fp, this): + // inner sub-loop until 0x00, each field group prefixed by its own + // marker byte + while (reader.GetBaseAddress() + 1 < reader.GetLength()) { + // Peek for the section terminator + size_t peekPos = reader.GetBaseAddress(); + uint8_t peekMarker = reader.ReadUByte(); + reader.Seek(peekPos, LUS::SeekOffsetType::Start); + + if (peekMarker == 0x00) { + reader.ReadUByte(); // eat the section terminator + break; + } + + // Each node starts with 0x01 + uint8_t nodeMarker = reader.ReadUByte(); + if (nodeMarker != 0x01) { + SPDLOG_WARN("[BK64:MAP] Expected node marker 0x01, got 0x{:02X} at " + "offset 0x{:X} in asset {}", + nodeMarker, reader.GetBaseAddress() - 1, symbol); + break; + } + + CameraNode node; + node.index = reader.ReadInt16(); + + // 0x02 marker, then the type byte + uint8_t typeMarker = reader.ReadUByte(); + if (typeMarker != 0x02) { + SPDLOG_WARN("[BK64:MAP] Expected type marker 0x02, got 0x{:02X} at " + "offset 0x{:X} in asset {}", + typeMarker, reader.GetBaseAddress() - 1, symbol); + break; + } + node.type = reader.ReadUByte(); + + // Inner sub-loop (cameraNodeTypeN_fromFile). Every type has its own set of + // sub-markers and ends at 0x00 — except type 0, which has no inner data and + // no 0x00 terminator at all. + // + // Type 1: 0x01→position[3] 0x02→hSpeed+vSpeed 0x03→rotation+accel + // 0x04→pitchYawRoll[3] 0x05→unknownFlag + // Type 2: 0x01→position[3] 0x02→pitchYawRoll[3] + // Type 3: 0x01→position[3] 0x02→hSpeed+vSpeed 0x03→rotation+accel + // 0x06→closeDist+farDist 0x04→pitchYawRoll[3] 0x05→unknownFlag + // Type 4: 0x01→unknownFlag + bool innerError = false; + if (node.type == 0) { + // Nothing to read; fall straight through to the push + } else + while (!innerError && reader.GetBaseAddress() < reader.GetLength()) { + uint8_t sub = reader.ReadUByte(); + if (sub == 0x00) + break; + + switch (node.type) { + case 1: + switch (sub) { + case 0x01: + node.data.type1.position[0] = reader.ReadFloat(); + node.data.type1.position[1] = reader.ReadFloat(); + node.data.type1.position[2] = reader.ReadFloat(); + break; + case 0x02: + node.data.type1.horizontalSpeed = reader.ReadFloat(); + node.data.type1.verticalSpeed = reader.ReadFloat(); + break; + case 0x03: + node.data.type1.rotation = reader.ReadFloat(); + node.data.type1.accelaration = reader.ReadFloat(); + break; + case 0x04: + node.data.type1.pitchYawRoll[0] = reader.ReadFloat(); + node.data.type1.pitchYawRoll[1] = reader.ReadFloat(); + node.data.type1.pitchYawRoll[2] = reader.ReadFloat(); + break; + case 0x05: + node.data.type1.unknownFlag = reader.ReadInt32(); + break; + default: + SPDLOG_WARN("[BK64:MAP] Unknown sub-marker 0x{:02X} in type1 " + "camera node at 0x{:X} in {}", + sub, reader.GetBaseAddress() - 1, symbol); + innerError = true; + } + break; + + case 2: + switch (sub) { + case 0x01: + node.data.type2.position[0] = reader.ReadFloat(); + node.data.type2.position[1] = reader.ReadFloat(); + node.data.type2.position[2] = reader.ReadFloat(); + break; + case 0x02: + node.data.type2.pitchYawRoll[0] = reader.ReadFloat(); + node.data.type2.pitchYawRoll[1] = reader.ReadFloat(); + node.data.type2.pitchYawRoll[2] = reader.ReadFloat(); + break; + default: + SPDLOG_WARN("[BK64:MAP] Unknown sub-marker 0x{:02X} in type2 " + "camera node at 0x{:X} in {}", + sub, reader.GetBaseAddress() - 1, symbol); + innerError = true; + } + break; + + case 3: + switch (sub) { + case 0x01: + node.data.type3.position[0] = reader.ReadFloat(); + node.data.type3.position[1] = reader.ReadFloat(); + node.data.type3.position[2] = reader.ReadFloat(); + break; + case 0x02: + node.data.type3.horizontalSpeed = reader.ReadFloat(); + node.data.type3.verticalSpeed = reader.ReadFloat(); + break; + case 0x03: + node.data.type3.rotation = reader.ReadFloat(); + node.data.type3.accelaration = reader.ReadFloat(); + break; + case 0x06: + node.data.type3.closeDistance = reader.ReadFloat(); + node.data.type3.farDistance = reader.ReadFloat(); + break; + case 0x04: + node.data.type3.pitchYawRoll[0] = reader.ReadFloat(); + node.data.type3.pitchYawRoll[1] = reader.ReadFloat(); + node.data.type3.pitchYawRoll[2] = reader.ReadFloat(); + break; + case 0x05: + node.data.type3.unknownFlag = reader.ReadInt32(); + break; + default: + SPDLOG_WARN("[BK64:MAP] Unknown sub-marker 0x{:02X} in type3 " + "camera node at 0x{:X} in {}", + sub, reader.GetBaseAddress() - 1, symbol); + innerError = true; + } + break; + + case 4: + switch (sub) { + case 0x01: + node.data.type4.unknownFlag = reader.ReadInt32(); + break; + default: + SPDLOG_WARN("[BK64:MAP] Unknown sub-marker 0x{:02X} in type4 " + "camera node at 0x{:X} in {}", + sub, reader.GetBaseAddress() - 1, symbol); + innerError = true; + } + break; + + default: + SPDLOG_WARN("[BK64:MAP] Unknown camera node type {} at offset 0x{:X} " + "in asset {}", + node.type, reader.GetBaseAddress(), symbol); + innerError = true; + } + } + + map->mCameraNodes.push_back(node); + SPDLOG_INFO("[BK64:MAP] {} camera node index={} type={}", symbol, node.index, node.type); + + if (innerError) + break; + } + // Terminator's already eaten, so the outer loop lands on the next chunk. + SPDLOG_INFO("[BK64:MAP] {} camera section: {} nodes", symbol, map->mCameraNodes.size()); +} + +// Lighting vector section (chunk type 0x04) +static void ParseLightingSection(LUS::BinaryReader& reader, std::shared_ptr<MapData>& map, const std::string& symbol) { + // Each entry: marker 0x01 + position (f32[3]) + fade_radii (f32[2]) + + // rgb (s32[3]). Keep going until we hit a chunk marker (0x00-0x04). + while (reader.GetBaseAddress() + 1 < reader.GetLength()) { + // Peek without consuming so we can spot a chunk marker + size_t peekPos = reader.GetBaseAddress(); + uint8_t peekMarker = reader.ReadUByte(); + reader.Seek(peekPos, LUS::SeekOffsetType::Start); + + // 0x00 ends the section; 0x01 starts an entry. Mirrors + // lightingVectorList_fromFile: while(!file_isNextByteExpected(fp, 0)). + if (peekMarker == 0x00) { + reader.ReadUByte(); // eat the terminator + break; + } + + uint8_t marker = reader.ReadUByte(); + + if (marker == 0x01) { + LightingVector light; + + // position + uint8_t posMarker = reader.ReadUByte(); + if (posMarker != 0x02) { + SPDLOG_WARN("[BK64:MAP] Expected position marker 0x02, got 0x{:02X} at " + "offset 0x{:X} in asset {}", + posMarker, reader.GetBaseAddress() - 1, symbol); + break; + } + light.position[0] = reader.ReadFloat(); + light.position[1] = reader.ReadFloat(); + light.position[2] = reader.ReadFloat(); + + // fade radii + uint8_t fadeMarker = reader.ReadUByte(); + if (fadeMarker != 0x03) { + SPDLOG_WARN("[BK64:MAP] Expected fade marker 0x03, got 0x{:02X} at " + "offset 0x{:X} in asset {}", + fadeMarker, reader.GetBaseAddress() - 1, symbol); + break; + } + light.fadeRadii[0] = reader.ReadFloat(); + light.fadeRadii[1] = reader.ReadFloat(); + + // rgb + uint8_t rgbMarker = reader.ReadUByte(); + if (rgbMarker != 0x04) { + SPDLOG_WARN("[BK64:MAP] Expected RGB marker 0x04, got 0x{:02X} at " + "offset 0x{:X} in asset {}", + rgbMarker, reader.GetBaseAddress() - 1, symbol); + break; + } + light.rgb[0] = reader.ReadInt32(); + light.rgb[1] = reader.ReadInt32(); + light.rgb[2] = reader.ReadInt32(); + + map->mLightingVectors.push_back(light); + SPDLOG_INFO("[BK64:MAP] {} light pos=({:.2f},{:.2f},{:.2f}) " + "radii=({:.2f},{:.2f}) rgb=({},{},{})", + symbol, light.position[0], light.position[1], light.position[2], light.fadeRadii[0], + light.fadeRadii[1], light.rgb[0], light.rgb[1], light.rgb[2]); + } else { + SPDLOG_WARN("[BK64:MAP] Unexpected marker 0x{:02X} in lighting section " + "at offset 0x{:X} in asset {}", + marker, reader.GetBaseAddress() - 1, symbol); + break; + } + } + // Terminator's already eaten, so the outer loop lands on the next chunk. + SPDLOG_INFO("[BK64:MAP] {} lighting section: {} vectors", symbol, map->mLightingVectors.size()); +} + +} // namespace BK64 diff --git a/src/factories/bk64/MapFactory.h b/src/factories/bk64/MapFactory.h new file mode 100644 index 0000000..20c26fe --- /dev/null +++ b/src/factories/bk64/MapFactory.h @@ -0,0 +1,314 @@ +#pragma once + +#include <factories/BaseFactory.h> +#include <vector> + +namespace BK64 { + +/** + * NodeProp: spawn point, warp, trigger, or event marker - 20 bytes + * Decomp: NodeProp from props.h + * + * These are the spatial triggers and spawn locations for a level. At load time + * cubeList_fromFile (actor_cubebounds.c) turns each one into an ActorMarker. + * The category field is what decides what it actually does: + * * 6 = Actor spawn point (spawns an entity via markerActorTypeArray dispatch) + * * 7 = Warp destination (teleports the player to another map) + * * 9 = Trigger zone (fires events when the player enters the radius) + * * 0xA = Event marker, used by level-specific systems + * + * Flow: ROM → MapFactory parse() → NodeProp array → cubeList_fromFile → + * ActorMarker → actor spawn / event trigger via overlay callbacks + * + * Structure Layout: + * Offset 0x00: position[3] (s16[3]) - X, Y, Z world coordinates + * Offset 0x06: selector_or_radius:9, category:6, bit0:1 (u16) + * Offset 0x08: actorId (u16) - Actor/Warp/Event ID depending on category + * Offset 0x0A: markerId (u8), padB (u8) + * Offset 0x0C: yaw:9, scale:23 (u32) + * Offset 0x10: unk10_31:12, unk10_19:12, pad10_7:1, unk10_6:1, pad10_5:4, + * unk10_0:2 (u32) + */ +typedef struct NodeProp { + int16_t position[3]; // X, Y, Z world position (s16) + uint16_t radius : 9; // selector_or_radius: trigger/volume radius + uint16_t bit6 : 6; // category (6=actor, 7=warp, 9=trigger, 0xA=event) + uint16_t bit0 : 1; // active/enabled + uint16_t unk8; // actorId — meaning depends on category + uint8_t unkA; // markerId: index into the ActorMarker lookup table + uint8_t padB; // padding + uint32_t yaw : 9; // spawn Y rotation; *2 for degrees (0-511 → 0-1022°) + uint32_t scale : 23; // spawn scale, fixed point (/1000.0 for the real value) + uint32_t unk10_31 : 12; // secondary ID or overlay-specific param + uint32_t unk10_19 : 12; // more params (animation phase, variant, ...) + uint32_t pad10_7 : 1; // padding + uint32_t unk10_6 : 1; // "initialized" flag, set at runtime + uint32_t pad10_5 : 4; // padding + uint32_t unk10_0 : 2; // param passed to func_803303B8 +} NodeProp; + +/** + * ModelProp: Static 3D model (is_actor=0, is_3d=1) - 12 bytes + * Decomp: model_prop_s from props.h + * + * Structure Layout: + * Offset 0x00: unk0 (u16) - modelId:12, pad0_19:4 + * Offset 0x02: yaw (u8) - rotation Y-axis + * Offset 0x03: roll (u8) - rotation around local axis + * Offset 0x04: position[3] (s16[3]) - X, Y, Z world position + * Offset 0x0A: scale (u8) + * Offset 0x0B: flags (u8) - isModelProp:1, isActorProp:1, etc. + * + * Asset ID = (modelId & 0xFFF) + MODEL_ASSET_OFFSET (0x2D1) + */ +typedef struct ModelProp { + uint16_t unk0; // modelId:12, pad0_19:4 + uint8_t yaw; // Y-axis rotation + uint8_t roll; // roll + int16_t position[3]; // X, Y, Z world position + uint8_t scale; // scale + uint8_t flags; // discriminator (isModelProp=1, isActorProp=0) +} ModelProp; + +/** + * SpriteProp: 2D billboard sprite (is_actor=0, is_3d=0) - 12 bytes + * Decomp: sprite_prop_s from props.h + * + * Complete Structure Layout (12 bytes): + * Offset 0x00-0x03: word0 (32-bit packed) - sprite appearance/rendering + * parameters Offset 0x04-0x09: unk4[3] (s16[3]) - X, Y, Z world position Offset + * 0x0A-0x0B: wordA (16-bit packed) - animation frame + discriminator flags + * + * Packed Field Layout (word0 - 32-bit big-endian at offset 0x00): + * Bits 31-20: spriteId (12 bits) → Asset ID = spriteId + SPRITE_ASSET_OFFSET + * (0x572) Bit 19: unk0_19 (1 bit) Bits 18-16: rgb_remove_red (3 bits, 0-7 color + * removal value) Bits 15-13: rgb_remove_green (3 bits, 0-7 color removal value) + * Bits 12-10: rgb_remove_blue (3 bits, 0-7 color removal value) + * Bits 9-2: scale (8 bits) + * Bit 1: isMirrored (1 bit, horizontal flip) + * Bit 0: pad0_0 (1 bit) + * + * Packed Field Layout (wordA - 16-bit big-endian at offset 0x0A): + * Bits 15-11: frame (5 bits, animation frame index) + * Bits 10-6: unk8_10 (5 bits) + * Bit 5: unk8_5 (1 bit) + * Bit 4: isNotFeatherEggOrNote (1 bit) + * Bit 3: unk8_3 (1 bit) + * Bit 2: isCollisionResolved (1 bit) + * Bit 1: isModelProp (1 bit, always 0 for sprites) + * Bit 0: isActorProp (1 bit, always 0 for sprites) + */ +typedef struct SpriteProp { + uint32_t word0; + int16_t unk4[3]; + uint16_t wordA; +} SpriteProp; + +/** + * ActorProp: Dynamic entity created at runtime (is_actor=1) - 12 bytes + * Decomp: actor_prop_s from props.h + * + * Structure Layout: + * Offset 0x00: marker (ActorMarker* - 4 bytes, runtime pointer) + * Offset 0x04: position[3] (s16[3] - 6 bytes, X/Y/Z cache) + * Offset 0x0A: flags (u16 - 2 bytes) - frame:5, unk8_10:5, isMirrored:1, + * isNotFeatherEggOrNote:1, unk8_3:1, isCollisionResolved:1, + * isModelProp:1 (0), isActorProp:1 (1) + * + * Yes, ActorProps really are in ROM. The `marker` field (bytes 0-3) is always + * NULL there and only gets filled in when the engine spawns the actor. The + * position and flags fields, though, are genuine ROM data. + */ +typedef struct ActorProp { + uint32_t marker; // ActorMarker* — runtime only + int16_t position[3]; // X, Y, Z position cache + uint16_t flags; // discriminator (isActorProp=1) +} ActorProp; + +/** + * Prop: union of ModelProp, SpriteProp, ActorProp (12 bytes) + * + * Which one it is comes from the flags at offset 0x0A, bits 0-1: + * isActorProp=1 → ActorProp (carries actor marker metadata) + * isActorProp=0, isModelProp=1 → ModelProp (static 3D geometry) + * isActorProp=0, isModelProp=0 → SpriteProp (2D billboard sprite) + * + * All three are 12 bytes and live in ROM; only the flags tell them apart. + */ +typedef union Prop { + ModelProp model; + SpriteProp sprite; + ActorProp actor; + struct { + uint32_t pad0; + int16_t unk4[3]; + uint16_t padA_15 : 10; + uint16_t unkA_5 : 1; + uint16_t unkA_4 : 1; + uint16_t unkA_3 : 1; + uint16_t unkA_2 : 1; + uint16_t is_3d : 1; + uint16_t is_actor : 1; + }; + uint8_t raw[12]; +} Prop; + +/** + * CubeData: one cell of the 32×32×32 spatial partition grid + * + * x/y/z are 5-bit grid coords (0-31). + * unk0_4 splits the NodeProps: [0..unk0_4) are regular spawns, + * [unk0_4..prop1Cnt) are events. + */ +typedef struct CubeData { + int32_t x : 5; + int32_t y : 5; + int32_t z : 5; + uint32_t prop1Cnt : 6; + uint32_t prop2Cnt : 6; + uint32_t unk0_4 : 5; // split point: [0..unk0_4)=spawns, [unk0_4..prop1Cnt)=events + std::vector<NodeProp> nodeProps; + std::vector<Prop> props; +} CubeData; + +/** + * CameraNodeType1: scripted/path camera (CAMERA_TYPE_1_UNKNOWN) + * Decomp: CameraNodeType1 from camera.h + * Size: 44 bytes (11 floats + 1 s32) + * + * Cutscenes and scripted sequences, most likely. No per-frame update handler — + * the camera just rides a predefined path from these position/speed/accel/ + * orientation params. + */ +typedef struct CameraNodeType1 { + float position[3]; // camera position + float horizontalSpeed; // horizontal speed + float verticalSpeed; // vertical speed + float rotation; // rotation speed + float accelaration; // acceleration + float pitchYawRoll[3]; // orientation (pitch, yaw, roll) + int32_t unknownFlag; // unknown; tested against 1, 2, 4 +} CameraNodeType1; + +/** + * CameraNodeType2: dynamic camera (CAMERA_TYPE_2_DYNAMIC) + * Decomp: CameraNodeType2 from camera.h + * Size: 24 bytes (6 floats) + */ +typedef struct CameraNodeType2 { + float position[3]; // camera position + float pitchYawRoll[3]; // orientation (pitch, yaw, roll) +} CameraNodeType2; + +/** + * CameraNodeType3: static camera (CAMERA_TYPE_3_STATIC) + * Decomp: CameraNodeType3 from camera.h + * Size: 52 bytes (12 floats + 1 s32) + */ +typedef struct CameraNodeType3 { + float position[3]; // camera position + float horizontalSpeed; // horizontal speed + float verticalSpeed; // vertical speed + float rotation; // rotation speed + float accelaration; // acceleration + float closeDistance; // near clip distance + float farDistance; // far clip distance + float pitchYawRoll[3]; // orientation (pitch, yaw, roll) + int32_t unknownFlag; // unknown +} CameraNodeType3; + +/** + * CameraNodeType4: random camera (CAMERA_TYPE_4_RANDOM) + * Decomp: CameraNodeType4 from camera.h + * Size: 4 bytes (1 s32) + */ +typedef struct CameraNodeType4 { + int32_t unknownFlag; // unknown +} CameraNodeType4; + +/** + * CameraNode: Camera path/behavior node + * Decomp: CameraNode from camera.h + * Format: marker 0x01 + index (s16) + type marker 0x02 + type (u8) + + * type-specific data + * + * Camera types: + * 1 = Scripted/path camera (cutscenes, camera paths) + * 2 = Dynamic camera (follows player) + * 3 = Static camera (fixed position) + * 4 = Random camera (special behavior) + */ +typedef struct CameraNode { + int16_t index; // node index + uint8_t type; // 1=Scripted, 2=Dynamic, 3=Static, 4=Random + union { + CameraNodeType1 type1; + CameraNodeType2 type2; + CameraNodeType3 type3; + CameraNodeType4 type4; + } data; +} CameraNode; + +/** + * LightingVector: point light with a fade radius + * Decomp: Lighting from gclights.c + * Format: marker 0x01 + position (f32[3]) + fade_radii (f32[2]) + rgb (s32[3]) + */ +typedef struct LightingVector { + float position[3]; // X, Y, Z light position + float fadeRadii[2]; // inner/outer fade distances + int32_t rgb[3]; // R, G, B +} LightingVector; + +class MapData : public IParsedData { + public: + // Cube data section (chunk type 0x01) + int32_t mCubeMin[3]; // grid min bounds + int32_t mCubeMax[3]; // grid max bounds + std::vector<CubeData> mCubes; + + // Camera nodes section (chunk type 0x03) + std::vector<CameraNode> mCameraNodes; + + // Lighting section (chunk type 0x04) + std::vector<LightingVector> mLightingVectors; + + MapData() { + mCubeMin[0] = mCubeMin[1] = mCubeMin[2] = 0; + mCubeMax[0] = mCubeMax[1] = mCubeMax[2] = 0; + } +}; + +class MapHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MapBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MapCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MapModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MapFactory : public BaseFactory { + public: + std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override; + inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override { + return { REGISTER(Code, MapCodeExporter) REGISTER(Header, MapHeaderExporter) REGISTER(Binary, MapBinaryExporter) + REGISTER(Modding, MapModdingExporter) }; + } + + bool HasModdedDependencies() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/ModelFactory.cpp b/src/factories/bk64/ModelFactory.cpp new file mode 100644 index 0000000..0906e78 --- /dev/null +++ b/src/factories/bk64/ModelFactory.cpp @@ -0,0 +1,994 @@ +#include "ModelFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" + +#define BK64_MODEL_HEADER 0xB +#define TEXTURE_HEADER_SIZE 0x8 +#define TEXTURE_METADATA_SIZE 0x10 +#define GFX_HEADER_SIZE 0x8 +#define GFX_CMD_SIZE 0x8 +#define VTX_HEADER_SIZE 0x18 +#define ANIM_TEXTURE_LIST_COUNT 4 + +namespace BK64 { + +static const std::unordered_map<std::string, uint8_t> gF3DTable = { + { "G_VTX", 0x04 }, { "G_DL", 0x06 }, { "G_MTX", 0x1 }, { "G_ENDDL", 0xB8 }, + { "G_SETTIMG", 0xFD }, { "G_MOVEMEM", 0x03 }, { "G_MV_L0", 0x86 }, { "G_MV_L1", 0x88 }, + { "G_MV_LIGHT", 0xA }, { "G_TRI2", 0xB1 }, { "G_QUAD", -1 } +}; + +static const std::unordered_map<std::string, uint8_t> gF3DExTable = { + { "G_VTX", 0x04 }, { "G_DL", 0x06 }, { "G_MTX", 0x1 }, { "G_ENDDL", 0xB8 }, + { "G_SETTIMG", 0xFD }, { "G_MOVEMEM", 0x03 }, { "G_MV_L0", 0x86 }, { "G_MV_L1", 0x88 }, + { "G_MV_LIGHT", 0xA }, { "G_TRI2", 0xB1 }, { "G_QUAD", 0xB5 } +}; + +static const std::unordered_map<std::string, uint8_t> gF3DEx2Table = { + { "G_VTX", 0x01 }, { "G_DL", 0xDE }, { "G_MTX", 0xDA }, { "G_ENDDL", 0xDF }, + { "G_SETTIMG", 0xFD }, { "G_MOVEMEM", 0xDC }, { "G_MV_L0", 0x86 }, { "G_MV_L1", 0x88 }, + { "G_MV_LIGHT", 0xA }, { "G_TRI2", 0x06 }, { "G_QUAD", 0x07 } +}; + +static const std::unordered_map<GBIVersion, std::unordered_map<std::string, uint8_t>> gGBITable = { + { GBIVersion::f3d, gF3DTable }, + { GBIVersion::f3dex, gF3DExTable }, + { GBIVersion::f3dex2, gF3DEx2Table }, +}; + +#define GBI(cmd) gGBITable.at(Companion::Instance->GetGBIVersion()).at(#cmd) + +ExportResult ModelHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + auto model = std::static_pointer_cast<ModelData>(raw); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + write << "extern BKModelHeader " << symbol << "_Header;\n"; + + if (model->mHasAnimation && !model->mBones.empty()) { + write << "extern BKAnimHeader " << symbol << "_AnimHeader;\n"; + write << "extern BKBone " << symbol << "_Bones[];\n"; + } + + if (model->mHasCollision) { + write << "extern BKCollisionHeader " << symbol << "_CollisionHeader;\n"; + if (!model->mGeoCubes.empty()) { + write << "extern BKGeoCube " << symbol << "_GeoCubes[];\n"; + } + if (!model->mCollisionTris.empty()) { + write << "extern BKCollisionTri " << symbol << "_CollisionTris[];\n"; + } + } + + if (!model->mEffects.empty()) { + write << "extern BKEffect " << symbol << "_Effects[];\n"; + } + + if (!model->mAnimTextures.empty()) { + write << "extern BKAnimTexture " << symbol << "_AnimTextures[];\n"; + } + + return std::nullopt; +} + +ExportResult ModelCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto model = std::static_pointer_cast<ModelData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + // Header + write << "BKModelHeader " << symbol << "_Header = {\n"; + write << fourSpaceTab << "/* geoType */ " << model->mGeoType << ",\n"; + write << fourSpaceTab << "/* triCount */ " << model->mTriCount << ",\n"; + write << fourSpaceTab << "/* vertCount */ " << model->mVertCount << "\n"; + write << "};\n\n"; + + // Animation, if the model has any + if (model->mHasAnimation && !model->mBones.empty()) { + write << "BKAnimHeader " << symbol << "_AnimHeader = {\n"; + write << fourSpaceTab << "/* scalingFactor */ " << model->mAnimHeader.scalingFactor << "f,\n"; + write << fourSpaceTab << "/* boneCount */ " << model->mBones.size() << "\n"; + write << "};\n\n"; + + write << "BKBone " << symbol << "_Bones[] = {\n"; + for (const auto& bone : model->mBones) { + write << fourSpaceTab << "{ "; + write << bone.pos[0] << "f, " << bone.pos[1] << "f, " << bone.pos[2] << "f, "; + write << bone.id << ", " << bone.parentId; + write << " },\n"; + } + write << "};\n\n"; + } + + // Collision + if (model->mHasCollision) { + write << "BKCollisionHeader " << symbol << "_CollisionHeader = {\n"; + write << fourSpaceTab << "/* minIndex */ { " << model->mCollisionHeader.minIndex[0] << ", " + << model->mCollisionHeader.minIndex[1] << ", " << model->mCollisionHeader.minIndex[2] << " },\n"; + write << fourSpaceTab << "/* maxIndex */ { " << model->mCollisionHeader.maxIndex[0] << ", " + << model->mCollisionHeader.maxIndex[1] << ", " << model->mCollisionHeader.maxIndex[2] << " },\n"; + write << fourSpaceTab << "/* yStride */ " << model->mCollisionHeader.yStride << ",\n"; + write << fourSpaceTab << "/* zStride */ " << model->mCollisionHeader.zStride << ",\n"; + write << fourSpaceTab << "/* geoCubeScale */ " << model->mCollisionHeader.geoCubeScale << ",\n"; + write << fourSpaceTab << "/* geoCubeCount */ " << model->mGeoCubes.size() << ",\n"; + write << fourSpaceTab << "/* triCount */ " << model->mCollisionTris.size() << "\n"; + write << "};\n\n"; + + if (!model->mGeoCubes.empty()) { + write << "BKGeoCube " << symbol << "_GeoCubes[] = {\n"; + for (const auto& cube : model->mGeoCubes) { + write << fourSpaceTab << "{ " << cube.startTri << ", " << cube.triCount << " },\n"; + } + write << "};\n\n"; + } + + if (!model->mCollisionTris.empty()) { + write << "BKCollisionTri " << symbol << "_CollisionTris[] = {\n"; + for (const auto& tri : model->mCollisionTris) { + write << fourSpaceTab << "{ "; + write << "{ " << tri.vtxIds[0] << ", " << tri.vtxIds[1] << ", " << tri.vtxIds[2] << " }, "; + write << tri.unk6 << ", " << std::hex << "0x" << tri.flags << std::dec; + write << " },\n"; + } + write << "};\n\n"; + } + } + + // Effects + if (!model->mEffects.empty()) { + write << "BKEffect " << symbol << "_Effects[] = {\n"; + for (const auto& effect : model->mEffects) { + write << fourSpaceTab << "{ " << effect.dataInfo << ", "; + write << effect.vtxIndices.size() << ", { "; + for (size_t i = 0; i < effect.vtxIndices.size(); i++) { + write << effect.vtxIndices[i]; + if (i < effect.vtxIndices.size() - 1) + write << ", "; + } + write << " } },\n"; + } + write << "};\n\n"; + } + + // Animated textures + if (!model->mAnimTextures.empty()) { + write << "BKAnimTexture " << symbol << "_AnimTextures[] = {\n"; + for (const auto& animTex : model->mAnimTextures) { + write << fourSpaceTab << "{ "; + write << animTex.frameSize << ", " << animTex.frameCount << ", "; + write << animTex.frameRate << "f"; + write << " },\n"; + } + write << "};\n\n"; + } + + return offset; +} + +ExportResult BK64::ModelBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto model = std::static_pointer_cast<ModelData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKModel, 0); + + // ── Core ────────────────────────────────────────────────────────────────── + writer.Write(model->mGeoType); + writer.Write(model->mTriCount); + writer.Write(model->mVertCount); + + // ── Presence flags ──────────────────────────────────────────────────────── + writer.Write(static_cast<uint8_t>(model->mHasGeo ? 1 : 0)); + writer.Write(static_cast<uint8_t>(model->mHasVtx ? 1 : 0)); + writer.Write(static_cast<uint8_t>(model->mHasDL ? 1 : 0)); + writer.Write(static_cast<uint16_t>(model->mTexInfos.size())); + writer.Write(static_cast<uint8_t>(model->mHasAnimation ? 1 : 0)); + writer.Write(static_cast<uint8_t>(model->mHasCollision ? 1 : 0)); + writer.Write(static_cast<uint8_t>(model->mHasUnk14 ? 1 : 0)); + writer.Write(static_cast<uint8_t>(model->mHasUnk20 ? 1 : 0)); + writer.Write(static_cast<uint8_t>(!model->mEffects.empty() ? 1 : 0)); + writer.Write(static_cast<uint8_t>(model->mHasUnk28 ? 1 : 0)); + writer.Write(static_cast<uint8_t>(!model->mAnimTextures.empty() ? 1 : 0)); + + // ── VTX header ──────────────────────────────────────────────────────────── + if (model->mHasVtx) { + const auto& vh = model->mVtxHeader; + writer.Write(vh.minCoord[0]); + writer.Write(vh.minCoord[1]); + writer.Write(vh.minCoord[2]); + writer.Write(vh.maxCoord[0]); + writer.Write(vh.maxCoord[1]); + writer.Write(vh.maxCoord[2]); + writer.Write(vh.centerCoord[0]); + writer.Write(vh.centerCoord[1]); + writer.Write(vh.centerCoord[2]); + writer.Write(vh.localNorm); + writer.Write(vh.count); + writer.Write(vh.globalNorm); + } + + // ── GFX / display-list info ─────────────────────────────────────────────── + if (model->mHasDL) { + writer.Write(model->mDLCount); + writer.Write(model->mDLUnkInfo); + writer.Write(model->mGfxSubListCount); + + // Build a lookup from each static texture's IMAGE segment-2 offset back to its texture + // index. CI4/CI8 put the palette at textureDataOffset and the actual image (the _tex_<i> + // resource) right after it at + tlutColors*2; everything else has the image at the offset. + std::unordered_map<uint32_t, uint32_t> imageOffsetToTex; + for (uint32_t ti = 0; ti < model->mTexInfos.size(); ti++) { + const auto& tex = model->mTexInfos[ti]; + const bool isCI = tex.type == 0x1 || tex.type == 0x2; // CI4 / CI8 + const uint32_t tlutByteSize = isCI ? tex.tlutColors * 2u : 0u; + imageOffsetToTex[tex.textureDataOffset + tlutByteSize] = ti; + } + + // The falling jiggies transition rewrites its own texture at runtime, so leave its + // G_SETTIMG alone. Touch it and you get the white fallback texture instead. + const bool isFramebufferSubstitutionModel = entryName.find("TRANSITION_FALLING_JIGGIES") != std::string::npos; + for (size_t i = 0; i + 1 < model->mRawDLWords.size(); i += 2) { + uint32_t w0 = model->mRawDLWords[i]; + uint32_t w1 = model->mRawDLWords[i + 1]; + if (!isFramebufferSubstitutionModel && (w0 >> 24) == 0xFD /* G_SETTIMG */ && + SEGMENT_NUMBER(w1) == 2) { + auto it = imageOffsetToTex.find(SEGMENT_OFFSET(w1)); + if (it != imageOffsetToTex.end()) { + w1 = 0xFF000000u | (it->second & 0x00FFFFFFu); + } + } + writer.Write(w0); + writer.Write(w1); + } + } + + // ── Texture metadata ────────────────────────────────────────────────────── + for (const auto& tex : model->mTexInfos) { + writer.Write(tex.type); + writer.Write(tex.width); + writer.Write(tex.height); + writer.Write(tex.tlutColors); + writer.Write(tex.textureDataOffset); + } + + // ── Raw texture data blob ──────────────────────────────────────────────── + // [port] The whole contiguous texture data area from the decompressed model. Keeps the + // animated texture frames, plus any bytes wedged between listed textures that DL commands + // reach via segment offsets. + writer.Write(model->mTexDataSize); + if (model->mTexDataSize > 0 && !model->mRawTexData.empty()) { + writer.Write((char*)model->mRawTexData.data(), model->mRawTexData.size()); + } + + // ── Animation list ──────────────────────────────────────────────────────── + if (model->mHasAnimation) { + writer.Write(model->mAnimHeader.scalingFactor); + writer.Write(static_cast<uint16_t>(model->mBones.size())); + for (const auto& bone : model->mBones) { + writer.Write(bone.pos[0]); + writer.Write(bone.pos[1]); + writer.Write(bone.pos[2]); + writer.Write(bone.id); + writer.Write(bone.parentId); + } + } + + // ── Collision list ──────────────────────────────────────────────────────── + if (model->mHasCollision) { + const auto& col = model->mCollisionHeader; + writer.Write(col.minIndex[0]); + writer.Write(col.minIndex[1]); + writer.Write(col.minIndex[2]); + writer.Write(col.maxIndex[0]); + writer.Write(col.maxIndex[1]); + writer.Write(col.maxIndex[2]); + writer.Write(col.yStride); + writer.Write(col.zStride); + writer.Write(col.geoCubeScale); + writer.Write(static_cast<uint16_t>(model->mGeoCubes.size())); + writer.Write(static_cast<uint16_t>(model->mCollisionTris.size())); + for (const auto& cube : model->mGeoCubes) { + writer.Write(cube.startTri); + writer.Write(cube.triCount); + } + for (const auto& tri : model->mCollisionTris) { + writer.Write(tri.vtxIds[0]); + writer.Write(tri.vtxIds[1]); + writer.Write(tri.vtxIds[2]); + writer.Write(tri.unk6); + writer.Write(tri.flags); + } + } + + // ── Unk14 (hitbox) ──────────────────────────────────────────────────────── + if (model->mHasUnk14) { + writer.Write(static_cast<int16_t>(model->mUnk14Entries0.size())); + writer.Write(static_cast<int16_t>(model->mUnk14Entries1.size())); + writer.Write(static_cast<int16_t>(model->mUnk14Entries2.size())); + writer.Write(model->mUnk14Unk6); + for (const auto& e : model->mUnk14Entries0) { + writer.Write(e.scale1[0]); + writer.Write(e.scale1[1]); + writer.Write(e.scale1[2]); + writer.Write(e.scale2[0]); + writer.Write(e.scale2[1]); + writer.Write(e.scale2[2]); + writer.Write(e.pos[0]); + writer.Write(e.pos[1]); + writer.Write(e.pos[2]); + writer.Write(e.rot[0]); + writer.Write(e.rot[1]); + writer.Write(e.rot[2]); + writer.Write(e.unk15); + writer.Write(e.animIndex); + writer.Write(e.pad); + } + for (const auto& e : model->mUnk14Entries1) { + writer.Write(e.unk0); + writer.Write(e.unk2); + writer.Write(e.pos[0]); + writer.Write(e.pos[1]); + writer.Write(e.pos[2]); + writer.Write(e.rot[0]); + writer.Write(e.rot[1]); + writer.Write(e.rot[2]); + writer.Write(e.unkD); + writer.Write(e.animIndex); + writer.Write(e.pad); + } + for (const auto& e : model->mUnk14Entries2) { + writer.Write(e.unk0); + writer.Write(e.unk2[0]); + writer.Write(e.unk2[1]); + writer.Write(e.unk2[2]); + writer.Write(e.unk8); + writer.Write(e.unk9); + writer.Write(e.pad[0]); + writer.Write(e.pad[1]); + } + } + + // ── Unk20 ───────────────────────────────────────────────────────────────── + if (model->mHasUnk20) { + writer.Write(static_cast<uint8_t>(model->mUnk20Entries.size())); + for (const auto& e : model->mUnk20Entries) { + writer.Write(e.unk0[0]); + writer.Write(e.unk0[1]); + writer.Write(e.unk0[2]); + writer.Write(e.unk6[0]); + writer.Write(e.unk6[1]); + writer.Write(e.unk6[2]); + writer.Write(e.unkC); + writer.Write(e.pad); + } + } + + // ── Effects ─────────────────────────────────────────────────────────────── + if (!model->mEffects.empty()) { + writer.Write(static_cast<uint16_t>(model->mEffects.size())); + for (const auto& fx : model->mEffects) { + writer.Write(fx.dataInfo); + writer.Write(static_cast<uint16_t>(fx.vtxIndices.size())); + for (auto idx : fx.vtxIndices) { + writer.Write(idx); + } + } + } + + // ── Unk28 ───────────────────────────────────────────────────────────────── + if (model->mHasUnk28) { + writer.Write(static_cast<int16_t>(model->mUnk28Entries.size())); + for (const auto& e : model->mUnk28Entries) { + writer.Write(e.coord[0]); + writer.Write(e.coord[1]); + writer.Write(e.coord[2]); + writer.Write(e.animIndex); + writer.Write(static_cast<int8_t>(e.vtxList.size())); + for (auto idx : e.vtxList) { + writer.Write(idx); + } + } + } + + // ── Animated textures (always 4 slots) ─────────────────────────────────── + if (!model->mAnimTextures.empty()) { + for (const auto& at : model->mAnimTextures) { + writer.Write(at.frameSize); + writer.Write(at.frameCount); + writer.Write(at.frameRate); + } + } + + writer.Finish(write); + + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> ModelFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + const auto modelOffset = GetSafeNode<uint32_t>(node, "offset"); // Should always be 0 in reality + const auto modelOffsetEnd = modelOffset + segment.size; + const auto fileOffset = Companion::Instance->GetCurrentVRAM().value().offset; + + if (reader.ReadInt32() != BK64_MODEL_HEADER) { + SPDLOG_ERROR("Invalid Header For BK64 Model {}", symbol); + return std::nullopt; + } + + /* 0x04 */ auto geoLayoutOffset = reader.ReadUInt32(); + /* 0x08 */ auto textureSetupOffset = reader.ReadUInt16(); + /* 0x0A */ auto geoType = reader.ReadUInt16(); + /* 0x0C */ auto displayListSetupOffset = reader.ReadUInt32(); + /* 0x10 */ auto vertexSetupOffset = reader.ReadUInt32(); + /* 0x14 */ auto unkHitboxInfoOffset = reader.ReadUInt32(); + /* 0x18 */ auto animationSetupOffset = reader.ReadUInt32(); + /* 0x1C */ auto collisionSetupOffset = reader.ReadUInt32(); + /* 0x20 */ auto modelUnk20Offset = reader.ReadUInt32(); + /* 0x24 */ auto effectsSetupOffset = reader.ReadUInt32(); + /* 0x28 */ auto modelUnk28Offset = reader.ReadUInt32(); + /* 0x2C */ auto animatedTextureOffset = reader.ReadUInt32(); + /* 0x30 */ auto triCount = reader.ReadUInt16(); + /* 0x32 */ auto vertCount = reader.ReadUInt16(); + + auto modelData = std::make_shared<ModelData>(geoType, triCount, vertCount); + + uint16_t textureCount; + + if (geoLayoutOffset != 0) { + SPDLOG_INFO("HAS GL {}", symbol); + modelData->mHasGeo = true; + YAML::Node geoLayout; + geoLayout["type"] = "BK64:GEO_LAYOUT"; + geoLayout["offset"] = modelOffset + geoLayoutOffset; + geoLayout["symbol"] = symbol + "_GEO"; + Companion::Instance->AddAsset(geoLayout); + } + + if (textureSetupOffset != 0) { + + reader.Seek(modelOffset + textureSetupOffset, LUS::SeekOffsetType::Start); + + auto textureDataSize = reader.ReadUInt32(); + textureCount = reader.ReadUInt16(); + reader.ReadUInt16(); // pad + + Companion::Instance->SetCompressedSegment(2, fileOffset, + modelOffset + textureSetupOffset + TEXTURE_HEADER_SIZE + + textureCount * TEXTURE_METADATA_SIZE); + + for (uint16_t i = 0; i < textureCount; i++) { + auto textureDataOffset = reader.ReadUInt32(); + auto textureType = reader.ReadUInt16(); + reader.ReadUInt16(); // pad + uint32_t width = reader.ReadUByte(); + uint32_t height = reader.ReadUByte(); + reader.ReadUInt16(); // pad + reader.ReadUInt32(); // pad + + std::string format; + std::string ctype; + uint32_t tlutSize = 0; + uint16_t tlutColors = 0; + + // Stash texture metadata for the binary exporter. Type 0x1 just means "has a TLUT" — + // it's both CI4 and CI8. We can't tell which until all the headers are in, so the real + // bit depth gets resolved further down. + TexInfo texInfo; + texInfo.type = textureType; + texInfo.width = static_cast<uint8_t>(width); + texInfo.height = static_cast<uint8_t>(height); + texInfo.tlutColors = 0; + texInfo.textureDataOffset = textureDataOffset; + + switch (textureType) { + case 0x1: + // Sorted out later, once every header is read + break; + case 0x2: + texInfo.tlutColors = 0x100; + break; + case 0x4: + case 0x8: + case 0x10: + break; + default: + throw std::runtime_error("BK64::ModelFactory: Invalid Texture Format Found " + + std::to_string(textureType)); + } + + modelData->mTexInfos.push_back(texInfo); + } + + uint32_t texDataStart = + modelOffset + textureSetupOffset + TEXTURE_HEADER_SIZE + textureCount * TEXTURE_METADATA_SIZE; + modelData->mTexDataSize = textureDataSize; + + // Now disambiguate the type 0x1 textures. 0x1 means "has TLUT", which is either CI4 + // (16-entry palette) or CI8 (256-entry palette) — the header doesn't say which. Trick is + // to measure the gap to the next texture: if it's big enough for a full CI8 payload + // (0x200 TLUT + W*H pixels), call it CI8, otherwise CI4. The last texture in a list can be + // padded, hence >= instead of ==. CI8 always needs more room than CI4 at the same W*H + // (delta = 0x1E0 - W*H/2 > 0 for any BK texture up to 64x64), so there's no overlap to + // worry about. + for (uint16_t i = 0; i < textureCount; i++) { + auto& tex = modelData->mTexInfos[i]; + if (tex.type != 0x1) { + continue; + } + + uint32_t nextOffset = + (i + 1 < textureCount) ? modelData->mTexInfos[i + 1].textureDataOffset : textureDataSize; + uint32_t gap = nextOffset - tex.textureDataOffset; + uint32_t ci4Size = 0x20 + ((uint32_t)tex.width * tex.height) / 2; // 16-entry TLUT + CI4 pixels + uint32_t ci8Size = 0x200 + (uint32_t)tex.width * tex.height; // 256-entry TLUT + CI8 pixels + + if (gap >= ci8Size) { + tex.type = 0x2; // CI8 + tex.tlutColors = 0x100; + if (gap != ci8Size) { + SPDLOG_INFO("[BK64::Model] tex[{}] {}x{}: gap=0x{:X} >= CI8 (0x{:X}), classified CI8 (pad=0x{:X})", + i, tex.width, tex.height, gap, ci8Size, gap - ci8Size); + } + } else { + tex.tlutColors = 0x10; // CI4 + if (gap < ci4Size) { + SPDLOG_WARN("[BK64::Model] tex[{}] {}x{}: gap=0x{:X} smaller than CI4 (0x{:X}), data may be " + "truncated", + i, tex.width, tex.height, gap, ci4Size); + } else if (gap != ci4Size) { + SPDLOG_INFO("[BK64::Model] tex[{}] {}x{}: gap=0x{:X} (CI4 0x{:X}, pad=0x{:X})", i, tex.width, + tex.height, gap, ci4Size, gap - ci4Size); + } + } + } + + // [port] Grab the entire raw texture area so animated frames and any unlisted bytes + // between textures survive into the binary. + if (textureDataSize > 0 && texDataStart + textureDataSize <= segment.size) { + modelData->mRawTexData.assign(segment.data + texDataStart, segment.data + texDataStart + textureDataSize); + } + + // [port] Also emit each texture as its own OTEX resource for modders. The raw blob above + // already keeps the animated frames intact; these per-texture resources are the hook for + // dropping in replacement textures that the importer overlays on top. + for (uint16_t i = 0; i < textureCount; i++) { + const auto& tex = modelData->mTexInfos[i]; + uint32_t texOffset = texDataStart + tex.textureDataOffset; + + std::string format; + uint32_t tlutByteSize = 0; + + switch (tex.type) { + case 0x1: + format = "CI4"; + tlutByteSize = tex.tlutColors * 2; + break; + case 0x2: + format = "CI8"; + tlutByteSize = tex.tlutColors * 2; + break; + case 0x4: + format = "RGBA16"; + break; + case 0x8: + format = "RGBA32"; + break; + case 0x10: + format = "IA8"; + break; + default: + continue; + } + + std::string texSymbol = symbol + "_tex_" + std::to_string(i); + + if (tlutByteSize > 0) { + YAML::Node tlut; + tlut["type"] = "TEXTURE"; + tlut["offset"] = texOffset; + tlut["format"] = "TLUT"; + tlut["ctype"] = "u16"; + tlut["colors"] = (int)tex.tlutColors; + tlut["symbol"] = texSymbol + "_TLUT"; + Companion::Instance->AddAsset(tlut); + } + + YAML::Node texture; + texture["type"] = "TEXTURE"; + texture["offset"] = texOffset + tlutByteSize; + texture["format"] = format; + texture["ctype"] = "u16"; + texture["width"] = (int)tex.width; + texture["height"] = (int)tex.height; + texture["symbol"] = texSymbol; + if (tlutByteSize > 0) { + texture["tlut_symbol"] = texSymbol + "_TLUT"; + } + Companion::Instance->AddAsset(texture); + } + } + + // Parse First To Avoid Auto Extraction By DLs + if (vertexSetupOffset != 0) { + reader.Seek(modelOffset + vertexSetupOffset, LUS::SeekOffsetType::Start); + Companion::Instance->SetCompressedSegment(1, fileOffset, modelOffset + vertexSetupOffset + VTX_HEADER_SIZE); + + modelData->mHasVtx = true; + modelData->mVtxHeader.minCoord[0] = reader.ReadInt16(); + modelData->mVtxHeader.minCoord[1] = reader.ReadInt16(); + modelData->mVtxHeader.minCoord[2] = reader.ReadInt16(); + modelData->mVtxHeader.maxCoord[0] = reader.ReadInt16(); + modelData->mVtxHeader.maxCoord[1] = reader.ReadInt16(); + modelData->mVtxHeader.maxCoord[2] = reader.ReadInt16(); + modelData->mVtxHeader.centerCoord[0] = reader.ReadInt16(); + modelData->mVtxHeader.centerCoord[1] = reader.ReadInt16(); + modelData->mVtxHeader.centerCoord[2] = reader.ReadInt16(); + modelData->mVtxHeader.localNorm = reader.ReadInt16(); + modelData->mVtxHeader.count = reader.ReadUInt16(); + modelData->mVtxHeader.globalNorm = reader.ReadInt16(); + + // The header vtx count lies for some models, so derive the real count from the byte span + // between the VTX section and whatever section comes next. + constexpr uint32_t kVtxRawSize = 16; // sizeof(Vtx) in the ROM + const uint32_t vtxDataStart = vertexSetupOffset + VTX_HEADER_SIZE; + uint32_t vtxDataEnd = static_cast<uint32_t>(modelOffsetEnd - modelOffset); + for (uint32_t candidate : { geoLayoutOffset, static_cast<uint32_t>(textureSetupOffset), displayListSetupOffset, + unkHitboxInfoOffset, animationSetupOffset, collisionSetupOffset, modelUnk20Offset, + effectsSetupOffset, modelUnk28Offset, animatedTextureOffset }) { + if (candidate > vtxDataStart && candidate < vtxDataEnd) { + vtxDataEnd = candidate; + } + } + const uint32_t trueVtxCount = (vtxDataEnd - vtxDataStart) / kVtxRawSize; + if (trueVtxCount != static_cast<uint32_t>(modelData->mVtxHeader.count)) { + SPDLOG_DEBUG("[BKModel] {} vtx header count {} vs section-derived " + "count {} — using section-derived", + symbol, modelData->mVtxHeader.count, trueVtxCount); + modelData->mVtxHeader.count = static_cast<uint16_t>(trueVtxCount); + } + + // We hold off registering _VTX until after the DL bytes are read (so the count can grow + // to cover DL refs), but it has to land BEFORE AddAsset(gfxNode) kicks off sub-DL parsing. + // Register it too late and the DL G_VTX scans run with no _VTX in the registry — SearchVtx + // then misses every reference into the model's own vtx region and spits out a flat autogen + // entry per reference. + } + + // Read the DL bytes and work out the sub-DL boundaries — but don't register the GFX assets + // yet. Registering triggers DL parsing, and the DL parser wants _VTX in the registry first + // (see above). + std::set<uint32_t> dlOffsets; + if (displayListSetupOffset != 0) { + reader.Seek(modelOffset + displayListSetupOffset, LUS::SeekOffsetType::Start); + Companion::Instance->SetCompressedSegment(3, fileOffset, + modelOffset + displayListSetupOffset + GFX_HEADER_SIZE); + modelData->mHasDL = true; + auto dlCount = reader.ReadUInt32(); + auto unkDLInfo = reader.ReadUInt32(); + modelData->mDLCount = dlCount; + modelData->mDLUnkInfo = unkDLInfo; + + uint32_t dlOffset = 0; + if (dlCount > 0) { + dlOffsets.emplace(dlOffset); + } + modelData->mRawDLWords.reserve(dlCount * 2); + while (dlOffset < dlCount * GFX_CMD_SIZE) { + auto w0 = reader.ReadUInt32(); + auto w1 = reader.ReadUInt32(); + modelData->mRawDLWords.push_back(w0); + modelData->mRawDLWords.push_back(w1); + dlOffset += GFX_CMD_SIZE; + uint8_t opCode = w0 >> 24; + + if (opCode == GBI(G_ENDDL) && dlOffset != dlCount * GFX_CMD_SIZE) { + dlOffsets.emplace(dlOffset); + } + // G_DL jump targets inside segment 3 are split points too. Splitting on G_ENDDL only + // catches the sequential sub-lists; an intra-buffer G_DL can jump to some arbitrary + // offset that no G_ENDDL precedes. + if (opCode == GBI(G_DL) && SEGMENT_NUMBER(w1) == 3) { + dlOffsets.emplace(SEGMENT_OFFSET(w1)); + } + } + } + + // That section-boundary heuristic can still undercount, so scan the DL for the highest vertex + // index it actually touches. That's the count we trust. + if (modelData->mHasVtx && modelData->mHasDL && !modelData->mRawDLWords.empty()) { + constexpr uint32_t kN64VtxSize = 16; + uint32_t maxVtxNeeded = modelData->mVtxHeader.count; + for (size_t i = 0; i < modelData->mRawDLWords.size(); i += 2) { + uint32_t w0 = modelData->mRawDLWords[i]; + uint32_t w1 = modelData->mRawDLWords[i + 1]; + uint8_t opCode = w0 >> 24; + if (opCode == GBI(G_VTX) && SEGMENT_NUMBER(w1) == 1) { + uint32_t n = (w0 >> 10) & 0x3F; + uint32_t off = SEGMENT_OFFSET(w1); + uint32_t vtxEnd = off / kN64VtxSize + n; + if (vtxEnd > maxVtxNeeded) { + maxVtxNeeded = vtxEnd; + } + } + } + if (maxVtxNeeded > modelData->mVtxHeader.count) { + SPDLOG_WARN("[BKModel] {} DL references vertex {} but header count is {} — extending to {}", symbol, + maxVtxNeeded - 1, modelData->mVtxHeader.count, maxVtxNeeded); + modelData->mVtxHeader.count = static_cast<uint16_t>(maxVtxNeeded); + } + } + + // Register _VTX with the corrected count now, before the GFX sub-DLs go in — the sub-DL + // G_VTX handler leans on SearchVtx finding this entry, otherwise it autogens per reference. + if (modelData->mHasVtx) { + YAML::Node vtx; + vtx["type"] = "VTX"; + vtx["count"] = modelData->mVtxHeader.count; + vtx["offset"] = modelOffset + vertexSetupOffset + VTX_HEADER_SIZE; + vtx["symbol"] = symbol + "_VTX"; + Companion::Instance->AddAsset(vtx); + } + + // Safe to register the sub-DLs now; their parse pass resolves segmented vtx refs against the + // _VTX we just registered. + if (displayListSetupOffset != 0) { + uint32_t count = 0; + for (const auto& extractOffset : dlOffsets) { + YAML::Node gfxNode; + gfxNode["type"] = "GFX"; + gfxNode["offset"] = modelOffset + displayListSetupOffset + GFX_HEADER_SIZE + extractOffset; + gfxNode["symbol"] = symbol + "_GFX_" + std::to_string(count); + // Binary export only: we parse these purely for the side effect of auto-registering + // VTX sub-assets, but skip writing a per-sub-DL entry. The raw DL words already live + // in the parent model resource, and emitting each one separately can shove us past the + // 65,535-entry ZIP limit. Code and Header exports still write the standalone entries. + if (Companion::Instance->GetConfig().exporterType == ExportType::Binary) { + gfxNode["no_export"] = true; + } + Companion::Instance->AddAsset(gfxNode); + count++; + } + modelData->mGfxSubListCount = count; + } + + if (unkHitboxInfoOffset != 0) { + reader.Seek(modelOffset + unkHitboxInfoOffset, LUS::SeekOffsetType::Start); + modelData->mHasUnk14 = true; + auto count1 = reader.ReadInt16(); + auto count2 = reader.ReadInt16(); + auto count3 = reader.ReadInt16(); + modelData->mUnk14Unk6 = reader.ReadInt16(); + + for (int16_t i = 0; i < count1; i++) { + Unk14_0 e{}; + e.scale1[0] = reader.ReadInt16(); + e.scale1[1] = reader.ReadInt16(); + e.scale1[2] = reader.ReadInt16(); + e.scale2[0] = reader.ReadInt16(); + e.scale2[1] = reader.ReadInt16(); + e.scale2[2] = reader.ReadInt16(); + e.pos[0] = reader.ReadInt16(); + e.pos[1] = reader.ReadInt16(); + e.pos[2] = reader.ReadInt16(); + e.rot[0] = reader.ReadUByte(); + e.rot[1] = reader.ReadUByte(); + e.rot[2] = reader.ReadUByte(); + e.unk15 = reader.ReadUByte(); + e.animIndex = reader.ReadUByte(); + e.pad = reader.ReadUByte(); + modelData->mUnk14Entries0.push_back(e); + } + + for (int16_t i = 0; i < count2; i++) { + Unk14_1 e{}; + e.unk0 = reader.ReadInt16(); + e.unk2 = reader.ReadInt16(); + e.pos[0] = reader.ReadInt16(); + e.pos[1] = reader.ReadInt16(); + e.pos[2] = reader.ReadInt16(); + e.rot[0] = reader.ReadUByte(); + e.rot[1] = reader.ReadUByte(); + e.rot[2] = reader.ReadUByte(); + e.unkD = reader.ReadUByte(); + e.animIndex = reader.ReadUByte(); + e.pad = reader.ReadUByte(); + modelData->mUnk14Entries1.push_back(e); + } + + for (int16_t i = 0; i < count3; i++) { + Unk14_2 e{}; + e.unk0 = reader.ReadInt16(); + e.unk2[0] = reader.ReadInt16(); + e.unk2[1] = reader.ReadInt16(); + e.unk2[2] = reader.ReadInt16(); + e.unk8 = reader.ReadUByte(); + e.unk9 = reader.ReadUByte(); + e.pad[0] = reader.ReadUByte(); + e.pad[1] = reader.ReadUByte(); + modelData->mUnk14Entries2.push_back(e); + } + } + + if (animationSetupOffset != 0) { + reader.Seek(modelOffset + animationSetupOffset, LUS::SeekOffsetType::Start); + modelData->mHasAnimation = true; + modelData->mAnimHeader.scalingFactor = reader.ReadFloat(); + auto boneCount = reader.ReadUInt16(); + reader.ReadUInt16(); // pad + + for (uint16_t i = 0; i < boneCount; i++) { + BoneData bone; + bone.pos[0] = reader.ReadFloat(); + bone.pos[1] = reader.ReadFloat(); + bone.pos[2] = reader.ReadFloat(); + bone.id = reader.ReadUInt16(); + bone.parentId = reader.ReadUInt16(); + modelData->mBones.push_back(bone); + } + } + + if (collisionSetupOffset != 0) { + constexpr size_t kCollHeaderSize = 0x18; + constexpr size_t kGeoCubeSize = 4; + constexpr size_t kCollTriSize = 12; + auto looksLikeCollisionList = [&](uint32_t at) -> bool { + if (at + kCollHeaderSize > segment.size) { + return false; + } + uint16_t geoCnt = (uint16_t)((segment.data[at + 0x10] << 8) | segment.data[at + 0x11]); + uint16_t triCnt = (uint16_t)((segment.data[at + 0x14] << 8) | segment.data[at + 0x15]); + size_t needed = + (size_t)kCollHeaderSize + (size_t)geoCnt * kGeoCubeSize + (size_t)triCnt * kCollTriSize; + return at + needed <= segment.size; + }; + + uint32_t collAt = modelOffset + collisionSetupOffset; + bool collOk = looksLikeCollisionList(collAt); + if (!collOk) { + for (int delta : { 1, -1, 2, -2, 3, -3, 4, -4 }) { + int64_t candidate = (int64_t)collAt + delta; + if (candidate < (int64_t)modelOffset) { + continue; + } + if (looksLikeCollisionList((uint32_t)candidate)) { + SPDLOG_WARN("[BKModel] {} collisionSetupOffset 0x{:X} fails structural check — recovered " + "real section at 0x{:X} (delta {:+d})", + symbol, collisionSetupOffset, (uint32_t)candidate - modelOffset, delta); + collAt = (uint32_t)candidate; + collOk = true; + break; + } + } + } + if (!collOk) { + SPDLOG_ERROR("[BKModel] {} collisionSetupOffset 0x{:X} fails structural check and no nearby valid " + "BKCollisionList found; skipping collision section (segSize 0x{:X})", + symbol, collisionSetupOffset, segment.size); + } else { + reader.Seek(collAt, LUS::SeekOffsetType::Start); + + modelData->mHasCollision = true; + modelData->mCollisionHeader.minIndex[0] = reader.ReadInt16(); + modelData->mCollisionHeader.minIndex[1] = reader.ReadInt16(); + modelData->mCollisionHeader.minIndex[2] = reader.ReadInt16(); + modelData->mCollisionHeader.maxIndex[0] = reader.ReadInt16(); + modelData->mCollisionHeader.maxIndex[1] = reader.ReadInt16(); + modelData->mCollisionHeader.maxIndex[2] = reader.ReadInt16(); + modelData->mCollisionHeader.yStride = reader.ReadUInt16(); + modelData->mCollisionHeader.zStride = reader.ReadUInt16(); + auto geoCubeCount = reader.ReadUInt16(); + modelData->mCollisionHeader.geoCubeScale = reader.ReadUInt16(); + auto triCount = reader.ReadUInt16(); + reader.ReadUInt16(); // pad + + for (uint16_t i = 0; i < geoCubeCount; i++) { + GeoCube cube; + cube.startTri = reader.ReadUInt16(); + cube.triCount = reader.ReadUInt16(); + modelData->mGeoCubes.push_back(cube); + } + + for (uint16_t i = 0; i < triCount; i++) { + CollisionTri tri; + tri.vtxIds[0] = reader.ReadUInt16(); + tri.vtxIds[1] = reader.ReadUInt16(); + tri.vtxIds[2] = reader.ReadUInt16(); + tri.unk6 = reader.ReadUInt16(); + tri.flags = reader.ReadUInt32(); + modelData->mCollisionTris.push_back(tri); + } + } + } + + if (modelUnk20Offset != 0) { + reader.Seek(modelOffset + modelUnk20Offset, LUS::SeekOffsetType::Start); + modelData->mHasUnk20 = true; + auto count = reader.ReadInt8(); + reader.ReadInt8(); // pad + + for (int8_t i = 0; i < count; i++) { + Unk20_0 e{}; + e.unk0[0] = reader.ReadInt16(); + e.unk0[1] = reader.ReadInt16(); + e.unk0[2] = reader.ReadInt16(); + e.unk6[0] = reader.ReadInt16(); + e.unk6[1] = reader.ReadInt16(); + e.unk6[2] = reader.ReadInt16(); + e.unkC = reader.ReadUByte(); + e.pad = reader.ReadUByte(); + modelData->mUnk20Entries.push_back(e); + } + } + + if (effectsSetupOffset != 0) { + reader.Seek(modelOffset + effectsSetupOffset, LUS::SeekOffsetType::Start); + auto effectCount = reader.ReadUInt16(); + + for (uint16_t i = 0; i < effectCount; i++) { + Effect effect; + effect.dataInfo = reader.ReadUInt16(); + auto vtxCount = reader.ReadUInt16(); + for (uint16_t j = 0; j < vtxCount; j++) { + effect.vtxIndices.push_back(reader.ReadUInt16()); + } + modelData->mEffects.push_back(effect); + } + } + + if (modelUnk28Offset != 0) { + SPDLOG_INFO("HAS UNK 28"); + reader.Seek(modelOffset + modelUnk28Offset, LUS::SeekOffsetType::Start); + modelData->mHasUnk28 = true; + auto count = reader.ReadInt16(); + reader.ReadInt16(); // pad + + for (int16_t i = 0; i < count; i++) { + Unk28_0 e{}; + e.coord[0] = reader.ReadInt16(); + e.coord[1] = reader.ReadInt16(); + e.coord[2] = reader.ReadInt16(); + e.animIndex = reader.ReadInt8(); + auto vtxCount = reader.ReadInt8(); + for (int16_t j = 0; j < vtxCount; j++) { + e.vtxList.push_back(reader.ReadInt16()); + } + modelData->mUnk28Entries.push_back(e); + } + } + + if (animatedTextureOffset != 0) { + reader.Seek(modelOffset + animatedTextureOffset, LUS::SeekOffsetType::Start); + for (uint32_t i = 0; i < ANIM_TEXTURE_LIST_COUNT; i++) { + AnimTexture animTexture; + animTexture.frameSize = reader.ReadUInt16(); + animTexture.frameCount = reader.ReadUInt16(); + animTexture.frameRate = reader.ReadFloat(); + + // Point the segment at frame 0's texture + if (animTexture.frameSize != 0) { + Companion::Instance->SetCompressedSegment(15 - i, fileOffset, + modelOffset + textureSetupOffset + TEXTURE_HEADER_SIZE + + textureCount * TEXTURE_METADATA_SIZE); + } + + modelData->mAnimTextures.push_back(animTexture); + } + } + + return modelData; +} + +} // namespace BK64 diff --git a/src/factories/bk64/ModelFactory.h b/src/factories/bk64/ModelFactory.h new file mode 100644 index 0000000..0b2dd95 --- /dev/null +++ b/src/factories/bk64/ModelFactory.h @@ -0,0 +1,279 @@ +#pragma once + +#include <factories/BaseFactory.h> + +namespace BK64 { + +/** + * BoneData: one bone joint in the animation skeleton. + * Decomp: BKAnimation from model.h + * + * Structure Layout: + * Offset 0x00: unk0[3] (f32[3]) - X, Y, Z bone position offset from parent + * Offset 0x0C: bone_id (s16) - Bone index + * Offset 0x0E: mtx_id (s16) - Parent bone matrix ID + */ +typedef struct BoneData { + float pos[3]; + uint16_t id; + uint16_t parentId; +} BoneData; + +/** + * GeoCube: one cell of the collision spatial partition. + * Decomp: BKCollisionGeo from model.h + * + * Structure Layout: + * Offset 0x00: start_tri_index (s16) - Index of first triangle in this cube + * Offset 0x02: tri_count (s16) - Number of triangles in this cube + */ +typedef struct GeoCube { + uint16_t startTri; + uint16_t triCount; +} GeoCube; + +/** + * CollisionTri: a single collision triangle. + * Decomp: BKCollisionTri from model.h + * + * Structure Layout: + * Offset 0x00: unk0[3] (s16[3]) - Vertex indices (we name it vtxIds[3]) + * Offset 0x06: unk6 (s16) - Additional flags/material ID + * Offset 0x08: flags (s32) - Surface type flags + */ +typedef struct CollisionTri { + uint16_t vtxIds[3]; + uint16_t unk6; + uint32_t flags; +} CollisionTri; + +/** + * Effect: a group of vertices tagged for some special rendering effect. + */ +typedef struct Effect { + uint16_t dataInfo; // packed effect type + params + std::vector<uint16_t> vtxIndices; // the vertices this effect acts on +} Effect; + +/** + * AnimTexture: one animated-texture slot. + * Decomp: AnimTexture from model.h + * + * Structure Layout (decomp field names): + * Offset 0x00: frame_size (s16) - Bytes per texture frame + * Offset 0x02: frame_cnt (s16) - Number of animation frames + * Offset 0x04: frame_rate (f32) - Animation speed (frames per second) + */ +typedef struct AnimTexture { + uint16_t frameSize; + uint16_t frameCount; + float frameRate; +} AnimTexture; + +/** + * CollisionHeader: the grid parameters for collision lookups. + * Decomp: BKCollisionList from model.h + * + * Structure Layout (decomp field names): + * Offset 0x00: unk0[3] (s16[3]) - min[X,Y,Z] + * Offset 0x06: unk6[3] (s16[3]) - max[X,Y,Z] + * Offset 0x0C: unkC (s16) - y_stride + * Offset 0x0E: unkE (s16) - z_stride + * Offset 0x10: unk10 (s16) - geo_cnt (we calculate from vector.size()) + * Offset 0x12: unk12 (s16) - scale + * Offset 0x14: unk14 (s16) - tri_cnt (we calculate from vector.size()) + * + * Grid Cell Calculation: + * cubeX = (worldX - minX) / geoCubeScale + * cubeY = (worldY - minY) / geoCubeScale + * cubeZ = (worldZ - minZ) / geoCubeScale + * cubeIndex = cubeX + cubeY * yStride + cubeZ * yStride * zStride + */ +typedef struct CollisionHeader { + int16_t minIndex[3]; + int16_t maxIndex[3]; + uint16_t yStride; + uint16_t zStride; + uint16_t geoCubeScale; +} CollisionHeader; + +/** + * AnimationHeader: just the animation scaling factor. + * Decomp: BKAnimationList from model.h + * + * Structure Layout: + * Offset 0x00: unk0 (f32) - Scaling multiplier for animation keyframes + * Offset 0x04: cnt_4 (s16) - Number of bones (we calculate from + * vector.size()) + */ +typedef struct AnimationHeader { + float scalingFactor; +} AnimationHeader; + +/** + * VtxHeader: the bounding/position metadata at the front of a BKVertexList — the + * first 24 bytes, ahead of the Vtx[] array. We write it into the binary so the + * port can rebuild the BKVertexList header at runtime without the raw ROM segment. + */ +typedef struct VtxHeader { + int16_t minCoord[3]; + int16_t maxCoord[3]; + int16_t centerCoord[3]; + int16_t localNorm; + uint16_t count; + int16_t globalNorm; +} VtxHeader; + +/** + * TexInfo: the per-texture metadata from the BKTextureList header. + * tlutColors: 0x10 for CI4, 0x100 for CI8, 0 for non-paletted formats. + */ +typedef struct TexInfo { + uint16_t type; + uint8_t width; + uint8_t height; + uint16_t tlutColors; + uint32_t textureDataOffset; // ROM offset, relative to the texture data base +} TexInfo; + +/** + * Unk14_0 / _1 / _2: hitbox / bone-space definitions, the entries of a + * BKModelUnk14List. Sizes: _0 = 24 bytes, _1 = 16 bytes, _2 = 12 bytes. + */ +typedef struct Unk14_0 { + int16_t scale1[3]; + int16_t scale2[3]; + int16_t pos[3]; + uint8_t rot[3]; + uint8_t unk15; + int8_t animIndex; + uint8_t pad; +} Unk14_0; // 24 bytes + +typedef struct Unk14_1 { + int16_t unk0; + int16_t unk2; + int16_t pos[3]; + uint8_t rot[3]; + uint8_t unkD; + int8_t animIndex; + uint8_t pad; +} Unk14_1; // 16 bytes + +typedef struct Unk14_2 { + int16_t unk0; + int16_t unk2[3]; + uint8_t unk8; + int8_t unk9; + uint8_t pad[2]; +} Unk14_2; // 12 bytes + +/** + * Unk20_0: one BKModelUnk20List entry. 14 bytes of data, padded to 16. + */ +typedef struct Unk20_0 { + int16_t unk0[3]; + int16_t unk6[3]; + uint8_t unkC; + uint8_t pad; +} Unk20_0; // 14 bytes raw, pad to 16 + +/** + * Unk28_0: a variable-length BKModelUnk28 entry. + */ +typedef struct Unk28_0 { + int16_t coord[3]; + int8_t animIndex; + std::vector<int16_t> vtxList; +} Unk28_0; + +class ModelData : public IParsedData { + public: + uint16_t mGeoType; + uint16_t mTriCount; + uint16_t mVertCount; + + // ── GEO layout ──────────────────────────────────────────────────────────── + bool mHasGeo = false; + + // ── Vertex list ─────────────────────────────────────────────────────────── + bool mHasVtx = false; + VtxHeader mVtxHeader{}; + + // ── Display lists ───────────────────────────────────────────────────────── + bool mHasDL = false; + uint32_t mDLCount = 0; // total GFX command words over all sub-lists + uint32_t mDLUnkInfo = 0; // checksum, or whatever — from the GFX header + uint32_t mGfxSubListCount = 0; // how many _GFX_* sub-assets we made + std::vector<uint32_t> mRawDLWords; // raw N64 DL words, as w0/w1 pairs + + // ── Texture list ────────────────────────────────────────────────────────── + std::vector<TexInfo> mTexInfos; + uint32_t mTexDataSize = 0; // size of the raw texture area, for segment 2 allocation + std::vector<uint8_t> mRawTexData; // the whole raw texture blob, animated frames and all + + // ── Animation data ──────────────────────────────────────────────────────── + bool mHasAnimation = false; + AnimationHeader mAnimHeader{}; + std::vector<BoneData> mBones; + + // ── Collision data ──────────────────────────────────────────────────────── + bool mHasCollision = false; + CollisionHeader mCollisionHeader{}; + std::vector<GeoCube> mGeoCubes; + std::vector<CollisionTri> mCollisionTris; + + // ── Unk14 (hitbox) ──────────────────────────────────────────────────────── + bool mHasUnk14 = false; + int16_t mUnk14Unk6 = 0; + std::vector<Unk14_0> mUnk14Entries0; + std::vector<Unk14_1> mUnk14Entries1; + std::vector<Unk14_2> mUnk14Entries2; + + // ── Unk20 ───────────────────────────────────────────────────────────────── + bool mHasUnk20 = false; + std::vector<Unk20_0> mUnk20Entries; + + // ── Effects ─────────────────────────────────────────────────────────────── + std::vector<Effect> mEffects; + + // ── Unk28 ───────────────────────────────────────────────────────────────── + bool mHasUnk28 = false; + std::vector<Unk28_0> mUnk28Entries; + + // ── Animated textures ───────────────────────────────────────────────────── + std::vector<AnimTexture> mAnimTextures; + + ModelData(uint16_t geoType, uint16_t triCount, uint16_t vertCount) + : mGeoType(geoType), mTriCount(triCount), mVertCount(vertCount) { + } +}; + +class ModelHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class ModelBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class ModelCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class ModelFactory : public BaseFactory { + public: + std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override; + inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override { + return { REGISTER(Code, ModelCodeExporter) REGISTER(Header, ModelHeaderExporter) + REGISTER(Binary, ModelBinaryExporter) }; + } + + bool HasModdedDependencies() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/QuizQuestionFactory.cpp b/src/factories/bk64/QuizQuestionFactory.cpp new file mode 100644 index 0000000..678e8dd --- /dev/null +++ b/src/factories/bk64/QuizQuestionFactory.cpp @@ -0,0 +1,273 @@ +#include "QuizQuestionFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" +#include "utils/TorchUtils.h" + +#define QUIZ_QUESTION_HEADER_1 0x01 +#define QUIZ_QUESTION_HEADER_2 0x01 +#define QUIZ_QUESTION_HEADER_3 0x02 +#define QUIZ_QUESTION_HEADER_4 0x05 +#define QUIZ_QUESTION_HEADER_5 0x00 + +#define FORMAT_HEX(x, w) \ + std::hex << std::uppercase << std::setfill('0') << std::setw(w) << x << std::nouppercase << std::dec +#define YAML_HEX(num) YAML::Hex << (num) << YAML::Dec + +namespace BK64 { + +ExportResult QuizQuestionHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult QuizQuestionCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto offset = GetSafeNode<uint32_t>(node, "offset"); + auto quizQuestion = std::static_pointer_cast<QuizQuestionData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "u8 " << symbol << "[] = {\n"; + + write << fourSpaceTab << "QUIZ_QUESTION_HEADER_1" + << ", " + << "QUIZ_QUESTION_HEADER_2" + << ", " + << "QUIZ_QUESTION_HEADER_3" + << ", " + << "QUIZ_QUESTION_HEADER_4" + << ", " + << "QUIZ_QUESTION_HEADER_5" + << ",\n"; + write << fourSpaceTab << "/* QuizQuestion */\n"; + write << fourSpaceTab << quizQuestion->mText.size() << ",\n"; + for (const auto [cmd, str] : quizQuestion->mText) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } + write << ",\n"; + } + write << fourSpaceTab << "/* Options */\n"; + write << fourSpaceTab << quizQuestion->mOptions.size() << ",\n"; + for (const auto [cmd, str] : quizQuestion->mOptions) { + write << fourSpaceTab << "0x" << FORMAT_HEX((uint32_t)cmd, 2) << ", " << str.length(); + for (auto& c : str) { + if (c < ' ') { + write << ", 0x" << FORMAT_HEX((uint32_t)c, 2); + } else if (c == '\'') { + write << ", \'\\" << c << "\'"; + } else { + write << ", \'" << c << "\'"; + } + } + write << ",\n"; + } + + write << "};\n\n"; + + return offset; +} + +ExportResult BK64::QuizQuestionBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + const auto quizQuestion = std::static_pointer_cast<QuizQuestionData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKQuizQuestion, 0); + + writer.Write((uint32_t)quizQuestion->mText.size()); + for (const auto& dialogString : quizQuestion->mText) { + writer.Write(dialogString.cmd); + writer.Write((uint32_t)dialogString.str.length()); + writer.Write((char*)dialogString.str.data(), + dialogString.str.size()); // [port] Write(string) would prefix the length twice + } + + writer.Write((uint32_t)quizQuestion->mOptions.size()); + for (const auto& optionString : quizQuestion->mOptions) { + writer.Write(optionString.cmd); + writer.Write((uint32_t)optionString.str.length()); + writer.Write((char*)optionString.str.data(), + optionString.str.size()); // [port] Write(string) would prefix the length twice + } + + writer.Finish(write); + return std::nullopt; +} + +ExportResult BK64::QuizQuestionModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + const auto quizQuestion = std::static_pointer_cast<QuizQuestionData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "Text"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto [cmd, str] : quizQuestion->mText) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + out << str.c_str(); + out << YAML::EndSeq; + } + out << YAML::EndSeq; + + out << YAML::Key << "Options"; + out << YAML::Value; + + out << YAML::BeginSeq; + for (const auto [cmd, str] : quizQuestion->mOptions) { + out << YAML::Flow; + out << YAML::BeginSeq; + out << YAML_HEX((uint32_t)cmd); + out << str.c_str(); + out << YAML::EndSeq; + } + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +// The quiz data itself. Same layout for US and for PAL English. +static std::shared_ptr<QuizQuestionData> ParseQuizBlock(LUS::BinaryReader& reader) { + std::vector<DialogString> text; + std::vector<DialogString> options; + + auto textSize = reader.ReadUByte(); + + for (uint8_t i = 0; i < textSize - 3; i++) { + DialogString dialogString; + dialogString.cmd = reader.ReadUByte(); + auto strLen = reader.ReadUByte(); + dialogString.str = reader.ReadString(strLen); + text.push_back(dialogString); + } + + for (uint8_t i = textSize - 3; i < textSize; i++) { + DialogString optionString; + optionString.cmd = reader.ReadUByte(); + auto strLen = reader.ReadUByte(); + optionString.str = reader.ReadString(strLen); + options.push_back(optionString); + } + + return std::make_shared<QuizQuestionData>(text, options); +} + +std::optional<std::shared_ptr<IParsedData>> QuizQuestionFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + const auto symbol = GetSafeNode<std::string>(node, "symbol"); + + auto header1 = reader.ReadInt8(); + auto header2 = reader.ReadInt8(); + auto header3 = reader.ReadInt8(); + + if (header1 == QUIZ_QUESTION_HEADER_1 && header2 == QUIZ_QUESTION_HEADER_2 && header3 == QUIZ_QUESTION_HEADER_3) { + // US: 01 01 02 05 00, then quiz data + reader.ReadInt8(); // header4 (0x05) + reader.ReadInt8(); // header5 (0x00) + return ParseQuizBlock(reader); + } + + if (header1 == 0x03 && header2 == 0x01 && header3 == 0x02) { + // PAL: 03 01 02, then 3 x LE u16 offsets (EN/FR/DE start positions). + // The English block lives at the first of those offsets. + uint16_t enOffset = reader.ReadUByte() | (reader.ReadUByte() << 8); + reader.ReadUByte(); + reader.ReadUByte(); // skip FR offset + reader.ReadUByte(); + reader.ReadUByte(); // skip DE offset + // We're now at byte 9 = enOffset, so just read the English block. + return ParseQuizBlock(reader); + } + + SPDLOG_ERROR("Invalid Header For BK64 QuizQuestion {}: {:02X} {:02X} {:02X}", symbol, header1, header2, header3); + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> QuizQuestionFactory::parse_modding(std::vector<uint8_t>& buffer, + YAML::Node& node) { + YAML::Node assetNode; + + try { + std::string text((char*)buffer.data(), buffer.size()); + assetNode = YAML::Load(text.c_str()); + } catch (YAML::ParserException& e) { + SPDLOG_ERROR("Failed to parse message data: {}", e.what()); + SPDLOG_ERROR("{}", (char*)buffer.data()); + return std::nullopt; + } + + const auto info = assetNode.begin()->second; + + std::vector<DialogString> text; + std::vector<DialogString> options; + + auto textNode = info["Text"]; + auto optionsNode = info["Options"]; + + for (YAML::iterator it = textNode.begin(); it != textNode.end(); ++it) { + DialogString dialogString; + dialogString.cmd = (*it)[0].as<uint32_t>(); + dialogString.str = (*it)[1].as<std::string>(); + dialogString.str += '\0'; + text.push_back(dialogString); + } + + uint32_t i = 0; + for (YAML::iterator it = optionsNode.begin(); it != optionsNode.end(); ++it) { + if (i >= 3) { + SPDLOG_WARN("BK64 QuizQuestion: Only 3 Options Allowed; extra options ignored"); + break; + } + DialogString optionString; + optionString.cmd = (*it)[0].as<uint32_t>(); + optionString.str = (*it)[1].as<std::string>(); + optionString.str += '\0'; + options.push_back(optionString); + i++; + } + + if (i != 3) { + throw std::runtime_error("BK64 QuizQuestion: Requires Exactly 3 Options"); + } + + return std::make_shared<QuizQuestionData>(text, options); +} + +} // namespace BK64 diff --git a/src/factories/bk64/QuizQuestionFactory.h b/src/factories/bk64/QuizQuestionFactory.h new file mode 100644 index 0000000..2c9598c --- /dev/null +++ b/src/factories/bk64/QuizQuestionFactory.h @@ -0,0 +1,50 @@ +#pragma once + +#include "DialogFactory.h" +#include <factories/BaseFactory.h> + +namespace BK64 { + +class QuizQuestionData : public IParsedData { + public: + std::vector<DialogString> mText; + std::vector<DialogString> mOptions; + + QuizQuestionData(std::vector<DialogString> text, std::vector<DialogString> options) + : mText(std::move(text)), mOptions(std::move(options)) { + } +}; + +class QuizQuestionHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class QuizQuestionBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class QuizQuestionCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class QuizQuestionModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class QuizQuestionFactory : 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(Code, QuizQuestionCodeExporter) REGISTER(Header, QuizQuestionHeaderExporter) + REGISTER(Binary, QuizQuestionBinaryExporter) REGISTER(Modding, QuizQuestionModdingExporter) }; + } + bool SupportModdedAssets() override { + return true; + } +}; +} // namespace BK64 diff --git a/src/factories/bk64/SoundfontTblFactory.cpp b/src/factories/bk64/SoundfontTblFactory.cpp new file mode 100644 index 0000000..99d5b76 --- /dev/null +++ b/src/factories/bk64/SoundfontTblFactory.cpp @@ -0,0 +1,138 @@ +#include "SoundfontTblFactory.h" + +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "types/RawBuffer.h" +#include "utils/Decompressor.h" + +#include <cstdint> +#include <cstring> +#include <stdexcept> +#include <unordered_set> + +namespace BK64 { + +namespace { + +uint16_t ReadU16BE(const uint8_t* p) { + return (uint16_t)((p[0] << 8) | p[1]); +} +int16_t ReadS16BE(const uint8_t* p) { + return (int16_t)ReadU16BE(p); +} +uint32_t ReadU32BE(const uint8_t* p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; +} +int32_t ReadS32BE(const uint8_t* p) { + return (int32_t)ReadU32BE(p); +} + +// Walk an N64 ALBankFile binary (big-endian, 32-bit offsets relative to ctl +// start) and return max(wavetable.base + wavetable.len) over every wavetable +// reachable from the file's banks. Throws on any structural inconsistency — +// silent fallbacks would just produce a wrong-size tbl and reproduce the bug +// we're fixing. +// +// ALBankFile: [0] s16 revision, [2] s16 bankCount, [4+] u32 bankOffsets[] +// ALBank: [0] s16 instCount, [4] s32 sampleRate, [8] u32 percussion, +// [12+] u32 instOffsets[] +// ALInstrument:[14] s16 soundCount, [16+] u32 soundOffsets[] +// ALSound: [8] u32 wavetable +// ALWaveTable: [0] u32 base, [4] s32 len +size_t ComputeTblSize(const uint8_t* ctl, size_t ctlSize, uint32_t ctlRomOffset) { + auto check = [&](uint32_t off, size_t need, const char* what) { + if ((size_t)off + need > ctlSize) { + SPDLOG_ERROR("SoundfontTblFactory: ctl walk OOB reading {} (ctl@0x{:X} size=0x{:X} off=0x{:X} need=0x{:X})", + what, ctlRomOffset, ctlSize, off, need); + throw std::runtime_error("SoundfontTblFactory: ctl walk OOB"); + } + }; + + check(0, 4, "header"); + int16_t bankCount = ReadS16BE(ctl + 2); + if (bankCount <= 0) { + throw std::runtime_error("SoundfontTblFactory: bankCount <= 0"); + } + + uint64_t maxEnd = 0; + std::unordered_set<uint32_t> seenWt; + + for (int b = 0; b < bankCount; b++) { + check(4 + b * 4, 4, "bankArray entry"); + uint32_t bankOff = ReadU32BE(ctl + 4 + b * 4); + if (bankOff == 0) { + continue; + } + + check(bankOff, 12, "ALBank header"); + int16_t instCount = ReadS16BE(ctl + bankOff + 0); + uint32_t percOff = ReadU32BE(ctl + bankOff + 8); + + auto visitInst = [&](uint32_t instOff) { + if (instOff == 0) { + return; + } + check(instOff, 16, "ALInstrument header"); + int16_t soundCount = ReadS16BE(ctl + instOff + 14); + for (int s = 0; s < soundCount; s++) { + check(instOff + 16 + s * 4, 4, "ALInstrument soundArray entry"); + uint32_t sndOff = ReadU32BE(ctl + instOff + 16 + s * 4); + if (sndOff == 0) { + continue; + } + check(sndOff, 12, "ALSound header"); + uint32_t wtOff = ReadU32BE(ctl + sndOff + 8); + if (wtOff == 0 || !seenWt.insert(wtOff).second) { + continue; + } + check(wtOff, 8, "ALWaveTable header"); + uint32_t base = ReadU32BE(ctl + wtOff + 0); + int32_t len = ReadS32BE(ctl + wtOff + 4); + if (len < 0) { + throw std::runtime_error("SoundfontTblFactory: negative wavetable len"); + } + uint64_t end = (uint64_t)base + (uint64_t)len; + if (end > maxEnd) { + maxEnd = end; + } + } + }; + + visitInst(percOff); + for (int i = 0; i < instCount; i++) { + check(bankOff + 12 + i * 4, 4, "ALBank instArray entry"); + visitInst(ReadU32BE(ctl + bankOff + 12 + i * 4)); + } + } + + if (maxEnd == 0) { + throw std::runtime_error("SoundfontTblFactory: ctl referenced no wavetables"); + } + + // Round up to BK's 16-byte ROM alignment. + return (size_t)((maxEnd + 0xF) & ~(uint64_t)0xF); +} + +} // namespace + +std::optional<std::shared_ptr<IParsedData>> SoundfontTblFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + const auto tblOffset = GetSafeNode<uint32_t>(node, "offset"); + const auto ctlOffset = GetSafeNode<uint32_t>(node, "ctl_offset"); + const auto ctlSize = GetSafeNode<uint32_t>(node, "ctl_size"); + + if ((size_t)ctlOffset + ctlSize > buffer.size()) { + throw std::runtime_error("SoundfontTblFactory: ctl_offset + ctl_size exceeds ROM size"); + } + + const size_t tblSize = ComputeTblSize(buffer.data() + ctlOffset, ctlSize, ctlOffset); + + if ((size_t)tblOffset + tblSize > buffer.size()) { + throw std::runtime_error("SoundfontTblFactory: computed tbl size exceeds ROM bounds"); + } + + SPDLOG_INFO("SoundfontTbl: tbl@0x{:X} size 0x{:X} (computed from ctl@0x{:X})", tblOffset, tblSize, ctlOffset); + + return std::make_shared<RawBuffer>(buffer.data() + tblOffset, tblSize); +} + +} // namespace BK64 diff --git a/src/factories/bk64/SoundfontTblFactory.h b/src/factories/bk64/SoundfontTblFactory.h new file mode 100644 index 0000000..4d63609 --- /dev/null +++ b/src/factories/bk64/SoundfontTblFactory.h @@ -0,0 +1,16 @@ +#pragma once + +#include "factories/BaseFactory.h" +#include "factories/BlobFactory.h" + +namespace BK64 { +class SoundfontTblFactory : public BaseFactory { + public: + std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override; + inline std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override { + return { REGISTER(Header, BlobHeaderExporter) REGISTER(Binary, BlobBinaryExporter) + REGISTER(Code, BlobCodeExporter) }; + } +}; + +} // namespace BK64 diff --git a/src/factories/bk64/SpriteFactory.cpp b/src/factories/bk64/SpriteFactory.cpp new file mode 100644 index 0000000..6a14b6a --- /dev/null +++ b/src/factories/bk64/SpriteFactory.cpp @@ -0,0 +1,354 @@ +#include "SpriteFactory.h" +#include "Companion.h" +#include "archive/SWrapper.h" +#include "spdlog/spdlog.h" +#include "utils/Decompressor.h" +#include <cstring> +#include <iomanip> +#include <yaml-cpp/yaml.h> +extern "C" { +#include "n64graphics/n64graphics.h" +} + +namespace BK64 { + +static const std::unordered_map<std::string, std::string> sTextureCTypes = { + { "RGBA16", "u16" }, { "RGBA32", "u16" }, { "CI4", "u8" }, { "CI8", "u8" }, { "I4", "u8" }, { "I8", "u8" }, + { "IA1", "u8" }, { "IA4", "u8" }, { "IA8", "u8" }, { "IA16", "u16" }, { "TLUT", "u16" }, +}; + +static const std::unordered_map<std::string, TextureType> sTextureFormats = { + { "RGBA16", TextureType::RGBA16bpp }, + { "RGBA32", TextureType::RGBA32bpp }, + { "CI4", TextureType::Palette4bpp }, + { "CI8", TextureType::Palette8bpp }, + { "I4", TextureType::Grayscale4bpp }, + { "I8", TextureType::Grayscale8bpp }, + { "IA1", TextureType::GrayscaleAlpha1bpp }, + { "IA4", TextureType::GrayscaleAlpha4bpp }, + { "IA8", TextureType::GrayscaleAlpha8bpp }, + { "IA16", TextureType::GrayscaleAlpha16bpp }, + { "TLUT", TextureType::TLUT }, +}; + +#define ALIGN8(val) (((val) + 7) & ~7) + +void ExtractChunk(LUS::BinaryReader& reader, std::vector<std::pair<int16_t, int16_t>>& positions, uint32_t& offset, + std::string format, std::string symbol, uint32_t chunkNo) { + reader.Seek(offset, LUS::SeekOffsetType::Start); + + int16_t x = reader.ReadInt16(); + int16_t y = reader.ReadInt16(); + int16_t width = reader.ReadInt16(); + int16_t height = reader.ReadInt16(); + + positions.emplace_back(x, y); + + offset += 4 * sizeof(int16_t); + offset = ALIGN8(offset); + + auto size = TextureUtils::CalculateTextureSize(sTextureFormats.at(format), width, height); + + YAML::Node texture; + texture["type"] = "TEXTURE"; + texture["offset"] = offset; + texture["format"] = format; + if (format == "CI4" || format == "CI8") { + texture["tlut_symbol"] = symbol + "TLUT"; + } + texture["ctype"] = "u16"; + texture["width"] = width; + texture["height"] = height; + texture["symbol"] = symbol + std::to_string(chunkNo); + + Companion::Instance->AddAsset(texture); + offset += size; +} + +ExportResult SpriteHeaderExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + const auto symbol = GetSafeNode(node, "symbol", entryName); + + if (Companion::Instance->IsOTRMode()) { + write << "static const ALIGN_ASSET(2) char " << symbol << "[] = \"__OTR__" << (*replacement) << "\";\n\n"; + return std::nullopt; + } + + return std::nullopt; +} + +ExportResult SpriteCodeExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto sprite = std::static_pointer_cast<SpriteData>(raw); + const auto offset = GetSafeNode<uint32_t>(node, "offset"); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + write << "BKSpriteHeader " << symbol << "_Header = { " << sprite->mFrameCount << ", " << sprite->mFormatCode + << " };\n\n"; + + // Chunk count per frame + if (!sprite->mChunkCounts.empty()) { + write << "u16 " << symbol << "_ChunkCounts[] = {\n" << fourSpaceTab; + for (size_t i = 0; i < sprite->mChunkCounts.size(); i++) { + write << sprite->mChunkCounts[i]; + if (i < sprite->mChunkCounts.size() - 1) { + write << ", "; + } + } + write << "\n};\n\n"; + } + + // Chunk positions + if (!sprite->mPositions.empty()) { + write << "BKSpriteChunk " << symbol << "_Chunks[] = {\n"; + + size_t chunkIndex = 0; + for (size_t frameIdx = 0; frameIdx < sprite->mChunkCounts.size(); frameIdx++) { + write << fourSpaceTab << "// Frame " << frameIdx << "\n"; + uint16_t chunkCount = sprite->mChunkCounts[frameIdx]; + + for (uint16_t i = 0; i < chunkCount; i++) { + if (chunkIndex < sprite->mPositions.size()) { + auto [x, y] = sprite->mPositions[chunkIndex]; + write << fourSpaceTab << "{ " << x << ", " << y << " }"; + + // Tag the row with which texture it points at + write << ", // " << symbol << "_" << frameIdx << "_" << i; + + if (chunkIndex < sprite->mPositions.size() - 1) { + write << ","; + } + write << "\n"; + chunkIndex++; + } + } + } + write << "};\n\n"; + } + + return offset; +} + +ExportResult SpriteBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, + YAML::Node& node, std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto sprites = std::static_pointer_cast<SpriteData>(raw); + + WriteHeader(writer, Torch::ResourceType::BKSprite, 0); + + auto wrapper = Companion::Instance->GetCurrentWrapper(); + + writer.Write(sprites->mFormatCode); + writer.Write(sprites->mUnk4); + writer.Write(sprites->mUnk6); + writer.Write(sprites->mUnk8); + writer.Write(sprites->mUnkA); + // Animation params unpacked from the ROM unkC bitfield + writer.Write(sprites->mAnimSpeed); + writer.Write(sprites->mAnimType); + writer.Write(sprites->mAnimDirection); + writer.Write(sprites->mAnimFlip); + writer.Write((uint32_t)sprites->mPositions.size()); + for (auto position : sprites->mPositions) { + writer.Write(position.first); + writer.Write(position.second); + } + writer.Write((uint32_t)sprites->mChunkCounts.size()); + for (auto chunkCount : sprites->mChunkCounts) { + writer.Write(chunkCount); + } + // Per-frame header data (x, y, w, h, unkA..unk12) + writer.Write((uint32_t)sprites->mFrameHeaders.size()); + for (const auto& fh : sprites->mFrameHeaders) { + writer.Write(fh.x); + writer.Write(fh.y); + writer.Write(fh.w); + writer.Write(fh.h); + writer.Write(fh.unkA); + writer.Write(fh.unkC); + writer.Write(fh.unkE); + writer.Write(fh.unk10); + writer.Write(fh.unk12); + } + + writer.Finish(write); + + return std::nullopt; +} + +ExportResult SpriteModdingExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + const auto sprite = std::static_pointer_cast<SpriteData>(raw); + const auto symbol = GetSafeNode(node, "symbol", entryName); + + *replacement += ".yaml"; + + YAML::Emitter out; + out << YAML::BeginMap; + out << YAML::Key << symbol; + out << YAML::Value; + out.SetIndent(2); + + out << YAML::BeginMap; + out << YAML::Key << "FrameCount"; + out << YAML::Value << sprite->mFrameCount; + out << YAML::Key << "FormatCode"; + out << YAML::Value << sprite->mFormatCode; + out << YAML::Key << "Frames"; + out << YAML::Value; + + out << YAML::BeginSeq; + size_t chunkIndex = 0; + for (size_t frameIdx = 0; frameIdx < sprite->mChunkCounts.size(); frameIdx++) { + out << YAML::BeginMap; + out << YAML::Key << "ChunkCount"; + out << YAML::Value << sprite->mChunkCounts[frameIdx]; + out << YAML::Key << "Chunks"; + out << YAML::Value; + + out << YAML::BeginSeq; + uint16_t chunkCount = sprite->mChunkCounts[frameIdx]; + for (uint16_t i = 0; i < chunkCount; i++) { + if (chunkIndex < sprite->mPositions.size()) { + auto [x, y] = sprite->mPositions[chunkIndex]; + out << YAML::Flow; + out << YAML::BeginMap; + out << YAML::Key << "X" << YAML::Value << x; + out << YAML::Key << "Y" << YAML::Value << y; + out << YAML::EndMap; + chunkIndex++; + } + } + out << YAML::EndSeq; + out << YAML::EndMap; + } + out << YAML::EndSeq; + + out << YAML::EndMap; + out << YAML::EndMap; + + write.write(out.c_str(), out.size()); + + return std::nullopt; +} + +std::optional<std::shared_ptr<IParsedData>> SpriteFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + LUS::BinaryReader reader(segment.data, segment.size); + auto symbol = GetSafeNode<std::string>(node, "symbol"); + const auto spriteOffset = GetSafeNode<uint32_t>(node, "offset"); // realistically always 0 + uint32_t offset; + + reader.SetEndianness(Torch::Endianness::Big); + + int16_t frameCount = reader.ReadInt16(); + int16_t formatCode = reader.ReadInt16(); + int16_t unk4 = reader.ReadInt16(); + int16_t unk6 = reader.ReadInt16(); + int16_t unk8 = reader.ReadInt16(); // display width, drives billboard vertex positioning + int16_t unkA = reader.ReadInt16(); // display height, same deal + // unkC packs the animation params into a BE u32 at offset 0x0C + uint32_t unkC_raw = reader.ReadUInt32(); + uint8_t animSpeed = (unkC_raw >> 28) & 0xF; // bits 31-28 + uint8_t animType = (unkC_raw >> 25) & 0x7; // bits 27-25 + uint8_t animDirection = (unkC_raw >> 23) & 0x3; // bits 24-23 + uint8_t animFlip = (unkC_raw >> 21) & 0x3; // bits 22-21 + 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: + SPDLOG_WARN("UNRECOGNISED FORMAT 0x{:X}", formatCode); + return std::nullopt; + } + + std::vector<uint16_t> chunkCounts; + std::vector<std::pair<int16_t, int16_t>> positions; + std::vector<SpriteFrameHeader> frameHeaders; + + if (frameCount > 0x100) { + offset = spriteOffset + 8; + std::string texSymbol = symbol + "_0_"; + ExtractChunk(reader, positions, offset, "RGBA16", texSymbol, 0); + return std::make_shared<SpriteData>(frameCount, formatCode, chunkCounts, positions, + std::vector<SpriteFrameHeader>{}, unk4, unk6, unk8, unkA, animSpeed, + animType, animDirection, animFlip); + } + + reader.Seek(0x10, LUS::SeekOffsetType::Start); + std::vector<uint32_t> offsets; + for (int16_t i = 0; i < frameCount; i++) { + offsets.push_back(reader.ReadUInt32()); + } + + uint32_t frame = 0; + for (const auto& frameOffset : offsets) { + offset = spriteOffset + 0x10 + frameOffset + frameCount * sizeof(uint32_t); + reader.Seek(offset - spriteOffset, LUS::SeekOffsetType::Start); + int16_t x = reader.ReadInt16(); + int16_t y = reader.ReadInt16(); + int16_t width = reader.ReadInt16(); + int16_t height = reader.ReadInt16(); + uint16_t chunkCount = reader.ReadInt16(); + auto unkA = reader.ReadInt16(); + auto unkC = reader.ReadInt16(); + auto unkE = reader.ReadInt16(); + auto unk10 = reader.ReadInt16(); + auto unk12 = reader.ReadInt16(); + + offset += 0x14; + + chunkCounts.push_back(chunkCount); + frameHeaders.push_back({ x, y, width, height, unkA, unkC, unkE, unk10, unk12 }); + + if (format == "CI4" || format == "CI8") { + offset = ALIGN8(offset); + + int16_t colors = (format == "CI4") ? 0x10 : 0x100; + YAML::Node tlut; + tlut["type"] = "TEXTURE"; + tlut["offset"] = offset; + tlut["format"] = "TLUT"; + tlut["ctype"] = "u16"; + tlut["colors"] = colors; + tlut["symbol"] = symbol + "_" + std::to_string(frame) + "_TLUT"; + Companion::Instance->AddAsset(tlut); + + offset += colors * sizeof(int16_t); + } + + for (uint16_t i = 0; i < chunkCount; i++) { + std::string texSymbol = symbol + "_" + std::to_string(frame) + "_"; + ExtractChunk(reader, positions, offset, format, texSymbol, i); + } + frame++; + } + + return std::make_shared<SpriteData>(frameCount, formatCode, chunkCounts, 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 new file mode 100644 index 0000000..a56a306 --- /dev/null +++ b/src/factories/bk64/SpriteFactory.h @@ -0,0 +1,95 @@ +#pragma once + +#include "factories/BaseFactory.h" +#include "utils/TextureUtils.h" +#include <string> +#include <types/RawBuffer.h> +#include <unordered_map> +#include <vector> + +namespace BK64 { + +/** + * One frame's header out of the ROM sprite struct. This is BKSpriteFrame in the decomp. + */ +struct SpriteFrameHeader { + int16_t x; // unk0 — X origin offset + int16_t y; // unk2 — Y origin offset + int16_t w; // frame width + int16_t h; // frame height + int16_t unkA; + int16_t unkC; + int16_t unkE; + int16_t unk10; + int16_t unk12; +}; + +/** + * A 2D billboard sprite. + */ +class SpriteData : public IParsedData { + public: + int16_t mFrameCount; // animation frame count + int16_t mFormatCode; // texture format (RGBA16=0, RGBA32=1, CI4=2, CI8=3, etc.) + int16_t mUnk4; // ROM header field at offset 4 + int16_t mUnk6; // ROM header field at offset 6 + int16_t mUnk8; // display width, drives billboard vertex positioning + int16_t mUnkA; // display height, same + // unkC bitfield: the animation params, a BE u32 at ROM offset 0x0C + uint8_t mAnimSpeed; // bits 31-28: 4 bits — animation speed divisor + uint8_t mAnimType; // bits 27-25: 3 bits — animation type (0=none, 1-4=various + // loop modes) + uint8_t mAnimDirection; // bits 24-23: 2 bits — animation direction control + uint8_t mAnimFlip; // bits 22-21: 2 bits — flip/mirror control + std::vector<uint16_t> mChunkCounts; // chunks per frame (length = mFrameCount) + std::vector<std::pair<int16_t, int16_t>> mPositions; // (x, y) offset per chunk + std::vector<SpriteFrameHeader> mFrameHeaders; // per-frame header data + + SpriteData(int16_t frameCount, int16_t formatCode, std::vector<uint16_t> chunkCounts, + std::vector<std::pair<int16_t, int16_t>> positions, std::vector<SpriteFrameHeader> frameHeaders = {}, + int16_t unk4 = 0, int16_t unk6 = 0, int16_t unk8 = 0, int16_t unkA = 0, uint8_t animSpeed = 0, + uint8_t animType = 0, uint8_t animDirection = 0, uint8_t animFlip = 0) + : mFrameCount(frameCount), mFormatCode(formatCode), mUnk4(unk4), mUnk6(unk6), mUnk8(unk8), mUnkA(unkA), + mAnimSpeed(animSpeed), mAnimType(animType), mAnimDirection(animDirection), mAnimFlip(animFlip), + mChunkCounts(std::move(chunkCounts)), mPositions(std::move(positions)), + mFrameHeaders(std::move(frameHeaders)) { + } +}; + +class SpriteHeaderExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, + std::string* replacement) override; +}; + +class SpriteBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, + std::string* replacement) override; +}; + +class SpriteCodeExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, + std::string* replacement) override; +}; + +class SpriteModdingExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName, YAML::Node& node, + std::string* replacement) override; +}; + +class SpriteFactory : public BaseFactory { + public: + std::optional<std::shared_ptr<IParsedData>> parse(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) }; + } + + bool HasModdedDependencies() override { + return true; + } + bool SupportModdedAssets() override { + return true; + } +}; + +} // namespace BK64 |
