mirror of
https://github.com/N64Recomp/N64Recomp.git
synced 2026-04-06 02:07:05 +00:00
Adds a --store-only CLI flag that creates the .nrm zip archive without compression (store only). This is useful when accessing files inside the zip via DMA (example Audio API). On Windows, this sets Compress-Archive's CompressionLevel to NoCompression. On Linux, this passes -0 to zip instead of -9.
1264 lines
57 KiB
C++
1264 lines
57 KiB
C++
#include <array>
|
|
#include <fstream>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <numeric>
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
#include "fmt/format.h"
|
|
#include "fmt/ostream.h"
|
|
#include "recompiler/context.h"
|
|
#include <toml++/toml.hpp>
|
|
|
|
#ifdef _WIN32
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#else
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
#endif
|
|
|
|
constexpr std::string_view symbol_filename = "mod_syms.bin";
|
|
constexpr std::string_view binary_filename = "mod_binary.bin";
|
|
constexpr std::string_view manifest_filename = "mod.json";
|
|
|
|
struct ModManifest {
|
|
std::string mod_id;
|
|
std::string version_string;
|
|
std::string display_name;
|
|
std::string description;
|
|
std::string short_description;
|
|
std::vector<std::string> authors;
|
|
std::string game_id;
|
|
std::string minimum_recomp_version;
|
|
std::unordered_map<std::string, std::vector<std::string>> native_libraries;
|
|
bool custom_gamemode = false;
|
|
std::vector<toml::table> config_options;
|
|
std::vector<std::string> dependencies;
|
|
std::vector<std::string> full_dependency_strings;
|
|
std::vector<std::string> optional_dependencies;
|
|
std::vector<std::string> full_optional_dependency_strings;
|
|
};
|
|
|
|
struct ModInputs {
|
|
std::filesystem::path elf_path;
|
|
std::string mod_filename;
|
|
std::filesystem::path func_reference_syms_file_path;
|
|
std::vector<std::filesystem::path> data_reference_syms_file_paths;
|
|
std::vector<std::filesystem::path> additional_files;
|
|
};
|
|
|
|
struct ModConfig {
|
|
ModManifest manifest;
|
|
ModInputs inputs;
|
|
};
|
|
|
|
static std::filesystem::path concat_if_not_empty(const std::filesystem::path& parent, const std::filesystem::path& child) {
|
|
if (child.is_absolute()) {
|
|
return child;
|
|
}
|
|
if (!child.empty()) {
|
|
return parent / child;
|
|
}
|
|
return child;
|
|
}
|
|
|
|
static bool validate_version_string(std::string_view str, bool& has_label) {
|
|
std::array<size_t, 2> period_indices;
|
|
size_t num_periods = 0;
|
|
size_t cur_pos = 0;
|
|
uint16_t major;
|
|
uint16_t minor;
|
|
uint16_t patch;
|
|
|
|
// Find the 2 required periods.
|
|
cur_pos = str.find('.', cur_pos);
|
|
period_indices[0] = cur_pos;
|
|
cur_pos = str.find('.', cur_pos + 1);
|
|
period_indices[1] = cur_pos;
|
|
|
|
// Check that both were found.
|
|
if (period_indices[0] == std::string::npos || period_indices[1] == std::string::npos) {
|
|
return false;
|
|
}
|
|
|
|
// Parse the 3 numbers formed by splitting the string via the periods.
|
|
std::array<std::from_chars_result, 3> parse_results;
|
|
std::array<size_t, 3> parse_starts { 0, period_indices[0] + 1, period_indices[1] + 1 };
|
|
std::array<size_t, 3> parse_ends { period_indices[0], period_indices[1], str.size() };
|
|
parse_results[0] = std::from_chars(str.data() + parse_starts[0], str.data() + parse_ends[0], major);
|
|
parse_results[1] = std::from_chars(str.data() + parse_starts[1], str.data() + parse_ends[1], minor);
|
|
parse_results[2] = std::from_chars(str.data() + parse_starts[2], str.data() + parse_ends[2], patch);
|
|
|
|
// Check that the first two parsed correctly.
|
|
auto did_parse = [&](size_t i) {
|
|
return parse_results[i].ec == std::errc{} && parse_results[i].ptr == str.data() + parse_ends[i];
|
|
};
|
|
|
|
if (!did_parse(0) || !did_parse(1)) {
|
|
return false;
|
|
}
|
|
|
|
// Check that the third had a successful parse, but not necessarily read all the characters.
|
|
if (parse_results[2].ec != std::errc{}) {
|
|
return false;
|
|
}
|
|
|
|
// Allow a plus or minus directly after the third number.
|
|
if (parse_results[2].ptr != str.data() + parse_ends[2]) {
|
|
has_label = true;
|
|
if (*parse_results[2].ptr != '+' && *parse_results[2].ptr != '-') {
|
|
// Failed to parse, as nothing is allowed directly after the last number besides a plus or minus.
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
has_label = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool validate_dependency_string(const std::string& val, size_t& name_length, bool& has_label) {
|
|
std::string ret;
|
|
size_t name_length_temp;
|
|
|
|
// Don't allow an empty dependency name.
|
|
if (val.size() == 0) {
|
|
return false;
|
|
}
|
|
bool validated_name;
|
|
bool validated_version;
|
|
|
|
// Check if there's a version number specified.
|
|
size_t colon_pos = val.find(':');
|
|
if (colon_pos == std::string::npos) {
|
|
// No version present, so just validate the dependency's id.
|
|
|
|
validated_name = N64Recomp::validate_mod_id(std::string_view{val});
|
|
name_length_temp = val.size();
|
|
validated_version = true;
|
|
has_label = false;
|
|
}
|
|
else {
|
|
// Version present, validate it.
|
|
|
|
// Don't allow an empty dependency name after accounting for the colon.
|
|
if (colon_pos == 0) {
|
|
return false;
|
|
}
|
|
|
|
name_length_temp = colon_pos;
|
|
|
|
// Validate the dependency's id and version.
|
|
validated_name = N64Recomp::validate_mod_id(std::string_view{val.begin(), val.begin() + colon_pos});
|
|
validated_version = validate_version_string(std::string_view{val.begin() + colon_pos + 1, val.end()}, has_label);
|
|
}
|
|
|
|
if (validated_name && validated_version) {
|
|
name_length = name_length_temp;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
template <typename T>
|
|
static T read_toml_value(const toml::table& data, std::string_view key, bool required) {
|
|
const toml::node* value_node = data.get(key);
|
|
|
|
if (value_node == nullptr) {
|
|
if (required) {
|
|
throw toml::parse_error(("Missing required field \"" + std::string{key} + "\"").c_str(), data.source());
|
|
}
|
|
else {
|
|
return T{};
|
|
}
|
|
}
|
|
|
|
std::optional<T> opt = value_node->value_exact<T>();
|
|
if (opt.has_value()) {
|
|
return opt.value();
|
|
}
|
|
else {
|
|
throw toml::parse_error(("Incorrect type for field \"" + std::string{key} + "\"").c_str(), data.source());
|
|
}
|
|
}
|
|
|
|
static const toml::array& read_toml_array(const toml::table& data, std::string_view key, bool required) {
|
|
static const toml::array empty_array = toml::array{};
|
|
const toml::node* value_node = data.get(key);
|
|
|
|
if (value_node == nullptr) {
|
|
if (required) {
|
|
throw toml::parse_error(("Missing required field " + std::string{ key }).c_str(), data.source());
|
|
}
|
|
else {
|
|
return empty_array;
|
|
}
|
|
}
|
|
|
|
if (!value_node->is_array()) {
|
|
throw toml::parse_error(("Incorrect type for field " + std::string{ key }).c_str(), value_node->source());
|
|
}
|
|
|
|
return *value_node->as_array();
|
|
}
|
|
|
|
static std::vector<std::filesystem::path> get_toml_path_array(const toml::array& toml_array, const std::filesystem::path& basedir) {
|
|
std::vector<std::filesystem::path> ret;
|
|
|
|
// Reserve room for all the funcs in the map.
|
|
ret.reserve(toml_array.size());
|
|
toml_array.for_each([&ret, &basedir](auto&& el) {
|
|
if constexpr (toml::is_string<decltype(el)>) {
|
|
ret.emplace_back(concat_if_not_empty(basedir, el.template ref<std::string>()));
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for file entry", el.source());
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool validate_config_option(const toml::table& option) {
|
|
// TODO config option validation.
|
|
return true;
|
|
}
|
|
|
|
ModManifest parse_mod_config_manifest(const std::filesystem::path& basedir, const toml::table& manifest_table) {
|
|
ModManifest ret;
|
|
|
|
// Mod ID
|
|
ret.mod_id = read_toml_value<std::string_view>(manifest_table, "id", true);
|
|
if (!N64Recomp::validate_mod_id(ret.mod_id)) {
|
|
throw toml::parse_error("Invalid mod id", manifest_table["id"].node()->source());
|
|
}
|
|
|
|
// Mod version
|
|
ret.version_string = read_toml_value<std::string_view>(manifest_table, "version", true);
|
|
bool version_has_label;
|
|
if (!validate_version_string(ret.version_string, version_has_label)) {
|
|
throw toml::parse_error("Invalid mod version", manifest_table["version"].node()->source());
|
|
}
|
|
|
|
// Display name
|
|
ret.display_name = read_toml_value<std::string_view>(manifest_table, "display_name", true);
|
|
|
|
// Description (optional)
|
|
ret.description = read_toml_value<std::string_view>(manifest_table, "description", false);
|
|
|
|
// Short description (optional)
|
|
ret.short_description = read_toml_value<std::string_view>(manifest_table, "short_description", false);
|
|
|
|
// Authors
|
|
const toml::array& authors_array = read_toml_array(manifest_table, "authors", true);
|
|
authors_array.for_each([&ret](auto&& el) {
|
|
if constexpr (toml::is_string<decltype(el)>) {
|
|
ret.authors.emplace_back(el.template ref<std::string>());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for author entry", el.source());
|
|
}
|
|
});
|
|
|
|
// Game ID
|
|
ret.game_id = read_toml_value<std::string_view>(manifest_table, "game_id", true);
|
|
|
|
// Minimum recomp version
|
|
ret.minimum_recomp_version = read_toml_value<std::string_view>(manifest_table, "minimum_recomp_version", true);
|
|
bool minimum_recomp_version_has_label;
|
|
if (!validate_version_string(ret.minimum_recomp_version, minimum_recomp_version_has_label)) {
|
|
throw toml::parse_error("Invalid minimum recomp version", manifest_table["minimum_recomp_version"].node()->source());
|
|
}
|
|
if (minimum_recomp_version_has_label) {
|
|
throw toml::parse_error("Minimum recomp version may not have a label", manifest_table["minimum_recomp_version"].node()->source());
|
|
}
|
|
|
|
// Native libraries (optional)
|
|
const toml::array& native_libraries = read_toml_array(manifest_table, "native_libraries", false);
|
|
if (!native_libraries.empty()) {
|
|
native_libraries.for_each([&ret](const auto& el) {
|
|
if constexpr (toml::is_table<decltype(el)>) {
|
|
const toml::table& el_table = *el.as_table();
|
|
std::string_view library_name = read_toml_value<std::string_view>(el_table, "name", true);
|
|
const toml::array funcs_array = read_toml_array(el_table, "funcs", true);
|
|
std::vector<std::string> cur_funcs{};
|
|
funcs_array.for_each([&ret, &cur_funcs](const auto& func_el) {
|
|
if constexpr (toml::is_string<decltype(func_el)>) {
|
|
cur_funcs.emplace_back(func_el.template ref<std::string>());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for native library function entry", func_el.source());
|
|
}
|
|
});
|
|
ret.native_libraries.emplace(std::string{library_name}, std::move(cur_funcs));
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for native library entry", el.source());
|
|
}
|
|
});
|
|
}
|
|
|
|
// Dependency list (optional)
|
|
const toml::array& dependency_array = read_toml_array(manifest_table, "dependencies", false);
|
|
if (!dependency_array.empty()) {
|
|
// Reserve room for all the dependencies.
|
|
ret.dependencies.reserve(dependency_array.size());
|
|
dependency_array.for_each([&ret](const auto& el) {
|
|
if constexpr (toml::is_string<decltype(el)>) {
|
|
size_t dependency_id_length;
|
|
bool dependency_version_has_label;
|
|
if (!validate_dependency_string(el.template ref<std::string>(), dependency_id_length, dependency_version_has_label)) {
|
|
throw toml::parse_error("Invalid dependency entry", el.source());
|
|
}
|
|
if (dependency_version_has_label) {
|
|
throw toml::parse_error("Dependency versions may not have labels", el.source());
|
|
}
|
|
std::string dependency_id = el.template ref<std::string>().substr(0, dependency_id_length);
|
|
ret.dependencies.emplace_back(dependency_id);
|
|
ret.full_dependency_strings.emplace_back(el.template ref<std::string>());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for dependency entry", el.source());
|
|
}
|
|
});
|
|
}
|
|
|
|
// Optional dependency list (optional)
|
|
const toml::array& optional_dependency_array = read_toml_array(manifest_table, "optional_dependencies", false);
|
|
if (!optional_dependency_array.empty()) {
|
|
// Reserve room for all the dependencies.
|
|
ret.dependencies.reserve(optional_dependency_array.size());
|
|
optional_dependency_array.for_each([&ret](const auto& el) {
|
|
if constexpr (toml::is_string<decltype(el)>) {
|
|
size_t dependency_id_length;
|
|
bool dependency_version_has_label;
|
|
if (!validate_dependency_string(el.template ref<std::string>(), dependency_id_length, dependency_version_has_label)) {
|
|
throw toml::parse_error("Invalid optional dependency entry", el.source());
|
|
}
|
|
if (dependency_version_has_label) {
|
|
throw toml::parse_error("Dependency versions may not have labels", el.source());
|
|
}
|
|
std::string dependency_id = el.template ref<std::string>().substr(0, dependency_id_length);
|
|
ret.optional_dependencies.emplace_back(dependency_id);
|
|
ret.full_optional_dependency_strings.emplace_back(el.template ref<std::string>());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for optional dependency entry", el.source());
|
|
}
|
|
});
|
|
}
|
|
|
|
// Config schema (optional)
|
|
const toml::array& config_options_array = read_toml_array(manifest_table, "config_options", false);
|
|
if (!config_options_array.empty()) {
|
|
ret.config_options.reserve(config_options_array.size());
|
|
config_options_array.for_each([&ret](const auto& el) {
|
|
if constexpr (toml::is_table<decltype(el)>) {
|
|
if (!validate_config_option(el)) {
|
|
throw toml::parse_error("Invalid config option", el.source());
|
|
}
|
|
ret.config_options.emplace_back(el);
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid type for config option", el.source());
|
|
}
|
|
});
|
|
}
|
|
|
|
// Custom gamemode (optional)
|
|
ret.custom_gamemode = read_toml_value<bool>(manifest_table, "custom_gamemode", false);
|
|
|
|
return ret;
|
|
}
|
|
|
|
ModInputs parse_mod_config_inputs(const std::filesystem::path& basedir, const toml::table& inputs_table) {
|
|
ModInputs ret;
|
|
|
|
// Elf file
|
|
std::optional<std::string> elf_path_opt = inputs_table["elf_path"].value<std::string>();
|
|
if (elf_path_opt.has_value()) {
|
|
ret.elf_path = concat_if_not_empty(basedir, elf_path_opt.value());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Mod toml input section is missing elf file", inputs_table.source());
|
|
}
|
|
|
|
// Output NRM file
|
|
std::optional<std::string> mod_filename_opt = inputs_table["mod_filename"].value<std::string>();
|
|
if (mod_filename_opt.has_value()) {
|
|
ret.mod_filename = std::move(mod_filename_opt.value());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Mod toml input section is missing the output mod filename", inputs_table.source());
|
|
}
|
|
|
|
// Function reference symbols file
|
|
std::optional<std::string> func_reference_syms_file_opt = inputs_table["func_reference_syms_file"].value<std::string>();
|
|
if (func_reference_syms_file_opt.has_value()) {
|
|
ret.func_reference_syms_file_path = concat_if_not_empty(basedir, func_reference_syms_file_opt.value());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Mod toml input section is missing function reference symbol file", inputs_table.source());
|
|
}
|
|
|
|
// Data reference symbols files
|
|
toml::node_view data_reference_syms_file_data = inputs_table["data_reference_syms_files"];
|
|
if (data_reference_syms_file_data.is_array()) {
|
|
const toml::array& array = *data_reference_syms_file_data.as_array();
|
|
ret.data_reference_syms_file_paths = get_toml_path_array(array, basedir);
|
|
}
|
|
else {
|
|
if (data_reference_syms_file_data) {
|
|
throw toml::parse_error("Mod toml input section is missing data reference symbol file list", inputs_table.source());
|
|
}
|
|
else {
|
|
throw toml::parse_error("Invalid data reference symbol file list", data_reference_syms_file_data.node()->source());
|
|
}
|
|
}
|
|
|
|
// Additional files (optional)
|
|
const toml::array& additional_files_array = read_toml_array(inputs_table, "additional_files", false);
|
|
if (!additional_files_array.empty()) {
|
|
ret.additional_files = get_toml_path_array(additional_files_array, basedir);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
ModConfig parse_mod_config(const std::filesystem::path& config_path, bool& good) {
|
|
ModConfig ret{};
|
|
good = false;
|
|
|
|
toml::table toml_data{};
|
|
|
|
try {
|
|
toml_data = toml::parse_file(config_path.native());
|
|
std::filesystem::path basedir = config_path.parent_path();
|
|
|
|
// Find the manifest section and validate its type.
|
|
const toml::node* manifest_data_ptr = toml_data.get("manifest");
|
|
if (manifest_data_ptr == nullptr) {
|
|
throw toml::parse_error("Mod toml is missing manifest section", toml::source_region{});
|
|
}
|
|
if (!manifest_data_ptr->is_table()) {
|
|
throw toml::parse_error("Incorrect type for mod toml manifest section", manifest_data_ptr->source());
|
|
}
|
|
const toml::table& manifest_table = *manifest_data_ptr->as_table();
|
|
|
|
// Find the inputs section and validate its type.
|
|
const toml::node* inputs_data_ptr = toml_data.get("inputs");
|
|
if (inputs_data_ptr == nullptr) {
|
|
throw toml::parse_error("Mod toml is missing inputs section", toml::source_region{});
|
|
}
|
|
if (!inputs_data_ptr->is_table()) {
|
|
throw toml::parse_error("Incorrect type for mod toml inputs section", inputs_data_ptr->source());
|
|
}
|
|
const toml::table& inputs_table = *inputs_data_ptr->as_table();
|
|
|
|
// Parse the manifest.
|
|
ret.manifest = parse_mod_config_manifest(basedir, manifest_table);
|
|
// Parse the inputs.
|
|
ret.inputs = parse_mod_config_inputs(basedir, inputs_table);
|
|
}
|
|
catch (const toml::parse_error& err) {
|
|
std::cerr << "Syntax error parsing toml: " << config_path << " (" << err.source().begin << "):\n" << err.description() << std::endl;
|
|
return {};
|
|
}
|
|
|
|
good = true;
|
|
return ret;
|
|
}
|
|
|
|
static inline uint32_t round_up_16(uint32_t value) {
|
|
return (value + 15) & (~15);
|
|
}
|
|
|
|
bool parse_callback_name(std::string_view data, std::string& dependency_name, std::string& event_name) {
|
|
size_t period_pos = data.find(':');
|
|
|
|
if (period_pos == std::string::npos) {
|
|
return false;
|
|
}
|
|
|
|
std::string_view dependency_name_view = std::string_view{data}.substr(0, period_pos);
|
|
std::string_view event_name_view = std::string_view{data}.substr(period_pos + 1);
|
|
|
|
if (!N64Recomp::validate_mod_id(dependency_name_view)) {
|
|
return false;
|
|
}
|
|
|
|
dependency_name = dependency_name_view;
|
|
event_name = event_name_view;
|
|
return true;
|
|
}
|
|
|
|
toml::array string_vector_to_toml(const std::vector<std::string>& input) {
|
|
toml::array ret{};
|
|
for (const std::string& str : input) {
|
|
ret.emplace_back(str);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void write_manifest(const std::filesystem::path& path, const ModManifest& manifest) {
|
|
toml::table output_data{};
|
|
|
|
output_data.emplace("game_id", manifest.game_id);
|
|
output_data.emplace("id", manifest.mod_id);
|
|
output_data.emplace("version", manifest.version_string);
|
|
output_data.emplace("display_name", manifest.display_name);
|
|
|
|
if (!manifest.description.empty()) {
|
|
output_data.emplace("description", manifest.description);
|
|
}
|
|
|
|
if (!manifest.short_description.empty()) {
|
|
output_data.emplace("short_description", manifest.short_description);
|
|
}
|
|
|
|
output_data.emplace("authors", string_vector_to_toml(manifest.authors));
|
|
|
|
output_data.emplace("minimum_recomp_version", manifest.minimum_recomp_version);
|
|
|
|
if (!manifest.native_libraries.empty()) {
|
|
toml::table libraries_table{};
|
|
|
|
size_t library_index = 0;
|
|
for (const auto& [library, funcs] : manifest.native_libraries) {
|
|
libraries_table.emplace(library, string_vector_to_toml(funcs));
|
|
}
|
|
|
|
output_data.emplace("native_libraries", std::move(libraries_table));
|
|
}
|
|
|
|
if (!manifest.full_dependency_strings.empty()) {
|
|
output_data.emplace("dependencies", string_vector_to_toml(manifest.full_dependency_strings));
|
|
}
|
|
|
|
if (!manifest.full_optional_dependency_strings.empty()) {
|
|
output_data.emplace("optional_dependencies", string_vector_to_toml(manifest.full_optional_dependency_strings));
|
|
}
|
|
|
|
if (!manifest.config_options.empty()) {
|
|
toml::array options_array{};
|
|
for (const auto& option : manifest.config_options) {
|
|
options_array.emplace_back(option);
|
|
}
|
|
output_data.emplace("config_schema", toml::table{{"options", std::move(options_array)}});
|
|
}
|
|
|
|
if (manifest.custom_gamemode) {
|
|
output_data.emplace("custom_gamemode", manifest.custom_gamemode);
|
|
}
|
|
|
|
toml::json_formatter formatter{output_data, toml::format_flags::indentation | toml::format_flags::indentation};
|
|
std::ofstream output_file(path);
|
|
|
|
output_file << formatter << std::endl;
|
|
}
|
|
|
|
N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bool& good) {
|
|
N64Recomp::Context ret{};
|
|
good = false;
|
|
|
|
// Make a vector containing 0, 1, 2, ... section count - 1
|
|
std::vector<uint16_t> section_order;
|
|
section_order.resize(input_context.sections.size());
|
|
std::iota(section_order.begin(), section_order.end(), 0);
|
|
|
|
// TODO this sort is currently disabled because sections seem to already be ordered
|
|
// by elf offset. Determine if this is always the case and remove this if so.
|
|
//// Sort the vector based on the rom address of the corresponding section.
|
|
//std::sort(section_order.begin(), section_order.end(),
|
|
// [&](uint16_t a, uint16_t b) {
|
|
// const auto& section_a = input_context.sections[a];
|
|
// const auto& section_b = input_context.sections[b];
|
|
// // Sort primarily by ROM address.
|
|
// if (section_a.rom_addr != section_b.rom_addr) {
|
|
// return section_a.rom_addr < section_b.rom_addr;
|
|
// }
|
|
// // Sort secondarily by RAM address.
|
|
// return section_a.ram_addr < section_b.ram_addr;
|
|
// }
|
|
//);
|
|
|
|
// TODO avoid a copy here.
|
|
ret.rom = input_context.rom;
|
|
|
|
// Copy the dependency data from the input context.
|
|
ret.dependencies_by_name = input_context.dependencies_by_name;
|
|
ret.import_symbols = input_context.import_symbols;
|
|
ret.dependency_events = input_context.dependency_events;
|
|
ret.dependency_events_by_name = input_context.dependency_events_by_name;
|
|
ret.dependency_imports_by_name = input_context.dependency_imports_by_name;
|
|
|
|
uint32_t rom_to_ram = (uint32_t)-1;
|
|
size_t output_section_index = (size_t)-1;
|
|
ret.sections.resize(1);
|
|
|
|
// Mapping of input section to output section for fixing up relocations.
|
|
std::unordered_map<uint16_t, uint16_t> input_section_to_output_section{};
|
|
|
|
// Iterate over the input sections in their sorted order.
|
|
for (uint16_t section_index : section_order) {
|
|
const auto& cur_section = input_context.sections[section_index];
|
|
uint32_t cur_rom_to_ram = cur_section.ram_addr - cur_section.rom_addr;
|
|
|
|
// Check if this is a non-allocated section.
|
|
if (cur_section.rom_addr == (uint32_t)-1) {
|
|
// If so, check if it has a vram address directly after the current output section. If it does, then add this
|
|
// section's size to the output section's bss size.
|
|
if (output_section_index != -1 && cur_section.size != 0) {
|
|
auto& section_out = ret.sections[output_section_index];
|
|
uint32_t output_section_bss_start = section_out.ram_addr + section_out.size;
|
|
uint32_t output_section_bss_end = output_section_bss_start + section_out.bss_size;
|
|
// Check if the current section starts at the end of the output section, allowing for a range of matches to account for 16 byte section alignment.
|
|
if (cur_section.ram_addr >= output_section_bss_end && cur_section.ram_addr <= round_up_16(output_section_bss_end)) {
|
|
// Calculate the output section's bss size by using its non-bss end address and the current section's end address.
|
|
section_out.bss_size = cur_section.ram_addr + cur_section.size - output_section_bss_start;
|
|
input_section_to_output_section[section_index] = output_section_index;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Check if this section matches up with the previous section to merge them together.
|
|
if (rom_to_ram == cur_rom_to_ram) {
|
|
auto& section_out = ret.sections[output_section_index];
|
|
uint32_t cur_section_end = cur_section.rom_addr + cur_section.size;
|
|
section_out.size = cur_section_end - section_out.rom_addr;
|
|
}
|
|
// Otherwise, create a new output section and advance to it.
|
|
else {
|
|
output_section_index++;
|
|
ret.sections.resize(output_section_index + 1);
|
|
ret.section_functions.resize(output_section_index + 1);
|
|
rom_to_ram = cur_rom_to_ram;
|
|
|
|
auto& new_section = ret.sections[output_section_index];
|
|
new_section.rom_addr = cur_section.rom_addr;
|
|
new_section.ram_addr = cur_section.ram_addr;
|
|
new_section.size = cur_section.size;
|
|
}
|
|
|
|
// Map this section to the current output section.
|
|
input_section_to_output_section[section_index] = output_section_index;
|
|
|
|
// Check for special section names.
|
|
bool patch_section = cur_section.name == N64Recomp::PatchSectionName;
|
|
bool force_patch_section = cur_section.name == N64Recomp::ForcedPatchSectionName;
|
|
bool export_section = cur_section.name == N64Recomp::ExportSectionName;
|
|
bool event_section = cur_section.name == N64Recomp::EventSectionName;
|
|
bool import_section = cur_section.name.starts_with(N64Recomp::ImportSectionPrefix);
|
|
bool callback_section = cur_section.name.starts_with(N64Recomp::CallbackSectionPrefix);
|
|
bool hook_section = cur_section.name.starts_with(N64Recomp::HookSectionPrefix);
|
|
bool hook_return_section = cur_section.name.starts_with(N64Recomp::HookReturnSectionPrefix);
|
|
|
|
// Add the functions from the current input section to the current output section.
|
|
auto& section_out = ret.sections[output_section_index];
|
|
|
|
const auto& cur_section_funcs = input_context.section_functions[section_index];
|
|
|
|
|
|
// Skip the functions and relocs in this section if it's the event section, instead opting to create event functions from the section's functions.
|
|
// This has to be done to find events that are never called, which may pop up as a valid use case for maintaining backwards compatibility
|
|
// if a mod removes a call to an event but doesn't want to break mods that reference it. If this code wasn't present, then only events that are actually
|
|
// triggered would show up in the mod's symbol file.
|
|
if (event_section) {
|
|
// Create event reference symbols for any functions in the event section. Ignore functions that already
|
|
// have a symbol, since relocs from previous sections may have triggered creation of the event's reference symbol already.
|
|
for (const auto& input_func_index : cur_section_funcs) {
|
|
const auto& cur_func = input_context.functions[input_func_index];
|
|
|
|
// Check if this event already has a symbol to prevent creating a duplicate.
|
|
N64Recomp::SymbolReference event_ref;
|
|
if (!ret.find_event_symbol(cur_func.name, event_ref)) {
|
|
ret.add_event_symbol(cur_func.name);
|
|
}
|
|
}
|
|
}
|
|
// Otherwise, copy the functions and relocs over from this section into the output context.
|
|
// Import sections can be skipped, as those only contain dummy functions. Imports will be found while scanning relocs.
|
|
else if (!import_section) {
|
|
for (size_t section_function_index = 0; section_function_index < cur_section_funcs.size(); section_function_index++) {
|
|
size_t output_func_index = ret.functions.size();
|
|
size_t input_func_index = cur_section_funcs[section_function_index];
|
|
const auto& cur_func = input_context.functions[input_func_index];
|
|
|
|
// If this is the patch section, create a replacement for this function.
|
|
if (patch_section || force_patch_section) {
|
|
// Find the corresponding symbol in the reference symbols.
|
|
N64Recomp::SymbolReference cur_reference;
|
|
bool original_func_exists = input_context.find_regular_reference_symbol(cur_func.name, cur_reference);
|
|
|
|
// Check that the function being patched exists in the original reference symbols.
|
|
if (!original_func_exists) {
|
|
fmt::print(stderr, "Function {} is marked as a patch but doesn't exist in the original ROM.\n", cur_func.name);
|
|
return {};
|
|
}
|
|
|
|
// Check that the reference symbol is actually a function.
|
|
const auto& reference_symbol = input_context.get_reference_symbol(cur_reference);
|
|
if (!reference_symbol.is_function) {
|
|
fmt::print(stderr, "Function {0} is marked as a patch, but {0} was a variable in the original ROM.\n", cur_func.name);
|
|
return {};
|
|
}
|
|
|
|
uint32_t reference_section_vram = input_context.get_reference_section_vram(reference_symbol.section_index);
|
|
uint32_t reference_section_rom = input_context.get_reference_section_rom(reference_symbol.section_index);
|
|
|
|
// Add a replacement for this function to the output context.
|
|
ret.replacements.emplace_back(
|
|
N64Recomp::FunctionReplacement {
|
|
.func_index = (uint32_t)output_func_index,
|
|
.original_section_vrom = reference_section_rom,
|
|
.original_vram = reference_section_vram + reference_symbol.section_offset,
|
|
.flags = force_patch_section ? N64Recomp::ReplacementFlags::Force : N64Recomp::ReplacementFlags{}
|
|
}
|
|
);
|
|
}
|
|
|
|
if (hook_section || hook_return_section) {
|
|
// Get the name of the hooked function.
|
|
size_t section_prefix_length = hook_section ? N64Recomp::HookSectionPrefix.size() : N64Recomp::HookReturnSectionPrefix.size();
|
|
std::string hooked_function_name = cur_section.name.substr(section_prefix_length);
|
|
|
|
// Find the corresponding symbol in the reference symbols.
|
|
N64Recomp::SymbolReference cur_reference;
|
|
bool original_func_exists = input_context.find_regular_reference_symbol(hooked_function_name, cur_reference);
|
|
|
|
// Check that the function being patched exists in the original reference symbols.
|
|
if (!original_func_exists) {
|
|
fmt::print(stderr, "Function {} hooks a function ({}) that doesn't exist in the original ROM.\n", cur_func.name, hooked_function_name);
|
|
return {};
|
|
}
|
|
|
|
// Check that the reference symbol is actually a function.
|
|
const auto& reference_symbol = input_context.get_reference_symbol(cur_reference);
|
|
if (!reference_symbol.is_function) {
|
|
fmt::print(stderr, "Function {0} hooks {1}, but {1} was a variable in the original ROM.\n", cur_func.name, hooked_function_name);
|
|
return {};
|
|
}
|
|
|
|
uint32_t reference_section_vram = input_context.get_reference_section_vram(reference_symbol.section_index);
|
|
uint32_t reference_section_rom = input_context.get_reference_section_rom(reference_symbol.section_index);
|
|
|
|
// Add a replacement for this function to the output context.
|
|
ret.hooks.emplace_back(
|
|
N64Recomp::FunctionHook {
|
|
.func_index = (uint32_t)output_func_index,
|
|
.original_section_vrom = reference_section_rom,
|
|
.original_vram = reference_section_vram + reference_symbol.section_offset,
|
|
.flags = hook_return_section ? N64Recomp::HookFlags::AtReturn : N64Recomp::HookFlags{}
|
|
}
|
|
);
|
|
}
|
|
|
|
std::string name_out;
|
|
|
|
if (export_section) {
|
|
ret.exported_funcs.push_back(output_func_index);
|
|
// Names are required for exported funcs, so copy the input function's name if we're in the export section.
|
|
name_out = cur_func.name;
|
|
}
|
|
|
|
if (callback_section) {
|
|
std::string dependency_name, event_name;
|
|
if (!parse_callback_name(std::string_view{ cur_section.name }.substr(N64Recomp::CallbackSectionPrefix.size()), dependency_name, event_name)) {
|
|
fmt::print(stderr, "Invalid mod name or event name for callback function {}.\n",
|
|
cur_func.name);
|
|
return {};
|
|
}
|
|
|
|
size_t dependency_index;
|
|
if (!ret.find_dependency(dependency_name, dependency_index)) {
|
|
fmt::print(stderr, "Failed to register callback {} to event {} from mod {} as the mod is not a registered dependency.\n",
|
|
cur_func.name, event_name, dependency_name);
|
|
return {};
|
|
}
|
|
|
|
size_t event_index;
|
|
if (!ret.add_dependency_event(event_name, dependency_index, event_index)) {
|
|
fmt::print(stderr, "Internal error: Failed to register event {} for dependency {}. Please report this issue.\n",
|
|
event_name, dependency_name);
|
|
return {};
|
|
}
|
|
|
|
if (!ret.add_callback(event_index, output_func_index)) {
|
|
fmt::print(stderr, "Internal error: Failed to add callback {} to event {} in dependency {}. Please report this issue.\n",
|
|
cur_func.name, event_name, dependency_name);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
ret.section_functions[output_section_index].push_back(output_func_index);
|
|
|
|
// Add this function to the output context.
|
|
ret.functions.emplace_back(
|
|
cur_func.vram,
|
|
cur_func.rom,
|
|
std::vector<uint32_t>{}, // words
|
|
std::move(name_out), // name
|
|
(uint16_t)output_section_index,
|
|
false, // ignored
|
|
false, // reimplemented
|
|
false // stubbed
|
|
);
|
|
|
|
// Resize the words vector so the function has the correct size. No need to copy the words, as they aren't used when making a mod symbol file.
|
|
ret.functions[output_func_index].words.resize(cur_func.words.size());
|
|
}
|
|
|
|
// Copy relocs and patch HI16/LO16/26 relocs for non-relocatable reference symbols
|
|
section_out.relocs.reserve(section_out.relocs.size() + cur_section.relocs.size());
|
|
for (const auto& cur_reloc : cur_section.relocs) {
|
|
// Skip null relocs.
|
|
if (cur_reloc.type == N64Recomp::RelocType::R_MIPS_NONE) {
|
|
continue;
|
|
}
|
|
// Reloc to a special section symbol.
|
|
if (!input_context.is_regular_reference_section(cur_reloc.target_section)) {
|
|
section_out.relocs.emplace_back(cur_reloc);
|
|
}
|
|
// Reloc to a reference symbol.
|
|
else if (cur_reloc.reference_symbol) {
|
|
bool is_relocatable = input_context.is_reference_section_relocatable(cur_reloc.target_section);
|
|
uint32_t section_vram = input_context.get_reference_section_vram(cur_reloc.target_section);
|
|
// Patch relocations to non-relocatable reference sections.
|
|
if (!is_relocatable) {
|
|
uint32_t reloc_target_address = section_vram + cur_reloc.target_section_offset;
|
|
uint32_t reloc_rom_address = cur_reloc.address - cur_section.ram_addr + cur_section.rom_addr;
|
|
|
|
uint32_t* reloc_word_ptr = reinterpret_cast<uint32_t*>(ret.rom.data() + reloc_rom_address);
|
|
uint32_t reloc_word = byteswap(*reloc_word_ptr);
|
|
switch (cur_reloc.type) {
|
|
case N64Recomp::RelocType::R_MIPS_32:
|
|
// Don't patch MIPS32 relocations, as they've already been patched during elf parsing.
|
|
break;
|
|
case N64Recomp::RelocType::R_MIPS_26:
|
|
// Don't patch MIPS26 relocations, as there may be multiple functions with the same vram. Emit the reloc instead.
|
|
section_out.relocs.emplace_back(cur_reloc);
|
|
break;
|
|
case N64Recomp::RelocType::R_MIPS_NONE:
|
|
// Nothing to do.
|
|
break;
|
|
case N64Recomp::RelocType::R_MIPS_HI16:
|
|
reloc_word &= 0xFFFF0000;
|
|
reloc_word |= (reloc_target_address - (int16_t)(reloc_target_address & 0xFFFF)) >> 16 & 0xFFFF;
|
|
break;
|
|
case N64Recomp::RelocType::R_MIPS_LO16:
|
|
reloc_word &= 0xFFFF0000;
|
|
reloc_word |= reloc_target_address & 0xFFFF;
|
|
break;
|
|
default:
|
|
fmt::print(stderr, "Unsupported or unknown relocation type {} in reloc at address 0x{:08X} in section {}.\n",
|
|
(int)cur_reloc.type, cur_reloc.address, cur_section.name);
|
|
return {};
|
|
}
|
|
*reloc_word_ptr = byteswap(reloc_word);
|
|
}
|
|
// Copy relocations to relocatable reference sections as-is.
|
|
else {
|
|
section_out.relocs.emplace_back(cur_reloc);
|
|
}
|
|
}
|
|
// Reloc to an internal symbol.
|
|
else {
|
|
const N64Recomp::Section& target_section = input_context.sections[cur_reloc.target_section];
|
|
uint32_t output_section_offset = cur_reloc.target_section_offset + target_section.ram_addr - cur_section.ram_addr;
|
|
|
|
// Check if the target section is the event section. If so, create a reference symbol reloc
|
|
// to the event symbol, creating the event symbol if necessary.
|
|
if (target_section.name == N64Recomp::EventSectionName) {
|
|
if (cur_reloc.type != N64Recomp::RelocType::R_MIPS_26) {
|
|
fmt::print(stderr, "Symbol {} is an event and cannot have its address taken.\n",
|
|
cur_section.name);
|
|
return {};
|
|
}
|
|
|
|
uint32_t target_function_vram = cur_reloc.target_section_offset + target_section.ram_addr;
|
|
size_t target_function_index = input_context.find_function_by_vram_section(target_function_vram, cur_reloc.target_section);
|
|
if (target_function_index == (size_t)-1) {
|
|
fmt::print(stderr, "Internal error: Failed to find event symbol in section {} with offset 0x{:08X} (vram 0x{:08X}). Please report this issue.\n",
|
|
target_section.name, cur_reloc.target_section_offset, target_function_vram);
|
|
return {};
|
|
}
|
|
|
|
const auto& target_function = input_context.functions[target_function_index];
|
|
|
|
// Check if this event already has a symbol to prevent creating a duplicate.
|
|
N64Recomp::SymbolReference event_ref;
|
|
if (!ret.find_event_symbol(target_function.name, event_ref)) {
|
|
ret.add_event_symbol(target_function.name);
|
|
// Update the event symbol reference now that the symbol was created.
|
|
ret.find_event_symbol(target_function.name, event_ref);
|
|
}
|
|
|
|
// Create a reloc to the event symbol.
|
|
section_out.relocs.emplace_back(N64Recomp::Reloc{
|
|
.address = cur_reloc.address,
|
|
.target_section_offset = output_section_offset,
|
|
.symbol_index = static_cast<uint32_t>(event_ref.symbol_index),
|
|
.target_section = N64Recomp::SectionEvent,
|
|
.type = cur_reloc.type,
|
|
.reference_symbol = true,
|
|
});
|
|
}
|
|
// Check if the target is an import section. If so, create a reference symbol reloc
|
|
// to the import symbol, creating the import symbol if necessary.
|
|
else if (target_section.name.starts_with(N64Recomp::ImportSectionPrefix)) {
|
|
if (cur_reloc.type != N64Recomp::RelocType::R_MIPS_26) {
|
|
fmt::print(stderr, "Symbol {} is an import and cannot have its address taken.\n",
|
|
cur_section.name);
|
|
return {};
|
|
}
|
|
|
|
uint32_t target_function_vram = cur_reloc.target_section_offset + target_section.ram_addr;
|
|
size_t target_function_index = input_context.find_function_by_vram_section(target_function_vram, cur_reloc.target_section);
|
|
if (target_function_index == (size_t)-1) {
|
|
fmt::print(stderr, "Internal error: Failed to find import symbol in section {} with offset 0x{:08X} (vram 0x{:08X}). Please report this issue.\n",
|
|
target_section.name, cur_reloc.target_section_offset, target_function_vram);
|
|
return {};
|
|
}
|
|
|
|
const auto& target_function = input_context.functions[target_function_index];
|
|
|
|
// Find the dependency that this import belongs to.
|
|
std::string dependency_name = target_section.name.substr(N64Recomp::ImportSectionPrefix.size());
|
|
size_t dependency_index;
|
|
if (!ret.find_dependency(dependency_name, dependency_index)) {
|
|
fmt::print(stderr, "Failed to import function {} from mod {} as the mod is not a registered dependency.\n",
|
|
target_function.name, dependency_name);
|
|
return {};
|
|
}
|
|
|
|
// Check if this event already has a symbol to prevent creating a duplicate.
|
|
N64Recomp::SymbolReference import_ref;
|
|
if (!ret.find_import_symbol(target_function.name, dependency_index, import_ref)) {
|
|
ret.add_import_symbol(target_function.name, dependency_index);
|
|
// Update the event symbol reference now that the symbol was created.
|
|
ret.find_import_symbol(target_function.name, dependency_index, import_ref);
|
|
}
|
|
|
|
// Create a reloc to the event symbol.
|
|
section_out.relocs.emplace_back(N64Recomp::Reloc{
|
|
.address = cur_reloc.address,
|
|
.target_section_offset = output_section_offset,
|
|
.symbol_index = static_cast<uint32_t>(import_ref.symbol_index),
|
|
.target_section = N64Recomp::SectionImport,
|
|
.type = cur_reloc.type,
|
|
.reference_symbol = true,
|
|
});
|
|
}
|
|
// Not an import or event section, so handle the reloc normally.
|
|
else {
|
|
uint32_t target_rom_to_ram = target_section.ram_addr - target_section.rom_addr;
|
|
bool is_noload = target_section.rom_addr == (uint32_t)-1;
|
|
if (!is_noload && target_rom_to_ram != cur_rom_to_ram) {
|
|
fmt::print(stderr, "Reloc at address 0x{:08X} in section {} points to a different section.\n",
|
|
cur_reloc.address, cur_section.name);
|
|
return {};
|
|
}
|
|
section_out.relocs.emplace_back(N64Recomp::Reloc{
|
|
.address = cur_reloc.address,
|
|
// Use the original target section offset, this will be recalculated based on the output section afterwards.
|
|
.target_section_offset = cur_reloc.target_section_offset,
|
|
.symbol_index = 0,
|
|
// Use the input section index in the reloc, this will be converted to the output section afterwards.
|
|
.target_section = cur_reloc.target_section,
|
|
.type = cur_reloc.type,
|
|
.reference_symbol = false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fix up every internal reloc's target section based on the input to output section mapping.
|
|
for (auto& section : ret.sections) {
|
|
for (auto& reloc : section.relocs) {
|
|
if (!reloc.reference_symbol) {
|
|
uint16_t input_section_index = reloc.target_section;
|
|
auto find_it = input_section_to_output_section.find(input_section_index);
|
|
if (find_it == input_section_to_output_section.end()) {
|
|
fmt::print(stderr, "Reloc at address 0x{:08X} references section {}, which didn't get mapped to an output section\n",
|
|
reloc.address, input_context.sections[input_section_index].name);
|
|
return {};
|
|
}
|
|
uint16_t output_section_index = find_it->second;
|
|
const auto& input_section = input_context.sections[input_section_index];
|
|
const auto& output_section = ret.sections[output_section_index];
|
|
// Adjust the reloc's target section offset based on the reloc's new section.
|
|
reloc.target_section_offset = reloc.target_section_offset + input_section.ram_addr - output_section.ram_addr;
|
|
// Replace the target section with the mapped output section.
|
|
reloc.target_section = find_it->second;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy the reference sections from the input context as-is for resolving reference symbol relocations.
|
|
ret.copy_reference_sections_from(input_context);
|
|
|
|
good = true;
|
|
return ret;
|
|
}
|
|
|
|
bool create_mod_zip(const std::filesystem::path& output_dir, const ModConfig& config, bool store_only) {
|
|
std::filesystem::path output_path = output_dir / (config.inputs.mod_filename + ".nrm");
|
|
|
|
#ifdef _WIN32
|
|
std::filesystem::path temp_zip_path = output_path;
|
|
temp_zip_path.replace_extension(".zip");
|
|
std::string compression_level = store_only ? "NoCompression" : "Optimal";
|
|
std::string command_string = fmt::format("powershell -command Compress-Archive -Force -CompressionLevel {} -DestinationPath '{}' -Path '{}','{}','{}'",
|
|
compression_level, temp_zip_path.string(), (output_dir / symbol_filename).string(), (output_dir / binary_filename).string(), (output_dir / manifest_filename).string());
|
|
|
|
for (const auto& cur_file : config.inputs.additional_files) {
|
|
command_string += fmt::format(",'{}'", cur_file.string());
|
|
}
|
|
|
|
STARTUPINFOA si{};
|
|
PROCESS_INFORMATION pi{};
|
|
|
|
ZeroMemory( &si, sizeof(si) );
|
|
si.cb = sizeof(si);
|
|
ZeroMemory( &pi, sizeof(pi) );
|
|
|
|
std::vector<char> command_string_buffer;
|
|
command_string_buffer.resize(command_string.size() + 1);
|
|
std::copy(command_string.begin(), command_string.end(), command_string_buffer.begin());
|
|
command_string_buffer[command_string.size()] = '\x00';
|
|
|
|
if (!CreateProcessA(NULL, command_string_buffer.data(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
|
|
fmt::print(stderr, "Process creation failed {}\n", GetLastError());
|
|
return false;
|
|
}
|
|
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
|
|
DWORD ec;
|
|
GetExitCodeProcess(pi.hProcess, &ec);
|
|
|
|
CloseHandle(pi.hProcess);
|
|
CloseHandle(pi.hThread);
|
|
|
|
if (ec != EXIT_SUCCESS) {
|
|
fmt::print(stderr, "Compress-Archive failed with exit code {}\n", ec);
|
|
return false;
|
|
}
|
|
|
|
std::error_code rename_ec;
|
|
std::filesystem::rename(temp_zip_path, output_path, rename_ec);
|
|
if (rename_ec != std::error_code{}) {
|
|
fmt::print(stderr, "Failed to rename temporary zip to output path\n");
|
|
return false;
|
|
}
|
|
#else
|
|
std::string args_string{};
|
|
std::vector<size_t> arg_positions{};
|
|
|
|
// Adds an argument with a null terminator to args_string, which is used as a buffer to hold null terminated arguments.
|
|
// Also adds the argument's offset into the string into arg_positions for creating the array of character pointers for the exec.
|
|
auto add_arg = [&args_string, &arg_positions](const std::string& arg){
|
|
arg_positions.emplace_back(args_string.size());
|
|
args_string += (arg + '\x00');
|
|
};
|
|
|
|
add_arg("zip"); // The program name (argv[0]).
|
|
add_arg("-q"); // Quiet mode.
|
|
add_arg(store_only ? "-0" : "-9"); // Store only or maximum compression level.
|
|
add_arg("-MM"); // Error if any files aren't found.
|
|
add_arg("-j"); // Junk the paths (store just as the provided filename).
|
|
add_arg("-T"); // Test zip integrity.
|
|
add_arg(output_path.string());
|
|
add_arg((output_dir / symbol_filename).string());
|
|
add_arg((output_dir / binary_filename).string());
|
|
add_arg((output_dir / manifest_filename).string());
|
|
|
|
// Add arguments for every additional file in the archive.
|
|
for (const auto& cur_file : config.inputs.additional_files) {
|
|
add_arg(cur_file.string());
|
|
}
|
|
|
|
// Build the argument char* array in a vector.
|
|
std::vector<char*> arg_pointers{};
|
|
for (size_t arg_index = 0; arg_index < arg_positions.size(); arg_index++) {
|
|
arg_pointers.emplace_back(args_string.data() + arg_positions[arg_index]);
|
|
}
|
|
|
|
// Termimate the argument list with a null pointer.
|
|
arg_pointers.emplace_back(nullptr);
|
|
|
|
// Delete the output file if it exists already.
|
|
std::filesystem::remove(output_path);
|
|
|
|
// Fork-exec to run zip.
|
|
pid_t pid = fork();
|
|
if (pid == -1) {
|
|
fmt::print(stderr, "Failed to run \"zip\"\n");
|
|
return false;
|
|
}
|
|
else if (pid == 0) {
|
|
// This is the child process, so exec zip with the arguments.
|
|
if (execvp(arg_pointers[0], arg_pointers.data()) == -1) {
|
|
fmt::print(stderr, "Failed to run \"zip\" ({})\n", errno);
|
|
exit(-1);
|
|
}
|
|
}
|
|
else {
|
|
// This is the parent process, so wait for the child process to complete and check its exit code.
|
|
int status;
|
|
if (waitpid(pid, &status, 0) == (pid_t)-1) {
|
|
fmt::print(stderr, "Waiting for \"zip\" failed\n");
|
|
return false;
|
|
}
|
|
if (status != EXIT_SUCCESS) {
|
|
fmt::print(stderr, "\"zip\" failed with exit code {}\n", status);
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, const char** argv) {
|
|
bool store_only = false;
|
|
int positional_start = 1;
|
|
|
|
// Parse optional flags.
|
|
for (int i = 1; i < argc; i++) {
|
|
std::string_view arg{argv[i]};
|
|
if (arg == "--store-only") {
|
|
store_only = true;
|
|
positional_start = i + 1;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (argc - positional_start != 2) {
|
|
fmt::print("Usage: {} [--store-only] [mod toml] [output folder]\n", argv[0]);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
bool config_good;
|
|
std::filesystem::path output_dir{ argv[positional_start + 1] };
|
|
|
|
if (!std::filesystem::exists(output_dir)) {
|
|
fmt::print(stderr, "Specified output folder does not exist!\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (!std::filesystem::is_directory(output_dir)) {
|
|
fmt::print(stderr, "Specified output folder is not a folder!\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
ModConfig config = parse_mod_config(argv[positional_start], config_good);
|
|
|
|
if (!config_good) {
|
|
fmt::print(stderr, "Failed to read mod config file: {}\n", argv[positional_start]);
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
N64Recomp::Context context{};
|
|
|
|
// Import symbols from symbols files that were provided.
|
|
{
|
|
// Create a new temporary context to read the function reference symbol file into, since it's the same format as the recompilation symbol file.
|
|
std::vector<uint8_t> dummy_rom{};
|
|
N64Recomp::Context reference_context{};
|
|
if (!N64Recomp::Context::from_symbol_file(config.inputs.func_reference_syms_file_path, std::move(dummy_rom), reference_context, false)) {
|
|
fmt::print(stderr, "Failed to load provided function reference symbol file\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
// Use the reference context to build a reference symbol list for the actual context.
|
|
if (!context.import_reference_context(reference_context)) {
|
|
fmt::print(stderr, "Internal error: failed to import reference context. Please report this issue.\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
for (const std::filesystem::path& cur_data_sym_path : config.inputs.data_reference_syms_file_paths) {
|
|
if (!context.read_data_reference_syms(cur_data_sym_path)) {
|
|
fmt::print(stderr, "Failed to load provided data reference symbol file: {}\n", cur_data_sym_path.string());
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
// Copy the dependencies and optional dependencies from the config into the context.
|
|
context.add_dependencies(config.manifest.dependencies);
|
|
context.add_dependencies(config.manifest.optional_dependencies);
|
|
|
|
N64Recomp::ElfParsingConfig elf_config {
|
|
.bss_section_suffix = {},
|
|
.manually_sized_funcs = {},
|
|
.relocatable_sections = {},
|
|
.has_entrypoint = false,
|
|
.entrypoint_address = 0,
|
|
.use_absolute_symbols = false,
|
|
.unpaired_lo16_warnings = false,
|
|
.all_sections_relocatable = true,
|
|
.use_mdebug = false,
|
|
};
|
|
bool dummy_found_entrypoint;
|
|
N64Recomp::DataSymbolMap dummy_syms_map;
|
|
bool elf_good = N64Recomp::Context::from_elf_file(config.inputs.elf_path, context, elf_config, false, dummy_syms_map, dummy_found_entrypoint);
|
|
|
|
if (!elf_good) {
|
|
fmt::print(stderr, "Failed to parse mod elf\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
if (context.sections.size() == 0) {
|
|
fmt::print(stderr, "No sections found in mod elf\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
bool mod_context_good;
|
|
N64Recomp::Context mod_context = build_mod_context(context, mod_context_good);
|
|
if (!mod_context_good) {
|
|
fmt::print(stderr, "Failed to create mod context\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
std::vector<uint8_t> symbols_bin = N64Recomp::symbols_to_bin_v1(mod_context);
|
|
if (symbols_bin.empty()) {
|
|
fmt::print(stderr, "Failed to create symbol file\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
std::filesystem::path output_syms_path = output_dir / symbol_filename;
|
|
std::filesystem::path output_binary_path = output_dir / binary_filename;
|
|
std::filesystem::path output_manifest_path = output_dir / manifest_filename;
|
|
|
|
// Write the symbol file.
|
|
{
|
|
std::ofstream output_syms_file{ output_syms_path, std::ios::binary };
|
|
output_syms_file.write(reinterpret_cast<const char*>(symbols_bin.data()), symbols_bin.size());
|
|
}
|
|
|
|
// Write the binary file.
|
|
{
|
|
std::ofstream output_binary_file{ output_binary_path, std::ios::binary };
|
|
output_binary_file.write(reinterpret_cast<const char*>(mod_context.rom.data()), mod_context.rom.size());
|
|
}
|
|
|
|
// Write the manifest.
|
|
write_manifest(output_manifest_path, config.manifest);
|
|
|
|
// Create the zip.
|
|
if (!create_mod_zip(output_dir, config, store_only)) {
|
|
fmt::print(stderr, "Failed to create mod file.\n");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|