diff --git a/src/Companion.cpp b/src/Companion.cpp index d73b6165..310bbc32 100644 --- a/src/Companion.cpp +++ b/src/Companion.cpp @@ -133,6 +133,9 @@ #include "factories/oot/OoTPathFactory.h" #include "factories/oot/OoTCutsceneFactory.h" #include "factories/oot/OoTAudioFactory.h" +#include "factories/oot/MMTextureAnimationFactory.h" +#include "factories/oot/MMTextFactory.h" +#include "factories/oot/MMKeyFrameFactory.h" #endif #ifdef NAUDIO_SUPPORT @@ -327,6 +330,33 @@ void Companion::Init(const ExportType type, std::atomic& assetCount, boo this->RegisterFactory("OOT:CUTSCENE", std::make_shared()); this->RegisterFactory("OOT:PATH", std::make_shared()); this->RegisterFactory("OOT:AUDIO", std::make_shared()); + + // Majora's Mask runs on the same engine and shares most asset formats with + // Ocarina of Time, so MM: names resolve to the OoT factories for now. Which of + // these actually produce byte-identical output is being measured type by type; + // the ones that do are what moves to a shared zelda64/ namespace, and the ones + // that don't get real MM implementations. + // + // MM-only types with no OoT counterpart -- TEXTURE_ANIMATION, KEYFRAME_ANIMATION, + // KEYFRAME_SKELETON -- are deliberately absent: nothing here can serve them. + this->RegisterFactory("MM:ARRAY", std::make_shared()); + this->RegisterFactory("MM:MTX", std::make_shared()); + this->RegisterFactory("MM:SKELETON", std::make_shared()); + this->RegisterFactory("MM:LIMB", std::make_shared()); + this->RegisterFactory("MM:ANIMATION", std::make_shared()); + this->RegisterFactory("MM:CURVE_ANIMATION", std::make_shared()); + this->RegisterFactory("MM:PLAYER_ANIMATION", std::make_shared()); + this->RegisterFactory("MM:PLAYER_ANIMATION_DATA", std::make_shared()); + this->RegisterFactory("MM:COLLISION", std::make_shared()); + this->RegisterFactory("MM:TEXT", std::make_shared()); + this->RegisterFactory("MM:SCENE", std::make_shared()); + this->RegisterFactory("MM:ROOM", std::make_shared()); + this->RegisterFactory("MM:CUTSCENE", std::make_shared()); + this->RegisterFactory("MM:PATH", std::make_shared()); + this->RegisterFactory("MM:TEXTURE_ANIMATION", std::make_shared()); + this->RegisterFactory("MM:KEYFRAME_SKELETON", std::make_shared()); + this->RegisterFactory("MM:KEYFRAME_ANIMATION", std::make_shared()); + this->RegisterFactory("MM:AUDIO", std::make_shared()); #endif #ifdef BUILD_UI @@ -528,6 +558,26 @@ void Companion::ParseModdingConfig() { } } +// Resolve an explicit `compression:` override from a file's :config:. Formats with +// no magic of their own (CmpDma containers) cannot be sniffed by +// GetCompressionType, so a file has to name them. Returns nullopt when the key is +// absent, leaving auto-detection in charge. +static std::optional ExplicitCompressionType(const YAML::Node& config) { + if (!config || !config["compression"]) { + return std::nullopt; + } + const auto name = config["compression"].as(); + if (name == "CMPDMA") { + return CompressionType::CMPDMA; + } + if (name == "NONE") { + return CompressionType::None; + } + throw std::runtime_error("Unknown `compression:` value \"" + name + + "\".\n\nSupported: CMPDMA, NONE. Omit the key to auto-detect from the " + "file's magic (MIO0/Yay0/Yay1/Yaz0)."); +} + void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic& assetCount) { if (node["external_files"]) { auto externalFiles = node["external_files"]; @@ -620,16 +670,20 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic& ass // Set global variables for segmented data if (segments.IsSequence() && segments.size()) { if (segments[0].IsSequence() && segments[0].size() == 2) { - gCurrentSegmentNumber = segments[0][0].as(); - gCurrentFileOffset = segments[0][1].as(); + SetSegmentInfo(segments); + gCurrentCompressionType = Decompressor::GetCompressionType(this->gRomData, gCurrentFileOffset); + if (const auto explicitType = ExplicitCompressionType(node)) { + gCurrentCompressionType = *explicitType; + } if (node["no_compression"]) { gCurrentCompressionType = CompressionType::None; } } else { throw std::runtime_error( - "Incorrect yaml syntax for segments.\n\nThe yaml expects:\n:config:\n segments:\n - [, " - "]\n\nLike so:\nsegments:\n - [0x06, 0x821D10]"); + "Incorrect yaml syntax for segments.\n\nThe yaml expects:\n:config:\n segments:\n - [, " + "] or - [, " + "] \n\nLike so:\nsegments:\n - [0x06, 0x821D10] or [0x06, object_jya_obj"); } } @@ -638,13 +692,14 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic& ass auto segment = segments[i]; if (segment.IsSequence() && segment.size() == 2) { const auto id = segment[0].as(); - const auto replacement = segment[1].as(); + const auto replacement = GetFileOffsetFromNodeStr(segment[1].as()); this->gConfig.segment.local[id] = replacement; SPDLOG_DEBUG("Segment {} replaced with 0x{:X}", id, replacement); } else { throw std::runtime_error( "Incorrect yaml syntax for segments.\n\nThe yaml expects:\n:config:\n segments:\n - [, " - "]\n\nLike so:\nsegments:\n - [0x06, 0x821D10]"); + "] or - [, " + "] \n\nLike so:\nsegments:\n - [0x06, 0x821D10] or [0x06, object_jya_obj"); } } } @@ -1183,6 +1238,22 @@ void Companion::ProcessExportFile() { } } +void Companion::SetSegmentInfo(const YAML::Node& segments) { + gCurrentSegmentNumber = segments[0][0].as(); + + const auto offsetNode = segments[0][1].as(); + gCurrentFileOffset = GetFileOffsetFromNodeStr(offsetNode); +} + +uint32_t Companion::GetFileOffsetFromNodeStr(const std::string& str) const { + // IsValidHex demands an 0x prefix and at least three characters, so a bare + // "0" would fall through to the filelist and throw. IsValidOffset covers it. + if (StringHelper::IsValidOffset(str)) { + return strtoul(str.c_str(), nullptr, 16); + } + return GetFileOffsetFromName(str); +} + void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { // Reset per-file state so segment/offset settings from a previous file don't // bleed into this file's Phase 1 gAddrMap registration. @@ -1201,16 +1272,20 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { if (auto segments = root[":config"]["segments"]) { if (segments.IsSequence() && segments.size() > 0) { if (segments[0].IsSequence() && segments[0].size() == 2) { - gCurrentSegmentNumber = segments[0][0].as(); - gCurrentFileOffset = segments[0][1].as(); + SetSegmentInfo(segments); + gCurrentCompressionType = Decompressor::GetCompressionType(this->gRomData, gCurrentFileOffset); + if (const auto explicitType = ExplicitCompressionType(root[":config"])) { + gCurrentCompressionType = *explicitType; + } if (root[":config"]["no_compression"]) { gCurrentCompressionType = CompressionType::None; } } else { throw std::runtime_error( "Incorrect yaml syntax for segments.\n\nThe yaml expects:\n:config:\n segments:\n - [, " - "]\n\nLike so:\nsegments:\n - [0x06, 0x821D10]"); + "] or - [, " + "] \n\nLike so:\nsegments:\n - [0x06, 0x821D10] or [0x06, object_jya_obj"); } } } @@ -1234,9 +1309,14 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { continue; } + auto offset = GetFileOffsetFromNodeStr(node["offset"].as()); + if (gCurrentSegmentNumber) { - if (IS_SEGMENTED(node["offset"].as()) == false) { - node["offset"] = (gCurrentSegmentNumber << 24) | node["offset"].as(); + + if (IS_SEGMENTED(offset) == false) { + offset = (gCurrentSegmentNumber << 24) | offset; + node["offset"] = offset; + } } @@ -1244,7 +1324,14 @@ void Companion::ProcessFile(YAML::Node root, std::atomic& assetCount) { node["path"] = gCurrentVirtualPath; } - this->gAddrMap[this->gCurrentFile][node["offset"].as()] = std::make_tuple(output, node); + // A `duplicate_of` node is a second copy of an asset already declared at + // this offset under another name, so it must not claim the address -- + // pointers to that offset belong to the original. + if (node["duplicate_of"]) { + continue; + } + + this->gAddrMap[this->gCurrentFile][offset] = std::make_tuple(output, node); } // Stupid hack because the iteration broke the assets @@ -1414,9 +1501,32 @@ void Companion::Process(std::atomic& assetCount) { } } this->gAssetPath = (this->gSourceDirectory / rom["path"].as()).string(); + + if (rom["filelist"]) { + const std::string filelistPath = (this->gSourceDirectory / rom["filelist"].as()).string(); + if (!fs::exists(filelistPath)) { + SPDLOG_ERROR("A filelist was specified but the file doesn't exist"); + return; + } + ParseFilelist(filelistPath); + } + auto opath = cfg["output"]; auto gbi = cfg["gbi"]; auto gbi_floats = cfg["gbi_floats"]; + + // OoT and MM share this engine and most of their asset formats; the places + // they diverge need to know which one they are looking at. + this->gConfig.zelda64Game = Zelda64Game::OoT; + if (cfg["game"]) { + const auto game = cfg["game"].as(); + if (game == "MM") { + this->gConfig.zelda64Game = Zelda64Game::MM; + } else if (game != "OOT") { + throw std::runtime_error("Unknown `game:` value \"" + game + + "\".\n\nSupported: OOT (default), MM."); + } + } auto modding_path = opath && opath["modding"] ? opath["modding"].as() : "modding"; if (!this->gDestinationDirectory.empty() && !fs::exists(this->gDestinationDirectory)) { @@ -2590,3 +2700,16 @@ bool Companion::GetCompressedSegmentOffset(uint32_t* addr) { } return false; } + +void Companion::ParseFilelist(const std::string& filelistPath) { + YAML::Node root = YAML::LoadFile(filelistPath); + + for (const auto f : root["Files"]) { + for (const auto& kv : f) { + const auto file = kv.first.as(); + const auto offset = kv.second.as(); + gFileOffsets[file] = offset; + } + + } +} \ No newline at end of file diff --git a/src/Companion.h b/src/Companion.h index b9466ec9..ba184f1f 100644 --- a/src/Companion.h +++ b/src/Companion.h @@ -101,8 +101,18 @@ struct GBIConfig { bool useFloats = false; }; +// Which Zelda 64 title a rom is. Ocarina of Time and Majora's Mask run on the same +// engine and share most asset formats, but a few structures carry extra fields in +// MM -- pathways, for one -- so shared code has to be able to tell them apart. +// Declared per rom with `game: MM` in its config; defaults to OoT. +enum class Zelda64Game { + OoT, + MM, +}; + struct TorchConfig { GBIConfig gbi; + Zelda64Game zelda64Game = Zelda64Game::OoT; SegmentConfig segment; std::string outputPath; std::string moddingPath; @@ -189,6 +199,8 @@ class Companion { std::string GetDestRelativeOutputPath() { return RelativePathToDestDir(GetOutputPath()); } GBIVersion GetGBIVersion() const { return this->gConfig.gbi.version; } + Zelda64Game GetZelda64Game() const { return this->gConfig.zelda64Game; } + bool IsMajorasMask() const { return this->gConfig.zelda64Game == Zelda64Game::MM; } GBIMinorVersion GetGBIMinorVersion() const { return this->gConfig.gbi.subversion; } std::unordered_map> GetCourseMetadata() { return this->gCourseMetadata; } std::optional GetEnumFromValue(const std::string& key, int id); @@ -217,8 +229,9 @@ class Companion { const std::vector>* GetNodesByTypeRef(const std::string& type, bool includeAutogen = false); std::string GetSymbolFromAddr(uint32_t addr, bool validZero = false); - std::optional GetFileOffset(void) const { return this->gCurrentFileOffset; }; - std::optional GetCurrSegmentNumber(void) const { return this->gCurrentSegmentNumber; }; + std::string GetCurrentFile(void) { return this->gCurrentFile; } + std::optional GetFileOffsetFromName(void) const { return this->gCurrentFileOffset; }; + std::uint32_t GetCurrSegmentNumber(void) const { return this->gCurrentSegmentNumber; }; CompressionType GetCurrCompressionType(void) const { return this->gCurrentCompressionType; }; std::optional GetCurrentCompressedSize(void) const { return this->gCurrentCompressedSize; }; std::optional GetCurrentVRAM(void) const { return this->gCurrentVram; }; @@ -253,6 +266,7 @@ class Companion { bool GetCompressedSegmentOffset(uint32_t* addr); void SetSingleYMLPath(const std::string& path) { this->gSingleYMLPath = path; } + uint32_t GetFileOffsetFromName(const std::string& file) const { return this->gFileOffsets.at(file); } #ifdef BUILD_UI void RegisterUIFactory(const std::string& type, const std::shared_ptr& factory); @@ -301,6 +315,7 @@ class Companion { std::vector gCurrentExternalFiles; std::unordered_map gManualSegments; std::unordered_set gProcessedFiles; + std::unordered_map gFileOffsets; std::unordered_map> gCompanionFiles; std::vector>> gArchiveFiles; @@ -341,4 +356,7 @@ class Companion { void LoadYAMLRecursively(const std::string &dirPath, std::vector &result, bool skipRoot); std::vector GetAssetYMLs(YAML::Node& rom) const; std::optional ParseNode(YAML::Node& node, std::string& name); + void ParseFilelist(const std::string& filelistPath); + void SetSegmentInfo(const YAML::Node& segments); + uint32_t GetFileOffsetFromNodeStr(const std::string& str) const; }; diff --git a/src/factories/ResourceType.h b/src/factories/ResourceType.h index 17d5ae90..5ffdb8b1 100644 --- a/src/factories/ResourceType.h +++ b/src/factories/ResourceType.h @@ -82,6 +82,12 @@ enum class ResourceType { OoTBackground = 0x4F424749, // OBGI OoTSceneCommand = 0x4F52434D, // ORCM + // MM + MMTextureAnimation = 0x4F54414E, // OTAN + MMText = 0x4F54584D, // OTXM + MMKeyFrameAnim = 0x4F4B4641, // OKFA + MMKeyFrameSkel = 0x4F4B4653, // OKFS + // BK64 BKSprite = 0x424B5350, // BKSP BKAnimation = 0x424B414E, // BKAN diff --git a/src/factories/oot/MMKeyFrameFactory.cpp b/src/factories/oot/MMKeyFrameFactory.cpp new file mode 100644 index 00000000..33ea8289 --- /dev/null +++ b/src/factories/oot/MMKeyFrameFactory.cpp @@ -0,0 +1,210 @@ +#include "MMKeyFrameFactory.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" + +namespace OoT { + +namespace { + +// ZKeyframeSkelType +constexpr uint8_t KF_SKEL_NORMAL = 0; +constexpr uint8_t KF_SKEL_FLEX = 1; + +// Limb entries are 0xC bytes for a standard skeleton and 0x8 for a flex one. +constexpr uint32_t KF_STANDARD_LIMB_SIZE = 0x0C; +constexpr uint32_t KF_FLEX_LIMB_SIZE = 0x08; + +uint8_t ParseLimbType(const std::string& s) { + return s == "Flex" ? KF_SKEL_FLEX : KF_SKEL_NORMAL; +} + +// A yaml offset is file-relative; give it the file's segment so node lookups and +// AutoDecode see the same form everything else uses. +uint32_t SegmentedOffset(uint32_t offset) { + if (IS_SEGMENTED(offset)) { + return offset; + } + return (Companion::Instance->GetCurrSegmentNumber() << 24) | offset; +} + +uint32_t CountSetBits(uint32_t v) { + uint32_t n = 0; + while (v) { + n += v & 1; + v >>= 1; + } + return n; +} + +class RawData : public IParsedData { +public: + std::vector mBinary; + explicit RawData(std::vector data) : mBinary(std::move(data)) {} +}; + +std::vector Finish(LUS::BinaryWriter& w) { + std::stringstream ss; + w.Finish(ss); + auto str = ss.str(); + return std::vector(str.begin(), str.end()); +} + +} // namespace + +std::optional> MMKeyFrameSkelFactory::parse(std::vector& buffer, + YAML::Node& node) { + const auto offset = GetSafeNode(node, "offset"); + const uint8_t limbType = ParseLimbType(GetSafeNode(node, "limb_type", "Normal")); + + // limbCount @0, dListCount @1, limbsPtr @4 + auto head = ReadSubArray(buffer, offset, 8); + const uint8_t limbCount = head.ReadUByte(); + const uint8_t dListCount = head.ReadUByte(); + head.ReadUInt16(); + const uint32_t limbsPtr = head.ReadUInt32(); + + const uint32_t limbSize = (limbType == KF_SKEL_FLEX) ? KF_FLEX_LIMB_SIZE : KF_STANDARD_LIMB_SIZE; + auto limbs = ReadSubArray(buffer, limbsPtr, limbCount * limbSize); + + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::MMKeyFrameSkel, 0); + w.Write(limbCount); + w.Write(dListCount); + w.Write(limbType); + w.Write(limbCount); // limbList->numLimbs, which the skeleton's own count sets + + for (uint8_t i = 0; i < limbCount; i++) { + const uint32_t dlist = limbs.ReadUInt32(); + const uint8_t numChildren = limbs.ReadUByte(); + const uint8_t flags = limbs.ReadUByte(); + + w.Write(SEGMENT_OFFSET(dlist) != 0 ? ResolvePointer(dlist) : std::string()); + w.Write(numChildren); + w.Write(flags); + + if (limbType == KF_SKEL_FLEX) { + w.Write(limbs.ReadUByte()); // callbackIndex + limbs.ReadUByte(); // pad to the 0x8 stride + } else { + w.Write(limbs.ReadInt16()); // translation.x + w.Write(limbs.ReadInt16()); // translation.y + w.Write(limbs.ReadInt16()); // translation.z + } + } + + return std::make_shared(Finish(w)); +} + +ExportResult MMKeyFrameSkelBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto data = std::static_pointer_cast(raw); + write.write(data->mBinary.data(), data->mBinary.size()); + return std::nullopt; +} + +std::optional> MMKeyFrameAnimFactory::parse(std::vector& buffer, + YAML::Node& node) { + const auto offset = GetSafeNode(node, "offset"); + const auto skelOffset = SegmentedOffset(GetSafeNode(node, "skel_offset")); + + // The animation is sized by its skeleton: how many limbs, and whether the + // per-limb bit flags are 8 or 16 bits wide. + uint8_t limbType = KF_SKEL_NORMAL; + auto skelNode = Companion::Instance->GetNodeByAddr(skelOffset); + if (skelNode.has_value()) { + auto [_, sn] = skelNode.value(); + limbType = ParseLimbType(GetSafeNode(sn, "limb_type", "Normal")); + } else { + SPDLOG_WARN("MM keyframe anim at 0x{:X}: no skeleton declared at 0x{:X}", offset, skelOffset); + } + const uint8_t limbCount = ReadSubArray(buffer, skelOffset, 1).ReadUByte(); + + auto head = ReadSubArray(buffer, offset, 0x14); + const uint32_t bitFlagsAddr = head.ReadUInt32(); + const uint32_t keyFramesAddr = head.ReadUInt32(); + const uint32_t kfNumsAddr = head.ReadUInt32(); + const uint32_t presetValuesAddr = head.ReadUInt32(); + const uint16_t unk10 = head.ReadUInt16(); + const int16_t duration = head.ReadInt16(); + + // Each limb's flags say which of its channels are animated: a set bit spends a + // kfNum, a clear one spends a preset value. Standard skeletons use six bits of + // a byte, flex ones nine bits of a halfword. + std::vector bitFlags; + uint32_t kfNumsSize = 0, presetValuesSize = 0; + const uint32_t flagWidth = (limbType == KF_SKEL_FLEX) ? 2 : 1; + const uint32_t flagMask = (limbType == KF_SKEL_FLEX) ? 0b111111111 : 0b111111; + const uint32_t flagInvert = (limbType == KF_SKEL_FLEX) ? 0xFFFF : 0xFF; + + auto flags = ReadSubArray(buffer, bitFlagsAddr, limbCount * flagWidth); + for (uint8_t i = 0; i < limbCount; i++) { + const uint16_t e = (flagWidth == 2) ? flags.ReadUInt16() : flags.ReadUByte(); + bitFlags.push_back(e); + kfNumsSize += CountSetBits(e & flagMask); + presetValuesSize += CountSetBits((e ^ flagInvert) & flagMask); + } + + std::vector kfNums; + uint32_t keyFramesCount = 0; + if (kfNumsSize > 0) { + auto r = ReadSubArray(buffer, kfNumsAddr, kfNumsSize * 2); + for (uint32_t i = 0; i < kfNumsSize; i++) { + const int16_t n = r.ReadInt16(); + keyFramesCount += n; + kfNums.push_back(n); + } + } + + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::MMKeyFrameAnim, 0); + w.Write(limbType); + + w.Write(static_cast(bitFlags.size())); + for (const auto b : bitFlags) { + if (flagWidth == 2) { + w.Write(b); + } else { + w.Write(static_cast(b)); + } + } + + w.Write(keyFramesCount); + if (keyFramesCount > 0) { + auto r = ReadSubArray(buffer, keyFramesAddr, keyFramesCount * 6); + for (uint32_t i = 0; i < keyFramesCount; i++) { + w.Write(r.ReadInt16()); // frame + w.Write(r.ReadInt16()); // value + w.Write(r.ReadInt16()); // velocity + } + } + + w.Write(static_cast(kfNums.size())); + for (const auto n : kfNums) { + w.Write(n); + } + + w.Write(presetValuesSize); + if (presetValuesSize > 0) { + auto r = ReadSubArray(buffer, presetValuesAddr, presetValuesSize * 2); + for (uint32_t i = 0; i < presetValuesSize; i++) { + w.Write(r.ReadInt16()); + } + } + + w.Write(unk10); + w.Write(duration); + + return std::make_shared(Finish(w)); +} + +ExportResult MMKeyFrameAnimBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto data = std::static_pointer_cast(raw); + write.write(data->mBinary.data(), data->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT diff --git a/src/factories/oot/MMKeyFrameFactory.h b/src/factories/oot/MMKeyFrameFactory.h new file mode 100644 index 00000000..0ab9a0b7 --- /dev/null +++ b/src/factories/oot/MMKeyFrameFactory.h @@ -0,0 +1,37 @@ +#pragma once + +#include "factories/BaseFactory.h" + +namespace OoT { + +// Majora's Mask keyframe skeletons and animations. OoT has neither, so these are +// MM-only rather than a branch inside a shared factory. Mirrors OTRExporter's +// CKeyFrameExporter.cpp and ZAPD's ZCKeyFrame / ZCKeyFrameAnim. + +class MMKeyFrameSkelBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MMKeyFrameSkelFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { REGISTER(Binary, MMKeyFrameSkelBinaryExporter) }; + } +}; + +class MMKeyFrameAnimBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MMKeyFrameAnimFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { REGISTER(Binary, MMKeyFrameAnimBinaryExporter) }; + } +}; + +} // namespace OoT diff --git a/src/factories/oot/MMTextFactory.cpp b/src/factories/oot/MMTextFactory.cpp new file mode 100644 index 00000000..0edbcd51 --- /dev/null +++ b/src/factories/oot/MMTextFactory.cpp @@ -0,0 +1,238 @@ +#include "MMTextFactory.h" +#include "spdlog/spdlog.h" +#include "Companion.h" +#include "utils/Decompressor.h" + +// Majora's Mask message text. The layout, the control codes and their argument +// widths all come from ZAPDTR/ZAPD/ZTextMM.cpp (ZTextMM::ParseMM); the field +// order written out is OTRExporter/TextMMExporter.cpp. +// +// The message *table* lives in `code` and the message *text* in the resource's +// own file, so this needs both: code_phys_start/code_offset for the table, and +// segment 128 for the text. +// +// MM's format is not OoT's. The table entry is 8 bytes with the offset at +4 +// (top byte segment, low 24 bits the offset), each message carries a 11-byte +// header of its own, the terminator is 0xBF rather than 0x02, and the control +// codes that take arguments are a different set. + +namespace OoT { + +struct MMTextData : public IParsedData { + std::vector mBinary; +}; + +namespace { + +// ZAPD indexes its buffers without bounds checks. Reads past the end are not +// expected; returning 0 keeps a malformed table from running off the buffer +// instead of crashing, and matches ZAPD wherever the data is well formed. +uint8_t ReadU8(const uint8_t* data, size_t size, size_t at) { + return at < size ? data[at] : 0; +} + +uint16_t ReadU16BE(const uint8_t* data, size_t size, size_t at) { + return static_cast((ReadU8(data, size, at) << 8) | ReadU8(data, size, at + 1)); +} + +uint32_t ReadU32BE(const uint8_t* data, size_t size, size_t at) { + return (static_cast(ReadU16BE(data, size, at)) << 16) | ReadU16BE(data, size, at + 2); +} + +// The staff credits messages have no header, end at 0x02, and use their own +// control codes. ZTextMM.cpp, the `staff_message_data_static` branch. +std::string ReadStaffMessage(const uint8_t* rawData, size_t rawSize, uint32_t msgPtr) { + std::string msg; + + while (msgPtr < rawSize) { + const uint8_t c = ReadU8(rawData, rawSize, msgPtr); + msg += static_cast(c); + + if (c == 0x02) { // END + break; + } + + unsigned int args = 0; + switch (c) { + case 0x05: // COLOR + case 0x06: // SHIFT + case 0x0E: // FADE + case 0x13: // ITEM ICON + case 0x14: // TEXT SPEED + case 0x1E: // HIGHSCORE + args = 1; + break; + case 0x07: // TEXTID + case 0x0C: // BOX BREAK DELAY + case 0x11: // FADE2 + case 0x12: // SFX + args = 2; + break; + case 0x15: // BACKGROUND + args = 3; + break; + default: + break; + } + + for (unsigned int i = 1; i <= args; i++) { + msg += static_cast(ReadU8(rawData, rawSize, msgPtr + i)); + } + msgPtr += args + 1; + } + + return msg; +} + +// NES messages end at 0xBF. The 11-byte header has already been consumed by the +// caller, which is where msgPtr points. +std::string ReadNesMessage(const uint8_t* rawData, size_t rawSize, uint32_t msgPtr) { + std::string msg; + + while (msgPtr < rawSize) { + const uint8_t c = ReadU8(rawData, rawSize, msgPtr); + msg += static_cast(c); + + if (c == 0xBF) { // END + break; + } + + unsigned int args = 0; + switch (c) { + case 0x14: // SHIFT + args = 1; + break; + case 0x1B: // BOX BREAK DELAY + case 0x1C: // FADE + case 0x1D: // FADE SKIPPABLE + case 0x1E: // SFX + case 0x1F: // DELAY + args = 2; + break; + default: + break; + } + + for (unsigned int i = 1; i <= args; i++) { + msg += static_cast(ReadU8(rawData, rawSize, msgPtr + i)); + } + msgPtr += args + 1; + } + + return msg; +} + +} // namespace + +std::optional> MMTextFactory::parse(std::vector& buffer, YAML::Node& node) { + auto codePhysStart = GetSafeNode(node, "code_phys_start"); + auto codeOffset = GetSafeNode(node, "code_offset"); + const uint32_t langOffset = node["lang_offset"] ? node["lang_offset"].as() : 0; + + // ZAPD keys the staff format off the file's name; so does this. + const auto symbol = GetSafeNode(node, "symbol"); + const bool isStaff = symbol == "staff_message_data_static"; + + 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("MMTextFactory: failed to decode code segment"); + return std::nullopt; + } + const uint8_t* codeData = codeChunk->data; + const size_t codeSize = codeChunk->size; + + auto msgSeg = Companion::Instance->GetFileOffsetFromSegmentedAddr(128); + if (!msgSeg.has_value()) { + SPDLOG_ERROR("MMTextFactory: message data segment 128 not found"); + return std::nullopt; + } + const uint8_t* rawData = buffer.data() + msgSeg.value(); + const size_t rawSize = buffer.size() - msgSeg.value(); + + uint32_t currentPtr = codeOffset; + uint32_t langPtr = currentPtr; + const bool isPalLang = (langOffset != 0 && langOffset != codeOffset); + if (langOffset != 0) { + langPtr = langOffset; + } + + std::vector messages; + while (currentPtr + 8 <= codeSize && langPtr + 8 <= codeSize) { + MMMessageEntry entry; + entry.id = ReadU16BE(codeData, codeSize, currentPtr); + + uint32_t msgPtr = ReadU32BE(codeData, codeSize, langPtr + 4) & 0x00FFFFFF; + + if (isStaff) { + // ZAPD reads the packed type/position byte out of the table and then + // immediately zeroes textboxType again -- but not textboxYPos, which + // keeps the low nibble. Reproduced as written. + const uint8_t typePos = ReadU8(codeData, codeSize, currentPtr + 2); + entry.textboxType = (typePos & 0xF0) >> 4; + entry.textboxYPos = typePos & 0x0F; + entry.textboxType = 0; + + entry.msg = ReadStaffMessage(rawData, rawSize, msgPtr); + } else { + entry.textboxType = ReadU8(rawData, rawSize, msgPtr + 0); + entry.textboxYPos = ReadU8(rawData, rawSize, msgPtr + 1); + entry.icon = ReadU8(rawData, rawSize, msgPtr + 2); + entry.nextMessageID = ReadU16BE(rawData, rawSize, msgPtr + 3); + entry.firstItemCost = ReadU16BE(rawData, rawSize, msgPtr + 5); + entry.secondItemCost = ReadU16BE(rawData, rawSize, msgPtr + 7); + + entry.msg = ReadNesMessage(rawData, rawSize, msgPtr + 11); + } + + messages.push_back(std::move(entry)); + + if (messages.back().id == 0xFFFC || messages.back().id == 0xFFFF) { + break; + } + + currentPtr += 8; + langPtr += isPalLang ? 4 : 8; + } + + SPDLOG_INFO("MMTextFactory: parsed {} messages for {}", messages.size(), symbol); + + auto data = std::make_shared(); + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::MMText, 0); + + w.Write(static_cast(messages.size())); + for (auto& m : messages) { + w.Write(m.id); + w.Write(m.textboxType); + w.Write(m.textboxYPos); + w.Write(m.icon); + w.Write(m.nextMessageID); + w.Write(m.firstItemCost); + w.Write(m.secondItemCost); + w.Write(m.msg); + } + + std::stringstream ss; + w.Finish(ss); + std::string str = ss.str(); + data->mBinary = std::vector(str.begin(), str.end()); + + return data; +} + +ExportResult MMTextBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, std::string* replacement) { + auto data = std::static_pointer_cast(raw); + write.write(data->mBinary.data(), data->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT diff --git a/src/factories/oot/MMTextFactory.h b/src/factories/oot/MMTextFactory.h new file mode 100644 index 00000000..3aacc894 --- /dev/null +++ b/src/factories/oot/MMTextFactory.h @@ -0,0 +1,36 @@ +#pragma once + +#include "factories/BaseFactory.h" + +namespace OoT { + +// One entry of Majora's Mask's message table. Mirrors MessageEntryMM in +// ZAPDTR/ZAPD/ZTextMM.h -- the fields, and their widths, are what the exporter +// writes. +struct MMMessageEntry { + uint16_t id = 0; + uint8_t textboxType = 0; + uint8_t textboxYPos = 0; + uint16_t icon = 0; + uint16_t nextMessageID = 0; + uint16_t firstItemCost = 0; + uint16_t secondItemCost = 0; + std::string msg; +}; + +class MMTextBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MMTextFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, MMTextBinaryExporter) + }; + } +}; + +} // namespace OoT diff --git a/src/factories/oot/MMTextureAnimationFactory.cpp b/src/factories/oot/MMTextureAnimationFactory.cpp new file mode 100644 index 00000000..b82e13fb --- /dev/null +++ b/src/factories/oot/MMTextureAnimationFactory.cpp @@ -0,0 +1,204 @@ +#include "MMTextureAnimationFactory.h" +#include "OoTSceneUtils.h" +#include "spdlog/spdlog.h" +#include "Companion.h" + +namespace OoT { + +// ZAPD's TextureAnimationParamsType +enum class TexAnimType : int16_t { + SingleScroll = 0, + DualScroll = 1, + ColorChange = 2, + ColorChangeLERP = 3, + ColorChangeLagrange = 4, + TextureCycle = 5, + Empty = 6, +}; + +class MMTextureAnimationData : public IParsedData { +public: + std::vector mBinary; + explicit MMTextureAnimationData(std::vector data) : mBinary(std::move(data)) {} +}; + +// Resolve a texture pointer to its asset path, for the TextureCycle list. +static std::string ResolveTexturePath(uint32_t ptr) { + auto node = Companion::Instance->GetNodeByAddr(ptr); + if (node.has_value()) { + return std::get<0>(node.value()); + } + SPDLOG_ERROR("MM texture animation: texture not found: 0x{:X}", ptr); + return ""; +} + +std::vector SerializeTextureAnimation(std::vector& buffer, uint32_t segAddr, + const std::string& resPath) { + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::MMTextureAnimation, 0); + + // The entry list is 8 bytes per entry -- segment (s8), type (s16 at +2), + // paramsPtr (u32 at +4) -- and runs until an entry whose segment is <= 0, which + // is itself included. + struct Entry { + int8_t segment; + int16_t type; + uint32_t paramsPtr; + }; + std::vector entries; + for (uint32_t i = 0;; i++) { + auto reader = ReadSubArray(buffer, segAddr + i * 8, 8); + Entry e{}; + e.segment = static_cast(reader.ReadUByte()); + reader.ReadUByte(); + e.type = reader.ReadInt16(); + e.paramsPtr = reader.ReadUInt32(); + entries.push_back(e); + if (e.segment <= 0) { + break; + } + if (i > 64) { // no real list is this long; avoid running away on bad data + SPDLOG_WARN("MM texture animation at 0x{:X}: entry list did not terminate", segAddr); + break; + } + } + + w.Write(static_cast(entries.size())); + + for (const auto& e : entries) { + w.Write(e.segment); + w.Write(e.type); + + switch (static_cast(e.type)) { + case TexAnimType::SingleScroll: + case TexAnimType::DualScroll: { + const int count = (static_cast(e.type) == TexAnimType::DualScroll) ? 2 : 1; + auto p = ReadSubArray(buffer, e.paramsPtr, count * 4); + for (int r = 0; r < count; r++) { + w.Write(static_cast(p.ReadUByte())); // xStep + w.Write(static_cast(p.ReadUByte())); // yStep + w.Write(p.ReadUByte()); // width + w.Write(p.ReadUByte()); // height + } + break; + } + case TexAnimType::ColorChange: + case TexAnimType::ColorChangeLERP: + case TexAnimType::ColorChangeLagrange: { + auto p = ReadSubArray(buffer, e.paramsPtr, 0x10); + uint16_t animLength = p.ReadUInt16(); + uint16_t colorListCount = p.ReadUInt16(); + uint32_t primColorListAddr = p.ReadUInt32(); + uint32_t envColorListAddr = p.ReadUInt32(); + uint32_t frameDataListAddr = p.ReadUInt32(); + + // Type 2 sizes its lists by animLength; 3 and 4 by colorListCount. + const uint16_t listLength = + (static_cast(e.type) == TexAnimType::ColorChange) ? animLength : colorListCount; + + w.Write(animLength); + w.Write(colorListCount); + + if (frameDataListAddr != 0) { + auto f = ReadSubArray(buffer, frameDataListAddr, listLength * 2); + w.Write(static_cast(listLength)); + for (uint16_t i = 0; i < listLength; i++) { + w.Write(f.ReadUInt16()); + } + } else { + w.Write(static_cast(0)); + } + + if (primColorListAddr != 0) { + auto c = ReadSubArray(buffer, primColorListAddr, listLength * 5); + w.Write(static_cast(listLength)); + for (uint16_t i = 0; i < listLength; i++) { + for (int b = 0; b < 5; b++) { // r, g, b, a, lodFrac + w.Write(c.ReadUByte()); + } + } + } else { + w.Write(static_cast(0)); + } + + if (envColorListAddr != 0) { + auto c = ReadSubArray(buffer, envColorListAddr, listLength * 4); + w.Write(static_cast(listLength)); + for (uint16_t i = 0; i < listLength; i++) { + for (int b = 0; b < 4; b++) { // r, g, b, a + w.Write(c.ReadUByte()); + } + } + } else { + w.Write(static_cast(0)); + } + break; + } + case TexAnimType::TextureCycle: { + auto p = ReadSubArray(buffer, e.paramsPtr, 0x0C); + uint16_t cycleLength = p.ReadUInt16(); + p.ReadUInt16(); // padding + uint32_t textureListAddr = p.ReadUInt32(); + uint32_t textureIndexListAddr = p.ReadUInt32(); + + // The index list is cycleLength bytes; the texture list is sized by + // the largest index it names, inclusive. + auto idx = ReadSubArray(buffer, textureIndexListAddr, cycleLength); + std::vector indices; + uint8_t maxIndex = 0; + for (uint16_t i = 0; i < cycleLength; i++) { + uint8_t v = idx.ReadUByte(); + indices.push_back(v); + maxIndex = std::max(maxIndex, v); + } + + const uint32_t textureCount = static_cast(maxIndex) + 1; + auto tex = ReadSubArray(buffer, textureListAddr, textureCount * 4); + + w.Write(cycleLength); + w.Write(textureCount); + for (uint32_t i = 0; i < textureCount; i++) { + w.Write(ResolveTexturePath(tex.ReadUInt32())); + } + for (const auto v : indices) { + w.Write(v); + } + break; + } + case TexAnimType::Empty: { + w.Write(static_cast(0)); // SEGMENTED_NULL + break; + } + default: { + SPDLOG_ERROR("MM texture animation {}: unknown params type {}", resPath, e.type); + break; + } + } + } + + std::stringstream ss; + w.Finish(ss); + auto str = ss.str(); + return std::vector(str.begin(), str.end()); +} + +std::optional> MMTextureAnimationFactory::parse(std::vector& buffer, + YAML::Node& node) { + auto offset = GetSafeNode(node, "offset"); + auto symbol = GetSafeNode(node, "symbol", ""); + auto data = SerializeTextureAnimation(buffer, offset, symbol); + if (data.empty()) { + return std::nullopt; + } + return std::make_shared(std::move(data)); +} + +ExportResult MMTextureAnimationBinaryExporter::Export(std::ostream& write, std::shared_ptr raw, + std::string& entryName, YAML::Node& node, + std::string* replacement) { + auto anim = std::static_pointer_cast(raw); + write.write(anim->mBinary.data(), anim->mBinary.size()); + return std::nullopt; +} + +} // namespace OoT diff --git a/src/factories/oot/MMTextureAnimationFactory.h b/src/factories/oot/MMTextureAnimationFactory.h new file mode 100644 index 00000000..780f8937 --- /dev/null +++ b/src/factories/oot/MMTextureAnimationFactory.h @@ -0,0 +1,30 @@ +#pragma once + +#include "factories/BaseFactory.h" + +namespace OoT { + +// Majora's Mask animated materials. OoT has no equivalent, so this is MM-only +// rather than a branch inside a shared factory. Mirrors OTRExporter's +// TextureAnimationExporter.cpp and ZAPD's ZTextureAnimation. +class MMTextureAnimationBinaryExporter : public BaseExporter { + ExportResult Export(std::ostream& write, std::shared_ptr data, std::string& entryName, + YAML::Node& node, std::string* replacement) override; +}; + +class MMTextureAnimationFactory : public BaseFactory { +public: + std::optional> parse(std::vector& buffer, YAML::Node& data) override; + std::unordered_map> GetExporters() override { + return { + REGISTER(Binary, MMTextureAnimationBinaryExporter) + }; + } +}; + +// Serialize an animated material list at a segmented address. Shared with the +// scene command writer, which discovers these rather than having them declared. +std::vector SerializeTextureAnimation(std::vector& buffer, uint32_t segAddr, + const std::string& resPath); + +} // namespace OoT diff --git a/src/factories/oot/OoTArrayFactory.cpp b/src/factories/oot/OoTArrayFactory.cpp index 8014de93..2013ac1a 100644 --- a/src/factories/oot/OoTArrayFactory.cpp +++ b/src/factories/oot/OoTArrayFactory.cpp @@ -56,11 +56,44 @@ std::optional> OoTArrayFactory::parse(std::vector(node, "scalar_type"); + uint32_t width = 1; + if (scalarType >= 4 && scalarType <= 6) { + width = 2; + } else if (scalarType >= 7 && scalarType <= 9) { + width = 4; + } else if (scalarType > 9) { + SPDLOG_ERROR("Unsupported scalar array type {}", scalarType); + return std::nullopt; + } + + LUS::BinaryReader reader(segment.data, segment.size); + reader.SetEndianness(Torch::Endianness::Big); + std::vector values; + for (size_t i = 0; i < count; i++) { + switch (width) { + case 2: values.push_back(reader.ReadUInt16()); break; + case 4: values.push_back(reader.ReadUInt32()); break; + default: values.push_back(reader.ReadUByte()); break; + } + } + return std::make_shared(scalarType, std::move(values)); + } + + if (arrayType == "CollisionPoly" || arrayType == "Pointer") { + const auto type = arrayType == "CollisionPoly" ? SohArrayType::CollisionPoly : SohArrayType::Pointer; + return std::make_shared(static_cast(type), count); + } + SPDLOG_ERROR("Unknown OoT Array type '{}'", arrayType); return std::nullopt; } -static void exportVtxArray(LUS::BinaryWriter& writer, std::shared_ptr data, bool zeroFlag) { +static void exportVtxArray(LUS::BinaryWriter& writer, std::shared_ptr data, bool zeroFlag, + bool sunTc) { writer.Write(static_cast(SohArrayType::Vertex)); writer.Write(static_cast(data->mVtxs.size())); @@ -74,7 +107,22 @@ static void exportVtxArray(LUS::BinaryWriter& writer, std::shared_ptr(0) : v.flag); writer.Write(v.tc[0]); - writer.Write(v.tc[1]); + // MM's sun textures are one 64x64 image the rom stores in pieces, which + // ZAPD cannot extract whole (gameplay_keep.xml says so above gSunSunsetTex). + // It compensates by rewriting the t coordinates of the vertices gSunDL + // loads -- ZDisplayList.cpp, GfxdCallback_Vtx: + // + // if (self->GetName() == "gSunDL") + // vtx.t = (((vtx.t >> 5) - 1) / 2) << 5; + // + // so the exported array does not match the rom. Reproduced verbatim, + // integer division and all. + if (sunTc) { + const int32_t t = v.tc[1]; + writer.Write(static_cast((((t >> 5) - 1) / 2) << 5)); + } else { + writer.Write(v.tc[1]); + } writer.Write(v.cn[0]); writer.Write(v.cn[1]); writer.Write(v.cn[2]); @@ -82,6 +130,34 @@ static void exportVtxArray(LUS::BinaryWriter& writer, std::shared_ptr data) { + writer.Write(static_cast(SohArrayType::Scalar)); + writer.Write(static_cast(data->mValues.size())); + + // Each element repeats its type tag, then the value at that type's width. + for (const auto v : data->mValues) { + writer.Write(data->mScalarType); + if (data->mScalarType >= 7 && data->mScalarType <= 9) { + writer.Write(static_cast(v)); + } else if (data->mScalarType >= 4 && data->mScalarType <= 6) { + writer.Write(static_cast(v)); + } else { + writer.Write(static_cast(v)); + } + } +} + +static void exportUntypedArray(LUS::BinaryWriter& writer, std::shared_ptr data) { + writer.Write(data->mArrayType); + writer.Write(static_cast(data->mCount)); + + // ArrayExporter has no writer for these element kinds, so each element is a + // lone type word of NONE with no payload. + for (size_t i = 0; i < data->mCount; i++) { + writer.Write(static_cast(SohScalarType::ZSCALAR_NONE)); + } +} + static void exportVec3sArray(LUS::BinaryWriter& writer, std::shared_ptr data) { writer.Write(static_cast(SohArrayType::Vector)); writer.Write(static_cast(data->mVecs.size())); @@ -106,9 +182,14 @@ ExportResult OoTArrayBinaryExporter::Export(std::ostream& write, std::shared_ptr if (arrayType == "VTX") { bool zeroFlag = node["zero_flag"] && node["zero_flag"].as(); - exportVtxArray(writer, std::static_pointer_cast(raw), zeroFlag); + bool sunTc = node["sun_tc"] && node["sun_tc"].as(); + exportVtxArray(writer, std::static_pointer_cast(raw), zeroFlag, sunTc); } else if (arrayType == "Vec3s") { exportVec3sArray(writer, std::static_pointer_cast(raw)); + } else if (arrayType == "Scalar") { + exportScalarArray(writer, std::static_pointer_cast(raw)); + } else if (arrayType == "CollisionPoly" || arrayType == "Pointer") { + exportUntypedArray(writer, std::static_pointer_cast(raw)); } writer.Finish(write); diff --git a/src/factories/oot/OoTArrayFactory.h b/src/factories/oot/OoTArrayFactory.h index 292f3861..41d4d881 100644 --- a/src/factories/oot/OoTArrayFactory.h +++ b/src/factories/oot/OoTArrayFactory.h @@ -10,8 +10,11 @@ namespace OoT { // Shipwright's ArrayResourceType enum values (must match reference O2R format) enum class SohArrayType : uint32_t { + Scalar = 16, Vector = 24, Vertex = 25, + CollisionPoly = 28, + Pointer = 29, }; // Shipwright's ZScalarType enum values (from ZAPDTR/ZAPD/ZScalar.h) @@ -32,6 +35,26 @@ class OoTVtxArrayData : public IParsedData { }; // Parsed data for OoT Array (Vec3s variant) +// An array of bare scalars. Each element carries its own type tag in the output, +// which is what OTRExporter's ArrayExporter writes. +class OoTScalarArrayData : public IParsedData { +public: + uint32_t mScalarType; + std::vector mValues; + OoTScalarArrayData(uint32_t type, std::vector values) + : mScalarType(type), mValues(std::move(values)) {} +}; + +// An array whose element kind ArrayExporter has no writer for -- CollisionPoly +// and Pointer. It writes one type word per element and no payload, so the +// contents are fully determined by the count. +class OoTUntypedArrayData : public IParsedData { +public: + uint32_t mArrayType; + size_t mCount; + OoTUntypedArrayData(uint32_t arrayType, size_t count) : mArrayType(arrayType), mCount(count) {} +}; + class OoTVec3sArrayData : public IParsedData { public: std::vector mVecs; diff --git a/src/factories/oot/OoTAudioSampleWriter.cpp b/src/factories/oot/OoTAudioSampleWriter.cpp index e8b41c6b..56b3ac5b 100644 --- a/src/factories/oot/OoTAudioSampleWriter.cpp +++ b/src/factories/oot/OoTAudioSampleWriter.cpp @@ -182,7 +182,11 @@ bool AudioSampleWriter::Extract(std::vector& buffer, YAML::Node& node, return false; } uint32_t tableOff = audiotableSeg.value(); - uint32_t tableSize = std::min((uint32_t)0x500000, (uint32_t)(buffer.size() - tableOff)); + // No fixed cap. This was 0x500000, which covers OoT's Audiotable but cuts MM's + // (0x548770) short -- the samples past the cut got a header with no data, + // because the write below is guarded on the data fitting. Every read into this + // buffer is bounds-checked, so taking the rest of the rom is safe. + uint32_t tableSize = static_cast(buffer.size() - tableOff); std::vector audioTable(buffer.begin() + tableOff, buffer.begin() + tableOff + tableSize); AudioParseContext ctx { diff --git a/src/factories/oot/OoTCollisionFactory.cpp b/src/factories/oot/OoTCollisionFactory.cpp index bd711996..96909a2d 100644 --- a/src/factories/oot/OoTCollisionFactory.cpp +++ b/src/factories/oot/OoTCollisionFactory.cpp @@ -201,16 +201,28 @@ std::optional> OoTCollisionFactory::parse(std::vect } } - // Read surface types: count = highest polygon type + 1 - if (polyTypeDefAddr != 0 && !col->polygons.empty()) { + // Read surface types: count = highest polygon type + 1. + // + // ZAPD's loop runs highestPolyType + 1 times unconditionally (ZCollision.cpp), + // so there is always at least one -- including when the header declares no + // polygons, and including when polyTypeDefAddress is null, in which case it + // reads from segment offset 0. Both cases occur in MM: guarding on a non-empty + // polygon list, or on a non-null address, wrote an empty list and dropped its + // eight bytes. + { uint16_t highestType = 0; for (const auto& p : col->polygons) { if (p.type > highestType) highestType = p.type; } uint32_t numSurfaceTypes = highestType + 1; + uint32_t surfaceTypeAddr = polyTypeDefAddr; + if (surfaceTypeAddr == 0) { + surfaceTypeAddr = Companion::Instance->GetCurrSegmentNumber() << 24; + } + YAML::Node stNode; - stNode["offset"] = polyTypeDefAddr; + stNode["offset"] = surfaceTypeAddr; auto stRaw = Decompressor::AutoDecode(stNode, buffer, numSurfaceTypes * 8); LUS::BinaryReader stReader(stRaw.segment.data, stRaw.segment.size); stReader.SetEndianness(Torch::Endianness::Big); diff --git a/src/factories/oot/OoTCutsceneFactory.cpp b/src/factories/oot/OoTCutsceneFactory.cpp index da5ac76e..f7b99c0a 100644 --- a/src/factories/oot/OoTCutsceneFactory.cpp +++ b/src/factories/oot/OoTCutsceneFactory.cpp @@ -218,7 +218,303 @@ void CutsceneSerializer::WriteEntryCountCmd(uint32_t cid, LUS::BinaryReader& rea } } +namespace { + +// Majora's Mask cutscene command classification, from ZCutscene::GetCommandMM. +// Everything funnels into a handful of shapes; the id ranges are the awkward part. +constexpr uint32_t MM_CS_TEXT = 10; // 0x00A +constexpr uint32_t MM_CS_CAMERA_SPLINE = 90; // 0x05A +constexpr uint32_t MM_CS_TRANSITION_GENERAL = 155; // 0x09B +constexpr uint32_t MM_CS_FADE_OUT_SEQ = 156; // 0x09C +constexpr uint32_t MM_CS_TIME = 157; // 0x09D +constexpr uint32_t MM_CS_PLAYER_CUE = 200; // 0x0C8 +constexpr uint32_t MM_CS_RUMBLE = 400; // 0x190 + +// Actor cues share the player cue's 0x30-byte layout. The ranges are from the +// goto in OTRExporter_Cutscene::SaveMM. +bool MMIsActorCue(uint32_t id) { + return (id >= 100 && id <= 149) || id == 201 || (id >= 450 && id <= 599) || id == MM_CS_PLAYER_CUE; +} + +// Bytes per entry in the rom. +uint32_t MMEntryRawSize(uint32_t id) { + if (MMIsActorCue(id)) { + return 0x30; + } + switch (id) { + case MM_CS_TEXT: + case MM_CS_TRANSITION_GENERAL: + case MM_CS_FADE_OUT_SEQ: + case MM_CS_TIME: + case MM_CS_RUMBLE: + return 0x0C; + default: + return 0x08; + } +} + +} // namespace + +// Walk the rom commands to find the cutscene's total byte length. Mirrors +// ZCutscene::ParseRawData: read the id, hand the rest to the command, then +// advance by its size less the four bytes already consumed. +uint32_t CutsceneSerializer::CalculateSizeMM(std::vector& buffer, uint32_t segAddr) { + auto probe = ReadSubArray(buffer, segAddr, 8); + if (probe.GetLength() < 8) { + return 0; + } + uint32_t numCommands = probe.ReadUInt32(); + if (numCommands > 0x1000) { + return 0; + } + + uint32_t pos = 8; + for (uint32_t i = 0; i < numCommands; i++) { + auto head = ReadSubArray(buffer, segAddr + pos, 8); + if (head.GetLength() < 8) { + return 0; + } + uint32_t id = head.ReadUInt32(); + uint32_t numEntries = head.ReadUInt32(); + pos += 4; + + // A wild entry count would send pos far outside the file, and ReadSubArray + // is not safe at an arbitrary address. Treat it as unparseable instead. + if (numEntries > 0x4000 || pos > 0x200000) { + return 0; + } + + if (id == MM_CS_CAMERA_SPLINE) { + // Headers run until a 0xFFFF marker; each is followed by two groups of + // numEntries camera points and one group of misc points. + uint32_t numHeaders = 0, totalCommands = 0; + uint32_t p = pos + 4; + for (;;) { + auto hdr = ReadSubArray(buffer, segAddr + p, 8); + if (hdr.GetLength() < 8) { + return 0; + } + uint16_t first = hdr.ReadUInt16(); + if (first == 0xFFFF) { + break; + } + numHeaders++; + totalCommands += first; + p += 8 + first * 0x0C * 2 + first * 0x08; + if (p > 0x200000) { + return 0; + } + if (numHeaders > 0x400) { + return 0; + } + } + pos += (8 + 8 * numHeaders + (totalCommands * 2) * 0x0C + totalCommands * 8 + 4) - 4; + } else { + pos += (8 + numEntries * MMEntryRawSize(id)) - 4; + } + } + return pos + 8; +} + +std::vector CutsceneSerializer::SerializeMM(std::vector& buffer, uint32_t segAddr) { + uint32_t size = CalculateSizeMM(buffer, segAddr); + if (size == 0) { + return {}; + } + + auto r = ReadSubArray(buffer, segAddr, size); + // ReadSubArray clamps to what the file actually holds. If it came up short the + // walk below would read past the end, so bail rather than run off. + if (r.GetLength() < size) { + SPDLOG_WARN("MM cutscene at 0x{:X}: wanted 0x{:X} bytes, got 0x{:X}", segAddr, size, r.GetLength()); + return {}; + } + + LUS::BinaryWriter w; + BaseExporter::WriteHeader(w, Torch::ResourceType::OoTCutscene, 0); + + uint32_t wordCountPos = w.GetStream()->GetLength(); + w.Write(static_cast(0)); + uint32_t dataStartPos = w.GetStream()->GetLength(); + + uint32_t numCommands = r.ReadUInt32(); + uint32_t endFrame = r.ReadUInt32(); + w.Write(numCommands); + w.Write(endFrame); + + // These never fire on the retail rom, but a layout mistake in a command below + // would otherwise walk off the end of the sub-array. Log and skip instead. + const auto overrun = [&](const char* what, uint32_t i, uint32_t id, uint32_t need) { + SPDLOG_ERROR("MM cutscene 0x{:X}: {} overran at command {}/{} (id {}), pos 0x{:X} + 0x{:X} > len 0x{:X}", + segAddr, what, i, numCommands, id, r.GetBaseAddress(), need, r.GetLength()); + }; + + for (uint32_t i = 0; i < numCommands; i++) { + if (r.GetBaseAddress() + 8 > r.GetLength()) { + overrun("command header", i, 0, 8); + return {}; + } + uint32_t id = r.ReadUInt32(); + w.Write(id); + + if (id == MM_CS_CAMERA_SPLINE) { + // The count here is a byte length, not an entry count. + uint32_t byteLength = r.ReadUInt32(); + w.Write(byteLength); + for (uint32_t guard = 0;; guard++) { + if (guard > 0x400) { + SPDLOG_WARN("MM cutscene at 0x{:X}: spline list did not terminate", segAddr); + return {}; + } + if (r.GetBaseAddress() + 8 > r.GetLength()) { + overrun("spline header", i, id, 8); + return {}; + } + uint16_t numEntries = r.ReadUInt16(); + if (numEntries == 0xFFFF) { + // Footer: the remaining half word belongs to it. + r.ReadUInt16(); + w.Write(static_cast(0xFFFF)); + w.Write(static_cast(0x0004)); + break; + } + uint16_t unused0 = r.ReadUInt16(); + uint16_t unused1 = r.ReadUInt16(); + uint16_t duration = r.ReadUInt16(); + w.Write(CS_CMD_HH(numEntries, unused0)); + w.Write(CS_CMD_HH(unused1, duration)); + + for (uint32_t k = 0; k < numEntries * 2u; k++) { + uint8_t interpType = r.ReadUByte(); + uint8_t weight = r.ReadUByte(); + uint16_t dur = r.ReadUInt16(); + uint16_t posX = r.ReadUInt16(); + uint16_t posY = r.ReadUInt16(); + uint16_t posZ = r.ReadUInt16(); + uint16_t relTo = r.ReadUInt16(); + w.Write(CS_CMD_BBH(interpType, weight, dur)); + w.Write(CS_CMD_HH(posX, posY)); + w.Write(CS_CMD_HH(posZ, relTo)); + } + for (uint32_t k = 0; k < numEntries; k++) { + uint16_t u0 = r.ReadUInt16(); + uint16_t roll = r.ReadUInt16(); + uint16_t fov = r.ReadUInt16(); + uint16_t u1 = r.ReadUInt16(); + w.Write(CS_CMD_HH(u0, roll)); + w.Write(CS_CMD_HH(fov, u1)); + } + } + continue; + } + + uint32_t count = r.ReadUInt32(); + w.Write(count); + + if (r.GetBaseAddress() + count * MMEntryRawSize(id) > r.GetLength()) { + overrun("entries", i, id, count * MMEntryRawSize(id)); + return {}; + } + + for (uint32_t e = 0; e < count; e++) { + uint16_t base = r.ReadUInt16(); + uint16_t startFrame = r.ReadUInt16(); + uint16_t endF = r.ReadUInt16(); + + if (MMIsActorCue(id)) { + uint16_t rotX = r.ReadUInt16(); + uint16_t rotY = r.ReadUInt16(); + uint16_t rotZ = r.ReadUInt16(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HH(endF, rotX)); + w.Write(CS_CMD_HH(rotY, rotZ)); + for (int j = 0; j < 6; j++) { // start/end positions + w.Write(r.ReadUInt32()); + } + for (int j = 0; j < 3; j++) { // normal + w.Write(r.ReadUInt32()); + } + continue; + } + + switch (id) { + case MM_CS_TEXT: { + uint16_t type = r.ReadUInt16(); + uint16_t textId1 = r.ReadUInt16(); + uint16_t textId2 = r.ReadUInt16(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HH(endF, type)); + w.Write(CS_CMD_HH(textId1, textId2)); + break; + } + case MM_CS_TRANSITION_GENERAL: { + uint8_t unk06 = r.ReadUByte(); + uint8_t unk07 = r.ReadUByte(); + uint8_t unk08 = r.ReadUByte(); + r.ReadUByte(); + r.ReadUInt16(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HBB(endF, unk06, unk07)); + w.Write(CS_CMD_BBBB(unk08, 0, 0, 0)); + break; + } + case MM_CS_FADE_OUT_SEQ: { + uint16_t pad = r.ReadUInt16(); + r.ReadUInt32(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HH(endF, pad)); + w.Write(static_cast(0)); + break; + } + case MM_CS_TIME: { + uint8_t hour = r.ReadUByte(); + uint8_t minute = r.ReadUByte(); + r.ReadUInt32(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HBB(endF, hour, minute)); + w.Write(static_cast(0)); + break; + } + case MM_CS_RUMBLE: { + uint8_t intensity = r.ReadUByte(); + uint8_t decayTimer = r.ReadUByte(); + uint8_t decayStep = r.ReadUByte(); + r.ReadUByte(); + r.ReadUInt16(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HBB(endF, intensity, decayTimer)); + w.Write(CS_CMD_BBBB(decayStep, 0, 0, 0)); + break; + } + default: { + uint16_t pad = r.ReadUInt16(); + w.Write(CS_CMD_HH(base, startFrame)); + w.Write(CS_CMD_HH(endF, pad)); + break; + } + } + } + } + + w.Write(static_cast(0xFFFFFFFF)); + w.Write(static_cast(0)); + + uint32_t endPos = w.GetStream()->GetLength(); + w.Seek(wordCountPos, LUS::SeekOffsetType::Start); + w.Write(static_cast((endPos - dataStartPos) / 4)); + w.Seek(endPos, LUS::SeekOffsetType::Start); + + std::stringstream ss; + w.Finish(ss); + auto str = ss.str(); + return std::vector(str.begin(), str.end()); +} + std::vector CutsceneSerializer::Serialize(std::vector& buffer, uint32_t segAddr) { + if (Companion::Instance->IsMajorasMask()) { + return SerializeMM(buffer, segAddr); + } + auto size = CalculateSize(buffer, segAddr); // Size of 0 means corrupt or empty cutscene data diff --git a/src/factories/oot/OoTDListHelpers.cpp b/src/factories/oot/OoTDListHelpers.cpp index 6e814771..d3fc0428 100644 --- a/src/factories/oot/OoTDListHelpers.cpp +++ b/src/factories/oot/OoTDListHelpers.cpp @@ -9,6 +9,24 @@ #include #include "n64/gbi-otr.h" #include "strhash64/StrHash64.h" +#include + +// These helpers are shared between Ocarina of Time and Majora's Mask, which run on +// the same engine and declare the same asset formats under their own type +// prefixes. Look-ups therefore have to accept either prefix -- a display list does +// not care which game's array it found, only that the vertex data is there. +static const char* kArrayTypes[] = { "VTX", "OOT:ARRAY", "MM:ARRAY" }; +static const char* kMtxTypes[] = { "OOT:MTX", "MM:MTX", "MTX" }; + +static bool IsArrayType(const std::string& type) { + for (const auto* candidate : kArrayTypes) { + if (type == candidate) { + return true; + } + } + return false; +} + #define C0(pos, width) ((w0 >> (pos)) & ((1U << width) - 1)) #define ALIGN16(val) (((val) + 0xF) & ~0xF) @@ -169,14 +187,20 @@ static void ExportVtx(uint32_t& w0, uint32_t& w1, return; } - // Direct lookup with OOT:ARRAY support - auto vtxNode = Companion::Instance->GetNodeByAddr(ptr); + // Direct lookup with OOT:ARRAY support. + // + // Look up the *unpatched* address. GetNodeByAddr resolves virtual addresses + // itself, and ptr has already been through PatchVirtualAddr -- which is the + // same function -- so passing ptr subtracts the file's vram base twice. For a + // file with a `virtual:` mapping that turns a correct address into a miss, and + // the miss falls through to the unresolved-virtual-segment path below. + auto vtxNode = Companion::Instance->GetNodeByAddr(w1); std::optional dec = std::nullopt; bool nullCrossFile = false; if (vtxNode.has_value()) { auto [vpath, vn] = vtxNode.value(); auto vtype = GetSafeNode(vn, "type"); - if (vtype == "VTX" || vtype == "OOT:ARRAY") { + if (IsArrayType(vtype)) { dec = vpath; nullCrossFile = vn["null_cross_file"] && vn["null_cross_file"].as(); } @@ -255,8 +279,23 @@ static void ExportDL(uint32_t& w0, uint32_t& w1, LUS::BinaryWriter& writer) { w0 = endValue.words.w0; w1 = endValue.words.w1; } + } else if (Companion::Instance->IsMajorasMask() && SEGMENT_OFFSET(w1) != 0) { + // MM exports a display list it cannot resolve as an index into that + // segment's gfx buffer, rather than leaving the raw address as OoT does. + // This is mostly segments 8-13, which are runtime-swapped and so never + // resolve. See the G_DL case in OTRExporter's DisplayListExporter. + if (branch) { + N64Gfx value = gsSPBranchListOTRIndex(w1); + w0 = value.words.w0; + w1 = value.words.w1; + } else { + N64Gfx value = gsSPDisplayListOTRIndex(w1); + w0 = value.words.w0; + w1 = value.words.w1; + } } else { SPDLOG_WARN("Could not find display list at 0x{:X}", ptr); + // A zero segment offset keeps the plain opcode in both games. w1 = (w1 & 0x0FFFFFFF) + 1; } } @@ -331,17 +370,38 @@ static void ExportGSunDLTextureFixup(uint8_t opcode, uint32_t& w0, uint32_t& w1, 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"); - } + + // GetSafeStringByAddr throws when a node exists but carries a different type, + // so it cannot be used to probe candidates. Resolve the node once and accept + // any of the matrix type names instead. + const auto matrixPathAt = [](uint32_t addr) -> std::optional { + auto node = Companion::Instance->GetNodeByAddr(addr); + if (!node.has_value()) { + return std::nullopt; + } + auto [path, n] = node.value(); + auto type = GetSafeNode(n, "type"); + std::transform(type.begin(), type.end(), type.begin(), ::toupper); + for (const auto* candidate : kMtxTypes) { + if (type == candidate) { + return path; + } + } + return std::nullopt; + }; + + std::optional dec = matrixPathAt(ptr); 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; + for (const auto* type : kMtxTypes) { + auto remapped = RemapSegmentedAddr(ptr, type); + if (remapped == ptr) { + continue; + } + dec = matrixPathAt(remapped); + if (dec.has_value()) { + ptr = remapped; + break; + } } } @@ -571,7 +631,7 @@ static void FlushVtx(YAML::Node& node) { std::optional> SearchVtx(uint32_t ptr) { if (Companion::Instance->GetGBIMinorVersion() != GBIMinorVersion::OoT) return std::nullopt; - std::vector vtxTypes = {"VTX", "OOT:ARRAY"}; + std::vector vtxTypes = { "VTX", "OOT:ARRAY", "MM:ARRAY" }; uint32_t absPtr = ptr; if (IS_SEGMENTED(ptr)) { @@ -589,7 +649,7 @@ std::optional> SearchVtx(uint32_t ptr) { for (auto& dec : decs.value()) { auto [name, node] = dec; - if (type == "OOT:ARRAY") { + if (type == "OOT:ARRAY" || type == "MM:ARRAY") { auto arrayType = GetSafeNode(node, "array_type", ""); if (arrayType != "VTX") continue; } @@ -628,7 +688,11 @@ std::optional Export(std::ostream& write, std::shared_ptr(0xFF)); - auto bhash = CRC64((*replacement).c_str()); + // A display list identifies itself by hashing its own path. ZAPD emits a few + // lists twice under two names (object_horse_link_child's skin-limb DL is one) + // and the copy carries the original's hash, not its own. + auto selfPath = node["duplicate_of"] ? node["duplicate_of"].as() : *replacement; + auto bhash = CRC64(selfPath.c_str()); writer.Write(static_cast((G_MARKER << 24))); writer.Write(0xBEEFBEEF); writer.Write(static_cast(bhash >> 32)); diff --git a/src/factories/oot/OoTPathFactory.cpp b/src/factories/oot/OoTPathFactory.cpp index 00e808b3..d4b0b71a 100644 --- a/src/factories/oot/OoTPathFactory.cpp +++ b/src/factories/oot/OoTPathFactory.cpp @@ -15,22 +15,22 @@ std::optional> OoTPathFactory::parse(std::vector(node, "offset"); uint32_t numPaths = node["num_paths"] ? node["num_paths"].as() : 1; - // Each path entry is 8 bytes: numPoints (u8), padding (3 bytes), pointsAddr (u32) + // Each path entry is 8 bytes: numPoints (u8), unk1 (s8), unk2 (s16), pointsAddr (u32). + // OoT ignores unk1/unk2; MM exports them. auto pathReader = ReadSubArray(buffer, offset, numPaths * 8); - // {numPoints, pointsAddr} - std::vector> pathways; + std::vector pathways; for (uint32_t i = 0; i < numPaths; i++) { - uint8_t numPoints = pathReader.ReadUByte(); - pathReader.ReadUByte(); // padding - pathReader.ReadUByte(); - pathReader.ReadUByte(); - uint32_t pointsAddr = pathReader.ReadUInt32(); + Pathway path{}; + path.numPoints = pathReader.ReadUByte(); + path.unk1 = static_cast(pathReader.ReadUByte()); + path.unk2 = pathReader.ReadInt16(); + path.pointsAddr = pathReader.ReadUInt32(); - if (pointsAddr == 0) break; + if (path.pointsAddr == 0) break; - pathways.push_back({numPoints, pointsAddr}); + pathways.push_back(path); } if (pathways.empty()) return std::nullopt; diff --git a/src/factories/oot/OoTSceneCommandWriter.cpp b/src/factories/oot/OoTSceneCommandWriter.cpp index b3553fa8..c647ce22 100644 --- a/src/factories/oot/OoTSceneCommandWriter.cpp +++ b/src/factories/oot/OoTSceneCommandWriter.cpp @@ -1,4 +1,5 @@ #include "OoTSceneCommandWriter.h" +#include "MMTextureAnimationFactory.h" #include "AliasManager.h" #include "spdlog/spdlog.h" #include "Companion.h" @@ -48,8 +49,28 @@ SceneCommand SceneCommandWriter::Write(uint32_t w0, uint32_t w1, SceneWriteConte WriteSetRoomBehavior(cmdWriter, cmdArg1, cmdArg2); break; } - case SetCameraSettings: { - WriteSetCameraSettings(cmdWriter, cmdArg1, cmdArg2); + case SetCameraSettings: { // 0x19; SetWorldMapVisited in MM + // MM's SetWorldMapVisited writes no body at all -- see RoomExporter.cpp, + // where the 0x19 case is guarded by `game != MM_RETAIL`. + if (!Companion::Instance->IsMajorasMask()) { + WriteSetCameraSettings(cmdWriter, cmdArg1, cmdArg2); + } + break; + } + case SetAnimatedMaterialList: { + WriteSetAnimatedMaterialList(cmdWriter, cmdArg2, ctx); + break; + } + case SetActorCutsceneList: { + WriteSetActorCutsceneList(cmdWriter, cmdArg1, cmdArg2, ctx); + break; + } + case SetMinimapList: { + WriteSetMinimapList(cmdWriter, cmdArg2, ctx); + break; + } + case SetMinimapChests: { + WriteSetMinimapChests(cmdWriter, cmdArg1, cmdArg2, ctx); break; } case SetSpecialObjects: { @@ -109,7 +130,14 @@ SceneCommand SceneCommandWriter::Write(uint32_t w0, uint32_t w1, SceneWriteConte break; } case SetCutscenes: { - WriteSetCutscenes(cmdWriter, cmdArg2, ctx); + // MM's cutscene command is a list, and ZAPD rewrites the opcode to 0x1F + // when exporting it. Match both. + if (Companion::Instance->IsMajorasMask()) { + cmd.cmdID = SetCutscenesMM; + WriteSetCutscenesMM(cmdWriter, cmdArg1, cmdArg2, ctx); + } else { + WriteSetCutscenes(cmdWriter, cmdArg2, ctx); + } break; } case SetAlternateHeaders: { @@ -210,7 +238,20 @@ void SceneCommandWriter::WriteSetSkyboxSettings(LUS::BinaryWriter& w, uint8_t cm void SceneCommandWriter::WriteSetRoomBehavior(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2) { w.Write(static_cast(cmdArg1)); // gameplayFlags - w.Write(cmdArg2); // gameplayFlags2 + + // OoT writes gameplayFlags2 whole; MM unpacks it into the individual fields it + // encodes, six bytes in place of OoT's five. The bit positions are from + // SetRoomBehavior::ParseRawData in ZAPD. + if (Companion::Instance->IsMajorasMask()) { + w.Write(static_cast(cmdArg2 & 0xFF)); // currRoomUnk2 + w.Write(static_cast((cmdArg2 >> 8) & 1)); // currRoomUnk5 + w.Write(static_cast((cmdArg2 >> 10) & 1)); // msgCtxUnk + w.Write(static_cast((cmdArg2 >> 11) & 1)); // enablePosLights + w.Write(static_cast((cmdArg2 >> 12) & 1)); // kankyoContextUnkE2 + return; + } + + w.Write(cmdArg2); // gameplayFlags2 } void SceneCommandWriter::WriteSetCameraSettings(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2) { @@ -282,8 +323,13 @@ void SceneCommandWriter::WriteSetLightList(LUS::BinaryWriter& w, uint8_t cmdArg1 uint32_t count = cmdArg1; auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 14); w.Write(static_cast(count)); + // The rom struct is 14 bytes with a pad byte after type: type @0, x @2, y @4, + // z @6, r @8, g @9, b @10, drawGlow @11, radius @12 -- see LightInfo in ZAPD's + // SetLightList.cpp. Reading the fields back to back shifts everything after + // type by one byte. for (uint32_t i = 0; i < count; i++) { w.Write(sub.ReadUByte()); // type + sub.ReadUByte(); // padding w.Write(sub.ReadInt16()); // x w.Write(sub.ReadInt16()); // y w.Write(sub.ReadInt16()); // z @@ -310,6 +356,8 @@ void SceneCommandWriter::WriteSetRoomList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t count = cmdArg1; auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 8); w.Write(static_cast(count)); + // SetMinimapList takes its entry count from the scene's room count. + ctx.roomCount = count; // Derive scene base name from current directory (e.g. "scenes/nonmq/bdan_scene" → "bdan") std::string sceneBase; @@ -320,8 +368,15 @@ void SceneCommandWriter::WriteSetRoomList(LUS::BinaryWriter& w, uint8_t cmdArg1, roomBase = roomBase.substr(0, roomBase.size() - 6); } + // OoT numbers rooms plainly; MM zero-pads to two digits (_room_00), which is + // also how its XML names the room files. + const bool isMM = Companion::Instance->IsMajorasMask(); for (uint32_t i = 0; i < count; i++) { - std::string roomName = ctx.currentDir + "/" + roomBase + "_room_" + std::to_string(i); + std::string index = std::to_string(i); + if (isMM && index.size() < 2) { + index.insert(index.begin(), '0'); + } + std::string roomName = ctx.currentDir + "/" + roomBase + "_room_" + index; w.Write(roomName); w.Write(sub.ReadUInt32()); // virtualAddressStart w.Write(sub.ReadUInt32()); // virtualAddressEnd @@ -541,22 +596,28 @@ void SceneCommandWriter::WriteSetPathways(LUS::BinaryWriter& w, uint32_t cmdArg2 uint32_t maxPaths = GetNeighborSize(ctx.knownAddrs, cmdArg2, 8); if (maxPaths == 0) maxPaths = 256; auto pathReader = ReadSubArray(ctx.buffer, cmdArg2, maxPaths * 8); - std::vector> pathways; + std::vector pathways; for (uint32_t i = 0; i < maxPaths; i++) { - uint8_t np = pathReader.ReadUByte(); - pathReader.ReadUByte(); pathReader.ReadUByte(); pathReader.ReadUByte(); - uint32_t ptsAddr = pathReader.ReadUInt32(); - if (ptsAddr == 0 || !IS_SEGMENTED(ptsAddr) || ((ptsAddr >> 24) & 0xFF) != pathSeg) { + Pathway path{}; + path.numPoints = pathReader.ReadUByte(); + path.unk1 = static_cast(pathReader.ReadUByte()); + path.unk2 = pathReader.ReadInt16(); + path.pointsAddr = pathReader.ReadUInt32(); + if (path.pointsAddr == 0 || !IS_SEGMENTED(path.pointsAddr) || + ((path.pointsAddr >> 24) & 0xFF) != pathSeg) { break; } - pathways.push_back({np, ptsAddr}); + pathways.push_back(path); } - if (pathways.empty()) pathways.push_back({0, 0}); - + if (pathways.empty()) pathways.push_back(Pathway{}); auto existingPath = ResolvePointer(cmdArg2); bool hasPreExistingResource = !existingPath.empty(); - if (!hasPreExistingResource && ctx.isAltHeader && pathways.size() > 1) { + // OoT's alternate headers export only the first pathway of a shared list. + // MM exports all of them -- truncating there costs 19 paths, one per + // alt-header path command in the game. + if (!hasPreExistingResource && ctx.isAltHeader && pathways.size() > 1 && + !Companion::Instance->IsMajorasMask()) { pathways.erase(pathways.begin() + 1, pathways.end()); } bool doubled = hasPreExistingResource && (pathways.size() > 1); @@ -567,8 +628,7 @@ void SceneCommandWriter::WriteSetPathways(LUS::BinaryWriter& w, uint32_t cmdArg2 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); + uint32_t pointOffset = SEGMENT_OFFSET(pathways[i].pointsAddr); std::string pathSymbol = MakeAssetName(ctx.baseName, "PathwayList", pointOffset); std::string pathPath = ctx.currentDir + "/" + pathSymbol; w.Write(pathPath); @@ -576,8 +636,7 @@ void SceneCommandWriter::WriteSetPathways(LUS::BinaryWriter& w, uint32_t cmdArg2 } for (uint32_t i = 0; i < pathways.size(); i++) { - auto [np, ptsAddr] = pathways[i]; - uint32_t pointOffset = SEGMENT_OFFSET(ptsAddr); + uint32_t pointOffset = SEGMENT_OFFSET(pathways[i].pointsAddr); std::string pathSymbol = MakeAssetName(ctx.baseName, "PathwayList", pointOffset); auto pathData = SerializePathways(ctx.buffer, pathways, writeCount, repeats); Companion::Instance->RegisterCompanionFile(pathSymbol, pathData); @@ -683,6 +742,127 @@ std::string SceneCommandWriter::ResolveGfxWithAlias(uint32_t ptr, const std::str return path; } +// --- Majora's Mask only commands --- + +void SceneCommandWriter::WriteSetCutscenesMM(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 8); + w.Write(static_cast(count)); + + // 8 bytes per entry: segmentPtr (u32), exit (u16), entrance (u8), flag (u8). + for (uint32_t i = 0; i < count; i++) { + uint32_t segPtr = sub.ReadUInt32(); + uint16_t exit = sub.ReadUInt16(); + uint8_t entrance = sub.ReadUByte(); + uint8_t flag = sub.ReadUByte(); + + // A cutscene that is declared somewhere keeps its declared name; only + // undeclared ones get a synthesized one, off the scene's base name rather + // than the current header's -- an alternate header's cutscenes still belong + // to the scene. + std::string csSymbol; + auto resolved = ResolvePointer(segPtr); + if (resolved.empty()) { + uint32_t csOffset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(segPtr)); + csSymbol = MakeAssetName(ctx.baseName, "CutsceneData", csOffset); + resolved = ctx.currentDir + "/" + csSymbol; + } else { + csSymbol = resolved.substr(resolved.rfind('/') + 1); + } + w.Write(resolved); + w.Write(exit); + w.Write(entrance); + w.Write(flag); + + auto csData = CutsceneSerializer::Serialize(ctx.buffer, segPtr); + if (csData.empty()) { + SPDLOG_WARN("Scene: Skipping cutscene {} due to parse failure", csSymbol); + } + Companion::Instance->RegisterCompanionFile(csSymbol, csData); + } +} + +void SceneCommandWriter::WriteSetAnimatedMaterialList(LUS::BinaryWriter& w, uint32_t cmdArg2, + SceneWriteContext& ctx) { + // The body is just the path of the animated material list. The list itself is a + // separate OTAN resource, which torch has no factory for yet, so only the + // reference is written here. + uint32_t offset = SEGMENT_OFFSET(Companion::Instance->PatchVirtualAddr(cmdArg2)); + std::string symbol = MakeAssetName(ctx.baseName, "TexAnim", offset); + std::string resPath = ctx.currentDir + "/" + symbol; + w.Write(resPath); + + auto animData = SerializeTextureAnimation(ctx.buffer, cmdArg2, resPath); + if (animData.empty()) { + SPDLOG_WARN("Scene: skipping animated material list {} due to parse failure", symbol); + } + Companion::Instance->RegisterCompanionFile(symbol, animData); +} + +void SceneCommandWriter::WriteSetActorCutsceneList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 16); + w.Write(static_cast(count)); + + // 16 bytes per entry; field order and offsets from CutsceneEntry in ZAPD. + for (uint32_t i = 0; i < count; i++) { + int16_t priority = sub.ReadInt16(); + int16_t length = sub.ReadInt16(); + int16_t csCamId = sub.ReadInt16(); + int16_t scriptIndex = sub.ReadInt16(); + int16_t additionalCsId = sub.ReadInt16(); + uint8_t endSfx = sub.ReadUByte(); + uint8_t customValue = sub.ReadUByte(); + int16_t hudVisibility = sub.ReadInt16(); + uint8_t endCam = sub.ReadUByte(); + uint8_t letterboxSize = sub.ReadUByte(); + + w.Write(priority); + w.Write(length); + w.Write(csCamId); + w.Write(scriptIndex); + w.Write(additionalCsId); + w.Write(endSfx); + w.Write(customValue); + w.Write(hudVisibility); + w.Write(endCam); + w.Write(letterboxSize); + } +} + +void SceneCommandWriter::WriteSetMinimapList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx) { + // The command points at a struct holding the entry list address and a scale; the + // entry count is the scene's room count, which SetRoomList recorded. + auto header = ReadSubArray(ctx.buffer, cmdArg2, 8); + uint32_t listAddr = header.ReadUInt32(); + int16_t scale = header.ReadInt16(); + + uint32_t count = ctx.roomCount; + w.Write(static_cast(count)); + w.Write(scale); + + auto sub = ReadSubArray(ctx.buffer, listAddr, count * 10); + for (uint32_t i = 0; i < count; i++) { + for (int f = 0; f < 5; f++) { + w.Write(sub.ReadUInt16()); + } + } +} + +void SceneCommandWriter::WriteSetMinimapChests(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, + SceneWriteContext& ctx) { + uint32_t count = cmdArg1; + auto sub = ReadSubArray(ctx.buffer, cmdArg2, count * 10); + w.Write(static_cast(count)); + for (uint32_t i = 0; i < count; i++) { + for (int f = 0; f < 5; f++) { + w.Write(sub.ReadUInt16()); + } + } +} + uint32_t SceneCommandWriter::GetNeighborSize(const std::set& knownAddrs, uint32_t segAddr, uint32_t entrySize) { uint32_t addr = SEGMENT_OFFSET(segAddr); diff --git a/src/factories/oot/OoTSceneCommandWriter.h b/src/factories/oot/OoTSceneCommandWriter.h index 096a7aef..bba75b57 100644 --- a/src/factories/oot/OoTSceneCommandWriter.h +++ b/src/factories/oot/OoTSceneCommandWriter.h @@ -16,6 +16,9 @@ struct SceneWriteContext { const std::string& assetType; bool isAltHeader; std::vector& pendingAltHeaders; + // Set by SetRoomList; MM's SetMinimapList has one entry per room and carries + // no count of its own. + uint32_t roomCount = 0; }; class SceneCommandWriter { @@ -41,6 +44,11 @@ class SceneCommandWriter { 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 WriteSetCutscenesMM(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetAnimatedMaterialList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetActorCutsceneList(LUS::BinaryWriter& w, uint8_t cmdArg1, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetMinimapList(LUS::BinaryWriter& w, uint32_t cmdArg2, SceneWriteContext& ctx); + void WriteSetMinimapChests(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); diff --git a/src/factories/oot/OoTSceneFactory.h b/src/factories/oot/OoTSceneFactory.h index 06bcfab4..a20cd8f9 100644 --- a/src/factories/oot/OoTSceneFactory.h +++ b/src/factories/oot/OoTSceneFactory.h @@ -36,6 +36,16 @@ enum SceneCmdID : uint32_t { SetCutscenes = 0x17, SetAlternateHeaders = 0x18, SetCameraSettings = 0x19, + + // Majora's Mask only. 0x19 is a reuse, not an addition: MM has + // SetWorldMapVisited where OoT has SetCameraSettings. + SetAnimatedMaterialList = 0x1A, + SetActorCutsceneList = 0x1B, + SetMinimapList = 0x1C, + SetMinimapChests = 0x1E, + // Not a real opcode. ZAPD substitutes it for SetCutscenes when exporting MM, + // because MM's cutscene command carries a list where OoT's carries one pointer. + SetCutscenesMM = 0x1F, }; // Deferred alternate header entry, processed after primary commands. diff --git a/src/factories/oot/OoTSceneUtils.cpp b/src/factories/oot/OoTSceneUtils.cpp index ee0a3011..e889be43 100644 --- a/src/factories/oot/OoTSceneUtils.cpp +++ b/src/factories/oot/OoTSceneUtils.cpp @@ -26,15 +26,23 @@ std::string MakeAssetName(const std::string& baseName, const std::string& suffix return ss.str(); } -std::vector SerializePathways(std::vector& buffer, - const std::vector>& pathways, +std::vector SerializePathways(std::vector& buffer, const std::vector& pathways, uint32_t writeCount, uint32_t repeats) { LUS::BinaryWriter w; BaseExporter::WriteHeader(w, Torch::ResourceType::OoTPath, 0); w.Write(static_cast(writeCount)); + const bool isMM = Companion::Instance->IsMajorasMask(); for (uint32_t r = 0; r < repeats; r++) { - for (auto& [np, ptsAddr] : pathways) { + for (auto& path : pathways) { + const auto np = path.numPoints; + const auto ptsAddr = path.pointsAddr; w.Write(static_cast(np)); + // MM keeps the two fields OoT treats as padding. See OTRExporter's + // PathExporter.cpp, which writes them under a MM_RETAIL check. + if (isMM) { + w.Write(path.unk1); + w.Write(path.unk2); + } auto ptReader = ReadSubArray(buffer, ptsAddr, np * 6); for (uint8_t k = 0; k < np; k++) { w.Write(ptReader.ReadInt16()); diff --git a/src/factories/oot/OoTSceneUtils.h b/src/factories/oot/OoTSceneUtils.h index aa744433..cfbdff85 100644 --- a/src/factories/oot/OoTSceneUtils.h +++ b/src/factories/oot/OoTSceneUtils.h @@ -17,6 +17,9 @@ inline uint32_t CS_CMD_BBH(int8_t a, int8_t b, int16_t c) { 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); } +inline uint32_t CS_CMD_BBBB(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + return (uint32_t)a | ((uint32_t)b << 8) | ((uint32_t)c << 16) | ((uint32_t)d << 24); +} // Helper to read a sub-array from ROM given a segmented pointer LUS::BinaryReader ReadSubArray(std::vector& buffer, uint32_t segAddr, uint32_t size); @@ -27,15 +30,29 @@ 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); +// One entry of a scene's pathway list. The rom struct is 8 bytes: +// numPoints (u8), unk1 (s8), unk2 (s16), pointsAddr (u32) +// OoT ignores unk1/unk2 as padding; MM carries them into the exported asset. +struct Pathway { + uint8_t numPoints; + int8_t unk1; + int16_t unk2; + uint32_t pointsAddr; +}; + // Serialize pathway data into OoTPath binary format. -std::vector SerializePathways(std::vector& buffer, - const std::vector>& pathways, +std::vector SerializePathways(std::vector& buffer, const std::vector& pathways, uint32_t writeCount, uint32_t repeats); class CutsceneSerializer { public: static std::vector Serialize(std::vector& buffer, uint32_t segAddr); + + // Majora's Mask has its own command set; see CutsceneMM_Commands in ZAPD and + // OTRExporter_Cutscene::SaveMM. + static std::vector SerializeMM(std::vector& buffer, uint32_t segAddr); private: + static uint32_t CalculateSizeMM(std::vector& buffer, uint32_t segAddr); static uint32_t CalculateSize(std::vector& buffer, uint32_t segAddr); static std::vector Write(std::vector& buffer, uint32_t segAddr, uint32_t size); static void WriteCameraCmd(LUS::BinaryReader& reader, LUS::BinaryWriter& w); diff --git a/src/factories/oot/OoTSkeletonFactory.cpp b/src/factories/oot/OoTSkeletonFactory.cpp index 70bf1320..dac769ea 100644 --- a/src/factories/oot/OoTSkeletonFactory.cpp +++ b/src/factories/oot/OoTSkeletonFactory.cpp @@ -21,6 +21,15 @@ std::optional> OoTSkeletonFactory::parse(std::vecto uint32_t limbsArrayAddr = reader.ReadUInt32(); limbsArrayAddr = Companion::Instance->PatchVirtualAddr(limbsArrayAddr); uint8_t limbCount = reader.ReadUByte(); + + // The header's limb count and the limb table's length are separate values and + // can disagree: object_fsn's header claims 18 where only 17 limbs exist, and + // its xml comments on the discrepancy. ZAPD writes the header's count as-is + // but walks the table's, so only the table length is overridable here. + uint8_t limbTableCount = limbCount; + if (node["limb_table_count"]) { + limbTableCount = node["limb_table_count"].as(); + } uint8_t dListCount = 0; if (skelType == OoTSkeletonType::Flex) { @@ -30,14 +39,14 @@ std::optional> OoTSkeletonFactory::parse(std::vecto auto symbol = GetSafeNode(node, "symbol"); std::vector limbPaths; - if (limbsArrayAddr != 0 && limbCount > 0) { + if (limbsArrayAddr != 0 && limbTableCount > 0) { YAML::Node limbTableNode; limbTableNode["offset"] = limbsArrayAddr; - auto limbTableRaw = Decompressor::AutoDecode(limbTableNode, buffer, limbCount * 4); + auto limbTableRaw = Decompressor::AutoDecode(limbTableNode, buffer, limbTableCount * 4); LUS::BinaryReader limbTableReader(limbTableRaw.segment.data, limbTableRaw.segment.size); limbTableReader.SetEndianness(Torch::Endianness::Big); - for (uint8_t i = 0; i < limbCount; i++) { + for (uint8_t i = 0; i < limbTableCount; i++) { uint32_t limbAddr = limbTableReader.ReadUInt32(); limbAddr = Companion::Instance->PatchVirtualAddr(limbAddr); std::string limbPath = ResolvePointer(limbAddr); diff --git a/src/n64/gbi-otr.h b/src/n64/gbi-otr.h index 5590ca7b..6eebc14c 100644 --- a/src/n64/gbi-otr.h +++ b/src/n64/gbi-otr.h @@ -170,6 +170,13 @@ _DW({ (uint32_t)((SEGMENT_NUMBER(dl) << 24) | ((SEGMENT_OFFSET(dl) / 8) & 0x00FFFFFF)) \ }} +#define gsSPBranchListOTRIndex(dl) \ + {{ \ + (_SHIFTL((G_DL_OTR_INDEX), 24, 8) | _SHIFTL((0x01), 16, 8) | \ + _SHIFTL((0), 0, 16)), \ + (uint32_t)((SEGMENT_NUMBER(dl) << 24) | ((SEGMENT_OFFSET(dl) / 8) & 0x00FFFFFF)) \ + }} + #define gsSPDisplayListOTRFilePath(dl) \ {{ \ (_SHIFTL((G_DL_OTR_FILEPATH), 24, 8) | _SHIFTL((0x00), 16, 8) | \ diff --git a/src/utils/Decompressor.cpp b/src/utils/Decompressor.cpp index c787282e..53599ea1 100644 --- a/src/utils/Decompressor.cpp +++ b/src/utils/Decompressor.cpp @@ -104,6 +104,54 @@ DataChunk* Decompressor::Decode(const std::vector& buffer, const uint32 gCachedChunks[offset] = new DataChunk{ decompressed, size }; return gCachedChunks[offset]; } + case CompressionType::CMPDMA: { + // Majora's Mask CmpDma container, per CmpDma_GetFileInfo in the decomp + // (mm/src/code/sys_cmpdma.c): + // + // word[0] dataStart -- size of this table, and where data begins + // word[1..n-1] start of sub-file 1..n-1, RELATIVE TO dataStart + // word[n] end of the last sub-file + // + // Sub-file 0 is implicit at relative 0. Sub-file count is dataStart/4 - 1. + // Each sub-file is an independent Yaz0 stream; asset offsets address the + // concatenation of all of them, which is what CmpDma_LoadAllFiles builds. + const auto readU32 = [&](const size_t index) -> uint32_t { + const unsigned char* p = in_buf + index * sizeof(uint32_t); + return static_cast(p[0]) << 24 | static_cast(p[1]) << 16 | + static_cast(p[2]) << 8 | static_cast(p[3]); + }; + + const uint32_t dataStart = readU32(0); + if (dataStart < 2 * sizeof(uint32_t) || dataStart % sizeof(uint32_t) != 0) { + throw std::runtime_error("CMPDMA: implausible dataStart 0x" + Torch::to_hex(dataStart, false) + + " at ROM offset 0x" + Torch::to_hex(offset, false) + + ". Is this file really a CmpDma container?"); + } + const size_t count = dataStart / sizeof(uint32_t) - 1; + + std::vector joined; + for (size_t i = 0; i < count; i++) { + const uint32_t rel = (i == 0) ? 0 : readU32(i); + uint32_t subSize = 0; + uint8_t* sub = yaz0_decode(in_buf + dataStart + rel, &subSize); + if (!sub) { + throw std::runtime_error("CMPDMA: sub-file " + std::to_string(i) + " of " + + std::to_string(count) + " is not a Yaz0 stream (ROM offset 0x" + + Torch::to_hex(offset + dataStart + rel, false) + ")"); + } + joined.insert(joined.end(), sub, sub + subSize); + free(sub); + } + + const auto decompressed = new uint8_t[joined.size()]; + std::copy(joined.begin(), joined.end(), decompressed); + + { + std::lock_guard lock(gDecompCacheMutex); + gCachedChunks[offset] = new DataChunk{ decompressed, joined.size() }; + return gCachedChunks[offset]; + } + } default: throw std::runtime_error("Unknown compression type"); } @@ -209,6 +257,9 @@ DecompressedData Decompressor::AutoDecode(YAML::Node& node, std::vector case CompressionType::YAY0: case CompressionType::YAY1: case CompressionType::MIO0: + // CMPDMA decodes to the concatenation of its sub-files, and asset offsets + // already address that, so it indexes exactly like a single decoded file. + case CompressionType::CMPDMA: case CompressionType::YAZ0: { offset = ASSET_PTR(offset); diff --git a/src/utils/Decompressor.h b/src/utils/Decompressor.h index cc07193f..4db54906 100644 --- a/src/utils/Decompressor.h +++ b/src/utils/Decompressor.h @@ -15,6 +15,11 @@ enum class CompressionType { YAY1, YAZ0, BKZIP, + // Majora's Mask CmpDma container: a table of big-endian u32 offsets followed + // by one independent Yaz0 stream per sub-file. Decoding concatenates them. + // Has no magic of its own, so it is never auto-detected -- a file must opt in + // with `compression: CMPDMA` in its :config:. + CMPDMA, };