mirror of
https://github.com/HarbourMasters/Shipwright
synced 2026-08-12 20:22:37 -04:00
Move the asset tools under soh/assets
soh-torch and soh-o2r-packer both exist to turn assets into archives, so they sit better beside the assets they read than at the repo root. Nothing tangled: both targets are declared in the root CMakeLists with explicit paths rather than add_subdirectory, and soh/CMakeLists.txt globs only include/, soh/ and src/, so sources under assets/ aren't swept into the soh target.
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
// Encoding a PNG into an N64 texture is the one part of building soh.o2r that nothing else
|
||||
// provides. Torch decodes N64 texture data out of a rom and libultraship decodes it again for
|
||||
// rendering, but neither goes the other way. Everything else the packer needs -- the resource
|
||||
// header, the texture type enum, size arithmetic, directory walking, zipping, portVersion --
|
||||
// comes from torch, so this file is the whole of what had to be written.
|
||||
//
|
||||
// The quantisation follows ZAPD's ZTexture exactly, since these bytes have to match the archives
|
||||
// ZAPD produced. n64graphics is not a substitute: it scales (x * 15 / 255) where ZAPD shifts
|
||||
// (x >> 4), which differ for most inputs.
|
||||
|
||||
#include "PngTexture.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
#include "binarytools/BinaryWriter.h"
|
||||
#include "factories/BaseFactory.h"
|
||||
#include "factories/ResourceType.h"
|
||||
#include "n64graphics/stb_image.h"
|
||||
#include "utils/TextureUtils.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
TextureType TypeFromString(const std::string& format) {
|
||||
if (format == "rgba32") return TextureType::RGBA32bpp;
|
||||
if (format == "rgb5a1") return TextureType::RGBA16bpp;
|
||||
if (format == "ci4") return TextureType::Palette4bpp;
|
||||
if (format == "ci8") return TextureType::Palette8bpp;
|
||||
if (format == "i4") return TextureType::Grayscale4bpp;
|
||||
if (format == "i8") return TextureType::Grayscale8bpp;
|
||||
if (format == "ia4") return TextureType::GrayscaleAlpha4bpp;
|
||||
if (format == "ia8") return TextureType::GrayscaleAlpha8bpp;
|
||||
if (format == "ia16") return TextureType::GrayscaleAlpha16bpp;
|
||||
return TextureType::Error;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> Encode(const uint8_t* rgba, int w, int h, TextureType type) {
|
||||
auto px = [&](int y, int x, int c) -> uint8_t { return rgba[(((size_t)y * w) + x) * 4 + c]; };
|
||||
std::vector<uint8_t> out(TextureUtils::CalculateTextureSize(type, w, h));
|
||||
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
const size_t i = (size_t)y * w + x;
|
||||
|
||||
switch (type) {
|
||||
case TextureType::RGBA32bpp:
|
||||
out[i * 4 + 0] = px(y, x, 0);
|
||||
out[i * 4 + 1] = px(y, x, 1);
|
||||
out[i * 4 + 2] = px(y, x, 2);
|
||||
out[i * 4 + 3] = px(y, x, 3);
|
||||
break;
|
||||
|
||||
case TextureType::RGBA16bpp: {
|
||||
const uint16_t data = ((px(y, x, 0) >> 3) << 11) | ((px(y, x, 1) >> 3) << 6) |
|
||||
((px(y, x, 2) >> 3) << 1) | (px(y, x, 3) != 0);
|
||||
out[i * 2 + 0] = (data & 0xFF00) >> 8;
|
||||
out[i * 2 + 1] = (data & 0x00FF);
|
||||
break;
|
||||
}
|
||||
|
||||
case TextureType::Grayscale4bpp:
|
||||
if (x % 2 == 0) {
|
||||
out[i / 2] = (uint8_t)(((px(y, x, 0) / 16) << 4) + (px(y, x + 1, 0) / 16));
|
||||
}
|
||||
break;
|
||||
|
||||
case TextureType::Grayscale8bpp:
|
||||
out[i] = px(y, x, 0);
|
||||
break;
|
||||
|
||||
case TextureType::GrayscaleAlpha4bpp:
|
||||
if (x % 2 == 0) {
|
||||
const uint8_t hi = ((px(y, x, 0) >> 5) << 1) | (px(y, x, 3) != 0);
|
||||
const uint8_t lo = ((px(y, x + 1, 0) >> 5) << 1) | (px(y, x + 1, 3) != 0);
|
||||
out[i / 2] = (uint8_t)((hi << 4) | lo);
|
||||
}
|
||||
break;
|
||||
|
||||
case TextureType::GrayscaleAlpha8bpp:
|
||||
out[i] = (uint8_t)((((px(y, x, 0) >> 4) & 0xF) << 4) | ((px(y, x, 3) >> 4) & 0xF));
|
||||
break;
|
||||
|
||||
case TextureType::GrayscaleAlpha16bpp:
|
||||
out[i * 2 + 0] = px(y, x, 0);
|
||||
out[i * 2 + 1] = px(y, x, 3);
|
||||
break;
|
||||
|
||||
// Palettes need a TLUT this packer has no way to build.
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace PngTexture {
|
||||
|
||||
bool IsFormat(const std::string& format) {
|
||||
return TypeFromString(format) != TextureType::Error;
|
||||
}
|
||||
|
||||
bool Convert(const fs::path& png, const fs::path& dest, const std::string& format) {
|
||||
const TextureType type = TypeFromString(format);
|
||||
|
||||
int w = 0, h = 0, channels = 0;
|
||||
uint8_t* pixels = stbi_load(png.string().c_str(), &w, &h, &channels, 4);
|
||||
if (pixels == nullptr) {
|
||||
fprintf(stderr, "%s: %s\n", png.string().c_str(), stbi_failure_reason());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<uint8_t> encoded = Encode(pixels, w, h, type);
|
||||
stbi_image_free(pixels);
|
||||
|
||||
if (encoded.empty()) {
|
||||
fprintf(stderr, "%s: cannot encode %s\n", png.string().c_str(), format.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LUS::BinaryWriter writer;
|
||||
BaseExporter::WriteHeader(writer, Torch::ResourceType::Texture, 0);
|
||||
writer.Write((uint32_t)type);
|
||||
writer.Write((uint32_t)w);
|
||||
writer.Write((uint32_t)h);
|
||||
writer.Write((uint32_t)encoded.size());
|
||||
writer.Write((char*)encoded.data(), encoded.size());
|
||||
|
||||
const std::vector<char> payload = writer.ToVector();
|
||||
writer.Close();
|
||||
|
||||
fs::create_directories(dest.parent_path());
|
||||
std::ofstream out(dest, std::ios::binary);
|
||||
out.write(payload.data(), payload.size());
|
||||
|
||||
return out.good();
|
||||
}
|
||||
|
||||
} // namespace PngTexture
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef PNGTEXTURE_H
|
||||
#define PNGTEXTURE_H
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace PngTexture {
|
||||
|
||||
// Whether format names an N64 texture format, as in <name>.<format>.png
|
||||
bool IsFormat(const std::string& format);
|
||||
|
||||
// Decodes png and writes it to dest as a libultraship texture resource.
|
||||
bool Convert(const std::filesystem::path& png, const std::filesystem::path& dest, const std::string& format);
|
||||
|
||||
} // namespace PngTexture
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
// Packs soh/assets/custom into soh.o2r -- the port's own assets, as opposed to anything
|
||||
// extracted from a rom.
|
||||
//
|
||||
// usage: soh-o2r-packer <custom assets dir> <out.o2r> <M.m.p>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "Companion.h"
|
||||
#include "PngTexture.h"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 4) {
|
||||
fprintf(stderr, "usage: %s <custom assets dir> <out.o2r> <M.m.p>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const fs::path assetsDir = argv[1];
|
||||
const fs::path outPath = argv[2];
|
||||
const std::string version = argv[3];
|
||||
|
||||
if (!fs::is_directory(assetsDir)) {
|
||||
fprintf(stderr, "not a directory: %s\n", assetsDir.string().c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Companion::Pack archives a directory as-is, so stage the assets in the shape the archive
|
||||
// should have and let torch do the rest.
|
||||
const fs::path stage = outPath.string() + ".stage";
|
||||
std::error_code ec;
|
||||
fs::remove_all(stage, ec);
|
||||
fs::create_directories(stage);
|
||||
|
||||
for (const auto& entry : fs::recursive_directory_iterator(assetsDir)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fs::path& path = entry.path();
|
||||
const std::string rel = fs::relative(path, assetsDir).generic_string();
|
||||
const std::string filename = path.filename().string();
|
||||
|
||||
// <name>.<format>.png becomes a texture resource archived as <name>
|
||||
if (std::count(filename.begin(), filename.end(), '.') >= 2 && path.extension() == ".png") {
|
||||
const std::string stem = path.stem().string();
|
||||
const std::string format = stem.substr(stem.find_last_of('.') + 1);
|
||||
|
||||
if (PngTexture::IsFormat(format)) {
|
||||
const std::string arc = rel.substr(0, rel.size() - (format.size() + 5));
|
||||
if (!PngTexture::Convert(path, stage / arc, format)) {
|
||||
return 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Only json is carried over from accessibility
|
||||
if (rel.find("accessibility") != std::string::npos && path.extension() != ".json") {
|
||||
continue;
|
||||
}
|
||||
|
||||
fs::create_directories((stage / rel).parent_path());
|
||||
fs::copy_file(path, stage / rel, fs::copy_options::overwrite_existing);
|
||||
}
|
||||
|
||||
fs::remove(outPath, ec);
|
||||
Companion::Pack(stage.string(), outPath.string(), ArchiveType::O2R, version);
|
||||
fs::remove_all(stage, ec);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -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