mirror of
https://github.com/HarbourMasters/Shipwright
synced 2026-08-12 12:18:32 -04:00
Add soh-torch, rebuild ExtractAssets on it
soh links torch as a static library, which compiles out torch's own CLI, so build-time extraction needs an entry point. soh-torch supplies one around the same SohTorch::Extract the game calls -- one implementation, so the CLI and the in-game path can't drift. ExtractAssets keeps its old contract: SOH_ROM_PATH accepts roms and/or directories of roms and defaults to roms/, so dropping a vanilla and a master quest rom in produces oot.o2r and oot-mq.o2r in one run, and chaining GenerateSohOtr keeps soh.o2r coming out of it too. Extract now returns the archive name torch chose rather than deriving it from IsMasterQuest(), so config.yml is the only thing naming archives. The name comes from Companion::GetOutputPath(): scanning the destination for *.o2r picks up an unrelated soh.o2r when extracting into the build directory, which is only invisible in the game because it extracts into an empty temp dir. ExtractAssetHeaders stays as a target that explains why it can't run -- torch registers no OoT header exporter and emits no #define d<sym>, which soh source references ~24k times. Documented in BUILDING.md alongside it. This removes the last ZAPD dependency from the build.
This commit is contained in:
+32
-20
@@ -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 "$<TARGET_FILE:ZAPD>" --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="$<TARGET_FILE_DIR:ZAPD>" -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 $<TARGET_FILE:soh-torch>
|
||||
--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<sym>` 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 "$<TARGET_FILE:ZAPD>" --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)
|
||||
|
||||
+14
-2
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<size_t>* progress) {
|
||||
std::string Extract(const std::string& romPath, const std::string& srcDir, const std::string& destDir,
|
||||
const std::string& portVersion, std::atomic<size_t>* progress) {
|
||||
std::string archiveName;
|
||||
|
||||
try {
|
||||
// Companion::Instance is a raw global with no getter; factories dereference it.
|
||||
auto companion = std::make_unique<Companion>(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
|
||||
|
||||
@@ -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<size_t>* 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<size_t>* progress);
|
||||
|
||||
} // namespace SohTorch
|
||||
|
||||
|
||||
@@ -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 <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "TorchExtract.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static void Usage(const char* argv0) {
|
||||
fprintf(stderr, "usage: %s --src <asset yml dir> --dest <output dir> --version <M.m.p> <rom|dir> [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<std::string> CollectRoms(const std::vector<std::string>& args) {
|
||||
std::vector<std::string> roms;
|
||||
|
||||
for (const auto& arg : args) {
|
||||
std::error_code ec;
|
||||
if (fs::is_directory(arg, ec)) {
|
||||
std::vector<std::string> 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<std::string> 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<std::string> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user