summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt19
-rw-r--r--lib/libyaz0/yaz0.c57
-rw-r--r--lib/libyaz0/yaz0.h7
-rw-r--r--src/AliasManager.cpp23
-rw-r--r--src/AliasManager.h19
-rw-r--r--src/Companion.cpp259
-rw-r--r--src/Companion.h18
-rw-r--r--src/factories/BaseFactory.h3
-rw-r--r--src/factories/BlobFactory.cpp5
-rw-r--r--src/factories/DisplayListFactory.cpp18
-rw-r--r--src/factories/DisplayListFactory.h4
-rw-r--r--src/factories/ResourceType.h17
-rw-r--r--src/factories/oot/DeferredVtx.cpp128
-rw-r--r--src/factories/oot/DeferredVtx.h16
-rw-r--r--src/factories/oot/OoTAnimationFactory.cpp118
-rw-r--r--src/factories/oot/OoTAnimationFactory.h34
-rw-r--r--src/factories/oot/OoTAnimationTypes.h26
-rw-r--r--src/factories/oot/OoTArrayFactory.cpp118
-rw-r--r--src/factories/oot/OoTArrayFactory.h57
-rw-r--r--src/factories/oot/OoTAudioFactory.cpp132
-rw-r--r--src/factories/oot/OoTAudioFactory.h34
-rw-r--r--src/factories/oot/OoTAudioFontWriter.cpp333
-rw-r--r--src/factories/oot/OoTAudioFontWriter.h78
-rw-r--r--src/factories/oot/OoTAudioSampleWriter.cpp203
-rw-r--r--src/factories/oot/OoTAudioSampleWriter.h24
-rw-r--r--src/factories/oot/OoTAudioSequenceWriter.cpp93
-rw-r--r--src/factories/oot/OoTAudioSequenceWriter.h19
-rw-r--r--src/factories/oot/OoTAudioTypes.cpp34
-rw-r--r--src/factories/oot/OoTAudioTypes.h52
-rw-r--r--src/factories/oot/OoTCollisionFactory.cpp331
-rw-r--r--src/factories/oot/OoTCollisionFactory.h76
-rw-r--r--src/factories/oot/OoTCurveAnimationFactory.cpp134
-rw-r--r--src/factories/oot/OoTCurveAnimationFactory.h32
-rw-r--r--src/factories/oot/OoTCutsceneFactory.cpp323
-rw-r--r--src/factories/oot/OoTCutsceneFactory.h22
-rw-r--r--src/factories/oot/OoTDListHelpers.cpp759
-rw-r--r--src/factories/oot/OoTDListHelpers.h24
-rw-r--r--src/factories/oot/OoTLimbFactory.cpp272
-rw-r--r--src/factories/oot/OoTLimbFactory.h39
-rw-r--r--src/factories/oot/OoTMtxFactory.cpp38
-rw-r--r--src/factories/oot/OoTMtxFactory.h29
-rw-r--r--src/factories/oot/OoTPathFactory.cpp49
-rw-r--r--src/factories/oot/OoTPathFactory.h22
-rw-r--r--src/factories/oot/OoTPlayerAnimationFactory.cpp91
-rw-r--r--src/factories/oot/OoTPlayerAnimationFactory.h51
-rw-r--r--src/factories/oot/OoTSceneCommandWriter.cpp696
-rw-r--r--src/factories/oot/OoTSceneCommandWriter.h57
-rw-r--r--src/factories/oot/OoTSceneFactory.cpp167
-rw-r--r--src/factories/oot/OoTSceneFactory.h78
-rw-r--r--src/factories/oot/OoTSceneUtils.cpp52
-rw-r--r--src/factories/oot/OoTSceneUtils.h51
-rw-r--r--src/factories/oot/OoTSkeletonFactory.cpp91
-rw-r--r--src/factories/oot/OoTSkeletonFactory.h22
-rw-r--r--src/factories/oot/OoTSkeletonTypes.cpp38
-rw-r--r--src/factories/oot/OoTSkeletonTypes.h98
-rw-r--r--src/factories/oot/OoTTextFactory.cpp300
-rw-r--r--src/factories/oot/OoTTextFactory.h51
-rw-r--r--src/n64/Cartridge.cpp4
-rw-r--r--src/utils/Decompressor.cpp20
59 files changed, 5933 insertions, 32 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index aa4d5b5..1c6a7d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -30,6 +30,9 @@ option(BUILD_FZERO "Build with F-Zero X support" ON)
option(BUILD_BK64 "Build with Banjo Kazooie support" ON)
option(BUILD_MARIO_ARTIST "Build with Mario Artist support" ON)
option(BUILD_NAUDIO "Build with NAudio support" ON)
+option(BUILD_OOT "Build with Ocarina of Time support" ON)
+option(PORT_VERSION_ENDIANNESS "Include endianness byte in portVersion file" OFF)
+option(ROM_CRC_BSWAP "Byte-swap the ROM CRC written to the version file" OFF)
option(BUILD_UI "Build with the interactive viewer (libultraship + Fast3D + ImGui)" OFF)
@@ -137,7 +140,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 ${CMAKE_CURRENT_SOURCE_DIR}/lib/bk_zip/*.cpp)
-file(GLOB C_FILES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c ${CMAKE_CURRENT_SOURCE_DIR}/src/**/*.c ${CMAKE_CURRENT_SOURCE_DIR}/lib/libmio0/*.c ${CMAKE_CURRENT_SOURCE_DIR}/lib/libyay0/*.c)
+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} ${UI_FILES})
if(BUILD_UI)
@@ -196,6 +199,20 @@ 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(ROM_CRC_BSWAP)
+ add_definitions(-DROM_CRC_BSWAP)
+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 0000000..20f94fb
--- /dev/null
+++ b/lib/libyaz0/yaz0.c
@@ -0,0 +1,57 @@
+#include "yaz0.h"
+#include "libmio0/utils.h"
+#include <stdlib.h>
+#include <string.h>
+
+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 0000000..447f361
--- /dev/null
+++ b/lib/libyaz0/yaz0.h
@@ -0,0 +1,7 @@
+#pragma once
+
+#include <stdint.h>
+
+#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 0000000..1f12aeb
--- /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<char>& 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 0000000..501a671
--- /dev/null
+++ b/src/AliasManager.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <string>
+#include <vector>
+#include <unordered_map>
+#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<char>& data);
+ void Clear();
+
+private:
+ std::unordered_map<std::string, std::vector<std::string>> mAliases;
+};
diff --git a/src/Companion.cpp b/src/Companion.cpp
index 27b2472..74d6ec1 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"
@@ -118,6 +119,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"
@@ -294,6 +311,23 @@ void Companion::Init(const ExportType type, std::atomic<size_t>& assetCount, boo
this->RegisterFactory("NAUDIO:V1:ADPCM_BOOK", std::make_shared<ADPCMBookFactory>());
this->RegisterFactory("NAUDIO:V1:SEQUENCE", std::make_shared<NSequenceFactory>());
#endif
+#ifdef OOT_SUPPORT
+ this->RegisterFactory("OOT:ARRAY", std::make_shared<OoT::OoTArrayFactory>());
+ this->RegisterFactory("OOT:MTX", std::make_shared<OoT::OoTMtxFactory>());
+ this->RegisterFactory("OOT:SKELETON", std::make_shared<OoT::OoTSkeletonFactory>());
+ this->RegisterFactory("OOT:LIMB", std::make_shared<OoT::OoTLimbFactory>());
+ this->RegisterFactory("OOT:ANIMATION", std::make_shared<OoT::OoTAnimationFactory>());
+ this->RegisterFactory("OOT:CURVE_ANIMATION", std::make_shared<OoT::OoTCurveAnimationFactory>());
+ this->RegisterFactory("OOT:PLAYER_ANIMATION", std::make_shared<OoT::OoTPlayerAnimationHeaderFactory>());
+ this->RegisterFactory("OOT:PLAYER_ANIMATION_DATA", std::make_shared<OoT::OoTPlayerAnimationDataFactory>());
+ this->RegisterFactory("OOT:COLLISION", std::make_shared<OoT::OoTCollisionFactory>());
+ this->RegisterFactory("OOT:TEXT", std::make_shared<OoT::OoTTextFactory>());
+ this->RegisterFactory("OOT:SCENE", std::make_shared<OoT::OoTSceneFactory>());
+ this->RegisterFactory("OOT:ROOM", std::make_shared<OoT::OoTSceneFactory>());
+ this->RegisterFactory("OOT:CUTSCENE", std::make_shared<OoT::OoTCutsceneFactory>());
+ this->RegisterFactory("OOT:PATH", std::make_shared<OoT::OoTPathFactory>());
+ this->RegisterFactory("OOT:AUDIO", std::make_shared<OoT::OoTAudioFactory>());
+#endif
#ifdef BUILD_UI
// Custom preview renderers; other types fall back to BaseFactoryUI.
@@ -422,7 +456,12 @@ std::optional<ParseResultData> 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();
@@ -550,6 +589,10 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic<size_t>& ass
}
}
+ if (node["directory"]) {
+ this->gCurrentDirectory = node["directory"].as<std::string>();
+ }
+
if (node["manual_segments"]) {
auto manualSegments = node["manual_segments"];
if (manualSegments.IsSequence() && manualSegments.size()) {
@@ -854,9 +897,12 @@ void Companion::ProcessExportFile() {
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();
std::replace(output.begin(), output.end(), '\\', '/');
@@ -1143,6 +1189,14 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) {
gCurrentSegmentNumber = 0;
gCurrentFileOffset = 0;
gCurrentCompressionType = CompressionType::None;
+ // gAddrMap registration below keys assets by their output path, so the
+ // directory must be resolved before the loop. Default to the file's own
+ // path, then honor a :config directory override (used to register a room's
+ // assets under its scene's directory).
+ this->gCurrentDirectory = relative(fs::path(this->gCurrentFile), this->gAssetPath).replace_extension("");
+ if (auto directory = root[":config"]["directory"]) {
+ this->gCurrentDirectory = directory.as<std::string>();
+ }
// Set compressed file offsets and compression type
if (auto segments = root[":config"]["segments"]) {
if (segments.IsSequence() && segments.size() > 0) {
@@ -1207,6 +1261,7 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) {
this->gTables.clear();
this->gCurrentExternalFiles.clear();
this->gSubFileList.clear();
+ this->gManualSegments.clear();
GFXDOverride::ClearVtx();
if (root[":config"]) {
@@ -1413,6 +1468,9 @@ void Companion::Process(std::atomic<size_t>& 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") {
@@ -1431,6 +1489,10 @@ void Companion::Process(std::atomic<size_t>& assetCount) {
}
}
+ if (cfg["primary_virtual_segment"]) {
+ this->gConfig.segment.primaryVirtual = cfg["primary_virtual_segment"].as<uint32_t>();
+ }
+
this->gConfig.defaultShading = GetSafeNode<std::string>(cfg, "default_shading", "");
if (auto sort = cfg["sort"]) {
@@ -1495,6 +1557,8 @@ void Companion::Process(std::atomic<size_t>& assetCount) {
this->gConfig.includeAutogen = cfg["include_autogen"] && cfg["include_autogen"].as<bool>();
this->gConfig.dialogPack = cfg["dialog_pack"] && cfg["dialog_pack"].as<bool>();
+ this->gConfig.strictDeclarations = GetSafeNode<bool>(cfg, "strict_declarations", false);
+
this->ParseHash();
SPDLOG_CRITICAL("------------------------------------------------");
@@ -1578,7 +1642,6 @@ void Companion::Process(std::atomic<size_t>& 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)) {
@@ -1926,6 +1989,66 @@ std::optional<std::uint32_t> 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 };
+}
+
std::optional<std::pair<std::uint32_t, std::uint32_t>>
Companion::GetFileOffsetFromCompressedSegmentedAddr(const uint8_t segment) const {
@@ -1939,14 +2062,65 @@ Companion::GetFileOffsetFromCompressedSegmentedAddr(const uint8_t segment) const
}
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<std::tuple<std::string, YAML::Node>> 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<std::tuple<std::string, YAML::Node>> 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;
}
- return addr;
+ // 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];
+ }
+
+ // Address doesn't belong to this external file.
+ return std::nullopt;
}
std::optional<std::tuple<std::string, YAML::Node>> Companion::GetNodeByAddr(uint32_t addr) {
@@ -1954,32 +2128,55 @@ std::optional<std::tuple<std::string, YAML::Node>> 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)) {
- auto realAddr = addr;
- if (IS_SEGMENTED(addr) && GetCompressedSegmentOffset(&realAddr)) {
- if (this->gAddrMap[this->gCurrentFile].contains(realAddr)) {
- return this->gAddrMap[this->gCurrentFile][realAddr];
- }
+ // 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];
+ }
+
+ // Compressed (BKZIP) segment: translate the segmented address to the decoded
+ // file offset before looking it up.
+ if (uint32_t realAddr = addr; IS_SEGMENTED(addr) && GetCompressedSegmentOffset(&realAddr)) {
+ if (this->gAddrMap[this->gCurrentFile].contains(realAddr)) {
+ return this->gAddrMap[this->gCurrentFile][realAddr];
}
+ }
- for (auto& file : this->gCurrentExternalFiles) {
- if (!Torch::contains(this->gAddrMap, file)) {
- SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file);
- 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;
+ }
+ }
- if (!Torch::contains(this->gAddrMap[file], addr)) {
- continue;
+ // 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<std::tuple<std::string, YAML::Node>> Companion::GetNodeByAddr(uint32_t addr, const std::string& file) {
@@ -2242,6 +2439,9 @@ std::vector<char> Companion::ParseVersionString(const std::string& version) {
auto wv = LUS::BinaryWriter();
wv.SetEndianness(Torch::Endianness::Big);
+#ifdef PORT_VERSION_ENDIANNESS
+ wv.Write(static_cast<uint8_t>(Torch::Endianness::Big));
+#endif
wv.Write(major);
wv.Write(minor);
wv.Write(patch);
@@ -2302,6 +2502,15 @@ std::optional<YAML::Node> Companion::AddAsset(YAML::Node asset) {
const auto symbol = GetSafeNode<std::string>(asset, "symbol", "");
const auto decl = this->GetNodeByAddr(offset);
+ // With strict_declarations, every asset must be pre-declared in the YAML.
+ // Throw if an undeclared asset is encountered to catch declaration gaps.
+ if (!decl.has_value() && this->gConfig.strictDeclarations) {
+ throw std::runtime_error(
+ "AddAsset: undeclared " + type + " at " + Torch::to_hex(offset, false) +
+ " (symbol: " + symbol + ") in " + this->gCurrentFile +
+ " — YAML declarations 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 f91fb55..2b28a8e 100644
--- a/src/Companion.h
+++ b/src/Companion.h
@@ -39,6 +39,7 @@ enum class GBIMinorVersion {
Mk64,
SM64,
PM64,
+ OoT,
BK64
};
@@ -57,6 +58,7 @@ struct SegmentConfig {
std::unordered_map<uint32_t, uint32_t> global;
std::unordered_map<uint32_t, uint32_t> local;
std::unordered_map<uint32_t, uint32_t> temporal;
+ std::optional<uint32_t> primaryVirtual;
std::unordered_map<std::string, std::unordered_map<uint32_t, std::pair<std::uint32_t, std::uint32_t>>> compressed;
};
@@ -73,6 +75,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;
@@ -99,6 +112,7 @@ struct TorchConfig {
bool debug;
bool modding;
bool textureDefines;
+ bool strictDeclarations;
bool includeAutogen;
bool dialogPack = false;
// Default model-preview shading mode name (e.g. "textured + lit"); empty
@@ -187,6 +201,7 @@ public:
std::optional<std::pair<std::uint32_t, std::uint32_t>> GetFileOffsetFromCompressedSegmentedAddr(uint8_t segment) const;
std::optional<std::shared_ptr<BaseFactory>> GetFactory(const std::string& type);
uint32_t PatchVirtualAddr(uint32_t addr);
+ ResolvedAddr ResolveVirtualAddr(uint32_t addr);
std::optional<std::tuple<std::string, YAML::Node>> GetNodeByAddr(uint32_t addr);
// File-scoped variant for use outside the parse/export passes, where
// gCurrentFile no longer points at the owning file.
@@ -233,6 +248,7 @@ public:
std::optional<std::tuple<std::string, YAML::Node>> RegisterAsset(const std::string& name, YAML::Node& node);
std::optional<YAML::Node> AddSubFileAsset(YAML::Node asset, std::string newFileName, CompressionType newCompressionType, uint32_t compressedSize = 0);
std::optional<YAML::Node> AddAsset(YAML::Node asset);
+ std::string GetCurrentDirectory() const { return gCurrentDirectory.string(); }
void SetCompressedSegment(uint32_t segmentId, uint32_t compressedFileOffset, uint32_t offset);
bool GetCompressedSegmentOffset(uint32_t* addr);
@@ -310,6 +326,8 @@ private:
void ProcessExportFile();
void ProcessFile(YAML::Node root);
void ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount);
+ std::optional<std::tuple<std::string, YAML::Node>> FindNodeInOverlaySegments(uint32_t addr, const std::string& file);
+ std::optional<std::tuple<std::string, YAML::Node>> 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 3335827..2486517 100644
--- a/src/factories/BaseFactory.h
+++ b/src/factories/BaseFactory.h
@@ -31,7 +31,8 @@ struct ParseResultData;
#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 7e69cf0..fc40e16 100644
--- a/src/factories/BlobFactory.cpp
+++ b/src/factories/BlobFactory.cpp
@@ -51,6 +51,11 @@ ExportResult BlobBinaryExporter::Export(std::ostream& write, std::shared_ptr<IPa
auto writer = LUS::BinaryWriter();
auto data = std::static_pointer_cast<RawBuffer>(raw)->mBuffer;
+ if (data.empty() && node["size"] && node["size"].as<uint32_t>() == 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 2d6d48e..2e336f0 100644
--- a/src/factories/DisplayListFactory.cpp
+++ b/src/factories/DisplayListFactory.cpp
@@ -6,6 +6,9 @@
#include <fstream>
#include <tuple>
#include "n64/gbi-otr.h"
+#ifdef OOT_SUPPORT
+#include "oot/OoTDListHelpers.h"
+#endif
#ifdef STANDALONE
#include <gfxd.h>
@@ -196,6 +199,11 @@ void DebugDisplayList(uint32_t w0, uint32_t w1) {
#endif
std::optional<std::tuple<std::string, YAML::Node>> SearchVtx(uint32_t ptr) {
+#ifdef OOT_SUPPORT
+ auto result = OoT::DListHelpers::SearchVtx(ptr);
+ if (result.has_value()) return result;
+#endif
+
const auto* decs = Companion::Instance->GetNodesByTypeRef("VTX", Companion::Instance->GetConfig().includeAutogen);
if (decs == nullptr) {
return std::nullopt;
@@ -230,6 +238,11 @@ std::optional<std::tuple<std::string, YAML::Node>> SearchVtx(uint32_t ptr) {
ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw, std::string& entryName,
YAML::Node& node, std::string* replacement) {
+#ifdef OOT_SUPPORT
+ auto ootResult = OoT::DListHelpers::Export(write, raw, entryName, node, replacement);
+ if (ootResult.has_value()) return ootResult.value();
+#endif
+
const auto gbi = Companion::Instance->GetGBIVersion();
auto cmds = std::static_pointer_cast<DListData>(raw)->mGfxs;
auto writer = LUS::BinaryWriter();
@@ -511,6 +524,11 @@ ExportResult DListBinaryExporter::Export(std::ostream& write, std::shared_ptr<IP
}
std::optional<std::shared_ptr<IParsedData>> DListFactory::parse(std::vector<uint8_t>& raw_buffer, YAML::Node& node) {
+#ifdef OOT_SUPPORT
+ auto ootResult = OoT::DListHelpers::Parse(raw_buffer, node);
+ if (ootResult.has_value()) return ootResult.value();
+#endif
+
const auto gbi = Companion::Instance->GetGBIVersion();
auto count = GetSafeNode<int32_t>(node, "count", -1);
diff --git a/src/factories/DisplayListFactory.h b/src/factories/DisplayListFactory.h
index 705ef74..6db0c51 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<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
diff --git a/src/factories/ResourceType.h b/src/factories/ResourceType.h
index 846c1c4..17d5ae9 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
+
// BK64
BKSprite = 0x424B5350, // BKSP
BKAnimation = 0x424B414E, // BKAN
diff --git a/src/factories/oot/DeferredVtx.cpp b/src/factories/oot/DeferredVtx.cpp
new file mode 100644
index 0000000..9ea76d0
--- /dev/null
+++ b/src/factories/oot/DeferredVtx.cpp
@@ -0,0 +1,128 @@
+#include "DeferredVtx.h"
+#include "Companion.h"
+#include "factories/DisplayListOverrides.h"
+#include "n64/CommandMacros.h"
+#include "spdlog/spdlog.h"
+#include <algorithm>
+#include <iomanip>
+#include <sstream>
+
+// 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<PendingVtx> sPendingList;
+
+void BeginDefer() {
+ sDeferred = true;
+ sPendingList.clear();
+}
+
+bool IsDeferred() {
+ return sDeferred;
+}
+
+std::vector<PendingVtx> SaveAndClearPending() {
+ auto saved = std::move(sPendingList);
+ sPendingList.clear();
+ return saved;
+}
+
+void RestorePending(std::vector<PendingVtx>& 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<MergedVtx> 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
diff --git a/src/factories/oot/DeferredVtx.h b/src/factories/oot/DeferredVtx.h
new file mode 100644
index 0000000..ee84347
--- /dev/null
+++ b/src/factories/oot/DeferredVtx.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace DeferredVtx {
+ struct PendingVtx { uint32_t addr; uint32_t count; };
+ void BeginDefer();
+ bool IsDeferred();
+ void FlushDeferred(const std::string& baseName);
+ std::vector<PendingVtx> SaveAndClearPending();
+ void RestorePending(std::vector<PendingVtx>& saved);
+ void AddPending(uint32_t addr, uint32_t count);
+ void EndDefer();
+}
diff --git a/src/factories/oot/OoTAnimationFactory.cpp b/src/factories/oot/OoTAnimationFactory.cpp
new file mode 100644
index 0000000..a2e8edd
--- /dev/null
+++ b/src/factories/oot/OoTAnimationFactory.cpp
@@ -0,0 +1,118 @@
+#include "OoTAnimationFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+std::optional<std::shared_ptr<IParsedData>> OoTAnimationFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ // Check for legacy animation type (data stays in ROM segment, not extracted)
+ auto animType = GetSafeNode<std::string>(node, "anim_type", "normal");
+ if (animType == "legacy") {
+ auto anim = std::make_shared<OoTNormalAnimationData>();
+ 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<OoTNormalAnimationData>();
+ 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<uint32_t>(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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto anim = std::static_pointer_cast<OoTNormalAnimationData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTAnimation, 0);
+
+ if (anim->isLegacy) {
+ writer.Write(static_cast<uint32_t>(OoTAnimationType::Legacy));
+ writer.Finish(write);
+ return std::nullopt;
+ }
+
+ writer.Write(static_cast<uint32_t>(OoTAnimationType::Normal));
+ writer.Write(anim->frameCount);
+
+ writer.Write(static_cast<uint32_t>(anim->rotationValues.size()));
+ for (auto& val : anim->rotationValues) {
+ writer.Write(val);
+ }
+
+ writer.Write(static_cast<uint32_t>(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
diff --git a/src/factories/oot/OoTAnimationFactory.h b/src/factories/oot/OoTAnimationFactory.h
new file mode 100644
index 0000000..6d243ea
--- /dev/null
+++ b/src/factories/oot/OoTAnimationFactory.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "OoTAnimationTypes.h"
+#include <vector>
+#include <optional>
+
+namespace OoT {
+
+class OoTNormalAnimationData : public IParsedData {
+public:
+ int16_t frameCount;
+ std::vector<uint16_t> rotationValues;
+ std::vector<RotationIndex> rotationIndices;
+ int16_t limit;
+ bool isLegacy = false;
+};
+
+class OoTAnimationBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTAnimationFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTAnimationBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTAnimationTypes.h b/src/factories/oot/OoTAnimationTypes.h
new file mode 100644
index 0000000..16faa5f
--- /dev/null
+++ b/src/factories/oot/OoTAnimationTypes.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <cstdint>
+
+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
diff --git a/src/factories/oot/OoTArrayFactory.cpp b/src/factories/oot/OoTArrayFactory.cpp
new file mode 100644
index 0000000..8014de9
--- /dev/null
+++ b/src/factories/oot/OoTArrayFactory.cpp
@@ -0,0 +1,118 @@
+#include "OoTArrayFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+static std::shared_ptr<OoTVtxArrayData> parseVtxArray(DataChunk& segment, size_t count) {
+ LUS::BinaryReader reader(segment.data, count * sizeof(VtxRaw));
+ reader.SetEndianness(Torch::Endianness::Big);
+ std::vector<VtxRaw> 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<OoTVtxArrayData>(vertices);
+}
+
+static std::shared_ptr<OoTVec3sArrayData> parseVec3sArray(DataChunk& segment, size_t count) {
+ LUS::BinaryReader reader(segment.data, count * 6);
+ reader.SetEndianness(Torch::Endianness::Big);
+ std::vector<Vec3s> 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<OoTVec3sArrayData>(vecs);
+}
+
+std::optional<std::shared_ptr<IParsedData>> OoTArrayFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto count = GetSafeNode<size_t>(node, "count");
+ auto arrayType = GetSafeNode<std::string>(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<OoTVtxArrayData> data, bool zeroFlag) {
+ writer.Write(static_cast<uint32_t>(SohArrayType::Vertex));
+ writer.Write(static_cast<uint32_t>(data->mVtxs.size()));
+
+ for (const auto& v : data->mVtxs) {
+ writer.Write(v.ob[0]);
+ writer.Write(v.ob[1]);
+ writer.Write(v.ob[2]);
+ // ZAPD zeroes the flag for display-list-discovered vertices
+ // (DisplayListExporter's VTX() text round-trip) but preserves it for
+ // XML-declared arrays. zero_flag (set by zapd_to_torch for supplemental
+ // VTX arrays) selects the discovered behaviour.
+ writer.Write(zeroFlag ? static_cast<uint16_t>(0) : 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<OoTVec3sArrayData> data) {
+ writer.Write(static_cast<uint32_t>(SohArrayType::Vector));
+ writer.Write(static_cast<uint32_t>(data->mVecs.size()));
+
+ for (const auto& v : data->mVecs) {
+ // Per-element: scalar_type (u32) + dimensions (u32) + data
+ writer.Write(static_cast<uint32_t>(SohScalarType::ZSCALAR_S16));
+ writer.Write(static_cast<uint32_t>(3));
+ writer.Write(v.x);
+ writer.Write(v.y);
+ writer.Write(v.z);
+ }
+}
+
+ExportResult OoTArrayBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto arrayType = GetSafeNode<std::string>(node, "array_type");
+
+ WriteHeader(writer, Torch::ResourceType::Array, 0);
+
+ if (arrayType == "VTX") {
+ bool zeroFlag = node["zero_flag"] && node["zero_flag"].as<bool>();
+ exportVtxArray(writer, std::static_pointer_cast<OoTVtxArrayData>(raw), zeroFlag);
+ } else if (arrayType == "Vec3s") {
+ exportVec3sArray(writer, std::static_pointer_cast<OoTVec3sArrayData>(raw));
+ }
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTArrayFactory.h b/src/factories/oot/OoTArrayFactory.h
new file mode 100644
index 0000000..292f386
--- /dev/null
+++ b/src/factories/oot/OoTArrayFactory.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "factories/VtxFactory.h"
+#include <types/Vec3D.h>
+#include <variant>
+#include <vector>
+
+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<VtxRaw> mVtxs;
+ explicit OoTVtxArrayData(std::vector<VtxRaw> vtxs) : mVtxs(std::move(vtxs)) {}
+};
+
+// Parsed data for OoT Array (Vec3s variant)
+class OoTVec3sArrayData : public IParsedData {
+public:
+ std::vector<Vec3s> mVecs;
+ explicit OoTVec3sArrayData(std::vector<Vec3s> vecs) : mVecs(std::move(vecs)) {}
+};
+
+class OoTArrayBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTArrayFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTArrayBinaryExporter)
+ };
+ }
+ uint32_t GetAlignment() override { return 8; };
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTAudioFactory.cpp b/src/factories/oot/OoTAudioFactory.cpp
new file mode 100644
index 0000000..f4b1d6d
--- /dev/null
+++ b/src/factories/oot/OoTAudioFactory.cpp
@@ -0,0 +1,132 @@
+#include "OoTAudioFactory.h"
+#include "OoTAudioSequenceWriter.h"
+#include "OoTAudioSampleWriter.h"
+#include "OoTAudioFontWriter.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include <sstream>
+
+namespace OoT {
+
+std::vector<AudioTableEntry> 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<AudioTableEntry> 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<std::vector<uint8_t>> OoTAudioFactory::ParseSequenceFontTable(
+ const uint8_t* codeData, uint32_t tableOffset, uint32_t numSequences) {
+ std::vector<std::vector<uint8_t>> result;
+ result.reserve(numSequences);
+
+ LUS::BinaryReader reader((char*)(codeData + tableOffset), 0x10000);
+ reader.SetEndianness(Torch::Endianness::Big);
+
+ std::vector<uint16_t> 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<uint8_t> fonts;
+ for (uint8_t j = 0; j < count; j++) {
+ fonts.push_back(dataReader.ReadUByte());
+ }
+ result.push_back(fonts);
+ }
+ return result;
+}
+
+std::vector<char> 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<char>(str.begin(), str.end());
+}
+
+std::optional<SafeAudioBankReader> OoTAudioFactory::LoadAudioBank(std::vector<uint8_t>& 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<uint8_t>(buffer.begin() + bankOff, buffer.begin() + bankOff + bankSize));
+}
+
+std::optional<std::shared_ptr<IParsedData>> OoTAudioFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto data = std::make_shared<OoTAudioData>();
+ data->mMainEntry = BuildMainAudioHeader();
+
+ // Decompress the code segment (segment 128)
+ auto codeDecoded = Decompressor::AutoDecode(
+ node["offset"].as<uint32_t>(),
+ 0x200000, // max code segment size
+ buffer);
+
+ // Parse audio tables from code segment at YAML-specified offsets
+ auto seqTable = ParseAudioTable(codeDecoded.segment.data, GetSafeNode<uint32_t>(node, "sequence_table_offset"));
+ auto fontTable = ParseAudioTable(codeDecoded.segment.data, GetSafeNode<uint32_t>(node, "sound_font_table_offset"));
+ auto sampleBankTable = ParseAudioTable(codeDecoded.segment.data, GetSafeNode<uint32_t>(node, "sample_bank_table_offset"));
+ auto seqFontMap = ParseSequenceFontTable(codeDecoded.segment.data, GetSafeNode<uint32_t>(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<uint32_t, SampleInfo> 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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto audio = std::static_pointer_cast<OoTAudioData>(raw);
+ write.write(audio->mMainEntry.data(), audio->mMainEntry.size());
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTAudioFactory.h b/src/factories/oot/OoTAudioFactory.h
new file mode 100644
index 0000000..376a761
--- /dev/null
+++ b/src/factories/oot/OoTAudioFactory.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include "OoTAudioTypes.h"
+
+namespace OoT {
+
+class OoTAudioData : public IParsedData {
+public:
+ std::vector<char> mMainEntry;
+};
+
+class OoTAudioBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTAudioFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTAudioBinaryExporter)
+ };
+ }
+
+private:
+ std::vector<char> BuildMainAudioHeader();
+ std::optional<SafeAudioBankReader> LoadAudioBank(std::vector<uint8_t>& buffer);
+ std::vector<AudioTableEntry> ParseAudioTable(const uint8_t* codeData, uint32_t tableOffset);
+ std::vector<std::vector<uint8_t>> ParseSequenceFontTable(const uint8_t* codeData,
+ uint32_t tableOffset, uint32_t numSequences);
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTAudioFontWriter.cpp b/src/factories/oot/OoTAudioFontWriter.cpp
new file mode 100644
index 0000000..9afb0e9
--- /dev/null
+++ b/src/factories/oot/OoTAudioFontWriter.cpp
@@ -0,0 +1,333 @@
+#include "OoTAudioFontWriter.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include <sstream>
+
+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<std::pair<int16_t, int16_t>> AudioFontWriter::ParseEnvelope(uint32_t envOffset, FontWriteContext& ctx) {
+ std::vector<std::pair<int16_t, int16_t>> 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<std::pair<int16_t, int16_t>>& envs) {
+ w.Write(static_cast<uint32_t>(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<uint8_t>(0));
+ return;
+ }
+ uint32_t samplePtr = ctx.audioBank.ReadU32(sfeOffset);
+ if (samplePtr == 0) {
+ w.Write(static_cast<uint8_t>(0));
+ return;
+ }
+ w.Write(static_cast<uint8_t>(1)); // exists
+ w.Write(static_cast<uint8_t>(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<DrumEntry>& 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<InstEntry> AudioFontWriter::ParseInstruments(int numInstruments, uint32_t ptr,
+ FontWriteContext& ctx) {
+ std::vector<InstEntry> 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<SFXEntry> AudioFontWriter::ParseSFX(int numSfx, uint32_t ptr, int sampleBankId,
+ FontWriteContext& ctx) {
+ std::vector<SFXEntry> 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<DrumEntry> AudioFontWriter::ParseDrums(int numDrums, uint32_t ptr, int sampleBankId,
+ FontWriteContext& ctx) {
+ std::vector<DrumEntry> 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<DrumEntry>& 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<uint8_t>(d.sampleRef.empty() ? 0 : 1));
+ w.Write(d.sampleRef);
+ w.Write(d.tuning);
+ }
+}
+
+void AudioFontWriter::WriteInstruments(LUS::BinaryWriter& w, const std::vector<InstEntry>& instruments,
+ uint32_t ptr, int sampleBankId, FontWriteContext& ctx) {
+ for (auto& inst : instruments) {
+ w.Write(static_cast<uint8_t>(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<uint8_t>(0));
+ }
+ if (ctx.audioBank.ReadU32(inst.normalAddr) != 0) {
+ WriteSFE(w, inst.normalAddr, ptr, sampleBankId, ctx);
+ } else {
+ w.Write(static_cast<uint8_t>(0));
+ }
+ if (ctx.audioBank.ReadU32(inst.highAddr) != 0 && inst.normalRangeHi != 0x7F) {
+ WriteSFE(w, inst.highAddr, ptr, sampleBankId, ctx);
+ } else {
+ w.Write(static_cast<uint8_t>(0));
+ }
+ } else {
+ w.Write(static_cast<uint8_t>(0));
+ w.Write(static_cast<uint8_t>(0));
+ w.Write(static_cast<uint8_t>(0));
+ }
+ }
+}
+
+void AudioFontWriter::WriteSFXEntries(LUS::BinaryWriter& w, const std::vector<SFXEntry>& sfxEntries) {
+ for (auto& sfx : sfxEntries) {
+ if (sfx.exists) {
+ w.Write(static_cast<uint8_t>(1));
+ w.Write(static_cast<uint8_t>(1));
+ w.Write(sfx.sampleRef);
+ w.Write(sfx.tuning);
+ } else {
+ w.Write(static_cast<uint8_t>(0));
+ }
+ }
+}
+
+void AudioFontWriter::WriteFontCompanion(uint32_t fontIndex, const AudioTableEntry& fontEntry,
+ const std::vector<DrumEntry>& drums,
+ const std::vector<InstEntry>& instruments,
+ const std::vector<SFXEntry>& sfxEntries,
+ uint32_t ptr, int sampleBankId,
+ FontWriteContext& ctx,
+ const std::map<int, std::string>& fontNames) {
+ LUS::BinaryWriter w;
+ BaseExporter::WriteHeader(w, Torch::ResourceType::OoTAudioSoundFont, 2);
+
+ // Font metadata
+ w.Write(static_cast<uint32_t>(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<uint32_t>(drums.size()));
+ w.Write(static_cast<uint32_t>(instruments.size()));
+ w.Write(static_cast<uint32_t>(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<char>(str.begin(), str.end()));
+}
+
+void AudioFontWriter::Extract(YAML::Node& node,
+ SafeAudioBankReader& audioBank,
+ const std::vector<AudioTableEntry>& fontTable,
+ const std::vector<AudioTableEntry>& sampleBankTable,
+ std::map<uint32_t, SampleInfo>& sampleMap) {
+ // Build font name map from YAML
+ std::map<int, std::string> 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<int>()] = fn["name"].as<std::string>();
+ }
+ }
+
+ 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
diff --git a/src/factories/oot/OoTAudioFontWriter.h b/src/factories/oot/OoTAudioFontWriter.h
new file mode 100644
index 0000000..615ea7f
--- /dev/null
+++ b/src/factories/oot/OoTAudioFontWriter.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "OoTAudioTypes.h"
+
+namespace OoT {
+
+struct DrumEntry {
+ uint8_t releaseRate, pan, loaded;
+ float tuning;
+ std::vector<std::pair<int16_t, int16_t>> env;
+ std::string sampleRef;
+};
+
+struct InstEntry {
+ bool isValid;
+ uint8_t loaded, normalRangeLo, normalRangeHi, releaseRate;
+ std::vector<std::pair<int16_t, int16_t>> 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<DrumEntry>& 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<AudioTableEntry>& sampleBankTable;
+ std::map<uint32_t, SampleInfo>& sampleMap;
+};
+
+class AudioFontWriter {
+public:
+ void Extract(YAML::Node& node,
+ SafeAudioBankReader& audioBank,
+ const std::vector<AudioTableEntry>& fontTable,
+ const std::vector<AudioTableEntry>& sampleBankTable,
+ std::map<uint32_t, SampleInfo>& sampleMap);
+
+private:
+ std::string GetSampleRef(int bankIndex, uint32_t sampleAddr, uint32_t baseOffset, FontWriteContext& ctx);
+ std::vector<std::pair<int16_t, int16_t>> 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<DrumEntry>& drums,
+ const std::vector<InstEntry>& instruments,
+ const std::vector<SFXEntry>& sfxEntries,
+ uint32_t ptr, int sampleBankId,
+ FontWriteContext& ctx,
+ const std::map<int, std::string>& fontNames);
+ void WriteDrums(LUS::BinaryWriter& w, const std::vector<DrumEntry>& drums);
+ void WriteInstruments(LUS::BinaryWriter& w, const std::vector<InstEntry>& instruments,
+ uint32_t ptr, int sampleBankId, FontWriteContext& ctx);
+ void WriteSFXEntries(LUS::BinaryWriter& w, const std::vector<SFXEntry>& sfxEntries);
+ void WriteEnvData(LUS::BinaryWriter& w, const std::vector<std::pair<int16_t, int16_t>>& envs);
+ std::vector<InstEntry> ParseInstruments(int numInstruments, uint32_t ptr, FontWriteContext& ctx);
+ std::vector<SFXEntry> ParseSFX(int numSfx, uint32_t ptr, int sampleBankId, FontWriteContext& ctx);
+ std::vector<DrumEntry> ParseDrums(int numDrums, uint32_t ptr, int sampleBankId, FontWriteContext& ctx);
+
+ FontResidue mResidue;
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTAudioSampleWriter.cpp b/src/factories/oot/OoTAudioSampleWriter.cpp
new file mode 100644
index 0000000..e8b41c6
--- /dev/null
+++ b/src/factories/oot/OoTAudioSampleWriter.cpp
@@ -0,0 +1,203 @@
+#include "OoTAudioSampleWriter.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include <iomanip>
+#include <sstream>
+
+namespace OoT {
+
+std::map<int, std::map<int, std::string>> AudioSampleWriter::ParseSampleNames(YAML::Node& node) {
+ std::map<int, std::map<int, std::string>> 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<int>();
+ 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<int>()] = e["name"].as<std::string>();
+ }
+ }
+ }
+ }
+ 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<uint32_t, SampleInfo>& sampleMap,
+ const std::vector<uint8_t>& 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<uint32_t>(s.dataSize));
+ if (s.dataOffset + s.dataSize <= audioTable.size()) {
+ w.Write((char*)(audioTable.data() + s.dataOffset), s.dataSize);
+ }
+
+ w.Write(static_cast<uint32_t>(s.loopStart));
+ w.Write(static_cast<uint32_t>(s.loopEnd));
+ w.Write(static_cast<uint32_t>(s.loopCount));
+ w.Write(static_cast<uint32_t>(s.loopStates.size()));
+ for (auto ls : s.loopStates) w.Write(ls);
+
+ w.Write(static_cast<uint32_t>(s.bookOrder));
+ w.Write(static_cast<uint32_t>(s.bookNpredictors));
+ w.Write(static_cast<uint32_t>(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<char>(str.begin(), str.end()));
+ }
+
+ SPDLOG_INFO("OoTAudioFactory: wrote {} sample companion files", sampleMap.size());
+}
+
+void AudioSampleWriter::DiscoverSamples(const std::vector<AudioTableEntry>& 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<uint8_t>& buffer, YAML::Node& node,
+ SafeAudioBankReader& audioBank,
+ const std::vector<AudioTableEntry>& fontTable,
+ const std::vector<AudioTableEntry>& sampleBankTable,
+ std::map<uint32_t, SampleInfo>& 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<uint8_t> 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
diff --git a/src/factories/oot/OoTAudioSampleWriter.h b/src/factories/oot/OoTAudioSampleWriter.h
new file mode 100644
index 0000000..5522402
--- /dev/null
+++ b/src/factories/oot/OoTAudioSampleWriter.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "OoTAudioTypes.h"
+
+namespace OoT {
+
+class AudioSampleWriter {
+public:
+ bool Extract(std::vector<uint8_t>& buffer, YAML::Node& node,
+ SafeAudioBankReader& audioBank,
+ const std::vector<AudioTableEntry>& fontTable,
+ const std::vector<AudioTableEntry>& sampleBankTable,
+ std::map<uint32_t, SampleInfo>& sampleMap);
+
+private:
+ void DiscoverSamples(const std::vector<AudioTableEntry>& fontTable, AudioParseContext& ctx);
+ void WriteCompanionFiles(const std::map<uint32_t, SampleInfo>& sampleMap,
+ const std::vector<uint8_t>& audioTable);
+ std::map<int, std::map<int, std::string>> 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
diff --git a/src/factories/oot/OoTAudioSequenceWriter.cpp b/src/factories/oot/OoTAudioSequenceWriter.cpp
new file mode 100644
index 0000000..3863aff
--- /dev/null
+++ b/src/factories/oot/OoTAudioSequenceWriter.cpp
@@ -0,0 +1,93 @@
+#include "OoTAudioSequenceWriter.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include <iomanip>
+#include <sstream>
+
+namespace OoT {
+
+void AudioSequenceWriter::WriteCompanion(const uint8_t* seqData, uint32_t seqSize,
+ uint32_t originalIndex, uint8_t medium, uint8_t cachePolicy,
+ const std::vector<uint8_t>& 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<uint8_t>(originalIndex));
+ w.Write(medium);
+ w.Write(cachePolicy);
+
+ w.Write(static_cast<uint32_t>(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<char>(str.begin(), str.end()));
+}
+
+bool AudioSequenceWriter::Extract(std::vector<uint8_t>& buffer, YAML::Node& node,
+ const std::vector<AudioTableEntry>& seqTable,
+ const std::vector<std::vector<uint8_t>>& 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<std::string> 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<std::string>());
+ }
+ }
+
+ 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
diff --git a/src/factories/oot/OoTAudioSequenceWriter.h b/src/factories/oot/OoTAudioSequenceWriter.h
new file mode 100644
index 0000000..1d98f9b
--- /dev/null
+++ b/src/factories/oot/OoTAudioSequenceWriter.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "OoTAudioTypes.h"
+
+namespace OoT {
+
+class AudioSequenceWriter {
+public:
+ bool Extract(std::vector<uint8_t>& buffer, YAML::Node& node,
+ const std::vector<AudioTableEntry>& seqTable,
+ const std::vector<std::vector<uint8_t>>& seqFontMap);
+
+private:
+ void WriteCompanion(const uint8_t* seqData, uint32_t seqSize,
+ uint32_t originalIndex, uint8_t medium, uint8_t cachePolicy,
+ const std::vector<uint8_t>& fonts, const std::string& seqName);
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTAudioTypes.cpp b/src/factories/oot/OoTAudioTypes.cpp
new file mode 100644
index 0000000..3287331
--- /dev/null
+++ b/src/factories/oot/OoTAudioTypes.cpp
@@ -0,0 +1,34 @@
+#include "OoTAudioTypes.h"
+
+namespace OoT {
+
+SafeAudioBankReader::SafeAudioBankReader(std::vector<uint8_t> 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
diff --git a/src/factories/oot/OoTAudioTypes.h b/src/factories/oot/OoTAudioTypes.h
new file mode 100644
index 0000000..1d493df
--- /dev/null
+++ b/src/factories/oot/OoTAudioTypes.h
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include <map>
+
+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<uint8_t> 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<uint8_t> 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<int16_t> loopStates;
+ int32_t bookOrder, bookNpredictors;
+ std::vector<int16_t> books;
+ std::string name;
+};
+
+struct AudioParseContext {
+ SafeAudioBankReader& audioBank;
+ const std::vector<AudioTableEntry>& sampleBankTable;
+ std::map<int, std::map<int, std::string>> sampleNames;
+ std::map<uint32_t, SampleInfo>& sampleMap;
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTCollisionFactory.cpp b/src/factories/oot/OoTCollisionFactory.cpp
new file mode 100644
index 0000000..bd71199
--- /dev/null
+++ b/src/factories/oot/OoTCollisionFactory.cpp
@@ -0,0 +1,331 @@
+#include "OoTCollisionFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+static void parseCameraData(std::vector<uint8_t>& 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<uint32_t>(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<std::shared_ptr<IParsedData>> OoTCollisionFactory::parse(std::vector<uint8_t>& 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<OoTCollisionData>();
+
+ // 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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto col = std::static_pointer_cast<OoTCollisionData>(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<uint32_t>(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<uint32_t>(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<uint32_t>(col->surfaceTypes.size()));
+ for (auto& st : col->surfaceTypes) {
+ writer.Write(st.data1);
+ writer.Write(st.data0);
+ }
+
+ // Camera data entries
+ writer.Write(static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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
diff --git a/src/factories/oot/OoTCollisionFactory.h b/src/factories/oot/OoTCollisionFactory.h
new file mode 100644
index 0000000..f842a38
--- /dev/null
+++ b/src/factories/oot/OoTCollisionFactory.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include <vector>
+#include <string>
+#include <optional>
+
+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<CollisionVertex> vertices;
+ std::vector<CollisionPoly> polygons;
+ std::vector<SurfaceType> surfaceTypes;
+ std::vector<CamDataEntry> camDataEntries;
+ std::vector<CamPosData> camPositions;
+ std::vector<WaterBox> waterBoxes;
+};
+
+class OoTCollisionBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTCollisionFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTCollisionBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTCurveAnimationFactory.cpp b/src/factories/oot/OoTCurveAnimationFactory.cpp
new file mode 100644
index 0000000..d495324
--- /dev/null
+++ b/src/factories/oot/OoTCurveAnimationFactory.cpp
@@ -0,0 +1,134 @@
+#include "OoTCurveAnimationFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+std::optional<std::shared_ptr<IParsedData>> OoTCurveAnimationFactory::parse(std::vector<uint8_t>& 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<OoTCurveAnimationData>();
+ 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<uint32_t>(node, "offset");
+ uint8_t segNum = SEGMENT_NUMBER(animOffset);
+ uint32_t skelOffset = (segNum << 24) | GetSafeNode<uint32_t>(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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto anim = std::static_pointer_cast<OoTCurveAnimationData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTAnimation, 0);
+
+ writer.Write(static_cast<uint32_t>(OoTAnimationType::Curve));
+ writer.Write(anim->frameCount);
+
+ writer.Write(static_cast<uint32_t>(anim->refIndexArr.size()));
+ for (auto& val : anim->refIndexArr) {
+ writer.Write(val);
+ }
+
+ writer.Write(static_cast<uint32_t>(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<uint32_t>(anim->copyValuesArr.size()));
+ for (auto& val : anim->copyValuesArr) {
+ writer.Write(val);
+ }
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTCurveAnimationFactory.h b/src/factories/oot/OoTCurveAnimationFactory.h
new file mode 100644
index 0000000..55b837b
--- /dev/null
+++ b/src/factories/oot/OoTCurveAnimationFactory.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "OoTAnimationTypes.h"
+#include <vector>
+
+namespace OoT {
+
+class OoTCurveAnimationData : public IParsedData {
+public:
+ int16_t frameCount;
+ std::vector<uint8_t> refIndexArr;
+ std::vector<CurveInterpKnot> transformDataArr;
+ std::vector<int16_t> copyValuesArr;
+};
+
+class OoTCurveAnimationBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTCurveAnimationFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTCurveAnimationBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTCutsceneFactory.cpp b/src/factories/oot/OoTCutsceneFactory.cpp
new file mode 100644
index 0000000..da5ac76
--- /dev/null
+++ b/src/factories/oot/OoTCutsceneFactory.cpp
@@ -0,0 +1,323 @@
+#include "OoTCutsceneFactory.h"
+#include "OoTSceneUtils.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+
+#include <set>
+
+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<uint32_t> 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<uint8_t>& 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<uint32_t>(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<uint32_t>(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<char> CutsceneSerializer::Serialize(std::vector<uint8_t>& 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<char> CutsceneSerializer::Write(std::vector<uint8_t>& 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<uint32_t>(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<uint32_t>(0xFFFFFFFF));
+ w.Write(static_cast<uint32_t>(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<uint32_t>((dataEndPos - dataStartPos) / 4));
+ w.Seek(dataEndPos, LUS::SeekOffsetType::Start);
+
+ std::stringstream ss;
+ w.Finish(ss);
+ std::string str = ss.str();
+ return std::vector<char>(str.begin(), str.end());
+}
+
+class OoTCutsceneData : public IParsedData {
+public:
+ std::vector<char> mBinary;
+ OoTCutsceneData(std::vector<char> data) : mBinary(std::move(data)) {}
+};
+
+std::optional<std::shared_ptr<IParsedData>> OoTCutsceneFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto data = CutsceneSerializer::Serialize(buffer, GetSafeNode<uint32_t>(node, "offset"));
+ if (data.empty()) {
+ SPDLOG_WARN("OoTCutsceneFactory: Failed to serialize cutscene");
+ return std::nullopt;
+ }
+ return std::make_shared<OoTCutsceneData>(std::move(data));
+}
+
+ExportResult OoTCutsceneBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto cs = std::static_pointer_cast<OoTCutsceneData>(raw);
+ write.write(cs->mBinary.data(), cs->mBinary.size());
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTCutsceneFactory.h b/src/factories/oot/OoTCutsceneFactory.h
new file mode 100644
index 0000000..bbf4685
--- /dev/null
+++ b/src/factories/oot/OoTCutsceneFactory.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+
+namespace OoT {
+
+class OoTCutsceneBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTCutsceneFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTCutsceneBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTDListHelpers.cpp b/src/factories/oot/OoTDListHelpers.cpp
new file mode 100644
index 0000000..6e81477
--- /dev/null
+++ b/src/factories/oot/OoTDListHelpers.cpp
@@ -0,0 +1,759 @@
+#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 <iomanip>
+#include <sstream>
+#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<std::string>(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<std::pair<std::string, uint32_t>> rangedMatch;
+
+ for (const auto& type : std::vector<std::string>{"TEXTURE", "BLOB"}) {
+ auto decs = Companion::Instance->GetNodesByType(type);
+ if (!decs.has_value()) continue;
+ for (auto& [name, dnode] : decs.value()) {
+ auto doffset = GetSafeNode<uint32_t>(dnode, "offset");
+ uint32_t dsize = 0;
+ if (type == "TEXTURE") {
+ auto fmt = GetSafeNode<std::string>(dnode, "format");
+ auto w = GetSafeNode<uint32_t>(dnode, "width");
+ auto h = GetSafeNode<uint32_t>(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<uint32_t>(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<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(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('/'));
+ bool nullCrossFile = ovnode["null_cross_file"] && ovnode["null_cross_file"].as<bool>();
+ if (currentDir != vtxDir && nullCrossFile) {
+ 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<uint32_t>(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<std::string> dec = std::nullopt;
+ bool nullCrossFile = false;
+ if (vtxNode.has_value()) {
+ auto [vpath, vn] = vtxNode.value();
+ auto vtype = GetSafeNode<std::string>(vn, "type");
+ if (vtype == "VTX" || vtype == "OOT:ARRAY") {
+ dec = vpath;
+ nullCrossFile = vn["null_cross_file"] && vn["null_cross_file"].as<bool>();
+ }
+ }
+ if (dec.has_value()) {
+ auto currentDir = (*replacement).substr(0, (*replacement).rfind('/'));
+ auto vtxDir = dec.value().substr(0, dec.value().rfind('/'));
+ if (currentDir != vtxDir && nullCrossFile) {
+ 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<std::string> 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) {
+ // OTRExporter has no G_MOVEMEM handler: its DisplayListExporter switch falls
+ // through to the default (undefined-opcode) case, which emits just the opcode
+ // byte with all operands zeroed (w0 = G_MOVEMEM << 24, w1 = 0). Match that
+ // for byte-parity — a single 8-byte command written by the caller. (The
+ // generic DisplayListFactory path keeps the G_MOVEMEM_OTR_HASH form for
+ // other games; OoT follows OTRExporter here.)
+ w0 = F3DEX2_MOVEMEM << 24;
+ w1 = 0;
+ // 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<uint32_t>(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<uint32_t>& 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<uint32_t>(hash >> 32));
+ writer.Write(static_cast<uint32_t>(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<uint8_t> 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<uint32_t>()) == SEGMENT_NUMBER(w1)) {
+ // OoT pre-declares child DLists in YAML — skip AddAsset
+ auto parentSymbol = GetSafeNode<std::string>(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<uint8_t>& buffer) {
+ if (!IS_SEGMENTED(w1) || SEGMENT_NUMBER(w1) != SEGMENT_NUMBER(node["offset"].as<uint32_t>())) {
+ 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<uint32_t>())) {
+ 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<uint8_t>& 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<uint32_t>(vtx, "offset");
+ auto lCount = GetSafeNode<uint32_t>(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<std::string>(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<std::tuple<std::string, YAML::Node>> SearchVtx(uint32_t ptr) {
+ if (Companion::Instance->GetGBIMinorVersion() != GBIMinorVersion::OoT) return std::nullopt;
+
+ std::vector<std::string> 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<std::string>(node, "array_type", "");
+ if (arrayType != "VTX") continue;
+ }
+
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ auto count = GetSafeNode<uint32_t>(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<std::string>(node, "symbol", name), node);
+ }
+ }
+ }
+
+ return std::nullopt;
+}
+
+std::optional<ExportResult> Export(std::ostream& write, std::shared_ptr<IParsedData> 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<DListData>(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<int8_t>(0xFF));
+
+ auto bhash = CRC64((*replacement).c_str());
+ writer.Write(static_cast<uint32_t>((G_MARKER << 24)));
+ writer.Write(0xBEEFBEEF);
+ writer.Write(static_cast<uint32_t>(bhash >> 32));
+ writer.Write(static_cast<uint32_t>(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);
+ }
+
+ 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<uint32_t>(0));
+ writer.Write(static_cast<uint32_t>(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<std::optional<std::shared_ptr<IParsedData>>> Parse(
+ std::vector<uint8_t>& raw_buffer, YAML::Node& node) {
+ if (Companion::Instance->GetGBIMinorVersion() != GBIMinorVersion::OoT) return std::nullopt;
+
+ auto count = GetSafeNode<int32_t>(node, "count", -1);
+ auto [_, segment] = Decompressor::AutoDecode(node, raw_buffer);
+ LUS::BinaryReader reader(segment.data, segment.size);
+ reader.SetEndianness(Torch::Endianness::Big);
+
+ std::vector<uint32_t> 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<IParsedData>(std::make_shared<DListData>(gfxs))));
+}
+
+} // namespace DListHelpers
+} // namespace OoT
diff --git a/src/factories/oot/OoTDListHelpers.h b/src/factories/oot/OoTDListHelpers.h
new file mode 100644
index 0000000..6ed9ff5
--- /dev/null
+++ b/src/factories/oot/OoTDListHelpers.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include <string>
+#include <optional>
+#include <tuple>
+#include <vector>
+
+namespace OoT {
+namespace DListHelpers {
+
+// OoT replacement for SearchVtx. Returns result if handled, nullopt to fall through to main.
+std::optional<std::tuple<std::string, YAML::Node>> SearchVtx(uint32_t ptr);
+
+// OoT replacement for DListBinaryExporter::Export. Returns result if handled, nullopt to fall through to main.
+std::optional<ExportResult> Export(std::ostream& write, std::shared_ptr<IParsedData> 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<std::optional<std::shared_ptr<IParsedData>>> Parse(
+ std::vector<uint8_t>& raw_buffer, YAML::Node& node);
+
+} // namespace DListHelpers
+} // namespace OoT
diff --git a/src/factories/oot/OoTLimbFactory.cpp b/src/factories/oot/OoTLimbFactory.cpp
new file mode 100644
index 0000000..00e2ce6
--- /dev/null
+++ b/src/factories/oot/OoTLimbFactory.cpp
@@ -0,0 +1,272 @@
+#include "OoTLimbFactory.h"
+#include "OoTSkeletonTypes.h"
+#include "OoTSceneUtils.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+#include <sstream>
+#include <iomanip>
+
+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<uint8_t>& buffer, uint32_t addr, uint16_t count,
+ std::vector<OoTSkinTransformation>& 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<uint8_t>& buffer, uint32_t addr, uint16_t count,
+ std::vector<OoTSkinVertex>& 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<uint8_t>& 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<uint8_t>& buffer,
+ OoTLimbData& limb, const std::string& symbol) {
+ ParseLimbHeader(reader, limb);
+ limb.skinSegmentType = static_cast<OoTLimbSkinType>(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<std::shared_ptr<IParsedData>> OoTLimbFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto limbTypeStr = GetSafeNode<std::string>(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<OoTLimbData>();
+ limb->limbType = limbType;
+ auto symbol = GetSafeNode<std::string>(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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto limb = std::static_pointer_cast<OoTLimbData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTSkeletonLimb, 0);
+
+ writer.Write(static_cast<uint8_t>(limb->limbType));
+ writer.Write(static_cast<uint8_t>(limb->skinSegmentType));
+ writer.Write(limb->skinDList);
+ writer.Write(limb->skinVtxCnt);
+ writer.Write(static_cast<uint32_t>(limb->skinAnimData.limbModifications.size()));
+
+ for (auto& modif : limb->skinAnimData.limbModifications) {
+ writer.Write(modif.unk_4);
+
+ writer.Write(static_cast<uint32_t>(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<uint32_t>(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
diff --git a/src/factories/oot/OoTLimbFactory.h b/src/factories/oot/OoTLimbFactory.h
new file mode 100644
index 0000000..b9f88cc
--- /dev/null
+++ b/src/factories/oot/OoTLimbFactory.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "OoTSkeletonTypes.h"
+
+namespace OoT {
+
+class OoTLimbBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTLimbFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> 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<uint8_t>& buffer,
+ OoTLimbData& limb, const std::string& symbol);
+ void ParseAnimatedSkinData(std::vector<uint8_t>& buffer, uint32_t skinSegmentAddr,
+ OoTLimbData& limb, const std::string& symbol);
+ void ParseSkinVertices(std::vector<uint8_t>& buffer, uint32_t addr, uint16_t count,
+ std::vector<OoTSkinVertex>& out);
+ void ParseSkinTransformations(std::vector<uint8_t>& buffer, uint32_t addr, uint16_t count,
+ std::vector<OoTSkinTransformation>& out);
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTMtxFactory.cpp b/src/factories/oot/OoTMtxFactory.cpp
new file mode 100644
index 0000000..f0752be
--- /dev/null
+++ b/src/factories/oot/OoTMtxFactory.cpp
@@ -0,0 +1,38 @@
+#include "OoTMtxFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+std::optional<std::shared_ptr<IParsedData>> OoTMtxFactory::parse(std::vector<uint8_t>& 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<int32_t, 16> rawInts;
+ for (size_t i = 0; i < 16; i++) {
+ rawInts[i] = reader.ReadInt32();
+ }
+
+ return std::make_shared<OoTMtxData>(rawInts);
+}
+
+ExportResult OoTMtxBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto data = std::static_pointer_cast<OoTMtxData>(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
diff --git a/src/factories/oot/OoTMtxFactory.h b/src/factories/oot/OoTMtxFactory.h
new file mode 100644
index 0000000..27be82d
--- /dev/null
+++ b/src/factories/oot/OoTMtxFactory.h
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include <array>
+
+namespace OoT {
+
+class OoTMtxData : public IParsedData {
+public:
+ std::array<int32_t, 16> rawInts;
+ explicit OoTMtxData(std::array<int32_t, 16> data) : rawInts(data) {}
+};
+
+class OoTMtxBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTMtxFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTMtxBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTPathFactory.cpp b/src/factories/oot/OoTPathFactory.cpp
new file mode 100644
index 0000000..00e808b
--- /dev/null
+++ b/src/factories/oot/OoTPathFactory.cpp
@@ -0,0 +1,49 @@
+#include "OoTPathFactory.h"
+#include "OoTSceneUtils.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+
+namespace OoT {
+
+class OoTPathData : public IParsedData {
+public:
+ std::vector<char> mBinary;
+ OoTPathData(std::vector<char> data) : mBinary(std::move(data)) {}
+};
+
+std::optional<std::shared_ptr<IParsedData>> OoTPathFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto offset = GetSafeNode<uint32_t>(node, "offset");
+ uint32_t numPaths = node["num_paths"] ? node["num_paths"].as<uint32_t>() : 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<std::pair<uint8_t, uint32_t>> 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<OoTPathData>(std::move(data));
+}
+
+ExportResult OoTPathBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto path = std::static_pointer_cast<OoTPathData>(raw);
+ write.write(path->mBinary.data(), path->mBinary.size());
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTPathFactory.h b/src/factories/oot/OoTPathFactory.h
new file mode 100644
index 0000000..81e5d16
--- /dev/null
+++ b/src/factories/oot/OoTPathFactory.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+
+namespace OoT {
+
+class OoTPathBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTPathFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTPathBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTPlayerAnimationFactory.cpp b/src/factories/oot/OoTPlayerAnimationFactory.cpp
new file mode 100644
index 0000000..5e573bd
--- /dev/null
+++ b/src/factories/oot/OoTPlayerAnimationFactory.cpp
@@ -0,0 +1,91 @@
+#include "OoTPlayerAnimationFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+// ==================== OoT Player Animation Header Factory ====================
+
+std::optional<std::shared_ptr<IParsedData>> OoTPlayerAnimationHeaderFactory::parse(std::vector<uint8_t>& 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<OoTPlayerAnimationHeaderData>();
+ 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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto anim = std::static_pointer_cast<OoTPlayerAnimationHeaderData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTAnimation, 0);
+
+ writer.Write(static_cast<uint32_t>(OoTAnimationType::Link));
+ writer.Write(anim->frameCount);
+ writer.Write("__OTR__" + anim->animDataPath);
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+// ==================== OoT Player Animation Data Factory ====================
+
+std::optional<std::shared_ptr<IParsedData>> OoTPlayerAnimationDataFactory::parse(std::vector<uint8_t>& 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<int32_t>(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<OoTPlayerAnimationData>();
+ 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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto anim = std::static_pointer_cast<OoTPlayerAnimationData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTPlayerAnimation, 0);
+
+ writer.Write(static_cast<uint32_t>(anim->limbRotData.size()));
+ for (auto& val : anim->limbRotData) {
+ writer.Write(val);
+ }
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTPlayerAnimationFactory.h b/src/factories/oot/OoTPlayerAnimationFactory.h
new file mode 100644
index 0000000..05f080b
--- /dev/null
+++ b/src/factories/oot/OoTPlayerAnimationFactory.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "OoTAnimationTypes.h"
+#include <vector>
+#include <string>
+
+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<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTPlayerAnimationHeaderFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTPlayerAnimationHeaderBinaryExporter)
+ };
+ }
+};
+
+class OoTPlayerAnimationData : public IParsedData {
+public:
+ std::vector<int16_t> limbRotData;
+};
+
+class OoTPlayerAnimationDataBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTPlayerAnimationDataFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTPlayerAnimationDataBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSceneCommandWriter.cpp b/src/factories/oot/OoTSceneCommandWriter.cpp
new file mode 100644
index 0000000..b3553fa
--- /dev/null
+++ b/src/factories/oot/OoTSceneCommandWriter.cpp
@@ -0,0 +1,696 @@
+#include "OoTSceneCommandWriter.h"
+#include "AliasManager.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include "factories/DisplayListFactory.h"
+#include <sstream>
+#include <iomanip>
+
+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<uint8_t>(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<uint32_t>(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<uint32_t>(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<char>{});
+ }
+}
+
+void SceneCommandWriter::WriteSetWind(LUS::BinaryWriter& w, uint32_t w0, uint32_t w1) {
+ w.Write(static_cast<uint8_t>((w1 >> 24) & 0xFF)); // windWest
+ w.Write(static_cast<uint8_t>((w1 >> 16) & 0xFF)); // windVertical
+ w.Write(static_cast<uint8_t>((w1 >> 8) & 0xFF)); // windSouth
+ w.Write(static_cast<uint8_t>(w1 & 0xFF)); // clothFlappingStrength
+}
+
+void SceneCommandWriter::WriteSetTimeSettings(LUS::BinaryWriter& w, uint32_t w1) {
+ w.Write(static_cast<uint8_t>((w1 >> 24) & 0xFF)); // hour
+ w.Write(static_cast<uint8_t>((w1 >> 16) & 0xFF)); // min
+ w.Write(static_cast<uint8_t>((w1 >> 8) & 0xFF)); // unk
+}
+
+void SceneCommandWriter::WriteSetSkyboxModifier(LUS::BinaryWriter& w, uint32_t w1) {
+ w.Write(static_cast<uint8_t>((w1 >> 24) & 0xFF)); // disableSky
+ w.Write(static_cast<uint8_t>((w1 >> 16) & 0xFF)); // disableSunMoon
+}
+
+void SceneCommandWriter::WriteSetEchoSettings(LUS::BinaryWriter& w, uint32_t w1) {
+ w.Write(static_cast<uint8_t>(w1 & 0xFF)); // echo
+}
+
+void SceneCommandWriter::WriteSetSoundSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1) {
+ w.Write(static_cast<uint8_t>(cmdArg1)); // reverb
+ w.Write(static_cast<uint8_t>((w1 >> 8) & 0xFF)); // nightTimeSFX
+ w.Write(static_cast<uint8_t>(w1 & 0xFF)); // musicSequence
+}
+
+void SceneCommandWriter::WriteSetSkyboxSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1) {
+ w.Write(static_cast<uint8_t>(cmdArg1)); // unk1
+ w.Write(static_cast<uint8_t>((w1 >> 24) & 0xFF)); // skyboxNumber
+ w.Write(static_cast<uint8_t>((w1 >> 16) & 0xFF)); // cloudsType
+ w.Write(static_cast<uint8_t>((w1 >> 8) & 0xFF)); // isIndoors
+}
+
+void SceneCommandWriter::WriteSetRoomBehavior(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2) {
+ w.Write(static_cast<uint8_t>(cmdArg1)); // gameplayFlags
+ w.Write(cmdArg2); // gameplayFlags2
+}
+
+void SceneCommandWriter::WriteSetCameraSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2) {
+ w.Write(static_cast<uint8_t>(cmdArg1)); // cameraMovement
+ w.Write(cmdArg2); // mapHighlight
+}
+
+void SceneCommandWriter::WriteSetSpecialObjects(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t w1) {
+ w.Write(static_cast<uint8_t>(cmdArg1)); // elfMessage
+ w.Write(static_cast<uint16_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint8_t>(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<uint32_t>(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<uint32_t>(1));
+ w.Write(static_cast<uint16_t>(0)); // unk_00
+ w.Write(static_cast<uint8_t>(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<uint8_t>(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<CamEntry> 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<std::pair<uint8_t, uint32_t>> 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<uint32_t>(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) {
+ // An uninitialized alt-header pointer with a bogus segment resolves out of
+ // bounds. OTRExporter emits no alternate headers in that case; match it
+ // rather than reading from an invalid address.
+ auto altSegBase = Companion::Instance->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(cmdArg2));
+ uint32_t altFileOffset = altSegBase.has_value()
+ ? altSegBase.value() + SEGMENT_OFFSET(cmdArg2)
+ : cmdArg2;
+ if (altFileOffset >= ctx.buffer.size()) {
+ SPDLOG_WARN("Scene: skipping alternate headers for {}, unresolvable pointer 0x{:X}",
+ ctx.entryName, cmdArg2);
+ w.Write(static_cast<uint32_t>(0));
+ return;
+ }
+
+ uint32_t maxHeaders = GetNeighborSize(ctx.knownAddrs, cmdArg2, 4);
+ if (maxHeaders == 0) maxHeaders = 16;
+ auto hdrReader = ReadSubArray(ctx.buffer, cmdArg2, maxHeaders * 4);
+ std::vector<uint32_t> headers;
+ for (uint32_t i = 0; i < maxHeaders; i++) {
+ uint32_t seg = hdrReader.ReadUInt32();
+ headers.push_back(seg);
+ }
+
+ w.Write(static_cast<uint32_t>(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<uint8_t>& 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<uint32_t>(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<char>(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<uint32_t>& 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
diff --git a/src/factories/oot/OoTSceneCommandWriter.h b/src/factories/oot/OoTSceneCommandWriter.h
new file mode 100644
index 0000000..096a7ae
--- /dev/null
+++ b/src/factories/oot/OoTSceneCommandWriter.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "OoTSceneFactory.h"
+#include "OoTSceneUtils.h"
+#include <set>
+
+namespace OoT {
+
+// Context passed through scene command serialization.
+struct SceneWriteContext {
+ std::vector<uint8_t>& buffer;
+ const std::set<uint32_t>& knownAddrs;
+ const std::string& entryName;
+ const std::string& baseName;
+ const std::string& currentDir;
+ const std::string& assetType;
+ bool isAltHeader;
+ std::vector<PendingAltHeader>& 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<uint8_t>& 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<uint32_t>& knownAddrs, uint32_t segAddr, uint32_t entrySize);
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSceneFactory.cpp b/src/factories/oot/OoTSceneFactory.cpp
new file mode 100644
index 0000000..a55346f
--- /dev/null
+++ b/src/factories/oot/OoTSceneFactory.cpp
@@ -0,0 +1,167 @@
+#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 <set>
+
+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<uint32_t> OoTSceneFactory::CollectKnownAddresses(const std::vector<std::pair<uint32_t, uint32_t>>& rawCmds,
+ std::vector<uint8_t>& buffer) {
+ std::set<uint32_t> 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<std::shared_ptr<IParsedData>> OoTSceneFactory::parse(std::vector<uint8_t>& 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<OoTSceneData>();
+
+ // syotes_room_0 (a debug test room) has no valid command list — the data at
+ // offset 0 is a raw mesh header. ZAPD special-cases it by name and emits an
+ // empty room; match that. Its mesh DLs/textures/vtx are declared separately.
+ if (GetSafeNode<std::string>(node, "symbol") == "syotes_room_0") {
+ return scene;
+ }
+
+ // Read 8-byte scene commands until EndMarker
+ std::vector<std::pair<uint32_t, uint32_t>> 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<std::string>(node, "symbol");
+ auto currentDir = Companion::Instance->GetCurrentDirectory();
+ auto assetType = GetSafeNode<std::string>(node, "type");
+ std::string baseName = node["base_name"] ? node["base_name"].as<std::string>() : entryName;
+ bool isAltHeader = node["base_name"].IsDefined();
+
+ auto knownAddrs = CollectKnownAddresses(rawCmds, buffer);
+
+ std::vector<PendingAltHeader> 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<DeferredVtx::PendingVtx> 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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto scene = std::static_pointer_cast<OoTSceneData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTRoom, 0);
+ writer.Write(static_cast<uint32_t>(scene->commands.size()));
+
+ for (auto& cmd : scene->commands) {
+ writer.Write(cmd.cmdID);
+ if (!cmd.data.empty()) {
+ writer.Write(reinterpret_cast<char*>(cmd.data.data()), cmd.data.size());
+ }
+ }
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSceneFactory.h b/src/factories/oot/OoTSceneFactory.h
new file mode 100644
index 0000000..06bcfab
--- /dev/null
+++ b/src/factories/oot/OoTSceneFactory.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include <vector>
+#include <string>
+#include <optional>
+#include <set>
+
+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<uint8_t> data; // serialized binary data for the command body
+};
+
+// Parsed scene/room data
+class OoTSceneData : public IParsedData {
+public:
+ std::vector<SceneCommand> commands;
+};
+
+class OoTSceneBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTSceneFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTSceneBinaryExporter)
+ };
+ }
+
+private:
+ std::set<uint32_t> CollectKnownAddresses(const std::vector<std::pair<uint32_t, uint32_t>>& rawCmds,
+ std::vector<uint8_t>& buffer);
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSceneUtils.cpp b/src/factories/oot/OoTSceneUtils.cpp
new file mode 100644
index 0000000..ee0a301
--- /dev/null
+++ b/src/factories/oot/OoTSceneUtils.cpp
@@ -0,0 +1,52 @@
+#include "OoTSceneUtils.h"
+
+namespace OoT {
+
+LUS::BinaryReader ReadSubArray(std::vector<uint8_t>& 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<char> SerializePathways(std::vector<uint8_t>& buffer,
+ const std::vector<std::pair<uint8_t, uint32_t>>& pathways,
+ uint32_t writeCount, uint32_t repeats) {
+ LUS::BinaryWriter w;
+ BaseExporter::WriteHeader(w, Torch::ResourceType::OoTPath, 0);
+ w.Write(static_cast<uint32_t>(writeCount));
+ for (uint32_t r = 0; r < repeats; r++) {
+ for (auto& [np, ptsAddr] : pathways) {
+ w.Write(static_cast<uint32_t>(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<char>(str.begin(), str.end());
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSceneUtils.h b/src/factories/oot/OoTSceneUtils.h
new file mode 100644
index 0000000..aa74443
--- /dev/null
+++ b/src/factories/oot/OoTSceneUtils.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+#include <sstream>
+#include <iomanip>
+#include <set>
+
+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<uint8_t>& 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<char> SerializePathways(std::vector<uint8_t>& buffer,
+ const std::vector<std::pair<uint8_t, uint32_t>>& pathways,
+ uint32_t writeCount, uint32_t repeats);
+
+class CutsceneSerializer {
+public:
+ static std::vector<char> Serialize(std::vector<uint8_t>& buffer, uint32_t segAddr);
+private:
+ static uint32_t CalculateSize(std::vector<uint8_t>& buffer, uint32_t segAddr);
+ static std::vector<char> Write(std::vector<uint8_t>& 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<uint32_t> sUnimplementedCmds;
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSkeletonFactory.cpp b/src/factories/oot/OoTSkeletonFactory.cpp
new file mode 100644
index 0000000..70bf132
--- /dev/null
+++ b/src/factories/oot/OoTSkeletonFactory.cpp
@@ -0,0 +1,91 @@
+#include "OoTSkeletonFactory.h"
+#include "OoTSkeletonTypes.h"
+#include "OoTSceneUtils.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+std::optional<std::shared_ptr<IParsedData>> OoTSkeletonFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto skelTypeStr = GetSafeNode<std::string>(node, "skel_type");
+ auto limbTypeStr = GetSafeNode<std::string>(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<std::string>(node, "symbol");
+ std::vector<std::string> 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<OoTSkeletonData>();
+ 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<IParsedData> raw,
+ std::string& entryName, YAML::Node& node,
+ std::string* replacement) {
+ auto writer = LUS::BinaryWriter();
+ auto skel = std::static_pointer_cast<OoTSkeletonData>(raw);
+
+ WriteHeader(writer, Torch::ResourceType::OoTSkeleton, 0);
+
+ writer.Write(static_cast<uint8_t>(skel->skelType));
+ writer.Write(static_cast<uint8_t>(skel->limbType));
+ writer.Write(static_cast<uint32_t>(skel->limbCount));
+ writer.Write(static_cast<uint32_t>(skel->dListCount));
+ writer.Write(static_cast<uint8_t>(skel->limbType));
+ writer.Write(static_cast<uint32_t>(skel->limbPaths.size()));
+
+ for (auto& path : skel->limbPaths) {
+ writer.Write(path);
+ }
+
+ writer.Finish(write);
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSkeletonFactory.h b/src/factories/oot/OoTSkeletonFactory.h
new file mode 100644
index 0000000..13872fe
--- /dev/null
+++ b/src/factories/oot/OoTSkeletonFactory.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+
+namespace OoT {
+
+class OoTSkeletonBinaryExporter : public BaseExporter {
+ ExportResult Export(std::ostream& write, std::shared_ptr<IParsedData> data, std::string& entryName,
+ YAML::Node& node, std::string* replacement) override;
+};
+
+class OoTSkeletonFactory : public BaseFactory {
+public:
+ std::optional<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& data) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> GetExporters() override {
+ return {
+ REGISTER(Binary, OoTSkeletonBinaryExporter)
+ };
+ }
+};
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTSkeletonTypes.cpp b/src/factories/oot/OoTSkeletonTypes.cpp
new file mode 100644
index 0000000..2c4237e
--- /dev/null
+++ b/src/factories/oot/OoTSkeletonTypes.cpp
@@ -0,0 +1,38 @@
+#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
diff --git a/src/factories/oot/OoTSkeletonTypes.h b/src/factories/oot/OoTSkeletonTypes.h
new file mode 100644
index 0000000..d546149
--- /dev/null
+++ b/src/factories/oot/OoTSkeletonTypes.h
@@ -0,0 +1,98 @@
+#pragma once
+
+#include "factories/BaseFactory.h"
+#include <vector>
+#include <string>
+
+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<OoTSkinVertex> skinVertices;
+ std::vector<OoTSkinTransformation> limbTransformations;
+};
+
+struct OoTSkinAnimatedLimbData {
+ uint16_t totalVtxCount;
+ std::vector<OoTSkinLimbModif> 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<std::string> 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
diff --git a/src/factories/oot/OoTTextFactory.cpp b/src/factories/oot/OoTTextFactory.cpp
new file mode 100644
index 0000000..7b42fab
--- /dev/null
+++ b/src/factories/oot/OoTTextFactory.cpp
@@ -0,0 +1,300 @@
+#include "OoTTextFactory.h"
+#include "spdlog/spdlog.h"
+#include "Companion.h"
+#include "utils/Decompressor.h"
+
+namespace OoT {
+
+struct OoTTextData : public IParsedData {
+ std::vector<char> 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;
+}
+
+bool OoTTextFactory::IsEndOfMessageCodeJapanese(uint16_t c) {
+ return c == 0x8170 // END
+ || c == 0x81CB; // NEW_TEXT_ID (also ends the message)
+}
+
+unsigned int OoTTextFactory::GetTrailingShortsJapanese(uint16_t c) {
+ switch (c) {
+ case 0x000B:
+ case 0x819A:
+ case 0x819E:
+ case 0x81A3:
+ case 0x869F:
+ case 0x86C7:
+ case 0x86C9:
+ case 0x81F3:
+ case 0x81CB: // NEW_TEXT_ID (also ends the message)
+ return 1;
+ case 0x86B3:
+ return 2;
+ default:
+ return 0;
+ }
+}
+
+// Japanese text: two-byte big-endian characters, stored byte-swapped.
+// Control codes have 0-2 trailing shorts that must be included in the output.
+std::string OoTTextFactory::ReadMessageTextJapanese(const uint8_t* rawData, size_t rawSize, uint32_t offset) {
+ std::string msg;
+ uint32_t ptr = offset;
+
+ while (ptr + 2 <= rawSize) {
+ uint16_t c = (rawData[ptr] << 8) | rawData[ptr + 1];
+
+ msg += (char)rawData[ptr + 1];
+ msg += (char)rawData[ptr];
+ ptr += 2;
+
+ // Consume trailing shorts for control codes
+ unsigned int trailing = GetTrailingShortsJapanese(c);
+ for (unsigned int i = 0; i < trailing && ptr + 2 <= rawSize; i++) {
+ msg += (char)rawData[ptr + 1];
+ msg += (char)rawData[ptr];
+ ptr += 2;
+ }
+
+ if (IsEndOfMessageCodeJapanese(c)) 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<MessageEntry> OoTTextFactory::ParseMessagesEnglishNTSC(const DataChunk& code, uint32_t codeOffset,
+ const uint8_t* rawData, size_t rawSize) {
+ std::vector<MessageEntry> 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<MessageEntry> OoTTextFactory::ParseMessagesJapaneseNTSC(const DataChunk& code, uint32_t codeOffset,
+ const uint8_t* rawData, size_t rawSize) {
+ std::vector<MessageEntry> 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 = ReadMessageTextJapanese(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<MessageEntry> OoTTextFactory::ParseMessagesPAL(const DataChunk& code, uint32_t codeOffset,
+ uint32_t langPtr,
+ const uint8_t* rawData, size_t rawSize) {
+ std::vector<MessageEntry> 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<std::shared_ptr<IParsedData>> OoTTextFactory::parse(std::vector<uint8_t>& buffer, YAML::Node& node) {
+ auto codePhysStart = GetSafeNode<uint32_t>(node, "code_phys_start");
+ auto codeOffset = GetSafeNode<uint32_t>(node, "code_offset");
+ uint32_t langOffset = node["lang_offset"] ? node["lang_offset"].as<uint32_t>() : 0;
+ bool isPalLang = (langOffset != 0 && langOffset != codeOffset);
+ bool isJapanese = node["language"] && node["language"].as<std::string>() == "Japanese";
+
+ // Decode the code segment. It is usually YAZ0-compressed, but some ROMs
+ // store it uncompressed, so detect the type rather than assuming YAZ0.
+ DataChunk uncompressedChunk{};
+ DataChunk* codeChunk;
+ auto codeCompression = Decompressor::GetCompressionType(buffer, codePhysStart);
+ if (codeCompression == CompressionType::None) {
+ uncompressedChunk = { buffer.data() + codePhysStart, buffer.size() - codePhysStart };
+ codeChunk = &uncompressedChunk;
+ } else {
+ codeChunk = Decompressor::Decode(buffer, codePhysStart, codeCompression);
+ }
+ if (!codeChunk || !codeChunk->data) {
+ SPDLOG_ERROR("OoTTextFactory: failed to decode 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
+ std::vector<MessageEntry> messages;
+ if (isPalLang) {
+ messages = ParseMessagesPAL(*codeChunk, codeOffset, langOffset, rawData, rawSize);
+ } else if (isJapanese) {
+ messages = ParseMessagesJapaneseNTSC(*codeChunk, codeOffset, rawData, rawSize);
+ } else {
+ messages = ParseMessagesEnglishNTSC(*codeChunk, codeOffset, rawData, rawSize);
+ }
+
+ SPDLOG_INFO("OoTTextFactory: parsed {} messages", messages.size());
+
+ // Build OTXT binary
+ auto data = std::make_shared<OoTTextData>();
+ LUS::BinaryWriter w;
+ BaseExporter::WriteHeader(w, Torch::ResourceType::OoTText, 0);
+
+ w.Write(static_cast<uint32_t>(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<char>(str.begin(), str.end());
+
+ return data;
+}
+
+ExportResult OoTTextBinaryExporter::Export(std::ostream& write, std::shared_ptr<IParsedData> raw,
+ std::string& entryName, YAML::Node& node, std::string* replacement) {
+ auto data = std::static_pointer_cast<OoTTextData>(raw);
+ write.write(data->mBinary.data(), data->mBinary.size());
+ return std::nullopt;
+}
+
+} // namespace OoT
diff --git a/src/factories/oot/OoTTextFactory.h b/src/factories/oot/OoTTextFactory.h
new file mode 100644
index 0000000..47a039d
--- /dev/null
+++ b/src/factories/oot/OoTTextFactory.h
@@ -0,0 +1,51 @@
+#pragma once
+
+#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<IParsedData> 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<std::shared_ptr<IParsedData>> parse(std::vector<uint8_t>& buffer, YAML::Node& node) override;
+ std::unordered_map<ExportType, std::shared_ptr<BaseExporter>> 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 bool IsEndOfMessageCodeJapanese(uint16_t c);
+ static unsigned int GetTrailingShortsJapanese(uint16_t c);
+ static std::string ReadMessageTextJapanese(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<MessageEntry> ParseMessagesEnglishNTSC(const DataChunk& code, uint32_t codeOffset,
+ const uint8_t* rawData, size_t rawSize);
+ static std::vector<MessageEntry> ParseMessagesJapaneseNTSC(const DataChunk& code, uint32_t codeOffset,
+ const uint8_t* rawData, size_t rawSize);
+ static std::vector<MessageEntry> ParseMessagesPAL(const DataChunk& code, uint32_t codeOffset,
+ uint32_t langPtr,
+ const uint8_t* rawData, size_t rawSize);
+};
+
+} // namespace OoT
diff --git a/src/n64/Cartridge.cpp b/src/n64/Cartridge.cpp
index cca2f78..f165b5b 100644
--- a/src/n64/Cartridge.cpp
+++ b/src/n64/Cartridge.cpp
@@ -7,7 +7,11 @@ 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);
+#ifdef ROM_CRC_BSWAP
this->gRomCRC = BSWAP32(reader.ReadUInt32());
+#else
+ this->gRomCRC = reader.ReadUInt32();
+#endif
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 c591f6c..c787282 100644
--- a/src/utils/Decompressor.cpp
+++ b/src/utils/Decompressor.cpp
@@ -10,6 +10,7 @@ extern "C" {
#include <libmio0/mio0.h>
#include <libyay0/yay0.h>
#include <libyay0/yay1.h>
+#include <libyaz0/yaz0.h>
#include <libmio0/tkmk00.h>
}
@@ -92,6 +93,17 @@ DataChunk* Decompressor::Decode(const std::vector<uint8_t>& buffer, const uint32
" compressed size 0x" + Torch::to_hex(in_size, false) + ")");
}
}
+ 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");
}
@@ -196,7 +208,8 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>
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);
@@ -246,9 +259,6 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector<uint8_t>
offset, fileOffset, offsetFromFile, sizeEntry.value(), decoded->size, size);
return { .root = decoded, .segment = { decoded->data + offsetFromFile, size } };
}
- case CompressionType::YAZ0:
- throw std::runtime_error(
- "Found compressed yaz0 segment.\nDecompression of yaz0 has not been implemented yet.");
case CompressionType::None: // The data does not have compression
{
fileOffset = TranslateAddr(offset, false);
@@ -293,7 +303,7 @@ DecompressedData Decompressor::AutoDecode(uint32_t offset, std::optional<size_t>
}
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()) {
const auto compressedSegmentPair =