Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
fc7f1c9
Add support for reading offsets from a symbol and file list
louist103 Aug 13, 2026
f306edc
Review throw message
louist103 Aug 13, 2026
d6b2be9
Accept a bare 0 offset when resolving against the filelist
briaguya0 Aug 13, 2026
3256012
Add CMPDMA: Majora's Mask CmpDma container decompression
briaguya0 Aug 13, 2026
f2cc9a6
Register MM: types, and let shared DList lookups see either game
briaguya0 Aug 13, 2026
c4c3c4a
Register MM:PATH
briaguya0 Aug 13, 2026
aaa70f5
Export MM pathways: 688/688 byte-identical
briaguya0 Aug 13, 2026
d1f22c6
Implement MM room and scene commands: OROM 595 -> 21 failing
briaguya0 Aug 13, 2026
7405193
Keep declared cutscene names in MM's cutscene list
briaguya0 Aug 13, 2026
8ce5cdc
Add MM texture animations: 293/293 byte-identical first try
briaguya0 Aug 13, 2026
fa092a3
Export MM's unresolvable display lists as indices: dlists 438 -> 42
briaguya0 Aug 13, 2026
f703682
Add MM cutscene serialization: 318 -> 244 failing
briaguya0 Aug 13, 2026
c421bd4
Fix the MM cutscene command ids: cutscenes 244 -> 0 failing
briaguya0 Aug 13, 2026
6ea0c63
Stop resolving virtual vertex addresses twice: dlists 6 -> 0
briaguya0 Aug 13, 2026
d72a4be
Account for the pad byte in SetLightList: 13 scenes fixed
briaguya0 Aug 13, 2026
1da8354
Always emit a surface type in collision headers: 4 fixed
briaguya0 Aug 13, 2026
2d8ef4e
Add MM keyframe skeletons and animations: 12/12 byte-identical
briaguya0 Aug 13, 2026
66eb5e2
Stop capping the Audiotable read at 5MB: 20 samples fixed
briaguya0 Aug 13, 2026
2e954ce
Let a LimbTable override the skeleton's limb table length
briaguya0 Aug 13, 2026
1e9f4fd
Support scalar arrays: object_link_zora_U8_011710
briaguya0 Aug 13, 2026
6e5389f
Let a display list declare itself a duplicate of another
briaguya0 Aug 13, 2026
ce7303a
Build CollisionPoly and Pointer arrays
briaguya0 Aug 13, 2026
7c14f46
Apply ZAPD's gSunDL t-coordinate rewrite
briaguya0 Aug 13, 2026
2899fed
Add MM's message text factory: 50496/50496
briaguya0 Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 135 additions & 12 deletions src/Companion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -327,6 +330,33 @@ void Companion::Init(const ExportType type, std::atomic<size_t>& assetCount, boo
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>());

// 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<OoT::OoTArrayFactory>());
this->RegisterFactory("MM:MTX", std::make_shared<OoT::OoTMtxFactory>());
this->RegisterFactory("MM:SKELETON", std::make_shared<OoT::OoTSkeletonFactory>());
this->RegisterFactory("MM:LIMB", std::make_shared<OoT::OoTLimbFactory>());
this->RegisterFactory("MM:ANIMATION", std::make_shared<OoT::OoTAnimationFactory>());
this->RegisterFactory("MM:CURVE_ANIMATION", std::make_shared<OoT::OoTCurveAnimationFactory>());
this->RegisterFactory("MM:PLAYER_ANIMATION", std::make_shared<OoT::OoTPlayerAnimationHeaderFactory>());
this->RegisterFactory("MM:PLAYER_ANIMATION_DATA", std::make_shared<OoT::OoTPlayerAnimationDataFactory>());
this->RegisterFactory("MM:COLLISION", std::make_shared<OoT::OoTCollisionFactory>());
this->RegisterFactory("MM:TEXT", std::make_shared<OoT::MMTextFactory>());
this->RegisterFactory("MM:SCENE", std::make_shared<OoT::OoTSceneFactory>());
this->RegisterFactory("MM:ROOM", std::make_shared<OoT::OoTSceneFactory>());
this->RegisterFactory("MM:CUTSCENE", std::make_shared<OoT::OoTCutsceneFactory>());
this->RegisterFactory("MM:PATH", std::make_shared<OoT::OoTPathFactory>());
this->RegisterFactory("MM:TEXTURE_ANIMATION", std::make_shared<OoT::MMTextureAnimationFactory>());
this->RegisterFactory("MM:KEYFRAME_SKELETON", std::make_shared<OoT::MMKeyFrameSkelFactory>());
this->RegisterFactory("MM:KEYFRAME_ANIMATION", std::make_shared<OoT::MMKeyFrameAnimFactory>());
this->RegisterFactory("MM:AUDIO", std::make_shared<OoT::OoTAudioFactory>());
#endif

#ifdef BUILD_UI
Expand Down Expand Up @@ -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<CompressionType> ExplicitCompressionType(const YAML::Node& config) {
if (!config || !config["compression"]) {
return std::nullopt;
}
const auto name = config["compression"].as<std::string>();
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<size_t>& assetCount) {
if (node["external_files"]) {
auto externalFiles = node["external_files"];
Expand Down Expand Up @@ -620,16 +670,20 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic<size_t>& 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<uint32_t>();
gCurrentFileOffset = segments[0][1].as<uint32_t>();
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 - [<segment>, "
"<file_offset>]\n\nLike so:\nsegments:\n - [0x06, 0x821D10]");
"Incorrect yaml syntax for segments.\n\nThe yaml expects:\n:config:\n segments:\n - [<segment>, "
"<file_offset>] or - [<segment>, "
"<file_name>] \n\nLike so:\nsegments:\n - [0x06, 0x821D10] or [0x06, object_jya_obj");
}
}

