diff options
| author | Jeod <47716344+JeodC@users.noreply.github.com> | 2026-06-20 16:30:58 -0400 |
|---|---|---|
| committer | Lywx <kiritodev01@gmail.com> | 2026-06-29 18:48:27 -0600 |
| commit | 89fd2f9ac9b69d5f6b77dca9347aa61f9850d3b4 (patch) | |
| tree | aa60856e631bd81db5b08fe7f94a6ac861fa9f8e | |
| parent | dd6eb7897a30b881c1f93e8a6f66b946e3b599a1 (diff) | |
Add bk_zip library and BK64 decompression foundation
| -rw-r--r-- | CMakeLists.txt | 52 | ||||
| -rw-r--r-- | config.yml | 2 | ||||
| -rw-r--r-- | lib/binarytools/BinaryReader.cpp | 10 | ||||
| -rw-r--r-- | lib/binarytools/BinaryReader.h | 1 | ||||
| -rw-r--r-- | lib/bk_zip/bk_unzip.cpp | 118 | ||||
| -rw-r--r-- | lib/bk_zip/bk_unzip.h | 22 | ||||
| -rw-r--r-- | src/Companion.h | 53 | ||||
| -rw-r--r-- | src/factories/ResourceType.h | 10 | ||||
| -rw-r--r-- | src/preprocess/CompTool.cpp | 2 | ||||
| -rw-r--r-- | src/utils/Decompressor.cpp | 125 | ||||
| -rw-r--r-- | src/utils/Decompressor.h | 3 | ||||
| -rw-r--r-- | src/utils/TorchUtils.cpp | 16 |
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}) @@ -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); |
