mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
485 lines
21 KiB
C++
485 lines
21 KiB
C++
// psxrecomp — self-contained developer front-end.
|
|
//
|
|
// Disc image + BIOS in; generated C project + build scripts out.
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifdef _WIN32
|
|
#include <process.h>
|
|
#else
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "fmt/format.h"
|
|
#include "bios_rom_alias.h"
|
|
#include "cli_boot_path.h"
|
|
#include "iso_reader.h"
|
|
#include "ps1_exe_parser.h"
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace {
|
|
|
|
struct Options {
|
|
fs::path disc;
|
|
fs::path bios;
|
|
fs::path output;
|
|
std::string name;
|
|
};
|
|
|
|
struct BiosProfile {
|
|
std::string filename;
|
|
std::string out_stem;
|
|
};
|
|
|
|
void usage(const char* program) {
|
|
fmt::print(
|
|
"Usage:\n"
|
|
" {} build --disc <game.cue|bin|iso|chd> --bios <PS1_BIOS.BIN> "
|
|
"--output <directory> [--name <title>]\n\n"
|
|
"The output contains generated game/BIOS C, game.toml, CMakeLists.txt,\n"
|
|
"and build scripts. No compiler toolchain is bundled.\n"
|
|
"Supported BIOS: OpenBIOS, SCPH1001, SCPH101, SCPH5552 (any 512 KiB PS1 BIOS dump).\n",
|
|
program);
|
|
}
|
|
|
|
std::string toml_string(std::string value) {
|
|
std::string out;
|
|
out.reserve(value.size());
|
|
for (char c : value) {
|
|
if (c == '\\') out += '/';
|
|
else if (c == '"') out += "\\\"";
|
|
else out += c;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string trim(std::string value) {
|
|
while (!value.empty() && std::isspace((unsigned char)value.back())) value.pop_back();
|
|
size_t first = 0;
|
|
while (first < value.size() && std::isspace((unsigned char)value[first])) first++;
|
|
return value.substr(first);
|
|
}
|
|
|
|
std::string parse_boot_path(const std::string& cnf) {
|
|
std::string lower = cnf;
|
|
std::transform(lower.begin(), lower.end(), lower.begin(),
|
|
[](unsigned char c) { return (char)std::tolower(c); });
|
|
size_t pos = lower.find("cdrom:");
|
|
if (pos == std::string::npos) pos = lower.find("cdrom0:");
|
|
if (pos == std::string::npos) return {};
|
|
pos = lower.find(':', pos) + 1;
|
|
while (pos < cnf.size() && (cnf[pos] == '\\' || cnf[pos] == '/')) pos++;
|
|
std::string path;
|
|
while (pos < cnf.size()) {
|
|
char c = cnf[pos++];
|
|
if (c == ';' || c == '\r' || c == '\n' || c == '\0' ||
|
|
std::isspace((unsigned char)c)) break;
|
|
path += c;
|
|
}
|
|
return path;
|
|
}
|
|
|
|
std::string safe_stem(std::string value) {
|
|
std::string out;
|
|
for (char c : value) {
|
|
if (std::isalnum((unsigned char)c)) out += c;
|
|
else if (!out.empty() && out.back() != '_') out += '_';
|
|
}
|
|
while (!out.empty() && out.back() == '_') out.pop_back();
|
|
return out.empty() ? "PSXGameRecomp" : out;
|
|
}
|
|
|
|
void write_file(const fs::path& path, const std::string& contents) {
|
|
fs::create_directories(path.parent_path());
|
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
if (!out) throw std::runtime_error("cannot write " + path.string());
|
|
out << contents;
|
|
if (!out) throw std::runtime_error("write failed for " + path.string());
|
|
}
|
|
|
|
void write_bytes(const fs::path& path, const std::vector<uint8_t>& contents) {
|
|
fs::create_directories(path.parent_path());
|
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
|
if (!out) throw std::runtime_error("cannot write " + path.string());
|
|
out.write((const char*)contents.data(), (std::streamsize)contents.size());
|
|
if (!out) throw std::runtime_error("write failed for " + path.string());
|
|
}
|
|
|
|
fs::path executable_dir(const char* argv0) {
|
|
std::error_code ec;
|
|
fs::path path = fs::absolute(argv0, ec);
|
|
if (ec) path = argv0;
|
|
path = fs::weakly_canonical(path, ec);
|
|
return (ec ? fs::path(argv0) : path).parent_path();
|
|
}
|
|
|
|
fs::path find_helper(const fs::path& exe_dir, const std::string& name) {
|
|
#ifdef _WIN32
|
|
const std::string filename = name + ".exe";
|
|
#else
|
|
const std::string filename = name;
|
|
#endif
|
|
for (const fs::path& candidate : {
|
|
exe_dir / "libexec" / filename,
|
|
exe_dir / filename,
|
|
}) {
|
|
if (fs::is_regular_file(candidate)) return fs::absolute(candidate);
|
|
}
|
|
throw std::runtime_error("missing packaged helper: " + filename);
|
|
}
|
|
|
|
fs::path find_framework(const fs::path& exe_dir) {
|
|
for (const fs::path& candidate : {
|
|
exe_dir / "framework",
|
|
exe_dir.parent_path().parent_path(),
|
|
}) {
|
|
if (fs::is_regular_file(candidate / "runtime" / "runtime.cmake"))
|
|
return fs::absolute(candidate);
|
|
}
|
|
throw std::runtime_error("packaged framework sources are missing");
|
|
}
|
|
|
|
const BiosProfile& select_bios_profile(const fs::path& bios_path) {
|
|
static const std::vector<std::pair<std::string, BiosProfile>> profiles = {
|
|
{"OPENBIOS", {"OpenBIOS.toml", "OpenBIOS"}},
|
|
{"SCPH1001", {"SCPH1001.toml", "SCPH1001"}},
|
|
{"SCPH101", {"SCPH101.toml", "SCPH101"}},
|
|
{"SCPH5552", {"SCPH5552.toml", "SCPH5552"}},
|
|
};
|
|
const std::string token = PSXRecompV4::bios_model_token(bios_path);
|
|
for (const auto& [candidate, profile] : profiles) {
|
|
if (candidate == token) return profile;
|
|
}
|
|
throw std::runtime_error(
|
|
"unsupported BIOS filename '" + bios_path.filename().string() +
|
|
"' (expected openbios.bin, SCPH1001.BIN, SCPH101.BIN, or SCPH5552.BIN)");
|
|
}
|
|
|
|
fs::path find_bios_profile(const fs::path& framework,
|
|
const BiosProfile& profile) {
|
|
for (const fs::path& candidate : {
|
|
framework / "bios" / profile.filename,
|
|
framework / profile.filename,
|
|
}) {
|
|
if (fs::is_regular_file(candidate)) return fs::absolute(candidate);
|
|
}
|
|
throw std::runtime_error(
|
|
"packaged BIOS profile is missing: " + profile.filename);
|
|
}
|
|
|
|
int run_process(const std::vector<std::string>& args) {
|
|
std::vector<char*> raw;
|
|
raw.reserve(args.size() + 1);
|
|
for (const std::string& arg : args) raw.push_back(const_cast<char*>(arg.c_str()));
|
|
raw.push_back(nullptr);
|
|
#ifdef _WIN32
|
|
intptr_t result = _spawnv(_P_WAIT, args[0].c_str(), raw.data());
|
|
return result < 0 ? -1 : (int)result;
|
|
#else
|
|
pid_t pid = fork();
|
|
if (pid < 0) return -1;
|
|
if (pid == 0) {
|
|
execv(args[0].c_str(), raw.data());
|
|
_exit(127);
|
|
}
|
|
int status = 0;
|
|
if (waitpid(pid, &status, 0) < 0) return -1;
|
|
return WIFEXITED(status) ? WEXITSTATUS(status) : 128;
|
|
#endif
|
|
}
|
|
|
|
class CurrentPathGuard {
|
|
public:
|
|
explicit CurrentPathGuard(const fs::path& path) : original_(fs::current_path()) {
|
|
fs::current_path(path);
|
|
}
|
|
~CurrentPathGuard() {
|
|
std::error_code ignored;
|
|
fs::current_path(original_, ignored);
|
|
}
|
|
|
|
CurrentPathGuard(const CurrentPathGuard&) = delete;
|
|
CurrentPathGuard& operator=(const CurrentPathGuard&) = delete;
|
|
|
|
private:
|
|
fs::path original_;
|
|
};
|
|
|
|
void copy_framework(const fs::path& source, const fs::path& destination) {
|
|
fs::create_directories(destination);
|
|
for (const auto& entry : fs::directory_iterator(source)) {
|
|
fs::copy(entry.path(), destination / entry.path().filename(),
|
|
fs::copy_options::recursive | fs::copy_options::overwrite_existing);
|
|
}
|
|
}
|
|
|
|
Options parse_options(int argc, char** argv) {
|
|
Options options;
|
|
for (int i = 2; i < argc; ++i) {
|
|
std::string arg = argv[i];
|
|
auto value = [&](const char* flag) -> std::string {
|
|
if (i + 1 >= argc) throw std::runtime_error(std::string(flag) + " needs a value");
|
|
return argv[++i];
|
|
};
|
|
if (arg == "--disc") options.disc = value("--disc");
|
|
else if (arg == "--bios") options.bios = value("--bios");
|
|
else if (arg == "--output" || arg == "-o") options.output = value("--output");
|
|
else if (arg == "--name") options.name = value("--name");
|
|
else if (arg == "-h" || arg == "--help") {
|
|
usage(argv[0]);
|
|
std::exit(0);
|
|
} else {
|
|
throw std::runtime_error("unknown argument: " + arg);
|
|
}
|
|
}
|
|
if (options.disc.empty() || options.bios.empty() || options.output.empty())
|
|
throw std::runtime_error("--disc, --bios, and --output are required");
|
|
options.disc = fs::absolute(options.disc);
|
|
options.bios = fs::absolute(options.bios);
|
|
options.output = fs::absolute(options.output);
|
|
return options;
|
|
}
|
|
|
|
int build_project(const Options& options, const fs::path& exe_dir) {
|
|
if (!fs::is_regular_file(options.disc))
|
|
throw std::runtime_error("disc image not found: " + options.disc.string());
|
|
if (!fs::is_regular_file(options.bios))
|
|
throw std::runtime_error("BIOS not found: " + options.bios.string());
|
|
if (fs::file_size(options.bios) != 512 * 1024)
|
|
throw std::runtime_error("BIOS must be an uncompressed 512 KiB PlayStation BIOS dump");
|
|
if (fs::exists(options.output)) {
|
|
if (!fs::is_directory(options.output))
|
|
throw std::runtime_error("output path is not a directory: " + options.output.string());
|
|
if (!fs::is_empty(options.output))
|
|
throw std::runtime_error("output directory is not empty: " + options.output.string());
|
|
}
|
|
|
|
PS1::ISOReader disc;
|
|
if (!disc.Open(options.disc.string()))
|
|
throw std::runtime_error("could not open the disc image or its CUE tracks");
|
|
size_t cnf_size = disc.GetFileSize("SYSTEM.CNF");
|
|
if (!cnf_size) throw std::runtime_error("SYSTEM.CNF was not found on the disc");
|
|
std::vector<uint8_t> cnf_bytes(cnf_size + 1, 0);
|
|
size_t cnf_read = disc.ReadFile("SYSTEM.CNF", cnf_bytes.data(), cnf_size);
|
|
std::string boot = parse_boot_path(std::string((char*)cnf_bytes.data(), cnf_read));
|
|
if (boot.empty()) throw std::runtime_error("SYSTEM.CNF does not contain a BOOT executable");
|
|
// Normalize boot path: convert backslashes to forward slashes so that
|
|
// std::filesystem::path (which treats '\' as a regular character on Linux)
|
|
// correctly splits the path into directory and filename components.
|
|
const std::string boot_norm = PSXRecompCLI::normalize_boot_path(boot);
|
|
size_t exe_size = disc.GetFileSize(boot_norm);
|
|
if (!exe_size) throw std::runtime_error("boot executable was not found on disc: " + boot);
|
|
std::vector<uint8_t> exe_bytes(exe_size);
|
|
if (disc.ReadFile(boot_norm, exe_bytes.data(), exe_bytes.size()) != exe_bytes.size())
|
|
throw std::runtime_error("failed to read the complete boot executable");
|
|
if (exe_bytes.size() < 2048 || std::string((char*)exe_bytes.data(), 8) != "PS-X EXE")
|
|
throw std::runtime_error("the disc BOOT file is not a PS-X EXE");
|
|
|
|
fs::create_directories(options.output);
|
|
// Mark the generated folder as its own project before config_loader walks
|
|
// upward looking for a project root. This matters when the output folder
|
|
// happens to live inside another checkout.
|
|
write_file(options.output / ".gitignore",
|
|
"build/\ninput/\ngenerated/\nbios-generated/\n*.mcr\nsaves/\n");
|
|
const std::string boot_file = PSXRecompCLI::boot_filename(boot_norm);
|
|
const fs::path local_exe = options.output / "input" / boot_file;
|
|
write_bytes(local_exe, exe_bytes);
|
|
|
|
std::string parse_error;
|
|
auto parsed = PSXRecomp::PS1ExeParser::parse_file(local_exe, parse_error);
|
|
if (!parsed) throw std::runtime_error("failed to parse boot executable: " + parse_error);
|
|
const auto& image = *parsed;
|
|
const std::string serial = PSXRecompCLI::serial_from_boot(boot_norm);
|
|
const std::string title = options.name.empty()
|
|
? trim(disc.GetVolumeID())
|
|
: options.name;
|
|
const std::string game_name = title.empty() ? serial : title;
|
|
const std::string project_name = safe_stem(game_name) + "Recomp";
|
|
const BiosProfile& bios_profile = select_bios_profile(options.bios);
|
|
const fs::path framework_source = find_framework(exe_dir);
|
|
const fs::path profile_source =
|
|
find_bios_profile(framework_source, bios_profile);
|
|
|
|
std::set<uint32_t> seeds = {image.entry_point()};
|
|
for (uint32_t address = image.load_address(); address + 4 <= image.end_address(); address += 4) {
|
|
auto word = image.read_word(address);
|
|
if (!word || ((*word >> 26) & 0x3F) != 0x03) continue;
|
|
uint32_t target = (address & 0xF0000000u) | ((*word & 0x03FFFFFFu) << 2);
|
|
if (target >= image.load_address() && target < image.end_address()) seeds.insert(target);
|
|
}
|
|
std::string seed_text = fmt::format("# Auto-generated JAL targets for {}\n", serial);
|
|
for (uint32_t seed : seeds) seed_text += fmt::format("0x{:08X}\n", seed);
|
|
write_file(options.output / "seeds" / "functions.txt", seed_text);
|
|
|
|
fmt::print("[1/4] Copying build framework...\n");
|
|
const fs::path framework_destination = options.output / "psxrecomp";
|
|
copy_framework(framework_source, framework_destination);
|
|
const fs::path profile_destination =
|
|
framework_destination / "bios" / bios_profile.filename;
|
|
fs::create_directories(profile_destination.parent_path());
|
|
fs::copy_file(profile_source, profile_destination,
|
|
fs::copy_options::overwrite_existing);
|
|
|
|
uint32_t text_size = image.code_size();
|
|
if (text_size & 0xFFF) text_size = (text_size + 0xFFF) & ~0xFFFu;
|
|
const uint32_t stack = image.header.stack_base ? image.header.stack_base : 0x801FFFF0u;
|
|
const std::string config = fmt::format(
|
|
"[game]\n"
|
|
"name = \"{}\"\n"
|
|
"id = \"{}\"\n"
|
|
"exe = \"input/{}\"\n"
|
|
"disc = \"{}\"\n"
|
|
"load_address = \"0x{:08X}\"\n"
|
|
"entry_pc = \"0x{:08X}\"\n"
|
|
"text_size = \"0x{:X}\"\n"
|
|
"stack_base = \"0x{:08X}\"\n\n"
|
|
"[recompiler]\n"
|
|
"seeds = \"seeds/functions.txt\"\n"
|
|
"bios_config = \"psxrecomp/bios/{}\"\n"
|
|
"strict = true\n"
|
|
"out_dir = \"generated\"\n\n"
|
|
"[runtime]\n"
|
|
"window_title = \"{} Recompiled\"\n"
|
|
"bios_hle = false\n"
|
|
"overlay_cache = true\n\n"
|
|
"[video]\n"
|
|
"renderer = \"opengl\"\n",
|
|
toml_string(game_name), serial, toml_string(boot_file),
|
|
toml_string(options.disc.generic_string()), image.load_address(),
|
|
image.entry_point(), text_size, stack, bios_profile.filename,
|
|
toml_string(game_name));
|
|
write_file(options.output / "game.toml", config);
|
|
|
|
const fs::path game_tool = find_helper(exe_dir, "psxrecomp-game");
|
|
const fs::path bios_tool = find_helper(exe_dir, "psxrecomp-bios");
|
|
fmt::print("[2/4] Recompiling game executable...\n");
|
|
{
|
|
CurrentPathGuard project_directory(options.output);
|
|
if (run_process({game_tool.string(), "--config", "game.toml"}))
|
|
throw std::runtime_error("game recompilation failed");
|
|
}
|
|
fmt::print("[3/4] Recompiling BIOS...\n");
|
|
const fs::path bios_generated = framework_destination / "generated";
|
|
fs::create_directories(bios_generated);
|
|
if (run_process({bios_tool.string(),
|
|
"--config", fs::absolute(profile_destination).string(),
|
|
"--rom", options.bios.string(),
|
|
"--out-dir",
|
|
fs::absolute(bios_generated).string()}))
|
|
throw std::runtime_error("BIOS recompilation failed");
|
|
|
|
const std::string cmake = fmt::format(
|
|
"cmake_minimum_required(VERSION 3.20)\n"
|
|
"set(PSXRECOMP_ROOT \"${{CMAKE_CURRENT_SOURCE_DIR}}/psxrecomp\")\n"
|
|
"if(NOT EXISTS \"${{PSXRECOMP_ROOT}}/runtime/runtime.cmake\")\n"
|
|
" message(FATAL_ERROR\n"
|
|
" \"PSXRecomp runtime is missing: ${{PSXRECOMP_ROOT}}/runtime/runtime.cmake\\n\"\n"
|
|
" \"This generated project needs the psxrecomp framework tree at ${{PSXRECOMP_ROOT}}.\\n\"\n"
|
|
" \"If this project came from git, run: git submodule update --init --recursive\\n\"\n"
|
|
" \"If this project came from psxrecomp.exe, regenerate it from the full CLI zip and keep the generated psxrecomp/ folder.\")\n"
|
|
"endif()\n"
|
|
"project({} C CXX)\n"
|
|
"set(CMAKE_C_STANDARD 99)\n"
|
|
"set(CMAKE_CXX_STANDARD 17)\n"
|
|
"set(PSX_RECOMP_UI OFF CACHE BOOL \"\" FORCE)\n"
|
|
"set(PSXRECOMP_BIOS_STEMS \"{}\" CACHE STRING \"\" FORCE)\n"
|
|
"include(\"${{PSXRECOMP_ROOT}}/runtime/runtime.cmake\")\n"
|
|
"file(GLOB GAME_FULL CONFIGURE_DEPENDS\n"
|
|
" \"${{CMAKE_CURRENT_SOURCE_DIR}}/generated/*_full.c\"\n"
|
|
" \"${{CMAKE_CURRENT_SOURCE_DIR}}/generated/*_full_*.c\")\n"
|
|
"file(GLOB GAME_DISPATCH \"${{CMAKE_CURRENT_SOURCE_DIR}}/generated/*_dispatch.c\")\n"
|
|
"psxrecomp_add_runtime_target(psx-runtime\n"
|
|
" GAME_GENERATED_FULL_C \"${{GAME_FULL}}\"\n"
|
|
" GAME_GENERATED_DISPATCH_C \"${{GAME_DISPATCH}}\"\n"
|
|
" WINDOW_TITLE \"{} Recompiled\"\n"
|
|
" DEFAULT_GAME_CONFIG_PATH \"game.toml\"\n"
|
|
")\n",
|
|
project_name, bios_profile.out_stem, game_name);
|
|
write_file(options.output / "CMakeLists.txt", cmake);
|
|
|
|
write_file(options.output / "build.ps1",
|
|
"$ErrorActionPreference = 'Stop'\n"
|
|
"$Root = Split-Path -Parent $MyInvocation.MyCommand.Path\n"
|
|
"$RuntimeCMake = Join-Path $Root 'psxrecomp/runtime/runtime.cmake'\n"
|
|
"if (-not (Test-Path -LiteralPath $RuntimeCMake)) {\n"
|
|
" Write-Error \"PSXRecomp runtime is missing: $RuntimeCMake`nThis generated project needs the psxrecomp framework tree at '$Root\\psxrecomp'.`nIf this project came from git, run: git submodule update --init --recursive`nIf this project came from psxrecomp.exe, regenerate it from the full CLI zip and keep the generated psxrecomp folder.\"\n"
|
|
" exit 1\n"
|
|
"}\n"
|
|
"cmake -S $Root -B (Join-Path $Root 'build') -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF\n"
|
|
"if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }\n"
|
|
"cmake --build (Join-Path $Root 'build') --config Release --parallel\n"
|
|
"exit $LASTEXITCODE\n");
|
|
write_file(options.output / "build.sh",
|
|
"#!/usr/bin/env sh\n"
|
|
"set -eu\n"
|
|
"ROOT=$(CDPATH= cd -- \"$(dirname -- \"$0\")\" && pwd)\n"
|
|
"if [ ! -f \"$ROOT/psxrecomp/runtime/runtime.cmake\" ]; then\n"
|
|
" echo \"error: PSXRecomp runtime is missing: $ROOT/psxrecomp/runtime/runtime.cmake\" >&2\n"
|
|
" echo \"This generated project needs the psxrecomp framework tree at $ROOT/psxrecomp.\" >&2\n"
|
|
" echo \"If this project came from git, run: git submodule update --init --recursive\" >&2\n"
|
|
" echo \"If this project came from psxrecomp.exe, regenerate it from the full CLI zip and keep the generated psxrecomp folder.\" >&2\n"
|
|
" exit 1\n"
|
|
"fi\n"
|
|
"cmake -S \"$ROOT\" -B \"$ROOT/build\" -G Ninja -DCMAKE_BUILD_TYPE=Release -DPSX_RECOMP_UI=OFF\n"
|
|
"cmake --build \"$ROOT/build\" --config Release --parallel\n");
|
|
write_file(options.output / ".gitignore",
|
|
"build/\ninput/\ngenerated/\nbios-generated/\n*.mcr\nsaves/\n");
|
|
write_file(options.output / "README.md", fmt::format(
|
|
"# {}\n\n"
|
|
"Generated locally by PSXRecomp from your own disc and BIOS.\n\n"
|
|
"## Build\n\n"
|
|
"Install CMake, Ninja, and a C/C++ compiler. SDL3 is fetched automatically.\n"
|
|
"Then run `sh build.sh` on macOS/Linux or `.\\build.ps1` in PowerShell.\n\n"
|
|
"This project must keep the generated `psxrecomp/` framework folder beside\n"
|
|
"`CMakeLists.txt`. If it is missing, restore it from source control with\n"
|
|
"`git submodule update --init --recursive` or regenerate the project from\n"
|
|
"the full PSXRecomp CLI zip.\n\n"
|
|
"SDL3 is the default. To use SDL2 explicitly, configure once with\n"
|
|
"`cmake -S . -B build -DPSX_SDL_BACKEND=SDL2`; later build-script runs\n"
|
|
"preserve that cached selection.\n\n"
|
|
"The executable is written under `build/`. Keep your original disc image\n"
|
|
"at the path stored in `game.toml`, or update that path before running.\n\n"
|
|
"The `input/`, `generated/`, and `bios-generated/` folders contain data\n"
|
|
"derived from copyrighted files you supplied. Do not redistribute them.\n",
|
|
game_name));
|
|
fmt::print("[4/4] Wrote project files.\n\nReady: {}\nBuild with: {}\n",
|
|
options.output.string(),
|
|
#ifdef _WIN32
|
|
(options.output / "build.ps1").string()
|
|
#else
|
|
(options.output / "build.sh").string()
|
|
#endif
|
|
);
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
try {
|
|
if (argc < 2 || std::string(argv[1]) == "-h" || std::string(argv[1]) == "--help") {
|
|
usage(argv[0]);
|
|
return argc < 2 ? 2 : 0;
|
|
}
|
|
if (std::string(argv[1]) != "build") {
|
|
usage(argv[0]);
|
|
throw std::runtime_error("unknown command: " + std::string(argv[1]));
|
|
}
|
|
return build_project(parse_options(argc, argv), executable_dir(argv[0]));
|
|
} catch (const std::exception& error) {
|
|
fmt::print(stderr, "psxrecomp: error: {}\n", error.what());
|
|
return 1;
|
|
}
|
|
}
|