summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeod <47716344+JeodC@users.noreply.github.com>2026-06-20 16:30:58 -0400
committerLywx <kiritodev01@gmail.com>2026-06-29 18:48:27 -0600
commit89fd2f9ac9b69d5f6b77dca9347aa61f9850d3b4 (patch)
treeaa60856e631bd81db5b08fe7f94a6ac861fa9f8e
parentdd6eb7897a30b881c1f93e8a6f66b946e3b599a1 (diff)
Add bk_zip library and BK64 decompression foundation
-rw-r--r--CMakeLists.txt52
-rw-r--r--config.yml2
-rw-r--r--lib/binarytools/BinaryReader.cpp10
-rw-r--r--lib/binarytools/BinaryReader.h1
-rw-r--r--lib/bk_zip/bk_unzip.cpp118
-rw-r--r--lib/bk_zip/bk_unzip.h22
-rw-r--r--src/Companion.h53
-rw-r--r--src/factories/ResourceType.h10
-rw-r--r--src/preprocess/CompTool.cpp2
-rw-r--r--src/utils/Decompressor.cpp125
-rw-r--r--src/utils/Decompressor.h3
-rw-r--r--src/utils/TorchUtils.cpp16
12 files changed, 369 insertions, 45 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 44a70e7..d63295d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.12)
project(torch)
include(FetchContent)
+set(CMAKE_POLICY_VERSION_MINIMUM 3.12)
+
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -17,6 +19,7 @@ option(BUILD_MK64 "Build with Mario Kart 64 support" ON)
option(BUILD_SF64 "Build with Star Fox 64 support" ON)
option(BUILD_PM64 "Build with Paper Mario support" ON)
option(BUILD_FZERO "Build with F-Zero X support" ON)
+option(BUILD_BK64 "Build with Banjo Kazooie support" ON)
option(BUILD_MARIO_ARTIST "Build with Mario Artist support" ON)
option(BUILD_NAUDIO "Build with NAudio support" ON)
@@ -69,7 +72,7 @@ endif()
include_directories(${CMAKE_CURRENT_SOURCE_DIR})
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/lib)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
-file(GLOB_RECURSE CXX_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/**/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/lib/strhash64/*.cpp)
+file(GLOB_RECURSE CXX_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/**/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/lib/strhash64/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/lib/bk_zip/*.cpp)
file(GLOB C_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c ${CMAKE_CURRENT_SOURCE_DIR}/src/**/*.c ${CMAKE_CURRENT_SOURCE_DIR}/lib/libmio0/*.c ${CMAKE_CURRENT_SOURCE_DIR}/lib/libyay0/*.c)
set(SRC_DIR ${CXX_FILES} ${C_FILES} ${LGFXD_FILES})
@@ -103,6 +106,12 @@ else()
list(FILTER SRC_DIR EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/src/factories/fzerox/*")
endif()
+if(BUILD_BK64)
+ add_definitions(-DBK64_SUPPORT)
+else()
+ list(FILTER SRC_DIR EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/src/factories/bk64/*")
+endif()
+
if(BUILD_MARIO_ARTIST)
add_definitions(-DMARIO_ARTIST_SUPPORT)
else()
@@ -120,6 +129,11 @@ if(ENABLE_ASAN)
add_link_options(-fsanitize=address)
endif()
+# MSVC static runtime for all targets (must be set before add_subdirectory/FetchContent)
+if (MSVC)
+ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
+endif()
+
# Build
if (USE_STANDALONE)
add_definitions(-DSTANDALONE)
@@ -143,7 +157,7 @@ else()
$<$<CONFIG:Debug>:
/w;
/Od;
- /ZI;
+ $<IF:$<BOOL:${ENABLE_ASAN}>,/Zi,/ZI>;
/${LINK_TYPE}d
>
$<$<CONFIG:Release>:
@@ -176,7 +190,6 @@ else()
target_link_options(${PROJECT_NAME} PRIVATE /bigobj)
endif()
add_definitions(-DSTORMLIB_NO_AUTO_LINK)
- set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
@@ -256,6 +269,24 @@ if(USE_STANDALONE)
FetchContent_MakeAvailable(spdlog)
target_link_libraries(${PROJECT_NAME} PRIVATE spdlog)
+
+ if (MSVC)
+ FetchContent_Declare(
+ zlib
+ GIT_REPOSITORY "https://github.com/madler/zlib.git"
+ GIT_TAG v1.3.1
+ FIND_PACKAGE_ARGS NAMES ZLIB
+ )
+ FetchContent_MakeAvailable(zlib)
+ set(ZLIB_INCLUDE_DIRS ${zlib_SOURCE_DIR})
+ set(ZLIB_LIBRARY zlibstatic CACHE STRING "")
+ set(ZLIB_LIBRARIES ${ZLIB_LIBRARY})
+ set(ZLIB_FOUND 1)
+ add_library(ZLIB::ZLIB ALIAS zlibstatic)
+ else ()
+ find_package(ZLIB REQUIRED)
+ target_link_libraries(${PROJECT_NAME} PRIVATE ZLIB::ZLIB)
+ endif()
else()
find_package(spdlog QUIET)
if(NOT spdlog_FOUND)
@@ -280,7 +311,20 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(tinyxml2)
-target_link_libraries(${PROJECT_NAME} PRIVATE tinyxml2 yaml-cpp N64Graphics BinaryTools)
+# Link zlib
+set(ZLIB_BUILD_SHARED_LIBS OFF CACHE BOOL "Build zlib as a static library" FORCE)
+set(ZLIB_BUILD_TESTS OFF CACHE BOOL "Disable building zlib tests" FORCE)
+set(SKIP_INSTALL_ALL ON CACHE BOOL "Skip zlib install targets" FORCE)
+
+FetchContent_Declare(
+ zlib
+ GIT_REPOSITORY https://github.com/madler/zlib.git
+ GIT_TAG v1.3.1
+ OVERRIDE_FIND_PACKAGE
+)
+FetchContent_MakeAvailable(zlib)
+
+target_link_libraries(${PROJECT_NAME} PRIVATE tinyxml2 zlibstatic yaml-cpp N64Graphics BinaryTools)
if(NOT USE_STANDALONE)
target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/config.yml b/config.yml
index a2ae31b..7b1403c 100644
--- a/config.yml
+++ b/config.yml
@@ -5,7 +5,7 @@ N64Hash:
name: Game # The name of the game; you can use any name you want.
path: assets/example/ # The path to the ymls folder
config:
- gbi: F3D # You can use F3D, F3DEX, F3DEX-MK64, F3DB, F3DEX2, F3DEXB,
+ gbi: F3D # You can use F3D, F3DEX, F3DEX-MK64, F3DEX_BK64, F3DB, F3DEX2, F3DEXB,
sort: OFFSET # This is how the assets are going to be sorted when generating the Header / C files. You can use OFFSET, ROFFSET for a reverse order or even specify a manual order using a list with the types.
output:
binary: example.otr # The name of the output binary file
diff --git a/lib/binarytools/BinaryReader.cpp b/lib/binarytools/BinaryReader.cpp
index 8032a2b..6e47e12 100644
--- a/lib/binarytools/BinaryReader.cpp
+++ b/lib/binarytools/BinaryReader.cpp
@@ -199,6 +199,16 @@ std::string LUS::BinaryReader::ReadString() {
return res;
}
+std::string LUS::BinaryReader::ReadString(size_t length) {
+ std::string res;
+
+ for (int i = 0; i < length; i++) {
+ res += ReadChar();
+ }
+
+ return res;
+}
+
std::string LUS::BinaryReader::ReadCString() {
std::string res;
diff --git a/lib/binarytools/BinaryReader.h b/lib/binarytools/BinaryReader.h
index 304455d..9041c40 100644
--- a/lib/binarytools/BinaryReader.h
+++ b/lib/binarytools/BinaryReader.h
@@ -41,6 +41,7 @@ class BinaryReader {
float ReadFloat();
double ReadDouble();
std::string ReadString();
+ std::string ReadString(size_t length);
std::string ReadCString();
std::vector<char> ToVector();
diff --git a/lib/bk_zip/bk_unzip.cpp b/lib/bk_zip/bk_unzip.cpp
new file mode 100644
index 0000000..f276d07
--- /dev/null
+++ b/lib/bk_zip/bk_unzip.cpp
@@ -0,0 +1,118 @@
+#include <vector>
+#include <stdexcept>
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <utility>
+#include <zlib.h>
+#include "bk_unzip.h"
+#include "spdlog/spdlog.h"
+
+namespace BK64 {
+
+static const char* zlibResultName(int result) {
+ switch (result) {
+ case Z_OK:
+ return "Z_OK";
+ case Z_STREAM_END:
+ return "Z_STREAM_END";
+ case Z_NEED_DICT:
+ return "Z_NEED_DICT";
+ case Z_ERRNO:
+ return "Z_ERRNO";
+ case Z_STREAM_ERROR:
+ return "Z_STREAM_ERROR";
+ case Z_DATA_ERROR:
+ return "Z_DATA_ERROR";
+ case Z_MEM_ERROR:
+ return "Z_MEM_ERROR";
+ case Z_BUF_ERROR:
+ return "Z_BUF_ERROR";
+ case Z_VERSION_ERROR:
+ return "Z_VERSION_ERROR";
+ default:
+ return "UNKNOWN";
+ }
+}
+
+/**
+ * Implementation of bk_unzip as declared in bk_unzip.h
+ */
+uint8_t* bk_unzip(const uint8_t* in_buffer, uint32_t* size) {
+ // Check buffer size
+ if (*size < 6) {
+ throw std::runtime_error("Input buffer too small");
+ }
+
+ // Check for BK magic header
+ if (in_buffer[0] != 0x11 || in_buffer[1] != 0x72) {
+ throw std::runtime_error("Input buffer does not have BK header");
+ }
+
+ // Extract expected uncompressed length (big-endian)
+ uint32_t expected_len = (static_cast<uint32_t>(in_buffer[2]) << 24) | (static_cast<uint32_t>(in_buffer[3]) << 16) |
+ (static_cast<uint32_t>(in_buffer[4]) << 8) | static_cast<uint32_t>(in_buffer[5]);
+
+ if (expected_len == 0) {
+ *size = 0;
+ return (uint8_t*)malloc(1);
+ }
+
+ if (expected_len > 8 * 1024 * 1024) {
+ throw std::runtime_error("BKZIP expected size too large: " + std::to_string(expected_len) +
+ " bytes (compressed: " + std::to_string(*size) + ")");
+ }
+
+ uint8_t* out_buffer = (uint8_t*)malloc(expected_len);
+ memset(out_buffer, 0, expected_len);
+
+ // Set up zlib stream
+ z_stream stream;
+ stream.zalloc = Z_NULL;
+ stream.zfree = Z_NULL;
+ stream.opaque = Z_NULL;
+ stream.avail_in = static_cast<uInt>(*size - 6);
+ stream.next_in = const_cast<Bytef*>(in_buffer + 6);
+ stream.avail_out = static_cast<uInt>(expected_len);
+ stream.next_out = out_buffer;
+
+ // Initialize for raw inflate (no zlib/gzip header) - same as wbits=-15 in Python
+ int result = inflateInit2(&stream, -15);
+ if (result != Z_OK) {
+ throw std::runtime_error(std::string("Failed to initialize zlib: ") + zlibResultName(result) +
+ (stream.msg ? std::string(" (") + stream.msg + ")" : ""));
+ }
+
+ // Perform decompression
+ result = inflate(&stream, Z_FINISH);
+
+ // Check for successful decompression:
+ // - Z_STREAM_END: normal completion
+ // - Z_BUF_ERROR with no input remaining: input fully consumed
+ bool success = (result == Z_STREAM_END) || (result == Z_BUF_ERROR && stream.avail_in == 0);
+
+ // Clean up zlib stream
+ inflateEnd(&stream);
+
+ if (!success) {
+ free(out_buffer);
+ throw std::runtime_error(std::string("Decompression failed: ") + zlibResultName(result) +
+ (stream.msg ? std::string(" (") + stream.msg + ")" : "") +
+ " compressed=" + std::to_string(*size) + " expected=" + std::to_string(expected_len) +
+ " produced=" + std::to_string(stream.total_out));
+ }
+
+ // Resize buffer to the actual decompressed size
+ // out_buffer.resize(stream.total_out);
+ *size = stream.total_out;
+
+ // Verify decompressed size matches expected size
+ if (stream.total_out != expected_len) {
+ throw std::runtime_error("Decompressed size (" + std::to_string(stream.total_out) +
+ ") does not match expected size (" + std::to_string(expected_len) + ")");
+ }
+
+ return out_buffer;
+}
+
+} // namespace BK64
diff --git a/lib/bk_zip/bk_unzip.h b/lib/bk_zip/bk_unzip.h
new file mode 100644
index 0000000..0a53edd
--- /dev/null
+++ b/lib/bk_zip/bk_unzip.h
@@ -0,0 +1,22 @@
+#ifndef BK_UNZIP_H
+#define BK_UNZIP_H
+
+#include <vector>
+#include <cstdint>
+#include <utility>
+
+namespace BK64 {
+
+/**
+ * Decompresses a buffer with a Banjo-Kazooie header format
+ *
+ * @param in_buffer Input compressed data with BK header (starting with 0x11, 0x72)
+ * @param size Size of the buffer (input as compressed size, output as decompressed size)
+ * @return Decompressed data as vector of bytes
+ * @throws std::runtime_error if decompression fails or header is invalid
+ */
+uint8_t* bk_unzip(const uint8_t* in_buffer, uint32_t* size);
+
+} // namespace BK64
+
+#endif // BK_UNZIP_H
diff --git a/src/Companion.h b/src/Companion.h
index eb6de7d..f8bcd68 100644
--- a/src/Companion.h
+++ b/src/Companion.h
@@ -5,10 +5,12 @@
#include <filesystem>
#include <vector>
#include <atomic>
+#include <array>
#include <fstream>
#include <unordered_map>
#include <unordered_set>
#include <variant>
+#include <functional>
#include "factories/BaseFactory.h"
#include "n64/Cartridge.h"
#include "utils/Decompressor.h"
@@ -36,7 +38,8 @@ enum class GBIMinorVersion {
None,
Mk64,
SM64,
- PM64
+ PM64,
+ BK64
};
enum class TableMode {
@@ -54,6 +57,7 @@ struct SegmentConfig {
std::unordered_map<uint32_t, uint32_t> global;
std::unordered_map<uint32_t, uint32_t> local;
std::unordered_map<uint32_t, uint32_t> temporal;
+ std::unordered_map<std::string, std::unordered_map<uint32_t, std::pair<std::uint32_t, std::uint32_t>>> compressed;
};
struct Table {
@@ -95,6 +99,8 @@ struct TorchConfig {
bool debug;
bool modding;
bool textureDefines;
+ bool includeAutogen;
+ bool dialogPack = false;
};
struct ParseResultData {
@@ -124,6 +130,7 @@ public:
this->gConfig.debug = debug;
this->gConfig.modding = modding;
this->gConfig.textureDefines = false;
+ this->gConfig.includeAutogen = false;
}
explicit Companion(std::vector<uint8_t> rom, const ArchiveType otr, const bool debug, const bool modding = false,
@@ -134,8 +141,11 @@ public:
this->gConfig.debug = debug;
this->gConfig.modding = modding;
this->gConfig.textureDefines = false;
+ this->gConfig.includeAutogen = false;
}
+ void SetRomPath(std::filesystem::path path) { this->gRomPath = std::move(path); }
+
explicit Companion(std::filesystem::path rom, const ArchiveType otr, const bool debug, const std::string& srcDir = "", const std::string& destPath = "") :
Companion(rom, otr, debug, false, srcDir, destPath) {}
@@ -143,7 +153,7 @@ public:
Companion(rom, otr, debug, false, srcDir, destPath) {}
void Init(ExportType type);
- void Init(ExportType type, std::atomic<size_t>& assetCount);
+ void Init(ExportType type, std::atomic<size_t>& assetCount, bool shouldProcess);
bool NodeHasChanges(const std::string& string);
@@ -156,6 +166,7 @@ public:
N64::Cartridge* GetCartridge() const { return this->gCartridge.get(); }
std::vector<uint8_t>& GetRomData() { return this->gRomData; }
std::string GetOutputPath() { return this->gConfig.outputPath; }
+ const std::string& GetAssetPath() const { return this->gAssetPath; }
std::string GetDestRelativeOutputPath() { return RelativePathToDestDir(GetOutputPath()); }
GBIVersion GetGBIVersion() const { return this->gConfig.gbi.version; }
@@ -168,18 +179,25 @@ public:
std::optional<ParseResultData> GetParseDataBySymbol(const std::string& symbol);
std::optional<std::uint32_t> GetFileOffsetFromSegmentedAddr(uint8_t segment) const;
+ std::optional<std::pair<std::uint32_t, std::uint32_t>> GetFileOffsetFromCompressedSegmentedAddr(uint8_t segment) const;
std::optional<std::shared_ptr<BaseFactory>> GetFactory(const std::string& type);
uint32_t PatchVirtualAddr(uint32_t addr);
std::optional<std::tuple<std::string, YAML::Node>> GetNodeByAddr(uint32_t addr);
std::optional<std::string> GetStringByAddr(uint32_t addr);
std::optional<std::tuple<std::string, YAML::Node>> GetSafeNodeByAddr(const uint32_t addr, std::string type);
std::optional<std::string> GetSafeStringByAddr(const uint32_t addr, std::string type);
- std::optional<std::vector<std::tuple<std::string, YAML::Node>>> GetNodesByType(const std::string& type);
+ std::optional<std::vector<std::tuple<std::string, YAML::Node>>> GetNodesByType(const std::string& type, bool includeAutogen = false);
+ // Same as GetNodesByType but returns a pointer into the internal cache to
+ // avoid copying. The pointer is valid until the cache is invalidated; do not
+ // hold across AddAsset/RegisterAsset calls. Returns nullptr if the current
+ // file has no nodes registered.
+ const std::vector<std::tuple<std::string, YAML::Node>>* GetNodesByTypeRef(const std::string& type, bool includeAutogen = false);
std::string GetSymbolFromAddr(uint32_t addr, bool validZero = false);
std::optional<std::uint32_t> GetFileOffset(void) const { return this->gCurrentFileOffset; };
std::optional<std::uint32_t> GetCurrSegmentNumber(void) const { return this->gCurrentSegmentNumber; };
CompressionType GetCurrCompressionType(void) const { return this->gCurrentCompressionType; };
+ std::optional<std::uint32_t> GetCurrentCompressedSize(void) const { return this->gCurrentCompressedSize; };
std::optional<VRAMEntry> GetCurrentVRAM(void) const { return this->gCurrentVram; };
std::optional<Table> SearchTable(uint32_t addr);
@@ -194,13 +212,19 @@ public:
void SetAdditionalFiles(const std::vector<std::string>& files) { this->gAdditionalFiles = files; }
void SetVersion(const std::string& version) { this->gVersion = version; }
+ std::string GetCurrentAssetName() const { return this->gCurrentAssetName; }
+ void SetAssetTotal(std::atomic<size_t>* total) { this->gAssetTotal = total; }
+ void SetPhaseCallback(std::function<void(int)> cb) { this->gPhaseCallback = cb; }
+
void SetProcess(bool shouldProcess);
TorchConfig& GetConfig() { return this->gConfig; }
BinaryWrapper* GetCurrentWrapper() { return this->gCurrentWrapper; }
std::optional<std::tuple<std::string, YAML::Node>> RegisterAsset(const std::string& name, YAML::Node& node);
+ std::optional<YAML::Node> AddSubFileAsset(YAML::Node asset, std::string newFileName, CompressionType newCompressionType, uint32_t compressedSize = 0);
std::optional<YAML::Node> AddAsset(YAML::Node asset);
- void RegisterFactory(const std::string& type, const std::shared_ptr<BaseFactory>& factory);
+ void SetCompressedSegment(uint32_t segmentId, uint32_t compressedFileOffset, uint32_t offset);
+ bool GetCompressedSegmentOffset(uint32_t* addr);
private:
TorchConfig gConfig;
YAML::Node gModdingConfig;
@@ -221,16 +245,22 @@ private:
BinaryWrapper* gCurrentWrapper;
// Temporal Variables
+ std::string gCurrentAssetName;
+ std::atomic<size_t>* gAssetCounter = nullptr;
+ std::atomic<size_t>* gAssetTotal = nullptr;
+ std::function<void(int)> gPhaseCallback;
std::string gCurrentFile;
+ std::vector<std::string> gSubFileList;
std::string gCurrentVirtualPath;
std::string gFileHeader;
bool gEnablePadGen = false;
- bool process = true;
+ bool mShouldProcess = true;
uint32_t gCurrentPad = 0;
uint32_t gCurrentFileOffset;
uint32_t gCurrentSegmentNumber;
std::optional<VRAMEntry> gCurrentVram;
CompressionType gCurrentCompressionType = CompressionType::None;
+ std::optional<std::uint32_t> gCurrentCompressedSize;
std::vector<Table> gTables;
std::vector<std::string> gCurrentExternalFiles;
std::unordered_map<int, std::string> gManualSegments;
@@ -246,13 +276,24 @@ private:
std::unordered_map<std::string, std::map<std::string, std::vector<WriteEntry>>> gWriteMap;
std::unordered_map<std::string, std::tuple<uint32_t, uint32_t>> gVirtualAddrMap;
std::unordered_map<std::string, std::unordered_map<uint32_t, std::tuple<std::string, YAML::Node>>> gAddrMap;
-
+ // Cached results of GetNodesByType keyed by [file][type][includeAutogen?].
+ // Built lazily; invalidated on RegisterAsset for the affected (file, type).
+ // Index 0 = !includeAutogen, index 1 = includeAutogen.
+ std::unordered_map<std::string,
+ std::unordered_map<std::string, std::array<std::vector<std::tuple<std::string, YAML::Node>>, 2>>>
+ gNodesByTypeCache;
+ // Validity bitmap; bit 0 = !includeAutogen, bit 1 = includeAutogen.
+ std::unordered_map<std::string, std::unordered_map<std::string, uint8_t>> gNodesByTypeCacheValid;
+
+ void ProcessParseFile(YAML::Node root, std::atomic<size_t>& assetCount);
+ void ProcessExportFile();
void ProcessFile(YAML::Node root);
void ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount);
void ParseEnums(std::string& file);
void ParseHash();
void ParseModdingConfig();
void ParseCurrentFileConfig(YAML::Node node, std::atomic<size_t>& assetCount);
+ void RegisterFactory(const std::string& type, const std::shared_ptr<BaseFactory>& factory);
void ExtractNode(YAML::Node& node, std::string& name, BinaryWrapper* binary);
void ProcessTables(YAML::Node& rom);
void LoadYAMLRecursively(const std::string &dirPath, std::vector<YAML::Node> &result, bool skipRoot);
diff --git a/src/factories/ResourceType.h b/src/factories/ResourceType.h
index ae48138..846c1c4 100644
--- a/src/factories/ResourceType.h
+++ b/src/factories/ResourceType.h
@@ -65,6 +65,16 @@ enum class ResourceType {
CourseData = 0x58435253, // XCRS
GhostRecord = 0x58475244, // XGRD
+ // BK64
+ BKSprite = 0x424B5350, // BKSP
+ BKAnimation = 0x424B414E, // BKAN
+ BKModel = 0x424B4D4F, // BKMO
+ BKDemoInput = 0x424B4449, // BKDI
+ BKDialog = 0x424B444C, // BKDL
+ BKMap = 0x424B4D50, // BKMP
+ BKGruntyQuestion = 0x424B4751, // BKGQ
+ BKQuizQuestion = 0x424B5151, // BKQQ
+
// NAudio v0
Bank = 0x42414E4B, // BANK
Sample = 0x41554643, // AIFC
diff --git a/src/preprocess/CompTool.cpp b/src/preprocess/CompTool.cpp
index 3b04a2e..da0afe5 100644
--- a/src/preprocess/CompTool.cpp
+++ b/src/preprocess/CompTool.cpp
@@ -93,7 +93,7 @@ std::vector<uint8_t> CompTool::Decompress(std::vector<uint8_t> rom) {
v_size = p_size;
break;
case CompType::COMPRESSED:
- decoded = Decompressor::Decode(std::vector(bytes, bytes + p_size), 0, CompressionType::MIO0, true);
+ decoded = Decompressor::Decode(std::vector(bytes, bytes + p_size), 0, CompressionType::MIO0, p_size, true);
bytes = decoded->data;
v_size = decoded->size;
break;
diff --git a/src/utils/Decompressor.cpp b/src/utils/Decompressor.cpp
index 2cf9412..f9c800c 100644
--- a/src/utils/Decompressor.cpp
+++ b/src/utils/Decompressor.cpp
@@ -2,6 +2,7 @@
#include "TorchUtils.h"
#include <stdexcept>
+#include <mutex>
#include "spdlog/spdlog.h"
#include <Companion.h>
@@ -12,13 +13,19 @@ extern "C" {
#include <libmio0/tkmk00.h>
}
+#include <bk_zip/bk_unzip.h>
+
std::unordered_map<uint32_t, DataChunk*> gCachedChunks;
+std::mutex gDecompCacheMutex;
DataChunk* Decompressor::Decode(const std::vector<uint8_t>& buffer, const uint32_t offset, const CompressionType type,
- bool ignoreCache) {
+ const uint32_t in_size, bool ignoreCache) {
- if (!ignoreCache && Torch::contains(gCachedChunks, offset)) {
- return gCachedChunks[offset];
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ if (!ignoreCache && Torch::contains(gCachedChunks, offset)) {
+ return gCachedChunks[offset];
+ }
}
const unsigned char* in_buf = buffer.data() + offset;
@@ -32,8 +39,11 @@ DataChunk* Decompressor::Decode(const std::vector<uint8_t>& buffer, const uint32
const auto decompressed = new uint8_t[head.dest_size];
mio0_decode(in_buf, decompressed, nullptr);
- gCachedChunks[offset] = new DataChunk{ decompressed, head.dest_size };
- return gCachedChunks[offset];
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ gCachedChunks[offset] = new DataChunk{ decompressed, head.dest_size };
+ return gCachedChunks[offset];
+ }
}
case CompressionType::YAY0: {
uint32_t size = 0;
@@ -43,8 +53,11 @@ DataChunk* Decompressor::Decode(const std::vector<uint8_t>& buffer, const uint32
throw std::runtime_error("Failed to decode YAY0");
}
- gCachedChunks[offset] = new DataChunk{ decompressed, size };
- return gCachedChunks[offset];
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ gCachedChunks[offset] = new DataChunk{ decompressed, size };
+ return gCachedChunks[offset];
+ }
}
case CompressionType::YAY1: {
uint32_t size = 0;
@@ -54,8 +67,30 @@ DataChunk* Decompressor::Decode(const std::vector<uint8_t>& buffer, const uint32
throw std::runtime_error("Failed to decode YAY1");
}
- gCachedChunks[offset] = new DataChunk{ decompressed, size };
- return gCachedChunks[offset];
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ gCachedChunks[offset] = new DataChunk{ decompressed, size };
+ return gCachedChunks[offset];
+ }
+ }
+ case CompressionType::BKZIP: {
+ uint32_t size = in_size;
+ try {
+ uint8_t* decompressed = BK64::bk_unzip(in_buf, &size);
+
+ if (!decompressed) {
+ throw std::runtime_error("bk_unzip returned null");
+ }
+
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ gCachedChunks[offset] = new DataChunk{ decompressed, size };
+ return gCachedChunks[offset];
+ }
+ } catch (const std::exception& e) {
+ throw std::runtime_error(std::string(e.what()) + " (ROM offset 0x" + Torch::to_hex(offset, false) +
+ " compressed size 0x" + Torch::to_hex(in_size, false) + ")");
+ }
}
default:
throw std::runtime_error("Unknown compression type");
@@ -64,8 +99,11 @@ DataChunk* Decompressor::Decode(const std::vector<uint8_t>& buffer, const uint32
DataChunk* Decompressor::DecodeTKMK00(const std::vector<uint8_t>& buffer, const uint32_t offset, const uint32_t size,
const uint32_t alpha) {
- if (Torch::contains(gCachedChunks, offset)) {
- return gCachedChunks[offset];
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ if (Torch::contains(gCachedChunks, offset)) {
+ return gCachedChunks[offset];
+ }
}
const uint8_t* in_buf = buffer.data() + offset;
@@ -73,8 +111,11 @@ DataChunk* Decompressor::DecodeTKMK00(const std::vector<uint8_t>& buffer, const
const auto decompressed = new uint8_t[size];
const auto rgba = new uint8_t[size];
tkmk00_decode(in_buf, decompressed, rgba, alpha);
- gCachedChunks[offset] = new DataChunk{ rgba, size };
- return gCachedChunks[offset];
+ {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
+ gCachedChunks[offset] = new DataChunk{ rgba, size };
+ return gCachedChunks[offset];
+ }
}
DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>& buffer,
@@ -84,16 +125,14 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>
CompressionType type = Companion::Instance->GetCurrCompressionType();
auto fileOffset = TranslateAddr(offset, true);
+ auto offsetFromFile = TranslateAddr(offset, false) - fileOffset;
// Check if an asset in a yaml file is mio0 compressed and extract.
if (node["mio0"]) {
auto assetPtr = ASSET_PTR(offset);
auto gameSize = Companion::Instance->GetRomData().size();
- auto fileOffset = TranslateAddr(offset, true);
- offset = ASSET_PTR(offset);
-
- auto decoded = Decode(buffer, fileOffset + offset, CompressionType::MIO0);
+ auto decoded = Decode(buffer, fileOffset + offsetFromFile, CompressionType::MIO0);
size_t decodedSize = decoded->size - offset;
size_t size;
@@ -144,7 +183,12 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>
size, decodedSize, assetPtr);
size = decodedSize;
}
-
+ return { .root = decoded, .segment = { decoded->data, size } };
+ }
+ if (node["bkzip"]) {
+ const auto compressedSize = GetSafeNode<uint32_t>(node, "compressed_size");
+ auto decoded = Decode(buffer, fileOffset + offsetFromFile, CompressionType::BKZIP, compressedSize);
+ auto size = node["size"] ? node["size"].as<size_t>() : manualSize.value_or(decoded->size);
return { .root = decoded, .segment = { decoded->data, size } };
}
@@ -176,6 +220,20 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>
return { .root = decoded, .segment = { decoded->data + offset, size } };
}
+ case CompressionType::BKZIP: {
+ const auto sizeEntry = Companion::Instance->GetCurrentCompressedSize();
+
+ if (!sizeEntry.has_value()) {
+ throw std::runtime_error("Auto decode missing file compressed size.");
+ }
+
+ auto decoded = Decode(buffer, fileOffset, type, sizeEntry.value());
+ auto size = node["size"] ? node["size"].as<size_t>() : manualSize.value_or(decoded->size - offsetFromFile);
+ SPDLOG_INFO("AutoDecode BKZIP: offset=0x{:X} fileOffset=0x{:X} offsetFromFile=0x{:X} compSize=0x{:X} "
+ "decodedSize=0x{:X} segSize=0x{:X}",
+ offset, fileOffset, offsetFromFile, sizeEntry.value(), decoded->size, size);
+ return { .root = decoded, .segment = { decoded->data + offsetFromFile, size } };
+ }
case CompressionType::YAZ0:
throw std::runtime_error(
"Found compressed yaz0 segment.\nDecompression of yaz0 has not been implemented yet.");
@@ -203,10 +261,9 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>
return { .root = nullptr, .segment = { buffer.data() + fileOffset, size } };
}
+ default:
+ throw std::runtime_error("Auto decode could not find a supported compression type.");
}
-
- throw std::runtime_error("Auto decode could not find a compression type nor uncompressed segment.\nThis is one of "
- "those issues that should never really happen.");
}
DecompressedData Decompressor::AutoDecode(uint32_t offset, std::optional<size_t> size, std::vector<uint8_t>& buffer) {
@@ -220,9 +277,15 @@ uint32_t Decompressor::TranslateAddr(uint32_t addr, bool baseAddress) {
if (IS_SEGMENTED(addr)) {
const auto segment = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(addr));
if (!segment.has_value()) {
- SPDLOG_ERROR("Segment data missing from game config\nPlease add an entry for segment {}",
- SEGMENT_NUMBER(addr));
- return 0;
+ const auto compressedSegmentPair =
+ Companion::Instance->GetFileOffsetFromCompressedSegmentedAddr(SEGMENT_NUMBER(addr));
+ if (!compressedSegmentPair.has_value()) {
+ SPDLOG_ERROR("Segment data missing from game config\nPlease add an entry for segment {}",
+ SEGMENT_NUMBER(addr));
+ return 0;
+ }
+ return compressedSegmentPair.value().first +
+ (!baseAddress ? (compressedSegmentPair.value().second + SEGMENT_OFFSET(addr)) : 0);
}
return segment.value() + (!baseAddress ? SEGMENT_OFFSET(addr) : 0);
@@ -234,7 +297,7 @@ uint32_t Decompressor::TranslateAddr(uint32_t addr, bool baseAddress) {
const auto vram = vramEntry.value();
if (addr >= vram.addr) {
- return vram.offset + (addr - vram.addr);
+ return vram.offset + (!baseAddress ? (addr - vram.addr) : 0);
}
}
@@ -273,11 +336,14 @@ bool Decompressor::IsSegmented(uint32_t addr) {
const auto segment = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(addr));
if (!segment.has_value()) {
- SPDLOG_ERROR("Segment data missing from game config\nPlease add an entry for segment {}",
- SEGMENT_NUMBER(addr));
- return false;
+ const auto compressedSegmentPair =
+ Companion::Instance->GetFileOffsetFromCompressedSegmentedAddr(SEGMENT_NUMBER(addr));
+ if (!compressedSegmentPair.has_value()) {
+ SPDLOG_ERROR("Segment data missing from game config\nPlease add an entry for segment {}",
+ SEGMENT_NUMBER(addr));
+ return false;
+ }
}
-
return true;
}
@@ -285,6 +351,7 @@ bool Decompressor::IsSegmented(uint32_t addr) {
}
void Decompressor::ClearCache() {
+ std::lock_guard<std::mutex> lock(gDecompCacheMutex);
for (auto& [key, value] : gCachedChunks) {
delete[] value->data;
}
diff --git a/src/utils/Decompressor.h b/src/utils/Decompressor.h
index 22f4b24..cc07193 100644
--- a/src/utils/Decompressor.h
+++ b/src/utils/Decompressor.h
@@ -14,6 +14,7 @@ enum class CompressionType {
YAY0,
YAY1,
YAZ0,
+ BKZIP,
};
@@ -33,7 +34,7 @@ struct DecompressedData {
class Decompressor {
public:
- static DataChunk* Decode(const std::vector<uint8_t>& buffer, uint32_t offset, CompressionType type, bool ignoreCache = false);
+ static DataChunk* Decode(const std::vector<uint8_t>& buffer, uint32_t offset, CompressionType type, const uint32_t in_size = 0, bool ignoreCache = false);
static DataChunk* DecodeTKMK00(const std::vector<uint8_t>& buffer, const uint32_t offset, const uint32_t size, const uint32_t alpha);
static DecompressedData AutoDecode(YAML::Node& node, std::vector<uint8_t>& buffer, std::optional<size_t> size = std::nullopt);
static DecompressedData AutoDecode(uint32_t offset, std::optional<size_t> size, std::vector<uint8_t>& buffer);
diff --git a/src/utils/TorchUtils.cpp b/src/utils/TorchUtils.cpp
index 86c836f..ec95c4a 100644
--- a/src/utils/TorchUtils.cpp
+++ b/src/utils/TorchUtils.cpp
@@ -8,12 +8,22 @@
namespace fs = std::filesystem;
uint32_t Torch::translate(const uint32_t offset) {
- if (SEGMENT_NUMBER(offset) > 0x01) {
+ // Segment 0 is a raw offset. BK64 also translates segment 1 (it stores assets
+ // in compressed segments); other games leave segment 1 raw.
+ const uint32_t firstSegment =
+ Companion::Instance->GetGBIMinorVersion() == GBIMinorVersion::BK64 ? 0x00 : 0x01;
+ if (SEGMENT_NUMBER(offset) > firstSegment) {
auto segment = SEGMENT_NUMBER(offset);
const auto addr = Companion::Instance->GetFileOffsetFromSegmentedAddr(segment);
if (!addr.has_value()) {
- SPDLOG_ERROR("Segment data missing from game config\nPlease add an entry for segment {}", segment);
- throw std::runtime_error("Failed to find offset");
+ // Fall back to the compressed-segment map
+ const auto compressedSegmentPair = Companion::Instance->GetFileOffsetFromCompressedSegmentedAddr(segment);
+
+ if (!compressedSegmentPair.has_value()) {
+ SPDLOG_ERROR("Segment data missing from game config\nPlease add an entry for segment {}", segment);
+ throw std::runtime_error("Failed to find offset");
+ }
+ return compressedSegmentPair.value().first + compressedSegmentPair.value().second + SEGMENT_OFFSET(offset);
}
return addr.value() + SEGMENT_OFFSET(offset);