Expand All @@ -638,13 +692,14 @@ void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic<size_t>& ass
auto segment = segments[i];
if (segment.IsSequence() && segment.size() == 2) {
const auto id = segment[0].as<uint32_t>();
const auto replacement = segment[1].as<uint32_t>();
const auto replacement = GetFileOffsetFromNodeStr(segment[1].as<std::string>());
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 - [<segment>, "
"<file_offset>]\n\nLike so:\nsegments:\n - [0x06, 0x821D10]");
"<file_offset>] or - [<segment>, "
"<file_name>] \n\nLike so:\nsegments:\n - [0x06, 0x821D10] or [0x06, object_jya_obj");
}
}
}
Expand Down Expand Up @@ -1183,6 +1238,22 @@ void Companion::ProcessExportFile() {
}
}

void Companion::SetSegmentInfo(const YAML::Node& segments) {
gCurrentSegmentNumber = segments[0][0].as<uint32_t>();

const auto offsetNode = segments[0][1].as<std::string>();
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<size_t>& assetCount) {
// Reset per-file state so segment/offset settings from a previous file don't
// bleed into this file's Phase 1 gAddrMap registration.
Expand All @@ -1201,16 +1272,20 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& 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<uint32_t>();
gCurrentFileOffset = segments[0][1].as<uint32_t>();
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 - [<segment>, "
"<file_offset>]\n\nLike so:\nsegments:\n - [0x06, 0x821D10]");
"<file_offset>] or - [<segment>, "
"<file_name>] \n\nLike so:\nsegments:\n - [0x06, 0x821D10] or [0x06, object_jya_obj");
}
}
}
Expand All @@ -1234,17 +1309,29 @@ void Companion::ProcessFile(YAML::Node root, std::atomic<size_t>& assetCount) {
continue;
}

auto offset = GetFileOffsetFromNodeStr(node["offset"].as<std::string>());

if (gCurrentSegmentNumber) {
if (IS_SEGMENTED(node["offset"].as<uint32_t>()) == false) {
node["offset"] = (gCurrentSegmentNumber << 24) | node["offset"].as<uint32_t>();

if (IS_SEGMENTED(offset) == false) {
offset = (gCurrentSegmentNumber << 24) | offset;
node["offset"] = offset;

}
}

if (!gCurrentVirtualPath.empty()) {
node["path"] = gCurrentVirtualPath;
}

this->gAddrMap[this->gCurrentFile][node["offset"].as<uint32_t>()] = 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
Expand Down Expand Up @@ -1414,9 +1501,32 @@ void Companion::Process(std::atomic<size_t>& assetCount) {
}
}
this->gAssetPath = (this->gSourceDirectory / rom["path"].as<std::string>()).string();

if (rom["filelist"]) {
const std::string filelistPath = (this->gSourceDirectory / rom["filelist"].as<std::string>()).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<std::string>();
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<std::string>() : "modding";

if (!this->gDestinationDirectory.empty() && !fs::exists(this->gDestinationDirectory)) {
Expand Down Expand Up @@ -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<std::string>();
const auto offset = kv.second.as<uint32_t>();
gFileOffsets[file] = offset;
}

}
}
22 changes: 20 additions & 2 deletions src/Companion.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<std::string, std::vector<YAML::Node>> GetCourseMetadata() { return this->gCourseMetadata; }
std::optional<std::string> GetEnumFromValue(const std::string& key, int id);
Expand Down Expand Up @@ -217,8 +229,9 @@ class Companion {
const std::vector<std::tuple<std::string, YAML::Node>>* GetNodesByTypeRef(const std::string& type, bool includeAutogen = false);
std::string GetSymbolFromAddr(uint32_t addr, bool validZero = false);

std::optional<std::uint32_t> GetFileOffset(void) const { return this->gCurrentFileOffset; };
std::optional<std::uint32_t> GetCurrSegmentNumber(void) const { return this->gCurrentSegmentNumber; };
std::string GetCurrentFile(void) { return this->gCurrentFile; }
std::optional<std::uint32_t> GetFileOffsetFromName(void) const { return this->gCurrentFileOffset; };
std::uint32_t GetCurrSegmentNumber(void) const { return this->gCurrentSegmentNumber; };
CompressionType GetCurrCompressionType(void) const { return this->gCurrentCompressionType; };
std::optional<std::uint32_t> GetCurrentCompressedSize(void) const { return this->gCurrentCompressedSize; };
std::optional<VRAMEntry> GetCurrentVRAM(void) const { return this->gCurrentVram; };
Expand Down Expand Up @@ -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<BaseFactoryUI>& factory);
Expand Down Expand Up @@ -301,6 +315,7 @@ class Companion {
std::vector<std::string> gCurrentExternalFiles;
std::unordered_map<int, std::string> gManualSegments;
std::unordered_set<std::string> gProcessedFiles;
std::unordered_map<std::string, uint32_t> gFileOffsets;

std::unordered_map<std::string, std::vector<char>> gCompanionFiles;
std::vector<std::pair<std::string, std::vector<char>>> gArchiveFiles;
Expand Down Expand Up @@ -341,4 +356,7 @@ class Companion {
void LoadYAMLRecursively(const std::string &dirPath, std::vector<YAML::Node> &result, bool skipRoot);
std::vector<fs::directory_entry> GetAssetYMLs(YAML::Node& rom) const;
std::optional<ParseResultData> 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;
};
6 changes: 6 additions & 0 deletions src/factories/ResourceType.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading