diff options
| author | briaguya <70942617+briaguya0@users.noreply.github.com> | 2026-07-24 18:09:55 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-24 18:09:55 -0400 |
| commit | 4cae44160693e1beb562e39dc301bd42278be1f9 (patch) | |
| tree | 5d247f7821a6040221b4bf34d2cae2b87c42d23a /src/factories | |
| parent | cd33252589a61d044138f449041a87adee2a8291 (diff) | |
oot support (#219)
Diffstat (limited to 'src/factories')
50 files changed, 5538 insertions, 1 deletions
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 |
