diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a18428eab..f33c97b336 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,32 +242,42 @@ install(DIRECTORY "${CMAKE_SOURCE_DIR}/soh/assets/extractor/" DESTINATION ./asse install(DIRECTORY "${CMAKE_SOURCE_DIR}/soh/assets/xml/" DESTINATION ./assets/xml COMPONENT ship) endif() -find_package(Python3 COMPONENTS Interpreter) +# Build-time ROM extraction. soh links torch as a static library, which compiles out torch's +# own CLI, so this supplies an entry point around the same code the game runs. +add_executable(soh-torch + ${CMAKE_CURRENT_SOURCE_DIR}/tools/torch-cli/main.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/soh/soh/Extractor/TorchExtract.cpp +) +target_include_directories(soh-torch PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/soh/soh/Extractor) +target_link_libraries(soh-torch PRIVATE torch) -# Target to generate OTRs +# Target to generate OTRs. SOH_ROM_PATH takes roms and/or directories of roms; torch names each +# archive (oot.o2r or oot-mq.o2r) from its hash, so a vanilla and a master quest rom produce both +# in one run. soh.o2r comes from GenerateSohOtr, chained below. +set(SOH_ROM_PATH "${CMAKE_SOURCE_DIR}/roms" CACHE STRING "Roms, or directories of roms, to extract") add_custom_target( ExtractAssets - COMMAND ${CMAKE_COMMAND} -E rm -f oot.o2r oot-mq.o2r soh.o2r - - # copy LUS default shaders into assets/custom - COMMAND ${CMAKE_COMMAND} -E rm -r -f ${CMAKE_CURRENT_SOURCE_DIR}/soh/assets/custom/shaders/ - COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/libultraship/src/fast/shaders/ ${CMAKE_CURRENT_SOURCE_DIR}/soh/assets/custom/shaders/ - - COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/OTRExporter/extract_assets.py -z "$" --non-interactive --xml-root assets/xml --custom-otr-file soh.o2r "--custom-assets-path" ${CMAKE_CURRENT_SOURCE_DIR}/soh/assets/custom --port-ver "${CMAKE_PROJECT_VERSION}" - COMMAND ${CMAKE_COMMAND} -DSYSTEM_NAME=${CMAKE_SYSTEM_NAME} -DTARGET_DIR="$" -DSOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR} -DBINARY_DIR=${CMAKE_BINARY_DIR} -P ${CMAKE_CURRENT_SOURCE_DIR}/copy-existing-otrs.cmake - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/soh + COMMAND ${CMAKE_COMMAND} -E rm -f ${CMAKE_BINARY_DIR}/soh/oot.o2r ${CMAKE_BINARY_DIR}/soh/oot-mq.o2r + COMMAND $ + --src ${CMAKE_CURRENT_SOURCE_DIR}/soh/assets/yml + --dest ${CMAKE_BINARY_DIR}/soh + --version ${CMAKE_PROJECT_VERSION} + ${SOH_ROM_PATH} + # torch caches extraction state next to the archive; drop it so the game's directory stays clean + COMMAND ${CMAKE_COMMAND} -E rm -f ${CMAKE_BINARY_DIR}/soh/torch.hash.yml COMMENT "Running asset extraction..." - DEPENDS ZAPD - BYPRODUCTS oot.o2r ${CMAKE_SOURCE_DIR}/oot.o2r oot-mq.o2r ${CMAKE_SOURCE_DIR}/oot-mq.o2r ${CMAKE_SOURCE_DIR}/soh.o2r + DEPENDS soh-torch + BYPRODUCTS ${CMAKE_BINARY_DIR}/soh/oot.o2r ${CMAKE_BINARY_DIR}/soh/oot-mq.o2r ) -# Target to generate headers +# Target to generate headers. Torch has no OoT header exporter yet -- every REGISTER in +# src/factories/oot/ is REGISTER(Binary, ...) -- and it emits no `#define d` line, which +# soh source references ~24k times. Kept so the failure explains itself. add_custom_target( ExtractAssetHeaders - COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/OTRExporter/extract_assets.py -z "$" --non-interactive --xml-root assets/xml --gen-headers - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/soh - COMMENT "Generating asset headers..." - DEPENDS ZAPD + COMMAND ${CMAKE_COMMAND} -E echo "ExtractAssetHeaders is currently unavailable: torch does not emit OoT asset headers yet." + COMMAND ${CMAKE_COMMAND} -E echo "The checked-in headers under soh/assets are unaffected. See docs/BUILDING.md." + COMMAND ${CMAKE_COMMAND} -E false ) # TEMPORARY (Torch migration): soh.o2r is copied from prebuilt/, not generated. @@ -313,11 +323,13 @@ add_custom_target( GenerateSohOtr COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SOH_PREBUILT_O2R}" ${CMAKE_CURRENT_SOURCE_DIR}/soh.o2r COMMAND ${CMAKE_COMMAND} -E copy_if_different "${SOH_PREBUILT_O2R}" ${CMAKE_BINARY_DIR}/soh/soh.o2r - # No BYPRODUCTS: ExtractAssets already claims ${CMAKE_SOURCE_DIR}/soh.o2r as one of its - # own (see above), and two rules generating the same path is a hard ninja error. COMMENT "Copying prebuilt soh.o2r (TEMPORARY -- see prebuilt/README.md)..." + BYPRODUCTS ${CMAKE_SOURCE_DIR}/soh.o2r ${CMAKE_BINARY_DIR}/soh/soh.o2r ) +# ExtractAssets produced soh.o2r as well as the rom archives, so keep doing that. +add_dependencies(ExtractAssets GenerateSohOtr) + if(CMAKE_SYSTEM_NAME MATCHES "Linux") find_package(ImageMagick COMPONENTS convert) if (ImageMagick_FOUND) diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 4a6ba5faec..0c6de6fc8b 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -80,6 +80,10 @@ C:\Program Files\CMake\bin\cmake.exe --build build-cmake --target clean ``` #### Regenerate Asset Headers +> **Currently unavailable.** The Torch migration removed the ZAPD-based header +> generator, and Torch does not emit OoT asset headers yet. The checked-in headers +> under `soh/assets` are unaffected; only regenerating them is. + ```powershell # If you need to regenerate the asset headers to check them into source C:\Program Files\CMake\bin\cmake.exe --build build-cmake --target ExtractAssetHeaders @@ -221,6 +225,10 @@ cpack -G External (creates appimage) cmake --build build-cmake --target clean ``` #### Regenerate Asset Headers +> **Currently unavailable.** The Torch migration removed the ZAPD-based header +> generator, and Torch does not emit OoT asset headers yet. The checked-in headers +> under `soh/assets` are unaffected; only regenerating them is. + ```bash # If you need to regenerate the asset headers to check them into source cmake --build build-cmake --target ExtractAssetHeaders @@ -278,6 +286,10 @@ cmake --build build-cmake --target clean ``` #### Regenerate Asset Headers +> **Currently unavailable.** The Torch migration removed the ZAPD-based header +> generator, and Torch does not emit OoT asset headers yet. The checked-in headers +> under `soh/assets` are unaffected; only regenerating them is. + ```bash # If you need to regenerate the asset headers to check them into source cmake --build build-cmake --target ExtractAssetHeaders @@ -287,7 +299,7 @@ cmake --build build-cmake --target ExtractAssetHeaders 1. Requires that your build machine is setup with the tools necessary for your platform above 2. Requires that you have the switch build tools installed 3. Clone the Ship of Harkinian repository -4. Place one or more [compatible](#compatible-roms) roms in the `OTRExporter` directory with namings of your choice +4. Place one or more [compatible](#compatible-roms) roms in the `roms` directory with namings of your choice ```bash cd Shipwright @@ -308,7 +320,7 @@ cmake --build build-switch --target soh_nro 1. Requires that your build machine is setup with the tools necessary for your platform above 2. Requires that you have the Wii U build tools installed 3. Clone the Ship of Harkinian repository -4. Place one or more [compatible](#compatible-roms) roms in the `OTRExporter` directory with namings of your choice +4. Place one or more [compatible](#compatible-roms) roms in the `roms` directory with namings of your choice ```bash cd Shipwright diff --git a/soh/soh/Extractor/Extract.cpp b/soh/soh/Extractor/Extract.cpp index c0903a1837..e934d92a28 100644 --- a/soh/soh/Extractor/Extract.cpp +++ b/soh/soh/Extractor/Extract.cpp @@ -646,7 +646,6 @@ bool Extractor::CallTorch(std::string installPath, std::string exportdir, std::a char portVersion[18]; // 5 digits for int16_max (x3) + separators + terminator snprintf(portVersion, 18, "%d.%d.%d", gBuildVersionMajor, gBuildVersionMinor, gBuildVersionPatch); - const char* archiveName = IsMasterQuest() ? "oot-mq.o2r" : "oot.o2r"; std::string romPath = std::filesystem::absolute(mCurrentRomPath).string(); std::string srcDir = std::filesystem::absolute(installPath).string() + "/assets"; exportdir = std::filesystem::absolute(exportdir).string(); @@ -656,7 +655,9 @@ bool Extractor::CallTorch(std::string installPath, std::string exportdir, std::a *totalExtract = SohTorch::CountAssetFiles(srcDir + "/" + GetTorchVersionDir()); *extractCount = 0; - bool success = SohTorch::Extract(romPath, srcDir, tempdir, portVersion, archiveName, extractCount); + // config.yml decides whether this is oot.o2r or oot-mq.o2r. + std::string archiveName = SohTorch::Extract(romPath, srcDir, tempdir, portVersion, extractCount); + bool success = !archiveName.empty(); std::error_code ec; if (success) { diff --git a/soh/soh/Extractor/TorchExtract.cpp b/soh/soh/Extractor/TorchExtract.cpp index 1640c3138b..b6649ccc8c 100644 --- a/soh/soh/Extractor/TorchExtract.cpp +++ b/soh/soh/Extractor/TorchExtract.cpp @@ -31,8 +31,10 @@ size_t CountAssetFiles(const std::string& ymlDir) { return count; } -bool Extract(const std::string& romPath, const std::string& srcDir, const std::string& destDir, - const std::string& portVersion, const std::string& archiveName, std::atomic* progress) { +std::string Extract(const std::string& romPath, const std::string& srcDir, const std::string& destDir, + const std::string& portVersion, std::atomic* progress) { + std::string archiveName; + try { // Companion::Instance is a raw global with no getter; factories dereference it. auto companion = std::make_unique(fs::path(romPath), ArchiveType::O2R, false, srcDir, destDir); @@ -47,23 +49,32 @@ bool Extract(const std::string& romPath, const std::string& srcDir, const std::s // Init is the whole run; it calls Process() internally. companion->Init(ExportType::Binary); + // config.yml names the archive per rom; ask rather than guess, and ask before the + // companion goes away. + archiveName = fs::path(companion->GetOutputPath()).filename().string(); + // Companion holds every parsed asset, so don't leak it into the game's lifetime. companion.reset(); Companion::Instance = nullptr; } catch (const std::exception& e) { SPDLOG_ERROR("Torch extraction failed: {}", e.what()); Companion::Instance = nullptr; - return false; + return ""; } catch (...) { SPDLOG_ERROR("Torch extraction failed with an unknown exception"); Companion::Instance = nullptr; - return false; + return ""; } // Process() returns void and several fatal paths only log and return, so confirm the - // archive exists rather than trusting the run. + // archive is really there rather than trusting the run. std::error_code ec; - return fs::exists(fs::path(destDir) / archiveName, ec); + if (archiveName.empty() || !fs::exists(fs::path(destDir) / archiveName, ec)) { + SPDLOG_ERROR("Torch produced no archive in {}", destDir); + return ""; + } + + return archiveName; } } // namespace SohTorch diff --git a/soh/soh/Extractor/TorchExtract.h b/soh/soh/Extractor/TorchExtract.h index f942dfa9fa..f2f3647ee4 100644 --- a/soh/soh/Extractor/TorchExtract.h +++ b/soh/soh/Extractor/TorchExtract.h @@ -12,12 +12,12 @@ namespace SohTorch { // so this is the progress denominator. size_t CountAssetFiles(const std::string& ymlDir); -// Extracts romPath into destDir/archiveName, where archiveName is what config.yml names the -// output for that ROM. Torch picks the version directory under srcDir by hashing the ROM. -// Increments progress once per asset file. False if it threw or produced no archive. -bool Extract(const std::string& romPath, const std::string& srcDir, const std::string& destDir, - const std::string& portVersion, const std::string& archiveName, - std::atomic* progress); +// Extracts romPath into destDir. Torch picks both the version directory under srcDir and the +// archive name (oot.o2r, oot-mq.o2r) from config.yml by hashing the ROM, so the name it chose +// is returned rather than assumed. Empty if extraction threw or produced no archive. +// Increments progress once per asset file. +std::string Extract(const std::string& romPath, const std::string& srcDir, const std::string& destDir, + const std::string& portVersion, std::atomic* progress); } // namespace SohTorch diff --git a/tools/torch-cli/main.cpp b/tools/torch-cli/main.cpp new file mode 100644 index 0000000000..bc2a3d1417 --- /dev/null +++ b/tools/torch-cli/main.cpp @@ -0,0 +1,110 @@ +// Build-time ROM extraction, for the ExtractAssets target and anything else that needs an +// archive without launching the game. +// +// soh links torch as a static library (USE_STANDALONE=OFF), which compiles out torch's own +// CLI, so this supplies the entry point. It calls the same SohTorch::Extract the game does. + +#include +#include +#include +#include +#include +#include + +#include "TorchExtract.h" + +namespace fs = std::filesystem; + +static void Usage(const char* argv0) { + fprintf(stderr, "usage: %s --src --dest --version [rom|dir...]\n", + argv0); +} + +static bool IsRom(const fs::path& path) { + const std::string ext = path.extension().string(); + return ext == ".z64" || ext == ".n64" || ext == ".v64"; +} + +// A directory argument extracts every rom directly inside it, which is how the target is +// normally driven: drop a vanilla and a master quest rom in, get oot.o2r and oot-mq.o2r. +static std::vector CollectRoms(const std::vector& args) { + std::vector roms; + + for (const auto& arg : args) { + std::error_code ec; + if (fs::is_directory(arg, ec)) { + std::vector found; + for (fs::directory_iterator it(arg, ec), end; it != end; it.increment(ec)) { + if (ec) { + break; + } + if (it->is_regular_file(ec) && IsRom(it->path())) { + found.push_back(it->path().string()); + } + } + std::sort(found.begin(), found.end()); + roms.insert(roms.end(), found.begin(), found.end()); + } else { + roms.push_back(arg); + } + } + + return roms; +} + +int main(int argc, char** argv) { + std::string src, dest, version; + std::vector romArgs; + + for (int i = 1; i < argc; i++) { + const std::string arg = argv[i]; + auto next = [&](const char* what) -> std::string { + if (i + 1 >= argc) { + fprintf(stderr, "missing argument after %s\n", what); + exit(1); + } + return argv[++i]; + }; + + if (arg == "--src") { + src = next("--src"); + } else if (arg == "--dest") { + dest = next("--dest"); + } else if (arg == "--version") { + version = next("--version"); + } else if (!arg.empty() && arg[0] == '-') { + fprintf(stderr, "unknown option: %s\n", arg.c_str()); + Usage(argv[0]); + return 1; + } else { + romArgs.push_back(arg); + } + } + + if (src.empty() || dest.empty() || version.empty() || romArgs.empty()) { + Usage(argv[0]); + return 1; + } + + const std::vector roms = CollectRoms(romArgs); + if (roms.empty()) { + fprintf(stderr, "no roms found in: "); + for (const auto& arg : romArgs) { + fprintf(stderr, "%s ", arg.c_str()); + } + fprintf(stderr, "\n"); + return 1; + } + + for (const auto& rom : roms) { + // A fresh extraction per ROM; torch names the archive from config.yml. + const std::string archive = SohTorch::Extract(rom, src, dest, version, nullptr); + if (archive.empty()) { + fprintf(stderr, "failed to extract %s\n", rom.c_str()); + return 1; + } + printf("%s -> %s/%s\n", rom.c_str(), dest.c_str(), archive.c_str()); + } + + return 0; +}