diff --git a/CMakeLists.txt b/CMakeLists.txt index 44a70e7b..42e03f69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,8 @@ option(BUILD_PM64 "Build with Paper Mario support" ON) option(BUILD_FZERO "Build with F-Zero X support" ON) option(BUILD_MARIO_ARTIST "Build with Mario Artist support" ON) option(BUILD_NAUDIO "Build with NAudio support" ON) +option(BUILD_OOT "Build with Ocarina of Time support" ON) +option(PORT_VERSION_ENDIANNESS "Include endianness byte in portVersion file" OFF) if(EMSCRIPTEN) set(BUILD_SM64 OFF) # TODO: This is broken for some reason @@ -70,7 +72,7 @@ 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 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) +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 ${CMAKE_CURRENT_SOURCE_DIR}/lib/libyaz0/*.c) set(SRC_DIR ${CXX_FILES} ${C_FILES} ${LGFXD_FILES}) if(BUILD_SM64) @@ -115,6 +117,16 @@ else() list(FILTER SRC_DIR EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/src/factories/naudio/*") endif() +if(PORT_VERSION_ENDIANNESS) + add_definitions(-DPORT_VERSION_ENDIANNESS) +endif() + +if(BUILD_OOT) + add_definitions(-DOOT_SUPPORT) +else() + list(FILTER SRC_DIR EXCLUDE REGEX "${CMAKE_CURRENT_SOURCE_DIR}/src/factories/oot/*") +endif() + if(ENABLE_ASAN) add_compile_options(-fsanitize=address) add_link_options(-fsanitize=address) diff --git a/lib/libyaz0/yaz0.c b/lib/libyaz0/yaz0.c new file mode 100644 index 00000000..20f94fbc --- /dev/null +++ b/lib/libyaz0/yaz0.c @@ -0,0 +1,57 @@ +#include "yaz0.h" +#include "libmio0/utils.h" +#include +#include + +uint8_t* yaz0_decode(const uint8_t* in, uint32_t* out_size) { + if (strncmp((const char*)in, "Yaz0", 4) != 0) { + return NULL; + } + + uint32_t decompressed_size = read_u32_be(in + 4); + uint8_t* out = malloc(decompressed_size); + if (!out) { + return NULL; + } + + uint32_t src = YAZ0_HEADER_LENGTH; + uint32_t dst = 0; + uint8_t group_head = 0; + int bits_left = 0; + + while (dst < decompressed_size) { + if (bits_left == 0) { + group_head = in[src++]; + bits_left = 8; + } + + if (group_head & 0x80) { + /* literal byte */ + out[dst++] = in[src++]; + } else { + /* back-reference */ + uint8_t b1 = in[src++]; + uint8_t b2 = in[src++]; + + uint32_t dist = ((b1 & 0x0F) << 8) | b2; + uint32_t copy_src = dst - (dist + 1); + uint32_t count; + + if (b1 >> 4) { + count = (b1 >> 4) + 2; + } else { + count = in[src++] + 0x12; + } + + for (uint32_t i = 0; i < count; i++) { + out[dst++] = out[copy_src + i]; + } + } + + group_head <<= 1; + bits_left--; + } + + *out_size = decompressed_size; + return out; +} diff --git a/lib/libyaz0/yaz0.h b/lib/libyaz0/yaz0.h new file mode 100644 index 00000000..447f3617 --- /dev/null +++ b/lib/libyaz0/yaz0.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#define YAZ0_HEADER_LENGTH 16 + +extern uint8_t* yaz0_decode(const uint8_t* in, uint32_t* out_size); diff --git a/src/AliasManager.cpp b/src/AliasManager.cpp new file mode 100644 index 00000000..1f12aeb2 --- /dev/null +++ b/src/AliasManager.cpp @@ -0,0 +1,23 @@ +#include "AliasManager.h" +#include "utils/TorchUtils.h" + +static AliasManager sAliasManagerInstance; +AliasManager* AliasManager::Instance = &sAliasManagerInstance; + +void AliasManager::Register(const std::string& primaryPath, const std::string& aliasPath) { + mAliases[primaryPath].push_back(aliasPath); +} + +void AliasManager::WriteAliases(const std::string& primaryPath, BinaryWrapper* wrapper, + const std::vector& data) { + if (!Torch::contains(mAliases, primaryPath)) return; + + for (auto& alias : mAliases[primaryPath]) { + wrapper->AddFile(alias, data); + } + mAliases.erase(primaryPath); +} + +void AliasManager::Clear() { + mAliases.clear(); +} diff --git a/src/AliasManager.h b/src/AliasManager.h new file mode 100644 index 00000000..501a6718 --- /dev/null +++ b/src/AliasManager.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include +#include "archive/BinaryWrapper.h" + +class AliasManager { +public: + static AliasManager* Instance; + + void Register(const std::string& primaryPath, const std::string& aliasPath); + void WriteAliases(const std::string& primaryPath, BinaryWrapper* wrapper, + const std::vector& data); + void Clear(); + +private: + std::unordered_map> mAliases; +}; diff --git a/src/Companion.cpp b/src/Companion.cpp index c61ab8a3..69029fe6 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -1,4 +1,5 @@ #include "Companion.h" +#include "AliasManager.h" #include "utils/Decompressor.h" #include "utils/StringHelper.h" @@ -98,6 +99,22 @@ #include "factories/mario_artist/MA2D1Factory.h" #endif +#ifdef OOT_SUPPORT +#include "factories/oot/OoTArrayFactory.h" +#include "factories/oot/OoTSkeletonFactory.h" +#include "factories/oot/OoTLimbFactory.h" +#include "factories/oot/OoTMtxFactory.h" +#include "factories/oot/OoTAnimationFactory.h" +#include "factories/oot/OoTCurveAnimationFactory.h" +#include "factories/oot/OoTPlayerAnimationFactory.h" +#include "factories/oot/OoTCollisionFactory.h" +#include "factories/oot/OoTTextFactory.h" +#include "factories/oot/OoTSceneFactory.h" +#include "factories/oot/OoTPathFactory.h" +#include "factories/oot/OoTCutsceneFactory.h" +#include "factories/oot/OoTAudioFactory.h" +#endif + #ifdef NAUDIO_SUPPORT #include "factories/naudio/v0/AudioHeaderFactory.h" #include "factories/naudio/v0/BankFactory.h" @@ -255,6 +272,24 @@ void Companion::Init(const ExportType type, std::atomic& assetCount) { this->RegisterFactory("NAUDIO:V1:ADPCM_BOOK", std::make_shared()); this->RegisterFactory("NAUDIO:V1:SEQUENCE", std::make_shared()); #endif +#ifdef OOT_SUPPORT + this->RegisterFactory("OOT:ARRAY", std::make_shared()); + this->RegisterFactory("OOT:MTX", std::make_shared()); + this->RegisterFactory("OOT:SKELETON", std::make_shared()); + this->RegisterFactory("OOT:LIMB", std::make_shared()); + this->RegisterFactory("OOT:ANIMATION", std::make_shared()); + this->RegisterFactory("OOT:CURVE_ANIMATION", std::make_shared()); + this->RegisterFactory("OOT:PLAYER_ANIMATION", std::make_shared()); + this->RegisterFactory("OOT:PLAYER_ANIMATION_DATA", std::make_shared()); + this->RegisterFactory("OOT:COLLISION", std::make_shared()); + this->RegisterFactory("OOT:TEXT", std::make_shared()); + this->RegisterFactory("OOT:SCENE", std::make_shared()); + this->RegisterFactory("OOT:ROOM", std::make_shared()); + this->RegisterFactory("OOT:CUTSCENE", std::make_shared()); + this->RegisterFactory("OOT:PATH", std::make_shared()); + this->RegisterFactory("OOT:AUDIO", std::make_shared()); +#endif + #ifndef __EMSCRIPTEN__ // We call this manually this->Process(assetCount); #endif @@ -322,7 +357,12 @@ std::optional Companion::ParseNode(YAML::Node& node, std::strin auto factory = this->GetFactory(type); if (!factory.has_value()) { +#ifdef THROW_ON_UNKNOWN_TYPE throw std::runtime_error("No factory by the name '" + type + "' found for '" + name + "'"); +#else + SPDLOG_WARN("No factory by the name '{}' found for '{}', skipping", type, name); + return std::nullopt; +#endif } auto impl = factory->get(); @@ -443,6 +483,10 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic& ass } } + if (node["directory"]) { + this->gCurrentDirectory = node["directory"].as(); + } + if (node["manual_segments"]) { auto manualSegments = node["manual_segments"]; if (manualSegments.IsSequence() && manualSegments.size()) { @@ -666,9 +710,7 @@ void Companion::ProcessFile(YAML::Node root) { ProcessFile(root, assetCount); } -void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { - assetCount++; - // Set compressed file offsets and compression type +void Companion::PreparseConfig(YAML::Node& root) { if (auto segments = root[":config"]["segments"]) { if (segments.IsSequence() && segments.size() > 0) { if (segments[0].IsSequence() && segments[0].size() == 2) { @@ -686,6 +728,12 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { } } + if (auto directory = root[":config"]["directory"]) { + this->gCurrentDirectory = directory.as(); + } +} + +void Companion::PopulateAddrMap(YAML::Node& root) { for (auto asset = root.begin(); asset != root.end(); ++asset) { auto node = asset->second; auto entryName = asset->first.as(); @@ -715,9 +763,9 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { this->gAddrMap[this->gCurrentFile][node["offset"].as()] = std::make_tuple(output, node); } +} - // Stupid hack because the iteration broke the assets - root = YAML::LoadFile(this->gCurrentFile); +void Companion::ResetTemporalState() { this->gConfig.segment.local.clear(); this->gFileHeader.clear(); this->gCurrentPad = 0; @@ -726,10 +774,22 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { this->gCurrentSegmentNumber = 0; this->gCurrentCompressionType = CompressionType::None; this->gCurrentFileOffset = 0; + this->gCurrentDirectory = relative(fs::path(this->gCurrentFile), this->gAssetPath).replace_extension(""); this->gTables.clear(); this->gCurrentExternalFiles.clear(); this->gManualSegments.clear(); GFXDOverride::ClearVtx(); +} + +void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { + assetCount++; + ResetTemporalState(); + PreparseConfig(root); + PopulateAddrMap(root); + + // Reload YAML because iteration invalidates yaml-cpp nodes + root = YAML::LoadFile(this->gCurrentFile); + ResetTemporalState(); if (root[":config"]) { this->ParseCurrentFileConfig(root[":config"], assetCount); @@ -795,7 +855,10 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { 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())); + auto dataVec = std::vector(data.begin(), data.end()); + this->gCurrentWrapper->AddFile(result.name, dataVec); + + AliasManager::Instance->WriteAliases(result.name, this->gCurrentWrapper, dataVec); for (auto& entry : this->gCompanionFiles) { auto output = (this->gCurrentDirectory / entry.first).string(); @@ -1213,6 +1276,9 @@ void Companion::Process(std::atomic& assetCount) { } else if (key == "F3DEX2_PM64") { this->gConfig.gbi.version = GBIVersion::f3dex2; this->gConfig.gbi.subversion = GBIMinorVersion::PM64; + } else if (key == "F3DEX2_OoT") { + this->gConfig.gbi.version = GBIVersion::f3dex2; + this->gConfig.gbi.subversion = GBIMinorVersion::OoT; } else if (key == "F3DEXB") { this->gConfig.gbi.version = GBIVersion::f3dexb; } else if (key == "F3DEX_MK64") { @@ -1228,6 +1294,11 @@ void Companion::Process(std::atomic& assetCount) { } } + if (cfg["primary_virtual_segment"]) { + this->gConfig.segment.primaryVirtual = cfg["primary_virtual_segment"].as(); + } + + if (auto sort = cfg["sort"]) { if (sort.IsSequence()) { this->gWriteOrder = sort.as>(); @@ -1362,7 +1433,6 @@ void Companion::Process(std::atomic& assetCount) { } YAML::Node root = YAML::LoadFile(yamlPath); - this->gCurrentDirectory = relative(entry.path(), this->gAssetPath).replace_extension(""); this->gCurrentFile = yamlPath; if (!Torch::contains(this->gProcessedFiles, this->gCurrentFile)) { @@ -1553,15 +1623,126 @@ std::optional Companion::GetFileOffsetFromSegmentedAddr(const uin return std::nullopt; } +// Convert a VRAM address (0x80XXXXXX) to a segmented address that Torch can look up. +// +// N64 overlays store pointers as VRAM addresses (e.g. 0x80B65CC4). Torch needs +// segmented addresses (e.g. 0x06000CC4) to find assets in gAddrMap. This function +// translates between the two using the file's virtual address mapping. +// +// Not all 0x80XXXXXX addresses are VRAM pointers — segment 0x80 addresses also have +// bit 31 set. We distinguish them by checking if the address falls within the current +// file's VRAM range. +ResolvedAddr Companion::ResolveVirtualAddr(uint32_t addr) { + // Addresses below 0x80000000 aren't virtual. + if (!(addr & 0x80000000)) { + return { addr, IS_SEGMENTED(addr) ? ResolvedAddr::Segmented : ResolvedAddr::FileRelative }; + } + + // If the current file doesn't have a virtual address mapping, we can't + // determine if this is a segment-0x80 address or VRAM for another file. + if (!Torch::contains(gVirtualAddrMap, gCurrentFile)) { + return { addr, ResolvedAddr::Unknown }; + } + + // vramBase: where this file's data is loaded in VRAM at runtime (e.g. 0x80B65000) + // physStart: where this file's data lives in the ROM (e.g. 0x00D24FC0) + auto vramBase = std::get<0>(gVirtualAddrMap[gCurrentFile]); + auto physStart = std::get<1>(gVirtualAddrMap[gCurrentFile]); + + // If addr is below vramBase, it's not pointing into this file's VRAM range. + // It's a segmented address using segment 0x80+. + if (addr < vramBase) { + return { addr, ResolvedAddr::Segmented }; + } + + // addr is within this file's VRAM range. Compute how far into the file it points. + auto relOffset = addr - vramBase; + + // No segments configured — return absolute ROM address. + if (this->gConfig.segment.local.empty()) { + return { relOffset + physStart, ResolvedAddr::Absolute }; + } + + // Convert the relative offset to a segmented address by finding which segment + // maps to this file's ROM data. When multiple segments alias the same ROM data, + // prefer the configured primary virtual segment for consistency. + if (this->gConfig.segment.primaryVirtual.has_value()) { + auto primarySeg = this->gConfig.segment.primaryVirtual.value(); + if (Torch::contains(this->gConfig.segment.local, primarySeg) && + this->gConfig.segment.local[primarySeg] == physStart) { + return { (primarySeg << 24) | relOffset, ResolvedAddr::Segmented }; + } + } + for (auto& [seg, segOffset] : this->gConfig.segment.local) { + if (segOffset == physStart) { + return { (seg << 24) | relOffset, ResolvedAddr::Segmented }; + } + } + + // No matching segment found — return absolute ROM address as fallback. + return { relOffset + physStart, ResolvedAddr::Absolute }; +} + uint32_t Companion::PatchVirtualAddr(uint32_t addr) { - if (addr & 0x80000000) { - if (Torch::contains(gVirtualAddrMap, gCurrentFile)) { - addr -= std::get<0>(gVirtualAddrMap[gCurrentFile]); - addr += std::get<1>(gVirtualAddrMap[gCurrentFile]); + return ResolveVirtualAddr(addr).addr; +} + +// Look up an asset by converting a segmented address to an absolute ROM address, +// then scanning all entries in the file for one that resolves to the same address. +// Used when multiple segments map to the same ROM data, so the same asset may be +// registered under a different segment number than the one being looked up. +std::optional> Companion::FindNodeInOverlaySegments( + uint32_t addr, const std::string& file) { + // Only applies to files with virtual address mappings (overlays). + if (!Torch::contains(gVirtualAddrMap, file)) { + return std::nullopt; + } + + // Convert the segmented address to an absolute ROM address. + auto addrSeg = this->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(addr)); + if (!addrSeg.has_value()) { + return std::nullopt; + } + auto absAddr = addrSeg.value() + SEGMENT_OFFSET(addr); + + // Scan all entries in the file for one whose absolute ROM address matches. + for (auto& [storedAddr, entry] : this->gAddrMap[file]) { + if (storedAddr == addr || !IS_SEGMENTED(storedAddr)) { + continue; + } + auto storedSeg = this->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(storedAddr)); + if (storedSeg.has_value() && storedSeg.value() + SEGMENT_OFFSET(storedAddr) == absAddr) { + return entry; } } + return std::nullopt; +} + +// Check if this address is a VRAM pointer belonging to the given external file. +// If it falls within the file's VRAM range, convert to a virtual segment address +// and look it up in the file's gAddrMap. +std::optional> Companion::FindInExternalByVRAM( + uint32_t addr, const std::string& file) { + // Can't resolve if the external file doesn't have a virtual address mapping. + if (!Torch::contains(gVirtualAddrMap, file)) { + return std::nullopt; + } + + // Address doesn't belong to this file if it's below the file's VRAM base. + auto vramBase = std::get<0>(gVirtualAddrMap[file]); + if (addr < vramBase) { + return std::nullopt; + } + + // Convert VRAM address to virtual segment address: + // relative offset within the file, with 0x80 as the segment number. + uint32_t virtualSegmentAddr = 0x80000000 | (addr - vramBase); + if (Torch::contains(this->gAddrMap[file], virtualSegmentAddr)) { + return this->gAddrMap[file][virtualSegmentAddr]; + } - return addr; + // Address doesn't belong to this external file. + return std::nullopt; } std::optional> Companion::GetNodeByAddr(uint32_t addr) { @@ -1569,25 +1750,47 @@ std::optional> Companion::GetNodeByAddr(uint return std::nullopt; } - // HACK: Adjust address to rom address if virtual address - addr = PatchVirtualAddr(addr); + auto resolved = ResolveVirtualAddr(addr); - if (!Torch::contains(this->gAddrMap[this->gCurrentFile], addr)) { - for (auto& file : this->gCurrentExternalFiles) { - if (!Torch::contains(this->gAddrMap, file)) { - SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file); - continue; - } + // Direct lookup by resolved address. This works because ResolveVirtualAddr + // produces addresses in the same format that PopulateAddrMap uses to register + // assets (segmented offset for files with segments, plain offset otherwise). + if (Torch::contains(this->gAddrMap[this->gCurrentFile], resolved.addr)) { + return this->gAddrMap[this->gCurrentFile][resolved.addr]; + } - if (!Torch::contains(this->gAddrMap[file], addr)) { - continue; + // For overlay files, the same data may be registered under a different segment + // number (e.g. looking up segment 8 when the asset was registered under segment 6). + if (resolved.kind == ResolvedAddr::Segmented) { + auto match = FindNodeInOverlaySegments(resolved.addr, this->gCurrentFile); + if (match.has_value()) { + return match; + } + } + + // Search external files + for (auto& file : this->gCurrentExternalFiles) { + if (!Torch::contains(this->gAddrMap, file)) { + SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file); + continue; + } + + // Direct lookup in external file by resolved address. + if (Torch::contains(this->gAddrMap[file], resolved.addr)) { + return this->gAddrMap[file][resolved.addr]; + } + + // If we couldn't determine the address type, it may be a VRAM address + // belonging to this external file's virtual address space. + if (resolved.kind == ResolvedAddr::Unknown) { + auto vramMatch = FindInExternalByVRAM(resolved.addr, file); + if (vramMatch.has_value()) { + return vramMatch; } - return this->gAddrMap[file][addr]; } - return std::nullopt; } - return this->gAddrMap[this->gCurrentFile][addr]; + return std::nullopt; } std::optional Companion::GetStringByAddr(const uint32_t addr) { @@ -1746,6 +1949,7 @@ void Companion::RegisterCompanionFile(const std::string path, std::vector SPDLOG_TRACE("Registered companion file {}", path); } + std::string Companion::NormalizeAsset(const std::string& name) const { auto path = fs::path(this->gCurrentFile).stem().string() + "_" + name; return path; @@ -1796,6 +2000,9 @@ std::vector Companion::ParseVersionString(const std::string& version) { auto wv = LUS::BinaryWriter(); wv.SetEndianness(Torch::Endianness::Big); +#ifdef PORT_VERSION_ENDIANNESS + wv.Write(static_cast(Torch::Endianness::Big)); +#endif wv.Write(major); wv.Write(minor); wv.Write(patch); @@ -1814,6 +2021,15 @@ std::optional Companion::AddAsset(YAML::Node asset) { const auto symbol = GetSafeNode(asset, "symbol", ""); const auto decl = this->GetNodeByAddr(offset); + // For OoT, all assets should be pre-declared in enriched YAML. + // Throw if an undeclared asset is encountered to catch enrichment gaps. + if (!decl.has_value() && this->gConfig.gbi.subversion == GBIMinorVersion::OoT) { + throw std::runtime_error( + "AddAsset: undeclared " + type + " at " + Torch::to_hex(offset, false) + + " (symbol: " + symbol + ") in " + this->gCurrentFile + + " — YAML enrichment incomplete"); + } + if (decl.has_value()) { auto found = std::get<1>(decl.value()); if (GetTypeNode(found) != type) { diff --git a/src/Companion.h b/src/Companion.h index eb6de7da..75a5f67a 100644 --- a/src/Companion.h +++ b/src/Companion.h @@ -36,7 +36,8 @@ enum class GBIMinorVersion { None, Mk64, SM64, - PM64 + PM64, + OoT }; enum class TableMode { @@ -54,6 +55,7 @@ struct SegmentConfig { std::unordered_map global; std::unordered_map local; std::unordered_map temporal; + std::optional primaryVirtual; }; struct Table { @@ -69,6 +71,17 @@ struct VRAMEntry { uint32_t offset; }; +struct ResolvedAddr { + uint32_t addr; + enum Kind { + Segmented, // addr has a segment number (e.g. 0x06001CC4) + Absolute, // addr is a physical ROM address (e.g. 0x00D269A0) + FileRelative, // addr is a plain offset within the file (e.g. 0x00005CC8) + Unknown, // addr has bit 31 set but we can't determine if it's + // segment-0x80 or VRAM for another file + } kind; +}; + struct WriteEntry { std::string name; uint32_t addr; @@ -170,6 +183,7 @@ class Companion { std::optional GetFileOffsetFromSegmentedAddr(uint8_t segment) const; std::optional> GetFactory(const std::string& type); uint32_t PatchVirtualAddr(uint32_t addr); + ResolvedAddr ResolveVirtualAddr(uint32_t addr); std::optional> GetNodeByAddr(uint32_t addr); std::optional GetStringByAddr(uint32_t addr); std::optional> GetSafeNodeByAddr(const uint32_t addr, std::string type); @@ -200,6 +214,7 @@ class Companion { std::optional> RegisterAsset(const std::string& name, YAML::Node& node); std::optional AddAsset(YAML::Node asset); + std::string GetCurrentDirectory() const { return gCurrentDirectory.string(); } void RegisterFactory(const std::string& type, const std::shared_ptr& factory); private: TorchConfig gConfig; @@ -249,6 +264,11 @@ class Companion { void ProcessFile(YAML::Node root); void ProcessFile(YAML::Node root, std::atomic& assetCount); + void PreparseConfig(YAML::Node& root); + void PopulateAddrMap(YAML::Node& root); + void ResetTemporalState(); + std::optional> FindNodeInOverlaySegments(uint32_t addr, const std::string& file); + std::optional> FindInExternalByVRAM(uint32_t addr, const std::string& file); void ParseEnums(std::string& file); void ParseHash(); void ParseModdingConfig(); diff --git a/src/factories/BaseFactory.h b/src/factories/BaseFactory.h index 3fdb1cf5..b63f025f 100644 --- a/src/factories/BaseFactory.h +++ b/src/factories/BaseFactory.h @@ -24,7 +24,8 @@ namespace fs = std::filesystem; #define SEGMENT_NUMBER(x) (((uint32_t)(x) >> 24) & 0xFF) // I would love to use 0x01000000, but the stupid compiler takes it as 0x01 #define IS_SEGMENTED(x) ((SEGMENT_NUMBER(x) > 0) && (SEGMENT_NUMBER(x) < 0x20)) -#define ASSET_PTR(x) (IS_SEGMENTED(x) ? SEGMENT_OFFSET(x) : (x)) +#define IS_VIRTUAL_SEGMENT(x) (SEGMENT_NUMBER(x) >= 0x80) +#define ASSET_PTR(x) ((IS_SEGMENTED(x) || IS_VIRTUAL_SEGMENT(x)) ? SEGMENT_OFFSET(x) : (x)) #define tab_t "\t" #define fourSpaceTab " " diff --git a/src/factories/BlobFactory.cpp b/src/factories/BlobFactory.cpp index 7e69cf0c..fc40e169 100644 --- a/src/factories/BlobFactory.cpp +++ b/src/factories/BlobFactory.cpp @@ -51,6 +51,11 @@ ExportResult BlobBinaryExporter::Export(std::ostream& write, std::shared_ptr(raw)->mBuffer; + if (data.empty() && node["size"] && node["size"].as() == 0) { + // YAML explicitly declares size: 0 — write a 0-byte file with no header. + return std::nullopt; + } + WriteHeader(writer, Torch::ResourceType::Blob, 0); writer.Write((uint32_t)data.size()); writer.Write((char*)data.data(), data.size()); diff --git a/src/factories/DisplayListFactory.cpp b/src/factories/DisplayListFactory.cpp index a0f268cc..0fd5843a 100644 --- a/src/factories/DisplayListFactory.cpp +++ b/src/factories/DisplayListFactory.cpp @@ -5,6 +5,7 @@ #include "Companion.h" #include #include "n64/gbi-otr.h" +#include "oot/OoTDListHelpers.h" #ifdef STANDALONE #include @@ -195,6 +196,9 @@ void DebugDisplayList(uint32_t w0, uint32_t w1) { #endif std::optional> SearchVtx(uint32_t ptr) { + auto result = OoT::DListHelpers::SearchVtx(ptr); + if (result.has_value()) return result; + auto decs = Companion::Instance->GetNodesByType("VTX"); if (!decs.has_value()) { @@ -218,6 +222,9 @@ std::optional> SearchVtx(uint32_t ptr) { ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, std::string& entryName, YAML::Node& node, std::string* replacement) { + auto ootResult = OoT::DListHelpers::Export(write, raw, entryName, node, replacement); + if (ootResult.has_value()) return ootResult.value(); + const auto gbi = Companion::Instance->GetGBIVersion(); auto cmds = std::static_pointer_cast(raw)->mGfxs; auto writer = LUS::BinaryWriter(); @@ -472,6 +479,9 @@ ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr> DListFactory::parse(std::vector& raw_buffer, YAML::Node& node) { + auto ootResult = OoT::DListHelpers::Parse(raw_buffer, node); + if (ootResult.has_value()) return ootResult.value(); + const auto gbi = Companion::Instance->GetGBIVersion(); auto count = GetSafeNode(node, "count", -1); diff --git a/src/factories/DisplayListFactory.h b/src/factories/DisplayListFactory.h index e46ab221..e578d367 100644 --- a/src/factories/DisplayListFactory.h +++ b/src/factories/DisplayListFactory.h @@ -26,6 +26,10 @@ class DListCodeExporter : public BaseExporter { }; #endif +#ifdef OOT_SUPPORT +#include "oot/DeferredVtx.h" +#endif + class DListFactory : public BaseFactory { public: std::optional> parse(std::vector& buffer, YAML::Node& data) override; diff --git a/src/factories/ResourceType.h b/src/factories/ResourceType.h index ae481386..38f0f42b 100644 --- a/src/factories/ResourceType.h +++ b/src/factories/ResourceType.h @@ -65,6 +65,23 @@ enum class ResourceType { CourseData = 0x58435253, // XCRS GhostRecord = 0x58475244, // XGRD + // OoT + OoTAnimation = 0x4F414E4D, // OANM + OoTPlayerAnimation = 0x4F50414D, // OPAM + OoTRoom = 0x4F524F4D, // OROM + OoTCollisionHeader = 0x4F434F4C, // OCOL + OoTSkeleton = 0x4F534B4C, // OSKL + OoTSkeletonLimb = 0x4F534C42, // OSLB + OoTPath = 0x4F505448, // OPTH + OoTCutscene = 0x4F435654, // OCUT + OoTText = 0x4F545854, // OTXT + OoTAudio = 0x4F415544, // OAUD + OoTAudioSample = 0x4F534D50, // OSMP + OoTAudioSoundFont = 0x4F534654, // OSFT + OoTAudioSequence = 0x4F534551, // OSEQ + OoTBackground = 0x4F424749, // OBGI + OoTSceneCommand = 0x4F52434D, // ORCM + // NAudio v0 Bank = 0x42414E4B, // BANK Sample = 0x41554643, // AIFC diff --git a/src/factories/oot/DeferredVtx.cpp b/src/factories/oot/DeferredVtx.cpp new file mode 100644 index 00000000..e3efe6a6 --- /dev/null +++ b/src/factories/oot/DeferredVtx.cpp @@ -0,0 +1,132 @@ +#ifdef OOT_SUPPORT + +#include "DeferredVtx.h" +#include "Companion.h" +#include "factories/DisplayListOverrides.h" +#include "n64/CommandMacros.h" +#include "spdlog/spdlog.h" +#include +#include +#include + +// N64 vertex size in bytes (matching N64Vtx_t: 3*int16 + uint16 + 2*int16 + 4*uchar = 16) +static constexpr uint32_t kVtxSize = 16; + +// Deferred VTX consolidation state (ZAPD-style MergeConnectingVertexLists). +// ZAPD merges VTX per-DList (each DList has its own vertices map and merge pass). +// We collect VTX during each DList parse call and flush at the end of that parse. +namespace DeferredVtx { + +bool sDeferred = false; +std::vector sPendingList; + +void BeginDefer() { + sDeferred = true; + sPendingList.clear(); +} + +bool IsDeferred() { + return sDeferred; +} + +std::vector SaveAndClearPending() { + auto saved = std::move(sPendingList); + sPendingList.clear(); + return saved; +} + +void RestorePending(std::vector& saved) { + // Prepend saved items to current pending list (in case anything was added during the save) + saved.insert(saved.end(), sPendingList.begin(), sPendingList.end()); + sPendingList = std::move(saved); +} + +void AddPending(uint32_t addr, uint32_t count) { + sPendingList.push_back({addr, count}); +} + +// Flush pending VTX for a single DList: merge adjacent arrays and register assets. +// Called at the end of each DList parse() to match ZAPD's per-DList merge scope. +void FlushDeferred(const std::string& baseName) { + // Don't clear sDeferred here — it stays active for the entire room. + // Each DList parse flushes its own collected VTX. + auto pending = std::move(sPendingList); + sPendingList.clear(); + + if (pending.empty()) { + return; + } + + SPDLOG_INFO("VTX FlushDeferred: {} pending VTX for {}", pending.size(), baseName); + + // Sort by segment offset + std::sort(pending.begin(), pending.end(), + [](const PendingVtx& a, const PendingVtx& b) { + return SEGMENT_OFFSET(a.addr) < SEGMENT_OFFSET(b.addr); + }); + + // Merge adjacent/overlapping VTX arrays (ZAPD's MergeConnectingVertexLists algorithm). + // Two arrays merge if the first array's end >= the second's start. + struct MergedVtx { + uint32_t addr; // segment address of start + uint32_t endOff; // segment offset of end (exclusive) + }; + std::vector merged; + + for (auto& pv : pending) { + uint32_t startOff = SEGMENT_OFFSET(pv.addr); + uint32_t endOff = startOff + pv.count * kVtxSize; + + if (merged.empty() || startOff > merged.back().endOff) { + // New group + merged.push_back({pv.addr, endOff}); + } else { + // Extend existing group + if (endOff > merged.back().endOff) { + merged.back().endOff = endOff; + } + } + } + + // Register each merged VTX group as an asset + for (auto& mg : merged) { + uint32_t startOff = SEGMENT_OFFSET(mg.addr); + uint32_t totalBytes = mg.endOff - startOff; + uint32_t totalCount = totalBytes / kVtxSize; + + // Build proper symbol: baseName + "Vtx_" + 6-digit hex offset + std::ostringstream ss; + ss << baseName << "Vtx_" << std::uppercase << std::hex + << std::setfill('0') << std::setw(6) << startOff; + std::string symbol = ss.str(); + + SPDLOG_INFO("VTX consolidation: {} at 0x{:X} count={}", symbol, mg.addr, totalCount); + + // Look up the pre-declared VTX in YAML (should exist with enrichment) + auto registeredNode = Companion::Instance->GetNodeByAddr(mg.addr); + if (!registeredNode.has_value()) { + SPDLOG_WARN("Undeclared VTX at 0x{:X} — YAML enrichment incomplete", mg.addr); + } + + // Register overlap mappings for all pending addresses within this group. + if (registeredNode.has_value()) { + auto [fullPath, vtxNode] = registeredNode.value(); + auto overlapTuple = std::make_tuple(symbol, vtxNode); + for (auto& pv : pending) { + uint32_t pvOff = SEGMENT_OFFSET(pv.addr); + if (pvOff > startOff && pvOff < mg.endOff) { + GFXDOverride::RegisterVTXOverlap(pv.addr, overlapTuple); + } + } + } + } +} + +void EndDefer() { + sDeferred = false; + sPendingList.clear(); +} + +} // namespace DeferredVtx + +#endif diff --git a/src/factories/oot/DeferredVtx.h b/src/factories/oot/DeferredVtx.h new file mode 100644 index 00000000..033abf28 --- /dev/null +++ b/src/factories/oot/DeferredVtx.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include +#include +#include + +namespace DeferredVtx { + struct PendingVtx { uint32_t addr; uint32_t count; }; + void BeginDefer(); + bool IsDeferred(); + void FlushDeferred(const std::string& baseName); + std::vector SaveAndClearPending(); + void RestorePending(std::vector& saved); + void AddPending(uint32_t addr, uint32_t count); + void EndDefer(); +} + +#endif diff --git a/src/factories/oot/OoTAnimationFactory.cpp b/src/factories/oot/OoTAnimationFactory.cpp new file mode 100644 index 00000000..64a155b4 --- /dev/null +++ b/src/factories/oot/OoTAnimationFactory.cpp @@ -0,0 +1,122 @@ +#ifdef OOT_SUPPORT + +#include "OoTAnimationFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +std::optional> OoTAnimationFactory::parse(std::vector& buffer, YAML::Node& node) { + // Check for legacy animation type (data stays in ROM segment, not extracted) + auto animType = GetSafeNode(node, "anim_type", "normal"); + if (animType == "legacy") { + auto anim = std::make_shared(); + anim->frameCount = 0; + anim->limit = 0; + anim->isLegacy = true; + return anim; + } + + auto [_, segment] = Decompressor::AutoDecode(node, buffer, 0x10); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + // ROM layout: AnimationHeader (16 bytes) + // +0x00: int16 frameCount + // +0x02: int16 padding + // +0x04: segptr rotationValues + // +0x08: segptr rotationIndices + // +0x0C: int16 limit + // +0x0E: int16 padding + int16_t frameCount = reader.ReadInt16(); + reader.ReadInt16(); // padding + uint32_t rawRotValues = reader.ReadUInt32(); + uint32_t rawRotIndices = reader.ReadUInt32(); + int16_t limit = reader.ReadInt16(); + + uint32_t rotValuesAddr = Companion::Instance->PatchVirtualAddr(rawRotValues); + uint32_t rotIndicesAddr = Companion::Instance->PatchVirtualAddr(rawRotIndices); + + auto anim = std::make_shared(); + anim->frameCount = frameCount; + anim->limit = limit; + + // Translate segmented addresses to file offsets + uint32_t rotValuesOffset = Decompressor::TranslateAddr(rotValuesAddr); + uint32_t rotIndicesOffset = Decompressor::TranslateAddr(rotIndicesAddr); + uint32_t animHeaderOffset = Decompressor::TranslateAddr(Companion::Instance->PatchVirtualAddr(GetSafeNode(node, "offset"))); + + // Read rotation values: array of uint16 from rotValues to rotIndices + uint32_t rotValuesCount = (rotIndicesOffset - rotValuesOffset) / 2; + if (rotValuesCount > 0 && rotValuesOffset < rotIndicesOffset) { + YAML::Node rvNode; + rvNode["offset"] = rotValuesAddr; + auto rvRaw = Decompressor::AutoDecode(rvNode, buffer, rotValuesCount * 2); + LUS::BinaryReader rvReader(rvRaw.segment.data, rvRaw.segment.size); + rvReader.SetEndianness(Torch::Endianness::Big); + + for (uint32_t i = 0; i < rotValuesCount; i++) { + anim->rotationValues.push_back(rvReader.ReadUInt16()); + } + } + + // Read rotation indices: array of {x,y,z} uint16 from rotIndices to animHeader + uint32_t rotIndicesCount = (animHeaderOffset - rotIndicesOffset) / 6; + if (rotIndicesCount > 0 && rotIndicesOffset < animHeaderOffset) { + YAML::Node riNode; + riNode["offset"] = rotIndicesAddr; + auto riRaw = Decompressor::AutoDecode(riNode, buffer, rotIndicesCount * 6); + LUS::BinaryReader riReader(riRaw.segment.data, riRaw.segment.size); + riReader.SetEndianness(Torch::Endianness::Big); + + for (uint32_t i = 0; i < rotIndicesCount; i++) { + RotationIndex ri; + ri.x = riReader.ReadUInt16(); + ri.y = riReader.ReadUInt16(); + ri.z = riReader.ReadUInt16(); + anim->rotationIndices.push_back(ri); + } + } + + return anim; +} + +ExportResult OoTAnimationBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto anim = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTAnimation, 0); + + if (anim->isLegacy) { + writer.Write(static_cast(OoTAnimationType::Legacy)); + writer.Finish(write); + return std::nullopt; + } + + writer.Write(static_cast(OoTAnimationType::Normal)); + writer.Write(anim->frameCount); + + writer.Write(static_cast(anim->rotationValues.size())); + for (auto& val : anim->rotationValues) { + writer.Write(val); + } + + writer.Write(static_cast(anim->rotationIndices.size())); + for (auto& ri : anim->rotationIndices) { + writer.Write(ri.x); + writer.Write(ri.y); + writer.Write(ri.z); + } + + writer.Write(anim->limit); + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAnimationFactory.h b/src/factories/oot/OoTAnimationFactory.h new file mode 100644 index 00000000..db9e9593 --- /dev/null +++ b/src/factories/oot/OoTAnimationFactory.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "OoTAnimationTypes.h" +#include +#include + +namespace OoT { + +class OoTNormalAnimationData : public IParsedData { +public: + int16_t frameCount; + std::vector rotationValues; + std::vector rotationIndices; + int16_t limit; + bool isLegacy = false; +}; + +class OoTAnimationBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTAnimationFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTAnimationBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAnimationTypes.h b/src/factories/oot/OoTAnimationTypes.h new file mode 100644 index 00000000..0ba51d73 --- /dev/null +++ b/src/factories/oot/OoTAnimationTypes.h @@ -0,0 +1,30 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include + +namespace OoT { + +enum class OoTAnimationType : uint32_t { + Normal = 0, + Link = 1, + Curve = 2, + Legacy = 3, +}; + +struct RotationIndex { + uint16_t x, y, z; +}; + +struct CurveInterpKnot { + uint16_t unk_00; + int16_t unk_02; + int16_t unk_04; + int16_t unk_06; + float unk_08; +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTArrayFactory.cpp b/src/factories/oot/OoTArrayFactory.cpp new file mode 100644 index 00000000..ee517a6b --- /dev/null +++ b/src/factories/oot/OoTArrayFactory.cpp @@ -0,0 +1,117 @@ +#ifdef OOT_SUPPORT + +#include "OoTArrayFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +static std::shared_ptr parseVtxArray(DataChunk& segment, size_t count) { + LUS::BinaryReader reader(segment.data, count * sizeof(VtxRaw)); + reader.SetEndianness(Torch::Endianness::Big); + std::vector vertices; + + for (size_t i = 0; i < count; i++) { + auto x = reader.ReadInt16(); + auto y = reader.ReadInt16(); + auto z = reader.ReadInt16(); + auto flag = reader.ReadUInt16(); + auto tc1 = reader.ReadInt16(); + auto tc2 = reader.ReadInt16(); + auto cn1 = reader.ReadUByte(); + auto cn2 = reader.ReadUByte(); + auto cn3 = reader.ReadUByte(); + auto cn4 = reader.ReadUByte(); + vertices.push_back(VtxRaw({{x, y, z}, flag, {tc1, tc2}, {cn1, cn2, cn3, cn4}})); + } + + return std::make_shared(vertices); +} + +static std::shared_ptr parseVec3sArray(DataChunk& segment, size_t count) { + LUS::BinaryReader reader(segment.data, count * 6); + reader.SetEndianness(Torch::Endianness::Big); + std::vector vecs; + + for (size_t i = 0; i < count; i++) { + auto x = reader.ReadInt16(); + auto y = reader.ReadInt16(); + auto z = reader.ReadInt16(); + vecs.push_back(Vec3s(x, y, z)); + } + + return std::make_shared(vecs); +} + +std::optional> OoTArrayFactory::parse(std::vector& buffer, YAML::Node& node) { + auto count = GetSafeNode(node, "count"); + auto arrayType = GetSafeNode(node, "array_type"); + + auto [_, segment] = Decompressor::AutoDecode(node, buffer); + + if (arrayType == "VTX") { + return parseVtxArray(segment, count); + } + + if (arrayType == "Vec3s") { + return parseVec3sArray(segment, count); + } + + SPDLOG_ERROR("Unknown OoT Array type '{}'", arrayType); + return std::nullopt; +} + +static void exportVtxArray(LUS::BinaryWriter& writer, std::shared_ptr data) { + writer.Write(static_cast(SohArrayType::Vertex)); + writer.Write(static_cast(data->mVtxs.size())); + + for (const auto& v : data->mVtxs) { + writer.Write(v.ob[0]); + writer.Write(v.ob[1]); + writer.Write(v.ob[2]); + writer.Write(v.flag); + writer.Write(v.tc[0]); + writer.Write(v.tc[1]); + writer.Write(v.cn[0]); + writer.Write(v.cn[1]); + writer.Write(v.cn[2]); + writer.Write(v.cn[3]); + } +} + +static void exportVec3sArray(LUS::BinaryWriter& writer, std::shared_ptr data) { + writer.Write(static_cast(SohArrayType::Vector)); + writer.Write(static_cast(data->mVecs.size())); + + for (const auto& v : data->mVecs) { + // Per-element: scalar_type (u32) + dimensions (u32) + data + writer.Write(static_cast(SohScalarType::ZSCALAR_S16)); + writer.Write(static_cast(3)); + writer.Write(v.x); + writer.Write(v.y); + writer.Write(v.z); + } +} + +ExportResult OoTArrayBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto arrayType = GetSafeNode(node, "array_type"); + + WriteHeader(writer, Torch::ResourceType::Array, 0); + + if (arrayType == "VTX") { + exportVtxArray(writer, std::static_pointer_cast(raw)); + } else if (arrayType == "Vec3s") { + exportVec3sArray(writer, std::static_pointer_cast(raw)); + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTArrayFactory.h b/src/factories/oot/OoTArrayFactory.h new file mode 100644 index 00000000..011bef17 --- /dev/null +++ b/src/factories/oot/OoTArrayFactory.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "factories/VtxFactory.h" +#include +#include +#include + +namespace OoT { + +// Shipwright's ArrayResourceType enum values (must match reference O2R format) +enum class SohArrayType : uint32_t { + Vector = 24, + Vertex = 25, +}; + +// Shipwright's ZScalarType enum values (from ZAPDTR/ZAPD/ZScalar.h) +enum class SohScalarType : uint32_t { + ZSCALAR_NONE = 0, + ZSCALAR_S8 = 1, + ZSCALAR_U8 = 2, + ZSCALAR_X8 = 3, + ZSCALAR_S16 = 4, + ZSCALAR_U16 = 5, +}; + +// Parsed data for OoT Array (Vertex variant) +class OoTVtxArrayData : public IParsedData { +public: + std::vector mVtxs; + explicit OoTVtxArrayData(std::vector vtxs) : mVtxs(std::move(vtxs)) {} +}; + +// Parsed data for OoT Array (Vec3s variant) +class OoTVec3sArrayData : public IParsedData { +public: + std::vector mVecs; + explicit OoTVec3sArrayData(std::vector vecs) : mVecs(std::move(vecs)) {} +}; + +class OoTArrayBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTArrayFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTArrayBinaryExporter) + }; + } + uint32_t GetAlignment() override { return 8; }; +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioFactory.cpp b/src/factories/oot/OoTAudioFactory.cpp new file mode 100644 index 00000000..b9a3f5f7 --- /dev/null +++ b/src/factories/oot/OoTAudioFactory.cpp @@ -0,0 +1,136 @@ +#ifdef OOT_SUPPORT + +#include "OoTAudioFactory.h" +#include "OoTAudioSequenceWriter.h" +#include "OoTAudioSampleWriter.h" +#include "OoTAudioFontWriter.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" +#include + +namespace OoT { + +std::vector OoTAudioFactory::ParseAudioTable(const uint8_t* codeData, uint32_t tableOffset) { + LUS::BinaryReader reader((char*)(codeData + tableOffset), 0x10000); + reader.SetEndianness(Torch::Endianness::Big); + + uint16_t numEntries = reader.ReadUInt16(); + reader.ReadUInt16(); // padding + reader.ReadUInt32(); // romAddr (unused) + reader.ReadUInt32(); // padding + reader.ReadUInt32(); // padding + + std::vector entries; + entries.reserve(numEntries); + for (uint16_t i = 0; i < numEntries; i++) { + AudioTableEntry e; + e.ptr = reader.ReadUInt32(); + e.size = reader.ReadUInt32(); + e.medium = reader.ReadUByte(); + e.cachePolicy = reader.ReadUByte(); + e.data1 = reader.ReadInt16(); + e.data2 = reader.ReadInt16(); + e.data3 = reader.ReadInt16(); + entries.push_back(e); + } + return entries; +} + +std::vector> OoTAudioFactory::ParseSequenceFontTable( + const uint8_t* codeData, uint32_t tableOffset, uint32_t numSequences) { + std::vector> result; + result.reserve(numSequences); + + LUS::BinaryReader reader((char*)(codeData + tableOffset), 0x10000); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector offsets; + for (uint32_t i = 0; i < numSequences; i++) { + offsets.push_back(reader.ReadUInt16()); + } + + for (uint32_t i = 0; i < numSequences; i++) { + LUS::BinaryReader dataReader((char*)(codeData + tableOffset + offsets[i]), 256); + dataReader.SetEndianness(Torch::Endianness::Big); + uint8_t count = dataReader.ReadUByte(); + std::vector fonts; + for (uint8_t j = 0; j < count; j++) { + fonts.push_back(dataReader.ReadUByte()); + } + result.push_back(fonts); + } + return result; +} + +std::vector OoTAudioFactory::BuildMainAudioHeader() { + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTAudio, 2); + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + return std::vector(str.begin(), str.end()); +} + +std::optional OoTAudioFactory::LoadAudioBank(std::vector& buffer) { + auto audiobankSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(1); + if (!audiobankSeg.has_value()) { + SPDLOG_ERROR("OoTAudioFactory: Audiobank segment not found"); + return std::nullopt; + } + uint32_t bankOff = audiobankSeg.value(); + uint32_t bankSize = std::min((uint32_t)0x40000, (uint32_t)(buffer.size() - bankOff)); + return SafeAudioBankReader(std::vector(buffer.begin() + bankOff, buffer.begin() + bankOff + bankSize)); +} + +std::optional> OoTAudioFactory::parse(std::vector& buffer, YAML::Node& node) { + auto data = std::make_shared(); + data->mMainEntry = BuildMainAudioHeader(); + + // Decompress the code segment (segment 128) + auto codeDecoded = Decompressor::AutoDecode( + node["offset"].as(), + 0x200000, // max code segment size + buffer); + + // Parse audio tables from code segment at YAML-specified offsets + auto seqTable = ParseAudioTable(codeDecoded.segment.data, GetSafeNode(node, "sequence_table_offset")); + auto fontTable = ParseAudioTable(codeDecoded.segment.data, GetSafeNode(node, "sound_font_table_offset")); + auto sampleBankTable = ParseAudioTable(codeDecoded.segment.data, GetSafeNode(node, "sample_bank_table_offset")); + auto seqFontMap = ParseSequenceFontTable(codeDecoded.segment.data, GetSafeNode(node, "sequence_font_table_offset"), seqTable.size()); + + SPDLOG_INFO("OoTAudioFactory: {} sequences, {} fonts, {} sample banks", + seqTable.size(), fontTable.size(), sampleBankTable.size()); + + AudioSequenceWriter seqWriter; + if (!seqWriter.Extract(buffer, node, seqTable, seqFontMap)) { + return data; + } + + auto audioBank = LoadAudioBank(buffer); + if (!audioBank.has_value()) { + return data; + } + + std::map sampleMap; + AudioSampleWriter sampleWriter; + if (!sampleWriter.Extract(buffer, node, audioBank.value(), fontTable, sampleBankTable, sampleMap)) { + return data; + } + + AudioFontWriter fontWriter; + fontWriter.Extract(node, audioBank.value(), fontTable, sampleBankTable, sampleMap); + + return data; +} + +ExportResult OoTAudioBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto audio = std::static_pointer_cast(raw); + write.write(audio->mMainEntry.data(), audio->mMainEntry.size()); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioFactory.h b/src/factories/oot/OoTAudioFactory.h new file mode 100644 index 00000000..25c29675 --- /dev/null +++ b/src/factories/oot/OoTAudioFactory.h @@ -0,0 +1,38 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "OoTAudioTypes.h" + +namespace OoT { + +class OoTAudioData : public IParsedData { +public: + std::vector mMainEntry; +}; + +class OoTAudioBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTAudioFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTAudioBinaryExporter) + }; + } + +private: + std::vector BuildMainAudioHeader(); + std::optional LoadAudioBank(std::vector& buffer); + std::vector ParseAudioTable(const uint8_t* codeData, uint32_t tableOffset); + std::vector> ParseSequenceFontTable(const uint8_t* codeData, + uint32_t tableOffset, uint32_t numSequences); +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioFontWriter.cpp b/src/factories/oot/OoTAudioFontWriter.cpp new file mode 100644 index 00000000..403df269 --- /dev/null +++ b/src/factories/oot/OoTAudioFontWriter.cpp @@ -0,0 +1,337 @@ +#ifdef OOT_SUPPORT + +#include "OoTAudioFontWriter.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include + +namespace OoT { + +std::string AudioFontWriter::GetSampleRef(int bankIndex, uint32_t sampleAddr, uint32_t baseOffset, + FontWriteContext& ctx) { + if (sampleAddr + 16 > ctx.audioBank.Size()) return ""; + uint32_t samplePtr = ctx.audioBank.ReadU32(sampleAddr); + if (samplePtr == 0) return ""; + samplePtr += baseOffset; + if (samplePtr + 4 > ctx.audioBank.Size()) return ""; + uint32_t dataRelPtr = ctx.audioBank.ReadU32(samplePtr + 4); + uint32_t absOffset = dataRelPtr + ctx.sampleBankTable[bankIndex].ptr; + if (ctx.sampleMap.count(absOffset)) { + return "audio/samples/" + ctx.sampleMap[absOffset].name + "_META"; + } + return ""; +} + +std::vector> AudioFontWriter::ParseEnvelope(uint32_t envOffset, FontWriteContext& ctx) { + std::vector> envs; + while (envOffset + 4 <= ctx.audioBank.Size()) { + int16_t delay = ctx.audioBank.ReadS16(envOffset); + int16_t arg = ctx.audioBank.ReadS16(envOffset + 2); + envs.push_back({delay, arg}); + envOffset += 4; + if (delay < 0) break; + } + return envs; +} + +void AudioFontWriter::WriteEnvData(LUS::BinaryWriter& w, const std::vector>& envs) { + w.Write(static_cast(envs.size())); + for (auto& [delay, arg] : envs) { + w.Write(delay); + w.Write(arg); + } +} + +void AudioFontWriter::WriteSFE(LUS::BinaryWriter& w, uint32_t sfeOffset, uint32_t baseOffset, + int bankIndex, FontWriteContext& ctx) { + if (sfeOffset + 8 > ctx.audioBank.Size()) { + w.Write(static_cast(0)); + return; + } + uint32_t samplePtr = ctx.audioBank.ReadU32(sfeOffset); + if (samplePtr == 0) { + w.Write(static_cast(0)); + return; + } + w.Write(static_cast(1)); // exists + w.Write(static_cast(1)); // padding (V2 compat) + + samplePtr += baseOffset; + if (samplePtr + 4 <= ctx.audioBank.Size()) { + uint32_t dataRelPtr = ctx.audioBank.ReadU32(samplePtr + 4); + uint32_t absOffset = dataRelPtr + ctx.sampleBankTable[bankIndex].ptr; + if (ctx.sampleMap.count(absOffset)) { + w.Write(std::string("audio/samples/" + ctx.sampleMap[absOffset].name + "_META")); + } else { + w.Write(std::string("")); + } + } else { + w.Write(std::string("")); + } + w.Write(ctx.audioBank.ReadFloat(sfeOffset + 4)); +} + +void FontResidue::Reset() { + mLoaded = mRangeLo = mRangeHi = mRelease = 0; +} + +// Seed from last drum's stack layout (DrumEntry→InstrumentEntry mapping). +void FontResidue::SeedFromDrums(const std::vector& drums) { + if (drums.empty()) return; + auto& lastDrum = drums.back(); + mLoaded = lastDrum.pan; // drum.pan (offset 1) → inst.loaded (offset 1) + mRangeLo = lastDrum.loaded; // drum.loaded (offset 2) → inst.normalRangeLo (offset 2) + mRangeHi = 0; // padding (offset 3) + mRelease = 0; // drum.offset=0 (offset 4) +} + +void FontResidue::ApplyToInstrument(InstEntry& inst) const { + inst.loaded = mLoaded; + inst.normalRangeLo = mRangeLo; + inst.normalRangeHi = mRangeHi; + inst.releaseRate = mRelease; +} + +void FontResidue::UpdateFromInstrument(const InstEntry& inst) { + mLoaded = inst.loaded; + mRangeLo = inst.normalRangeLo; + mRangeHi = inst.normalRangeHi; + mRelease = inst.releaseRate; +} + +std::vector AudioFontWriter::ParseInstruments(int numInstruments, uint32_t ptr, + FontWriteContext& ctx) { + std::vector instruments; + for (int i = 0; i < numInstruments; i++) { + if (ptr + 8 + (i + 1) * 4 > ctx.audioBank.Size()) break; + uint32_t instPtr = ctx.audioBank.ReadU32(ptr + 8 + i * 4); + InstEntry inst = {}; + inst.isValid = (instPtr != 0); + mResidue.ApplyToInstrument(inst); + if (instPtr != 0) { + instPtr += ptr; + if (instPtr + 28 <= ctx.audioBank.Size()) { + inst.loaded = ctx.audioBank.ReadU8(instPtr); + inst.normalRangeLo = ctx.audioBank.ReadU8(instPtr + 1); + inst.normalRangeHi = ctx.audioBank.ReadU8(instPtr + 2); + inst.releaseRate = ctx.audioBank.ReadU8(instPtr + 3); + inst.env = ParseEnvelope(ctx.audioBank.ReadU32(instPtr + 4) + ptr, ctx); + inst.lowAddr = instPtr + 8; + inst.normalAddr = instPtr + 16; + inst.highAddr = instPtr + 24; + mResidue.UpdateFromInstrument(inst); + } + } + instruments.push_back(inst); + } + + return instruments; +} + +std::vector AudioFontWriter::ParseSFX(int numSfx, uint32_t ptr, int sampleBankId, + FontWriteContext& ctx) { + std::vector sfxEntries; + if (ptr + 8 > ctx.audioBank.Size()) return sfxEntries; + + uint32_t sfxListAddr = ctx.audioBank.ReadU32(ptr + 4) + ptr; + for (int i = 0; i < numSfx; i++) { + uint32_t sfeAddr = sfxListAddr + i * 8; + if (sfeAddr + 8 > ctx.audioBank.Size()) break; + uint32_t sp = ctx.audioBank.ReadU32(sfeAddr); + if (sp != 0) { + sp += ptr; + std::string ref; + if (sp + 4 <= ctx.audioBank.Size()) { + uint32_t relPtr = ctx.audioBank.ReadU32(sp + 4); + uint32_t absOff = relPtr + ctx.sampleBankTable[sampleBankId].ptr; + if (ctx.sampleMap.count(absOff)) + ref = "audio/samples/" + ctx.sampleMap[absOff].name + "_META"; + } + sfxEntries.push_back({true, ref, ctx.audioBank.ReadFloat(sfeAddr + 4)}); + } else { + sfxEntries.push_back({false, "", ctx.audioBank.ReadFloat(sfeAddr + 4)}); + } + } + return sfxEntries; +} + +std::vector AudioFontWriter::ParseDrums(int numDrums, uint32_t ptr, int sampleBankId, + FontWriteContext& ctx) { + std::vector drums; + if (ptr + 4 > ctx.audioBank.Size()) return drums; + + uint32_t drumListAddr = ctx.audioBank.ReadU32(ptr) + ptr; + for (int i = 0; i < numDrums; i++) { + if (drumListAddr + (i + 1) * 4 > ctx.audioBank.Size()) break; + uint32_t drumPtr = ctx.audioBank.ReadU32(drumListAddr + i * 4); + if (drumPtr != 0) { + drumPtr += ptr; + if (drumPtr + 16 <= ctx.audioBank.Size()) { + DrumEntry d; + d.releaseRate = ctx.audioBank.ReadU8(drumPtr); + d.pan = ctx.audioBank.ReadU8(drumPtr + 1); + d.loaded = ctx.audioBank.ReadU8(drumPtr + 2); + d.tuning = ctx.audioBank.ReadFloat(drumPtr + 8); + d.env = ParseEnvelope(ctx.audioBank.ReadU32(drumPtr + 12) + ptr, ctx); + + uint32_t sampleEntryPtr = ctx.audioBank.ReadU32(drumPtr + 4) + ptr; + if (sampleEntryPtr + 4 <= ctx.audioBank.Size()) { + uint32_t dataRelPtr = ctx.audioBank.ReadU32(sampleEntryPtr + 4); + uint32_t absOffset = dataRelPtr + ctx.sampleBankTable[sampleBankId].ptr; + if (ctx.sampleMap.count(absOffset)) { + d.sampleRef = "audio/samples/" + ctx.sampleMap[absOffset].name + "_META"; + } + } + drums.push_back(d); + continue; + } + } + drums.push_back({0, 0, 0, 0.0f, {}, ""}); + } + mResidue.SeedFromDrums(drums); + return drums; +} + +void AudioFontWriter::WriteDrums(LUS::BinaryWriter& w, const std::vector& drums) { + for (auto& d : drums) { + w.Write(d.releaseRate); + w.Write(d.pan); + w.Write(d.loaded); + WriteEnvData(w, d.env); + w.Write(static_cast(d.sampleRef.empty() ? 0 : 1)); + w.Write(d.sampleRef); + w.Write(d.tuning); + } +} + +void AudioFontWriter::WriteInstruments(LUS::BinaryWriter& w, const std::vector& instruments, + uint32_t ptr, int sampleBankId, FontWriteContext& ctx) { + for (auto& inst : instruments) { + w.Write(static_cast(inst.isValid ? 1 : 0)); + w.Write(inst.loaded); + w.Write(inst.normalRangeLo); + w.Write(inst.normalRangeHi); + w.Write(inst.releaseRate); + WriteEnvData(w, inst.env); + + if (inst.isValid) { + if (ctx.audioBank.ReadU32(inst.lowAddr) != 0) { + WriteSFE(w, inst.lowAddr, ptr, sampleBankId, ctx); + } else { + w.Write(static_cast(0)); + } + if (ctx.audioBank.ReadU32(inst.normalAddr) != 0) { + WriteSFE(w, inst.normalAddr, ptr, sampleBankId, ctx); + } else { + w.Write(static_cast(0)); + } + if (ctx.audioBank.ReadU32(inst.highAddr) != 0 && inst.normalRangeHi != 0x7F) { + WriteSFE(w, inst.highAddr, ptr, sampleBankId, ctx); + } else { + w.Write(static_cast(0)); + } + } else { + w.Write(static_cast(0)); + w.Write(static_cast(0)); + w.Write(static_cast(0)); + } + } +} + +void AudioFontWriter::WriteSFXEntries(LUS::BinaryWriter& w, const std::vector& sfxEntries) { + for (auto& sfx : sfxEntries) { + if (sfx.exists) { + w.Write(static_cast(1)); + w.Write(static_cast(1)); + w.Write(sfx.sampleRef); + w.Write(sfx.tuning); + } else { + w.Write(static_cast(0)); + } + } +} + +void AudioFontWriter::WriteFontCompanion(uint32_t fontIndex, const AudioTableEntry& fontEntry, + const std::vector& drums, + const std::vector& instruments, + const std::vector& sfxEntries, + uint32_t ptr, int sampleBankId, + FontWriteContext& ctx, + const std::map& fontNames) { + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTAudioSoundFont, 2); + + // Font metadata + w.Write(static_cast(fontIndex)); + w.Write(fontEntry.medium); + w.Write(fontEntry.cachePolicy); + w.Write(fontEntry.data1); + w.Write(fontEntry.data2); + w.Write(fontEntry.data3); + + w.Write(static_cast(drums.size())); + w.Write(static_cast(instruments.size())); + w.Write(static_cast(sfxEntries.size())); + + WriteDrums(w, drums); + WriteInstruments(w, instruments, ptr, sampleBankId, ctx); + WriteSFXEntries(w, sfxEntries); + + std::string fontName; + if (fontNames.count(fontIndex)) { + fontName = fontNames.at(fontIndex); + } else { + fontName = std::to_string(fontIndex) + "_Font"; + } + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + Companion::Instance->RegisterCompanionFile( + "fonts/" + fontName, std::vector(str.begin(), str.end())); +} + +void AudioFontWriter::Extract(YAML::Node& node, + SafeAudioBankReader& audioBank, + const std::vector& fontTable, + const std::vector& sampleBankTable, + std::map& sampleMap) { + // Build font name map from YAML + std::map fontNames; + if (node["fonts"] && node["fonts"].IsSequence()) { + auto fontsNode = node["fonts"]; + for (size_t i = 0; i < fontsNode.size(); i++) { + auto fn = fontsNode[i]; + fontNames[fn["index"].as()] = fn["name"].as(); + } + } + + FontWriteContext ctx { + .audioBank = audioBank, + .sampleBankTable = sampleBankTable, + .sampleMap = sampleMap, + }; + + mResidue.Reset(); + + for (uint32_t fi = 0; fi < fontTable.size(); fi++) { + auto& fe = fontTable[fi]; + uint32_t ptr = fe.ptr; + int sampleBankId = (fe.data1 >> 8) & 0xFF; + int numInstruments = (fe.data2 >> 8) & 0xFF; + int numDrums = fe.data2 & 0xFF; + int numSfx = fe.data3; + + auto drums = ParseDrums(numDrums, ptr, sampleBankId, ctx); + auto sfxEntries = ParseSFX(numSfx, ptr, sampleBankId, ctx); + auto instruments = ParseInstruments(numInstruments, ptr, ctx); + + WriteFontCompanion(fi, fe, drums, instruments, sfxEntries, ptr, sampleBankId, ctx, fontNames); + } + + SPDLOG_INFO("OoTAudioFactory: wrote {} font companion files", fontTable.size()); +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioFontWriter.h b/src/factories/oot/OoTAudioFontWriter.h new file mode 100644 index 00000000..3af6e1b5 --- /dev/null +++ b/src/factories/oot/OoTAudioFontWriter.h @@ -0,0 +1,82 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "OoTAudioTypes.h" + +namespace OoT { + +struct DrumEntry { + uint8_t releaseRate, pan, loaded; + float tuning; + std::vector> env; + std::string sampleRef; +}; + +struct InstEntry { + bool isValid; + uint8_t loaded, normalRangeLo, normalRangeHi, releaseRate; + std::vector> env; + uint32_t lowAddr, normalAddr, highAddr; +}; + +// Cross-font stack residue. ZAPDTR reuses the same stack frame across fonts, +// so invalid instruments inherit field values from the previous font's last +// valid instrument. See docs/oot-audio-font-residue-analysis.md +class FontResidue { +public: + void Reset(); + void SeedFromDrums(const std::vector& drums); + void ApplyToInstrument(InstEntry& inst) const; + void UpdateFromInstrument(const InstEntry& inst); +private: + uint8_t mLoaded = 0, mRangeLo = 0, mRangeHi = 0, mRelease = 0; +}; + +struct SFXEntry { + bool exists; + std::string sampleRef; + float tuning; +}; + +struct FontWriteContext { + SafeAudioBankReader& audioBank; + const std::vector& sampleBankTable; + std::map& sampleMap; +}; + +class AudioFontWriter { +public: + void Extract(YAML::Node& node, + SafeAudioBankReader& audioBank, + const std::vector& fontTable, + const std::vector& sampleBankTable, + std::map& sampleMap); + +private: + std::string GetSampleRef(int bankIndex, uint32_t sampleAddr, uint32_t baseOffset, FontWriteContext& ctx); + std::vector> ParseEnvelope(uint32_t envOffset, FontWriteContext& ctx); + void WriteSFE(LUS::BinaryWriter& w, uint32_t sfeOffset, uint32_t baseOffset, + int bankIndex, FontWriteContext& ctx); + void WriteFontCompanion(uint32_t fontIndex, const AudioTableEntry& fontEntry, + const std::vector& drums, + const std::vector& instruments, + const std::vector& sfxEntries, + uint32_t ptr, int sampleBankId, + FontWriteContext& ctx, + const std::map& fontNames); + void WriteDrums(LUS::BinaryWriter& w, const std::vector& drums); + void WriteInstruments(LUS::BinaryWriter& w, const std::vector& instruments, + uint32_t ptr, int sampleBankId, FontWriteContext& ctx); + void WriteSFXEntries(LUS::BinaryWriter& w, const std::vector& sfxEntries); + void WriteEnvData(LUS::BinaryWriter& w, const std::vector>& envs); + std::vector ParseInstruments(int numInstruments, uint32_t ptr, FontWriteContext& ctx); + std::vector ParseSFX(int numSfx, uint32_t ptr, int sampleBankId, FontWriteContext& ctx); + std::vector ParseDrums(int numDrums, uint32_t ptr, int sampleBankId, FontWriteContext& ctx); + + FontResidue mResidue; +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioSampleWriter.cpp b/src/factories/oot/OoTAudioSampleWriter.cpp new file mode 100644 index 00000000..1aa0479c --- /dev/null +++ b/src/factories/oot/OoTAudioSampleWriter.cpp @@ -0,0 +1,207 @@ +#ifdef OOT_SUPPORT + +#include "OoTAudioSampleWriter.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include +#include + +namespace OoT { + +std::map> AudioSampleWriter::ParseSampleNames(YAML::Node& node) { + std::map> sampleNames; + if (node["samples"] && node["samples"].IsSequence()) { + auto samplesNode = node["samples"]; + for (size_t i = 0; i < samplesNode.size(); i++) { + auto bankNode = samplesNode[i]; + int bank = bankNode["bank"].as(); + if (bankNode["entries"] && bankNode["entries"].IsSequence()) { + auto entries = bankNode["entries"]; + for (size_t j = 0; j < entries.size(); j++) { + auto e = entries[j]; + sampleNames[bank][e["offset"].as()] = e["name"].as(); + } + } + } + } + return sampleNames; +} + +void AudioSampleWriter::ParseSample(int bankIndex, uint32_t sampleAddr, uint32_t baseOffset, AudioParseContext& ctx) { + if (sampleAddr + 16 > ctx.audioBank.Size()) return; + + uint32_t dataRelPtr = ctx.audioBank.ReadU32(sampleAddr + 4); + uint32_t sampleDataOffset = dataRelPtr + ctx.sampleBankTable[bankIndex].ptr; + if (ctx.sampleMap.count(sampleDataOffset)) return; + + SampleInfo s; + uint32_t origField = ctx.audioBank.ReadU32(sampleAddr); + s.codec = (origField >> 28) & 0x0F; + s.medium = (origField >> 24) & 0x03; + s.unk_bit26 = (origField >> 22) & 0x01; + s.unk_bit25 = (origField >> 21) & 0x01; + s.dataSize = origField & 0x00FFFFFF; + s.dataOffset = sampleDataOffset; + + uint32_t loopAddr = ctx.audioBank.ReadU32(sampleAddr + 8) + baseOffset; + uint32_t bookAddr = ctx.audioBank.ReadU32(sampleAddr + 12) + baseOffset; + + if (loopAddr + 12 <= ctx.audioBank.Size()) { + s.loopStart = (int32_t)ctx.audioBank.ReadU32(loopAddr); + s.loopEnd = (int32_t)ctx.audioBank.ReadU32(loopAddr + 4); + s.loopCount = (int32_t)ctx.audioBank.ReadU32(loopAddr + 8); + + if (s.loopCount != 0 && loopAddr + 48 <= ctx.audioBank.Size()) { + for (int i = 0; i < 16; i++) { + s.loopStates.push_back(ctx.audioBank.ReadS16(loopAddr + 16 + i * 2)); + } + } + } + + if (bookAddr + 8 <= ctx.audioBank.Size()) { + s.bookOrder = (int32_t)ctx.audioBank.ReadU32(bookAddr); + s.bookNpredictors = (int32_t)ctx.audioBank.ReadU32(bookAddr + 4); + int numBooks = s.bookOrder * s.bookNpredictors * 8; + if (bookAddr + 8 + numBooks * 2 <= ctx.audioBank.Size()) { + for (int i = 0; i < numBooks; i++) { + s.books.push_back(ctx.audioBank.ReadS16(bookAddr + 8 + i * 2)); + } + } + } + + // Resolve name from YAML (use absolute offset as key, matching ZAPDTR behavior) + if (ctx.sampleNames.count(bankIndex) && ctx.sampleNames[bankIndex].count((int)sampleDataOffset)) { + s.name = ctx.sampleNames[bankIndex][(int)sampleDataOffset]; + } else { + std::ostringstream ss; + ss << "sample_" << bankIndex << "_" << std::setfill('0') << std::setw(8) + << std::hex << std::uppercase << sampleDataOffset; + s.name = ss.str(); + } + + ctx.sampleMap[sampleDataOffset] = s; +} + +void AudioSampleWriter::ParseSFESample(int bankIndex, uint32_t sfeAddr, uint32_t baseOffset, AudioParseContext& ctx) { + if (sfeAddr + 4 > ctx.audioBank.Size()) return; + uint32_t samplePtr = ctx.audioBank.ReadU32(sfeAddr); + if (samplePtr != 0) { + ParseSample(bankIndex, samplePtr + baseOffset, baseOffset, ctx); + } +} + +void AudioSampleWriter::WriteCompanionFiles(const std::map& sampleMap, + const std::vector& audioTable) { + for (auto& [offset, s] : sampleMap) { + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTAudioSample, 2); + + w.Write(s.codec); + w.Write(s.medium); + w.Write(s.unk_bit26); + w.Write(s.unk_bit25); + w.Write(static_cast(s.dataSize)); + if (s.dataOffset + s.dataSize <= audioTable.size()) { + w.Write((char*)(audioTable.data() + s.dataOffset), s.dataSize); + } + + w.Write(static_cast(s.loopStart)); + w.Write(static_cast(s.loopEnd)); + w.Write(static_cast(s.loopCount)); + w.Write(static_cast(s.loopStates.size())); + for (auto ls : s.loopStates) w.Write(ls); + + w.Write(static_cast(s.bookOrder)); + w.Write(static_cast(s.bookNpredictors)); + w.Write(static_cast(s.books.size())); + for (auto b : s.books) w.Write(b); + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + Companion::Instance->RegisterCompanionFile( + "samples/" + s.name + "_META", std::vector(str.begin(), str.end())); + } + + SPDLOG_INFO("OoTAudioFactory: wrote {} sample companion files", sampleMap.size()); +} + +void AudioSampleWriter::DiscoverSamples(const std::vector& fontTable, AudioParseContext& ctx) { + for (uint32_t fi = 0; fi < fontTable.size(); fi++) { + auto& fe = fontTable[fi]; + uint32_t ptr = fe.ptr; + int sampleBankId = (fe.data1 >> 8) & 0xFF; + int numInstruments = (fe.data2 >> 8) & 0xFF; + int numDrums = fe.data2 & 0xFF; + int numSfx = fe.data3; + + if (ptr + 8 > ctx.audioBank.Size()) continue; + + // Drums + uint32_t drumListAddr = ctx.audioBank.ReadU32(ptr) + ptr; + for (int i = 0; i < numDrums; i++) { + if (drumListAddr + (i + 1) * 4 > ctx.audioBank.Size()) break; + uint32_t drumPtr = ctx.audioBank.ReadU32(drumListAddr + i * 4); + if (drumPtr != 0) { + drumPtr += ptr; + if (drumPtr + 8 > ctx.audioBank.Size()) continue; + uint32_t sampleEntryPtr = ctx.audioBank.ReadU32(drumPtr + 4) + ptr; + ParseSample(sampleBankId, sampleEntryPtr, ptr, ctx); + } + } + + // SFX + uint32_t sfxListAddr = ctx.audioBank.ReadU32(ptr + 4) + ptr; + for (int i = 0; i < numSfx; i++) { + if (sfxListAddr + (i + 1) * 8 > ctx.audioBank.Size()) break; + ParseSFESample(sampleBankId, sfxListAddr + i * 8, ptr, ctx); + } + + // Instruments + for (int i = 0; i < numInstruments; i++) { + if (ptr + 8 + (i + 1) * 4 > ctx.audioBank.Size()) break; + uint32_t instPtr = ctx.audioBank.ReadU32(ptr + 8 + i * 4); + if (instPtr != 0) { + instPtr += ptr; + if (instPtr + 28 > ctx.audioBank.Size()) continue; + ParseSFESample(sampleBankId, instPtr + 8, ptr, ctx); + ParseSFESample(sampleBankId, instPtr + 16, ptr, ctx); + ParseSFESample(sampleBankId, instPtr + 24, ptr, ctx); + } + } + } +} + +bool AudioSampleWriter::Extract(std::vector& buffer, YAML::Node& node, + SafeAudioBankReader& audioBank, + const std::vector& fontTable, + const std::vector& sampleBankTable, + std::map& sampleMap) { + // Load Audiotable segment (only needed for sample data extraction) + auto audiotableSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(3); + if (!audiotableSeg.has_value()) { + SPDLOG_ERROR("OoTAudioFactory: Audiotable segment not found"); + return false; + } + uint32_t tableOff = audiotableSeg.value(); + uint32_t tableSize = std::min((uint32_t)0x500000, (uint32_t)(buffer.size() - tableOff)); + std::vector audioTable(buffer.begin() + tableOff, buffer.begin() + tableOff + tableSize); + + AudioParseContext ctx { + .audioBank = audioBank, + .sampleBankTable = sampleBankTable, + .sampleNames = ParseSampleNames(node), + .sampleMap = sampleMap, + }; + + DiscoverSamples(fontTable, ctx); + + SPDLOG_INFO("OoTAudioFactory: discovered {} unique samples", sampleMap.size()); + + WriteCompanionFiles(sampleMap, audioTable); + return true; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioSampleWriter.h b/src/factories/oot/OoTAudioSampleWriter.h new file mode 100644 index 00000000..0de49a0c --- /dev/null +++ b/src/factories/oot/OoTAudioSampleWriter.h @@ -0,0 +1,28 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "OoTAudioTypes.h" + +namespace OoT { + +class AudioSampleWriter { +public: + bool Extract(std::vector& buffer, YAML::Node& node, + SafeAudioBankReader& audioBank, + const std::vector& fontTable, + const std::vector& sampleBankTable, + std::map& sampleMap); + +private: + void DiscoverSamples(const std::vector& fontTable, AudioParseContext& ctx); + void WriteCompanionFiles(const std::map& sampleMap, + const std::vector& audioTable); + std::map> ParseSampleNames(YAML::Node& node); + void ParseSample(int bankIndex, uint32_t sampleAddr, uint32_t baseOffset, AudioParseContext& ctx); + void ParseSFESample(int bankIndex, uint32_t sfeAddr, uint32_t baseOffset, AudioParseContext& ctx); +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioSequenceWriter.cpp b/src/factories/oot/OoTAudioSequenceWriter.cpp new file mode 100644 index 00000000..74952ef9 --- /dev/null +++ b/src/factories/oot/OoTAudioSequenceWriter.cpp @@ -0,0 +1,97 @@ +#ifdef OOT_SUPPORT + +#include "OoTAudioSequenceWriter.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include +#include + +namespace OoT { + +void AudioSequenceWriter::WriteCompanion(const uint8_t* seqData, uint32_t seqSize, + uint32_t originalIndex, uint8_t medium, uint8_t cachePolicy, + const std::vector& fonts, const std::string& seqName) { + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTAudioSequence, 2); + + w.Write(seqSize); + w.Write((char*)seqData, seqSize); + + w.Write(static_cast(originalIndex)); + w.Write(medium); + w.Write(cachePolicy); + + w.Write(static_cast(fonts.size())); + for (auto f : fonts) { + w.Write(f); + } + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + Companion::Instance->RegisterCompanionFile( + "sequences/" + seqName, std::vector(str.begin(), str.end())); +} + +bool AudioSequenceWriter::Extract(std::vector& buffer, YAML::Node& node, + const std::vector& seqTable, + const std::vector>& seqFontMap) { + // Get Audioseq ROM data (segment 2, uncompressed) + auto audioseqSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(2); + if (!audioseqSeg.has_value()) { + SPDLOG_ERROR("OoTAudioFactory: Audioseq segment not found"); + return false; + } + uint32_t audioseqOff = audioseqSeg.value(); + + // Get sequence names from YAML + std::vector seqNames; + if (node["sequences"] && node["sequences"].IsSequence()) { + auto seqNode = node["sequences"]; + for (size_t i = 0; i < seqNode.size(); i++) { + seqNames.push_back(seqNode[i].as()); + } + } + + for (uint32_t i = 0; i < seqTable.size(); i++) { + auto medium = seqTable[i].medium; + auto cachePolicy = seqTable[i].cachePolicy; + auto& fonts = seqFontMap[i]; + + // Resolve alias: size==0 means ptr holds the index of the real sequence + bool isAlias = (seqTable[i].size == 0); + uint32_t dataIdx = isAlias ? seqTable[i].ptr : i; + if (dataIdx >= seqTable.size()) { + SPDLOG_WARN("OoTAudioFactory: sequence {} alias index {} out of range", i, dataIdx); + continue; + } + auto& dataEntry = seqTable[dataIdx]; + + // Sequence name + std::string seqName; + if (i < seqNames.size()) { + seqName = seqNames[i]; + } else { + std::ostringstream ss; + ss << std::setfill('0') << std::setw(3) << i << "_Sequence"; + seqName = ss.str(); + } + + // Resolve sequence data location in ROM (using dataEntry for aliased sequences) + uint32_t seqDataOff = audioseqOff + dataEntry.ptr; + if (seqDataOff + dataEntry.size > buffer.size()) { + SPDLOG_WARN("OoTAudioFactory: sequence {} out of bounds", seqName); + continue; + } + + WriteCompanion(buffer.data() + seqDataOff, dataEntry.size, + i, medium, cachePolicy, fonts, seqName); + } + + SPDLOG_INFO("OoTAudioFactory: wrote {} sequence companion files", seqTable.size()); + return true; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioSequenceWriter.h b/src/factories/oot/OoTAudioSequenceWriter.h new file mode 100644 index 00000000..f4ccc775 --- /dev/null +++ b/src/factories/oot/OoTAudioSequenceWriter.h @@ -0,0 +1,23 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "OoTAudioTypes.h" + +namespace OoT { + +class AudioSequenceWriter { +public: + bool Extract(std::vector& buffer, YAML::Node& node, + const std::vector& seqTable, + const std::vector>& seqFontMap); + +private: + void WriteCompanion(const uint8_t* seqData, uint32_t seqSize, + uint32_t originalIndex, uint8_t medium, uint8_t cachePolicy, + const std::vector& fonts, const std::string& seqName); +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioTypes.cpp b/src/factories/oot/OoTAudioTypes.cpp new file mode 100644 index 00000000..16ddcb85 --- /dev/null +++ b/src/factories/oot/OoTAudioTypes.cpp @@ -0,0 +1,38 @@ +#ifdef OOT_SUPPORT + +#include "OoTAudioTypes.h" + +namespace OoT { + +SafeAudioBankReader::SafeAudioBankReader(std::vector data) + : mData(std::move(data)), mReader((char*)mData.data(), mData.size()) { + mReader.SetEndianness(Torch::Endianness::Big); +} + +uint8_t SafeAudioBankReader::ReadU8(uint32_t offset) { + if (offset >= mData.size()) return 0; + mReader.Seek(offset, LUS::SeekOffsetType::Start); + return mReader.ReadUByte(); +} + +uint32_t SafeAudioBankReader::ReadU32(uint32_t offset) { + if (offset + 4 > mData.size()) return 0; + mReader.Seek(offset, LUS::SeekOffsetType::Start); + return mReader.ReadUInt32(); +} + +int16_t SafeAudioBankReader::ReadS16(uint32_t offset) { + if (offset + 2 > mData.size()) return 0; + mReader.Seek(offset, LUS::SeekOffsetType::Start); + return mReader.ReadInt16(); +} + +float SafeAudioBankReader::ReadFloat(uint32_t offset) { + if (offset + 4 > mData.size()) return 0.0f; + mReader.Seek(offset, LUS::SeekOffsetType::Start); + return mReader.ReadFloat(); +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTAudioTypes.h b/src/factories/oot/OoTAudioTypes.h new file mode 100644 index 00000000..64a82595 --- /dev/null +++ b/src/factories/oot/OoTAudioTypes.h @@ -0,0 +1,56 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include + +namespace OoT { + +// Safe big-endian random-access reader over audio bank data. +// Bounds-checks each read and returns 0 on out-of-range (avoids raw pointer arithmetic). +class SafeAudioBankReader { +public: + SafeAudioBankReader(std::vector data); + uint8_t ReadU8(uint32_t offset); + uint32_t ReadU32(uint32_t offset); + int16_t ReadS16(uint32_t offset); + float ReadFloat(uint32_t offset); + size_t Size() const { return mData.size(); } + +private: + std::vector mData; + LUS::BinaryReader mReader; +}; + +// Audio table entry (16 bytes each in ROM) +struct AudioTableEntry { + uint32_t ptr; + uint32_t size; + uint8_t medium; + uint8_t cachePolicy; + int16_t data1; + int16_t data2; + int16_t data3; +}; + +struct SampleInfo { + uint8_t codec, medium, unk_bit26, unk_bit25; + uint32_t dataSize, dataOffset; + int32_t loopStart, loopEnd, loopCount; + std::vector loopStates; + int32_t bookOrder, bookNpredictors; + std::vector books; + std::string name; +}; + +struct AudioParseContext { + SafeAudioBankReader& audioBank; + const std::vector& sampleBankTable; + std::map> sampleNames; + std::map& sampleMap; +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTCollisionFactory.cpp b/src/factories/oot/OoTCollisionFactory.cpp new file mode 100644 index 00000000..6f3efd8e --- /dev/null +++ b/src/factories/oot/OoTCollisionFactory.cpp @@ -0,0 +1,335 @@ +#ifdef OOT_SUPPORT + +#include "OoTCollisionFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +static void parseCameraData(std::vector& buffer, YAML::Node& node, + uint32_t camDataAddr, uint32_t polyTypeDefAddr, + uint32_t polyAddr, uint32_t vtxAddr, uint32_t waterBoxAddr, + OoTCollisionData& col) { + uint32_t camDataSegOff = SEGMENT_OFFSET(camDataAddr); + uint8_t sceneSeg = SEGMENT_NUMBER(camDataAddr); + + // Determine upper boundary for camera data entries (in segment offsets) + uint32_t upperBoundary = 0; + if (polyTypeDefAddr != 0) { + upperBoundary = SEGMENT_OFFSET(polyTypeDefAddr); + } else if (polyAddr != 0) { + upperBoundary = SEGMENT_OFFSET(polyAddr); + } else if (vtxAddr != 0) { + upperBoundary = SEGMENT_OFFSET(vtxAddr); + } else if (waterBoxAddr != 0) { + upperBoundary = SEGMENT_OFFSET(waterBoxAddr); + } else { + upperBoundary = SEGMENT_OFFSET(GetSafeNode(node, "offset")); + } + + // Initial Sharp Ocarina check: cam data entries come before the boundary + // in standard layout. If boundary < camDataSegOff, layout is reversed. + bool isSharpOcarina = false; + if (upperBoundary < camDataSegOff) { + uint32_t scanSize = 0x2000; + YAML::Node scanNode; + scanNode["offset"] = camDataAddr; + auto scanRaw = Decompressor::AutoDecode(scanNode, buffer, scanSize); + + uint32_t offset = 0; + while (offset + 8 <= scanSize && + scanRaw.segment.data[offset] == 0x00 && + scanRaw.segment.data[offset + 4] == 0x02) { + offset += 8; + } + upperBoundary = camDataSegOff + offset; + isSharpOcarina = true; + } + + uint32_t numEntries = (upperBoundary - camDataSegOff) / 8; + if (numEntries == 0 || numEntries >= 10000) { + return; + } + + YAML::Node cdNode; + cdNode["offset"] = camDataAddr; + auto cdRaw = Decompressor::AutoDecode(cdNode, buffer, numEntries * 8); + LUS::BinaryReader cdReader(cdRaw.segment.data, cdRaw.segment.size); + cdReader.SetEndianness(Torch::Endianness::Big); + + // Match OTRExporter's per-entry Sharp Ocarina detection: + // For each entry, if the position pointer's segment offset >= camDataSegOff, + // it's Sharp Ocarina layout. This also triggers when cameraPosDataSeg == 0 + // and camDataSegOff == 0, since 0 >= 0 (the case for object collisions + // where cam data is at the start of the file). + uint32_t lowestCamPosOffset = camDataSegOff; + uint32_t highestCamPosEnd = camDataSegOff; + + for (uint32_t i = 0; i < numEntries; i++) { + CamDataEntry entry; + entry.cameraSType = cdReader.ReadUInt16(); + entry.numData = cdReader.ReadInt16(); + uint32_t cameraPosDataSeg = cdReader.ReadUInt32(); + entry.cameraPosIndex = 0; + + uint32_t posSegOffset = SEGMENT_OFFSET(cameraPosDataSeg); + + if (camDataSegOff > posSegOffset) { + // Standard layout: positions are before cam data entries + if (cameraPosDataSeg != 0 && posSegOffset < lowestCamPosOffset) { + lowestCamPosOffset = posSegOffset; + } + } else { + // Sharp Ocarina layout: positions are after cam data entries + isSharpOcarina = true; + if (highestCamPosEnd < posSegOffset) { + highestCamPosEnd = posSegOffset; + } + } + + col.camDataEntries.push_back(entry); + } + + // Calculate camera position data count and offset (in segment offsets) + uint32_t camPosDataSegOff; + uint32_t numPosData; + if (!isSharpOcarina) { + camPosDataSegOff = lowestCamPosOffset; + numPosData = (camDataSegOff - camPosDataSegOff) / 6; + } else { + camPosDataSegOff = camDataSegOff + numEntries * 8; + numPosData = (highestCamPosEnd - camPosDataSegOff + 18) / 6; + } + + // Read camera position data + if (numPosData > 0 && numPosData < 100000) { + uint32_t camPosSeg = (sceneSeg << 24) | camPosDataSegOff; + YAML::Node cpNode; + cpNode["offset"] = camPosSeg; + auto cpRaw = Decompressor::AutoDecode(cpNode, buffer, numPosData * 6); + LUS::BinaryReader cpReader(cpRaw.segment.data, cpRaw.segment.size); + cpReader.SetEndianness(Torch::Endianness::Big); + + for (uint32_t i = 0; i < numPosData; i++) { + CamPosData pos; + pos.x = cpReader.ReadInt16(); + pos.y = cpReader.ReadInt16(); + pos.z = cpReader.ReadInt16(); + col.camPositions.push_back(pos); + } + } + + // Re-read entries to set camera position indices + cdReader.Seek(0, LUS::SeekOffsetType::Start); + for (uint32_t i = 0; i < numEntries; i++) { + cdReader.ReadUInt16(); // skip cameraSType + cdReader.ReadInt16(); // skip numData + uint32_t cameraPosDataSeg = cdReader.ReadUInt32(); + + if (cameraPosDataSeg != 0) { + uint32_t posSegOffset = SEGMENT_OFFSET(cameraPosDataSeg); + col.camDataEntries[i].cameraPosIndex = (posSegOffset - camPosDataSegOff) / 6; + } + } +} + +std::optional> OoTCollisionFactory::parse(std::vector& buffer, YAML::Node& node) { + // CollisionHeader: 44 bytes (0x2C) + auto [_, segment] = Decompressor::AutoDecode(node, buffer, 0x2C); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + auto col = std::make_shared(); + + // Bounding box + col->absMinX = reader.ReadInt16(); + col->absMinY = reader.ReadInt16(); + col->absMinZ = reader.ReadInt16(); + col->absMaxX = reader.ReadInt16(); + col->absMaxY = reader.ReadInt16(); + col->absMaxZ = reader.ReadInt16(); + + uint16_t numVerts = reader.ReadUInt16(); + reader.ReadInt16(); // padding + uint32_t vtxAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + + uint16_t numPolygons = reader.ReadUInt16(); + reader.ReadInt16(); // padding + uint32_t polyAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + uint32_t polyTypeDefAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + uint32_t camDataAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + + uint16_t numWaterBoxes = reader.ReadUInt16(); + reader.ReadInt16(); // padding + uint32_t waterBoxAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + + // Read vertices + if (numVerts > 0 && vtxAddr != 0) { + YAML::Node vNode; + vNode["offset"] = vtxAddr; + auto vRaw = Decompressor::AutoDecode(vNode, buffer, numVerts * 6); + LUS::BinaryReader vReader(vRaw.segment.data, vRaw.segment.size); + vReader.SetEndianness(Torch::Endianness::Big); + + for (uint16_t i = 0; i < numVerts; i++) { + CollisionVertex v; + v.x = vReader.ReadInt16(); + v.y = vReader.ReadInt16(); + v.z = vReader.ReadInt16(); + col->vertices.push_back(v); + } + } + + // Read polygons + if (numPolygons > 0 && polyAddr != 0) { + YAML::Node pNode; + pNode["offset"] = polyAddr; + auto pRaw = Decompressor::AutoDecode(pNode, buffer, numPolygons * 16); + LUS::BinaryReader pReader(pRaw.segment.data, pRaw.segment.size); + pReader.SetEndianness(Torch::Endianness::Big); + + for (uint16_t i = 0; i < numPolygons; i++) { + CollisionPoly p; + p.type = pReader.ReadUInt16(); + p.vtxA = pReader.ReadUInt16(); + p.vtxB = pReader.ReadUInt16(); + p.vtxC = pReader.ReadUInt16(); + p.normX = pReader.ReadUInt16(); + p.normY = pReader.ReadUInt16(); + p.normZ = pReader.ReadUInt16(); + p.dist = pReader.ReadUInt16(); + col->polygons.push_back(p); + } + } + + // Read surface types: count = highest polygon type + 1 + if (polyTypeDefAddr != 0 && !col->polygons.empty()) { + uint16_t highestType = 0; + for (const auto& p : col->polygons) { + if (p.type > highestType) highestType = p.type; + } + uint32_t numSurfaceTypes = highestType + 1; + + YAML::Node stNode; + stNode["offset"] = polyTypeDefAddr; + auto stRaw = Decompressor::AutoDecode(stNode, buffer, numSurfaceTypes * 8); + LUS::BinaryReader stReader(stRaw.segment.data, stRaw.segment.size); + stReader.SetEndianness(Torch::Endianness::Big); + + for (uint32_t i = 0; i < numSurfaceTypes; i++) { + SurfaceType st; + st.data0 = stReader.ReadUInt32(); + st.data1 = stReader.ReadUInt32(); + col->surfaceTypes.push_back(st); + } + } + + // Read camera data + // Uses segment offsets throughout to match OTRExporter's logic. + if (camDataAddr != 0) { + parseCameraData(buffer, node, camDataAddr, polyTypeDefAddr, polyAddr, vtxAddr, waterBoxAddr, *col); + } + + // Read water boxes + if (numWaterBoxes > 0 && waterBoxAddr != 0) { + YAML::Node wbNode; + wbNode["offset"] = waterBoxAddr; + auto wbRaw = Decompressor::AutoDecode(wbNode, buffer, numWaterBoxes * 16); + LUS::BinaryReader wbReader(wbRaw.segment.data, wbRaw.segment.size); + wbReader.SetEndianness(Torch::Endianness::Big); + + for (uint16_t i = 0; i < numWaterBoxes; i++) { + WaterBox wb; + wb.xMin = wbReader.ReadInt16(); + wb.ySurface = wbReader.ReadInt16(); + wb.zMin = wbReader.ReadInt16(); + wb.xLength = wbReader.ReadInt16(); + wb.zLength = wbReader.ReadInt16(); + wbReader.ReadInt16(); // padding + wb.properties = wbReader.ReadUInt32(); + col->waterBoxes.push_back(wb); + } + } + + return col; +} + +ExportResult OoTCollisionBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto col = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTCollisionHeader, 0); + + // Bounding box + writer.Write(col->absMinX); + writer.Write(col->absMinY); + writer.Write(col->absMinZ); + writer.Write(col->absMaxX); + writer.Write(col->absMaxY); + writer.Write(col->absMaxZ); + + // Vertices + writer.Write(static_cast(col->vertices.size())); + for (auto& v : col->vertices) { + writer.Write(v.x); + writer.Write(v.y); + writer.Write(v.z); + } + + // Polygons + writer.Write(static_cast(col->polygons.size())); + for (auto& p : col->polygons) { + writer.Write(p.type); + writer.Write(p.vtxA); + writer.Write(p.vtxB); + writer.Write(p.vtxC); + writer.Write(p.normX); + writer.Write(p.normY); + writer.Write(p.normZ); + writer.Write(p.dist); + } + + // Surface types (written in reversed data order to match OTRExporter) + writer.Write(static_cast(col->surfaceTypes.size())); + for (auto& st : col->surfaceTypes) { + writer.Write(st.data1); + writer.Write(st.data0); + } + + // Camera data entries + writer.Write(static_cast(col->camDataEntries.size())); + for (auto& entry : col->camDataEntries) { + writer.Write(entry.cameraSType); + writer.Write(entry.numData); + writer.Write(entry.cameraPosIndex); + } + + // Camera positions + writer.Write(static_cast(col->camPositions.size())); + for (auto& pos : col->camPositions) { + writer.Write(pos.x); + writer.Write(pos.y); + writer.Write(pos.z); + } + + // Water boxes + writer.Write(static_cast(col->waterBoxes.size())); + for (auto& wb : col->waterBoxes) { + writer.Write(wb.xMin); + writer.Write(wb.ySurface); + writer.Write(wb.zMin); + writer.Write(wb.xLength); + writer.Write(wb.zLength); + writer.Write(wb.properties); + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTCollisionFactory.h b/src/factories/oot/OoTCollisionFactory.h new file mode 100644 index 00000000..71737617 --- /dev/null +++ b/src/factories/oot/OoTCollisionFactory.h @@ -0,0 +1,80 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include +#include +#include + +namespace OoT { + +struct CollisionVertex { + int16_t x, y, z; +}; + +struct CollisionPoly { + uint16_t type; + uint16_t vtxA; + uint16_t vtxB; + uint16_t vtxC; + uint16_t normX; + uint16_t normY; + uint16_t normZ; + uint16_t dist; +}; + +struct SurfaceType { + uint32_t data0; + uint32_t data1; +}; + +struct CamDataEntry { + uint16_t cameraSType; + int16_t numData; + uint32_t cameraPosIndex; +}; + +struct CamPosData { + int16_t x, y, z; +}; + +struct WaterBox { + int16_t xMin; + int16_t ySurface; + int16_t zMin; + int16_t xLength; + int16_t zLength; + uint32_t properties; +}; + +class OoTCollisionData : public IParsedData { +public: + int16_t absMinX, absMinY, absMinZ; + int16_t absMaxX, absMaxY, absMaxZ; + std::vector vertices; + std::vector polygons; + std::vector surfaceTypes; + std::vector camDataEntries; + std::vector camPositions; + std::vector waterBoxes; +}; + +class OoTCollisionBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTCollisionFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTCollisionBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTCurveAnimationFactory.cpp b/src/factories/oot/OoTCurveAnimationFactory.cpp new file mode 100644 index 00000000..24592822 --- /dev/null +++ b/src/factories/oot/OoTCurveAnimationFactory.cpp @@ -0,0 +1,138 @@ +#ifdef OOT_SUPPORT + +#include "OoTCurveAnimationFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +std::optional> OoTCurveAnimationFactory::parse(std::vector& buffer, YAML::Node& node) { + // ROM layout: CurveAnimationHeader (16 bytes) + // +0x00: segptr refIndex + // +0x04: segptr transformData + // +0x08: segptr copyValues + // +0x0C: int16 unk_0C + // +0x0E: int16 unk_10 + auto [_, segment] = Decompressor::AutoDecode(node, buffer, 0x10); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + // ZAnimation base class reads frameCount from offset 0 (which for curve anims + // is actually the high 16 bits of refIndex pointer — matches OTRExporter behavior) + auto anim = std::make_shared(); + reader.Seek(0, LUS::SeekOffsetType::Start); + anim->frameCount = reader.ReadInt16(); + reader.Seek(0, LUS::SeekOffsetType::Start); + + uint32_t refIndexAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + uint32_t transformDataAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + uint32_t copyValuesAddr = Companion::Instance->PatchVirtualAddr(reader.ReadUInt32()); + + // Get limb count from the skeleton referenced by skel_offset + // skel_offset is segment-relative, so construct full segmented address + // using the same segment as this animation + uint32_t animOffset = GetSafeNode(node, "offset"); + uint8_t segNum = SEGMENT_NUMBER(animOffset); + uint32_t skelOffset = (segNum << 24) | GetSafeNode(node, "skel_offset"); + YAML::Node skelNode; + skelNode["offset"] = skelOffset; + auto skelRaw = Decompressor::AutoDecode(skelNode, buffer, 0x08); + LUS::BinaryReader skelReader(skelRaw.segment.data, skelRaw.segment.size); + skelReader.SetEndianness(Torch::Endianness::Big); + skelReader.ReadUInt32(); // skip limbs array ptr + uint8_t limbCount = skelReader.ReadUByte(); + + // Read refIndex array: 3 * 3 * limbCount entries of uint8 + size_t transformDataSize = 0; + size_t copyValuesSize = 0; + if (refIndexAddr != 0) { + uint32_t refCount = 3 * 3 * limbCount; + YAML::Node riNode; + riNode["offset"] = refIndexAddr; + auto riRaw = Decompressor::AutoDecode(riNode, buffer, refCount); + LUS::BinaryReader riReader(riRaw.segment.data, riRaw.segment.size); + + for (uint32_t i = 0; i < refCount; i++) { + uint8_t ref = riReader.ReadUByte(); + if (ref == 0) { + copyValuesSize++; + } else { + transformDataSize += ref; + } + anim->refIndexArr.push_back(ref); + } + } + + // Read transform data array + if (transformDataAddr != 0 && transformDataSize > 0) { + YAML::Node tdNode; + tdNode["offset"] = transformDataAddr; + auto tdRaw = Decompressor::AutoDecode(tdNode, buffer, transformDataSize * 0x0C); + LUS::BinaryReader tdReader(tdRaw.segment.data, tdRaw.segment.size); + tdReader.SetEndianness(Torch::Endianness::Big); + + for (size_t i = 0; i < transformDataSize; i++) { + CurveInterpKnot knot; + knot.unk_00 = tdReader.ReadUInt16(); + knot.unk_02 = tdReader.ReadInt16(); + knot.unk_04 = tdReader.ReadInt16(); + knot.unk_06 = tdReader.ReadInt16(); + knot.unk_08 = tdReader.ReadFloat(); + anim->transformDataArr.push_back(knot); + } + } + + // Read copy values array + if (copyValuesAddr != 0 && copyValuesSize > 0) { + YAML::Node cvNode; + cvNode["offset"] = copyValuesAddr; + auto cvRaw = Decompressor::AutoDecode(cvNode, buffer, copyValuesSize * 2); + LUS::BinaryReader cvReader(cvRaw.segment.data, cvRaw.segment.size); + cvReader.SetEndianness(Torch::Endianness::Big); + + for (size_t i = 0; i < copyValuesSize; i++) { + anim->copyValuesArr.push_back(cvReader.ReadInt16()); + } + } + + return anim; +} + +ExportResult OoTCurveAnimationBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto anim = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTAnimation, 0); + + writer.Write(static_cast(OoTAnimationType::Curve)); + writer.Write(anim->frameCount); + + writer.Write(static_cast(anim->refIndexArr.size())); + for (auto& val : anim->refIndexArr) { + writer.Write(val); + } + + writer.Write(static_cast(anim->transformDataArr.size())); + for (auto& knot : anim->transformDataArr) { + writer.Write(knot.unk_00); + writer.Write(knot.unk_02); + writer.Write(knot.unk_04); + writer.Write(knot.unk_06); + writer.Write(knot.unk_08); + } + + writer.Write(static_cast(anim->copyValuesArr.size())); + for (auto& val : anim->copyValuesArr) { + writer.Write(val); + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTCurveAnimationFactory.h b/src/factories/oot/OoTCurveAnimationFactory.h new file mode 100644 index 00000000..188e46b4 --- /dev/null +++ b/src/factories/oot/OoTCurveAnimationFactory.h @@ -0,0 +1,36 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "OoTAnimationTypes.h" +#include + +namespace OoT { + +class OoTCurveAnimationData : public IParsedData { +public: + int16_t frameCount; + std::vector refIndexArr; + std::vector transformDataArr; + std::vector copyValuesArr; +}; + +class OoTCurveAnimationBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTCurveAnimationFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTCurveAnimationBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTCutsceneFactory.cpp b/src/factories/oot/OoTCutsceneFactory.cpp new file mode 100644 index 00000000..0b6b2db7 --- /dev/null +++ b/src/factories/oot/OoTCutsceneFactory.cpp @@ -0,0 +1,327 @@ +#ifdef OOT_SUPPORT + +#include "OoTCutsceneFactory.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" + +#include + +namespace OoT { + +// Camera commands use variable-length entries terminated by a 0xFF marker byte. +bool CutsceneSerializer::IsCameraCmd(uint32_t id) { + return id == 1 || id == 2 || id == 5 || id == 6; +} + +// These commands use 0x0C-byte entries instead of the standard 0x30. +bool CutsceneSerializer::IsSmallEntryCmd(uint32_t id) { + return id == 0x09 || id == 0x13 || id == 0x8C; +} + +// Single-entry commands (transition: 0x2D, destination: 0x3E8). +bool CutsceneSerializer::IsSingleEntryCmd(uint32_t id) { + return id == 0x2D || id == 0x3E8; +} + +// Commands that are in the valid range (0x0E–0x90) but not implemented. +const std::set CutsceneSerializer::sUnimplementedCmds = { + 0x0B, 0x0C, 0x0D, 0x14, 0x15, 0x16, 0x1A, 0x1B, 0x1C, 0x20, 0x21, 0x38, 0x3B, 0x3D, + 0x47, 0x49, 0x5B, 0x5C, 0x5F, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x6D, 0x70, 0x71, 0x7A +}; + +bool CutsceneSerializer::IsHandledCmd(uint32_t cid) { + // Basic commands (camera, misc, rumble) + if (cid >= 0x01 && cid <= 0x0A) return true; + + // Transition and destination + if (IsSingleEntryCmd(cid)) return true; + + // Text, time, and non-actor-cue commands + if (cid == 0x13 || cid == 0x56 || cid == 0x57 || cid == 0x7C || cid == 0x8C) return true; + + // Actor cue commands (0x0E–0x90 range, excluding known unimplemented ones) + if (cid >= 0x0E && cid <= 0x90 && !sUnimplementedCmds.count(cid)) return true; + + return false; +} + +// Cutscene serialization — shared with OoTSceneFactory's SetCutscenes handler. +// Walk through cutscene commands to determine total byte size. +// Returns 0 if the data is corrupt or too small. +uint32_t CutsceneSerializer::CalculateSize(std::vector& buffer, uint32_t segAddr) { + auto reader = ReadSubArray(buffer, segAddr, 0x10000); + uint32_t endOffset = reader.GetLength(); + + // Need at least numCommands + endFrame + if (endOffset < 8) return 0; + + uint32_t numCommands = reader.ReadUInt32(); + reader.ReadUInt32(); // endFrame + + for (uint32_t cmd = 0; cmd < numCommands; cmd++) { + // Each command is at least 8 bytes (id + entryCount) + if (reader.GetBaseAddress() + 8 > endOffset) return 0; + + uint32_t cmdId = reader.ReadUInt32(); + if (cmdId == 0xFFFFFFFF) break; // end marker + + uint32_t entryCount = reader.ReadUInt32(); + + if (IsCameraCmd(cmdId)) { + // Camera commands have an extra uint32 before the entry list + if (reader.GetBaseAddress() + 4 > endOffset) return 0; + + reader.ReadUInt32(); + for (uint32_t ci = 0; ci < 1000; ci++) { + // Each camera entry is 0x10 bytes + if (reader.GetBaseAddress() + 0x10 > endOffset) return 0; + + uint8_t marker = reader.ReadUByte(); + reader.Seek(reader.GetBaseAddress() + 0x0F, LUS::SeekOffsetType::Start); + if (marker == 0xFF) break; // end of camera entries + } + continue; + } + + if (IsSingleEntryCmd(cmdId)) { + // Single entry is 0x08 bytes + if (reader.GetBaseAddress() + 0x08 > endOffset) return 0; + + reader.Seek(reader.GetBaseAddress() + 0x08, LUS::SeekOffsetType::Start); + continue; + } + + // Default: fixed-size entries (0x0C for rumble/text/time, 0x30 for everything else) + uint32_t entrySize = IsSmallEntryCmd(cmdId) ? 0x0C : 0x30; + uint32_t dataSize = entryCount * entrySize; + + // Ensure all entries fit within the cutscene data + if (reader.GetBaseAddress() + dataSize > endOffset) return 0; + + reader.Seek(reader.GetBaseAddress() + dataSize, LUS::SeekOffsetType::Start); + } + + // Read the end marker (0xFFFFFFFF + 0x00000000) to get final size + if (reader.GetBaseAddress() + 8 > endOffset) return 0; + reader.ReadUInt32(); + reader.ReadUInt32(); + + return reader.GetBaseAddress(); +} + +void CutsceneSerializer::WriteCameraCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w) { + uint16_t startFrame = reader.ReadUInt16(); + uint16_t endFrame = reader.ReadUInt16(); + w.Write(CS_CMD_HH(startFrame, endFrame)); + + uint16_t unk1 = reader.ReadUInt16(); + uint16_t unk2 = reader.ReadUInt16(); + w.Write(CS_CMD_HH(unk1, unk2)); + + while (true) { + int8_t marker = reader.ReadInt8(); + int8_t interpType = reader.ReadInt8(); + int16_t numPointsForFrame = reader.ReadInt16(); + float viewAngle = reader.ReadFloat(); + int16_t posX = reader.ReadInt16(); + int16_t posY = reader.ReadInt16(); + int16_t posZ = reader.ReadInt16(); + int16_t unused = reader.ReadInt16(); + + w.Write(CS_CMD_BBH(marker, interpType, numPointsForFrame)); + w.Write(viewAngle); + w.Write(CS_CMD_HH(posX, posY)); + w.Write(CS_CMD_HH(posZ, unused)); + + if ((uint8_t)marker == 0xFF) break; + } +} + +void CutsceneSerializer::WriteSingleEntryCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w) { + reader.ReadUInt32(); // skip original count + w.Write(static_cast(1)); + + uint16_t startFrame = reader.ReadUInt16(); + uint16_t endFrame = reader.ReadUInt16(); + uint16_t type = reader.ReadUInt16(); + reader.ReadUInt16(); // padding + + w.Write(CS_CMD_HH(startFrame, endFrame)); + w.Write(CS_CMD_HH(type, type)); +} + +// Helper: check if a command uses actor cue format (0x30-byte entries with rotation fields) +static bool IsActorCueCmd(uint32_t cid) { + return cid != 0x03 && cid != 0x04 && cid != 0x56 && cid != 0x57 && cid != 0x7C; +} + +void CutsceneSerializer::WriteEntryCountCmd(uint32_t cid, LUS::BinaryReader& reader, LUS::BinaryWriter& w) { + uint32_t entryCount = reader.ReadUInt32(); + w.Write(entryCount); + + for (uint32_t i = 0; i < entryCount; i++) { + // All entry types share a common header + uint16_t base = reader.ReadUInt16(); + uint16_t startFrame = reader.ReadUInt16(); + uint16_t endFrame = reader.ReadUInt16(); + + w.Write(CS_CMD_HH(base, startFrame)); + + if (cid == 0x09) { + // Rumble (0x0C bytes) + uint8_t sourceStrength = reader.ReadUByte(); + uint8_t duration = reader.ReadUByte(); + uint8_t decreaseRate = reader.ReadUByte(); + uint8_t unk = reader.ReadUByte(); + uint16_t unkA = reader.ReadUInt16(); + + w.Write(CS_CMD_HBB(endFrame, sourceStrength, duration)); + w.Write(CS_CMD_BBH(decreaseRate, unk, unkA)); + continue; + } + + if (cid == 0x13) { + // Text (0x0C bytes) + uint16_t type = reader.ReadUInt16(); + uint16_t textId1 = reader.ReadUInt16(); + uint16_t textId2 = reader.ReadUInt16(); + + w.Write(CS_CMD_HH(endFrame, type)); + w.Write(CS_CMD_HH(textId1, textId2)); + continue; + } + + if (cid == 0x8C) { + // Time (0x0C bytes) + uint8_t hour = reader.ReadUByte(); + uint8_t minute = reader.ReadUByte(); + reader.ReadUInt32(); // padding + + w.Write(CS_CMD_HBB(endFrame, hour, minute)); + w.Write(static_cast(0)); + continue; + } + + // Actor cue (0x30 bytes) + if (IsActorCueCmd(cid)) { + w.Write(CS_CMD_HH(endFrame, reader.ReadUInt16())); + uint16_t rotY = reader.ReadUInt16(); + uint16_t rotZ = reader.ReadUInt16(); + w.Write(CS_CMD_HH(rotY, rotZ)); + for (int j = 0; j < 9; j++) w.Write(reader.ReadUInt32()); + continue; + } + + // Non-actor-cue (0x30 bytes, commands 0x03, 0x04, 0x56, 0x57, 0x7C) + w.Write(CS_CMD_HH(endFrame, reader.ReadUInt16())); + for (int j = 0; j < 10; j++) w.Write(reader.ReadUInt32()); + } +} + +std::vector CutsceneSerializer::Serialize(std::vector& buffer, uint32_t segAddr) { + auto size = CalculateSize(buffer, segAddr); + + // Size of 0 means corrupt or empty cutscene data + if (size == 0) return {}; + + return Write(buffer, segAddr, size); +} + +std::vector CutsceneSerializer::Write(std::vector& buffer, uint32_t segAddr, uint32_t size) { + auto csReader = ReadSubArray(buffer, segAddr, size); + + // Use the actual bytes available (ReadSubArray may return less than requested) + size = csReader.GetLength(); + + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTCutscene, 0); + + // Write a placeholder for the cutscene word count. + // We don't know the final count until all commands are serialized, + // so we'll seek back and fill this in at the end. + uint32_t wordCountPos = w.GetStream()->GetLength(); + w.Write(static_cast(0)); + + // Used to calculate word count at the end + uint32_t dataStartPos = w.GetStream()->GetLength(); + + uint32_t numCmds = csReader.ReadUInt32(); + uint32_t endFrame = csReader.ReadUInt32(); + w.Write(numCmds); + w.Write(endFrame); + + for (uint32_t ci = 0; ci < numCmds && csReader.GetBaseAddress() + 8 <= size; ci++) { + uint32_t cid = csReader.ReadUInt32(); + + // End marker + if (cid == 0xFFFFFFFF) break; + + // Skip unhandled commands (entryCount * 0x30 bytes) + if (!IsHandledCmd(cid)) { + uint32_t entryCount = csReader.ReadUInt32(); + uint32_t dataSize = entryCount * 0x30; + if (csReader.GetBaseAddress() + dataSize <= size) + csReader.Seek(csReader.GetBaseAddress() + dataSize, LUS::SeekOffsetType::Start); + continue; + } + + // Write command ID, then serialize its data + w.Write(cid); + + if (IsCameraCmd(cid)) { + WriteCameraCmd(csReader, w); + continue; + } + + if (IsSingleEntryCmd(cid)) { + WriteSingleEntryCmd(csReader, w); + continue; + } + + // Default: entry-count-based commands + WriteEntryCountCmd(cid, csReader, w); + } + + // Write end marker + w.Write(static_cast(0xFFFFFFFF)); + w.Write(static_cast(0)); + + // Fill in the reserved word count now that we know the final size + uint32_t dataEndPos = w.GetStream()->GetLength(); + w.Seek(wordCountPos, LUS::SeekOffsetType::Start); + w.Write(static_cast((dataEndPos - dataStartPos) / 4)); + w.Seek(dataEndPos, LUS::SeekOffsetType::Start); + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + return std::vector(str.begin(), str.end()); +} + +class OoTCutsceneData : public IParsedData { +public: + std::vector mBinary; + OoTCutsceneData(std::vector data) : mBinary(std::move(data)) {} +}; + +std::optional> OoTCutsceneFactory::parse(std::vector& buffer, YAML::Node& node) { + auto data = CutsceneSerializer::Serialize(buffer, GetSafeNode(node, "offset")); + if (data.empty()) { + SPDLOG_WARN("OoTCutsceneFactory: Failed to serialize cutscene"); + return std::nullopt; + } + return std::make_shared(std::move(data)); +} + +ExportResult OoTCutsceneBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto cs = std::static_pointer_cast(raw); + write.write(cs->mBinary.data(), cs->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTCutsceneFactory.h b/src/factories/oot/OoTCutsceneFactory.h new file mode 100644 index 00000000..23eb0b02 --- /dev/null +++ b/src/factories/oot/OoTCutsceneFactory.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" + +namespace OoT { + +class OoTCutsceneBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTCutsceneFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTCutsceneBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTDListHelpers.cpp b/src/factories/oot/OoTDListHelpers.cpp new file mode 100644 index 00000000..f44e091b --- /dev/null +++ b/src/factories/oot/OoTDListHelpers.cpp @@ -0,0 +1,780 @@ +#ifdef OOT_SUPPORT + +#include "OoTDListHelpers.h" +#include "Companion.h" +#include "spdlog/spdlog.h" +#include "factories/DisplayListFactory.h" +#include "factories/DisplayListOverrides.h" +#include "utils/Decompressor.h" +#include "DeferredVtx.h" +#include +#include +#include "n64/gbi-otr.h" +#include "strhash64/StrHash64.h" + +#define C0(pos, width) ((w0 >> (pos)) & ((1U << width) - 1)) +#define ALIGN16(val) (((val) + 0xF) & ~0xF) + +// F3DEX2 opcodes (OoT is always f3dex2) +static constexpr uint8_t F3DEX2_VTX = 0x01; +static constexpr uint8_t F3DEX2_DL = 0xDE; +static constexpr uint8_t F3DEX2_MTX = 0xDA; +static constexpr uint8_t F3DEX2_ENDDL = 0xDF; +static constexpr uint8_t F3DEX2_SETTIMG = 0xFD; +static constexpr uint8_t F3DEX2_MOVEMEM = 0xDC; +static constexpr uint8_t F3DEX2_RDPHALF_1 = 0xE1; +static constexpr uint8_t F3DEX2_BRANCH_Z = 0x04; +static constexpr uint8_t F3DEX2_SETOTHERMODE_H = 0xE3; +static constexpr uint8_t F3DEX2_NOOP = 0x00; +static constexpr uint8_t F3DEX2_SETTILE = 0xF5; +static constexpr uint8_t F3DEX2_LOADBLOCK = 0xF3; +static constexpr uint8_t F3DEX2_MV_LIGHT = 0x0A; + +namespace OoT { +namespace DListHelpers { + +// ── Internal helpers (not exposed in header) ────────────────────────── + +static uint32_t RemapSegmentedAddr(uint32_t addr, const std::string& expectedType = "") { + uint8_t seg = SEGMENT_NUMBER(addr); + uint32_t offset = SEGMENT_OFFSET(addr); + auto segBase = Companion::Instance->GetFileOffsetFromSegmentedAddr(seg); + if (!segBase.has_value()) return addr; + + for (uint8_t otherSeg = 1; otherSeg < 0x20; otherSeg++) { + if (otherSeg == seg) continue; + auto otherBase = Companion::Instance->GetFileOffsetFromSegmentedAddr(otherSeg); + if (otherBase.has_value() && otherBase.value() == segBase.value()) { + uint32_t remapped = (otherSeg << 24) | offset; + auto node = Companion::Instance->GetNodeByAddr(remapped); + if (node.has_value()) { + if (!expectedType.empty()) { + auto n = std::get<1>(node.value()); + auto nType = GetSafeNode(n, "type"); + if (nType != expectedType) continue; + } + return remapped; + } + } + } + return addr; +} + +static bool IsAliasSegment(uint32_t addr) { + if (!IS_SEGMENTED(addr)) return false; + auto thisSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(addr)); + if (!thisSeg.has_value()) return true; + for (uint8_t s = 0; s < SEGMENT_NUMBER(addr); s++) { + auto otherSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(s); + if (otherSeg.has_value() && otherSeg.value() == thisSeg.value()) { + return true; + } + } + return false; +} + +// ── Export helpers ───────────────────────────────────────────────────── +// All Export helpers follow main's pattern: they modify w0/w1 in place and may write +// intermediate words. The loop's final writer.Write(w0); writer.Write(w1) handles the last pair. +// Helpers that need to skip the rest of the iteration (gSunDLVtx, BranchZ, RDPHALF_1) +// write all their words and return true for `continue`. + +static bool ExportGSunDLVtx(uint32_t w0, uint32_t w1, + LUS::BinaryWriter& writer, std::string* replacement) { + if (!replacement || replacement->find("gSunDL") == std::string::npos) return false; + + auto ptr = w1; + std::optional> rangedMatch; + + for (const auto& type : std::vector{"TEXTURE", "BLOB"}) { + auto decs = Companion::Instance->GetNodesByType(type); + if (!decs.has_value()) continue; + for (auto& [name, dnode] : decs.value()) { + auto doffset = GetSafeNode(dnode, "offset"); + uint32_t dsize = 0; + if (type == "TEXTURE") { + auto fmt = GetSafeNode(dnode, "format"); + auto w = GetSafeNode(dnode, "width"); + auto h = GetSafeNode(dnode, "height"); + uint32_t bpp = 16; + if (fmt == "I4" || fmt == "IA4" || fmt == "CI4") bpp = 4; + else if (fmt == "I8" || fmt == "IA8" || fmt == "CI8") bpp = 8; + else if (fmt == "RGBA16" || fmt == "IA16") bpp = 16; + else if (fmt == "RGBA32") bpp = 32; + dsize = (w * h * bpp) / 8; + } else if (type == "BLOB") { + dsize = GetSafeNode(dnode, "size"); + } + if (ASSET_PTR(ptr) >= ASSET_PTR(doffset) && ASSET_PTR(ptr) < ASSET_PTR(doffset) + dsize) { + auto path = Companion::Instance->GetSafeStringByAddr(doffset, type); + uint32_t diff = ASSET_PTR(ptr) - ASSET_PTR(doffset); + if (path.has_value() && (!rangedMatch.has_value() || diff < rangedMatch->second)) { + rangedMatch = std::make_pair(path.value(), diff); + } + } + } + } + + if (!rangedMatch.has_value()) return false; + + auto& [path, diff] = rangedMatch.value(); + uint64_t hash = CRC64(path.c_str()); + size_t nvtx = (w0 >> 12) & 0xFF; + size_t didx = ((w0 >> 1) & 0x7F) - nvtx; + N64Gfx value = gsSPVertexOTR(diff, nvtx, didx); + writer.Write(value.words.w0); + writer.Write(value.words.w1); + writer.Write(static_cast(hash >> 32)); + writer.Write(static_cast(hash & 0xFFFFFFFF)); + return true; // All words written, caller should continue +} + +// Modifies w0/w1 for the final write. Writes intermediate words. +static void ExportVtx(uint32_t& w0, uint32_t& w1, + LUS::BinaryWriter& writer, std::string* replacement) { + size_t nvtx = (w0 >> 12) & 0xFF; // C0(12, 8) + size_t didx = ((w0 >> 1) & 0x7F) - nvtx; // C0(1, 7) - C0(12, 8) + auto ptr = Companion::Instance->PatchVirtualAddr(w1); + + if (IsAliasSegment(w1)) { + w1 = w1 + 1; + SPDLOG_INFO("VTX export: alias segment for 0x{:X}", ptr); + return; // w0/w1 set, caller writes final pair + } + + // Check overlap with cross-file handling + if (auto overlap = GFXDOverride::GetVtxOverlap(ptr); overlap.has_value()) { + auto ovnode = std::get<1>(overlap.value()); + auto path = Companion::Instance->RelativePath(std::get<0>(overlap.value())); + + auto currentDir = (*replacement).substr(0, (*replacement).rfind('/')); + auto vtxDir = path.substr(0, path.rfind('/')); + if (currentDir != vtxDir) { + SPDLOG_WARN("Cross-file VTX overlap at 0x{:X} (from {}), writing null vtxDecl", ptr, path); + w0 = G_VTX_OTR_HASH << 24; + w1 = 0; + } else { + uint64_t hash = CRC64(path.c_str()); + if (hash == 0) { + throw std::runtime_error("Vtx hash is 0 for " + std::get<0>(overlap.value())); + } + SPDLOG_INFO("Found vtx: 0x{:X} Hash: 0x{:X} Path: {}", ptr, hash, path); + auto offset = GetSafeNode(ovnode, "offset"); + auto diff = ASSET_PTR(ptr) - ASSET_PTR(offset); + N64Gfx value = gsSPVertexOTR(diff, nvtx, didx); + writer.Write(value.words.w0); + writer.Write(value.words.w1); + w0 = hash >> 32; + w1 = hash & 0xFFFFFFFF; + } + return; + } + + // Direct lookup with OOT:ARRAY support + auto vtxNode = Companion::Instance->GetNodeByAddr(ptr); + std::optional dec = std::nullopt; + if (vtxNode.has_value()) { + auto [vpath, vn] = vtxNode.value(); + auto vtype = GetSafeNode(vn, "type"); + if (vtype == "VTX" || vtype == "OOT:ARRAY") { + dec = vpath; + } + } + if (dec.has_value()) { + auto currentDir = (*replacement).substr(0, (*replacement).rfind('/')); + auto vtxDir = dec.value().substr(0, dec.value().rfind('/')); + if (currentDir != vtxDir) { + SPDLOG_WARN("Cross-file VTX at 0x{:X} (from {}), writing null vtxDecl", ptr, dec.value()); + w0 = G_VTX_OTR_HASH << 24; + w1 = 0; + } else { + uint64_t hash = CRC64(dec.value().c_str()); + if (hash == 0) { + throw std::runtime_error("Vtx hash is 0 for " + dec.value()); + } + SPDLOG_INFO("Found vtx: 0x{:X} Hash: 0x{:X} Path: {}", ptr, hash, dec.value()); + N64Gfx value = gsSPVertexOTR(0, nvtx, didx); + writer.Write(value.words.w0); + writer.Write(value.words.w1); + w0 = hash >> 32; + w1 = hash & 0xFFFFFFFF; + } + return; + } + + // Virtual segment handling + if (IS_VIRTUAL_SEGMENT(w1)) { + w0 = G_VTX_OTR_HASH << 24; + w1 = 0; + return; + } + + // Cross-segment fallback + SPDLOG_WARN("VTX export: NOT FOUND vtx at 0x{:X} w1=0x{:X} replacement={}", ptr, w1, *replacement); + w1 = (w1 & 0x0FFFFFFF) + 1; +} + +static void ExportDL(uint32_t& w0, uint32_t& w1, LUS::BinaryWriter& writer) { + auto ptr = w1; + uint8_t dlSeg = SEGMENT_NUMBER(ptr); + + // Segments 8-13 are runtime-swapped and must stay unresolved + std::optional dec = std::nullopt; + if (dlSeg < 8 || dlSeg > 13) { + dec = Companion::Instance->GetSafeStringByAddr(ptr, "GFX"); + if (!dec.has_value()) { + auto remapped = RemapSegmentedAddr(ptr, "GFX"); + if (remapped != ptr) { + dec = Companion::Instance->GetSafeStringByAddr(remapped, "GFX"); + if (dec.has_value()) ptr = remapped; + } + } + } + auto branch = (w0 >> 16) & G_DL_NO_PUSH; + + if (dec.has_value()) { + uint64_t hash = CRC64(dec.value().c_str()); + SPDLOG_INFO("Found display list: 0x{:X} Hash: 0x{:X} Path: {}", ptr, hash, dec.value()); + + N64Gfx value = gsSPDisplayListOTRHash(ptr); + w0 = value.words.w0; + w1 = 0; + + writer.Write(w0); + writer.Write(w1); + + w0 = hash >> 32; + w1 = hash & 0xFFFFFFFF; + + if (branch) { + writer.Write(w0); + writer.Write(w1); + + N64Gfx endValue = gsSPRawOpcode(F3DEX2_ENDDL); + w0 = endValue.words.w0; + w1 = endValue.words.w1; + } + } else { + SPDLOG_WARN("Could not find display list at 0x{:X}", ptr); + w1 = (w1 & 0x0FFFFFFF) + 1; + } +} + +static void ExportMoveMem(uint32_t& w0, uint32_t& w1, LUS::BinaryWriter& writer) { + auto ptr = w1; + uint8_t index = (w0) & 0xFF; // C0(0, 8) + uint8_t offset = ((w0 >> 8) & 0xFF) * 8; // C0(8, 8) * 8 + bool hasOffset = false; + + auto res = Companion::Instance->GetStringByAddr(ptr); + + if (!res.has_value()) { + res = Companion::Instance->GetStringByAddr(ptr - 0x8); + hasOffset = res.has_value(); + } + + if (res.has_value()) { + uint64_t hash = CRC64(res.value().c_str()); + SPDLOG_INFO("Found movemem: 0x{:X} Hash: 0x{:X} Path: {}", ptr, hash, res.value()); + + w0 &= 0x00FFFFFF; + w0 += G_MOVEMEM_OTR_HASH << 24; + w1 = _SHIFTL(index, 24, 8) | _SHIFTL(offset, 16, 8) | _SHIFTL((uint8_t)(hasOffset ? 1 : 0), 8, 8); + + writer.Write(w0); + writer.Write(w1); + + w0 = hash >> 32; + w1 = hash & 0xFFFFFFFF; + } else { + SPDLOG_WARN("Could not find light at 0x{:X}", ptr); + } + // Final w0/w1 written by caller +} + +static void ExportSetTImg(uint32_t& w0, uint32_t& w1, + LUS::BinaryWriter& writer, std::string* replacement) { + auto ptr = w1; + auto dec = Companion::Instance->GetSafeStringByAddr(ptr, "TEXTURE"); + + if (dec.has_value()) { + uint64_t hash = CRC64(dec.value().c_str()); + if (hash == 0) { + throw std::runtime_error("Texture hash is 0 for " + dec.value()); + } + SPDLOG_INFO("Found texture: 0x{:X} Hash: 0x{:X} Path: {}", ptr, hash, dec.value()); + + uint32_t newW0 = (G_SETTIMG_OTR_HASH << 24) | (w0 & 0x00FFFFFF); + writer.Write(newW0); + writer.Write(static_cast(0)); + + w0 = hash >> 32; + w1 = hash & 0xFFFFFFFF; + } else { + SPDLOG_WARN("Could not find texture at 0x{:X}", ptr); + if (replacement && replacement->find("sShadowMaterialDL") != std::string::npos) { + w1 = 0x0C000001; + } else { + auto patchedPtr = Companion::Instance->PatchVirtualAddr(ptr); + w1 = (patchedPtr & 0x0FFFFFFF) + 1; + } + writer.Write(w0); + writer.Write(w1); + } + // Final w0/w1 written by caller +} + +static void ExportGSunDLTextureFixup(uint8_t opcode, uint32_t& w0, uint32_t& w1, + std::string* replacement) { + if (!replacement || replacement->find("gSunDL") == std::string::npos) return; + + constexpr uint8_t G_TX_LOADTILE = 7; + constexpr uint8_t G_IM_SIZ_4b = 0; + + if (opcode == F3DEX2_SETTILE) { + uint8_t tile = (w1 >> 24) & 0x07; + if (tile != G_TX_LOADTILE) { + w0 = (w0 & ~(0x3 << 19)) | (G_IM_SIZ_4b << 19); + } + } + + if (opcode == F3DEX2_LOADBLOCK) { + uint32_t ult = w0 & 0xFFF; + uint32_t texels = (w1 >> 12) & 0xFFF; + if (ult != G_TX_LOADTILE) { + texels = (texels + 1) / 2 - 1; + w1 = (w1 & ~(0xFFF << 12)) | ((texels & 0xFFF) << 12); + } + } +} + +static void ExportMtx(uint32_t& w0, uint32_t& w1, LUS::BinaryWriter& writer) { + auto ptr = w1; + auto dec = Companion::Instance->GetSafeStringByAddr(ptr, "OOT:MTX"); + if (!dec.has_value()) { + dec = Companion::Instance->GetSafeStringByAddr(ptr, "MTX"); + } + if (!dec.has_value()) { + auto remapped = RemapSegmentedAddr(ptr, "OOT:MTX"); + if (remapped == ptr) remapped = RemapSegmentedAddr(ptr, "MTX"); + if (remapped != ptr) { + dec = Companion::Instance->GetSafeStringByAddr(remapped, "OOT:MTX"); + if (!dec.has_value()) dec = Companion::Instance->GetSafeStringByAddr(remapped, "MTX"); + if (dec.has_value()) ptr = remapped; + } + } + + if (dec.has_value()) { + uint64_t hash = CRC64(dec.value().c_str()); + if (hash == 0) { + throw std::runtime_error("Matrix hash is 0 for " + dec.value()); + } + SPDLOG_INFO("Found matrix: 0x{:X} Hash: 0x{:X} Path: {}", ptr, hash, dec.value()); + + w0 &= 0x00FFFFFF; + w0 += G_MTX_OTR << 24; + + writer.Write(w0); + writer.Write(w1); + + w0 = hash >> 32; + w1 = hash & 0xFFFFFFFF; + } else { + SPDLOG_WARN("Could not find matrix at 0x{:X}", ptr); + w1 = (ptr & 0x0FFFFFFF) + 1; + } + // Final w0/w1 written by caller +} + +// Writes all words and returns true (caller should continue). +static bool ExportBranchZ(uint32_t w0, uint32_t w1, + size_t cmdIndex, const std::vector& cmds, + LUS::BinaryWriter& writer) { + uint32_t dlAddr = (cmdIndex >= 2) ? cmds[cmdIndex - 1] : 0; + auto dec = Companion::Instance->GetSafeStringByAddr(dlAddr, "GFX"); + if (!dec.has_value()) { + auto remapped = RemapSegmentedAddr(dlAddr, "GFX"); + if (remapped != dlAddr) { + dec = Companion::Instance->GetSafeStringByAddr(remapped, "GFX"); + } + } + + if (dec.has_value()) { + uint64_t hash = CRC64(dec.value().c_str()); + uint32_t a = (w0 >> 12) & 0xFFF; + uint32_t b = w0 & 0xFFF; + uint32_t branchW0 = (G_BRANCH_Z_OTR << 24) | _SHIFTL(a, 12, 12) | _SHIFTL(b, 0, 12); + writer.Write(branchW0); + writer.Write(w1); + writer.Write(static_cast(hash >> 32)); + writer.Write(static_cast(hash & 0xFFFFFFFF)); + } else { + SPDLOG_WARN("Could not find display list for G_BRANCH_Z at 0x{:X}", dlAddr); + writer.Write(w0); + writer.Write(w1); + } + return true; // All words written, caller should continue +} + +static void ExportOpcodeFixups(uint8_t opcode, uint32_t& w0, uint32_t& w1) { + // G_SETOTHERMODE_H texture LUT re-encoding + if (opcode == F3DEX2_SETOTHERMODE_H) { + uint8_t ss = (w0 >> 8) & 0xFF; + uint8_t nn = w0 & 0xFF; + int32_t sft = 32 - (nn + 1) - ss; + if (sft == 14) { + w1 = w1 >> 14; + } + } + + // G_NOOP zeroing + if (opcode == F3DEX2_NOOP) { + w0 = 0; + w1 = 0; + } + + // Unhandled opcode zeroing + static const std::unordered_set otrHandledOpcodes = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0xD7, 0xD8, 0xD9, 0xDA, 0xDB, 0xDC, 0xDE, 0xDF, + 0xE1, 0xE2, 0xE3, 0xE4, + 0xE6, 0xE7, 0xE8, 0xE9, 0xEF, + 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, + 0xFA, 0xFB, 0xFC, 0xFD, + }; + if (otrHandledOpcodes.find(opcode) == otrHandledOpcodes.end()) { + w0 = (uint32_t)opcode << 24; + w1 = 0; + } +} + +// ── Parse helpers ───────────────────────────────────────────────────── + +static void ParseDL(uint32_t w0, uint32_t w1, YAML::Node& node) { + if (C0(16, 1) == G_DL_NO_PUSH) { + // Branch — handled by caller (sets processing = false) + } + + if (SEGMENT_NUMBER(node["offset"].as()) == SEGMENT_NUMBER(w1)) { + // OoT pre-declares child DLists in YAML — skip AddAsset + auto parentSymbol = GetSafeNode(node, "symbol", ""); + auto dlPos = parentSymbol.rfind("DL_"); + if (dlPos != std::string::npos) { + auto base = parentSymbol.substr(0, dlPos + 3); + uint32_t childOffset = SEGMENT_OFFSET(w1); + std::ostringstream ss; + ss << base << std::uppercase << std::hex + << std::setfill('0') << std::setw(6) << childOffset; + // Symbol derived but not added (OoT pre-declares in YAML) + } + } +} + +static void ParseMoveMem(uint32_t w0, uint32_t w1) { + uint8_t subcommand = (w0 >> 16) & 0xFF; + uint8_t index = C0(0, 8); + uint8_t offset = C0(8, 8) * 8; + bool light = false; + + // OoT is f3dex2 only — check G_MV_LIGHT + if (index == F3DEX2_MV_LIGHT && offset == (2 * 24 + 24)) { + light = true; + } + + if (light) { + // OoT pre-declares lights in YAML — skip AddAsset + } +} + +static void ParseRdpHalf1(uint32_t w0, uint32_t w1, + YAML::Node& node, std::vector& buffer) { + if (!IS_SEGMENTED(w1) || SEGMENT_NUMBER(w1) != SEGMENT_NUMBER(node["offset"].as())) { + return; + } + + const auto decl = Companion::Instance->GetNodeByAddr(w1); + if (!decl.has_value()) { + // DList pre-declared in YAML — skip AddAsset + } + + if (DeferredVtx::IsDeferred()) { + auto branchData = Decompressor::AutoDecode(w1, std::nullopt, buffer); + LUS::BinaryReader branchReader(branchData.segment.data, branchData.segment.size); + branchReader.SetEndianness(Torch::Endianness::Big); + + while (branchReader.GetBaseAddress() + 8 <= branchData.segment.size) { + auto bw0 = branchReader.ReadUInt32(); + auto bw1 = branchReader.ReadUInt32(); + uint8_t bOpcode = bw0 >> 24; + + if (bOpcode == F3DEX2_ENDDL) break; + + if (bOpcode == F3DEX2_VTX && IS_SEGMENTED(bw1)) { + uint32_t bNvtx = (bw0 >> 12) & 0xFF; + DeferredVtx::AddPending(bw1, bNvtx); + } + } + } +} + +static void ParseMtx(uint32_t w1, YAML::Node& node) { + if (IS_SEGMENTED(w1) && SEGMENT_NUMBER(w1) == SEGMENT_NUMBER(node["offset"].as())) { + const auto decl = Companion::Instance->GetNodeByAddr(w1); + if (!decl.has_value()) { + SPDLOG_WARN("Undeclared MTX at 0x{:08X} — YAML enrichment incomplete", w1); + } + } +} + +static void ParseVtx(uint32_t w0, uint32_t w1, uint32_t nvtx, + YAML::Node& node, std::vector& buffer) { + const auto decl = Companion::Instance->GetNodeByAddr(w1); + if (decl.has_value()) { + SPDLOG_WARN("Found vtx at 0x{:X}", w1); + return; + } + + auto adjPtr = Companion::Instance->PatchVirtualAddr(w1); + auto search = SearchVtx(adjPtr); + + if (search.has_value()) { + auto [path, vtx] = search.value(); + SPDLOG_INFO("Path: {}", path); + + auto lOffset = GetSafeNode(vtx, "offset"); + auto lCount = GetSafeNode(vtx, "count"); + auto lSize = ALIGN16(lCount * 16); // sizeof(N64Vtx_t) + + // Compare in absolute ROM address space + uint32_t absPtr = adjPtr; + uint32_t absOffset = lOffset; + if (IS_SEGMENTED(adjPtr)) { + auto seg = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(adjPtr)); + if (seg.has_value()) absPtr = seg.value() + SEGMENT_OFFSET(adjPtr); + } + if (IS_SEGMENTED(lOffset)) { + auto seg = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(lOffset)); + if (seg.has_value()) absOffset = seg.value() + SEGMENT_OFFSET(lOffset); + } + + if (absPtr > absOffset && absPtr <= absOffset + lSize) { + SPDLOG_INFO("Found vtx at 0x{:X} matching last vtx at 0x{:X}", adjPtr, lOffset); + GFXDOverride::RegisterVTXOverlap(adjPtr, search.value()); + } + return; + } + + // Skip VTX auto-creation for alias segments, virtual addresses, etc. + bool skipVtx = !IS_SEGMENTED(adjPtr) || IsAliasSegment(adjPtr); + + if (!skipVtx) { + if (DeferredVtx::IsDeferred()) { + DeferredVtx::AddPending(adjPtr, nvtx); + } else { + SPDLOG_WARN("Undeclared VTX at 0x{:08X} — YAML enrichment incomplete", adjPtr); + } + } +} + +static void FlushVtx(YAML::Node& node) { + if (!DeferredVtx::IsDeferred()) return; + + auto symbol = GetSafeNode(node, "symbol", ""); + auto dlPos = symbol.rfind("DL_"); + std::string baseName = (dlPos != std::string::npos) ? symbol.substr(0, dlPos) : symbol; + DeferredVtx::FlushDeferred(baseName); +} + +// ── Public API ──────────────────────────────────────────────────────── + +std::optional> SearchVtx(uint32_t ptr) { + if (Companion::Instance->GetGBIMinorVersion() != GBIMinorVersion::OoT) return std::nullopt; + + std::vector vtxTypes = {"VTX", "OOT:ARRAY"}; + + uint32_t absPtr = ptr; + if (IS_SEGMENTED(ptr)) { + auto seg = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(ptr)); + if (!seg.has_value()) { + return std::nullopt; + } + absPtr = seg.value() + SEGMENT_OFFSET(ptr); + } + + for (const auto& type : vtxTypes) { + auto decs = Companion::Instance->GetNodesByType(type); + if (!decs.has_value()) continue; + + for (auto& dec : decs.value()) { + auto [name, node] = dec; + + if (type == "OOT:ARRAY") { + auto arrayType = GetSafeNode(node, "array_type", ""); + if (arrayType != "VTX") continue; + } + + auto offset = GetSafeNode(node, "offset"); + auto count = GetSafeNode(node, "count"); + auto end = ALIGN16(count * 16); // sizeof(N64Vtx_t) + + uint32_t absOffset = offset; + if (IS_SEGMENTED(offset)) { + auto seg = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(offset)); + if (!seg.has_value()) continue; + absOffset = seg.value() + SEGMENT_OFFSET(offset); + } + + if (absPtr > absOffset && absPtr < absOffset + end) { + return std::make_tuple(GetSafeNode(node, "symbol", name), node); + } + } + } + + return std::nullopt; +} + +std::optional Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + if (Companion::Instance->GetGBIMinorVersion() != GBIMinorVersion::OoT) return std::nullopt; + + auto cmds = std::static_pointer_cast(raw)->mGfxs; + auto writer = LUS::BinaryWriter(); + + BaseExporter::WriteHeader(writer, Torch::ResourceType::DisplayList, 0); + + writer.Write((int8_t)GBIVersion::f3dex2); + + while (writer.GetBaseAddress() % 8 != 0) + writer.Write(static_cast(0xFF)); + + auto bhash = CRC64((*replacement).c_str()); + writer.Write(static_cast((G_MARKER << 24))); + writer.Write(0xBEEFBEEF); + writer.Write(static_cast(bhash >> 32)); + writer.Write(static_cast(bhash & 0xFFFFFFFF)); + + for (size_t i = 0; i < cmds.size(); i += 2) { + auto w0 = cmds[i]; + auto w1 = cmds[i + 1]; + uint8_t opcode = w0 >> 24; + + // gSunDL VTX override — writes all words, skip rest of iteration + if (opcode == F3DEX2_VTX && ExportGSunDLVtx(w0, w1, writer, replacement)) { + continue; + } + + if (opcode == F3DEX2_VTX) { + ExportVtx(w0, w1, writer, replacement); + } + + if (opcode == F3DEX2_DL) { + ExportDL(w0, w1, writer); + } + + if (opcode == F3DEX2_MOVEMEM) { + ExportMoveMem(w0, w1, writer); + } + + if (opcode == F3DEX2_SETTIMG) { + ExportSetTImg(w0, w1, writer, replacement); + } + + if (opcode == F3DEX2_MTX) { + ExportMtx(w0, w1, writer); + } + + // RDPHALF_1 before BRANCH_Z — zero it out, BRANCH_Z writes both + if (opcode == F3DEX2_RDPHALF_1 && i + 2 < cmds.size()) { + uint8_t nextOpcode = cmds[i + 2] >> 24; + if (nextOpcode == F3DEX2_BRANCH_Z) { + writer.Write(static_cast(0)); + writer.Write(static_cast(0)); + continue; + } + } + + // BRANCH_Z — writes all words, skip rest of iteration + if (opcode == F3DEX2_BRANCH_Z && ExportBranchZ(w0, w1, i, cmds, writer)) { + continue; + } + + // gSunDL texture format fixups (modifies w0/w1 in place) + ExportGSunDLTextureFixup(opcode, w0, w1, replacement); + + // General opcode fixups (modifies w0/w1 in place) + ExportOpcodeFixups(opcode, w0, w1); + + writer.Write(w0); + writer.Write(w1); + } + + writer.Finish(write); + return ExportResult(std::nullopt); +} + +std::optional>> Parse( + std::vector& raw_buffer, YAML::Node& node) { + if (Companion::Instance->GetGBIMinorVersion() != GBIMinorVersion::OoT) return std::nullopt; + + auto count = GetSafeNode(node, "count", -1); + auto [_, segment] = Decompressor::AutoDecode(node, raw_buffer); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + std::vector gfxs; + auto processing = true; + size_t length = 0; + + while (processing) { + auto w0 = reader.ReadUInt32(); + auto w1 = reader.ReadUInt32(); + + uint8_t opcode = w0 >> 24; + + if (opcode == F3DEX2_ENDDL) { + processing = false; + } + + if (opcode == F3DEX2_DL) { + if (C0(16, 1) == G_DL_NO_PUSH) { + SPDLOG_INFO("Branch List Command Found"); + processing = false; + } + ParseDL(w0, w1, node); + } + + if (opcode == F3DEX2_MOVEMEM) { + ParseMoveMem(w0, w1); + } + + if (opcode == F3DEX2_RDPHALF_1) { + ParseRdpHalf1(w0, w1, node, raw_buffer); + } + + if (opcode == F3DEX2_MTX) { + ParseMtx(w1, node); + } + + if (opcode == F3DEX2_VTX) { + uint32_t nvtx = C0(12, 8); + ParseVtx(w0, w1, nvtx, node, raw_buffer); + } + + if (count != -1 && length++ >= count) { + break; + } + + gfxs.push_back(w0); + gfxs.push_back(w1); + } + + FlushVtx(node); + + return std::make_optional(std::make_optional( + std::static_pointer_cast(std::make_shared(gfxs)))); +} + +} // namespace DListHelpers +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTDListHelpers.h b/src/factories/oot/OoTDListHelpers.h new file mode 100644 index 00000000..d01fa1af --- /dev/null +++ b/src/factories/oot/OoTDListHelpers.h @@ -0,0 +1,28 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include +#include +#include +#include + +namespace OoT { +namespace DListHelpers { + +// OoT replacement for SearchVtx. Returns result if handled, nullopt to fall through to main. +std::optional> SearchVtx(uint32_t ptr); + +// OoT replacement for DListBinaryExporter::Export. Returns result if handled, nullopt to fall through to main. +std::optional Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement); + +// OoT replacement for DListFactory::parse. Returns result if handled, nullopt to fall through to main. +std::optional>> Parse( + std::vector& raw_buffer, YAML::Node& node); + +} // namespace DListHelpers +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTLimbFactory.cpp b/src/factories/oot/OoTLimbFactory.cpp new file mode 100644 index 00000000..8f48fe44 --- /dev/null +++ b/src/factories/oot/OoTLimbFactory.cpp @@ -0,0 +1,276 @@ +#ifdef OOT_SUPPORT + +#include "OoTLimbFactory.h" +#include "OoTSkeletonTypes.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +#include +#include + +namespace OoT { + +size_t OoTLimbFactory::GetLimbDataSize(OoTLimbType type) { + switch (type) { + case OoTLimbType::LOD: + case OoTLimbType::Skin: return 0x10; + case OoTLimbType::Legacy: return 0x20; + default: return 0x0C; + } +} + +void OoTLimbFactory::ParseStandardLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol) { + ParseLimbHeader(reader, limb); + uint32_t dListAddr = reader.ReadUInt32(); + limb.dListPtr = ResolveGfxPointer(dListAddr, symbol, "DL"); +} + +void OoTLimbFactory::ParseLODLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol) { + ParseLimbHeader(reader, limb); + uint32_t dListAddr = reader.ReadUInt32(); + uint32_t dList2Addr = reader.ReadUInt32(); + limb.dList2Ptr = ResolveGfxPointer(dList2Addr, symbol, "FarDL"); + limb.dListPtr = ResolveGfxPointer(dListAddr, symbol, "DL"); +} + +void OoTLimbFactory::ParseSkinTransformations(std::vector& buffer, uint32_t addr, uint16_t count, + std::vector& out) { + if (addr == 0 || count == 0) return; + + auto raw = Decompressor::AutoDecode(addr, count * 0x0A, buffer); + LUS::BinaryReader reader(raw.segment.data, raw.segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + for (uint16_t t = 0; t < count; t++) { + OoTSkinTransformation st; + st.limbIndex = reader.ReadUByte(); + reader.ReadUByte(); // padding + st.x = reader.ReadInt16(); + st.y = reader.ReadInt16(); + st.z = reader.ReadInt16(); + st.scale = reader.ReadUByte(); + reader.ReadUByte(); // padding + out.push_back(st); + } +} + +void OoTLimbFactory::ParseSkinVertices(std::vector& buffer, uint32_t addr, uint16_t count, + std::vector& out) { + if (addr == 0 || count == 0) return; + + auto raw = Decompressor::AutoDecode(addr, count * 0x0A, buffer); + LUS::BinaryReader reader(raw.segment.data, raw.segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + for (uint16_t v = 0; v < count; v++) { + OoTSkinVertex sv; + sv.index = reader.ReadUInt16(); + sv.s = reader.ReadInt16(); + sv.t = reader.ReadInt16(); + sv.normX = reader.ReadInt8(); + sv.normY = reader.ReadInt8(); + sv.normZ = reader.ReadInt8(); + sv.alpha = reader.ReadUByte(); + out.push_back(sv); + } +} + +void OoTLimbFactory::ParseAnimatedSkinData(std::vector& buffer, uint32_t skinSegmentAddr, + OoTLimbData& limb, const std::string& symbol) { + // Null pointer means no animated skin data + if (skinSegmentAddr == 0) return; + + auto skinRaw = Decompressor::AutoDecode(skinSegmentAddr, 0x0C, buffer); + LUS::BinaryReader skinReader(skinRaw.segment.data, skinRaw.segment.size); + skinReader.SetEndianness(Torch::Endianness::Big); + + limb.skinAnimData.totalVtxCount = skinReader.ReadUInt16(); + uint16_t limbModifCount = skinReader.ReadUInt16(); + uint32_t limbModifAddr = Companion::Instance->PatchVirtualAddr(skinReader.ReadUInt32()); + uint32_t skinDListAddr = Companion::Instance->PatchVirtualAddr(skinReader.ReadUInt32()); + + limb.skinVtxCnt = limb.skinAnimData.totalVtxCount; + limb.skinAnimData.dlist = ResolveGfxPointer(skinDListAddr, symbol, "SkinLimbDL"); + + if (limbModifAddr == 0 || limbModifCount == 0) return; + + auto modifRaw = Decompressor::AutoDecode(limbModifAddr, limbModifCount * 0x10, buffer); + LUS::BinaryReader modifReader(modifRaw.segment.data, modifRaw.segment.size); + modifReader.SetEndianness(Torch::Endianness::Big); + + for (uint16_t m = 0; m < limbModifCount; m++) { + OoTSkinLimbModif modif; + uint16_t vtxCount = modifReader.ReadUInt16(); + uint16_t transformCount = modifReader.ReadUInt16(); + modif.unk_4 = modifReader.ReadUInt16(); + modifReader.ReadUInt16(); // padding + uint32_t skinVerticesAddr = Companion::Instance->PatchVirtualAddr(modifReader.ReadUInt32()); + uint32_t limbTransAddr = Companion::Instance->PatchVirtualAddr(modifReader.ReadUInt32()); + + ParseSkinVertices(buffer, skinVerticesAddr, vtxCount, modif.skinVertices); + + ParseSkinTransformations(buffer, limbTransAddr, transformCount, modif.limbTransformations); + + limb.skinAnimData.limbModifications.push_back(modif); + } +} + +void OoTLimbFactory::ParseSkinLimb(LUS::BinaryReader& reader, std::vector& buffer, + OoTLimbData& limb, const std::string& symbol) { + ParseLimbHeader(reader, limb); + limb.skinSegmentType = static_cast(reader.ReadInt32()); + uint32_t skinSegmentAddr = reader.ReadUInt32(); + + skinSegmentAddr = Companion::Instance->PatchVirtualAddr(skinSegmentAddr); + if (limb.skinSegmentType == OoTLimbSkinType::SkinType_Normal) { + limb.skinDList = ResolveGfxPointer(skinSegmentAddr, symbol, "DL"); + return; + } + + if (limb.skinSegmentType == OoTLimbSkinType::SkinType_Animated) { + ParseAnimatedSkinData(buffer, skinSegmentAddr, limb, symbol); + } +} + +void OoTLimbFactory::ParseLimbHeader(LUS::BinaryReader& reader, OoTLimbData& limb) { + limb.transX = reader.ReadInt16(); + limb.transY = reader.ReadInt16(); + limb.transZ = reader.ReadInt16(); + limb.childIndex = reader.ReadUByte(); + limb.siblingIndex = reader.ReadUByte(); +} + +void OoTLimbFactory::ParseCurveLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol) { + limb.childIndex = reader.ReadUByte(); + limb.siblingIndex = reader.ReadUByte(); + reader.ReadUInt16(); // padding + uint32_t dListAddr = reader.ReadUInt32(); + uint32_t dList2Addr = reader.ReadUInt32(); + limb.dListPtr = ResolveGfxPointer(dListAddr, symbol, "CurveDL"); + limb.dList2Ptr = ResolveGfxPointer(dList2Addr, symbol, "Curve2DL"); +} + +void OoTLimbFactory::ParseLegacyLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol) { + uint32_t dListAddr = reader.ReadUInt32(); + limb.dListPtr = ResolveGfxPointer(dListAddr, symbol, "DL"); + limb.legTransX = reader.ReadFloat(); + limb.legTransY = reader.ReadFloat(); + limb.legTransZ = reader.ReadFloat(); + limb.rotX = reader.ReadUInt16(); + limb.rotY = reader.ReadUInt16(); + limb.rotZ = reader.ReadUInt16(); + reader.ReadUInt16(); // padding + uint32_t childAddr = reader.ReadUInt32(); + uint32_t siblingAddr = reader.ReadUInt32(); + limb.childPtr = ResolvePointer(childAddr); + limb.siblingPtr = ResolvePointer(siblingAddr); +} + +std::optional> OoTLimbFactory::parse(std::vector& buffer, YAML::Node& node) { + auto limbTypeStr = GetSafeNode(node, "limb_type"); + auto limbType = ParseLimbType(limbTypeStr); + if (limbType == OoTLimbType::Invalid) { + return std::nullopt; + } + + auto [_, segment] = Decompressor::AutoDecode(node, buffer, GetLimbDataSize(limbType)); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + auto limb = std::make_shared(); + limb->limbType = limbType; + auto symbol = GetSafeNode(node, "symbol"); + + if (limbType == OoTLimbType::Curve) { + ParseCurveLimb(reader, *limb, symbol); + return limb; + } + + if (limbType == OoTLimbType::Legacy) { + ParseLegacyLimb(reader, *limb, symbol); + return limb; + } + + if (limbType == OoTLimbType::Standard) { + ParseStandardLimb(reader, *limb, symbol); + return limb; + } + + if (limbType == OoTLimbType::LOD) { + ParseLODLimb(reader, *limb, symbol); + return limb; + } + + if (limbType == OoTLimbType::Skin) { + ParseSkinLimb(reader, buffer, *limb, symbol); + return limb; + } + + // Should never reach here — all limb types are handled above + return std::nullopt; +} + +ExportResult OoTLimbBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto limb = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTSkeletonLimb, 0); + + writer.Write(static_cast(limb->limbType)); + writer.Write(static_cast(limb->skinSegmentType)); + writer.Write(limb->skinDList); + writer.Write(limb->skinVtxCnt); + writer.Write(static_cast(limb->skinAnimData.limbModifications.size())); + + for (auto& modif : limb->skinAnimData.limbModifications) { + writer.Write(modif.unk_4); + + writer.Write(static_cast(modif.skinVertices.size())); + for (auto& sv : modif.skinVertices) { + writer.Write(sv.index); + writer.Write(sv.s); + writer.Write(sv.t); + writer.Write(sv.normX); + writer.Write(sv.normY); + writer.Write(sv.normZ); + writer.Write(sv.alpha); + } + + writer.Write(static_cast(modif.limbTransformations.size())); + for (auto& st : modif.limbTransformations) { + writer.Write(st.limbIndex); + writer.Write(st.x); + writer.Write(st.y); + writer.Write(st.z); + writer.Write(st.scale); + } + } + + writer.Write(limb->skinAnimData.dlist); + writer.Write(limb->legTransX); + writer.Write(limb->legTransY); + writer.Write(limb->legTransZ); + writer.Write(limb->rotX); + writer.Write(limb->rotY); + writer.Write(limb->rotZ); + writer.Write(limb->childPtr); + writer.Write(limb->siblingPtr); + writer.Write(limb->dListPtr); + writer.Write(limb->dList2Ptr); + writer.Write(limb->transX); + writer.Write(limb->transY); + writer.Write(limb->transZ); + writer.Write(limb->childIndex); + writer.Write(limb->siblingIndex); + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTLimbFactory.h b/src/factories/oot/OoTLimbFactory.h new file mode 100644 index 00000000..e99f9d16 --- /dev/null +++ b/src/factories/oot/OoTLimbFactory.h @@ -0,0 +1,43 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "OoTSkeletonTypes.h" + +namespace OoT { + +class OoTLimbBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTLimbFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTLimbBinaryExporter) + }; + } + +private: + static size_t GetLimbDataSize(OoTLimbType type); + void ParseLimbHeader(LUS::BinaryReader& reader, OoTLimbData& limb); + void ParseCurveLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol); + void ParseLegacyLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol); + void ParseStandardLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol); + void ParseLODLimb(LUS::BinaryReader& reader, OoTLimbData& limb, const std::string& symbol); + void ParseSkinLimb(LUS::BinaryReader& reader, std::vector& buffer, + OoTLimbData& limb, const std::string& symbol); + void ParseAnimatedSkinData(std::vector& buffer, uint32_t skinSegmentAddr, + OoTLimbData& limb, const std::string& symbol); + void ParseSkinVertices(std::vector& buffer, uint32_t addr, uint16_t count, + std::vector& out); + void ParseSkinTransformations(std::vector& buffer, uint32_t addr, uint16_t count, + std::vector& out); +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTMtxFactory.cpp b/src/factories/oot/OoTMtxFactory.cpp new file mode 100644 index 00000000..d76a7cf4 --- /dev/null +++ b/src/factories/oot/OoTMtxFactory.cpp @@ -0,0 +1,42 @@ +#ifdef OOT_SUPPORT + +#include "OoTMtxFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +std::optional> OoTMtxFactory::parse(std::vector& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer, 64); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + // Read 16 raw int32 values matching ZAPDTR's ZMtx::ParseRawData format + std::array rawInts; + for (size_t i = 0; i < 16; i++) { + rawInts[i] = reader.ReadInt32(); + } + + return std::make_shared(rawInts); +} + +ExportResult OoTMtxBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto data = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::Matrix, 0); + + for (size_t i = 0; i < 16; i++) { + writer.Write(data->rawInts[i]); + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTMtxFactory.h b/src/factories/oot/OoTMtxFactory.h new file mode 100644 index 00000000..9a587889 --- /dev/null +++ b/src/factories/oot/OoTMtxFactory.h @@ -0,0 +1,33 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include + +namespace OoT { + +class OoTMtxData : public IParsedData { +public: + std::array rawInts; + explicit OoTMtxData(std::array data) : rawInts(data) {} +}; + +class OoTMtxBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTMtxFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTMtxBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTPathFactory.cpp b/src/factories/oot/OoTPathFactory.cpp new file mode 100644 index 00000000..4c4565e7 --- /dev/null +++ b/src/factories/oot/OoTPathFactory.cpp @@ -0,0 +1,53 @@ +#ifdef OOT_SUPPORT + +#include "OoTPathFactory.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" + +namespace OoT { + +class OoTPathData : public IParsedData { +public: + std::vector mBinary; + OoTPathData(std::vector data) : mBinary(std::move(data)) {} +}; + +std::optional> OoTPathFactory::parse(std::vector& buffer, YAML::Node& node) { + auto offset = GetSafeNode(node, "offset"); + uint32_t numPaths = node["num_paths"] ? node["num_paths"].as() : 1; + + // Each path entry is 8 bytes: numPoints (u8), padding (3 bytes), pointsAddr (u32) + auto pathReader = ReadSubArray(buffer, offset, numPaths * 8); + + // {numPoints, pointsAddr} + std::vector> pathways; + + for (uint32_t i = 0; i < numPaths; i++) { + uint8_t numPoints = pathReader.ReadUByte(); + pathReader.ReadUByte(); // padding + pathReader.ReadUByte(); + pathReader.ReadUByte(); + uint32_t pointsAddr = pathReader.ReadUInt32(); + + if (pointsAddr == 0) break; + + pathways.push_back({numPoints, pointsAddr}); + } + + if (pathways.empty()) return std::nullopt; + + auto data = SerializePathways(buffer, pathways, pathways.size(), 1); + return std::make_shared(std::move(data)); +} + +ExportResult OoTPathBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto path = std::static_pointer_cast(raw); + write.write(path->mBinary.data(), path->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTPathFactory.h b/src/factories/oot/OoTPathFactory.h new file mode 100644 index 00000000..665f211b --- /dev/null +++ b/src/factories/oot/OoTPathFactory.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" + +namespace OoT { + +class OoTPathBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTPathFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTPathBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTPlayerAnimationFactory.cpp b/src/factories/oot/OoTPlayerAnimationFactory.cpp new file mode 100644 index 00000000..0a2f53c8 --- /dev/null +++ b/src/factories/oot/OoTPlayerAnimationFactory.cpp @@ -0,0 +1,95 @@ +#ifdef OOT_SUPPORT + +#include "OoTPlayerAnimationFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +// ==================== OoT Player Animation Header Factory ==================== + +std::optional> OoTPlayerAnimationHeaderFactory::parse(std::vector& buffer, YAML::Node& node) { + // ROM layout: PlayerAnimationHeader (8 bytes) + // +0x00: int16 frameCount + // +0x02: int16 padding + // +0x04: uint32 segPtr (segment 7 pointer to link_animetion data) + auto [_, segment] = Decompressor::AutoDecode(node, buffer, 0x08); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + auto anim = std::make_shared(); + anim->frameCount = reader.ReadInt16(); + reader.ReadInt16(); // padding + uint32_t segPtr = reader.ReadUInt32(); + + // Resolve the segment pointer to a path in link_animetion + auto path = Companion::Instance->GetStringByAddr(segPtr); + if (path.has_value()) { + anim->animDataPath = path.value(); + } else { + SPDLOG_WARN("PlayerAnimation: Could not resolve segment pointer 0x{:08X}", segPtr); + anim->animDataPath = ""; + } + + return anim; +} + +ExportResult OoTPlayerAnimationHeaderBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto anim = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTAnimation, 0); + + writer.Write(static_cast(OoTAnimationType::Link)); + writer.Write(anim->frameCount); + writer.Write("__OTR__" + anim->animDataPath); + + writer.Finish(write); + return std::nullopt; +} + +// ==================== OoT Player Animation Data Factory ==================== + +std::optional> OoTPlayerAnimationDataFactory::parse(std::vector& buffer, YAML::Node& node) { + // Player animation data is a flat array of int16 limb rotation values. + // Size = (6 * 22 + 2) * frameCount bytes = 134 bytes per frame. + int32_t frameCount = GetSafeNode(node, "frame_count"); + uint32_t dataSize = (6 * 22 + 2) * frameCount; + + auto [_, segment] = Decompressor::AutoDecode(node, buffer, dataSize); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + auto anim = std::make_shared(); + uint32_t numEntries = dataSize / 2; + anim->limbRotData.reserve(numEntries); + for (uint32_t i = 0; i < numEntries; i++) { + anim->limbRotData.push_back(reader.ReadInt16()); + } + + return anim; +} + +ExportResult OoTPlayerAnimationDataBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto anim = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTPlayerAnimation, 0); + + writer.Write(static_cast(anim->limbRotData.size())); + for (auto& val : anim->limbRotData) { + writer.Write(val); + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTPlayerAnimationFactory.h b/src/factories/oot/OoTPlayerAnimationFactory.h new file mode 100644 index 00000000..4ca96221 --- /dev/null +++ b/src/factories/oot/OoTPlayerAnimationFactory.h @@ -0,0 +1,55 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "OoTAnimationTypes.h" +#include +#include + +namespace OoT { + +class OoTPlayerAnimationHeaderData : public IParsedData { +public: + int16_t frameCount; + std::string animDataPath; +}; + +class OoTPlayerAnimationHeaderBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTPlayerAnimationHeaderFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTPlayerAnimationHeaderBinaryExporter) + }; + } +}; + +class OoTPlayerAnimationData : public IParsedData { +public: + std::vector limbRotData; +}; + +class OoTPlayerAnimationDataBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTPlayerAnimationDataFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTPlayerAnimationDataBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSceneCommandWriter.cpp b/src/factories/oot/OoTSceneCommandWriter.cpp new file mode 100644 index 00000000..eed0b209 --- /dev/null +++ b/src/factories/oot/OoTSceneCommandWriter.cpp @@ -0,0 +1,686 @@ +#ifdef OOT_SUPPORT + +#include "OoTSceneCommandWriter.h" +#include "AliasManager.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" +#include "factories/DisplayListFactory.h" +#include +#include + +namespace OoT { + +SceneCommand SceneCommandWriter::Write(uint32_t w0, uint32_t w1, SceneWriteContext& ctx) { + uint8_t cmdID = (w0 >> 24) & 0xFF; + uint8_t cmdArg1 = (w0 >> 16) & 0xFF; + uint32_t cmdArg2 = w1; + + SceneCommand cmd; + cmd.cmdID = cmdID; + + LUS::BinaryWriter cmdWriter; + + switch (cmdID) { + case SetWind: { + WriteSetWind(cmdWriter, w0, w1); + break; + } + case SetTimeSettings: { + WriteSetTimeSettings(cmdWriter, w1); + break; + } + case SetSkyboxModifier: { + WriteSetSkyboxModifier(cmdWriter, w1); + break; + } + case SetEchoSettings: { + WriteSetEchoSettings(cmdWriter, w1); + break; + } + case SetSoundSettings: { + WriteSetSoundSettings(cmdWriter, cmdArg1, w1); + break; + } + case SetSkyboxSettings: { + WriteSetSkyboxSettings(cmdWriter, cmdArg1, w1); + break; + } + case SetRoomBehavior: { + WriteSetRoomBehavior(cmdWriter, cmdArg1, cmdArg2); + break; + } + case SetCameraSettings: { + WriteSetCameraSettings(cmdWriter, cmdArg1, cmdArg2); + break; + } + case SetSpecialObjects: { + WriteSetSpecialObjects(cmdWriter, cmdArg1, w1); + break; + } + case SetStartPositionList: { + WriteSetStartPositionList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetActorList: { + WriteSetActorList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetTransitionActorList: { + WriteSetTransitionActorList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetEntranceList: { + WriteSetEntranceList(cmdWriter, cmdArg2, ctx); + break; + } + case SetObjectList: { + WriteSetObjectList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetLightingSettings: { + WriteSetLightingSettings(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetLightList: { + WriteSetLightList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetExitList: { + WriteSetExitList(cmdWriter, cmdArg2, ctx); + break; + } + case SetRoomList: { + WriteSetRoomList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetCollisionHeader: { + WriteSetCollisionHeader(cmdWriter, cmdArg2); + break; + } + case SetMesh: { + WriteSetMesh(cmdWriter, cmdArg2, ctx); + break; + } + case SetCsCamera: { + WriteSetCsCamera(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetPathways: { + WriteSetPathways(cmdWriter, cmdArg2, ctx); + break; + } + case SetCutscenes: { + WriteSetCutscenes(cmdWriter, cmdArg2, ctx); + break; + } + case SetAlternateHeaders: { + WriteSetAlternateHeaders(cmdWriter, cmdArg2, ctx); + break; + } + case EndMarker: { + break; + } + default: { + SPDLOG_WARN("Scene: Unhandled command 0x{:02X}", cmdID); + break; + } + } + + std::stringstream ss; + cmdWriter.Finish(ss); + std::string str = ss.str(); + cmd.data = std::vector(str.begin(), str.end()); + return cmd; +} + +void SceneCommandWriter::WriteSetStartPositionList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 16); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadInt16()); // actorNum + w.Write(sub.ReadInt16()); // posX + w.Write(sub.ReadInt16()); // posY + w.Write(sub.ReadInt16()); // posZ + w.Write(sub.ReadInt16()); // rotX + w.Write(sub.ReadInt16()); // rotY + w.Write(sub.ReadInt16()); // rotZ + w.Write(sub.ReadInt16()); // params + } +} + +void SceneCommandWriter::WriteSetActorList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 16); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadInt16()); // actorNum + w.Write(sub.ReadInt16()); // posX + w.Write(sub.ReadInt16()); // posY + w.Write(sub.ReadInt16()); // posZ + w.Write(sub.ReadInt16()); // rotX + w.Write(sub.ReadInt16()); // rotY + w.Write(sub.ReadInt16()); // rotZ + w.Write(sub.ReadInt16()); // params + } + + // Create 0-byte ActorEntry companion file (matches OTRExporter behavior). + if (cmdArg2 != 0 && count > 0) { + uint32_t actorOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(cmdArg2)); + std::string actorSymbol = MakeAssetName(ctx.baseName, "ActorEntry", actorOffset); + Companion::Instance->RegisterCompanionFile(actorSymbol, std::vector{}); + } +} + +void SceneCommandWriter::WriteSetWind(LUS::BinaryWriter& w, uint32_t w0, uint32_t w1) { + w.Write(static_cast((w1 >> 24) & 0xFF)); // windWest + w.Write(static_cast((w1 >> 16) & 0xFF)); // windVertical + w.Write(static_cast((w1 >> 8) & 0xFF)); // windSouth + w.Write(static_cast(w1 & 0xFF)); // clothFlappingStrength +} + +void SceneCommandWriter::WriteSetTimeSettings(LUS::BinaryWriter& w, uint32_t w1) { + w.Write(static_cast((w1 >> 24) & 0xFF)); // hour + w.Write(static_cast((w1 >> 16) & 0xFF)); // min + w.Write(static_cast((w1 >> 8) & 0xFF)); // unk +} + +void SceneCommandWriter::WriteSetSkyboxModifier(LUS::BinaryWriter& w, uint32_t w1) { + w.Write(static_cast((w1 >> 24) & 0xFF)); // disableSky + w.Write(static_cast((w1 >> 16) & 0xFF)); // disableSunMoon +} + +void SceneCommandWriter::WriteSetEchoSettings(LUS::BinaryWriter& w, uint32_t w1) { + w.Write(static_cast(w1 & 0xFF)); // echo +} + +void SceneCommandWriter::WriteSetSoundSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1) { + w.Write(static_cast(cmdArg1)); // reverb + w.Write(static_cast((w1 >> 8) & 0xFF)); // nightTimeSFX + w.Write(static_cast(w1 & 0xFF)); // musicSequence +} + +void SceneCommandWriter::WriteSetSkyboxSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1) { + w.Write(static_cast(cmdArg1)); // unk1 + w.Write(static_cast((w1 >> 24) & 0xFF)); // skyboxNumber + w.Write(static_cast((w1 >> 16) & 0xFF)); // cloudsType + w.Write(static_cast((w1 >> 8) & 0xFF)); // isIndoors +} + +void SceneCommandWriter::WriteSetRoomBehavior(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2) { + w.Write(static_cast(cmdArg1)); // gameplayFlags + w.Write(cmdArg2); // gameplayFlags2 +} + +void SceneCommandWriter::WriteSetCameraSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2) { + w.Write(static_cast(cmdArg1)); // cameraMovement + w.Write(cmdArg2); // mapHighlight +} + +void SceneCommandWriter::WriteSetSpecialObjects(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1) { + w.Write(static_cast(cmdArg1)); // elfMessage + w.Write(static_cast(w1 & 0xFFFF)); // globalObject +} + +void SceneCommandWriter::WriteSetTransitionActorList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 16); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadInt8()); // frontObjectRoom + w.Write(sub.ReadInt8()); // frontTransitionReaction + w.Write(sub.ReadInt8()); // backObjectRoom + w.Write(sub.ReadInt8()); // backTransitionReaction + w.Write(sub.ReadInt16()); // actorNum + w.Write(sub.ReadInt16()); // posX + w.Write(sub.ReadInt16()); // posY + w.Write(sub.ReadInt16()); // posZ + w.Write(sub.ReadInt16()); // rotY + w.Write(sub.ReadInt16()); // initVar + } +} + +void SceneCommandWriter::WriteSetEntranceList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + uint32_t count = GetNeighborSize(ctx.knownAddrs, cmdArg2, 2); + if (count == 0) count = 1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 2); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadUByte()); // startPositionIndex + w.Write(sub.ReadUByte()); // roomToLoad + } +} + +void SceneCommandWriter::WriteSetObjectList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 2); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadUInt16()); + } +} + +void SceneCommandWriter::WriteSetLightingSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 22); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + for (int j = 0; j < 18; j++) { + w.Write(sub.ReadUByte()); + } + w.Write(sub.ReadUInt16()); // unk + w.Write(sub.ReadUInt16()); // drawDistance + } +} + +void SceneCommandWriter::WriteSetLightList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 14); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadUByte()); // type + w.Write(sub.ReadInt16()); // x + w.Write(sub.ReadInt16()); // y + w.Write(sub.ReadInt16()); // z + w.Write(sub.ReadUByte()); // r + w.Write(sub.ReadUByte()); // g + w.Write(sub.ReadUByte()); // b + w.Write(sub.ReadUByte()); // drawGlow + w.Write(sub.ReadInt16()); // radius + } +} + +void SceneCommandWriter::WriteSetExitList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + uint32_t count = GetNeighborSize(ctx.knownAddrs, cmdArg2, 2); + if (count == 0) count = 1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 2); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + w.Write(sub.ReadUInt16()); + } +} + +void SceneCommandWriter::WriteSetRoomList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 8); + w.Write(static_cast(count)); + + // Derive scene base name from current directory (e.g. "scenes/nonmq/bdan_scene" → "bdan") + std::string sceneBase; + auto lastSlash = ctx.currentDir.rfind('/'); + sceneBase = (lastSlash != std::string::npos) ? ctx.currentDir.substr(lastSlash + 1) : ctx.currentDir; + std::string roomBase = sceneBase; + if (roomBase.size() > 6 && roomBase.substr(roomBase.size() - 6) == "_scene") { + roomBase = roomBase.substr(0, roomBase.size() - 6); + } + + for (uint32_t i = 0; i < count; i++) { + std::string roomName = ctx.currentDir + "/" + roomBase + "_room_" + std::to_string(i); + w.Write(roomName); + w.Write(sub.ReadUInt32()); // virtualAddressStart + w.Write(sub.ReadUInt32()); // virtualAddressEnd + } +} + +void SceneCommandWriter::WriteSetCollisionHeader(LUS::BinaryWriter& w, uint32_t cmdArg2) { + uint32_t colAddr = Companion::Instance->PatchVirtualAddr(cmdArg2); + auto resolved = ResolvePointer(cmdArg2); + if (resolved.empty()) { + SPDLOG_WARN("Undeclared collision at 0x{:08X} — YAML enrichment incomplete", colAddr); + } + w.Write(resolved); +} + +void SceneCommandWriter::WriteSetMesh(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + uint8_t meshData = 0; + auto meshReader = ReadSubArray(ctx.buffer, cmdArg2, 12); + uint8_t meshHeaderType = meshReader.ReadUByte(); + + w.Write(meshData); + w.Write(meshHeaderType); + + DeferredVtx::BeginDefer(); + + if (meshHeaderType == 0 || meshHeaderType == 2) { + uint32_t num = meshReader.ReadUByte(); + meshReader.ReadUInt16(); // padding + uint32_t polyStart = meshReader.ReadUInt32(); + + w.Write(static_cast(num)); + + uint32_t entrySize = (meshHeaderType == 2) ? 16 : 8; + auto polyReader = ReadSubArray(ctx.buffer, polyStart, num * entrySize); + + for (uint32_t i = 0; i < num; i++) { + uint8_t polyType = 0; + int16_t cx = 0, cy = 0, cz = 0; + int16_t unk_06 = 0; + + if (meshHeaderType == 2) { + polyType = 2; + cx = polyReader.ReadInt16(); + cy = polyReader.ReadInt16(); + cz = polyReader.ReadInt16(); + unk_06 = polyReader.ReadInt16(); + } + + uint32_t opaAddr = polyReader.ReadUInt32(); + uint32_t xluAddr = polyReader.ReadUInt32(); + + w.Write(polyType); + if (polyType == 2) { + w.Write(cx); + w.Write(cy); + w.Write(cz); + w.Write(unk_06); + } + + std::string opaPath; + if (opaAddr != 0) { + uint32_t opaOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(opaAddr)); + std::string opaSymbol = MakeAssetName(ctx.entryName, "DL", opaOffset); + opaPath = ResolveGfxWithAlias(opaAddr, opaSymbol, ctx.currentDir); + } + w.Write(opaPath.empty() ? std::string("") : opaPath); + + std::string xluPath; + if (xluAddr != 0) { + uint32_t xluOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(xluAddr)); + std::string xluSymbol = MakeAssetName(ctx.entryName, "DL", xluOffset); + xluPath = ResolveGfxWithAlias(xluAddr, xluSymbol, ctx.currentDir); + } + w.Write(xluPath.empty() ? std::string("") : xluPath); + } + } else if (meshHeaderType == 1) { + uint8_t format = meshReader.ReadUByte(); + meshReader.ReadUInt16(); // padding + uint32_t polyDListAddr = meshReader.ReadUInt32(); + + w.Write(format); + + auto pdlReader = ReadSubArray(ctx.buffer, polyDListAddr, 8); + uint32_t opaAddr = pdlReader.ReadUInt32(); + uint32_t xluAddr = pdlReader.ReadUInt32(); + + std::string opaPath, xluPath; + if (opaAddr != 0) { + uint32_t opaOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(opaAddr)); + std::string opaSymbol = MakeAssetName(ctx.entryName, "DL", opaOffset); + opaPath = ResolveGfxWithAlias(opaAddr, opaSymbol, ctx.currentDir); + } + if (xluAddr != 0) { + uint32_t xluOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(xluAddr)); + std::string xluSymbol = MakeAssetName(ctx.entryName, "DL", xluOffset); + xluPath = ResolveGfxWithAlias(xluAddr, xluSymbol, ctx.currentDir); + } + w.Write(opaPath.empty() ? std::string("") : opaPath); + w.Write(xluPath.empty() ? std::string("") : xluPath); + + if (format == 2) { + auto mhReader = ReadSubArray(ctx.buffer, cmdArg2 + 0x08, 8); + uint32_t count = mhReader.ReadUByte(); + mhReader.ReadUByte(); mhReader.ReadUByte(); mhReader.ReadUByte(); + uint32_t multiAddr = mhReader.ReadUInt32(); + + w.Write(static_cast(count)); + + auto bgReader = ReadSubArray(ctx.buffer, multiAddr, count * 28); + for (uint32_t i = 0; i < count; i++) { + uint16_t unk_00 = bgReader.ReadUInt16(); + uint8_t id = bgReader.ReadUByte(); + bgReader.ReadUByte(); + uint32_t source = bgReader.ReadUInt32(); + uint32_t unk_0C = bgReader.ReadUInt32(); + uint32_t tlut = bgReader.ReadUInt32(); + uint16_t width = bgReader.ReadUInt16(); + uint16_t height = bgReader.ReadUInt16(); + uint8_t fmt = bgReader.ReadUByte(); + uint8_t siz = bgReader.ReadUByte(); + uint16_t mode0 = bgReader.ReadUInt16(); + uint16_t tlutCount = bgReader.ReadUInt16(); + bgReader.ReadUInt16(); + + w.Write(unk_00); + w.Write(id); + + uint32_t bgOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(source)); + std::string bgSymbol = MakeAssetName(ctx.baseName, "Background", bgOffset); + std::string bgPath = ResolvePointer(source); + if (bgPath.empty()) { + bgPath = ctx.currentDir + "/" + bgSymbol; + } + w.Write(bgPath); + + w.Write(unk_0C); w.Write(tlut); + w.Write(width); w.Write(height); + w.Write(fmt); w.Write(siz); + w.Write(mode0); w.Write(tlutCount); + + CreateBackgroundCompanion(ctx.buffer, source, bgSymbol); + } + } else { + w.Write(static_cast(1)); + w.Write(static_cast(0)); // unk_00 + w.Write(static_cast(0)); // id + + auto bgReader = ReadSubArray(ctx.buffer, cmdArg2 + 0x08, 24); + uint32_t source = bgReader.ReadUInt32(); + uint32_t unk_0C = bgReader.ReadUInt32(); + uint32_t tlut = bgReader.ReadUInt32(); + uint16_t width = bgReader.ReadUInt16(); + uint16_t height = bgReader.ReadUInt16(); + uint8_t fmt = bgReader.ReadUByte(); + uint8_t siz = bgReader.ReadUByte(); + uint16_t mode0 = bgReader.ReadUInt16(); + uint16_t tlutCount = bgReader.ReadUInt16(); + + uint32_t bgOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(source)); + std::string bgSymbol = MakeAssetName(ctx.baseName, "Background", bgOffset); + std::string bgPath = ResolvePointer(source); + if (bgPath.empty()) { + bgPath = ctx.currentDir + "/" + bgSymbol; + } + w.Write(bgPath); + + w.Write(unk_0C); w.Write(tlut); + w.Write(width); w.Write(height); + w.Write(fmt); w.Write(siz); + w.Write(mode0); w.Write(tlutCount); + + CreateBackgroundCompanion(ctx.buffer, source, bgSymbol); + } + + if (polyDListAddr != 0) { + w.Write(static_cast(meshHeaderType)); + w.Write(opaPath.empty() ? std::string("") : opaPath); + w.Write(xluPath.empty() ? std::string("") : xluPath); + } + } +} + +void SceneCommandWriter::WriteSetCsCamera(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + auto camReader = ReadSubArray(ctx.buffer, cmdArg2, 0x1000); + uint32_t numCameras = cmdArg1; + w.Write(numCameras); + + uint32_t firstPointsAddr = 0; + struct CamEntry { int16_t type; int16_t numPoints; uint32_t segOff; }; + std::vector cameras; + + for (uint32_t i = 0; i < numCameras; i++) { + CamEntry c; + c.type = camReader.ReadInt16(); + c.numPoints = camReader.ReadInt16(); + c.segOff = camReader.ReadUInt32(); + if (i == 0) firstPointsAddr = c.segOff; + cameras.push_back(c); + } + + for (auto& c : cameras) { + w.Write(c.type); + w.Write(c.numPoints); + + auto pointReader = ReadSubArray(ctx.buffer, c.segOff, c.numPoints * 6); + for (int16_t j = 0; j < c.numPoints; j++) { + w.Write(pointReader.ReadInt16()); + w.Write(pointReader.ReadInt16()); + w.Write(pointReader.ReadInt16()); + } + } +} + +void SceneCommandWriter::WriteSetPathways(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + uint8_t pathSeg = (cmdArg2 >> 24) & 0xFF; + uint32_t maxPaths = GetNeighborSize(ctx.knownAddrs, cmdArg2, 8); + if (maxPaths == 0) maxPaths = 256; + auto pathReader = ReadSubArray(ctx.buffer, cmdArg2, maxPaths * 8); + std::vector> pathways; + for (uint32_t i = 0; i < maxPaths; i++) { + uint8_t np = pathReader.ReadUByte(); + pathReader.ReadUByte(); pathReader.ReadUByte(); pathReader.ReadUByte(); + uint32_t ptsAddr = pathReader.ReadUInt32(); + if (ptsAddr == 0 || !IS_SEGMENTED(ptsAddr) || ((ptsAddr >> 24) & 0xFF) != pathSeg) { + break; + } + pathways.push_back({np, ptsAddr}); + } + if (pathways.empty()) pathways.push_back({0, 0}); + + auto existingPath = ResolvePointer(cmdArg2); + bool hasPreExistingResource = !existingPath.empty(); + + if (!hasPreExistingResource && ctx.isAltHeader && pathways.size() > 1) { + pathways.erase(pathways.begin() + 1, pathways.end()); + } + bool doubled = hasPreExistingResource && (pathways.size() > 1); + uint32_t writeCount = doubled ? pathways.size() * 2 : pathways.size(); + + w.Write(static_cast(writeCount)); + + uint32_t repeats = doubled ? 2 : 1; + for (uint32_t r = 0; r < repeats; r++) { + for (uint32_t i = 0; i < pathways.size(); i++) { + auto [np, ptsAddr] = pathways[i]; + uint32_t pointOffset = SEGMENT_OFFSET(ptsAddr); + std::string pathSymbol = MakeAssetName(ctx.baseName, "PathwayList", pointOffset); + std::string pathPath = ctx.currentDir + "/" + pathSymbol; + w.Write(pathPath); + } + } + + for (uint32_t i = 0; i < pathways.size(); i++) { + auto [np, ptsAddr] = pathways[i]; + uint32_t pointOffset = SEGMENT_OFFSET(ptsAddr); + std::string pathSymbol = MakeAssetName(ctx.baseName, "PathwayList", pointOffset); + auto pathData = SerializePathways(ctx.buffer, pathways, writeCount, repeats); + Companion::Instance->RegisterCompanionFile(pathSymbol, pathData); + } +} + +void SceneCommandWriter::WriteSetCutscenes(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + std::string csSymbol; + uint32_t csAddr = Companion::Instance->PatchVirtualAddr(cmdArg2); + auto resolved = ResolvePointer(cmdArg2); + + if (resolved.empty()) { + uint32_t csOffset = SEGMENT_OFFSET(csAddr); + csSymbol = MakeAssetName(ctx.entryName, "CutsceneData", csOffset); + resolved = ctx.currentDir + "/" + csSymbol; + } else { + csSymbol = resolved.substr(resolved.rfind('/') + 1); + } + w.Write(resolved); + + auto csData = CutsceneSerializer::Serialize(ctx.buffer, cmdArg2); + if (csData.empty()) { + SPDLOG_WARN("Scene: Skipping cutscene {} due to parse failure", csSymbol); + } + Companion::Instance->RegisterCompanionFile(csSymbol, csData); +} + +void SceneCommandWriter::WriteSetAlternateHeaders(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + uint32_t maxHeaders = GetNeighborSize(ctx.knownAddrs, cmdArg2, 4); + if (maxHeaders == 0) maxHeaders = 16; + auto hdrReader = ReadSubArray(ctx.buffer, cmdArg2, maxHeaders * 4); + std::vector headers; + for (uint32_t i = 0; i < maxHeaders; i++) { + uint32_t seg = hdrReader.ReadUInt32(); + headers.push_back(seg); + } + + w.Write(static_cast(headers.size())); + for (auto seg : headers) { + if (seg == 0) { + w.Write(std::string("")); + } else { + uint32_t offset = SEGMENT_OFFSET(seg); + std::string setSymbol = MakeAssetName(ctx.entryName, "Set", offset); + std::string setPath = ctx.currentDir + "/" + setSymbol; + w.Write(setPath); + + ctx.pendingAltHeaders.push_back({seg, setSymbol}); + } + } +} + +void SceneCommandWriter::CreateBackgroundCompanion(std::vector& buffer, uint32_t source, + const std::string& bgSymbol) { + if (source == 0) return; + uint32_t bgDataSize = 320 * 240 * 2; + auto bgDataReader = ReadSubArray(buffer, source, bgDataSize); + LUS::BinaryWriter bgWriter; + BaseExporter::WriteHeader(bgWriter, Torch::ResourceType::OoTBackground, 0); + bgWriter.Write(static_cast(bgDataSize)); + for (uint32_t b = 0; b < bgDataSize; b++) { + bgWriter.Write(bgDataReader.ReadUByte()); + } + std::stringstream bgSS; + bgWriter.Finish(bgSS); + std::string bgStr = bgSS.str(); + Companion::Instance->RegisterCompanionFile( + bgSymbol, std::vector(bgStr.begin(), bgStr.end())); +} + +std::string SceneCommandWriter::ResolveGfxPointer(uint32_t ptr, const std::string& symbol) { + if (ptr == 0) return ""; + ptr = Companion::Instance->PatchVirtualAddr(ptr); + auto result = Companion::Instance->GetStringByAddr(ptr); + if (result.has_value()) return result.value(); + + SPDLOG_WARN("Scene: Could not resolve GFX pointer 0x{:08X} ({}) — YAML enrichment incomplete", ptr, symbol); + return ""; +} + +std::string SceneCommandWriter::ResolveGfxWithAlias(uint32_t ptr, const std::string& symbol, + const std::string& currentDir) { + std::string path = ResolveGfxPointer(ptr, symbol); + if (path.empty()) return ""; + std::string expectedPath = currentDir + "/" + symbol; + if (path != expectedPath) { + AliasManager::Instance->Register(path, expectedPath); + } + return path; +} + +uint32_t SceneCommandWriter::GetNeighborSize(const std::set& knownAddrs, uint32_t segAddr, + uint32_t entrySize) { + uint32_t addr = SEGMENT_OFFSET(segAddr); + auto it = knownAddrs.upper_bound(addr); + if (it != knownAddrs.end()) { + return (*it - addr) / entrySize; + } + return 0; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSceneCommandWriter.h b/src/factories/oot/OoTSceneCommandWriter.h new file mode 100644 index 00000000..59d92539 --- /dev/null +++ b/src/factories/oot/OoTSceneCommandWriter.h @@ -0,0 +1,61 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "OoTSceneFactory.h" +#include "OoTSceneUtils.h" +#include + +namespace OoT { + +// Context passed through scene command serialization. +struct SceneWriteContext { + std::vector& buffer; + const std::set& knownAddrs; + const std::string& entryName; + const std::string& baseName; + const std::string& currentDir; + const std::string& assetType; + bool isAltHeader; + std::vector& pendingAltHeaders; +}; + +class SceneCommandWriter { +public: + SceneCommand Write(uint32_t w0, uint32_t w1, SceneWriteContext& ctx); + +private: + void WriteSetWind(LUS::BinaryWriter& w, uint32_t w0, uint32_t w1); + void WriteSetTimeSettings(LUS::BinaryWriter& w, uint32_t w1); + void WriteSetSkyboxModifier(LUS::BinaryWriter& w, uint32_t w1); + void WriteSetEchoSettings(LUS::BinaryWriter& w, uint32_t w1); + void WriteSetSoundSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1); + void WriteSetSkyboxSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1); + void WriteSetRoomBehavior(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2); + void WriteSetCameraSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2); + void WriteSetSpecialObjects(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1); + void WriteSetStartPositionList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetActorList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetTransitionActorList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetEntranceList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetObjectList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetLightingSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetLightList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetExitList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetRoomList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetCollisionHeader(LUS::BinaryWriter& w, uint32_t cmdArg2); + void WriteSetMesh(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetCsCamera(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetPathways(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetCutscenes(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetAlternateHeaders(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + + void CreateBackgroundCompanion(std::vector& buffer, uint32_t source, const std::string& bgSymbol); + std::string ResolveGfxPointer(uint32_t ptr, const std::string& symbol); + std::string ResolveGfxWithAlias(uint32_t ptr, const std::string& symbol, const std::string& currentDir); + uint32_t GetNeighborSize(const std::set& knownAddrs, uint32_t segAddr, uint32_t entrySize); +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSceneFactory.cpp b/src/factories/oot/OoTSceneFactory.cpp new file mode 100644 index 00000000..8f8d4918 --- /dev/null +++ b/src/factories/oot/OoTSceneFactory.cpp @@ -0,0 +1,164 @@ +#ifdef OOT_SUPPORT + +#include "OoTSceneFactory.h" +#include "OoTSceneCommandWriter.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" +#include "factories/DisplayListFactory.h" +#include + +namespace OoT { + +// Collect all known data offsets from scene commands for neighbor-based size inference. +// Mimics ZAPD's GetDeclarationSizeFromNeighbor(): the size of a variable-length +// list is determined by the distance to the next known data structure. +std::set OoTSceneFactory::CollectKnownAddresses(const std::vector>& rawCmds, + std::vector& buffer) { + std::set addrs; + for (auto& [w0, w1] : rawCmds) { + uint8_t id = (w0 >> 24) & 0xFF; + uint8_t arg1 = (w0 >> 16) & 0xFF; + uint32_t addr = w1; + if (addr == 0 || !IS_SEGMENTED(addr)) continue; + + // Skip inline commands (data is in the command word itself, not a pointer) + if (id == SetWind || id == SetTimeSettings || id == SetSkyboxModifier || + id == SetEchoSettings || id == SetSoundSettings || id == SetSkyboxSettings || + id == SetRoomBehavior || id == SetCameraSettings || id == SetSpecialObjects || + id == EndMarker) { + continue; + } + + uint32_t off = SEGMENT_OFFSET(addr); + uint8_t seg = (addr >> 24) & 0xFF; + addrs.insert(off); + + // For fixed-count commands, also add end address (start + count * entrySize) + switch (id) { + case SetStartPositionList: addrs.insert(off + arg1 * 16); break; + case SetActorList: addrs.insert(off + arg1 * 16); break; + case SetTransitionActorList: addrs.insert(off + arg1 * 16); break; + case SetObjectList: addrs.insert(off + arg1 * 2); break; + case SetLightList: addrs.insert(off + arg1 * 14); break; + case SetLightingSettings: addrs.insert(off + arg1 * 22); break; + case SetRoomList: addrs.insert(off + arg1 * 8); break; + default: break; + } + + // For SetPathways, pre-scan the pathway entries to add their point data addresses. + if (id == SetPathways) { + auto pathPeek = ReadSubArray(buffer, addr, 256 * 8); + for (uint32_t i = 0; i < 256; i++) { + uint8_t np = pathPeek.ReadUByte(); + pathPeek.ReadUByte(); pathPeek.ReadUByte(); pathPeek.ReadUByte(); + uint32_t ptsAddr = pathPeek.ReadUInt32(); + if (ptsAddr == 0 || !IS_SEGMENTED(ptsAddr) || ((ptsAddr >> 24) & 0xFF) != seg) { + addrs.insert(off + i * 8); + break; + } + addrs.insert(SEGMENT_OFFSET(ptsAddr)); + addrs.insert(SEGMENT_OFFSET(ptsAddr) + np * 6); + } + } + } + addrs.insert(rawCmds.size() * 8); + return addrs; +} + +std::optional> OoTSceneFactory::parse(std::vector& buffer, YAML::Node& node) { + auto [_, segment] = Decompressor::AutoDecode(node, buffer, 0x10000); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + auto scene = std::make_shared(); + + // Read 8-byte scene commands until EndMarker + std::vector> rawCmds; + while (reader.GetBaseAddress() < segment.size - 7) { + uint32_t w0 = reader.ReadUInt32(); + uint32_t w1 = reader.ReadUInt32(); + uint8_t cmdID = (w0 >> 24) & 0xFF; + rawCmds.push_back({w0, w1}); + if (cmdID == EndMarker) break; + } + + auto entryName = GetSafeNode(node, "symbol"); + auto currentDir = Companion::Instance->GetCurrentDirectory(); + auto assetType = GetSafeNode(node, "type"); + std::string baseName = node["base_name"] ? node["base_name"].as() : entryName; + bool isAltHeader = node["base_name"].IsDefined(); + + auto knownAddrs = CollectKnownAddresses(rawCmds, buffer); + + std::vector pendingAltHeaders; + SceneWriteContext ctx { + .buffer = buffer, + .knownAddrs = knownAddrs, + .entryName = entryName, + .baseName = baseName, + .currentDir = currentDir, + .assetType = assetType, + .isAltHeader = isAltHeader, + .pendingAltHeaders = pendingAltHeaders, + }; + + SceneCommandWriter writer; + for (auto& [w0, w1] : rawCmds) { + scene->commands.push_back(writer.Write(w0, w1, ctx)); + } + + // Process deferred alternate headers now that primary DLists are registered. + for (auto& alt : pendingAltHeaders) { + if (Companion::Instance->GetNodeByAddr(alt.seg).has_value()) { + continue; + } + + std::vector savedVtx; + if (DeferredVtx::IsDeferred()) { + savedVtx = DeferredVtx::SaveAndClearPending(); + } + + YAML::Node altNode; + altNode["type"] = assetType; + altNode["offset"] = alt.seg; + altNode["symbol"] = alt.symbol; + altNode["base_name"] = entryName; + try { + Companion::Instance->AddAsset(altNode); + } catch (const std::exception& e) { + SPDLOG_WARN("Scene: Failed to create alternate header {}: {}", alt.symbol, e.what()); + } + + if (DeferredVtx::IsDeferred()) { + DeferredVtx::RestorePending(savedVtx); + } + } + + return scene; +} + +ExportResult OoTSceneBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto scene = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTRoom, 0); + writer.Write(static_cast(scene->commands.size())); + + for (auto& cmd : scene->commands) { + writer.Write(cmd.cmdID); + if (!cmd.data.empty()) { + writer.Write(reinterpret_cast(cmd.data.data()), cmd.data.size()); + } + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSceneFactory.h b/src/factories/oot/OoTSceneFactory.h new file mode 100644 index 00000000..8cda233a --- /dev/null +++ b/src/factories/oot/OoTSceneFactory.h @@ -0,0 +1,82 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include +#include +#include +#include + +namespace OoT { + +// Scene command IDs (matching OoT's RoomCommand enum) +enum SceneCmdID : uint32_t { + SetStartPositionList = 0x00, + SetActorList = 0x01, + SetCsCamera = 0x02, + SetCollisionHeader = 0x03, + SetRoomList = 0x04, + SetWind = 0x05, + SetEntranceList = 0x06, + SetSpecialObjects = 0x07, + SetRoomBehavior = 0x08, + // 0x09 = Unused + SetMesh = 0x0A, + SetObjectList = 0x0B, + SetLightList = 0x0C, + SetPathways = 0x0D, + SetTransitionActorList = 0x0E, + SetLightingSettings = 0x0F, + SetTimeSettings = 0x10, + SetSkyboxSettings = 0x11, + SetSkyboxModifier = 0x12, + SetExitList = 0x13, + EndMarker = 0x14, + SetSoundSettings = 0x15, + SetEchoSettings = 0x16, + SetCutscenes = 0x17, + SetAlternateHeaders = 0x18, + SetCameraSettings = 0x19, +}; + +// Deferred alternate header entry, processed after primary commands. +struct PendingAltHeader { + uint32_t seg; + std::string symbol; +}; + +// A single scene/room command as parsed from ROM +struct SceneCommand { + uint32_t cmdID; + std::vector data; // serialized binary data for the command body +}; + +// Parsed scene/room data +class OoTSceneData : public IParsedData { +public: + std::vector commands; +}; + +class OoTSceneBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTSceneFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTSceneBinaryExporter) + }; + } + +private: + std::set CollectKnownAddresses(const std::vector>& rawCmds, + std::vector& buffer); +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSceneUtils.cpp b/src/factories/oot/OoTSceneUtils.cpp new file mode 100644 index 00000000..6cc1b31e --- /dev/null +++ b/src/factories/oot/OoTSceneUtils.cpp @@ -0,0 +1,56 @@ +#ifdef OOT_SUPPORT + +#include "OoTSceneUtils.h" + +namespace OoT { + +LUS::BinaryReader ReadSubArray(std::vector& buffer, uint32_t segAddr, uint32_t size) { + YAML::Node node; + node["offset"] = Companion::Instance->PatchVirtualAddr(segAddr); + auto raw = Decompressor::AutoDecode(node, buffer, size); + LUS::BinaryReader reader(raw.segment.data, raw.segment.size); + reader.SetEndianness(Torch::Endianness::Big); + return reader; +} + +std::string ResolvePointer(uint32_t ptr) { + if (ptr == 0) return ""; + ptr = Companion::Instance->PatchVirtualAddr(ptr); + auto result = Companion::Instance->GetStringByAddr(ptr); + if (result.has_value()) return result.value(); + return ""; +} + +std::string MakeAssetName(const std::string& baseName, const std::string& suffix, uint32_t offset) { + std::ostringstream ss; + ss << baseName << suffix << "_" << std::uppercase << std::hex + << std::setfill('0') << std::setw(6) << offset; + return ss.str(); +} + +std::vector SerializePathways(std::vector& buffer, + const std::vector>& pathways, + uint32_t writeCount, uint32_t repeats) { + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTPath, 0); + w.Write(static_cast(writeCount)); + for (uint32_t r = 0; r < repeats; r++) { + for (auto& [np, ptsAddr] : pathways) { + w.Write(static_cast(np)); + auto ptReader = ReadSubArray(buffer, ptsAddr, np * 6); + for (uint8_t k = 0; k < np; k++) { + w.Write(ptReader.ReadInt16()); + w.Write(ptReader.ReadInt16()); + w.Write(ptReader.ReadInt16()); + } + } + } + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + return std::vector(str.begin(), str.end()); +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSceneUtils.h b/src/factories/oot/OoTSceneUtils.h new file mode 100644 index 00000000..ae379a8b --- /dev/null +++ b/src/factories/oot/OoTSceneUtils.h @@ -0,0 +1,55 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "Companion.h" +#include "utils/Decompressor.h" +#include +#include +#include + +namespace OoT { + +// Cutscene field packing macros (matching OTRExporter's command_macros_base.h) +inline uint32_t CS_CMD_HH(uint16_t a, uint16_t b) { return ((uint32_t)b << 16) | (uint32_t)a; } +inline uint32_t CS_CMD_BBH(int8_t a, int8_t b, int16_t c) { + return ((uint32_t)(uint8_t)a) | ((uint32_t)(uint8_t)b << 8) | ((uint32_t)(uint16_t)c << 16); +} +inline uint32_t CS_CMD_HBB(uint16_t a, uint8_t b, uint8_t c) { + return (uint32_t)a | ((uint32_t)b << 16) | ((uint32_t)c << 24); +} + +// Helper to read a sub-array from ROM given a segmented pointer +LUS::BinaryReader ReadSubArray(std::vector& buffer, uint32_t segAddr, uint32_t size); + +// Resolve a segmented pointer to an O2R asset path string +std::string ResolvePointer(uint32_t ptr); + +// Build a scene-relative asset name from offset +std::string MakeAssetName(const std::string& baseName, const std::string& suffix, uint32_t offset); + +// Serialize pathway data into OoTPath binary format. +std::vector SerializePathways(std::vector& buffer, + const std::vector>& pathways, + uint32_t writeCount, uint32_t repeats); + +class CutsceneSerializer { +public: + static std::vector Serialize(std::vector& buffer, uint32_t segAddr); +private: + static uint32_t CalculateSize(std::vector& buffer, uint32_t segAddr); + static std::vector Write(std::vector& buffer, uint32_t segAddr, uint32_t size); + static void WriteCameraCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w); + static void WriteSingleEntryCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w); + static void WriteEntryCountCmd(uint32_t cid, LUS::BinaryReader& reader, LUS::BinaryWriter& w); + static bool IsCameraCmd(uint32_t id); + static bool IsSingleEntryCmd(uint32_t id); + static bool IsSmallEntryCmd(uint32_t id); + static bool IsHandledCmd(uint32_t id); + static const std::set sUnimplementedCmds; +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSkeletonFactory.cpp b/src/factories/oot/OoTSkeletonFactory.cpp new file mode 100644 index 00000000..7692b2c7 --- /dev/null +++ b/src/factories/oot/OoTSkeletonFactory.cpp @@ -0,0 +1,95 @@ +#ifdef OOT_SUPPORT + +#include "OoTSkeletonFactory.h" +#include "OoTSkeletonTypes.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +std::optional> OoTSkeletonFactory::parse(std::vector& buffer, YAML::Node& node) { + auto skelTypeStr = GetSafeNode(node, "skel_type"); + auto limbTypeStr = GetSafeNode(node, "limb_type"); + auto skelType = ParseSkeletonType(skelTypeStr); + auto limbType = ParseLimbType(limbTypeStr); + + size_t skelSize = (skelType == OoTSkeletonType::Flex) ? 0x0C : 0x08; + auto [_, segment] = Decompressor::AutoDecode(node, buffer, skelSize); + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + + uint32_t limbsArrayAddr = reader.ReadUInt32(); + limbsArrayAddr = Companion::Instance->PatchVirtualAddr(limbsArrayAddr); + uint8_t limbCount = reader.ReadUByte(); + uint8_t dListCount = 0; + + if (skelType == OoTSkeletonType::Flex) { + reader.Seek(8, LUS::SeekOffsetType::Start); + dListCount = reader.ReadUByte(); + } + + auto symbol = GetSafeNode(node, "symbol"); + std::vector limbPaths; + if (limbsArrayAddr != 0 && limbCount > 0) { + YAML::Node limbTableNode; + limbTableNode["offset"] = limbsArrayAddr; + auto limbTableRaw = Decompressor::AutoDecode(limbTableNode, buffer, limbCount * 4); + LUS::BinaryReader limbTableReader(limbTableRaw.segment.data, limbTableRaw.segment.size); + limbTableReader.SetEndianness(Torch::Endianness::Big); + + for (uint8_t i = 0; i < limbCount; i++) { + uint32_t limbAddr = limbTableReader.ReadUInt32(); + limbAddr = Companion::Instance->PatchVirtualAddr(limbAddr); + std::string limbPath = ResolvePointer(limbAddr); + if (limbPath.empty() && limbAddr != 0) { + SPDLOG_WARN("Undeclared limb at 0x{:08X} — YAML enrichment incomplete", limbAddr); + } + limbPaths.push_back(limbPath); + } + } + + if (limbsArrayAddr != 0) { + auto limbTablePath = ResolvePointer(limbsArrayAddr); + if (limbTablePath.empty()) { + SPDLOG_WARN("Undeclared limb table at 0x{:08X} — YAML enrichment incomplete", limbsArrayAddr); + } + } + + auto skel = std::make_shared(); + skel->skelType = skelType; + skel->limbType = limbType; + skel->limbCount = limbCount; + skel->dListCount = dListCount; + skel->limbPaths = std::move(limbPaths); + + return skel; +} + +ExportResult OoTSkeletonBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto writer = LUS::BinaryWriter(); + auto skel = std::static_pointer_cast(raw); + + WriteHeader(writer, Torch::ResourceType::OoTSkeleton, 0); + + writer.Write(static_cast(skel->skelType)); + writer.Write(static_cast(skel->limbType)); + writer.Write(static_cast(skel->limbCount)); + writer.Write(static_cast(skel->dListCount)); + writer.Write(static_cast(skel->limbType)); + writer.Write(static_cast(skel->limbPaths.size())); + + for (auto& path : skel->limbPaths) { + writer.Write(path); + } + + writer.Finish(write); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSkeletonFactory.h b/src/factories/oot/OoTSkeletonFactory.h new file mode 100644 index 00000000..fe12c2eb --- /dev/null +++ b/src/factories/oot/OoTSkeletonFactory.h @@ -0,0 +1,26 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" + +namespace OoT { + +class OoTSkeletonBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class OoTSkeletonFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTSkeletonBinaryExporter) + }; + } +}; + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSkeletonTypes.cpp b/src/factories/oot/OoTSkeletonTypes.cpp new file mode 100644 index 00000000..9209158e --- /dev/null +++ b/src/factories/oot/OoTSkeletonTypes.cpp @@ -0,0 +1,42 @@ +#ifdef OOT_SUPPORT + +#include "OoTSkeletonTypes.h" +#include "spdlog/spdlog.h" +#include "Companion.h" + +namespace OoT { + +OoTLimbType ParseLimbType(const std::string& str) { + if (str == "Standard") return OoTLimbType::Standard; + if (str == "LOD") return OoTLimbType::LOD; + if (str == "Skin") return OoTLimbType::Skin; + if (str == "Curve") return OoTLimbType::Curve; + if (str == "Legacy") return OoTLimbType::Legacy; + SPDLOG_ERROR("Unknown OoT limb type '{}'", str); + return OoTLimbType::Invalid; +} + +OoTSkeletonType ParseSkeletonType(const std::string& str) { + if (str == "Normal") return OoTSkeletonType::Normal; + if (str == "Flex") return OoTSkeletonType::Flex; + if (str == "Curve") return OoTSkeletonType::Curve; + SPDLOG_ERROR("Unknown OoT skeleton type '{}'", str); + return OoTSkeletonType::Normal; +} + +std::string ResolveGfxPointer(uint32_t ptr, const std::string& limbSymbol, + const std::string& suffix) { + if (ptr == 0) return ""; + ptr = Companion::Instance->PatchVirtualAddr(ptr); + auto result = Companion::Instance->GetStringByAddr(ptr); + if (result.has_value()) { + return result.value(); + } + + SPDLOG_WARN("Could not resolve GFX pointer 0x{:08X} — YAML enrichment incomplete", ptr); + return ""; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTSkeletonTypes.h b/src/factories/oot/OoTSkeletonTypes.h new file mode 100644 index 00000000..9d7ed4cd --- /dev/null +++ b/src/factories/oot/OoTSkeletonTypes.h @@ -0,0 +1,102 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include +#include + +namespace OoT { + +// Must match SOH::SkeletonType enum values +enum class OoTSkeletonType : uint8_t { + Normal = 0, + Flex = 1, + Curve = 2, +}; + +// Must match SOH::LimbType enum values +enum class OoTLimbType : uint8_t { + Invalid = 0, + Standard = 1, + LOD = 2, + Skin = 3, + Curve = 4, + Legacy = 5, +}; + +// Must match ZLimbSkinType enum values +enum class OoTLimbSkinType : int32_t { + SkinType_Null = 0, + SkinType_Animated = 4, + SkinType_Normal = 11, +}; + +struct OoTSkinVertex { + uint16_t index; + int16_t s, t; + int8_t normX, normY, normZ; + uint8_t alpha; +}; + +struct OoTSkinTransformation { + uint8_t limbIndex; + int16_t x, y, z; + uint8_t scale; +}; + +struct OoTSkinLimbModif { + uint16_t unk_4; + std::vector skinVertices; + std::vector limbTransformations; +}; + +struct OoTSkinAnimatedLimbData { + uint16_t totalVtxCount; + std::vector limbModifications; + std::string dlist; // resolved path +}; + +// Parsed data for a single OoT limb +class OoTLimbData : public IParsedData { +public: + OoTLimbType limbType; + + // Skin-specific fields + OoTLimbSkinType skinSegmentType = OoTLimbSkinType::SkinType_Null; + std::string skinDList; // resolved path (Skin + SkinType_Normal) + uint16_t skinVtxCnt = 0; + OoTSkinAnimatedLimbData skinAnimData; + + // Legacy-specific fields + float legTransX = 0, legTransY = 0, legTransZ = 0; + uint16_t rotX = 0, rotY = 0, rotZ = 0; + std::string childPtr; // resolved path (Legacy only) + std::string siblingPtr; // resolved path (Legacy only) + + // Common fields for Standard/LOD/Skin/Curve + std::string dListPtr; // resolved path + std::string dList2Ptr; // resolved path (LOD/Curve only) + int16_t transX = 0, transY = 0, transZ = 0; + uint8_t childIndex = 0, siblingIndex = 0; +}; + +// Parsed data for an OoT skeleton +class OoTSkeletonData : public IParsedData { +public: + OoTSkeletonType skelType; + OoTLimbType limbType; + uint8_t limbCount; + uint8_t dListCount; // Flex only + std::vector limbPaths; // resolved paths to each limb +}; + +// Shared helpers +OoTLimbType ParseLimbType(const std::string& str); +OoTSkeletonType ParseSkeletonType(const std::string& str); +std::string ResolveGfxPointer(uint32_t ptr, const std::string& limbSymbol, + const std::string& suffix); + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTTextFactory.cpp b/src/factories/oot/OoTTextFactory.cpp new file mode 100644 index 00000000..c8dfd200 --- /dev/null +++ b/src/factories/oot/OoTTextFactory.cpp @@ -0,0 +1,212 @@ +#ifdef OOT_SUPPORT + +#include "OoTTextFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +namespace OoT { + +struct OoTTextData : public IParsedData { + std::vector mBinary; +}; + +bool OoTTextFactory::IsEndOfMessageCode(uint8_t c) { + return c == 0x02 // END + || c == 0x07; // FADE2 +} + +unsigned int OoTTextFactory::GetTrailingBytes(uint8_t c) { + switch (c) { + case 0x05: // COLOR + case 0x06: // SHIFT + case 0x0C: // PERSISTENT + case 0x0E: // FADE + case 0x13: // ICON + case 0x14: // SPEED + case 0x1E: // BACKGROUND + return 1; + case 0x07: // FADE2 (also ends message) + case 0x11: // HIGHSCORE + case 0x12: // SOUND_EFFECT + return 2; + case 0x15: // THREE_CHOICE + return 3; + default: + return 0; + } +} + +// Read a message string from raw data, handling OoT text control codes. +// Control codes have 0-3 trailing bytes that must be included in the output. +std::string OoTTextFactory::ReadMessageText(const uint8_t* rawData, size_t rawSize, uint32_t offset) { + std::string msg; + uint32_t ptr = offset; + + while (ptr < rawSize) { + uint8_t byte = rawData[ptr]; + if (byte == 0x00) break; + + msg += (char)byte; + ptr++; + + // Consume trailing bytes for control codes + unsigned int trailing = GetTrailingBytes(byte); + for (unsigned int i = 0; i < trailing && ptr < rawSize; i++) { + uint8_t trailingByte = rawData[ptr]; + msg += (char)trailingByte; + ptr++; + } + + if (IsEndOfMessageCode(byte)) break; + } + + return msg; +} + +// NTSC: message offset is at bytes 4-7 of the entry +uint32_t OoTTextFactory::ReadMessageOffsetNTSC(const uint8_t* codeData, uint32_t entryPtr) { + uint32_t raw = (codeData[entryPtr + 4] << 24) | + (codeData[entryPtr + 5] << 16) | + (codeData[entryPtr + 6] << 8) | + codeData[entryPtr + 7]; + + // Mask off segment byte to get file-relative offset + return raw & 0x00FFFFFF; +} + +// PAL: message offset is a 4-byte entry in a separate language table +uint32_t OoTTextFactory::ReadMessageOffsetPAL(const uint8_t* codeData, uint32_t langPtr) { + uint32_t raw = (codeData[langPtr] << 24) | + (codeData[langPtr + 1] << 16) | + (codeData[langPtr + 2] << 8) | + codeData[langPtr + 3]; + + // Mask off segment byte to get file-relative offset + return raw & 0x00FFFFFF; +} + +// Entry layout: [id_hi, id_lo, type_hi_nibble | ypos_lo_nibble, ...] +MessageEntry OoTTextFactory::ReadMessageMetadata(const uint8_t* codeData, uint32_t ptr) { + MessageEntry entry; + entry.id = (uint16_t)((codeData[ptr] << 8) | codeData[ptr + 1]); + entry.textboxType = (codeData[ptr + 2] >> 4) & 0x0F; + entry.textboxYPos = codeData[ptr + 2] & 0x0F; + return entry; +} + +std::vector OoTTextFactory::ParseMessagesNTSC(const DataChunk& code, uint32_t codeOffset, + const uint8_t* rawData, size_t rawSize) { + std::vector messages; + uint32_t currentPtr = codeOffset; + + while (true) { + // Each message table entry is 8 bytes + if (currentPtr + 8 > code.size) break; + + auto entry = ReadMessageMetadata(code.data, currentPtr); + + // NTSC stores the message offset inline at currentPtr + 4 + uint32_t msgOffset = ReadMessageOffsetNTSC(code.data, currentPtr); + + entry.msg = ReadMessageText(rawData, rawSize, msgOffset); + messages.push_back(entry); + + // End of message table (0xFFFF) or staff credits (0xFFFC) + if (entry.id == 0xFFFC || entry.id == 0xFFFF) break; + + currentPtr += 8; + } + + return messages; +} + +std::vector OoTTextFactory::ParseMessagesPAL(const DataChunk& code, uint32_t codeOffset, + uint32_t langPtr, + const uint8_t* rawData, size_t rawSize) { + std::vector messages; + uint32_t currentPtr = codeOffset; + + while (true) { + // Each message table entry is 8 bytes + if (currentPtr + 8 > code.size) break; + + auto entry = ReadMessageMetadata(code.data, currentPtr); + + // PAL stores message offsets in a separate language table (4 bytes each) + uint32_t msgOffset = ReadMessageOffsetPAL(code.data, langPtr); + + entry.msg = ReadMessageText(rawData, rawSize, msgOffset); + messages.push_back(entry); + + // End of message table (0xFFFF) or staff credits (0xFFFC) + if (entry.id == 0xFFFC || entry.id == 0xFFFF) break; + + currentPtr += 8; + langPtr += 4; + } + + return messages; +} + +std::optional> OoTTextFactory::parse(std::vector& buffer, YAML::Node& node) { + auto codePhysStart = GetSafeNode(node, "code_phys_start"); + auto codeOffset = GetSafeNode(node, "code_offset"); + uint32_t langOffset = node["lang_offset"] ? node["lang_offset"].as() : 0; + bool isPalLang = (langOffset != 0 && langOffset != codeOffset); + + // Decompress code segment + auto* codeChunk = Decompressor::Decode(buffer, codePhysStart, CompressionType::YAZ0); + if (!codeChunk || !codeChunk->data) { + SPDLOG_ERROR("OoTTextFactory: failed to decompress code segment"); + return std::nullopt; + } + + // Get message data segment (uncompressed) + auto msgSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(128); + if (!msgSeg.has_value()) { + SPDLOG_ERROR("OoTTextFactory: message data segment 128 not found"); + return std::nullopt; + } + + const uint8_t* rawData = buffer.data() + msgSeg.value(); + size_t rawSize = buffer.size() - msgSeg.value(); + + // Parse message entries + auto messages = isPalLang + ? ParseMessagesPAL(*codeChunk, codeOffset, langOffset, rawData, rawSize) + : ParseMessagesNTSC(*codeChunk, codeOffset, rawData, rawSize); + + SPDLOG_INFO("OoTTextFactory: parsed {} messages", messages.size()); + + // Build OTXT binary + auto data = std::make_shared(); + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTText, 0); + + w.Write(static_cast(messages.size())); + for (auto& m : messages) { + w.Write(m.id); + w.Write(m.textboxType); + w.Write(m.textboxYPos); + w.Write(m.msg); + } + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + data->mBinary = std::vector(str.begin(), str.end()); + + return data; +} + +ExportResult OoTTextBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto data = std::static_pointer_cast(raw); + write.write(data->mBinary.data(), data->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT + +#endif diff --git a/src/factories/oot/OoTTextFactory.h b/src/factories/oot/OoTTextFactory.h new file mode 100644 index 00000000..20e8c794 --- /dev/null +++ b/src/factories/oot/OoTTextFactory.h @@ -0,0 +1,50 @@ +#pragma once + +#ifdef OOT_SUPPORT + +#include "factories/BaseFactory.h" +#include "utils/Decompressor.h" + +namespace OoT { + +// Handles OOT:TEXT type (OTXT 0x4F545854), message tables + per-language data. +// Reference: Shipwright's TextFactory.cpp for binary format. + +class OoTTextBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +struct MessageEntry { + uint16_t id; + uint8_t textboxType; + uint8_t textboxYPos; + std::string msg; +}; + +class OoTTextFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& node) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, OoTTextBinaryExporter) + }; + } + +private: + static bool IsEndOfMessageCode(uint8_t c); + static unsigned int GetTrailingBytes(uint8_t c); + static std::string ReadMessageText(const uint8_t* rawData, size_t rawSize, uint32_t offset); + static uint32_t ReadMessageOffsetNTSC(const uint8_t* codeData, uint32_t entryPtr); + static uint32_t ReadMessageOffsetPAL(const uint8_t* codeData, uint32_t langPtr); + static MessageEntry ReadMessageMetadata(const uint8_t* codeData, uint32_t ptr); + static std::vector ParseMessagesNTSC(const DataChunk& code, uint32_t codeOffset, + const uint8_t* rawData, size_t rawSize); + static std::vector ParseMessagesPAL(const DataChunk& code, uint32_t codeOffset, + uint32_t langPtr, + const uint8_t* rawData, size_t rawSize); +}; + +} // namespace OoT + +#endif diff --git a/src/n64/Cartridge.cpp b/src/n64/Cartridge.cpp index cca2f78d..73435723 100644 --- a/src/n64/Cartridge.cpp +++ b/src/n64/Cartridge.cpp @@ -7,7 +7,7 @@ void N64::Cartridge::Initialize() { LUS::BinaryReader reader((char*)this->gRomData.data(), this->gRomData.size()); reader.SetEndianness(Torch::Endianness::Big); reader.Seek(0x10, LUS::SeekOffsetType::Start); - this->gRomCRC = BSWAP32(reader.ReadUInt32()); + this->gRomCRC = reader.ReadUInt32(); reader.Seek(0x20, LUS::SeekOffsetType::Start); this->gGameTitle = std::string(reader.ReadCString()); this->gGameTitle.pop_back(); // Remove null terminator diff --git a/src/utils/Decompressor.cpp b/src/utils/Decompressor.cpp index 2cf94125..5df6fb10 100644 --- a/src/utils/Decompressor.cpp +++ b/src/utils/Decompressor.cpp @@ -9,6 +9,7 @@ extern "C" { #include #include #include +#include #include } @@ -57,6 +58,17 @@ DataChunk* Decompressor::Decode(const std::vector& buffer, const uint32 gCachedChunks[offset] = new DataChunk{ decompressed, size }; return gCachedChunks[offset]; } + case CompressionType::YAZ0: { + uint32_t size = 0; + uint8_t* decompressed = yaz0_decode(in_buf, &size); + + if (!decompressed) { + throw std::runtime_error("Failed to decode YAZ0"); + } + + gCachedChunks[offset] = new DataChunk{ decompressed, size }; + return gCachedChunks[offset]; + } default: throw std::runtime_error("Unknown compression type"); } @@ -152,7 +164,8 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector switch (type) { case CompressionType::YAY0: case CompressionType::YAY1: - case CompressionType::MIO0: { + case CompressionType::MIO0: + case CompressionType::YAZ0: { offset = ASSET_PTR(offset); auto decoded = Decode(buffer, fileOffset, type); @@ -176,9 +189,6 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector return { .root = decoded, .segment = { decoded->data + offset, size } }; } - case CompressionType::YAZ0: - throw std::runtime_error( - "Found compressed yaz0 segment.\nDecompression of yaz0 has not been implemented yet."); case CompressionType::None: // The data does not have compression { fileOffset = TranslateAddr(offset, false); @@ -217,7 +227,7 @@ DecompressedData Decompressor::AutoDecode(uint32_t offset, std::optional } uint32_t Decompressor::TranslateAddr(uint32_t addr, bool baseAddress) { - if (IS_SEGMENTED(addr)) { + if (IS_SEGMENTED(addr) || IS_VIRTUAL_SEGMENT(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 {}",