From 58c746e740ce71354b35a7d7aee9dcc700ddda21 Mon Sep 17 00:00:00 2001 From: Jeod <47716344+JeodC@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:05:56 -0400 Subject: [PATCH 1/2] Fix a couple extract bugs --- src/Companion.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Companion.cpp b/src/Companion.cpp index 3bede42f..a9a5897a 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -597,6 +597,8 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic& ass } } + this->gCurrentFileConfig.reset(node); + if (node["directory"]) { this->gCurrentDirectory = node["directory"].as(); } @@ -1295,6 +1297,7 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { this->gCurrentExternalFiles.clear(); this->gSubFileList.clear(); this->gManualSegments.clear(); + this->gCurrentFileConfig.reset(YAML::Node()); GFXDOverride::ClearVtx(); if (root[":config"]) { @@ -1447,7 +1450,11 @@ void Companion::Process(std::atomic& assetCount) { } } this->gAssetPath = (this->gSourceDirectory / rom["path"].as()).string(); - this->gCommonAssetPath = (this->gSourceDirectory / rom["common_path"].as()).string(); + // Optional: a rom that keeps all its ymls under one tree has no common dir, and + // getRecursiveEntries already treats an empty path as "nothing to add". + if (rom["common_path"]) { + this->gCommonAssetPath = (this->gSourceDirectory / rom["common_path"].as()).string(); + } if (rom["filelist"]) { const std::string filelistPath = (this->gSourceDirectory / rom["filelist"].as()).string(); From a0599ab76c9bc38edf2ab1e4e982ab46e3c174b1 Mon Sep 17 00:00:00 2001 From: Jeod <47716344+JeodC@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:19:10 -0400 Subject: [PATCH 2/2] BK64: Trust the texture type field --- src/factories/bk64/GeoLayoutFactory.cpp | 34 +++++++ src/factories/bk64/GeoLayoutFactory.h | 9 +- src/factories/bk64/ModelFactory.cpp | 126 ++++++++---------------- 3 files changed, 81 insertions(+), 88 deletions(-) diff --git a/src/factories/bk64/GeoLayoutFactory.cpp b/src/factories/bk64/GeoLayoutFactory.cpp index b3a91b32..20016aa0 100644 --- a/src/factories/bk64/GeoLayoutFactory.cpp +++ b/src/factories/bk64/GeoLayoutFactory.cpp @@ -28,6 +28,11 @@ static uint32_t GetGeoCommandByteSize(const GeoLayoutCommand& cmd) { case GeoLayoutOpCode::LoadDL: bodySize = 4; break; // 2+2 + case GeoLayoutOpCode::NOP: + case GeoLayoutOpCode::NOP2: + case GeoLayoutOpCode::NOP3: + bodySize = 4; + break; // 4 bytes Banjo's Backpack writes and nothing reads case GeoLayoutOpCode::Skinning: // 2 per arg + 2 for terminator bodySize = static_cast(cmd.args.size()) * 2 + 2; @@ -150,6 +155,12 @@ ExportResult BK64::GeoLayoutBinaryExporter::Export(std::ostream& write, std::sha writeU16(std::get(arguments[0])); writeU16(std::get(arguments[1])); break; + case GeoLayoutOpCode::NOP: + case GeoLayoutOpCode::NOP2: + case GeoLayoutOpCode::NOP3: + for (size_t i = 0; i < arguments.size(); i++) + writeU8(std::get(arguments[i])); + break; case GeoLayoutOpCode::Skinning: writeU16(std::get(arguments[0])); for (size_t i = 1; i < arguments.size(); i++) @@ -298,6 +309,19 @@ ExportResult GeoLayoutModdingExporter::Export(std::ostream& write, std::shared_p out << YAML::Key << "dlIndex" << YAML::Value << std::get(arguments.at(0)); out << YAML::Key << "triCount" << YAML::Value << std::get(arguments.at(1)); break; + // Nothing reads the body, so there is nothing worth writing out + case GeoLayoutOpCode::NOP: + out << YAML::Key << "NOP"; + out << YAML::Value << YAML::BeginMap; + break; + case GeoLayoutOpCode::NOP2: + out << YAML::Key << "NOP2"; + out << YAML::Value << YAML::BeginMap; + break; + case GeoLayoutOpCode::NOP3: + out << YAML::Key << "NOP3"; + out << YAML::Value << YAML::BeginMap; + break; case GeoLayoutOpCode::Skinning: out << YAML::Key << "Skinning"; out << YAML::Value << YAML::BeginMap; @@ -543,6 +567,16 @@ std::optional> GeoLayoutFactory::parse(std::vector< args.emplace_back(triCount); break; } + case GeoLayoutOpCode::NOP: + case GeoLayoutOpCode::NOP2: + case GeoLayoutOpCode::NOP3: { + // sGeoCmdList dispatches these to modelRender_geoCmd_NOP--nothing reads the body. + // Keep the bytes so the binary exporter writes the command back as it was. + for (int32_t i = 0; i < 4; i++) { + args.emplace_back(reader.ReadUByte()); + } + break; + } case GeoLayoutOpCode::Skinning: { auto dlOffsetPreviousBone = reader.ReadUInt16(); diff --git a/src/factories/bk64/GeoLayoutFactory.h b/src/factories/bk64/GeoLayoutFactory.h index 092d55f5..1f099576 100644 --- a/src/factories/bk64/GeoLayoutFactory.h +++ b/src/factories/bk64/GeoLayoutFactory.h @@ -14,12 +14,15 @@ enum class GeoLayoutOpCode { Sort, Bone, LoadDL, - Skinning = 5, + NOP, + Skinning, Branch, UnknownCmd7, LOD, - ReferencePoint = 10, - Selector = 12, + NOP2, + ReferencePoint, + NOP3, + Selector, DrawDistance, UnknownCmdE, UnknownCmdF, diff --git a/src/factories/bk64/ModelFactory.cpp b/src/factories/bk64/ModelFactory.cpp index 565f06c8..0f22d6cf 100644 --- a/src/factories/bk64/ModelFactory.cpp +++ b/src/factories/bk64/ModelFactory.cpp @@ -5,6 +5,7 @@ #include "types/RawBuffer.h" #include "utils/Decompressor.h" #include "utils/TorchUtils.h" +#include #define BK64_MODEL_HEADER 0xB #define TEXTURE_HEADER_SIZE 0x8 @@ -43,6 +44,28 @@ static const std::unordered_mapGetGBIVersion()).at(#cmd) +// textureInfo_getBitDepth tests the type field bit by bit and takes the first one set, so a +// texture with extra bits still resolves to a format. The game's ladder stops at 0x8; Torch +// extends to 0x10. +static std::string_view GetTextureFormat(uint16_t type) { + if (type & 0x1) { + return "CI4"; + } + if (type & 0x2) { + return "CI8"; + } + if (type & 0x4) { + return "RGBA16"; + } + if (type & 0x8) { + return "RGBA32"; + } + if (type & 0x10) { + return "IA8"; + } + return {}; +} + ExportResult ModelHeaderExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { const auto symbol = GetSafeNode(node, "symbol", entryName); @@ -230,7 +253,7 @@ ExportResult BK64::ModelBinaryExporter::Export(std::ostream& write, std::shared_ std::unordered_map 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 bool isCI = tex.tlutColors != 0; // CI4 and CI8 are the only types with a palette const uint32_t tlutByteSize = isCI ? tex.tlutColors * 2u : 0u; imageOffsetToTex[tex.textureDataOffset + tlutByteSize] = ti; } @@ -478,14 +501,10 @@ std::optional> ModelFactory::parse(std::vector(width); @@ -493,20 +512,15 @@ std::optional> ModelFactory::parse(std::vectormTexInfos.push_back(texInfo); @@ -516,45 +530,6 @@ std::optional> ModelFactory::parse(std::vectormTexDataSize = 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) { @@ -568,30 +543,11 @@ std::optional> ModelFactory::parse(std::vectormTexInfos[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; + const std::string format{ GetTextureFormat(tex.type) }; + if (format.empty()) { + continue; } + uint32_t tlutByteSize = tex.tlutColors * 2; std::string texSymbol = symbol + "_tex_" + std::to_string(i);