diff --git a/OfflineModRecomp/main.cpp b/OfflineModRecomp/main.cpp index d795d39..d128c8f 100644 --- a/OfflineModRecomp/main.cpp +++ b/OfflineModRecomp/main.cpp @@ -24,17 +24,9 @@ static std::vector read_file(const std::filesystem::path& path, bool& f return ret; } -const std::filesystem::path func_reference_syms_file_path { - "C:/n64/MMRecompTestMod/Zelda64RecompSyms/mm.us.rev1.syms.toml" -}; -const std::vector data_reference_syms_file_paths { - "C:/n64/MMRecompTestMod/Zelda64RecompSyms/mm.us.rev1.datasyms.toml", - "C:/n64/MMRecompTestMod/Zelda64RecompSyms/mm.us.rev1.datasyms_static.toml" -}; - int main(int argc, const char** argv) { - if (argc != 4) { - printf("Usage: %s [mod symbol file] [ROM] [output C file]\n", argv[0]); + if (argc != 5) { + printf("Usage: %s [mod symbol file] [mod binary file] [recomp symbols file] [output C file]\n", argv[0]); return EXIT_SUCCESS; } bool found; @@ -54,7 +46,7 @@ int main(int argc, const char** argv) { std::vector dummy_rom{}; N64Recomp::Context reference_context{}; - if (!N64Recomp::Context::from_symbol_file(func_reference_syms_file_path, std::move(dummy_rom), reference_context, false)) { + if (!N64Recomp::Context::from_symbol_file(argv[3], std::move(dummy_rom), reference_context, false)) { printf("Failed to load provided function reference symbol file\n"); return EXIT_FAILURE; } @@ -73,12 +65,14 @@ int main(int argc, const char** argv) { N64Recomp::Context mod_context; - N64Recomp::ModSymbolsError error = N64Recomp::parse_mod_symbols(symbol_data_span, rom_data, sections_by_vrom, reference_context, mod_context); + N64Recomp::ModSymbolsError error = N64Recomp::parse_mod_symbols(symbol_data_span, rom_data, sections_by_vrom, mod_context); if (error != N64Recomp::ModSymbolsError::Good) { fprintf(stderr, "Error parsing mod symbols: %d\n", (int)error); return EXIT_FAILURE; } + mod_context.import_reference_context(reference_context); + // Populate R_MIPS_26 reloc symbol indices. Start by building a map of vram address to matching reference symbols. std::unordered_map> reference_symbols_by_vram{}; for (size_t reference_symbol_index = 0; reference_symbol_index < mod_context.num_regular_reference_symbols(); reference_symbol_index++) { @@ -124,7 +118,7 @@ int main(int argc, const char** argv) { std::vector> static_funcs_by_section{}; static_funcs_by_section.resize(mod_context.sections.size()); - std::ofstream output_file { argv[3] }; + std::ofstream output_file { argv[4] }; RabbitizerConfig_Cfg.pseudos.pseudoMove = false; RabbitizerConfig_Cfg.pseudos.pseudoBeqz = false; @@ -134,6 +128,9 @@ int main(int argc, const char** argv) { output_file << "#include \"mod_recomp.h\"\n\n"; + // Write the API version. + output_file << "RECOMP_EXPORT uint32_t recomp_api_version = 1;\n\n"; + output_file << "// Values populated by the runtime:\n\n"; // Write import function pointer array and defines (i.e. `#define testmod_inner_import imported_funcs[0]`) @@ -143,7 +140,8 @@ int main(int argc, const char** argv) { const auto& import = mod_context.import_symbols[import_index]; output_file << "#define " << import.base.name << " imported_funcs[" << import_index << "]\n"; } - output_file << "RECOMP_EXPORT recomp_func_t* imported_funcs[" << num_imports << "] = {};\n"; + + output_file << "RECOMP_EXPORT recomp_func_t* imported_funcs[" << std::max(size_t{1}, num_imports) << "] = {0};\n"; output_file << "\n"; // Use reloc list to write reference symbol function pointer array and defines (i.e. `#define func_80102468 reference_symbol_funcs[0]`) @@ -158,11 +156,12 @@ int main(int argc, const char** argv) { } } } - output_file << "RECOMP_EXPORT recomp_func_t* reference_symbol_funcs[" << num_reference_symbols << "] = {};\n\n"; + // C doesn't allow 0-sized arrays, so always add at least one member to all arrays. The actual size will be pulled from the mod symbols. + output_file << "RECOMP_EXPORT recomp_func_t* reference_symbol_funcs[" << std::max(size_t{1},num_reference_symbols) << "] = {0};\n\n"; // Write provided event array (maps internal event indices to global ones). - output_file << "// Mapping of internal event indices to global ones.\n"; - output_file << "RECOMP_EXPORT uint32_t event_indices[" << mod_context.event_symbols.size() <<"] = {};\n\n"; + output_file << "// Base global event index for this mod's events.\n"; + output_file << "RECOMP_EXPORT uint32_t base_event_index;\n\n"; // Write the event trigger function pointer. output_file << "// Pointer to the runtime function for triggering events.\n"; @@ -179,11 +178,16 @@ int main(int argc, const char** argv) { // Write the local section addresses pointer array. size_t num_sections = mod_context.sections.size(); output_file << "// Array of this mod's loaded section addresses.\n"; - output_file << "RECOMP_EXPORT int32_t section_addresses[" << num_sections << "] = {};\n\n"; + output_file << "RECOMP_EXPORT int32_t section_addresses[" << std::max(size_t{1}, num_sections) << "] = {0};\n\n"; + + // Create a set of the export indices to avoid renaming them. + std::unordered_set export_indices{mod_context.exported_funcs.begin(), mod_context.exported_funcs.end()}; for (size_t func_index = 0; func_index < mod_context.functions.size(); func_index++) { auto& func = mod_context.functions[func_index]; - func.name = "mod_func_" + std::to_string(func_index); + if (!export_indices.contains(func_index)) { + func.name = "mod_func_" + std::to_string(func_index); + } N64Recomp::recompile_function(mod_context, func, output_file, static_funcs_by_section, true); } diff --git a/RecompModTool/main.cpp b/RecompModTool/main.cpp index 8f6a61b..065bf2b 100644 --- a/RecompModTool/main.cpp +++ b/RecompModTool/main.cpp @@ -3,30 +3,64 @@ #include #include #include +#include #include "fmt/format.h" +#include "fmt/ostream.h" #include "n64recomp.h" #include -struct ModConfig { - std::filesystem::path output_syms_path; - std::filesystem::path output_binary_path; +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#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 = "manifest.json"; + +struct ModManifest { + std::string mod_id; + std::string version_string; + std::vector authors; + std::string game_id; + std::string minimum_recomp_version; + std::unordered_map> native_libraries; + std::vector dependencies; + std::vector full_dependency_strings; +}; + +struct ModInputs { std::filesystem::path elf_path; std::filesystem::path func_reference_syms_file_path; std::vector data_reference_syms_file_paths; - std::vector dependencies; + std::vector 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 parse_version_string(std::string_view str, uint8_t& major, uint8_t& minor, uint8_t& patch) { +static bool validate_version_string(std::string_view str, bool& has_label) { std::array 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); @@ -47,65 +81,273 @@ static bool parse_version_string(std::string_view str, uint8_t& major, uint8_t& 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 all 3 parsed correctly. + // 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) || !did_parse(2)) { + if (!did_parse(0) || !did_parse(1)) { return false; } - return true; -} - -static bool parse_dependency_string(const std::string& val, N64Recomp::Dependency& dep) { - N64Recomp::Dependency ret; - size_t id_pos = 0; - size_t id_length = 0; - - size_t colon_pos = val.find(':'); - if (colon_pos == std::string::npos) { - id_length = val.size(); - ret.major_version = 0; - ret.minor_version = 0; - ret.patch_version = 0; + // Check that the third had a successful parse, but not necessarily read all the characters. + if (parse_results[2].ec != std::errc{}) { + return false; } - else { - id_length = colon_pos; - uint8_t major, minor, patch; - if (!parse_version_string(std::string_view{val.begin() + colon_pos + 1, val.end()}, major, minor, patch)) { + + // 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; } - ret.major_version = major; - ret.minor_version = minor; - ret.patch_version = patch; + } + else { + has_label = false; } - ret.mod_id = val.substr(id_pos, id_length); - - dep = std::move(ret); return true; } -static std::vector get_toml_path_array(const toml::array* toml_array, const std::filesystem::path& basedir) { +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 +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 opt = value_node->value_exact(); + 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 get_toml_path_array(const toml::array& toml_array, const std::filesystem::path& basedir) { std::vector ret; // Reserve room for all the funcs in the map. - ret.reserve(toml_array->size()); - toml_array->for_each([&ret, &basedir](auto&& el) { + ret.reserve(toml_array.size()); + toml_array.for_each([&ret, &basedir](auto&& el) { if constexpr (toml::is_string) { - ret.emplace_back(concat_if_not_empty(basedir, el.ref())); + ret.emplace_back(concat_if_not_empty(basedir, el.template ref())); } else { - throw toml::parse_error("Invalid type for data reference symbol file entry", el.source()); + throw toml::parse_error("Invalid type for file entry", el.source()); } }); return ret; } +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(manifest_table, "id", true); + + // Mod version + ret.version_string = read_toml_value(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()); + } + + // 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) { + ret.authors.emplace_back(el.template ref()); + } + else { + throw toml::parse_error("Invalid type for author entry", el.source()); + } + }); + + // Game ID + ret.game_id = read_toml_value(manifest_table, "game_id", true); + + // Minimum recomp version + ret.minimum_recomp_version = read_toml_value(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) { + const toml::table& el_table = *el.as_table(); + std::string_view library_name = read_toml_value(el_table, "name", true); + const toml::array funcs_array = read_toml_array(el_table, "funcs", true); + std::vector cur_funcs{}; + funcs_array.for_each([&ret, &cur_funcs](const auto& func_el) { + if constexpr (toml::is_string) { + cur_funcs.emplace_back(func_el.template ref()); + } + 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) { + size_t dependency_id_length; + bool dependency_version_has_label; + if (!validate_dependency_string(el.template ref(), 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().substr(0, dependency_id_length); + ret.dependencies.emplace_back(dependency_id); + ret.full_dependency_strings.emplace_back(el.template ref()); + } + else { + throw toml::parse_error("Invalid type for dependency entry", el.source()); + } + }); + } + + return ret; +} + +ModInputs parse_mod_config_inputs(const std::filesystem::path& basedir, const toml::table& inputs_table) { + ModInputs ret; + + // Elf file + std::optional elf_path_opt = inputs_table["elf_path"].value(); + 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()); + } + + // Function reference symbols file + std::optional func_reference_syms_file_opt = inputs_table["func_reference_syms_file"].value(); + 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; @@ -116,84 +358,33 @@ ModConfig parse_mod_config(const std::filesystem::path& config_path, bool& good) toml_data = toml::parse_file(config_path.native()); std::filesystem::path basedir = config_path.parent_path(); - const auto config_data = toml_data["config"]; + // 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(); - // Output symbol file path - std::optional output_syms_path_opt = config_data["output_syms_path"].value(); - if (output_syms_path_opt.has_value()) { - ret.output_syms_path = concat_if_not_empty(basedir, output_syms_path_opt.value()); + // 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{}); } - else { - throw toml::parse_error("Mod toml is missing output symbol file path", config_data.node()->source()); + 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(); - // Output binary file path - std::optional output_binary_path_opt = config_data["output_binary_path"].value(); - if (output_binary_path_opt.has_value()) { - ret.output_binary_path = concat_if_not_empty(basedir, output_binary_path_opt.value()); - } - else { - throw toml::parse_error("Mod toml is missing output binary file path", config_data.node()->source()); - } - - // Elf file - std::optional elf_path_opt = config_data["elf_path"].value(); - 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 is missing elf file", config_data.node()->source()); - } - - // Function reference symbols file - std::optional func_reference_syms_file_opt = config_data["func_reference_syms_file"].value(); - 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 is missing function reference symbol file", config_data.node()->source()); - } - - // Data reference symbols files - toml::node_view data_reference_syms_file_data = config_data["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 is missing data reference symbol file list", config_data.node()->source()); - } - else { - throw toml::parse_error("Invalid data reference symbol file list", data_reference_syms_file_data.node()->source()); - } - } - - // Dependency list (optional) - toml::node_view dependency_data = config_data["dependencies"]; - if (dependency_data.is_array()) { - const toml::array* dependency_array = dependency_data.as_array(); - // Reserve room for all the dependencies. - ret.dependencies.reserve(dependency_array->size()); - dependency_array->for_each([&ret](auto&& el) { - if constexpr (toml::is_string) { - N64Recomp::Dependency dep; - if (!parse_dependency_string(el.ref(), dep)) { - throw toml::parse_error("Invalid dependency entry", el.source()); - } - ret.dependencies.emplace_back(std::move(dep)); - } - else { - throw toml::parse_error("Invalid toml type for dependency", el.source()); - } - }); - } - else if (dependency_data) { - throw toml::parse_error("Invalid mod dependency list", dependency_data.node()->source()); - } + // 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: " << *err.source().path << " (" << err.source().begin << "):\n" << err.description() << std::endl; + std::cerr << "Syntax error parsing toml: " << config_path << " (" << err.source().begin << "):\n" << err.description() << std::endl; return {}; } @@ -215,7 +406,7 @@ bool parse_callback_name(std::string_view data, std::string& dependency_name, st 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_name(dependency_name_view)) { + if (!N64Recomp::validate_mod_id(dependency_name_view)) { return false; } @@ -224,6 +415,59 @@ bool parse_callback_name(std::string_view data, std::string& dependency_name, st return true; } +void print_vector_elements(std::ostream& output_file, const std::vector& vec, bool compact) { + char separator = compact ? ' ' : '\n'; + for (size_t i = 0; i < vec.size(); i++) { + const std::string& val = vec[i]; + fmt::print(output_file, "{}\"{}\"{}{}", + compact ? "" : " ", val, i == vec.size() - 1 ? "" : ",", separator); + } +} + +void write_manifest(const std::filesystem::path& path, const ModManifest& manifest) { + std::ofstream output_file(path); + + fmt::print(output_file, + "{{\n" + " \"game_id\": \"{}\",\n" + " \"id\": \"{}\",\n" + " \"version\": \"{}\",\n" + " \"authors\": [\n", + manifest.game_id, manifest.mod_id, manifest.version_string); + + print_vector_elements(output_file, manifest.authors, false); + + fmt::print(output_file, + " ],\n" + " \"minimum_recomp_version\": \"{}\"", + manifest.minimum_recomp_version); + + if (!manifest.native_libraries.empty()) { + fmt::print(output_file, ",\n" + " \"native_libraries\": {{\n"); + size_t library_index = 0; + for (const auto& [library, funcs] : manifest.native_libraries) { + fmt::print(output_file, " \"{}\": [ ", + library); + print_vector_elements(output_file, funcs, true); + fmt::print(output_file, "]{}\n", + library_index == manifest.native_libraries.size() - 1 ? "" : ","); + library_index++; + } + fmt::print(output_file, " }}"); + } + + if (!manifest.full_dependency_strings.empty()) { + fmt::print(output_file, ",\n" + " \"dependencies\": [\n"); + print_vector_elements(output_file, manifest.full_dependency_strings, false); + fmt::print(output_file, " ]"); + } + + + fmt::print(output_file, "\n}}\n"); +} + N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bool& good) { N64Recomp::Context ret{}; good = false; @@ -253,7 +497,6 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo ret.rom = input_context.rom; // Copy the dependency data from the input context. - ret.dependencies = input_context.dependencies; ret.dependencies_by_name = input_context.dependencies_by_name; ret.import_symbols = input_context.import_symbols; ret.dependency_events = input_context.dependency_events; @@ -325,8 +568,11 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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. + + // 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. @@ -340,30 +586,9 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo } } } - // Skip the functions and relocs in this section if it's an import section, instead opting to create - // import symbols from the section's functions. - else if (import_section) { - for (const auto& input_func_index : cur_section_funcs) { - const auto& cur_func = input_context.functions[input_func_index]; - std::string dependency_name = cur_section.name.substr(N64Recomp::ImportSectionPrefix.size()); - if (!N64Recomp::validate_mod_name(dependency_name)) { - fmt::print("Failed to import function {} as {} is an invalid mod name.\n", - cur_func.name, dependency_name); - return {}; - } - - size_t dependency_index; - if (!ret.find_dependency(dependency_name, dependency_index)) { - fmt::print("Failed to import function {} from mod {} as the mod is not a registered dependency.\n", - cur_func.name, dependency_name); - return {}; - } - - ret.add_import_symbol(cur_func.name, dependency_index); - } - } - // Normal section, copy the functions and relocs over. - else { + // 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]; @@ -377,14 +602,14 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo // Check that the function being patched exists in the original reference symbols. if (!original_func_exists) { - fmt::print("Function {} is marked as a patch but doesn't exist in the original ROM.\n", cur_func.name); + 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("Function {0} is marked as a patch, but {0} was a variable in the original ROM.\n", cur_func.name); + fmt::print(stderr, "Function {0} is marked as a patch, but {0} was a variable in the original ROM.\n", cur_func.name); return {}; } @@ -413,27 +638,27 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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("Invalid mod name or event name for callback function {}.\n", + 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("Failed to register callback {} to event {} from mod {} as the mod is not a registered dependency.\n", + 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("Internal error: Failed to register event {} for dependency {}. Please report this issue.\n", + 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("Internal error: Failed to add callback {} to event {} in dependency {}. Please report this issue.\n", + 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 {}; } @@ -499,7 +724,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo reloc_word |= reloc_target_address & 0xFFFF; break; default: - fmt::print("Unsupported or unknown relocation type {} in reloc at address 0x{:08X} in section {}.\n", + 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 {}; } @@ -519,7 +744,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo // 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("Symbol {} is an event and cannot have its address taken.\n", + fmt::print(stderr, "Symbol {} is an event and cannot have its address taken.\n", cur_section.name); return {}; } @@ -527,7 +752,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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("Internal error: Failed to find event symbol in section {} with offset 0x{:08X} (vram 0x{:08X}). Please report this issue.\n", + 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 {}; } @@ -556,7 +781,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo // 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("Symbol {} is an import and cannot have its address taken.\n", + fmt::print(stderr, "Symbol {} is an import and cannot have its address taken.\n", cur_section.name); return {}; } @@ -564,7 +789,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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("Internal error: Failed to find import symbol in section {} with offset 0x{:08X} (vram 0x{:08X}). Please report this issue.\n", + 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 {}; } @@ -575,7 +800,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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("Failed to import function {} from mod {} as the mod is not a registered dependency.\n", + fmt::print(stderr, "Failed to import function {} from mod {} as the mod is not a registered dependency.\n", target_function.name, dependency_name); return {}; } @@ -603,7 +828,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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("Reloc at address 0x{:08X} in section {} points to a different section.\n", + fmt::print(stderr, "Reloc at address 0x{:08X} in section {} points to a different section.\n", cur_reloc.address, cur_section.name); return {}; } @@ -630,7 +855,7 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo 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("Reloc at address 0x{:08X} references section {}, which didn't get mapped to an output section\n", + 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 {}; } @@ -652,13 +877,140 @@ N64Recomp::Context build_mod_context(const N64Recomp::Context& input_context, bo return ret; } +bool create_mod_zip(const std::filesystem::path& output_dir, const ModConfig& config) { + std::filesystem::path output_path = output_dir / (config.manifest.mod_id + "-" + config.manifest.version_string + ".nrm"); + +#ifdef _WIN32 + std::filesystem::path temp_zip_path = output_path; + temp_zip_path.replace_extension(".zip"); + std::string command_string = fmt::format("powershell -command Compress-Archive -Force -CompressionLevel Optimal -DestinationPath \"{}\" -Path \"{}\",\"{}\",\"{}\"", + 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 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 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("-9"); // 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 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. + execvp(arg_pointers[0], arg_pointers.data()); + } + 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) { - if (argc != 2) { - fmt::print("Usage: {} [mod toml]\n", argv[0]); + if (argc != 3) { + fmt::print("Usage: {} [mod toml] [output folder]\n", argv[0]); return EXIT_SUCCESS; } bool config_good; + std::filesystem::path output_dir{ argv[2] }; + + 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[1], config_good); if (!config_good) { @@ -673,7 +1025,7 @@ int main(int argc, const char** argv) { // 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 dummy_rom{}; N64Recomp::Context reference_context{}; - if (!N64Recomp::Context::from_symbol_file(config.func_reference_syms_file_path, std::move(dummy_rom), reference_context, false)) { + 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; } @@ -685,7 +1037,7 @@ int main(int argc, const char** argv) { } } - for (const std::filesystem::path& cur_data_sym_path : config.data_reference_syms_file_paths) { + 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; @@ -693,7 +1045,7 @@ int main(int argc, const char** argv) { } // Copy the dependencies from the config into the context. - context.add_dependencies(config.dependencies); + context.add_dependencies(config.manifest.dependencies); N64Recomp::ElfParsingConfig elf_config { .bss_section_suffix = {}, @@ -707,7 +1059,7 @@ int main(int argc, const char** argv) { }; bool dummy_found_entrypoint; N64Recomp::DataSymbolMap dummy_syms_map; - bool elf_good = N64Recomp::Context::from_elf_file(config.elf_path, context, elf_config, false, dummy_syms_map, dummy_found_entrypoint); + 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"); @@ -727,11 +1079,30 @@ int main(int argc, const char** argv) { return EXIT_FAILURE; } - std::ofstream output_syms_file{ config.output_syms_path, std::ios::binary }; - output_syms_file.write(reinterpret_cast(symbols_bin.data()), symbols_bin.size()); + 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; - std::ofstream output_binary_file{ config.output_binary_path, std::ios::binary }; - output_binary_file.write(reinterpret_cast(mod_context.rom.data()), mod_context.rom.size()); + // Write the symbol file. + { + std::ofstream output_syms_file{ output_syms_path, std::ios::binary }; + output_syms_file.write(reinterpret_cast(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(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)) { + fmt::print(stderr, "Failed to create mod file.\n"); + return EXIT_FAILURE; + } return EXIT_SUCCESS; } diff --git a/include/n64recomp.h b/include/n64recomp.h index aaaa6a5..66ce06b 100644 --- a/include/n64recomp.h +++ b/include/n64recomp.h @@ -70,9 +70,9 @@ namespace N64Recomp { constexpr std::string_view ImportSectionPrefix = ".recomp_import."; constexpr std::string_view CallbackSectionPrefix = ".recomp_callback."; - // Special mod names. - constexpr std::string_view ModSelf = "."; - constexpr std::string_view ModBaseRecomp = "*"; + // Special dependency names. + constexpr std::string_view DependencySelf = "."; + constexpr std::string_view DependencyBaseRecomp = "*"; struct Section { uint32_t rom_addr = 0; @@ -128,13 +128,6 @@ namespace N64Recomp { extern const std::unordered_set ignored_funcs; extern const std::unordered_set renamed_funcs; - struct Dependency { - uint8_t major_version; - uint8_t minor_version; - uint8_t patch_version; - std::string mod_id; - }; - struct ImportSymbol { ReferenceSymbol base; size_t dependency_index; @@ -202,8 +195,6 @@ namespace N64Recomp { //// Mod dependencies and their symbols //// Imported values - // List of dependencies. - std::vector dependencies; // Mapping of dependency name to dependency index. std::unordered_map dependencies_by_name; // List of symbols imported from dependencies. @@ -235,54 +226,55 @@ namespace N64Recomp { Context() = default; - bool add_dependency(const std::string& id, uint8_t major_version, uint8_t minor_version, uint8_t patch_version) { + bool add_dependency(const std::string& id) { if (dependencies_by_name.contains(id)) { return false; } - size_t dependency_index = dependencies.size(); - dependencies.emplace_back(N64Recomp::Dependency { - .major_version = major_version, - .minor_version = minor_version, - .patch_version = patch_version, - .mod_id = id - }); + size_t dependency_index = dependencies_by_name.size(); dependencies_by_name.emplace(id, dependency_index); - dependency_events_by_name.resize(dependencies.size()); - dependency_imports_by_name.resize(dependencies.size()); + dependency_events_by_name.resize(dependencies_by_name.size()); + dependency_imports_by_name.resize(dependencies_by_name.size()); return true; } - bool add_dependencies(const std::vector& new_dependencies) { - dependencies.reserve(dependencies.size() + new_dependencies.size()); + bool add_dependencies(const std::vector& new_dependencies) { dependencies_by_name.reserve(dependencies_by_name.size() + new_dependencies.size()); // Check if any of the dependencies already exist and fail if so. - for (const Dependency& dep : new_dependencies) { - if (dependencies_by_name.contains(dep.mod_id)) { + for (const std::string& dep : new_dependencies) { + if (dependencies_by_name.contains(dep)) { return false; } } - for (const Dependency& dep : new_dependencies) { - size_t dependency_index = dependencies.size(); - dependencies.emplace_back(dep); - dependencies_by_name.emplace(dep.mod_id, dependency_index); + for (const std::string& dep : new_dependencies) { + size_t dependency_index = dependencies_by_name.size(); + dependencies_by_name.emplace(dep, dependency_index); } - dependency_events_by_name.resize(dependencies.size()); - dependency_imports_by_name.resize(dependencies.size()); + dependency_events_by_name.resize(dependencies_by_name.size()); + dependency_imports_by_name.resize(dependencies_by_name.size()); return true; } bool find_dependency(const std::string& mod_id, size_t& dependency_index) { auto find_it = dependencies_by_name.find(mod_id); - if (find_it == dependencies_by_name.end()) { - return false; + if (find_it != dependencies_by_name.end()) { + dependency_index = find_it->second; + } + else { + // Handle special dependency names. + if (mod_id == DependencySelf || mod_id == DependencyBaseRecomp) { + add_dependency(mod_id); + dependency_index = dependencies_by_name[mod_id]; + } + else { + return false; + } } - dependency_index = find_it->second; return true; } @@ -419,7 +411,7 @@ namespace N64Recomp { } bool find_import_symbol(const std::string& symbol_name, size_t dependency_index, SymbolReference& ref_out) const { - if (dependency_index >= dependencies.size()) { + if (dependency_index >= dependencies_by_name.size()) { return false; } @@ -467,7 +459,7 @@ namespace N64Recomp { } bool add_dependency_event(const std::string& event_name, size_t dependency_index, size_t& dependency_event_index) { - if (dependency_index >= dependencies.size()) { + if (dependency_index >= dependencies_by_name.size()) { return false; } @@ -535,21 +527,40 @@ namespace N64Recomp { FunctionOutOfBounds, }; - ModSymbolsError parse_mod_symbols(std::span data, std::span binary, const std::unordered_map& sections_by_vrom, const Context& reference_context, Context& context_out); + ModSymbolsError parse_mod_symbols(std::span data, std::span binary, const std::unordered_map& sections_by_vrom, Context& context_out); std::vector symbols_to_bin_v1(const Context& mod_context); - inline bool validate_mod_name(std::string_view str) { - // Disallow mod names with a colon in them, since you can't specify that in a dependency string orin callbacks. - for (char c : str) { - if (c == ':') { + inline bool validate_mod_id(std::string_view str) { + // Disallow empty ids. + if (str.size() == 0) { + return false; + } + + // Allow special dependency ids. + if (str == N64Recomp::DependencySelf || str == N64Recomp::DependencyBaseRecomp) { + return true; + } + + // These following rules basically describe C identifiers. There's no specific reason to enforce them besides colon (currently), + // so this is just to prevent "weird" mod ids. + + // Check the first character, which must be alphabetical or an underscore. + if (!isalpha(str[0]) && str[0] != '_') { + return false; + } + + // Check the remaining characters, which can be alphanumeric or underscore. + for (char c : str.substr(1)) { + if (!isalnum(c) && c != '_') { return false; } } + return true; } - inline bool validate_mod_name(const std::string& str) { - return validate_mod_name(std::string_view{str}); + inline bool validate_mod_id(const std::string& str) { + return validate_mod_id(std::string_view{str}); } } diff --git a/src/main.cpp b/src/main.cpp index ff98df3..05b79aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -559,6 +559,16 @@ int main(int argc, char** argv) { "#include \"funcs.h\"\n" "\n", config.recomp_include); + + // Print the extern for the base event index and the define to rename it if exports are allowed. + if (config.allow_exports) { + fmt::print(current_output_file, + "extern uint32_t builtin_base_event_index;\n" + "#define base_event_index builtin_base_event_index\n" + "\n" + ); + } + cur_file_function_count = 0; output_file_count++; }; @@ -571,11 +581,86 @@ int main(int argc, char** argv) { "#include \"funcs.h\"\n" "\n", config.recomp_include); + + // Print the extern for the base event index and the define to rename it if exports are allowed. + if (config.allow_exports) { + fmt::print(current_output_file, + "extern uint32_t builtin_base_event_index;\n" + "#define base_event_index builtin_base_event_index\n" + "\n" + ); + } } else if (config.functions_per_output_file > 1) { open_new_output_file(); } + std::unordered_map function_index_to_event_index{}; + + // If exports are enabled, scan all the relocs and modify ones that point to an event function. + if (config.allow_exports) { + // First, find the event section by scanning for a section with the special name. + bool event_section_found = false; + size_t event_section_index = 0; + uint32_t event_section_vram = 0; + for (size_t section_index = 0; section_index < context.sections.size(); section_index++) { + const auto& section = context.sections[section_index]; + if (section.name == N64Recomp::EventSectionName) { + event_section_found = true; + event_section_index = section_index; + event_section_vram = section.ram_addr; + break; + } + } + + // If an event section was found, proceed with the reloc scanning. + if (event_section_found) { + for (auto& section : context.sections) { + for (auto& reloc : section.relocs) { + // Event symbols aren't reference symbols, since they come from the elf itself. + // Therefore, skip reference symbol relocs. + if (reloc.reference_symbol) { + continue; + } + + // Check if the reloc points to the event section. + if (reloc.target_section == event_section_index) { + // It does, so find the function it's pointing at. + size_t func_index = context.find_function_by_vram_section(reloc.target_section_offset + event_section_vram, event_section_index); + + if (func_index == (size_t)-1) { + exit_failure(fmt::format("Failed to find event function with vram {}.\n", reloc.target_section_offset + event_section_vram)); + } + + // Ensure the reloc is a MIPS_R_26 one before modifying it, since those are the only type allowed to reference + if (reloc.type != N64Recomp::RelocType::R_MIPS_26) { + const auto& function = context.functions[func_index]; + exit_failure(fmt::format("Function {} is an import and cannot have its address taken.\n", + function.name)); + } + + // Check if this function has been assigned an event index already, and assign it if not. + size_t event_index; + auto find_event_it = function_index_to_event_index.find(func_index); + if (find_event_it != function_index_to_event_index.end()) { + event_index = find_event_it->second; + } + else { + event_index = function_index_to_event_index.size(); + function_index_to_event_index.emplace(func_index, event_index); + } + + // Modify the reloc's fields accordingly. + reloc.target_section_offset = 0; + reloc.symbol_index = event_index; + reloc.target_section = N64Recomp::SectionEvent; + reloc.reference_symbol = true; + } + } + } + } + } + std::vector export_function_indices{}; bool failed_strict_mode = false; @@ -840,19 +925,36 @@ int main(int argc, char** argv) { fmt::print(overlay_file, "}};\n"); if (config.allow_exports) { + // Emit the exported function table. fmt::print(overlay_file, "\n" "static FunctionExport export_table[] = {{\n" ); - for (size_t func_index : export_function_indices) { const auto& func = context.functions[func_index]; fmt::print(overlay_file, " {{ \"{}\", 0x{:08X} }},\n", func.name, func.vram); } - // Add a dummy element at the end to ensure the array has a valid length because C doesn't allow zero-size arrays. fmt::print(overlay_file, " {{ NULL, 0 }}\n"); + fmt::print(overlay_file, "}};\n"); + // Emit the event table. + std::vector functions_by_event{}; + functions_by_event.resize(function_index_to_event_index.size()); + for (auto [func_index, event_index] : function_index_to_event_index) { + functions_by_event[event_index] = func_index; + } + + fmt::print(overlay_file, + "\n" + "static const char* event_names[] = {{\n" + ); + for (size_t func_index : functions_by_event) { + const auto& func = context.functions[func_index]; + fmt::print(overlay_file, " \"{}\",\n", func.name); + } + // Add a dummy element at the end to ensure the array has a valid length because C doesn't allow zero-size arrays. + fmt::print(overlay_file, " NULL\n"); fmt::print(overlay_file, "}};\n"); } } diff --git a/src/mod_symbols.cpp b/src/mod_symbols.cpp index 2de7aa1..cd1faa3 100644 --- a/src/mod_symbols.cpp +++ b/src/mod_symbols.cpp @@ -49,9 +49,6 @@ struct RelocV1 { }; struct DependencyV1 { - uint8_t major_version; - uint8_t minor_version; - uint8_t patch_version; uint8_t reserved; uint32_t mod_id_start; uint32_t mod_id_size; @@ -143,7 +140,6 @@ bool parse_v1(std::span data, const std::unordered_map data, const std::unordered_map= mod_context.sections.size()) { printf("Reloc %zu in section %zu references local section %u, but only %zu exist\n", reloc_index, section_index, reloc_target_section, mod_context.sections.size()); + return false; } } else { @@ -269,10 +266,11 @@ bool parse_v1(std::span data, const std::unordered_map string_data_size) { printf("Dependency %zu has a name start of %u and size of %u, which extend beyond the string data's total size of %zu\n", dependency_index, mod_id_start, mod_id_size, string_data_size); + return false; } std::string_view mod_id{ string_data + mod_id_start, string_data + mod_id_start + mod_id_size }; - mod_context.add_dependency(std::string{mod_id}, dependency_in.major_version, dependency_in.minor_version, dependency_in.patch_version); + mod_context.add_dependency(std::string{mod_id}); } const ImportV1* imports = reinterpret_data(data, offset, num_imports); @@ -290,11 +288,13 @@ bool parse_v1(std::span data, const std::unordered_map string_data_size) { printf("Import %zu has a name start of %u and size of %u, which extend beyond the string data's total size of %zu\n", import_index, name_start, name_size, string_data_size); + return false; } if (dependency_index >= num_dependencies) { printf("Import %zu belongs to dependency %u, but only %zu dependencies were specified\n", import_index, dependency_index, num_dependencies); + return false; } std::string_view import_name{ string_data + name_start, string_data + name_start + name_size }; @@ -317,6 +317,7 @@ bool parse_v1(std::span data, const std::unordered_map string_data_size) { printf("Dependency event %zu has a name start of %u and size of %u, which extend beyond the string data's total size of %zu\n", dependency_event_index, name_start, name_size, string_data_size); + return false; } std::string_view dependency_event_name{ string_data + name_start, string_data + name_start + name_size }; @@ -355,15 +356,29 @@ bool parse_v1(std::span data, const std::unordered_map= mod_context.functions.size()) { printf("Export %zu has a function index of %u, but the symbol file only has %zu functions\n", export_index, func_index, mod_context.functions.size()); + return false; } if (name_start + name_size > string_data_size) { printf("Export %zu has a name start of %u and size of %u, which extend beyond the string data's total size of %zu\n", export_index, name_start, name_size, string_data_size); + return false; + } + + std::string_view export_name_view{ string_data + name_start, string_data + name_start + name_size }; + std::string export_name{export_name_view}; + + if (!mod_context.functions[func_index].name.empty()) { + printf("Function %u is exported twice (%s and %s)\n", + func_index, mod_context.functions[func_index].name.c_str(), export_name.c_str()); + return false; } // Add the function to the exported function list. mod_context.exported_funcs[export_index] = func_index; + + // Set the function's name to the export name. + mod_context.functions[func_index].name = std::move(export_name); } const CallbackV1* callbacks = reinterpret_data(data, offset, num_callbacks); @@ -380,15 +395,18 @@ bool parse_v1(std::span data, const std::unordered_map= num_dependency_events) { printf("Callback %zu is connected to dependency event %u, but only %zu dependency events were specified\n", callback_index, dependency_event_index, num_dependency_events); + return false; } if (function_index >= mod_context.functions.size()) { printf("Callback %zu uses function %u, but only %zu functions were specified\n", callback_index, function_index, mod_context.functions.size()); + return false; } if (!mod_context.add_callback(dependency_event_index, function_index)) { printf("Failed to add callback %zu\n", callback_index); + return false; } } @@ -406,6 +424,7 @@ bool parse_v1(std::span data, const std::unordered_map string_data_size) { printf("Event %zu has a name start of %u and size of %u, which extend beyond the string data's total size of %zu\n", event_index, name_start, name_size, string_data_size); + return false; } std::string_view import_name{ string_data + name_start, string_data + name_start + name_size }; @@ -416,13 +435,11 @@ bool parse_v1(std::span data, const std::unordered_map data, std::span binary, const std::unordered_map& sections_by_vrom, const Context& reference_context, Context& mod_context_out) { +N64Recomp::ModSymbolsError N64Recomp::parse_mod_symbols(std::span data, std::span binary, const std::unordered_map& sections_by_vrom, Context& mod_context_out) { size_t offset = 0; mod_context_out = {}; const FileHeader* header = reinterpret_data(data, offset); - mod_context_out.import_reference_context(reference_context); - if (header == nullptr) { return ModSymbolsError::NotASymbolFile; } @@ -485,7 +502,7 @@ std::vector N64Recomp::symbols_to_bin_v1(const N64Recomp::Context& cont vec_put(ret, &header); - size_t num_dependencies = context.dependencies.size(); + size_t num_dependencies = context.dependencies_by_name.size(); size_t num_imported_funcs = context.import_symbols.size(); size_t num_dependency_events = context.dependency_events.size(); @@ -512,15 +529,24 @@ std::vector N64Recomp::symbols_to_bin_v1(const N64Recomp::Context& cont // Build the string data from the exports and imports. size_t strings_start = ret.size(); + + // Order the dependencies by their index. This isn't necessary, but it makes the dependency name order + // in the symbol file match the indices of the dependencies makes debugging easier. + std::vector dependencies_ordered{}; + dependencies_ordered.resize(context.dependencies_by_name.size()); + + for (const auto& [dependency, dependency_index] : context.dependencies_by_name) { + dependencies_ordered[dependency_index] = dependency; + } // Track the start of every dependency's name in the string data. std::vector dependency_name_positions{}; dependency_name_positions.resize(num_dependencies); for (size_t dependency_index = 0; dependency_index < num_dependencies; dependency_index++) { - const Dependency& dependency = context.dependencies[dependency_index]; + const std::string& dependency = dependencies_ordered[dependency_index]; dependency_name_positions[dependency_index] = static_cast(ret.size() - strings_start); - vec_put(ret, dependency.mod_id); + vec_put(ret, dependency); } // Track the start of every imported function's name in the string data. @@ -637,14 +663,11 @@ std::vector N64Recomp::symbols_to_bin_v1(const N64Recomp::Context& cont // Write the dependencies. for (size_t dependency_index = 0; dependency_index < num_dependencies; dependency_index++) { - const Dependency& dependency = context.dependencies[dependency_index]; + const std::string& dependency = dependencies_ordered[dependency_index]; DependencyV1 dependency_out { - .major_version = dependency.major_version, - .minor_version = dependency.minor_version, - .patch_version = dependency.patch_version, .mod_id_start = dependency_name_positions[dependency_index], - .mod_id_size = static_cast(dependency.mod_id.size()) + .mod_id_size = static_cast(dependency.size()) }; vec_put(ret, &dependency_out); diff --git a/src/recompilation.cpp b/src/recompilation.cpp index 26ca4cc..ca66ae6 100644 --- a/src/recompilation.cpp +++ b/src/recompilation.cpp @@ -242,11 +242,11 @@ bool process_instruction(const N64Recomp::Context& context, const N64Recomp::Fun if (reloc_section == N64Recomp::SectionEvent) { needs_link_branch = link_branch; if (indent) { - if (!print_unconditional_branch(" recomp_trigger_event(rdram, ctx, event_indices[{}])", reloc_reference_symbol)) { + if (!print_unconditional_branch(" recomp_trigger_event(rdram, ctx, base_event_index + {})", reloc_reference_symbol)) { return false; } } else { - if (!print_unconditional_branch("recomp_trigger_event(rdram, ctx, event_indices[{}])", reloc_reference_symbol)) { + if (!print_unconditional_branch("recomp_trigger_event(rdram, ctx, base_event_index + {})", reloc_reference_symbol)) { return false; } }