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/Companion.cpp | |
| parent | cd33252589a61d044138f449041a87adee2a8291 (diff) | |
oot support (#219)
Diffstat (limited to 'src/Companion.cpp')
| -rw-r--r-- | src/Companion.cpp | 259 |
1 files changed, 234 insertions, 25 deletions
diff --git a/src/Companion.cpp b/src/Companion.cpp index 27b2472..74d6ec1 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -1,4 +1,5 @@ #include "Companion.h" +#include "AliasManager.h" #include "utils/Decompressor.h" #include "utils/StringHelper.h" @@ -118,6 +119,22 @@ #include "factories/mario_artist/MA2D1Factory.h" #endif +#ifdef OOT_SUPPORT +#include "factories/oot/OoTArrayFactory.h" +#include "factories/oot/OoTSkeletonFactory.h" +#include "factories/oot/OoTLimbFactory.h" +#include "factories/oot/OoTMtxFactory.h" +#include "factories/oot/OoTAnimationFactory.h" +#include "factories/oot/OoTCurveAnimationFactory.h" +#include "factories/oot/OoTPlayerAnimationFactory.h" +#include "factories/oot/OoTCollisionFactory.h" +#include "factories/oot/OoTTextFactory.h" +#include "factories/oot/OoTSceneFactory.h" +#include "factories/oot/OoTPathFactory.h" +#include "factories/oot/OoTCutsceneFactory.h" +#include "factories/oot/OoTAudioFactory.h" +#endif + #ifdef NAUDIO_SUPPORT #include "factories/naudio/v0/AudioHeaderFactory.h" #include "factories/naudio/v0/BankFactory.h" @@ -294,6 +311,23 @@ void Companion::Init(const ExportType type, std::atomic<size_t>& assetCount, boo this->RegisterFactory("NAUDIO:V1:ADPCM_BOOK", std::make_shared<ADPCMBookFactory>()); this->RegisterFactory("NAUDIO:V1:SEQUENCE", std::make_shared<NSequenceFactory>()); #endif +#ifdef OOT_SUPPORT + this->RegisterFactory("OOT:ARRAY", std::make_shared<OoT::OoTArrayFactory>()); + this->RegisterFactory("OOT:MTX", std::make_shared<OoT::OoTMtxFactory>()); + this->RegisterFactory("OOT:SKELETON", std::make_shared<OoT::OoTSkeletonFactory>()); + this->RegisterFactory("OOT:LIMB", std::make_shared<OoT::OoTLimbFactory>()); + this->RegisterFactory("OOT:ANIMATION", std::make_shared<OoT::OoTAnimationFactory>()); + this->RegisterFactory("OOT:CURVE_ANIMATION", std::make_shared<OoT::OoTCurveAnimationFactory>()); + this->RegisterFactory("OOT:PLAYER_ANIMATION", std::make_shared<OoT::OoTPlayerAnimationHeaderFactory>()); + this->RegisterFactory("OOT:PLAYER_ANIMATION_DATA", std::make_shared<OoT::OoTPlayerAnimationDataFactory>()); + this->RegisterFactory("OOT:COLLISION", std::make_shared<OoT::OoTCollisionFactory>()); + this->RegisterFactory("OOT:TEXT", std::make_shared<OoT::OoTTextFactory>()); + this->RegisterFactory("OOT:SCENE", std::make_shared<OoT::OoTSceneFactory>()); + this->RegisterFactory("OOT:ROOM", std::make_shared<OoT::OoTSceneFactory>()); + this->RegisterFactory("OOT:CUTSCENE", std::make_shared<OoT::OoTCutsceneFactory>()); + this->RegisterFactory("OOT:PATH", std::make_shared<OoT::OoTPathFactory>()); + this->RegisterFactory("OOT:AUDIO", std::make_shared<OoT::OoTAudioFactory>()); +#endif #ifdef BUILD_UI // Custom preview renderers; other types fall back to BaseFactoryUI. @@ -422,7 +456,12 @@ std::optional<ParseResultData> Companion::ParseNode(YAML::Node& node, std::strin auto factory = this->GetFactory(type); if (!factory.has_value()) { +#ifdef THROW_ON_UNKNOWN_TYPE throw std::runtime_error("No factory by the name '" + type + "' found for '" + name + "'"); +#else + SPDLOG_WARN("No factory by the name '{}' found for '{}', skipping", type, name); + return std::nullopt; +#endif } auto impl = factory->get(); @@ -550,6 +589,10 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic<size_t>& ass } } + if (node["directory"]) { + this->gCurrentDirectory = node["directory"].as<std::string>(); + } + if (node["manual_segments"]) { auto manualSegments = node["manual_segments"]; if (manualSegments.IsSequence() && manualSegments.size()) { @@ -854,9 +897,12 @@ void Companion::ProcessExportFile() { stream.clear(); exporter->get()->Export(stream, data, result.name, result.node, &result.name); auto data = stream.str(); - this->gCurrentWrapper->AddFile(result.name, std::vector(data.begin(), data.end())); + auto dataVec = std::vector(data.begin(), data.end()); + this->gCurrentWrapper->AddFile(result.name, dataVec); + AliasManager::Instance->WriteAliases(result.name, this->gCurrentWrapper, dataVec); } + for (auto& entry : this->gCompanionFiles) { auto output = (this->gCurrentDirectory / entry.first).string(); std::replace(output.begin(), output.end(), '\\', '/'); @@ -1143,6 +1189,14 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) { gCurrentSegmentNumber = 0; gCurrentFileOffset = 0; gCurrentCompressionType = CompressionType::None; + // gAddrMap registration below keys assets by their output path, so the + // directory must be resolved before the loop. Default to the file's own + // path, then honor a :config directory override (used to register a room's + // assets under its scene's directory). + this->gCurrentDirectory = relative(fs::path(this->gCurrentFile), this->gAssetPath).replace_extension(""); + if (auto directory = root[":config"]["directory"]) { + this->gCurrentDirectory = directory.as<std::string>(); + } // Set compressed file offsets and compression type if (auto segments = root[":config"]["segments"]) { if (segments.IsSequence() && segments.size() > 0) { @@ -1207,6 +1261,7 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) { this->gTables.clear(); this->gCurrentExternalFiles.clear(); this->gSubFileList.clear(); + this->gManualSegments.clear(); GFXDOverride::ClearVtx(); if (root[":config"]) { @@ -1413,6 +1468,9 @@ void Companion::Process(std::atomic<size_t>& assetCount) { } else if (key == "F3DEX2_PM64") { this->gConfig.gbi.version = GBIVersion::f3dex2; this->gConfig.gbi.subversion = GBIMinorVersion::PM64; + } else if (key == "F3DEX2_OoT") { + this->gConfig.gbi.version = GBIVersion::f3dex2; + this->gConfig.gbi.subversion = GBIMinorVersion::OoT; } else if (key == "F3DEXB") { this->gConfig.gbi.version = GBIVersion::f3dexb; } else if (key == "F3DEX_MK64") { @@ -1431,6 +1489,10 @@ void Companion::Process(std::atomic<size_t>& assetCount) { } } + if (cfg["primary_virtual_segment"]) { + this->gConfig.segment.primaryVirtual = cfg["primary_virtual_segment"].as<uint32_t>(); + } + this->gConfig.defaultShading = GetSafeNode<std::string>(cfg, "default_shading", ""); if (auto sort = cfg["sort"]) { @@ -1495,6 +1557,8 @@ void Companion::Process(std::atomic<size_t>& assetCount) { this->gConfig.includeAutogen = cfg["include_autogen"] && cfg["include_autogen"].as<bool>(); this->gConfig.dialogPack = cfg["dialog_pack"] && cfg["dialog_pack"].as<bool>(); + this->gConfig.strictDeclarations = GetSafeNode<bool>(cfg, "strict_declarations", false); + this->ParseHash(); SPDLOG_CRITICAL("------------------------------------------------"); @@ -1578,7 +1642,6 @@ void Companion::Process(std::atomic<size_t>& assetCount) { } YAML::Node root = YAML::LoadFile(yamlPath); - this->gCurrentDirectory = relative(entry.path(), this->gAssetPath).replace_extension(""); this->gCurrentFile = yamlPath; if (!Torch::contains(this->gProcessedFiles, this->gCurrentFile)) { @@ -1926,6 +1989,66 @@ std::optional<std::uint32_t> Companion::GetFileOffsetFromSegmentedAddr(const uin return std::nullopt; } +// Convert a VRAM address (0x80XXXXXX) to a segmented address that Torch can look up. +// +// N64 overlays store pointers as VRAM addresses (e.g. 0x80B65CC4). Torch needs +// segmented addresses (e.g. 0x06000CC4) to find assets in gAddrMap. This function +// translates between the two using the file's virtual address mapping. +// +// Not all 0x80XXXXXX addresses are VRAM pointers — segment 0x80 addresses also have +// bit 31 set. We distinguish them by checking if the address falls within the current +// file's VRAM range. +ResolvedAddr Companion::ResolveVirtualAddr(uint32_t addr) { + // Addresses below 0x80000000 aren't virtual. + if (!(addr & 0x80000000)) { + return { addr, IS_SEGMENTED(addr) ? ResolvedAddr::Segmented : ResolvedAddr::FileRelative }; + } + + // If the current file doesn't have a virtual address mapping, we can't + // determine if this is a segment-0x80 address or VRAM for another file. + if (!Torch::contains(gVirtualAddrMap, gCurrentFile)) { + return { addr, ResolvedAddr::Unknown }; + } + + // vramBase: where this file's data is loaded in VRAM at runtime (e.g. 0x80B65000) + // physStart: where this file's data lives in the ROM (e.g. 0x00D24FC0) + auto vramBase = std::get<0>(gVirtualAddrMap[gCurrentFile]); + auto physStart = std::get<1>(gVirtualAddrMap[gCurrentFile]); + + // If addr is below vramBase, it's not pointing into this file's VRAM range. + // It's a segmented address using segment 0x80+. + if (addr < vramBase) { + return { addr, ResolvedAddr::Segmented }; + } + + // addr is within this file's VRAM range. Compute how far into the file it points. + auto relOffset = addr - vramBase; + + // No segments configured — return absolute ROM address. + if (this->gConfig.segment.local.empty()) { + return { relOffset + physStart, ResolvedAddr::Absolute }; + } + + // Convert the relative offset to a segmented address by finding which segment + // maps to this file's ROM data. When multiple segments alias the same ROM data, + // prefer the configured primary virtual segment for consistency. + if (this->gConfig.segment.primaryVirtual.has_value()) { + auto primarySeg = this->gConfig.segment.primaryVirtual.value(); + if (Torch::contains(this->gConfig.segment.local, primarySeg) && + this->gConfig.segment.local[primarySeg] == physStart) { + return { (primarySeg << 24) | relOffset, ResolvedAddr::Segmented }; + } + } + for (auto& [seg, segOffset] : this->gConfig.segment.local) { + if (segOffset == physStart) { + return { (seg << 24) | relOffset, ResolvedAddr::Segmented }; + } + } + + // No matching segment found — return absolute ROM address as fallback. + return { relOffset + physStart, ResolvedAddr::Absolute }; +} + std::optional<std::pair<std::uint32_t, std::uint32_t>> Companion::GetFileOffsetFromCompressedSegmentedAddr(const uint8_t segment) const { @@ -1939,14 +2062,65 @@ Companion::GetFileOffsetFromCompressedSegmentedAddr(const uint8_t segment) const } uint32_t Companion::PatchVirtualAddr(uint32_t addr) { - if (addr & 0x80000000) { - if (Torch::contains(gVirtualAddrMap, gCurrentFile)) { - addr -= std::get<0>(gVirtualAddrMap[gCurrentFile]); - addr += std::get<1>(gVirtualAddrMap[gCurrentFile]); + return ResolveVirtualAddr(addr).addr; +} + +// Look up an asset by converting a segmented address to an absolute ROM address, +// then scanning all entries in the file for one that resolves to the same address. +// Used when multiple segments map to the same ROM data, so the same asset may be +// registered under a different segment number than the one being looked up. +std::optional<std::tuple<std::string, YAML::Node>> Companion::FindNodeInOverlaySegments( + uint32_t addr, const std::string& file) { + // Only applies to files with virtual address mappings (overlays). + if (!Torch::contains(gVirtualAddrMap, file)) { + return std::nullopt; + } + + // Convert the segmented address to an absolute ROM address. + auto addrSeg = this->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(addr)); + if (!addrSeg.has_value()) { + return std::nullopt; + } + auto absAddr = addrSeg.value() + SEGMENT_OFFSET(addr); + + // Scan all entries in the file for one whose absolute ROM address matches. + for (auto& [storedAddr, entry] : this->gAddrMap[file]) { + if (storedAddr == addr || !IS_SEGMENTED(storedAddr)) { + continue; } + auto storedSeg = this->GetFileOffsetFromSegmentedAddr(SEGMENT_NUMBER(storedAddr)); + if (storedSeg.has_value() && storedSeg.value() + SEGMENT_OFFSET(storedAddr) == absAddr) { + return entry; + } + } + return std::nullopt; +} + +// Check if this address is a VRAM pointer belonging to the given external file. +// If it falls within the file's VRAM range, convert to a virtual segment address +// and look it up in the file's gAddrMap. +std::optional<std::tuple<std::string, YAML::Node>> Companion::FindInExternalByVRAM( + uint32_t addr, const std::string& file) { + // Can't resolve if the external file doesn't have a virtual address mapping. + if (!Torch::contains(gVirtualAddrMap, file)) { + return std::nullopt; } - return addr; + // Address doesn't belong to this file if it's below the file's VRAM base. + auto vramBase = std::get<0>(gVirtualAddrMap[file]); + if (addr < vramBase) { + return std::nullopt; + } + + // Convert VRAM address to virtual segment address: + // relative offset within the file, with 0x80 as the segment number. + uint32_t virtualSegmentAddr = 0x80000000 | (addr - vramBase); + if (Torch::contains(this->gAddrMap[file], virtualSegmentAddr)) { + return this->gAddrMap[file][virtualSegmentAddr]; + } + + // Address doesn't belong to this external file. + return std::nullopt; } std::optional<std::tuple<std::string, YAML::Node>> Companion::GetNodeByAddr(uint32_t addr) { @@ -1954,32 +2128,55 @@ std::optional<std::tuple<std::string, YAML::Node>> Companion::GetNodeByAddr(uint return std::nullopt; } - // HACK: Adjust address to rom address if virtual address - addr = PatchVirtualAddr(addr); + auto resolved = ResolveVirtualAddr(addr); - if (!Torch::contains(this->gAddrMap[this->gCurrentFile], addr)) { - auto realAddr = addr; - if (IS_SEGMENTED(addr) && GetCompressedSegmentOffset(&realAddr)) { - if (this->gAddrMap[this->gCurrentFile].contains(realAddr)) { - return this->gAddrMap[this->gCurrentFile][realAddr]; - } + // Direct lookup by resolved address. This works because ResolveVirtualAddr + // produces addresses in the same format that PopulateAddrMap uses to register + // assets (segmented offset for files with segments, plain offset otherwise). + if (Torch::contains(this->gAddrMap[this->gCurrentFile], resolved.addr)) { + return this->gAddrMap[this->gCurrentFile][resolved.addr]; + } + + // Compressed (BKZIP) segment: translate the segmented address to the decoded + // file offset before looking it up. + if (uint32_t realAddr = addr; IS_SEGMENTED(addr) && GetCompressedSegmentOffset(&realAddr)) { + if (this->gAddrMap[this->gCurrentFile].contains(realAddr)) { + return this->gAddrMap[this->gCurrentFile][realAddr]; } + } - for (auto& file : this->gCurrentExternalFiles) { - if (!Torch::contains(this->gAddrMap, file)) { - SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file); - continue; - } + // For overlay files, the same data may be registered under a different segment + // number (e.g. looking up segment 8 when the asset was registered under segment 6). + if (resolved.kind == ResolvedAddr::Segmented) { + auto match = FindNodeInOverlaySegments(resolved.addr, this->gCurrentFile); + if (match.has_value()) { + return match; + } + } - if (!Torch::contains(this->gAddrMap[file], addr)) { - continue; + // Search external files + for (auto& file : this->gCurrentExternalFiles) { + if (!Torch::contains(this->gAddrMap, file)) { + SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file); + continue; + } + + // Direct lookup in external file by resolved address. + if (Torch::contains(this->gAddrMap[file], resolved.addr)) { + return this->gAddrMap[file][resolved.addr]; + } + + // If we couldn't determine the address type, it may be a VRAM address + // belonging to this external file's virtual address space. + if (resolved.kind == ResolvedAddr::Unknown) { + auto vramMatch = FindInExternalByVRAM(resolved.addr, file); + if (vramMatch.has_value()) { + return vramMatch; } - return this->gAddrMap[file][addr]; } - return std::nullopt; } - return this->gAddrMap[this->gCurrentFile][addr]; + return std::nullopt; } std::optional<std::tuple<std::string, YAML::Node>> Companion::GetNodeByAddr(uint32_t addr, const std::string& file) { @@ -2242,6 +2439,9 @@ std::vector<char> Companion::ParseVersionString(const std::string& version) { auto wv = LUS::BinaryWriter(); wv.SetEndianness(Torch::Endianness::Big); +#ifdef PORT_VERSION_ENDIANNESS + wv.Write(static_cast<uint8_t>(Torch::Endianness::Big)); +#endif wv.Write(major); wv.Write(minor); wv.Write(patch); @@ -2302,6 +2502,15 @@ std::optional<YAML::Node> Companion::AddAsset(YAML::Node asset) { const auto symbol = GetSafeNode<std::string>(asset, "symbol", ""); const auto decl = this->GetNodeByAddr(offset); + // With strict_declarations, every asset must be pre-declared in the YAML. + // Throw if an undeclared asset is encountered to catch declaration gaps. + if (!decl.has_value() && this->gConfig.strictDeclarations) { + throw std::runtime_error( + "AddAsset: undeclared " + type + " at " + Torch::to_hex(offset, false) + + " (symbol: " + symbol + ") in " + this->gCurrentFile + + " — YAML declarations incomplete"); + } + if (decl.has_value()) { auto found = std::get<1>(decl.value()); if (GetTypeNode(found) != type) { |
