#include "Companion.h" #include "utils/Decompressor.h" #include "utils/StringHelper.h" #include "utils/TorchUtils.h" #include "archive/SWrapper.h" #include "archive/ZWrapper.h" #include "spdlog/spdlog.h" #include "TinySHA1.hpp" #include #include #include #include #include #include #include "factories/GenericArrayFactory.h" #include "factories/VtxFactory.h" #include "factories/MtxFactory.h" #include "factories/FloatFactory.h" #include "factories/IncludeFactory.h" #include "factories/TextureFactory.h" #include "factories/DisplayListFactory.h" #include "factories/DisplayListOverrides.h" #include "factories/BlobFactory.h" #include "factories/LightsFactory.h" #include "factories/Vec3fFactory.h" #include "factories/Vec3sFactory.h" #include "factories/AssetArrayFactory.h" #include "factories/ViewportFactory.h" #include "factories/CompressedTextureFactory.h" #ifdef SM64_SUPPORT #include "factories/sm64/AnimationFactory.h" #include "factories/sm64/TextPreview.h" #include "factories/sm64/BehaviorScriptFactory.h" #include "factories/sm64/CollisionFactory.h" #include "factories/sm64/DialogFactory.h" #include "factories/sm64/DictionaryFactory.h" #include "factories/sm64/TextFactory.h" #include "factories/sm64/GeoLayoutFactory.h" #include "factories/sm64/LevelScriptFactory.h" #include "factories/sm64/MacroFactory.h" #include "factories/sm64/MovtexFactory.h" #include "factories/sm64/MovtexQuadFactory.h" #include "factories/sm64/PaintingFactory.h" #include "factories/sm64/PaintingMapFactory.h" #include "factories/sm64/TrajectoryFactory.h" #include "factories/sm64/WaterDropletFactory.h" #endif #ifdef MK64_SUPPORT #include "factories/mk64/CourseVtx.h" #include "factories/mk64/Paths.h" #include "factories/mk64/TrackSections.h" #include "factories/mk64/SpawnData.h" #include "factories/mk64/UnkSpawnData.h" #include "factories/mk64/DrivingBehaviour.h" #include "factories/mk64/ItemCurve.h" #include "factories/mk64/CourseMetadata.h" #include "factories/mk64/PackedDisplayListFactory.h" #endif #ifdef SF64_SUPPORT #include "factories/sf64/ColPolyFactory.h" #include "factories/sf64/MessageFactory.h" #include "factories/sf64/MessageLookupFactory.h" #include "factories/sf64/SkeletonFactory.h" #include "factories/sf64/SF64Preview.h" #include "factories/sf64/AnimFactory.h" #include "factories/sf64/ScriptFactory.h" #include "factories/sf64/HitboxFactory.h" #include "factories/sf64/EnvironmentFactory.h" #include "factories/sf64/ObjInitFactory.h" #include "factories/sf64/TriangleFactory.h" #endif #ifdef PM64_SUPPORT #include "factories/pm64/SpriteFactory.h" #include "factories/pm64/ShapeFactory.h" #include "factories/pm64/BackgroundFactory.h" #include "factories/pm64/AudioPreview.h" #include "factories/pm64/ImagePreview.h" #include "factories/pm64/CollisionFactory.h" #include "factories/pm64/MapTextureFactory.h" #include "factories/pm64/AudioFactory.h" #include "factories/pm64/StoryImageFactory.h" #include "factories/pm64/ImgFXAnimFactory.h" #include "factories/pm64/TitleDataFactory.h" #endif #ifdef FZERO_SUPPORT #include "factories/fzerox/EADAnimationFactory.h" #include "factories/fzerox/CourseFactory.h" #include "factories/fzerox/GhostRecordFactory.h" #include "factories/fzerox/EADLimbFactory.h" #include "factories/fzerox/SequenceFactory.h" #include "factories/fzerox/SoundFontFactory.h" #endif #ifdef BK64_SUPPORT #include "factories/bk64/AnimFactory.h" #include "factories/bk64/BKAssetFactory.h" #include "factories/bk64/ConfigFactory.h" #include "factories/bk64/DemoInputFactory.h" #include "factories/bk64/DialogFactory.h" #include "factories/bk64/GeoLayoutFactory.h" #include "factories/bk64/GruntyQuestionFactory.h" #include "factories/bk64/QuizQuestionFactory.h" #include "factories/bk64/SpriteFactory.h" #include "factories/bk64/ModelFactory.h" #include "factories/bk64/MapFactory.h" #include "factories/bk64/SoundfontTblFactory.h" #endif #ifdef MARIO_ARTIST_SUPPORT #include "factories/mario_artist/MA2D1Factory.h" #endif #ifdef NAUDIO_SUPPORT #include "factories/naudio/v0/AudioHeaderFactory.h" #include "factories/naudio/v0/BankFactory.h" #include "factories/naudio/v0/SampleFactory.h" #include "factories/naudio/v0/SequenceFactory.h" #ifdef BUILD_UI #include "factories/naudio/v0/SequencePlayerV0.h" #include "factories/naudio/v1/SequencePlayerV1.h" #include "ui/audio/SequenceDriver.h" #endif #include "factories/naudio/v1/AudioContext.h" #include "factories/naudio/v1/SoundFontFactory.h" #include "factories/naudio/v1/AudioTableFactory.h" #include "factories/naudio/v1/InstrumentFactory.h" #include "factories/naudio/v1/SampleFactory.h" #include "factories/naudio/v1/DrumFactory.h" #include "factories/naudio/v1/EnvelopeFactory.h" #include "factories/naudio/v1/LoopFactory.h" #include "factories/naudio/v1/BookFactory.h" #include "factories/naudio/v1/SequenceFactory.h" #endif #include "preprocess/CompTool.h" using namespace std::chrono; namespace fs = std::filesystem; static const std::string regular = "[%Y-%m-%d %H:%M:%S.%e] [%l] %v"; static const std::string line = "[%Y-%m-%d %H:%M:%S.%e] [%l] > %v"; static std::string ConvertType(std::string type) { int index = type.find(':'); if (index != std::string::npos) { type = type.substr(index + 1); } std::transform(type.begin(), type.end(), type.begin(), tolower); return type; } static std::string GetTypeNode(YAML::Node& node) { auto type = GetSafeNode(node, "type"); std::transform(type.begin(), type.end(), type.begin(), toupper); return type; } void Companion::Init(const ExportType type) { std::atomic assetCount{ 0 }; Init(type, assetCount, true); } void Companion::Init(const ExportType type, std::atomic& assetCount, bool shouldProcess) { this->mShouldProcess = shouldProcess; spdlog::set_level(spdlog::level::debug); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v"); this->gConfig.exporterType = type; this->RegisterFactory("BLOB", std::make_shared()); this->RegisterFactory("TEXTURE", std::make_shared()); this->RegisterFactory("VTX", std::make_shared()); this->RegisterFactory("MTX", std::make_shared()); this->RegisterFactory("F32", std::make_shared()); this->RegisterFactory("INC", std::make_shared()); this->RegisterFactory("LIGHTS", std::make_shared()); this->RegisterFactory("GFX", std::make_shared()); this->RegisterFactory("VEC3F", std::make_shared()); this->RegisterFactory("VEC3S", std::make_shared()); this->RegisterFactory("ARRAY", std::make_shared()); this->RegisterFactory("ASSET_ARRAY", std::make_shared()); this->RegisterFactory("VP", std::make_shared()); this->RegisterFactory("COMPRESSED_TEXTURE", std::make_shared()); #ifdef SM64_SUPPORT this->RegisterFactory("SM64:DIALOG", std::make_shared()); this->RegisterFactory("SM64:TEXT", std::make_shared()); this->RegisterFactory("SM64:DICTIONARY", std::make_shared()); this->RegisterFactory("SM64:ANIM", std::make_shared()); this->RegisterFactory("SM64:BEHAVIOR_SCRIPT", std::make_shared()); this->RegisterFactory("SM64:COLLISION", std::make_shared()); this->RegisterFactory("SM64:GEO_LAYOUT", std::make_shared()); this->RegisterFactory("SM64:LEVEL_SCRIPT", std::make_shared()); this->RegisterFactory("SM64:MACRO", std::make_shared()); this->RegisterFactory("SM64:MOVTEX_QUAD", std::make_shared()); this->RegisterFactory("SM64:MOVTEX", std::make_shared()); this->RegisterFactory("SM64:PAINTING", std::make_shared()); this->RegisterFactory("SM64:PAINTING_MAP", std::make_shared()); this->RegisterFactory("SM64:TRAJECTORY", std::make_shared()); this->RegisterFactory("SM64:WATER_DROPLET", std::make_shared()); #endif #ifdef MK64_SUPPORT this->RegisterFactory("MK64:COURSE_VTX", std::make_shared()); this->RegisterFactory("MK64:TRACK_PATH", std::make_shared()); this->RegisterFactory("MK64:TRACK_SECTIONS", std::make_shared()); this->RegisterFactory("MK64:SPAWN_DATA", std::make_shared()); this->RegisterFactory("MK64:UNK_SPAWN_DATA", std::make_shared()); this->RegisterFactory("MK64:DRIVING_BEHAVIOUR", std::make_shared()); this->RegisterFactory("MK64:ITEM_CURVE", std::make_shared()); // Item curve for decomp only this->RegisterFactory("MK64:METADATA", std::make_shared()); this->RegisterFactory("MK64:PACKED_GFX", std::make_shared()); #endif #ifdef PM64_SUPPORT this->RegisterFactory("PM64:SPRITE", std::make_shared()); this->RegisterFactory("PM64:SHAPE", std::make_shared()); this->RegisterFactory("PM64:BACKGROUND", std::make_shared()); this->RegisterFactory("PM64:BGM", std::make_shared()); this->RegisterFactory("PM64:BK_SAMPLE", std::make_shared()); this->RegisterFactory("PM64:COLLISION", std::make_shared()); this->RegisterFactory("PM64:MAP_TEXTURE", std::make_shared()); this->RegisterFactory("PM64:AUDIO", std::make_shared()); this->RegisterFactory("PM64:STORY_IMAGE", std::make_shared()); this->RegisterFactory("PM64:IMGFX_ANIM", std::make_shared()); this->RegisterFactory("PM64:TITLE_DATA", std::make_shared()); #endif #ifdef SF64_SUPPORT this->RegisterFactory("SF64:ANIM", std::make_shared()); this->RegisterFactory("SF64:SKELETON", std::make_shared()); this->RegisterFactory("SF64:MESSAGE", std::make_shared()); this->RegisterFactory("SF64:MSG_TABLE", std::make_shared()); this->RegisterFactory("SF64:SCRIPT", std::make_shared()); this->RegisterFactory("SF64:HITBOX", std::make_shared()); this->RegisterFactory("SF64:ENVIRONMENT", std::make_shared()); this->RegisterFactory("SF64:OBJECT_INIT", std::make_shared()); this->RegisterFactory("SF64:COLPOLY", std::make_shared()); this->RegisterFactory("SF64:TRIANGLE", std::make_shared()); #endif #ifdef FZERO_SUPPORT this->RegisterFactory("FZX:ANIM", std::make_shared()); this->RegisterFactory("FZX:COURSE", std::make_shared()); this->RegisterFactory("FZX:GHOST", std::make_shared()); this->RegisterFactory("FZX:LIMB", std::make_shared()); this->RegisterFactory("FZX:SEQUENCE", std::make_shared()); this->RegisterFactory("FZX:SOUNDFONT", std::make_shared()); #endif #ifdef BK64_SUPPORT this->RegisterFactory("BK64:ANIM", std::make_shared()); this->RegisterFactory("BK64:ASSET_TABLE", std::make_shared()); this->RegisterFactory("BK64:DEMO", std::make_shared()); this->RegisterFactory("BK64:DIALOG", std::make_shared()); this->RegisterFactory("BK64:GEO_LAYOUT", std::make_shared()); this->RegisterFactory("BK64:GRUNTYQ", std::make_shared()); this->RegisterFactory("BK64:MAP", std::make_shared()); this->RegisterFactory("BK64:QUIZQ", std::make_shared()); this->RegisterFactory("BK64:MODEL", std::make_shared()); this->RegisterFactory("BK64:SOUNDFONT_CTL", std::make_shared()); this->RegisterFactory("BK64:SOUNDFONT_TBL", std::make_shared()); this->RegisterFactory("BK64:SPRITE", std::make_shared()); #endif #ifdef MARIO_ARTIST_SUPPORT this->RegisterFactory("MA:MA2D1", std::make_shared()); #endif #ifdef NAUDIO_SUPPORT this->RegisterFactory("NAUDIO:V0:AUDIO_HEADER", std::make_shared()); this->RegisterFactory("NAUDIO:V0:SEQUENCE", std::make_shared()); this->RegisterFactory("NAUDIO:V0:SAMPLE", std::make_shared()); this->RegisterFactory("NAUDIO:V0:BANK", std::make_shared()); this->RegisterFactory("NAUDIO:V1:AUDIO_SETUP", std::make_shared()); this->RegisterFactory("NAUDIO:V1:AUDIO_TABLE", std::make_shared()); this->RegisterFactory("NAUDIO:V1:SOUND_FONT", std::make_shared()); this->RegisterFactory("NAUDIO:V1:INSTRUMENT", std::make_shared()); this->RegisterFactory("NAUDIO:V1:DRUM", std::make_shared()); this->RegisterFactory("NAUDIO:V1:SAMPLE", std::make_shared()); this->RegisterFactory("NAUDIO:V1:ENVELOPE", std::make_shared()); this->RegisterFactory("NAUDIO:V1:ADPCM_LOOP", std::make_shared()); this->RegisterFactory("NAUDIO:V1:ADPCM_BOOK", std::make_shared()); this->RegisterFactory("NAUDIO:V1:SEQUENCE", std::make_shared()); #endif #ifdef BUILD_UI // Custom preview renderers; other types fall back to BaseFactoryUI. this->RegisterUIFactory("TEXTURE", std::make_shared()); this->RegisterUIFactory("VTX", std::make_shared()); this->RegisterUIFactory("GFX", std::make_shared()); this->RegisterUIFactory("LIGHTS", std::make_shared()); #ifdef SM64_SUPPORT this->RegisterUIFactory("SM64:GEO_LAYOUT", std::make_shared()); this->RegisterUIFactory("SM64:COLLISION", std::make_shared()); this->RegisterUIFactory("SM64:TRAJECTORY", std::make_shared()); this->RegisterUIFactory("SM64:ANIM", std::make_shared()); this->RegisterUIFactory("SM64:DIALOG", std::make_shared()); this->RegisterUIFactory("SM64:TEXT", std::make_shared()); this->RegisterUIFactory("SM64:DICTIONARY", std::make_shared()); this->RegisterUIFactory("SM64:BEHAVIOR_SCRIPT", std::make_shared()); this->RegisterUIFactory("SM64:LEVEL_SCRIPT", std::make_shared()); this->RegisterUIFactory("SM64:MACRO", std::make_shared()); this->RegisterUIFactory("SM64:MOVTEX", std::make_shared()); this->RegisterUIFactory("SM64:MOVTEX_QUAD", std::make_shared()); this->RegisterUIFactory("SM64:PAINTING", std::make_shared()); this->RegisterUIFactory("SM64:PAINTING_MAP", std::make_shared()); this->RegisterUIFactory("SM64:WATER_DROPLET", std::make_shared()); #endif #ifdef PM64_SUPPORT this->RegisterUIFactory("PM64:SHAPE", std::make_shared()); this->RegisterUIFactory("PM64:COLLISION", std::make_shared()); this->RegisterUIFactory("PM64:BK_SAMPLE", std::make_shared()); this->RegisterUIFactory("PM64:SPRITE", std::make_shared()); this->RegisterUIFactory("PM64:MAP_TEXTURE", std::make_shared()); this->RegisterUIFactory("PM64:IMGFX_ANIM", std::make_shared()); this->RegisterUIFactory("PM64:TITLE_DATA", std::make_shared()); this->RegisterUIFactory("PM64:STORY_IMAGE", std::make_shared()); UI::RegisterSequenceDriver("PM64:BGM", std::make_shared()); this->RegisterUIFactory("PM64:BGM", std::make_shared()); #endif #ifdef NAUDIO_SUPPORT this->RegisterUIFactory("NAUDIO:V0:SAMPLE", std::make_shared()); UI::RegisterSequenceDriver("NAUDIO:V0:SEQUENCE", std::make_shared()); this->RegisterUIFactory("NAUDIO:V0:SEQUENCE", std::make_shared()); UI::RegisterSequenceDriver("NAUDIO:V1:SEQUENCE", std::make_shared()); this->RegisterUIFactory("NAUDIO:V1:SEQUENCE", std::make_shared()); this->RegisterUIFactory("NAUDIO:V1:SAMPLE", std::make_shared()); #ifdef SF64_SUPPORT // SF64: 3D skeleton preview, decoded messages, code preview for the rest. this->RegisterUIFactory("SF64:SKELETON", std::make_shared()); this->RegisterUIFactory("SF64:MESSAGE", std::make_shared()); // 3D previews for collision meshes and hitbox volumes. this->RegisterUIFactory("SF64:HITBOX", std::make_shared()); this->RegisterUIFactory("SF64:COLPOLY", std::make_shared()); this->RegisterUIFactory("SF64:TRIANGLE", std::make_shared()); // Data/script types use the default code-preview card. this->RegisterUIFactory("SF64:ANIM", std::make_shared()); this->RegisterUIFactory("SF64:SCRIPT", std::make_shared()); this->RegisterUIFactory("SF64:ENVIRONMENT", std::make_shared()); this->RegisterUIFactory("SF64:OBJECT_INIT", std::make_shared()); this->RegisterUIFactory("SF64:MSG_TABLE", std::make_shared()); #endif #endif #endif #ifndef __EMSCRIPTEN__ // We call this manually this->Process(assetCount); #endif } void Companion::ParseEnums(std::string& header) { std::ifstream file(header); if (!file.is_open()) { throw std::runtime_error("Failed to open header files for enums node in config"); } std::regex enumRegex(R"(enum\s+(\w+)\s*(?:\s*:\s*(\w+))?[\s\n\r]*\{)"); std::string line; std::smatch match; std::string enumName; bool inEnum = false; int enumIndex; while (std::getline(file, line)) { if (!inEnum && std::regex_search(line, match, enumRegex) && match.size() > 1) { enumName = match.str(1); inEnum = true; enumIndex = -1; continue; } if (!inEnum) { continue; } if (line.find('}') != std::string::npos) { inEnum = false; continue; } // Remove any comments and non-alphanumeric characters line = std::regex_replace(line, std::regex(R"((/\*.*?\*/)|(//.*$)|([^a-zA-Z0-9=_\-\.]))"), ""); if (line.find('=') != std::string::npos) { auto value = line.substr(line.find('=') + 1); auto name = line.substr(0, line.find('=')); enumIndex = static_cast(std::stoll(value, nullptr, 0)); this->gEnums[enumName][enumIndex] = name; } else { enumIndex++; this->gEnums[enumName][enumIndex] = line; } } } std::optional Companion::ParseNode(YAML::Node& node, std::string& name) { auto type = GetTypeNode(node); spdlog::set_pattern(regular); if (node["offset"]) { auto offset = node["offset"].as(); SPDLOG_INFO("- [{}] Processing {} at 0x{:X}", type, name, offset); } else { SPDLOG_INFO("- [{}] Processing {}", type, name); } spdlog::set_pattern(line); node["vpath"] = name; auto factory = this->GetFactory(type); if (!factory.has_value()) { throw std::runtime_error("No factory by the name '" + type + "' found for '" + name + "'"); } auto impl = factory->get(); auto exporter = impl->GetExporter(this->gConfig.exporterType); if (!exporter.has_value() && !impl->HasModdedDependencies()) { SPDLOG_WARN("No exporter found for {}", name); return std::nullopt; } bool executeDef = true; std::optional> result; if (this->gConfig.modding && impl->SupportModdedAssets() && Torch::contains(this->gModdedAssetPaths, name)) { auto path = fs::path(this->gConfig.moddingPath) / this->gModdedAssetPaths[name]; if (!exists(path)) { SPDLOG_ERROR("Modded asset {} not found", this->gModdedAssetPaths[name]); } else { std::ifstream input(path, std::ios::binary); std::vector data = std::vector(std::istreambuf_iterator(input), {}); input.close(); result = impl->parse_modding(data, node); executeDef = !result.has_value(); } } try { if (executeDef && this->gConfig.parseMode == ParseMode::Default) { result = impl->parse(this->gRomData, node); } if (executeDef && this->gConfig.parseMode == ParseMode::Directory) { auto path = GetSafeNode(node, "path"); std::ifstream input(path, std::ios::binary); auto data = std::vector(std::istreambuf_iterator(input), {}); result = impl->parse(data, node); input.close(); } } catch (const std::exception& e) { SPDLOG_ERROR("Exception parsing {}: {}", name, e.what()); throw std::runtime_error("Error parsing " + name + ": " + e.what()); } if (!result.has_value()) { SPDLOG_ERROR("Failed to process {}", name); return std::nullopt; } SPDLOG_INFO("Processed {}", name); return ParseResultData{ name, type, node, result }; } void Companion::ParseModdingConfig() { auto path = fs::path(this->gConfig.moddingPath) / "modding.yml"; if (!fs::exists(path)) { throw std::runtime_error("No modding config found, please run in export mode first"); } auto modding = YAML::LoadFile(path.string()); for (auto assets = modding["assets"].begin(); assets != modding["assets"].end(); ++assets) { auto name = assets->first.as(); auto asset = assets->second.as(); this->gModdedAssetPaths[name] = asset; } } void Companion::ParseCurrentFileConfig(YAML::Node node, std::atomic& assetCount) { if (node["external_files"]) { auto externalFiles = node["external_files"]; if (externalFiles.IsSequence() && externalFiles.size()) { for (size_t i = 0; i < externalFiles.size(); i++) { auto externalFile = externalFiles[i]; if (externalFile.size() == 0) { this->gCurrentExternalFiles.push_back( (this->gSourceDirectory / externalFile.as()).string()); } else { SPDLOG_INFO("External File size {}", externalFile.size()); throw std::runtime_error( "Incorrect yaml syntax for external files.\n\nThe yaml expects:\n:config:\n external_files:\n " " - \n\ne.g.:\nexternal_files:\n - actors/actor1.yaml"); } std::string externalFileName = (this->gSourceDirectory / externalFile.as()).string(); if (StringHelper::StartsWith(std::filesystem::relative(externalFileName, this->gAssetPath).string(), "../")) { throw std::runtime_error("External File " + externalFileName + " Not In Asset Directory " + this->gAssetPath); } else if (std::filesystem::relative(externalFileName, this->gAssetPath).string() == "") { throw std::runtime_error("External File " + externalFileName + " Not In Asset Directory " + this->gAssetPath); } if (!Torch::contains(this->gAddrMap, externalFileName)) { SPDLOG_INFO("Dependency on external file {}. Now processing {}", externalFileName, externalFileName); auto currentFile = this->gCurrentFile; auto currentDirectory = this->gCurrentDirectory; auto currentExternalFiles = this->gCurrentExternalFiles; auto currentVirtualPath = this->gCurrentVirtualPath; this->gCurrentFile = externalFileName; this->gCurrentDirectory = std::filesystem::relative(externalFileName, this->gAssetPath).replace_extension(""); YAML::Node root = YAML::LoadFile(externalFileName); if (!Torch::contains(this->gProcessedFiles, this->gCurrentFile)) { ProcessFile(root, assetCount); if (mShouldProcess) { this->gProcessedFiles.insert(this->gCurrentFile); } } SPDLOG_INFO("Finishing processing of file: {}", currentFile); this->gCurrentFile = currentFile; this->gCurrentDirectory = currentDirectory; this->gCurrentExternalFiles = currentExternalFiles; this->gCurrentVirtualPath = currentVirtualPath; this->gFileHeader.clear(); } else { SPDLOG_INFO("Skipping external file {} as it has already been processed", externalFileName); } } } } if (node["manual_segments"]) { auto manualSegments = node["manual_segments"]; if (manualSegments.IsSequence() && manualSegments.size()) { for (size_t i = 0; i < manualSegments.size(); i++) { auto segment = manualSegments[i]; if (segment.IsSequence() && segment.size() == 2) { const auto id = segment[0].as(); const auto replacement = segment[1].as(); this->gManualSegments[id] = replacement; SPDLOG_DEBUG("Manual Segment {} replaced with {}", id, replacement); } else { throw std::runtime_error( "Incorrect yaml syntax for manual segments.\n\nThe yaml expects:\n:config:\n " "manual_segments:\n - [, ]\n\nLike so:\nmanual_segments:\n - [0x05000000, " "\"textures/other_textures/texture_6447C4\"]"); } } } } if (node["segments"]) { auto segments = node["segments"]; // 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(); gCurrentCompressionType = Decompressor::GetCompressionType(this->gRomData, gCurrentFileOffset); 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]"); } } // Set file offset for later use. for (size_t i = 0; i < segments.size(); i++) { auto segment = segments[i]; if (segment.IsSequence() && segment.size() == 2) { const auto id = segment[0].as(); const auto replacement = 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]"); } } } if (node["virtual"]) { auto virtualAddrMap = node["virtual"]; gVirtualAddrMap[gCurrentFile] = std::make_tuple(virtualAddrMap[0].as(), virtualAddrMap[1].as()); } if (node["header"]) { auto header = node["header"]; switch (this->gConfig.exporterType) { case ExportType::Header: { if (header["header"].IsSequence()) { for (auto line = header["header"].begin(); line != header["header"].end(); ++line) { this->gFileHeader += line->as() + "\n"; } } break; } case ExportType::Code: { if (header["code"].IsSequence()) { for (auto line = header["code"].begin(); line != header["code"].end(); ++line) { this->gFileHeader += line->as() + "\n"; } } break; } default: break; } } if (node["tables"]) { for (auto table = node["tables"].begin(); table != node["tables"].end(); ++table) { auto name = table->first.as(); auto range = table->second["range"].as>(); auto start = gCurrentSegmentNumber ? gCurrentSegmentNumber << 24 | range[0] : range[0]; auto end = gCurrentSegmentNumber ? gCurrentSegmentNumber << 24 | range[1] : range[1]; auto mode = GetSafeNode(table->second, "mode", "APPEND"); TableMode tMode = mode == "REFERENCE" ? TableMode::Reference : TableMode::Append; auto index_size = GetSafeNode(table->second, "index_size", -1); this->gTables.push_back({ name, start, end, tMode, index_size }); } } if (node["vram"]) { auto vram = node["vram"]; const auto addr = GetSafeNode(vram, "addr"); const auto offset = GetSafeNode(vram, "offset"); this->gCurrentVram = { addr, offset }; } this->gEnablePadGen = GetSafeNode(node, "autopads", true); this->gNodeForceProcessing = GetSafeNode(node, "force", false); this->gIndividualIncludes = GetSafeNode(node, "individual_data_incs", false); this->gCurrentVirtualPath = GetSafeNode(node, "path", ""); } void Companion::ParseHash() { const auto out = this->gDestinationDirectory / "torch.hash.yml"; if (fs::exists(out)) { this->gHashNode = YAML::LoadFile(out.string()); } else { this->gHashNode = YAML::Node(); } } std::string ExportTypeToString(ExportType type) { switch (type) { case ExportType::Binary: return "Binary"; case ExportType::Header: return "Header"; case ExportType::Code: return "Code"; case ExportType::Modding: return "Modding"; case ExportType::XML: return "XML"; default: throw std::runtime_error("Invalid ExportType"); } } bool Companion::NodeHasChanges(const std::string& path) { if (this->gConfig.modding) { return true; } std::ifstream yaml(path); const std::vector data = std::vector(std::istreambuf_iterator(yaml), {}); this->gCurrentHash = CalculateHash(data); bool needsInit = true; auto srcRelativePath = RelativePathToSrcDir(path); if (this->gHashNode[srcRelativePath]) { auto entry = GetSafeNode(this->gHashNode, srcRelativePath); const auto hash = GetSafeNode(entry, "hash", "no-hash"); auto modes = GetSafeNode(entry, "extracted"); auto extracted = GetSafeNode(modes, ExportTypeToString(this->gConfig.exporterType)); if (hash == this->gCurrentHash) { needsInit = false; if (extracted) { SPDLOG_INFO("Skipping {} as it has not changed", srcRelativePath); return false; } } } if (needsInit) { this->gHashNode[srcRelativePath] = YAML::Node(); this->gHashNode[srcRelativePath]["hash"] = this->gCurrentHash; this->gHashNode[srcRelativePath]["extracted"] = YAML::Node(); for (size_t m = 0; m <= static_cast(ExportType::Modding); m++) { this->gHashNode[srcRelativePath]["extracted"][ExportTypeToString(static_cast(m))] = false; } } return true; } void Companion::LoadYAMLRecursively(const std::string& dirPath, std::vector& result, bool skipRoot) { for (const auto& entry : std::filesystem::directory_iterator(dirPath)) { if (entry.is_directory()) { // Skip the root directory if specified if (skipRoot && entry.path() == dirPath) { continue; } // Recursive call for subdirectories LoadYAMLRecursively(entry.path().generic_string(), result, false); } else if (entry.path().extension() == ".yaml" || entry.path().extension() == ".yml") { // Load YAML file and add it to the result vector result.push_back(YAML::LoadFile(entry.path().generic_string())); } } } /** * Config yaml requires tables: [assets/courses] * Activate the factory using a normal asset yaml with type, input_directory, and output_directory nodes. */ void Companion::ProcessTables(YAML::Node& rom) { auto dirs = rom["metadata"].as>(); for (const auto& dir : dirs) { std::vector configNodes; LoadYAMLRecursively(dir, configNodes, true); gCourseMetadata[dir] = configNodes; } // Write yaml data to console if (this->IsDebug()) { SPDLOG_INFO("------ Metadata ouptut ------"); for (auto& node : gCourseMetadata[dirs[0]]) { std::cout << node << std::endl; SPDLOG_INFO("------------"); } SPDLOG_INFO("------ Metadata end ------"); } } void Companion::ProcessFile(YAML::Node root) { std::atomic assetCount{ 0 }; ProcessFile(root, assetCount); } void Companion::ProcessParseFile(YAML::Node root, std::atomic& assetCount) { for (auto asset = root.begin(); asset != root.end(); ++asset) { auto entryName = asset->first.as(); auto assetNode = asset->second; if (entryName.find(":config") != std::string::npos) { continue; } if (this->gConfig.dialogPack) { bool isRoot = false; if (assetNode["type"]) { const auto factory = this->GetFactory(GetTypeNode(assetNode)); isRoot = factory.has_value() && factory->get()->IsDialogPackRoot(); } if (!isRoot) { continue; } } this->gCurrentAssetName = "Parsing: " + entryName; if (gCurrentFileOffset && assetNode["offset"]) { const auto offset = assetNode["offset"].as(); if (!IS_SEGMENTED(offset)) { assetNode["offset"] = (gCurrentSegmentNumber << 24) | offset; } } if (!gCurrentVirtualPath.empty()) { assetNode["path"] = gCurrentVirtualPath; } std::string output = (this->gCurrentDirectory / entryName).string(); std::replace(output.begin(), output.end(), '\\', '/'); this->gConfig.segment.temporal.clear(); auto result = this->ParseNode(assetNode, output); if (result.has_value()) { this->gParseResults[this->gCurrentFile].push_back(result.value()); } spdlog::set_pattern(regular); SPDLOG_INFO("------------------------------------------------"); spdlog::set_pattern(line); } } void Companion::ProcessExportFile() { for (auto& result : this->gParseResults[this->gCurrentFile]) { std::ostringstream stream; ExportResult endptr = std::nullopt; WriteEntry wEntry; this->gCurrentAssetName = "Exporting: " + result.name; if (this->gAssetCounter) { (*this->gAssetCounter)++; } auto data = result.data.value(); const auto impl = this->GetFactory(result.type)->get(); const auto exporter = impl->GetExporter(this->gConfig.exporterType); if (!exporter.has_value()) { continue; } if (mShouldProcess) { try { switch (this->gConfig.exporterType) { case ExportType::Binary: { // no_export: still parse it for the side effects — VTX sub-refs, // companion-file registration — just don't write the body itself. // Whatever companion files it spun up still need to go out. const bool noExport = (result.node["no_export"] && result.node["no_export"].as()) || (this->gConfig.dialogPack && impl->IsDialogPackRoot()); if (!noExport) { stream.str(""); stream.clear(); exporter->get()->Export(stream, data, result.name, result.node, &result.name); auto data = stream.str(); this->gCurrentWrapper->AddFile(result.name, std::vector(data.begin(), data.end())); } for (auto& entry : this->gCompanionFiles) { auto output = (this->gCurrentDirectory / entry.first).string(); std::replace(output.begin(), output.end(), '\\', '/'); this->gCurrentWrapper->AddFile(output, entry.second); } break; } case ExportType::XML: case ExportType::Modding: { stream.str(""); stream.clear(); std::string ogname = result.name; exporter->get()->Export(stream, data, result.name, result.node, &result.name); auto data = stream.str(); if (data.empty()) { break; } std::string dpath = Instance->GetOutputPath() + "/" + result.name; if (!exists(fs::path(dpath).parent_path())) { create_directories(fs::path(dpath).parent_path()); } this->gModdedAssetPaths[ogname] = result.name; std::ofstream file(dpath, std::ios::binary); file.write(data.c_str(), data.size()); file.close(); for (auto& entry : this->gCompanionFiles) { auto cpath = (Instance->GetOutputPath() / this->gCurrentDirectory / entry.first).string(); std::replace(cpath.begin(), cpath.end(), '\\', '/'); if (!exists(fs::path(cpath).parent_path())) { create_directories(fs::path(cpath).parent_path()); } std::ofstream cfile(cpath, std::ios::binary); cfile.write(entry.second.data(), entry.second.size()); cfile.close(); } break; } default: { endptr = exporter->get()->Export(stream, data, result.name, result.node, &result.name); break; } } } catch (const std::exception& e) { throw std::runtime_error("Error exporting " + result.name + ": " + e.what()); } } this->gCompanionFiles.clear(); if (result.node["offset"]) { auto alignment = GetSafeNode(result.node, "alignment", impl->GetAlignment()); if (!endptr.has_value()) { wEntry = { result.name, result.node["offset"].as(), alignment, stream.str(), GetNode(result.node, "comment"), std::nullopt }; } else { switch (endptr->index()) { case 0: wEntry = { result.name, result.node["offset"].as(), alignment, stream.str(), GetNode(result.node, "comment"), std::get(endptr.value()) }; break; case 1: { const auto oentry = std::get(endptr.value()); wEntry = { result.name, oentry.start, alignment, stream.str(), GetNode(result.node, "comment"), oentry.end }; break; } default: SPDLOG_ERROR("Invalid endptr index {}", endptr->index()); SPDLOG_ERROR("Type of endptr: {}", typeid(endptr).name()); throw std::runtime_error("We should never reach this point"); } } } this->gWriteMap[this->gCurrentFile][result.type].push_back(wEntry); } auto fsout = fs::path(this->gConfig.outputPath); if (mShouldProcess) { if (this->gConfig.exporterType == ExportType::Modding || this->gConfig.exporterType == ExportType::XML) { fsout /= "modding.yml"; YAML::Node modding; for (const auto& [key, value] : this->gModdedAssetPaths) { modding["assets"][key] = value; } std::ofstream file(fsout.string(), std::ios::binary); file << modding; file.close(); } else if (this->gConfig.exporterType != ExportType::Binary) { std::string filename = this->gCurrentDirectory.filename().string(); switch (this->gConfig.exporterType) { case ExportType::Header: { fsout /= this->gCurrentDirectory.parent_path() / (filename + ".h"); break; } case ExportType::Code: { fsout /= this->gCurrentDirectory / (filename + ".c"); break; } default: break; } std::ostringstream stream; std::vector entries; if (std::holds_alternative(this->gWriteOrder)) { auto sort = std::get(this->gWriteOrder); for (const auto& [type, raw] : this->gWriteMap[this->gCurrentFile]) { entries.insert(entries.end(), raw.begin(), raw.end()); } if (sort == "OFFSET") { std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return a.addr < b.addr; }); } else if (sort == "ROFFSET") { std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return a.addr > b.addr; }); } else if (sort != "LINEAR") { throw std::runtime_error("Invalid write order"); } } else { for (const auto& type : std::get>(this->gWriteOrder)) { entries = this->gWriteMap[this->gCurrentFile][type]; std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) { return a.addr > b.addr; }); } } for (size_t i = 0; i < entries.size(); i++) { const auto result = entries[i]; const auto hasSize = result.endptr.has_value(); if (result.comment.has_value()) { stream << "// " << result.comment.value() << "\n"; } if (hasSize && this->IsDebug()) { stream << "// 0x" << std::hex << std::uppercase << ASSET_PTR(result.addr) << "\n"; } stream << result.buffer; if (hasSize && this->IsDebug()) { stream << "// 0x" << std::hex << std::uppercase << ASSET_PTR(result.endptr.value()) << "\n\n"; } if (hasSize && i < entries.size() - 1 && this->gConfig.exporterType == ExportType::Code && !this->gIndividualIncludes) { int32_t startptr = ASSET_PTR(result.endptr.value()); int32_t end = ASSET_PTR(entries[i + 1].addr); uint32_t alignment = entries[i + 1].alignment; int32_t gap = end - startptr; if (gap < 0) { stream << "// WARNING: Overlap detected between 0x" << std::hex << startptr << " and 0x" << end << " with size 0x" << std::abs(gap) << "\n"; SPDLOG_WARN("Overlap detected between 0x{:X} and 0x{:X} with size 0x{:X} on file {}", startptr, end, gap, this->gCurrentFile); } else if (gap < 0x10 && gap >= alignment && end % alignment == 0 && this->gEnablePadGen) { SPDLOG_WARN("Gap detected between 0x{:X} and 0x{:X} with size 0x{:X} on file {}", startptr, end, gap, this->gCurrentFile); SPDLOG_WARN("Creating pad of 0x{:X} bytes", gap); const auto padfile = this->gCurrentDirectory.filename().string(); if (this->IsDebug()) { stream << "// 0x" << std::hex << std::uppercase << startptr << "\n"; } stream << "char pad_" << padfile << "_" << std::to_string(gCurrentPad++) << "[] = {\n" << tab_t; auto gapSize = gap & ~3; for (size_t j = 0; j < gapSize; j++) { stream << "0x00, "; } stream << "\n};\n"; if (this->IsDebug()) { stream << "// 0x" << std::hex << std::uppercase << end << "\n\n"; } else { stream << "\n"; } } else if (gap >= 0x10) { stream << "// WARNING: Gap detected between 0x" << std::hex << startptr << " and 0x" << end << " with size 0x" << gap << "\n"; } } if (this->gConfig.exporterType == ExportType::Code && this->gIndividualIncludes) { fs::path outinc = fs::path(this->gConfig.outputPath) / this->gCurrentDirectory.parent_path() / fs::relative(fs::path(result.name + ".inc.c"), this->gCurrentDirectory.parent_path()); if (!exists(outinc.parent_path())) { create_directories(outinc.parent_path()); } std::ofstream file(outinc, std::ios::binary); if (!this->gFileHeader.empty()) { file << this->gFileHeader << std::endl; } file << stream.str(); stream.str(""); stream.seekp(0); file.close(); } } this->gWriteMap.clear(); if (this->gConfig.exporterType != ExportType::Code || !this->gIndividualIncludes) { std::string buffer = stream.str(); if (buffer.empty()) { SPDLOG_WARN("No data to write for {}", this->gCurrentFile); return; } std::string output = fsout.string(); std::replace(output.begin(), output.end(), '\\', '/'); if (!exists(fs::path(output).parent_path())) { create_directories(fs::path(output).parent_path()); } std::ofstream file(output, std::ios::binary); SPDLOG_INFO("Writing {} to {}", this->gCurrentFile, output); if (this->gConfig.exporterType == ExportType::Header) { fs::path entryPath = this->gCurrentFile; std::string symbol = entryPath.stem().string(); std::transform(symbol.begin(), symbol.end(), symbol.begin(), toupper); if (this->IsOTRMode()) { file << "#pragma once\n\n"; } else { file << "#ifndef " << symbol << "_H" << std::endl; file << "#define " << symbol << "_H" << std::endl << std::endl; } if (!this->gFileHeader.empty()) { file << this->gFileHeader << std::endl; } file << buffer; if (!this->IsOTRMode()) { file << std::endl << "#endif" << std::endl; } } else { if (!this->gFileHeader.empty()) { file << this->gFileHeader << std::endl; } file << buffer; } file.close(); } } } if (this->gConfig.exporterType != ExportType::Binary) { this->gHashNode[RelativePathToSrcDir(this->gCurrentFile)]["extracted"] [ExportTypeToString(this->gConfig.exporterType)] = true; } } 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. gCurrentSegmentNumber = 0; gCurrentFileOffset = 0; gCurrentCompressionType = CompressionType::None; // Set compressed file offsets and compression type 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(); gCurrentCompressionType = Decompressor::GetCompressionType(this->gRomData, gCurrentFileOffset); 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]"); } } } for (auto asset = root.begin(); asset != root.end(); ++asset) { auto node = asset->second; auto entryName = asset->first.as(); auto output = (this->gCurrentDirectory / entryName).string(); std::replace(output.begin(), output.end(), '\\', '/'); if (node["type"]) { const auto type = GetTypeNode(node); #ifdef NAUDIO_SUPPORT if (type == "NAUDIO:V0:SAMPLE") { AudioManager::Instance->bind_sample(node, output); } #endif } if (!node["offset"]) { continue; } if (gCurrentSegmentNumber) { if (IS_SEGMENTED(node["offset"].as()) == false) { node["offset"] = (gCurrentSegmentNumber << 24) | node["offset"].as(); } } if (!gCurrentVirtualPath.empty()) { node["path"] = gCurrentVirtualPath; } this->gAddrMap[this->gCurrentFile][node["offset"].as()] = std::make_tuple(output, node); } // Stupid hack because the iteration broke the assets root = YAML::LoadFile(this->gCurrentFile); this->gConfig.segment.local.clear(); this->gConfig.segment.compressed.clear(); this->gFileHeader.clear(); this->gCurrentPad = 0; this->gCurrentVram = std::nullopt; this->gCurrentVirtualPath = ""; this->gCurrentSegmentNumber = 0; this->gCurrentCompressionType = CompressionType::None; this->gCurrentFileOffset = 0; this->gTables.clear(); this->gCurrentExternalFiles.clear(); this->gSubFileList.clear(); GFXDOverride::ClearVtx(); if (root[":config"]) { this->ParseCurrentFileConfig(root[":config"], assetCount); } if (!this->NodeHasChanges(this->gCurrentFile) && !this->gNodeForceProcessing) { return; } spdlog::set_pattern(regular); SPDLOG_INFO("------------------------------------------------"); spdlog::set_pattern(line); ProcessParseFile(root, assetCount); // Transition to export phase: reset counter and set total from actual parse results if (this->gPhaseCallback) { this->gPhaseCallback(2); } if (this->gAssetCounter) { *this->gAssetCounter = 0; } if (this->gAssetTotal) { size_t total = 0; for (const auto& [file, results] : this->gParseResults) { total += results.size(); } *this->gAssetTotal = total; } ProcessExportFile(); } void Companion::Process(std::atomic& assetCount) { this->gAssetCounter = &assetCount; // gAssetTotal is set by the caller (port-side) if progress reporting is needed auto configPath = this->gSourceDirectory / "config.yml"; if (!fs::exists(configPath)) { SPDLOG_ERROR("No config file found"); return; } auto start = duration_cast(system_clock::now().time_since_epoch()); YAML::Node config = YAML::LoadFile(configPath.string()); bool isDirectoryMode = config["mode"] && config["mode"].as() == "directory"; if (!isDirectoryMode) { if (this->gRomPath.has_value()) { std::ifstream input(this->gRomPath.value(), std::ios::binary); this->gRomData = std::vector(std::istreambuf_iterator(input), {}); input.close(); } this->gCartridge = std::make_shared(this->gRomData); this->gCartridge->Initialize(); this->gGameTitle = this->gCartridge->GetGameTitle(); if (!config[this->gCartridge->GetHash()]) { bool synthesized = false; #ifdef BK64_SUPPORT if (this->gRomPath.has_value()) { synthesized = BK64::TrySynthesizeRomConfig(config, this->gCartridge->GetHash(), this->gRomPath.value(), this->gRomData); } #endif if (!synthesized) { SPDLOG_ERROR("No config found for {}", this->gCartridge->GetHash()); return; } } this->gConfig.parseMode = ParseMode::Default; } else { this->gConfig.parseMode = ParseMode::Directory; } auto rom = !isDirectoryMode ? config[this->gCartridge->GetHash()] : config; if (rom["preprocess"]) { auto preprocess = rom["preprocess"]; for (auto job = preprocess.begin(); job != preprocess.end(); job++) { auto name = job->first.as(); auto item = job->second; auto method = GetSafeNode(item, "method"); if (method == "mio0-comptool") { auto type = GetTypeNode(item); auto target = GetSafeNode(item, "target"); auto restart = GetSafeNode(item, "restart"); if (type == "DECOMPRESS") { this->gRomData = CompTool::Decompress(this->gRomData); this->gCartridge = std::make_shared(this->gRomData); this->gCartridge->Initialize(); this->gGameTitle = this->gCartridge->GetGameTitle(); auto hash = this->gCartridge->GetHash(); SPDLOG_CRITICAL("ROM decompressed to {}", hash); if (hash != target) { throw std::runtime_error("Hash mismatch"); } if (restart) { rom = config[this->gCartridge->GetHash()]; } } else { throw std::runtime_error("Only decompression is supported"); } } else { throw std::runtime_error("Invalid preprocess method"); } } } auto cfg = rom["config"]; // Let factories rewrite the config before processing (e.g. dialog-pack output // routing). Each guards on its own keys, so this is a no-op for unrelated roms. for (auto& [type, factory] : this->gFactories) { factory->PreprocessConfig(cfg, this->gCartridge.get()); } if (!cfg) { SPDLOG_ERROR("No config found for {}", !isDirectoryMode ? this->gCartridge->GetHash() : GetSafeNode(config, "folder")); return; } if (rom["metadata"]) { ProcessTables(rom); } if (rom["segments"]) { auto segments = rom["segments"].as>(); for (int i = 0; i < segments.size(); i++) { this->gConfig.segment.global[i + 1] = segments[i]; } } this->gAssetPath = (this->gSourceDirectory / rom["path"].as()).string(); auto opath = cfg["output"]; auto gbi = cfg["gbi"]; auto gbi_floats = cfg["gbi_floats"]; auto modding_path = opath && opath["modding"] ? opath["modding"].as() : "modding"; if (!this->gDestinationDirectory.empty() && !fs::exists(this->gDestinationDirectory)) { create_directories(this->gDestinationDirectory); } auto output_path = this->gDestinationDirectory; this->gConfig.moddingPath = (this->gDestinationDirectory / modding_path).string(); // The output path is only consumed by the exporters. In parse-only mode // (mShouldProcess == false, e.g. the `ui` viewer) nothing is written, so skip // the setup — this also avoids requiring an archive type for Binary parsing. if (this->mShouldProcess) { switch (this->gConfig.exporterType) { case ExportType::Binary: { std::string extension = ""; switch (this->gConfig.otrMode) { case ArchiveType::OTR: extension = ".otr"; break; case ArchiveType::O2R: extension = ".o2r"; break; default: throw std::runtime_error("Invalid archive type for export type Binary"); } output_path /= opath && opath["binary"] ? opath["binary"].as() : ("generic" + extension); break; } case ExportType::Header: { output_path /= opath && opath["headers"] ? opath["headers"].as() : "headers"; break; } case ExportType::Code: { output_path /= opath && opath["code"] ? opath["code"].as() : "code"; break; } case ExportType::XML: case ExportType::Modding: { output_path /= modding_path; break; } } this->gConfig.outputPath = output_path.string(); } if (auto outParent = output_path.parent_path(); !outParent.empty() && !exists(outParent)) { create_directories(outParent); } if (gbi) { auto key = gbi.as(); if (key == "F3D") { this->gConfig.gbi.version = GBIVersion::f3d; } else if (key == "F3DEX") { this->gConfig.gbi.version = GBIVersion::f3dex; } else if (key == "F3DB") { this->gConfig.gbi.version = GBIVersion::f3db; } else if (key == "F3DEX2") { this->gConfig.gbi.version = GBIVersion::f3dex2; } else if (key == "F3DEX2_PM64") { this->gConfig.gbi.version = GBIVersion::f3dex2; this->gConfig.gbi.subversion = GBIMinorVersion::PM64; } else if (key == "F3DEXB") { this->gConfig.gbi.version = GBIVersion::f3dexb; } else if (key == "F3DEX_MK64") { this->gConfig.gbi.version = GBIVersion::f3dex; this->gConfig.gbi.subversion = GBIMinorVersion::Mk64; } else if (key == "F3DEX_BK64") { this->gConfig.gbi.version = GBIVersion::f3dex; this->gConfig.gbi.subversion = GBIMinorVersion::BK64; } else { SPDLOG_ERROR("Invalid GBI version"); return; } if (gbi_floats) { this->gConfig.gbi.useFloats = gbi_floats.as(); } } this->gConfig.defaultShading = GetSafeNode(cfg, "default_shading", ""); if (auto sort = cfg["sort"]) { if (sort.IsSequence()) { this->gWriteOrder = sort.as>(); } else { this->gWriteOrder = sort.as(); } } else { this->gWriteOrder = std::vector{ "LIGHTS", "TEXTURE", "VTX", "GFX" }; } if ((this->gConfig.exporterType == ExportType::Code || this->gConfig.exporterType == ExportType::Binary) && this->gConfig.modding) { this->ParseModdingConfig(); } if (std::holds_alternative>(this->gWriteOrder)) { for (auto& [key, _] : this->gFactories) { auto entries = std::get>(this->gWriteOrder); if (std::find(entries.begin(), entries.end(), key) != entries.end()) { continue; } entries.push_back(key); } } #ifdef STANDALONE if (cfg["enums"]) { auto enums = GetSafeNode>(cfg, "enums"); for (auto& file : enums) { file = (this->gSourceDirectory / file).string(); this->ParseEnums(file); } } #endif if (cfg["logging"]) { auto level = cfg["logging"].as(); if (level == "TRACE") { spdlog::set_level(spdlog::level::trace); } else if (level == "DEBUG") { spdlog::set_level(spdlog::level::debug); } else if (level == "INFO") { spdlog::set_level(spdlog::level::info); } else if (level == "WARN") { spdlog::set_level(spdlog::level::warn); } else if (level == "ERROR") { spdlog::set_level(spdlog::level::err); } else if (level == "CRITICAL") { spdlog::set_level(spdlog::level::critical); } else if (level == "OFF") { spdlog::set_level(spdlog::level::off); } else { throw std::runtime_error( "Invalid logging level, please use TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL or OFF"); } } this->gConfig.textureDefines = cfg["textures"] && (cfg["textures"].as() == "ADDITIONAL_DEFINES"); this->gConfig.includeAutogen = cfg["include_autogen"] && cfg["include_autogen"].as(); this->gConfig.dialogPack = cfg["dialog_pack"] && cfg["dialog_pack"].as(); this->ParseHash(); SPDLOG_CRITICAL("------------------------------------------------"); spdlog::set_pattern(line); SPDLOG_CRITICAL("Starting Torch..."); if (this->gConfig.parseMode == ParseMode::Default) { SPDLOG_CRITICAL("Game: {}", this->gCartridge->GetGameTitle()); SPDLOG_CRITICAL("CRC: {}", this->gCartridge->GetCRC()); SPDLOG_CRITICAL("Version: {}", this->gCartridge->GetVersion()); SPDLOG_CRITICAL("Country: [{}]", this->gCartridge->GetCountryCode()); SPDLOG_CRITICAL("Hash: {}", this->gCartridge->GetHash()); SPDLOG_CRITICAL("Assets: {}", this->gAssetPath); } else { SPDLOG_CRITICAL("Mode: Directory"); SPDLOG_CRITICAL("Directory: {}", rom["folder"].as()); } SPDLOG_CRITICAL("------------------------------------------------"); #ifdef NAUDIO_SUPPORT AudioManager::Instance = new AudioManager(); #endif BinaryWrapper* wrapper = nullptr; if (mShouldProcess) { if (this->gConfig.exporterType == ExportType::Binary) { switch (this->gConfig.otrMode) { case ArchiveType::OTR: wrapper = new SWrapper(this->gConfig.outputPath); break; case ArchiveType::O2R: wrapper = new ZWrapper(this->gConfig.outputPath); break; default: throw std::runtime_error("Invalid archive type for export type Binary"); } } if (wrapper) { wrapper->CreateArchive(); } this->gCurrentWrapper = wrapper; for (auto& entry : this->gCompanionFiles) { auto output = entry.first; std::replace(output.begin(), output.end(), '\\', '/'); this->gCurrentWrapper->AddFile(output, entry.second); } } auto vWriter = LUS::BinaryWriter(); vWriter.SetEndianness(Torch::Endianness::Big); vWriter.Write(static_cast(Torch::Endianness::Big)); if (this->gConfig.parseMode == ParseMode::Default) { vWriter.Write(this->gCartridge->GetCRC()); } else { vWriter.Write((uint32_t)0); } for (const auto& entry : Torch::getRecursiveEntries(this->gAssetPath)) { if (entry.is_directory()) { continue; } const auto yamlPath = entry.path().generic_string(); if (yamlPath.find(".yaml") == std::string::npos && yamlPath.find(".yml") == std::string::npos) { continue; } if (yamlPath.find("config.yml") != std::string::npos) { continue; } if (yamlPath.find("hashes.yaml") != std::string::npos) { continue; } YAML::Node root = YAML::LoadFile(yamlPath); this->gCurrentDirectory = relative(entry.path(), this->gAssetPath).replace_extension(""); this->gCurrentFile = yamlPath; if (!Torch::contains(this->gProcessedFiles, this->gCurrentFile)) { ProcessFile(root, assetCount); if (mShouldProcess) { this->gProcessedFiles.insert(this->gCurrentFile); } // Sub-files were already parsed when they got created, so all that's left is export. auto parentDir = this->gCurrentDirectory; if (this->gConfig.exporterType == ExportType::Modding || this->gConfig.exporterType == ExportType::XML) { // Modding export is parallel. std::mutex moddedPathsMutex; std::mutex dirCreateMutex; // Torch is already running on a worker thread, so leave a core for the parent. const unsigned int hwThreads = std::thread::hardware_concurrency(); const size_t numThreads = hwThreads > 1 ? hwThreads - 1 : 1u; const size_t totalFiles = this->gSubFileList.size(); SPDLOG_CRITICAL("Exporting {} sub-files using {} threads", totalFiles, numThreads); auto exportRange = [&](size_t start, size_t end) { for (size_t si = start; si < end; si++) { const auto subFile = this->gSubFileList[si]; auto it = this->gParseResults.find(subFile); if (it == this->gParseResults.end() || it->second.empty()) { continue; } auto localDir = parentDir / subFile; for (auto& result : it->second) { const auto factory = this->GetFactory(result.type); if (!factory.has_value()) continue; const auto impl = factory->get(); const auto exporter = impl->GetExporter(this->gConfig.exporterType); if (!exporter.has_value()) continue; try { std::ostringstream stream; std::string ogname = result.name; exporter->get()->Export(stream, result.data.value(), result.name, result.node, &result.name); auto data = stream.str(); if (data.empty()) continue; std::string dpath = this->GetOutputPath() + "/" + result.name; { std::lock_guard lock(dirCreateMutex); if (!exists(fs::path(dpath).parent_path())) { create_directories(fs::path(dpath).parent_path()); } } std::ofstream file(dpath, std::ios::binary); file.write(data.c_str(), data.size()); file.close(); { std::lock_guard lock(moddedPathsMutex); this->gModdedAssetPaths[ogname] = result.name; } } catch (const std::exception& e) { SPDLOG_ERROR("Sub-file export failed [{}] {}: {}", si, subFile, e.what()); } catch (...) { SPDLOG_ERROR("Sub-file export crashed [{}] {}", si, subFile); } } if (this->gAssetCounter) { (*this->gAssetCounter)++; } } }; std::vector threads; size_t chunkSize = (totalFiles + numThreads - 1) / numThreads; for (size_t t = 0; t < numThreads; t++) { size_t start = t * chunkSize; size_t end = std::min(start + chunkSize, totalFiles); if (start < end) { threads.emplace_back(exportRange, start, end); } } for (auto& t : threads) { t.join(); } SPDLOG_CRITICAL("Parallel export complete: {} modded assets", this->gModdedAssetPaths.size()); // gModdedAssetPaths is only filled once the threads finish, so write modding.yml here. if (mShouldProcess) { auto moddingPath = fs::path(this->gConfig.outputPath) / "modding.yml"; YAML::Node modding; for (const auto& [key, value] : this->gModdedAssetPaths) { modding["assets"][key] = value; } std::ofstream moddingFile(moddingPath.string(), std::ios::binary); moddingFile << modding; moddingFile.close(); } } else { // Binary/code/header all share wrapper state, so these have to go one at a time. for (size_t si = 0; si < this->gSubFileList.size(); si++) { const auto subFile = this->gSubFileList[si]; this->gCurrentDirectory = parentDir / subFile; this->gCurrentFile = subFile; if (!this->gProcessedFiles.contains(subFile)) { try { ProcessExportFile(); } catch (const std::exception& e) { SPDLOG_ERROR("Sub-file export failed [{}] {}: {}", si, subFile, e.what()); } catch (...) { SPDLOG_ERROR("Sub-file export crashed [{}] {}", si, subFile); } if (mShouldProcess) { this->gProcessedFiles.insert(subFile); } } } } } } this->gCurrentAssetName.clear(); if (wrapper != nullptr && mShouldProcess) { // Add additional files specified by the user for (const auto& filePath : this->gAdditionalFiles) { std::ifstream input(this->gSourceDirectory / filePath, std::ios::binary); if (input.is_open()) { std::vector data((std::istreambuf_iterator(input)), std::istreambuf_iterator()); input.close(); std::string filename = fs::path(filePath).filename().string(); wrapper->AddFile(filename, data); SPDLOG_INFO("Added additional file: {}", filename); } else { SPDLOG_WARN("Could not open additional file: {}", filePath); } } if (!this->gVersion.empty()) { auto data = ParseVersionString(this->gVersion); wrapper->AddFile("portVersion", data); } SPDLOG_CRITICAL("Writing version file"); wrapper->AddFile("version", vWriter.ToVector()); vWriter.Close(); // Extra top-level files a factory asked us to drop at the archive root. for (const auto& [name, fileData] : this->gArchiveFiles) { wrapper->AddFile(name, fileData); } this->gCurrentAssetName = "Writing archive to disk..."; wrapper->Close(); } // Write entries hash std::ofstream file(this->gDestinationDirectory / "torch.hash.yml", std::ios::binary); file << this->gHashNode; file.close(); auto end = duration_cast(system_clock::now().time_since_epoch()); auto level = spdlog::get_level(); spdlog::set_level(spdlog::level::info); SPDLOG_CRITICAL("Done! Took {}ms", end.count() - start.count()); SPDLOG_CRITICAL("------------------------------------------------"); spdlog::set_level(level); spdlog::set_pattern(regular); Decompressor::ClearCache(); this->gArchiveFiles.clear(); this->gCartridge = nullptr; } void Companion::Pack(const std::string& folder, const std::string& output, const ArchiveType otrMode, const std::string& version) { spdlog::set_level(spdlog::level::debug); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v"); SPDLOG_CRITICAL("------------------------------------------------"); SPDLOG_CRITICAL("Starting Torch..."); SPDLOG_CRITICAL("Scanning {}", folder); auto start = duration_cast(system_clock::now().time_since_epoch()); std::unordered_map> files; for (const auto& entry : Torch::getRecursiveEntries(folder)) { if (entry.is_directory()) { continue; } std::ifstream input(entry.path(), std::ios::binary); auto data = std::vector(std::istreambuf_iterator(input), {}); input.close(); files[entry.path().generic_string()] = data; } std::unique_ptr wrapper; switch (otrMode) { case ArchiveType::OTR: wrapper.reset(new SWrapper(output)); break; case ArchiveType::O2R: wrapper.reset(new ZWrapper(output)); break; default: throw std::runtime_error("Invalid archive type for export type Binary"); } wrapper->CreateArchive(); for (auto& [path, data] : files) { std::string normalized = path; std::replace(normalized.begin(), normalized.end(), '\\', '/'); // Remove parent folder normalized = normalized.substr(folder.length() + 1); wrapper->AddFile(normalized, data); SPDLOG_CRITICAL("> Added {}", normalized); } if (!version.empty()) { SPDLOG_CRITICAL("Adding version file"); auto data = ParseVersionString(version); wrapper->AddFile("portVersion", data); } auto end = duration_cast(system_clock::now().time_since_epoch()); SPDLOG_CRITICAL("Done! Took {}ms", end.count() - start.count()); SPDLOG_CRITICAL("Exported to {}", output); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%l] %v"); SPDLOG_CRITICAL("------------------------------------------------"); wrapper->Close(); } std::optional> Companion::RegisterAsset(const std::string& name, YAML::Node& node) { if (!node["offset"]) { return std::nullopt; } auto output = (this->gCurrentDirectory / name).string(); std::replace(output.begin(), output.end(), '\\', '/'); auto entry = std::make_tuple(output, node); this->gAddrMap[this->gCurrentFile][node["offset"].as()] = entry; // Append to the GetNodesByType cache for this (file, type) if it's already // built. Avoids a full rebuild on the hot DListFactory path where many new // VTX nodes are registered while parsing a single display list. { const auto type = GetTypeNode(node); auto vIt = this->gNodesByTypeCacheValid.find(this->gCurrentFile); if (vIt != this->gNodesByTypeCacheValid.end()) { auto bIt = vIt->second.find(type); if (bIt != vIt->second.end() && bIt->second != 0) { auto& cacheBuckets = this->gNodesByTypeCache[this->gCurrentFile][type]; // Index 1 always includes the new entry; index 0 includes it only if not autogen. if (bIt->second & 0x2) { cacheBuckets[1].push_back(entry); } if ((bIt->second & 0x1) && !node["autogen"]) { cacheBuckets[0].push_back(entry); } } } } auto dResult = this->ParseNode(node, output); if (dResult.has_value()) { this->gParseResults[this->gCurrentFile].push_back(dResult.value()); } spdlog::set_pattern(regular); SPDLOG_INFO("------------------------------------------------"); spdlog::set_pattern(line); return entry; } void Companion::RegisterFactory(const std::string& type, const std::shared_ptr& factory) { this->gFactories[type] = factory; SPDLOG_INFO("Registered factory for {}", type); } std::optional> Companion::GetFactory(const std::string& type) { if (!Torch::contains(this->gFactories, type)) { return std::nullopt; } return this->gFactories[type]; } #ifdef BUILD_UI void Companion::RegisterUIFactory(const std::string& type, const std::shared_ptr& factory) { this->gUIFactories[type] = factory; SPDLOG_INFO("Registered UI factory for {}", type); } std::optional> Companion::GetUIFactory(const std::string& type) { if (!Torch::contains(this->gUIFactories, type)) { return std::nullopt; } return this->gUIFactories[type]; } #endif std::optional Companion::SearchTable(uint32_t addr) { for (auto& table : this->gTables) { if (addr >= table.start && addr <= table.end) { return table; } } return std::nullopt; } std::optional Companion::GetEnumFromValue(const std::string& key, int32_t id) { if (!Torch::contains(this->gEnums, key)) { return std::nullopt; } if (!Torch::contains(this->gEnums[key], id)) { return std::nullopt; } return this->gEnums[key][id]; } std::optional Companion::GetFileOffsetFromSegmentedAddr(const uint8_t segment) const { auto segments = this->gConfig.segment; if (Torch::contains(segments.temporal, segment)) { return segments.temporal[segment]; } if (Torch::contains(segments.local, segment)) { return segments.local[segment]; } if (Torch::contains(segments.global, segment)) { return segments.global[segment]; } return std::nullopt; } std::optional> Companion::GetFileOffsetFromCompressedSegmentedAddr(const uint8_t segment) const { auto segments = this->gConfig.segment; if (segments.compressed[this->gCurrentFile].contains(segment)) { return segments.compressed[this->gCurrentFile][segment]; } return std::nullopt; } uint32_t Companion::PatchVirtualAddr(uint32_t addr) { if (addr & 0x80000000) { if (Torch::contains(gVirtualAddrMap, gCurrentFile)) { addr -= std::get<0>(gVirtualAddrMap[gCurrentFile]); addr += std::get<1>(gVirtualAddrMap[gCurrentFile]); } } return addr; } std::optional> Companion::GetNodeByAddr(uint32_t addr) { if (!Torch::contains(this->gAddrMap, this->gCurrentFile)) { return std::nullopt; } // HACK: Adjust address to rom address if virtual address addr = PatchVirtualAddr(addr); if (!Torch::contains(this->gAddrMap[this->gCurrentFile], addr)) { auto realAddr = addr; if (IS_SEGMENTED(addr) && GetCompressedSegmentOffset(&realAddr)) { if (this->gAddrMap[this->gCurrentFile].contains(realAddr)) { return this->gAddrMap[this->gCurrentFile][realAddr]; } } for (auto& file : this->gCurrentExternalFiles) { if (!Torch::contains(this->gAddrMap, file)) { SPDLOG_WARN("GetNodeByAddr: External File {} Not Found.", file); continue; } if (!Torch::contains(this->gAddrMap[file], addr)) { continue; } return this->gAddrMap[file][addr]; } return std::nullopt; } return this->gAddrMap[this->gCurrentFile][addr]; } std::optional> Companion::GetNodeByAddr(uint32_t addr, const std::string& file) { if (Torch::contains(this->gAddrMap, file) && Torch::contains(this->gAddrMap[file], addr)) { return this->gAddrMap[file][addr]; } // The external_files context used during parsing is unavailable here, so // approximate it: sibling ymls first, then any file. Segmented addresses // can repeat across groups, so proximity ordering matters. const auto slash = file.find_last_of('/'); const std::string dir = slash != std::string::npos ? file.substr(0, slash + 1) : ""; if (!dir.empty()) { for (auto& [f, map] : this->gAddrMap) { if (f != file && f.rfind(dir, 0) == 0 && Torch::contains(map, addr)) { return map[addr]; } } } for (auto& [f, map] : this->gAddrMap) { if (f != file && Torch::contains(map, addr)) { return map[addr]; } } return std::nullopt; } std::optional Companion::GetStringByAddr(const uint32_t addr) { if (Torch::contains(this->gManualSegments, addr)) { return this->gManualSegments[addr]; } auto node = this->GetNodeByAddr(addr); if (!node.has_value()) { return std::nullopt; } return std::get<0>(node.value()); } std::optional> Companion::GetSafeNodeByAddr(const uint32_t addr, std::string type) { auto node = this->GetNodeByAddr(addr); if (!node.has_value()) { return std::nullopt; } auto [name, n] = node.value(); auto n_type = GetTypeNode(n); if (n_type != type) { throw std::runtime_error("Requested node type does not match with the target node type at " + Torch::to_hex(addr, false) + " Found: " + n_type + " Expected: " + type); } return node; } std::optional Companion::GetSafeStringByAddr(const uint32_t addr, std::string type) { if (Torch::contains(this->gManualSegments, addr)) { return this->gManualSegments[addr]; } auto node = this->GetNodeByAddr(addr); if (!node.has_value()) { return std::nullopt; } auto [name, n] = node.value(); auto n_type = GetTypeNode(n); if (n_type != type) { throw std::runtime_error("Requested node type does not match with the target node type at " + Torch::to_hex(addr, false) + " Found: " + n_type + " Expected: " + type); } return std::get<0>(node.value()); } std::string Companion::GetSymbolFromAddr(uint32_t address, bool validZero) { auto dec = Companion::Instance->GetNodeByAddr(address); std::ostringstream outSymbol; if (address == 0 && !validZero) { outSymbol << "NULL"; } else if (dec.has_value()) { auto node = std::get<1>(dec.value()); auto symbol = GetSafeNode(node, "symbol"); outSymbol << "&" << symbol; } else { outSymbol << "0x" << std::uppercase << std::hex << address; } return outSymbol.str(); } std::optional Companion::GetParseDataByAddr(uint32_t addr) { if (CONTAINS(this->gParseResults, this->gCurrentFile)) { for (auto& result : this->gParseResults[this->gCurrentFile]) { if (result.data.has_value() && result.GetOffset() == addr) { return result; } } } for (auto& file : this->gCurrentExternalFiles) { if (!CONTAINS(this->gParseResults, this->gCurrentFile)) { SPDLOG_INFO("GetParseDataByAddr: External File {} Not Found.", file); continue; } for (auto& result : this->gParseResults[file]) { if (result.data.has_value() && result.GetOffset() == addr) { return result; } } } return std::nullopt; } std::optional Companion::GetParseDataBySymbol(const std::string& symbol) { auto searchBucket = [&](const std::string& file) -> std::optional { auto it = this->gParseResults.find(file); if (it == this->gParseResults.end()) { return std::nullopt; } for (auto& result : it->second) { auto sym = GetNode(result.node, "symbol"); if (result.data.has_value() && sym.has_value() && sym.value() == symbol) { return result; } } return std::nullopt; }; // Fast path: the current file and its declared externals. if (auto r = searchBucket(this->gCurrentFile)) { return r; } for (auto& file : this->gCurrentExternalFiles) { if (auto r = searchBucket(file)) { return r; } } // Global fallback to prevent racy lookups. for (auto& [file, results] : this->gParseResults) { for (auto& result : results) { auto sym = GetNode(result.node, "symbol"); if (result.data.has_value() && sym.has_value() && sym.value() == symbol) { return result; } } } return std::nullopt; } const std::vector>* Companion::GetNodesByTypeRef(const std::string& type, bool includeAutogen) { if (!Torch::contains(this->gAddrMap, this->gCurrentFile)) { return nullptr; } // Cache hit path so we don't rescan every node in gAddrMap on every call const size_t agIdx = includeAutogen ? 1 : 0; const uint8_t agBit = static_cast(1u << agIdx); auto& validBits = this->gNodesByTypeCacheValid[this->gCurrentFile][type]; if (!(validBits & agBit)) { auto& cached = this->gNodesByTypeCache[this->gCurrentFile][type][agIdx]; cached.clear(); for (auto& [addr, tpl] : this->gAddrMap[this->gCurrentFile]) { const auto& [name, node] = tpl; const auto n_type = GetTypeNode(const_cast(node)); if (!includeAutogen && node["autogen"]) { continue; } if (n_type == type) { cached.push_back(tpl); } } validBits |= agBit; } return &this->gNodesByTypeCache[this->gCurrentFile][type][agIdx]; } std::optional>> Companion::GetNodesByType(const std::string& type, bool includeAutogen) { auto* ref = GetNodesByTypeRef(type, includeAutogen); if (ref == nullptr) { return std::vector>{}; } return *ref; } void Companion::SetProcess(bool shouldProcess) { this->mShouldProcess = shouldProcess; } void Companion::RegisterCompanionFile(const std::string path, std::vector data) { this->gCompanionFiles[path] = data; SPDLOG_TRACE("Registered companion file {}", path); } void Companion::RegisterArchiveFile(const std::string& name, std::vector data) { this->gArchiveFiles.emplace_back(name, std::move(data)); SPDLOG_TRACE("Registered archive root file {}", name); } std::string Companion::NormalizeAsset(const std::string& name) const { auto path = fs::path(this->gCurrentFile).stem().string() + "_" + name; return path; } static std::string& ConvertWinToUnixSlash(std::string& path) { std::replace(path.begin(), path.end(), '\\', '/'); return path; } std::string Companion::RelativePath(const std::string& path) const { std::string doutput = (this->gCurrentDirectory / path).string(); ConvertWinToUnixSlash(doutput); return doutput; } std::string Companion::RelativePathToDestDir(const std::string& path) const { std::string doutput = fs::path(path).lexically_relative(this->gDestinationDirectory).string(); ConvertWinToUnixSlash(doutput); return doutput; } std::string Companion::RelativePathToSrcDir(const std::string& path) const { std::string doutput = fs::path(path).lexically_relative(this->gSourceDirectory).string(); ConvertWinToUnixSlash(doutput); return doutput; } std::string Companion::CalculateHash(const std::vector& data) { sha1::SHA1 s; s.processBytes(data.data(), data.size()); uint32_t hash[5]; s.getDigest(hash); char buf[41]; std::snprintf(buf, sizeof(buf), "%08x%08x%08x%08x%08x", hash[0], hash[1], hash[2], hash[3], hash[4]); return std::string(buf); } std::vector Companion::ParseVersionString(const std::string& version) { uint16_t major = 0; uint16_t minor = 0; uint16_t patch = 0; std::sscanf(version.c_str(), "%hu.%hu.%hu", &major, &minor, &patch); auto wv = LUS::BinaryWriter(); wv.SetEndianness(Torch::Endianness::Big); wv.Write(major); wv.Write(minor); wv.Write(patch); wv.Close(); return wv.ToVector(); } std::optional Companion::AddSubFileAsset(YAML::Node asset, std::string newFileName, CompressionType newCompressionType, uint32_t compressedSize) { if (!asset["offset"] || !asset["type"]) { return std::nullopt; } if (this->gParseResults.contains(newFileName) || this->gProcessedFiles.contains(newFileName)) { SPDLOG_WARN("File with name {} already exists, skipping..", newFileName); return std::nullopt; } auto addr = GetSafeNode(asset, "offset"); asset["offset"] = 0; auto oldFile = this->gCurrentFile; auto oldVram = this->gCurrentVram; auto oldCompressionType = this->gCurrentCompressionType; auto oldCompressedSize = this->gCurrentCompressedSize; this->gSubFileList.push_back(newFileName); this->gCurrentAssetName = "Parsing: " + newFileName; if (this->gAssetCounter) { (*this->gAssetCounter)++; } this->gCurrentFile = newFileName; this->gCurrentVram = { 0, addr }; this->gCurrentCompressionType = newCompressionType; if (newCompressionType == CompressionType::BKZIP) { this->gCurrentCompressedSize = compressedSize; } auto result = this->AddAsset(asset); // ...and put the parent file's state back the way we found it. this->gCurrentFile = oldFile; this->gCurrentVram = oldVram; this->gCurrentCompressionType = oldCompressionType; this->gCurrentCompressedSize = oldCompressedSize; return result; } std::optional Companion::AddAsset(YAML::Node asset) { if (!asset["offset"] || !asset["type"]) { return std::nullopt; } asset["offset"] = PatchVirtualAddr(GetSafeNode(asset, "offset")); const auto type = GetTypeNode(asset); const auto offset = GetSafeNode(asset, "offset"); const auto symbol = GetSafeNode(asset, "symbol", ""); const auto decl = this->GetNodeByAddr(offset); if (decl.has_value()) { auto found = std::get<1>(decl.value()); if (GetTypeNode(found) != type) { SPDLOG_ERROR("Asset clash detected {} vs {} at 0x{:X}", type, GetTypeNode(found), offset); } else { return found; } } auto rom = this->GetRomData(); auto factory = this->GetFactory(type); if (!factory.has_value()) { return std::nullopt; } std::string output; std::string typeId = ConvertType(type); if (!symbol.empty()) { output = symbol; } else if (Decompressor::IsSegmented(offset)) { output = this->NormalizeAsset("seg" + std::to_string(SEGMENT_NUMBER(offset)) + "_" + typeId + "_" + Torch::to_hex(SEGMENT_OFFSET(offset), false)); } else { output = this->NormalizeAsset(typeId + "_" + Torch::to_hex(offset, false)); } std::replace(output.begin(), output.end(), ':', '_'); asset["autogen"] = true; asset["symbol"] = output; if (!gCurrentVirtualPath.empty()) { asset["path"] = gCurrentVirtualPath; } auto result = this->RegisterAsset(output, asset); if (result.has_value()) { asset["vpath"] = std::get<0>(result.value()); return std::get<1>(result.value()); } return std::nullopt; } void Companion::SetCompressedSegment(uint32_t segmentId, uint32_t compressedFileOffset, uint32_t offset) { this->gConfig.segment.compressed[this->gCurrentFile][segmentId] = std::make_pair(compressedFileOffset, offset); } bool Companion::GetCompressedSegmentOffset(uint32_t* addr) { if (IS_SEGMENTED(*addr)) { const auto compressedSegmentPair = Companion::Instance->GetFileOffsetFromCompressedSegmentedAddr(SEGMENT_NUMBER(*addr)); if (compressedSegmentPair.has_value()) { *addr = compressedSegmentPair.value().second + SEGMENT_OFFSET(*addr); return true; } } return false; }