summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Companion.cpp243
-rw-r--r--src/archive/ZWrapper.cpp8
-rw-r--r--src/factories/DisplayListFactory.cpp72
-rw-r--r--src/factories/DisplayListOverrides.cpp20
-rw-r--r--src/factories/TextureFactory.cpp4
-rw-r--r--src/factories/bk64/AnimFactory.cpp212
-rw-r--r--src/factories/bk64/AnimFactory.h71
-rw-r--r--src/factories/bk64/BKAssetFactory.cpp397
-rw-r--r--src/factories/bk64/BKAssetFactory.h84
-rw-r--r--src/factories/bk64/DemoInputFactory.cpp172
-rw-r--r--src/factories/bk64/DemoInputFactory.h55
-rw-r--r--src/factories/bk64/DialogFactory.cpp276
-rw-r--r--src/factories/bk64/DialogFactory.h68
-rw-r--r--src/factories/bk64/GeoLayoutFactory.cpp715
-rw-r--r--src/factories/bk64/GeoLayoutFactory.h82
-rw-r--r--src/factories/bk64/GruntyQuestionFactory.cpp283
-rw-r--r--src/factories/bk64/GruntyQuestionFactory.h57
-rw-r--r--src/factories/bk64/MapFactory.cpp1029
-rw-r--r--src/factories/bk64/MapFactory.h314
-rw-r--r--src/factories/bk64/ModelFactory.cpp994
-rw-r--r--src/factories/bk64/ModelFactory.h279
-rw-r--r--src/factories/bk64/QuizQuestionFactory.cpp273
-rw-r--r--src/factories/bk64/QuizQuestionFactory.h50
-rw-r--r--src/factories/bk64/SoundfontTblFactory.cpp138
-rw-r--r--src/factories/bk64/SoundfontTblFactory.h16
-rw-r--r--src/factories/bk64/SpriteFactory.cpp354
-rw-r--r--src/factories/bk64/SpriteFactory.h95
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