N64ModernRuntime/librecomp/src/mods.cpp
Wiseguy df547d2c06
Some checks failed
validate / ubuntu (arm64, Debug) (push) Has been cancelled
validate / ubuntu (arm64, Release) (push) Has been cancelled
validate / ubuntu (x64, Debug) (push) Has been cancelled
validate / ubuntu (x64, Release) (push) Has been cancelled
validate / windows (x64, Debug) (push) Has been cancelled
validate / windows (x64, Release) (push) Has been cancelled
validate / macos (arm64, Debug) (push) Has been cancelled
validate / macos (arm64, Release) (push) Has been cancelled
validate / macos (x64, Debug) (push) Has been cancelled
validate / macos (x64, Release) (push) Has been cancelled
Update runtime for fixed address mod sections, fix some live recompiler errors not triggering mod loading errors (#120)
2025-07-19 04:09:14 -04:00

2580 lines
105 KiB
C++

#include <span>
#include <fstream>
#include <sstream>
#include <functional>
#include "librecomp/files.hpp"
#include "librecomp/mods.hpp"
#include "librecomp/overlays.hpp"
#include "librecomp/game.hpp"
#include "recompiler/context.h"
#include "recompiler/live_recompiler.h"
static bool read_json(std::ifstream input_file, nlohmann::json &json_out) {
if (!input_file.good()) {
return false;
}
try {
input_file >> json_out;
}
catch (nlohmann::json::parse_error &) {
return false;
}
return true;
}
static bool read_json_with_backups(const std::filesystem::path &path, nlohmann::json &json_out) {
// Try reading and parsing the base file.
if (read_json(std::ifstream{ path }, json_out)) {
return true;
}
// Try reading and parsing the backup file.
if (read_json(recomp::open_input_backup_file(path), json_out)) {
return true;
}
// Both reads failed.
return false;
}
template <typename T1, typename T2>
bool get_to_vec(const nlohmann::json& val, std::vector<T2>& out) {
const nlohmann::json::array_t* ptr = val.get_ptr<const nlohmann::json::array_t*>();
if (ptr == nullptr) {
return false;
}
out.clear();
for (const nlohmann::json& cur_val : *ptr) {
const T1* temp_ptr = cur_val.get_ptr<const T1*>();
if (temp_ptr == nullptr) {
out.clear();
return false;
}
out.emplace_back(*temp_ptr);
}
return true;
}
// Architecture detection.
// MSVC x86_64
#if defined (_M_AMD64) && (_M_AMD64 == 100) && !defined (_M_ARM64EC)
# define IS_X86_64
// GCC/Clang x86_64
#elif defined(__x86_64__)
# define IS_X86_64
// MSVC/GCC/Clang ARM64
#elif defined(__ARM_ARCH_ISA_A64)
# define IS_ARM64
#else
# error "Unsupported architecture!"
#endif
#if defined(_WIN32)
#define PATHFMT "%ls"
#else
#define PATHFMT "%s"
#endif
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
#if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN
# include "Windows.h"
class recomp::mods::DynamicLibrary {
public:
static constexpr std::string_view PlatformExtension = ".dll";
DynamicLibrary() = default;
DynamicLibrary(const std::filesystem::path& path) {
native_handle = LoadLibraryExW(std::filesystem::absolute(path).c_str(), nullptr, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR);
if (good()) {
uint32_t* recomp_api_version;
if (get_dll_symbol(recomp_api_version, "recomp_api_version")) {
api_version = *recomp_api_version;
}
else {
api_version = (uint32_t)-1;
}
}
}
~DynamicLibrary() {
unload();
}
DynamicLibrary(const DynamicLibrary&) = delete;
DynamicLibrary& operator=(const DynamicLibrary&) = delete;
DynamicLibrary(DynamicLibrary&&) = delete;
DynamicLibrary& operator=(DynamicLibrary&&) = delete;
void unload() {
if (native_handle != nullptr) {
FreeLibrary(native_handle);
}
native_handle = nullptr;
}
bool good() const {
return native_handle != nullptr;
}
template <typename T>
bool get_dll_symbol(T& out, const char* name) const {
out = (T)(void*)GetProcAddress(native_handle, name);
if (out == nullptr) {
return false;
}
return true;
};
uint32_t get_api_version() {
return api_version;
}
private:
HMODULE native_handle;
uint32_t api_version;
};
void unprotect(void* target_func, uint64_t* old_flags) {
DWORD old_flags_dword;
BOOL result = VirtualProtect(target_func,
16,
PAGE_READWRITE,
&old_flags_dword);
*old_flags = old_flags_dword;
(void)result;
}
void protect(void* target_func, uint64_t old_flags) {
DWORD dummy_old_flags;
BOOL result = VirtualProtect(target_func,
16,
static_cast<DWORD>(old_flags),
&dummy_old_flags);
(void)result;
}
#else
# include <unistd.h>
# include <dlfcn.h>
# include <sys/mman.h>
class recomp::mods::DynamicLibrary {
public:
#if defined(__APPLE__)
static constexpr std::string_view PlatformExtension = ".dylib";
#else
static constexpr std::string_view PlatformExtension = ".so";
#endif
DynamicLibrary() = default;
DynamicLibrary(const std::filesystem::path& path) {
native_handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
if (good()) {
uint32_t* recomp_api_version;
if (get_dll_symbol(recomp_api_version, "recomp_api_version")) {
api_version = *recomp_api_version;
}
else {
api_version = (uint32_t)-1;
}
}
}
~DynamicLibrary() {
unload();
}
DynamicLibrary(const DynamicLibrary&) = delete;
DynamicLibrary& operator=(const DynamicLibrary&) = delete;
DynamicLibrary(DynamicLibrary&&) = delete;
DynamicLibrary& operator=(DynamicLibrary&&) = delete;
void unload() {
if (native_handle != nullptr) {
dlclose(native_handle);
}
native_handle = nullptr;
}
bool good() const {
return native_handle != nullptr;
}
template <typename T>
bool get_dll_symbol(T& out, const char* name) const {
out = (T)dlsym(native_handle, name);
if (out == nullptr) {
return false;
}
return true;
};
uint32_t get_api_version() {
return api_version;
}
private:
void* native_handle;
uint32_t api_version;
};
void unprotect(void* target_func, uint64_t* old_flags) {
// Align the address to a page boundary.
uintptr_t page_start = (uintptr_t)target_func;
int page_size = getpagesize();
page_start = (page_start / page_size) * page_size;
int result = mprotect((void*)page_start, page_size, PROT_READ | PROT_WRITE);
*old_flags = 0;
(void)result;
}
void protect(void* target_func, uint64_t old_flags) {
// Align the address to a page boundary.
uintptr_t page_start = (uintptr_t)target_func;
int page_size = getpagesize();
page_start = (page_start / page_size) * page_size;
int result = mprotect((void*)page_start, page_size, PROT_READ | PROT_EXEC);
(void)result;
}
#endif
namespace modpaths {
constexpr std::string_view default_mod_extension = "nrm";
constexpr std::string_view binary_path = "mod_binary.bin";
constexpr std::string_view binary_syms_path = "mod_syms.bin";
};
recomp::mods::CodeModLoadError recomp::mods::validate_api_version(uint32_t api_version, std::string& error_param) {
switch (api_version) {
case 1:
return CodeModLoadError::Good;
case (uint32_t)-1:
return CodeModLoadError::NoSpecifiedApiVersion;
default:
error_param = std::to_string(api_version);
return CodeModLoadError::UnsupportedApiVersion;
}
}
recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types, std::vector<char>&& thumbnail) :
manifest(std::move(manifest)),
config_storage(std::move(config_storage)),
code_handle(),
recompiler_context{std::make_unique<N64Recomp::Context>()},
content_types{std::move(content_types)},
thumbnail{ std::move(thumbnail) },
game_indices{std::move(game_indices)}
{
runtime_toggleable = true;
for (ModContentTypeId type : this->content_types) {
if (!context.is_content_runtime_toggleable(type)) {
runtime_toggleable = false;
break;
}
}
}
recomp::mods::ModHandle::ModHandle(ModHandle&& rhs) = default;
recomp::mods::ModHandle& recomp::mods::ModHandle::operator=(ModHandle&& rhs) = default;
recomp::mods::ModHandle::~ModHandle() = default;
size_t recomp::mods::ModHandle::num_exports() const {
return recompiler_context->exported_funcs.size();
}
size_t recomp::mods::ModHandle::num_events() const {
return recompiler_context->event_symbols.size();
}
void recomp::mods::ModHandle::populate_exports() {
for (size_t func_index : recompiler_context->exported_funcs) {
const auto& func_handle = recompiler_context->functions[func_index];
exports_by_name.emplace(func_handle.name, func_index);
}
}
recomp::mods::CodeModLoadError recomp::mods::ModHandle::load_native_library(const recomp::mods::NativeLibraryManifest& lib_manifest, std::string& error_param) {
std::string lib_filename = lib_manifest.name + std::string{DynamicLibrary::PlatformExtension};
std::filesystem::path lib_path = manifest.mod_root_path.parent_path() / lib_filename;
std::unique_ptr<DynamicLibrary>& lib = native_libraries.emplace_back(std::make_unique<DynamicLibrary>(lib_path));
if (!lib->good()) {
error_param = lib_filename;
return CodeModLoadError::FailedToLoadNativeLibrary;
}
std::string api_error_param;
CodeModLoadError api_error = validate_api_version(lib->get_api_version(), api_error_param);
if (api_error != CodeModLoadError::Good) {
if (api_error_param.empty()) {
error_param = lib_filename;
}
else {
error_param = lib_filename + ":" + api_error_param;
}
return api_error;
}
native_library_exports.clear();
for (const std::string& export_name : lib_manifest.exports) {
recomp_func_t* cur_func;
if (native_library_exports.contains(export_name)) {
error_param = export_name;
return CodeModLoadError::DuplicateExport;
}
if (!lib->get_dll_symbol(cur_func, export_name.c_str())) {
error_param = lib_manifest.name + ":" + export_name;
return CodeModLoadError::FailedToFindNativeExport;
}
native_library_exports.emplace(export_name, cur_func);
}
return CodeModLoadError::Good;
}
bool recomp::mods::ModHandle::get_export_function(const std::string& export_name, GenericFunction& out) const {
// First, check the code exports.
auto code_find_it = exports_by_name.find(export_name);
if (code_find_it != exports_by_name.end()) {
out = code_handle->get_function_handle(code_find_it->second);
return true;
}
// Next, check the native library exports.
auto native_find_it = native_library_exports.find(export_name);
if (native_find_it != native_library_exports.end()) {
out = native_find_it->second;
return true;
}
// Nothing found.
return false;
}
void recomp::mods::ModHandle::populate_events() {
for (size_t event_index = 0; event_index < recompiler_context->event_symbols.size(); event_index++) {
const N64Recomp::EventSymbol& event = recompiler_context->event_symbols[event_index];
events_by_name.emplace(event.base.name, event_index);
}
}
bool recomp::mods::ModHandle::get_global_event_index(const std::string& event_name, size_t& event_index_out) const {
auto find_it = events_by_name.find(event_name);
if (find_it == events_by_name.end()) {
return false;
}
event_index_out = code_handle->get_base_event_index() + find_it->second;
return true;
}
recomp::mods::DynamicLibraryCodeHandle::DynamicLibraryCodeHandle(const std::filesystem::path& dll_path, const N64Recomp::Context& context, const ModCodeHandleInputs& inputs) {
is_good = true;
// Load the DLL.
dynamic_lib = std::make_unique<DynamicLibrary>(dll_path);
if (!dynamic_lib->good()) {
is_good = false;
return;
}
// Fill out the list of function pointers.
functions.resize(context.functions.size());
for (size_t i = 0; i < functions.size(); i++) {
if(!context.functions[i].name.empty()) {
is_good &= dynamic_lib->get_dll_symbol(functions[i], context.functions[i].name.c_str());
}
else {
std::string func_name = "mod_func_" + std::to_string(i);
is_good &= dynamic_lib->get_dll_symbol(functions[i], func_name.c_str());
}
if (!is_good) {
return;
}
}
// Get the standard exported symbols.
is_good = true;
is_good &= dynamic_lib->get_dll_symbol(imported_funcs, "imported_funcs");
is_good &= dynamic_lib->get_dll_symbol(reference_symbol_funcs, "reference_symbol_funcs");
is_good &= dynamic_lib->get_dll_symbol(base_event_index, "base_event_index");
is_good &= dynamic_lib->get_dll_symbol(recomp_trigger_event, "recomp_trigger_event");
is_good &= dynamic_lib->get_dll_symbol(get_function, "get_function");
is_good &= dynamic_lib->get_dll_symbol(cop0_status_write, "cop0_status_write");
is_good &= dynamic_lib->get_dll_symbol(cop0_status_read, "cop0_status_read");
is_good &= dynamic_lib->get_dll_symbol(switch_error, "switch_error");
is_good &= dynamic_lib->get_dll_symbol(do_break, "do_break");
is_good &= dynamic_lib->get_dll_symbol(reference_section_addresses, "reference_section_addresses");
is_good &= dynamic_lib->get_dll_symbol(section_addresses, "section_addresses");
if (is_good) {
*base_event_index = inputs.base_event_index;
*recomp_trigger_event = inputs.recomp_trigger_event;
*get_function = inputs.get_function;
*cop0_status_write = inputs.cop0_status_write;
*cop0_status_read = inputs.cop0_status_read;
*switch_error = inputs.switch_error;
*do_break = inputs.do_break;
*reference_section_addresses = inputs.reference_section_addresses;
}
}
bool recomp::mods::DynamicLibraryCodeHandle::good() {
return dynamic_lib->good() && is_good;
}
uint32_t recomp::mods::DynamicLibraryCodeHandle::get_api_version() {
return dynamic_lib->get_api_version();
}
void recomp::mods::DynamicLibraryCodeHandle::set_bad() {
dynamic_lib.reset();
is_good = false;
}
void recomp::mods::DynamicLibraryCodeHandle::set_imported_function(size_t import_index, GenericFunction func) {
std::visit(overloaded {
[this, import_index](recomp_func_t* native_func) {
imported_funcs[import_index] = native_func;
}
}, func);
}
recomp::mods::CodeModLoadError recomp::mods::DynamicLibraryCodeHandle::populate_reference_symbols(const N64Recomp::Context& context, std::string& error_param) {
size_t reference_symbol_index = 0;
for (const auto& section : context.sections) {
for (const auto& reloc : section.relocs) {
if (reloc.type == N64Recomp::RelocType::R_MIPS_26 && reloc.reference_symbol && context.is_regular_reference_section(reloc.target_section)) {
recomp_func_t* cur_func = recomp::overlays::get_func_by_section_index_function_offset(reloc.target_section, reloc.target_section_offset);
if (cur_func == nullptr) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: " << reloc.target_section <<
" func offset: 0x" << reloc.target_section_offset;
error_param = error_param_stream.str();
return CodeModLoadError::InvalidReferenceSymbol;
}
reference_symbol_funcs[reference_symbol_index] = cur_func;
reference_symbol_index++;
}
}
}
return CodeModLoadError::Good;
}
recomp::mods::LiveRecompilerCodeHandle::LiveRecompilerCodeHandle(
const N64Recomp::Context& context, const ModCodeHandleInputs& inputs,
std::unordered_map<size_t, size_t>&& entry_func_hooks, std::unordered_map<size_t, size_t>&& return_func_hooks, std::vector<size_t>&& original_section_indices, bool regenerated)
{
if (!regenerated) {
section_addresses = std::make_unique<int32_t[]>(context.sections.size());
}
base_event_index = inputs.base_event_index;
N64Recomp::LiveGeneratorInputs recompiler_inputs{
.base_event_index = inputs.base_event_index,
.cop0_status_write = inputs.cop0_status_write,
.cop0_status_read = inputs.cop0_status_read,
.switch_error = inputs.switch_error,
.do_break = inputs.do_break,
.get_function = inputs.get_function,
.syscall_handler = nullptr, // TODO hook this up
.pause_self = pause_self,
.trigger_event = inputs.recomp_trigger_event,
.reference_section_addresses = inputs.reference_section_addresses,
// Use the reference section addresses as the local section addresses if this is regenerated code so that jump tables work correctly.
.local_section_addresses = regenerated ? inputs.reference_section_addresses : section_addresses.get(),
.run_hook = run_hook,
.entry_func_hooks = std::move(entry_func_hooks),
.return_func_hooks = std::move(return_func_hooks),
.original_section_indices = std::move(original_section_indices)
};
N64Recomp::LiveGenerator generator{ context.functions.size(), recompiler_inputs };
std::vector<std::vector<uint32_t>> dummy_static_funcs{};
bool errored = false;
for (size_t func_index = 0; func_index < context.functions.size(); func_index++) {
std::ostringstream dummy_ostream{};
if (!N64Recomp::recompile_function_live(generator, context, func_index, dummy_ostream, dummy_static_funcs, true)) {
errored = true;
break;
}
}
// Generate the code.
recompiler_output = std::make_unique<N64Recomp::LiveGeneratorOutput>(generator.finish());
is_good = !errored && recompiler_output->good;
}
void recomp::mods::LiveRecompilerCodeHandle::set_imported_function(size_t import_index, GenericFunction func) {
std::visit(overloaded {
[this, import_index](recomp_func_t* native_func) {
recompiler_output->populate_import_symbol_jumps(import_index, native_func);
}
}, func);
}
recomp::mods::CodeModLoadError recomp::mods::LiveRecompilerCodeHandle::populate_reference_symbols(const N64Recomp::Context& context, std::string& error_param) {
size_t num_reference_jumps = recompiler_output->num_reference_symbol_jumps();
for (size_t jump_index = 0; jump_index < num_reference_jumps; jump_index++) {
N64Recomp::ReferenceJumpDetails jump_details = recompiler_output->get_reference_symbol_jump_details(jump_index);
recomp_func_t* cur_func = recomp::overlays::get_func_by_section_index_function_offset(jump_details.section, jump_details.section_offset);
if (cur_func == nullptr) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: " << jump_details.section <<
" func offset: 0x" << jump_details.section_offset;
error_param = error_param_stream.str();
return CodeModLoadError::InvalidReferenceSymbol;
}
recompiler_output->set_reference_symbol_jump(jump_index, cur_func);
}
return CodeModLoadError::Good;
}
recomp::mods::GenericFunction recomp::mods::LiveRecompilerCodeHandle::get_function_handle(size_t func_index) {
return GenericFunction{ recompiler_output->functions[func_index] };
}
void patch_func(recomp_func_t* target_func, recomp::mods::GenericFunction replacement_func) {
uint8_t* target_func_u8 = reinterpret_cast<uint8_t*>(target_func);
size_t offset = 0;
auto write_bytes = [&](const void* bytes, size_t count) {
memcpy(target_func_u8 + offset, bytes, count);
offset += count;
};
uint64_t old_flags;
unprotect(target_func_u8, &old_flags);
#if defined(IS_X86_64)
static const uint8_t movabs_rax[] = {0x48, 0xB8};
static const uint8_t jmp_rax[] = {0xFF, 0xE0};
std::visit(overloaded {
[&write_bytes](recomp_func_t* native_func) {
write_bytes(movabs_rax, sizeof(movabs_rax));
write_bytes(&native_func, sizeof(&native_func));
write_bytes(jmp_rax, sizeof(jmp_rax));
}
}, replacement_func);
#elif defined(IS_ARM64)
static const uint8_t ldr_x2_8__br_x2[] = {0x42, 0x00, 0x00, 0x58, 0x40, 0x00, 0x1F, 0xD6};
std::visit(overloaded {
[&write_bytes](recomp_func_t* native_func) {
write_bytes(ldr_x2_8__br_x2, sizeof(ldr_x2_8__br_x2));
write_bytes(&native_func, sizeof(&native_func));
}
}, replacement_func);
#else
# error "Unsupported architecture"
#endif
protect(target_func_u8, old_flags);
}
void unpatch_func(void* target_func, const recomp::mods::PatchData& data) {
uint64_t old_flags;
unprotect(target_func, &old_flags);
memcpy(target_func, data.replaced_bytes.data(), data.replaced_bytes.size());
protect(target_func, old_flags);
}
void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types, std::vector<char>&& thumbnail) {
std::unique_lock lock(opened_mods_mutex);
size_t mod_index = opened_mods.size();
opened_mods_by_id.emplace(manifest.mod_id, mod_index);
opened_mods_by_filename.emplace(manifest.mod_root_path.filename().native(), mod_index);
opened_mods.emplace_back(*this, std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail));
opened_mods_order.emplace_back(mod_index);
}
recomp::mods::ModLoadError recomp::mods::ModContext::load_mod(recomp::mods::ModHandle& mod, std::string& error_param) {
using namespace recomp::mods;
mod.section_load_addresses.clear();
// Check that the mod's minimum recomp version is met.
if (get_project_version() < mod.manifest.minimum_recomp_version) {
error_param = mod.manifest.minimum_recomp_version.to_string();
return ModLoadError::MinimumRecompVersionNotMet;
}
for (ModContentTypeId type_id : mod.content_types) {
content_enabled_callback* callback = content_types[type_id.value].on_enabled;
if (callback) {
callback(*this, mod);
}
}
return ModLoadError::Good;
}
void recomp::mods::ModContext::register_game(const std::string& mod_game_id) {
mod_game_ids.emplace(mod_game_id, mod_game_ids.size());
}
void recomp::mods::ModContext::register_embedded_mod(const std::string &mod_id, std::span<const uint8_t> mod_bytes) {
embedded_mod_bytes.emplace(mod_id, mod_bytes);
}
void recomp::mods::ModContext::close_mods() {
std::unique_lock lock(opened_mods_mutex);
opened_mods_by_id.clear();
opened_mods_by_filename.clear();
opened_mods.clear();
opened_mods_order.clear();
mod_order_lookup.clear();
mod_ids.clear();
enabled_mods.clear();
auto_enabled_mods.clear();
}
bool save_mod_config_storage(const std::filesystem::path &path, const std::string &mod_id, const recomp::Version &mod_version, const recomp::mods::ConfigStorage &config_storage, const recomp::mods::ConfigSchema &config_schema) {
using json = nlohmann::json;
json config_json;
config_json["mod_id"] = mod_id;
config_json["mod_version"] = mod_version.to_string();
config_json["recomp_version"] = recomp::get_project_version().to_string();
json &storage_json = config_json["storage"];
for (auto it : config_storage.value_map) {
auto id_it = config_schema.options_by_id.find(it.first);
if (id_it == config_schema.options_by_id.end()) {
continue;
}
const recomp::mods::ConfigOption &config_option = config_schema.options[id_it->second];
switch (config_option.type) {
case recomp::mods::ConfigOptionType::Enum:
storage_json[it.first] = std::get<recomp::mods::ConfigOptionEnum>(config_option.variant).options[std::get<uint32_t>(it.second)];
break;
case recomp::mods::ConfigOptionType::Number:
storage_json[it.first] = std::get<double>(it.second);
break;
case recomp::mods::ConfigOptionType::String:
storage_json[it.first] = std::get<std::string>(it.second);
break;
default:
assert(false && "Unknown config type.");
break;
}
}
std::ofstream output_file = recomp::open_output_file_with_backup(path);
if (!output_file.good()) {
return false;
}
output_file << std::setw(4) << config_json;
output_file.close();
return recomp::finalize_output_file_with_backup(path);
}
bool parse_mods_config(const std::filesystem::path &path, std::unordered_set<std::string> &enabled_mods, std::vector<std::string> &mod_order) {
using json = nlohmann::json;
json config_json;
if (!read_json_with_backups(path, config_json)) {
return false;
}
auto enabled_mods_json = config_json.find("enabled_mods");
if (enabled_mods_json != config_json.end()) {
std::vector<std::string> enabled_mods_vector;
if (get_to_vec<std::string>(*enabled_mods_json, enabled_mods_vector)) {
for (const std::string &mod_id : enabled_mods_vector) {
enabled_mods.emplace(mod_id);
}
}
}
auto mod_order_json = config_json.find("mod_order");
if (mod_order_json != config_json.end()) {
get_to_vec<std::string>(*mod_order_json, mod_order);
}
return true;
}
bool save_mods_config(const std::filesystem::path &path, const std::unordered_set<std::string> &enabled_mods, const std::vector<std::string> &mod_order) {
nlohmann::json config_json;
config_json["enabled_mods"] = enabled_mods;
config_json["mod_order"] = mod_order;
std::ofstream output_file = recomp::open_output_file_with_backup(path);
if (!output_file.good()) {
return false;
}
output_file << std::setw(4) << config_json;
output_file.close();
return recomp::finalize_output_file_with_backup(path);
}
void recomp::mods::ModContext::dirty_mod_configuration_thread_process() {
using namespace std::chrono_literals;
ModConfigQueueVariant variant;
ModConfigQueueSaveMod save_mod;
std::unordered_set<std::string> pending_mods;
std::unordered_map<std::string, ConfigStorage> pending_mod_storage;
std::unordered_map<std::string, ConfigSchema> pending_mod_schema;
std::unordered_map<std::string, Version> pending_mod_version;
std::unordered_set<std::string> config_enabled_mods;
std::vector<std::string> config_mod_order;
bool pending_config_save = false;
std::filesystem::path config_path;
bool active = true;
auto handle_variant = [&](const ModConfigQueueVariant &variant) {
if (std::get_if<ModConfigQueueEnd>(&variant) != nullptr) {
active = false;
}
else if (std::get_if<ModConfigQueueSave>(&variant) != nullptr) {
pending_config_save = true;
}
else if (const ModConfigQueueSaveMod* queue_save_mod = std::get_if<ModConfigQueueSaveMod>(&variant)) {
pending_mods.emplace(queue_save_mod->mod_id);
}
};
while (active) {
// Wait for at least one mod to require writing.
mod_configuration_thread_queue.wait_dequeue(variant);
handle_variant(variant);
// Clear out the entire queue to coalesce all writes with a timeout.
while (active && mod_configuration_thread_queue.wait_dequeue_timed(variant, 1s)) {
handle_variant(variant);
}
if (active && !pending_mods.empty()) {
{
std::unique_lock opened_mods_lock(opened_mods_mutex);
for (const std::string &id : pending_mods) {
auto it = opened_mods_by_id.find(id);
if (it != opened_mods_by_id.end()) {
const ModHandle &mod = opened_mods[it->second];
std::unique_lock config_storage_lock(mod_config_storage_mutex);
pending_mod_storage[id] = mod.config_storage;
pending_mod_schema[id] = mod.manifest.config_schema;
pending_mod_version[id] = mod.manifest.version;
}
}
}
for (const std::string &id : pending_mods) {
config_path = mod_config_directory / std::string(id + ".json");
save_mod_config_storage(config_path, id, pending_mod_version[id], pending_mod_storage[id], pending_mod_schema[id]);
}
pending_mods.clear();
}
if (active && pending_config_save) {
{
// Store the enabled mods and the order.
std::unique_lock lock(opened_mods_mutex);
config_enabled_mods = enabled_mods;
config_mod_order.clear();
for (size_t mod_index : opened_mods_order) {
config_mod_order.emplace_back(opened_mods[mod_index].manifest.mod_id);
}
}
save_mods_config(mods_config_path, config_enabled_mods, config_mod_order);
pending_config_save = false;
}
}
}
std::vector<recomp::mods::ModOpenErrorDetails> recomp::mods::ModContext::scan_mod_folder(const std::filesystem::path& mod_folder) {
std::vector<recomp::mods::ModOpenErrorDetails> ret{};
std::error_code ec;
close_mods();
static const std::vector<ModContentTypeId> empty_content_types{};
for (const auto& mod_path : std::filesystem::directory_iterator{mod_folder, std::filesystem::directory_options::skip_permission_denied, ec}) {
bool is_mod = false;
bool requires_manifest = true;
std::reference_wrapper<const std::vector<ModContentTypeId>> supported_content_types = std::cref(empty_content_types);
if (mod_path.is_regular_file()) {
auto find_container_it = container_types.find(mod_path.path().extension().string());
if (find_container_it != container_types.end()) {
is_mod = true;
supported_content_types = find_container_it->second.supported_content_types;
requires_manifest = find_container_it->second.requires_manifest;
}
}
else if (mod_path.is_directory()) {
is_mod = true;
}
if (is_mod) {
printf("Opening mod " PATHFMT "\n", mod_path.path().stem().c_str());
std::string open_error_param;
ModOpenError open_error = open_mod_from_path(mod_path, open_error_param, supported_content_types, requires_manifest);
if (open_error != ModOpenError::Good) {
ret.emplace_back(mod_path.path(), open_error, open_error_param);
}
}
else {
printf("Skipping non-mod " PATHFMT PATHFMT "\n", mod_path.path().stem().c_str(), mod_path.path().extension().c_str());
}
}
for (const auto &mod_bytes : embedded_mod_bytes) {
if (opened_mods_by_id.contains(mod_bytes.first)) {
continue;
}
std::string open_error_param;
ModOpenError open_error = open_mod_from_memory(mod_bytes.second, open_error_param, empty_content_types, true);
if (open_error != ModOpenError::Good) {
ret.emplace_back(mod_bytes.first, open_error, open_error_param);
}
}
return ret;
}
void recomp::mods::ModContext::load_mods_config() {
std::unordered_set<std::string> config_enabled_mods;
std::vector<std::string> config_mod_order;
std::vector<bool> opened_mod_is_known;
parse_mods_config(mods_config_path, config_enabled_mods, config_mod_order);
// Fill a vector with the relative order of the mods. Existing mods will get ordered below new mods.
std::vector<size_t> sort_order;
sort_order.resize(opened_mods.size());
opened_mod_is_known.resize(opened_mods.size(), false);
std::iota(sort_order.begin(), sort_order.end(), 0);
for (size_t i = 0; i < config_mod_order.size(); i++) {
auto it = opened_mods_by_id.find(config_mod_order[i]);
if (it != opened_mods_by_id.end()) {
sort_order[it->second] = opened_mods.size() + i;
opened_mod_is_known[it->second] = true;
}
}
// Run the sort using the relative order computed before.
std::iota(opened_mods_order.begin(), opened_mods_order.end(), 0);
std::sort(opened_mods_order.begin(), opened_mods_order.end(), [&](size_t i, size_t j) {
return sort_order[i] < sort_order[j];
});
rebuild_mod_order_lookup();
// Enable mods that are specified in the configuration or mods that are considered new.
for (size_t i = 0; i < opened_mods.size(); i++) {
const ModHandle& mod = opened_mods[i];
const std::string &mod_id = mod.manifest.mod_id;
bool is_default_enabled = !opened_mod_is_known[i] && mod.manifest.enabled_by_default;
bool is_manually_enabled = config_enabled_mods.contains(mod_id);
if (is_default_enabled || is_manually_enabled) {
enable_mod(mod_id, true, false);
}
}
}
void recomp::mods::ModContext::rebuild_mod_order_lookup() {
// Initialize the mod order lookup to all -1 so that mods that aren't enabled have an order index of -1.
mod_order_lookup.resize(opened_mods.size());
std::fill(mod_order_lookup.begin(), mod_order_lookup.end(), static_cast<size_t>(-1));
// Build the lookup of mod index to mod order by inverting the opened mods order list.
for (size_t mod_order_index = 0; mod_order_index < opened_mods_order.size(); mod_order_index++) {
size_t mod_index = opened_mods_order[mod_order_index];
mod_order_lookup[mod_index] = mod_order_index;
}
}
recomp::mods::ModContext::ModContext() {
// Register the code content type.
ModContentType code_content_type {
.content_filename = std::string{modpaths::binary_syms_path},
.allow_runtime_toggle = false,
.on_enabled = ModContext::on_code_mod_enabled,
.on_disabled = nullptr,
.on_reordered = nullptr
};
code_content_type_id = register_content_type(code_content_type);
// Register the default mod container type (.nrm) and allow it to have any content type by passing an empty vector.
register_container_type(std::string{ modpaths::default_mod_extension }, {}, true);
mod_configuration_thread = std::make_unique<std::thread>(&ModContext::dirty_mod_configuration_thread_process, this);
}
void recomp::mods::ModContext::on_code_mod_enabled(ModContext& context, const ModHandle& mod) {
auto find_mod_it = context.loaded_mods_by_id.find(mod.manifest.mod_id);
if (find_mod_it == context.loaded_mods_by_id.end()) {
assert(false && "Failed to find enabled code mod");
}
else {
context.loaded_code_mods.emplace_back(find_mod_it->second);
}
}
recomp::mods::ModContext::~ModContext() {
mod_configuration_thread_queue.enqueue(ModConfigQueueEnd());
mod_configuration_thread->join();
mod_configuration_thread.reset();
}
recomp::mods::ModContentTypeId recomp::mods::ModContext::register_content_type(const ModContentType& type) {
size_t ret = content_types.size();
content_types.emplace_back(type);
return ModContentTypeId{.value = ret};
}
bool recomp::mods::ModContext::register_container_type(const std::string& extension, const std::vector<ModContentTypeId>& container_content_types, bool requires_manifest) {
// Validate the provided content type IDs.
for (ModContentTypeId id : container_content_types) {
if (id.value >= content_types.size()) {
return false;
}
}
// Validate that the extension doesn't contain a dot.
if (extension.find('.') != std::string::npos) {
return false;
}
// Prepend a dot to the extension to get the real extension that will be registered..
std::string true_extension = "." + extension;
// Validate that this extension hasn't been registered already.
if (container_types.contains(true_extension)) {
return false;
}
// Register the container type.
container_types.emplace(true_extension,
ModContainerType {
.supported_content_types = container_content_types,
.requires_manifest = requires_manifest
});
return true;
}
std::string recomp::mods::ModContext::get_mod_display_name(size_t mod_index) const {
return opened_mods[mod_index].manifest.display_name;
}
std::filesystem::path recomp::mods::ModContext::get_mod_path(size_t mod_index) const {
return opened_mods[mod_index].manifest.mod_root_path;
}
std::pair<std::string, std::string> recomp::mods::ModContext::get_mod_import_info(size_t mod_index, size_t import_index) const {
const ModHandle& mod = opened_mods[mod_index];
const N64Recomp::ImportSymbol& imported_func = mod.recompiler_context->import_symbols[import_index];
const std::string& dependency_id = mod.recompiler_context->dependencies[imported_func.dependency_index];
return std::make_pair<std::string, std::string>(std::string{ dependency_id }, std::string{ imported_func.base.name });
}
recomp::mods::DependencyStatus recomp::mods::ModContext::is_dependency_met(size_t mod_index, const std::string& dependency_id) const {
const ModHandle& mod = opened_mods[mod_index];
auto find_dep = mod.manifest.dependencies_by_id.find(dependency_id);
if (find_dep == mod.manifest.dependencies_by_id.end()) {
return DependencyStatus::InvalidDependency;
}
auto find_dep_mod = loaded_mods_by_id.find(dependency_id);
if (find_dep_mod == loaded_mods_by_id.end()) {
return DependencyStatus::NotFound;
}
const Dependency& dep = mod.manifest.dependencies[find_dep->second];
const ModHandle& dep_mod = opened_mods[find_dep_mod->second];
if (dep_mod.manifest.version < dep.version) {
return DependencyStatus::WrongVersion;
}
return DependencyStatus::Found;
}
bool recomp::mods::ModContext::is_content_runtime_toggleable(ModContentTypeId content_type) const {
assert(content_type.value < content_types.size());
return content_types[content_type.value].allow_runtime_toggle;
}
void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enabled, bool trigger_save) {
// Check that the mod exists.
std::unique_lock lock(opened_mods_mutex);
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return;
}
ModHandle& mod = opened_mods[find_it->second];
bool mods_loaded = active_game != (size_t)-1;
// Do nothing if mods have already been loaded and this mod isn't runtime toggleable.
if (!mod.is_runtime_toggleable() && mods_loaded) {
return;
}
// Do nothing if mods have already been loaded and this mod isn't for the active game.
if (mods_loaded && !mod.is_for_game(active_game)) {
return;
}
if (enabled) {
bool was_enabled = enabled_mods.emplace(mod_id).second;
// If mods have been loaded and a mod was successfully enabled by this call, call the on_enabled handlers for its content types.
if (was_enabled && mods_loaded) {
for (ModContentTypeId type_id : mod.content_types) {
content_enabled_callback* callback = content_types[type_id.value].on_enabled;
if (callback) {
callback(*this, mod);
}
}
}
if (was_enabled) {
std::vector<std::string> mod_stack;
mod_stack.emplace_back(mod_id);
while (!mod_stack.empty()) {
std::string mod_from_stack = std::move(mod_stack.back());
mod_stack.pop_back();
auto mod_from_stack_it = opened_mods_by_id.find(mod_from_stack);
if (mod_from_stack_it != opened_mods_by_id.end()) {
const ModHandle &mod_from_stack_handle = opened_mods[mod_from_stack_it->second];
for (const Dependency &dependency : mod_from_stack_handle.manifest.dependencies) {
if (!dependency.optional && !auto_enabled_mods.contains(dependency.mod_id)) {
auto_enabled_mods.emplace(dependency.mod_id);
mod_stack.emplace_back(dependency.mod_id);
if (mods_loaded) {
for (ModContentTypeId type_id : mod_from_stack_handle.content_types) {
content_enabled_callback* callback = content_types[type_id.value].on_enabled;
if (callback) {
callback(*this, mod_from_stack_handle);
}
}
}
}
}
}
}
}
}
else {
bool was_disabled = enabled_mods.erase(mod_id) != 0;
// If mods have been loaded and a mod was successfully disabled by this call, call the on_disabled handlers for its content types.
if (was_disabled && mods_loaded) {
for (ModContentTypeId type_id : mod.content_types) {
content_disabled_callback* callback = content_types[type_id.value].on_disabled;
if (callback) {
callback(*this, mod);
}
}
}
if (was_disabled) {
// The algorithm needs to be run again with a new set of auto-enabled mods from scratch for all enabled mods.
std::unordered_set<std::string> new_auto_enabled_mods;
for (const std::string &enabled_mod_id : enabled_mods) {
std::vector<std::string> mod_stack;
mod_stack.emplace_back(enabled_mod_id);
while (!mod_stack.empty()) {
std::string mod_from_stack = std::move(mod_stack.back());
mod_stack.pop_back();
auto mod_from_stack_it = opened_mods_by_id.find(mod_from_stack);
if (mod_from_stack_it != opened_mods_by_id.end()) {
const ModHandle &mod_from_stack_handle = opened_mods[mod_from_stack_it->second];
for (const Dependency &dependency : mod_from_stack_handle.manifest.dependencies) {
if (!dependency.optional && !new_auto_enabled_mods.contains(dependency.mod_id)) {
new_auto_enabled_mods.emplace(dependency.mod_id);
mod_stack.emplace_back(dependency.mod_id);
}
}
}
}
}
if (mods_loaded) {
// Before replacing the old set with the new one, whatever does not exist in the new set anymore should trigger it's on_disabled callback.
for (const std::string &enabled_mod_id : auto_enabled_mods) {
if (!new_auto_enabled_mods.contains(enabled_mod_id)) {
auto enabled_mod_it = opened_mods_by_id.find(enabled_mod_id);
if (enabled_mod_it != opened_mods_by_id.end()) {
const ModHandle &enabled_mod_handle = opened_mods[enabled_mod_it->second];
for (ModContentTypeId type_id : enabled_mod_handle.content_types) {
content_disabled_callback* callback = content_types[type_id.value].on_disabled;
if (callback) {
callback(*this, enabled_mod_handle);
}
}
}
}
}
}
auto_enabled_mods = new_auto_enabled_mods;
}
}
if (trigger_save) {
mod_configuration_thread_queue.enqueue(ModConfigQueueSave());
}
}
bool recomp::mods::ModContext::is_mod_enabled(const std::string& mod_id) {
return enabled_mods.contains(mod_id);
}
bool recomp::mods::ModContext::is_mod_auto_enabled(const std::string& mod_id) {
return auto_enabled_mods.contains(mod_id);
}
size_t recomp::mods::ModContext::num_opened_mods() {
return opened_mods.size();
}
std::string recomp::mods::ModContext::get_mod_id_from_filename(const std::filesystem::path& filename) const {
auto find_it = opened_mods_by_filename.find(filename.native());
if (find_it == opened_mods_by_filename.end()) {
return {};
}
return opened_mods[find_it->second].manifest.mod_id;
}
std::filesystem::path recomp::mods::ModContext::get_mod_filename(const std::string& mod_id) const {
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return {};
}
return opened_mods[find_it->second].manifest.mod_root_path;
}
size_t recomp::mods::ModContext::get_mod_order_index(const std::string& mod_id) const {
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return static_cast<size_t>(-1);
}
return get_mod_order_index(find_it->second);
}
size_t recomp::mods::ModContext::get_mod_order_index(size_t mod_index) const {
size_t order_index = mod_order_lookup[mod_index];
// Check if the mod has a proper order index and assert if it doesn't, as that means the mod isn't actually loaded.
if (order_index == static_cast<size_t>(-1)) {
assert(false);
return static_cast<size_t>(-1);
}
return order_index;
}
std::optional<recomp::mods::ModDetails> recomp::mods::ModContext::get_details_for_mod(const std::string& mod_id) const {
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return {};
}
size_t mod_index = find_it->second;
const ModHandle &mod = opened_mods[mod_index];
return mod.get_details();
}
std::vector<recomp::mods::ModDetails> recomp::mods::ModContext::get_all_mod_details(const std::string &mod_game_id) {
std::vector<ModDetails> ret{};
bool all_games = mod_game_id.empty();
size_t game_index = (size_t)-1;
auto find_game_it = mod_game_ids.find(mod_game_id);
if (find_game_it != mod_game_ids.end()) {
game_index = find_game_it->second;
}
for (size_t mod_index : opened_mods_order) {
const ModHandle &mod = opened_mods[mod_index];
if (all_games || mod.is_for_game(game_index)) {
std::vector<Dependency> cur_dependencies{};
ret.emplace_back(mod.get_details());
}
}
return ret;
}
recomp::Version recomp::mods::ModContext::get_mod_version(size_t mod_index) {
return opened_mods[mod_index].manifest.version;
}
std::string recomp::mods::ModContext::get_mod_id(size_t mod_index) {
return opened_mods[mod_index].manifest.mod_id;
}
struct RegeneratedSection {
uint32_t rom_addr;
uint32_t ram_addr;
uint16_t original_index;
size_t first_func_index;
size_t first_reloc_index;
bool relocatable;
};
struct RegeneratedFunction {
uint32_t section_offset;
uint32_t size;
};
struct RegeneratedReloc {
uint32_t section_offset;
uint32_t target_section;
uint32_t target_section_offset;
RelocEntryType type;
};
struct RegeneratedList {
std::vector<RegeneratedSection> sections;
std::vector<RegeneratedFunction> functions;
std::vector<RegeneratedReloc> relocs;
// The native function pointers to be used for patching.
std::vector<recomp_func_t*> func_ptrs;
// Mappings of function index within context to hook slot index.
std::unordered_map<size_t, size_t> entry_func_hooks;
std::unordered_map<size_t, size_t> return_func_hooks;
// Regeneration list for the patches.
std::vector<std::pair<recomp::overlays::BasePatchedFunction, std::pair<recomp::mods::HookDefinition, size_t>>> patched_hooks;
};
N64Recomp::Context context_from_regenerated_list(const RegeneratedList& regenlist, std::span<const uint8_t> rom) {
N64Recomp::Context ret{};
// TODO avoid copying the whole ROM into the context somehow.
ret.rom.assign(rom.begin(), rom.end());
ret.sections.resize(regenlist.sections.size());
ret.section_functions.resize(regenlist.sections.size());
ret.functions.resize(regenlist.functions.size());
for (size_t section_index = 0; section_index < regenlist.sections.size(); section_index++) {
const RegeneratedSection& section_in = regenlist.sections[section_index];
N64Recomp::Section& section_out = ret.sections[section_index];
size_t cur_num_funcs;
size_t cur_num_relocs;
if (section_index == regenlist.sections.size() - 1) {
cur_num_funcs = regenlist.functions.size() - section_in.first_func_index;
cur_num_relocs = regenlist.relocs.size() - section_in.first_reloc_index;
}
else {
cur_num_funcs = regenlist.sections[section_index + 1].first_func_index - section_in.first_func_index;
cur_num_relocs = regenlist.sections[section_index + 1].first_reloc_index - section_in.first_reloc_index;
}
section_out.rom_addr = section_in.rom_addr;
section_out.ram_addr = section_in.ram_addr;
section_out.size = 0;
section_out.bss_size = 0;
section_out.function_addrs.resize(cur_num_funcs);
section_out.relocs.resize(cur_num_relocs);
section_out.name = "patch_section_" + std::to_string(section_index);
section_out.bss_section_index = 0;
section_out.executable = true;
section_out.relocatable = section_in.relocatable;
section_out.has_mips32_relocs = false;
std::vector<size_t>& section_funcs_out = ret.section_functions[section_index];
section_funcs_out.resize(cur_num_funcs);
for (size_t section_function_index = 0; section_function_index < cur_num_funcs; section_function_index++) {
// Get the global index of the function within the context.
size_t function_index = section_in.first_func_index + section_function_index;
section_funcs_out[section_function_index] = function_index;
// Populate the fields of the function.
const RegeneratedFunction& function_in = regenlist.functions[function_index];
N64Recomp::Function& function_out = ret.functions[function_index];
function_out.vram = section_out.ram_addr + function_in.section_offset;
function_out.rom = section_out.rom_addr + function_in.section_offset;
function_out.words.resize(function_in.size / sizeof(uint32_t));
function_out.name = "patch_function_" + std::to_string(function_index);
function_out.section_index = section_index;
function_out.ignored = false;
function_out.reimplemented = false;
function_out.stubbed = false;
function_out.function_hooks.clear();
// Copy the function's words.
const uint32_t* func_words = reinterpret_cast<const uint32_t*>(rom.data() + function_out.rom);
function_out.words.assign(func_words, func_words + function_in.size / sizeof(uint32_t));
// Add the function to the lookup table.
ret.functions_by_vram[function_out.vram].push_back(function_index);
}
for (size_t section_reloc_index = 0; section_reloc_index < cur_num_relocs; section_reloc_index++) {
// Get the global index of the reloc within the regenlist.
size_t reloc_index = section_in.first_reloc_index + section_reloc_index;
const RegeneratedReloc& reloc_in = regenlist.relocs[reloc_index];
N64Recomp::Reloc& reloc_out = section_out.relocs[section_reloc_index];
reloc_out.address = reloc_in.section_offset + section_out.ram_addr;
reloc_out.target_section_offset = reloc_in.target_section_offset;
if (reloc_in.target_section == N64Recomp::SectionEvent) {
// Symbol index holds the event index for event reference symbols.
reloc_out.symbol_index = reloc_in.target_section_offset;
}
else {
reloc_out.symbol_index = 0; // Unused for live recompilation.
}
reloc_out.target_section = reloc_in.target_section;
reloc_out.type = static_cast<N64Recomp::RelocType>(reloc_in.type);
reloc_out.reference_symbol = true;
}
}
return ret;
}
void recomp::mods::ModContext::set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index) {
std::unique_lock lock(opened_mods_mutex);
bool all_games = mod_game_id.empty();
size_t game_index = (size_t)-1;
auto find_game_it = mod_game_ids.find(mod_game_id);
if (find_game_it != mod_game_ids.end()) {
game_index = find_game_it->second;
}
auto id_it = opened_mods_by_id.find(mod_id);
if (id_it == opened_mods_by_id.end()) {
return;
}
size_t mod_index = id_it->second;
size_t search_index = 0;
bool inserted = false;
bool erased = false;
for (size_t i = 0; i < opened_mods_order.size() && (!inserted || !erased); i++) {
size_t current_index = opened_mods_order[i];
const ModHandle &mod = opened_mods[current_index];
if (all_games || mod.is_for_game(game_index)) {
if (index == search_index) {
// This index corresponds to the one from the view. Insert the mod here.
opened_mods_order.insert(opened_mods_order.begin() + i, mod_index);
inserted = true;
}
else if (mod_index == current_index) {
// This index corresponds to the previous position the mod had. Erase it.
opened_mods_order.erase(opened_mods_order.begin() + i);
erased = true;
}
search_index++;
}
}
if (!inserted) {
opened_mods_order.push_back(mod_index);
}
rebuild_mod_order_lookup();
for (ModContentTypeId type_id : opened_mods[mod_index].content_types) {
content_reordered_callback* callback = content_types[type_id.value].on_reordered;
if (callback) {
callback(*this);
}
}
mod_configuration_thread_queue.enqueue(ModConfigQueueSave());
}
const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schema(const std::string &mod_id) const {
// Check that the mod exists.
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return empty_schema;
}
const ModHandle &mod = opened_mods[find_it->second];
return mod.manifest.config_schema;
}
const std::vector<char> &recomp::mods::ModContext::get_mod_thumbnail(const std::string &mod_id) const {
// Check that the mod exists.
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return empty_bytes;
}
const ModHandle &mod = opened_mods[find_it->second];
return mod.thumbnail;
}
void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value) {
// Check that the mod exists.
if (mod_index >= opened_mods.size()) {
return;
}
ModHandle &mod = opened_mods[mod_index];
std::unique_lock lock(mod_config_storage_mutex);
auto option_by_id_it = mod.manifest.config_schema.options_by_id.find(option_id);
if (option_by_id_it != mod.manifest.config_schema.options_by_id.end()) {
// Only accept setting values if the value exists and the variant is the right type.
const ConfigOption &option = mod.manifest.config_schema.options[option_by_id_it->second];
switch (option.type) {
case ConfigOptionType::Enum:
if (std::holds_alternative<uint32_t>(value)) {
if (std::get<uint32_t>(value) < std::get<ConfigOptionEnum>(option.variant).options.size()) {
mod.config_storage.value_map[option_id] = value;
}
}
break;
case ConfigOptionType::Number:
if (std::holds_alternative<double>(value)) {
mod.config_storage.value_map[option_id] = value;
}
break;
case ConfigOptionType::String:
if (std::holds_alternative<std::string>(value)) {
mod.config_storage.value_map[option_id] = value;
}
break;
default:
assert(false && "Unknown config option type.");
return;
}
}
// Notify the asynchronous thread it should save the configuration for this mod.
mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod{ mod.manifest.mod_id });
}
void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) {
// Check that the mod exists.
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return;
}
set_mod_config_value(find_it->second, option_id, value);
}
recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(size_t mod_index, const std::string &option_id) const {
// Check that the mod exists.
if (mod_index >= opened_mods.size()) {
return std::monostate();
}
const ModHandle &mod = opened_mods[mod_index];
std::unique_lock lock(mod_config_storage_mutex);
auto it = mod.config_storage.value_map.find(option_id);
if (it != mod.config_storage.value_map.end()) {
return it->second;
}
else {
// Attempt to see if we can find a default value from the schema.
auto option_by_id_it = mod.manifest.config_schema.options_by_id.find(option_id);
if (option_by_id_it == mod.manifest.config_schema.options_by_id.end()) {
return std::monostate();
}
const ConfigOption &option = mod.manifest.config_schema.options[option_by_id_it->second];
switch (option.type) {
case ConfigOptionType::Enum:
return std::get<ConfigOptionEnum>(option.variant).default_value;
case ConfigOptionType::Number:
return std::get<ConfigOptionNumber>(option.variant).default_value;
case ConfigOptionType::String:
return std::get<ConfigOptionString>(option.variant).default_value;
default:
assert(false && "Unknown config option type.");
return std::monostate();
}
}
}
recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) const {
// Check that the mod exists.
auto find_it = opened_mods_by_id.find(mod_id);
if (find_it == opened_mods_by_id.end()) {
return std::monostate();
}
return get_mod_config_value(find_it->second, option_id);
}
void recomp::mods::ModContext::set_mods_config_path(const std::filesystem::path &path) {
mods_config_path = path;
}
void recomp::mods::ModContext::set_mod_config_directory(const std::filesystem::path &path) {
mod_config_directory = path;
}
std::vector<recomp::mods::ModLoadErrorDetails> recomp::mods::ModContext::load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used) {
std::vector<recomp::mods::ModLoadErrorDetails> ret{};
ram_used = 0;
num_events = recomp::overlays::num_base_events();
loaded_code_mods.clear();
std::span<const uint8_t> decompressed_rom{};
// Decompress the rom if needed.
std::vector<uint8_t> decompressed_rom_data{};
if (game_entry.has_compressed_code) {
if (game_entry.decompression_routine != nullptr) {
decompressed_rom_data = game_entry.decompression_routine(recomp::get_rom());
}
decompressed_rom = std::span{decompressed_rom_data};
}
// Otherwise, assign the regular rom as the decompressed rom since no decompression is needed.
else {
decompressed_rom = recomp::get_rom();
}
// Collect the set of functions patched by the base recomp.
std::unordered_map<recomp_func_t*, recomp::overlays::BasePatchedFunction> base_patched_funcs = recomp::overlays::get_base_patched_funcs();
auto find_index_it = mod_game_ids.find(game_entry.mod_game_id);
if (find_index_it == mod_game_ids.end()) {
ret.emplace_back(game_entry.mod_game_id, ModLoadError::InvalidGame, std::string{});
return ret;
}
size_t mod_game_index = find_index_it->second;
if (!patched_funcs.empty()) {
printf("Mods already loaded!\n");
return {};
}
const std::unordered_map<uint32_t, uint16_t>& section_vrom_map = recomp::overlays::get_vrom_to_section_map();
std::vector<size_t> active_mods{};
// Find and load active mods.
for (size_t mod_index = 0; mod_index < opened_mods.size(); mod_index++) {
auto& mod = opened_mods[mod_index];
if (mod.is_for_game(mod_game_index) && (enabled_mods.contains(mod.manifest.mod_id) || auto_enabled_mods.contains(mod.manifest.mod_id))) {
active_mods.push_back(mod_index);
loaded_mods_by_id.emplace(mod.manifest.mod_id, mod_index);
printf("Loading mod %s\n", mod.manifest.mod_id.c_str());
std::string load_error_param;
ModLoadError load_error = load_mod(mod, load_error_param);
if (load_error != ModLoadError::Good) {
ret.emplace_back(mod.manifest.mod_id, load_error, load_error_param);
}
}
}
// Exit early if errors were found.
if (!ret.empty()) {
unload_mods();
return ret;
}
// Check that mod dependencies are met.
for (size_t mod_index : active_mods) {
auto& mod = opened_mods[mod_index];
std::vector<std::pair<ModLoadError, std::string>> cur_errors;
check_dependencies(mod, cur_errors);
if (!cur_errors.empty()) {
for (auto const& [cur_error, cur_error_param] : cur_errors) {
ret.emplace_back(mod.manifest.mod_id, cur_error, cur_error_param);
}
}
}
// Exit early if errors were found.
if (!ret.empty()) {
unload_mods();
return ret;
}
std::vector<uint32_t> base_event_indices;
base_event_indices.resize(opened_mods.size());
// Parse the code mods and load their binary data.
for (size_t mod_index : loaded_code_mods) {
uint32_t cur_ram_used = 0;
auto& mod = opened_mods[mod_index];
std::string cur_error_param;
size_t base_event_index = num_events;
CodeModLoadError cur_error = init_mod_code(rdram, section_vrom_map, mod, load_address, !decompressed_rom.empty(), cur_ram_used, cur_error_param);
if (cur_error != CodeModLoadError::Good) {
if (cur_error_param.empty()) {
ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error));
}
else {
ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error) + ":" + cur_error_param);
}
}
else {
load_address += cur_ram_used;
ram_used += cur_ram_used;
base_event_indices[mod_index] = static_cast<uint32_t>(base_event_index);
}
}
// Exit early if errors were found.
if (!ret.empty()) {
unload_mods();
return ret;
}
// Set up the event callbacks based on the number of events allocated.
recomp::mods::setup_events(num_events);
// TODO if any hooks have been made but the decompressed rom isn't available,
// present an error and stop loading mods.
// Set up the hook slots based on the number of unique hooks.
recomp::mods::setup_hooks(hook_slots.size());
// Allocate room for tracking the processed hook slots.
processed_hook_slots.clear();
processed_hook_slots.resize(hook_slots.size());
// Load the code and exports from all mods.
for (size_t mod_index : loaded_code_mods) {
auto& mod = opened_mods[mod_index];
std::string cur_error_param;
CodeModLoadError cur_error = load_mod_code(rdram, mod, base_event_indices[mod_index], cur_error_param);
if (cur_error != CodeModLoadError::Good) {
if (cur_error_param.empty()) {
ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error));
}
else {
ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error) + ":" + cur_error_param);
}
}
}
// Exit early if errors were found.
if (!ret.empty()) {
unload_mods();
return ret;
}
// Resolve code dependencies for all mods.
for (size_t mod_index : loaded_code_mods) {
auto& mod = opened_mods[mod_index];
std::string cur_error_param;
CodeModLoadError cur_error = resolve_code_dependencies(mod, mod_index, base_patched_funcs, cur_error_param);
if (cur_error != CodeModLoadError::Good) {
if (cur_error_param.empty()) {
ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error));
}
else {
ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error) + ":" + cur_error_param);
}
}
}
// Exit early if errors were found.
if (!ret.empty()) {
unload_mods();
return ret;
}
// Regenerate any remaining hook slots that weren't handled during mod recompilation.
// List of unprocessed hooks and their hook index. Also set up which hooks are return hooks.
std::vector<std::pair<recomp::mods::HookDefinition, size_t>> unprocessed_hooks;
for (const auto& [def, index] : hook_slots) {
if (!processed_hook_slots[index]) {
unprocessed_hooks.emplace_back(std::make_pair(def, index));
}
recomp::mods::set_hook_type(index, def.at_return);
}
if (!unprocessed_hooks.empty()) {
// Sort the unprocessed hooks by section and vram.
std::sort(unprocessed_hooks.begin(), unprocessed_hooks.end(),
[](const std::pair<recomp::mods::HookDefinition, size_t>& lhs, const std::pair<recomp::mods::HookDefinition, size_t>& rhs) {
if (lhs.first.section_rom == rhs.first.section_rom) {
return lhs.first.function_vram < rhs.first.function_vram;
}
else {
return lhs.first.section_rom < rhs.first.section_rom;
}
}
);
ret = regenerate_with_hooks(unprocessed_hooks, section_vrom_map, base_patched_funcs, decompressed_rom);
// Exit early if errors were found.
if (!ret.empty()) {
unload_mods();
return ret;
}
}
finish_event_setup(*this);
finish_hook_setup(*this);
active_game = mod_game_index;
return ret;
}
template <bool patched_regenlist>
std::vector<recomp::mods::ModLoadErrorDetails> build_regen_list(
const std::vector<std::pair<recomp::mods::HookDefinition, size_t>>& sorted_unprocessed_hooks,
const std::unordered_map<uint32_t, uint16_t>& section_vrom_map,
const std::unordered_map<recomp_func_t*, recomp::overlays::BasePatchedFunction>& base_patched_funcs,
RegeneratedList& regenlist
) {
using namespace recomp;
using namespace recomp::mods;
std::vector<ModLoadErrorDetails> ret{};
uint32_t cur_section_rom = 0xFFFFFFFF;
uint32_t cur_section_vram = 0xFFFFFFFF;
uint16_t cur_section_index = 0xFFFF;
uint32_t cur_function_vram = 0xFFFFFFFF;
std::span<const RelocEntry> cur_section_relocs = {};
size_t cur_section_reloc_index = 0;
bool cur_func_is_base_patched = false;
recomp::overlays::BasePatchedFunction cur_base_patched = {};
// Collect the unprocessed hooks into a patch list.
// Hooks have been sorted by their section address and function address at this point so they
// can be gathered by section into the patch list.
for (size_t hook_index = 0; hook_index < sorted_unprocessed_hooks.size(); hook_index++) {
const auto& cur_hook = sorted_unprocessed_hooks[hook_index];
const auto& cur_hook_def = cur_hook.first;
size_t cur_hook_slot_index = cur_hook.second;
if (cur_hook_def.section_rom != cur_section_rom) {
// Get the index of the section.
auto find_section_it = section_vrom_map.find(cur_hook_def.section_rom);
if (find_section_it == section_vrom_map.end()) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: 0x" << cur_hook_def.section_rom <<
" func: 0x" << std::setfill('0') << std::setw(8) << cur_hook_def.function_vram;
ret.emplace_back(ModLoadErrorDetails{
"", ModLoadError::FailedToLoadCode, error_to_string(CodeModLoadError::InvalidHook) + ":" + error_param_stream.str()
});
return ret;
}
uint16_t section_index = find_section_it->second;
uint32_t section_ram_addr;
if constexpr (patched_regenlist) {
section_ram_addr = recomp::overlays::get_patch_section_ram_addr(section_index);
cur_section_relocs = recomp::overlays::get_patch_section_relocs(section_index);
}
else {
section_ram_addr = recomp::overlays::get_section_ram_addr(section_index);
cur_section_relocs = recomp::overlays::get_section_relocs(section_index);
}
// Allocate a new section.
auto& section_out = regenlist.sections.emplace_back(RegeneratedSection{
.rom_addr = cur_hook_def.section_rom,
.ram_addr = section_ram_addr,
.original_index = section_index,
.first_func_index = regenlist.functions.size(),
.first_reloc_index = regenlist.relocs.size(),
// Patch sections are never relocatable, so a section is relocatable if it has any relocs and is not a base patch section.
.relocatable = !patched_regenlist && !cur_section_relocs.empty()
});
// Update the tracked section fields.
cur_section_rom = section_out.rom_addr;
cur_section_vram = section_out.ram_addr;
cur_section_index = section_index;
cur_section_reloc_index = 0;
// Reset the tracked function vram to prevent issues when two functions have the same vram in different sections.
cur_function_vram = 0xFFFFFFFF;
}
if (cur_hook_def.function_vram != cur_function_vram) {
uint32_t function_section_offset = cur_hook_def.function_vram - cur_section_vram;
FuncEntry func_entry{};
bool found_func;
cur_func_is_base_patched = false;
if constexpr (patched_regenlist) {
found_func = recomp::overlays::get_patch_func_entry_by_section_index_function_offset(cur_section_index, function_section_offset, func_entry);
}
else {
found_func = recomp::overlays::get_func_entry_by_section_index_function_offset(cur_section_index, function_section_offset, func_entry);
}
if (!found_func) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: 0x" << cur_hook_def.section_rom <<
" func: 0x" << std::setfill('0') << std::setw(8) << cur_hook_def.function_vram;
ret.emplace_back(ModLoadErrorDetails{
"", ModLoadError::FailedToLoadCode, error_to_string(CodeModLoadError::InvalidHook) + ":" + error_param_stream.str()
});
return ret;
}
uint32_t function_rom_size = func_entry.rom_size;
// A size of 0 means the function can't be hooked (e.g. it's a native reimplemented function).
if (function_rom_size == 0) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: 0x" << cur_hook_def.section_rom <<
" func: 0x" << std::setfill('0') << std::setw(8) << cur_hook_def.function_vram;
ret.emplace_back(ModLoadErrorDetails{
"", ModLoadError::FailedToLoadCode, error_to_string(CodeModLoadError::CannotBeHooked) + ":" + error_param_stream.str()
});
return ret;
}
// Check if this function has been patched by the base recomp.
if constexpr (!patched_regenlist) {
auto find_patched_it = base_patched_funcs.find(func_entry.func);
if (find_patched_it != base_patched_funcs.end()) {
cur_func_is_base_patched = true;
cur_base_patched = find_patched_it->second;
}
}
if (!cur_func_is_base_patched) {
// Allocate a new function.
regenlist.functions.emplace_back(RegeneratedFunction{
.section_offset = function_section_offset,
.size = function_rom_size
});
regenlist.func_ptrs.push_back(func_entry.func);
}
// Update the tracked function address.
cur_function_vram = cur_hook_def.function_vram;
// Advance forward in the section's reloc list until reaching this function's offset or the end of the list.
size_t cur_function_offset = cur_function_vram - cur_section_vram;
size_t cur_function_end_offset = cur_function_offset + function_rom_size;
while (true) {
if (cur_section_reloc_index >= cur_section_relocs.size()) {
break;
}
const auto& reloc_in = cur_section_relocs[cur_section_reloc_index];
if (reloc_in.offset >= cur_function_offset) {
break;
}
cur_section_reloc_index++;
}
// Add all relocs until the end of this function or the end of the reloc list.
while (true) {
if (cur_section_reloc_index >= cur_section_relocs.size()) {
break;
}
const auto& reloc_in = cur_section_relocs[cur_section_reloc_index];
if (reloc_in.offset >= cur_function_end_offset) {
break;
}
regenlist.relocs.emplace_back(RegeneratedReloc {
.section_offset = reloc_in.offset,
.target_section = reloc_in.target_section,
.target_section_offset = reloc_in.target_section_offset,
.type = reloc_in.type
});
cur_section_reloc_index++;
}
}
if (cur_func_is_base_patched) {
regenlist.patched_hooks.emplace_back(std::make_pair(cur_base_patched, cur_hook));
}
else {
// Record the hooks in the function to hook mapping.
size_t func_index = regenlist.functions.size() - 1;
if (cur_hook_def.at_return) {
regenlist.return_func_hooks[func_index] = cur_hook_slot_index;
}
else {
regenlist.entry_func_hooks[func_index] = cur_hook_slot_index;
}
}
}
return {};
}
std::unique_ptr<recomp::mods::LiveRecompilerCodeHandle> apply_regenlist(RegeneratedList& regenlist, std::span<const uint8_t> rom) {
using namespace recomp::mods;
std::unique_ptr<LiveRecompilerCodeHandle> regenerated_code_handle{};
// Generate the recompiler context.
N64Recomp::Context hook_context = context_from_regenerated_list(regenlist, rom);
hook_context.set_all_reference_sections_relocatable();
hook_context.use_lookup_for_all_function_calls = true;
// Regenerate the functions using the live recompiler.
ModCodeHandleInputs handle_inputs{
.base_event_index = 0, // No events in vanilla functions, so this doesn't matter.
.recomp_trigger_event = recomp_trigger_event,
.get_function = get_function,
.cop0_status_write = cop0_status_write,
.cop0_status_read = cop0_status_read,
.switch_error = switch_error,
.do_break = do_break,
.reference_section_addresses = section_addresses,
};
std::vector<size_t> original_section_indices{};
original_section_indices.resize(regenlist.sections.size());
for (size_t new_section_index = 0; new_section_index < regenlist.sections.size(); new_section_index++) {
original_section_indices[new_section_index] = regenlist.sections[new_section_index].original_index;
}
regenerated_code_handle = std::make_unique<LiveRecompilerCodeHandle>(hook_context, handle_inputs,
std::move(regenlist.entry_func_hooks), std::move(regenlist.return_func_hooks), std::move(original_section_indices), true);
if (!regenerated_code_handle->good()) {
return {};
}
std::string reference_syms_error_param{};
CodeModLoadError reference_syms_error = regenerated_code_handle->populate_reference_symbols(hook_context, reference_syms_error_param);
if (reference_syms_error != CodeModLoadError::Good) {
return {};
}
// Patch the functions that were regenerated.
for (size_t patched_func_index = 0; patched_func_index < regenlist.func_ptrs.size(); patched_func_index++) {
patch_func(regenlist.func_ptrs[patched_func_index], regenerated_code_handle->get_function_handle(patched_func_index));
}
return regenerated_code_handle;
}
std::vector<recomp::mods::ModLoadErrorDetails> recomp::mods::ModContext::regenerate_with_hooks(
const std::vector<std::pair<HookDefinition, size_t>>& sorted_unprocessed_hooks,
const std::unordered_map<uint32_t, uint16_t>& section_vrom_map,
const std::unordered_map<recomp_func_t*, overlays::BasePatchedFunction>& base_patched_funcs,
std::span<const uint8_t> decompressed_rom
) {
// The output regenerated function list.
RegeneratedList regenlist{};
std::vector<ModLoadErrorDetails> ret = build_regen_list<false>(sorted_unprocessed_hooks, section_vrom_map, base_patched_funcs, regenlist);
if (!ret.empty()) {
return ret;
}
// Apply the regenlist.
if (!regenlist.functions.empty()) {
regenerated_code_handle = apply_regenlist(regenlist, decompressed_rom);
if (!regenerated_code_handle || !regenerated_code_handle->good()) {
regenerated_code_handle.reset();
ret.emplace_back(ModLoadErrorDetails{
"", ModLoadError::FailedToLoadCode, error_to_string(CodeModLoadError::InternalError)
});
return ret;
}
}
if (!regenlist.patched_hooks.empty()) {
// Create new hook definitions based on the actual addresses in the patch binary.
std::vector<std::pair<HookDefinition, size_t>> base_patched_hooks{};
base_patched_hooks.resize(regenlist.patched_hooks.size());
for (size_t i = 0; i < regenlist.patched_hooks.size(); i++) {
const auto& regenlist_entry = regenlist.patched_hooks[i];
uint16_t patch_section_index = static_cast<uint16_t>(regenlist_entry.first.patch_section);
uint32_t patch_section_ram_addr = overlays::get_patch_section_ram_addr(patch_section_index);
const FuncEntry* func_entry = overlays::get_patch_function_entry(patch_section_index, regenlist_entry.first.function_index);
base_patched_hooks[i].first = HookDefinition {
.section_rom = overlays::get_patch_section_rom_addr(patch_section_index),
.function_vram = patch_section_ram_addr + func_entry->offset,
.at_return = regenlist_entry.second.first.at_return
};
base_patched_hooks[i].second = regenlist_entry.second.second;
}
// Sort the hook definitions by rom and ram.
std::sort(base_patched_hooks.begin(), base_patched_hooks.end(),
[](const std::pair<recomp::mods::HookDefinition, size_t>& lhs, const std::pair<recomp::mods::HookDefinition, size_t>& rhs) {
if (lhs.first.section_rom == rhs.first.section_rom) {
return lhs.first.function_vram < rhs.first.function_vram;
}
else {
return lhs.first.section_rom < rhs.first.section_rom;
}
}
);
// Create the regenerated list for the base patched functions.
std::unordered_map<uint32_t, uint16_t> patch_section_vrom_map = overlays::get_patch_vrom_to_section_map();
RegeneratedList patch_regenlist{};
std::vector<ModLoadErrorDetails> ret = build_regen_list<true>(base_patched_hooks, patch_section_vrom_map, {}, patch_regenlist);
if (!ret.empty()) {
return ret;
}
// Apply the patched function regenlist.
base_patched_code_handle = apply_regenlist(patch_regenlist, overlays::get_patch_binary());
if (!base_patched_code_handle || !base_patched_code_handle->good()) {
regenerated_code_handle.reset();
base_patched_code_handle.reset();
ret.emplace_back(ModLoadErrorDetails{
"", ModLoadError::FailedToLoadCode, error_to_string(CodeModLoadError::InternalError)
});
return ret;
}
}
return ret;
}
void recomp::mods::ModContext::check_dependencies(recomp::mods::ModHandle& mod, std::vector<std::pair<recomp::mods::ModLoadError, std::string>>& errors) {
errors.clear();
// Prevent mods with dependencies from being toggled at runtime.
// TODO make this possible.
if (!mod.manifest.dependencies.empty()) {
mod.disable_runtime_toggle();
}
for (const recomp::mods::Dependency& cur_dep : mod.manifest.dependencies) {
if (!cur_dep.optional) {
// Look for the dependency in the loaded mod mapping.
auto find_loaded_dep_it = loaded_mods_by_id.find(cur_dep.mod_id);
if (find_loaded_dep_it != loaded_mods_by_id.end()) {
ModHandle& dep_mod = opened_mods[find_loaded_dep_it->second];
if (cur_dep.version > dep_mod.manifest.version)
{
std::stringstream error_param_stream{};
error_param_stream << "requires mod \"" << cur_dep.mod_id << "\" " <<
(int)cur_dep.version.major << "." << (int)cur_dep.version.minor << "." << (int)cur_dep.version.patch << ", got " <<
(int)dep_mod.manifest.version.major << "." << (int)dep_mod.manifest.version.minor << "." << (int)dep_mod.manifest.version.patch << "";
errors.emplace_back(ModLoadError::WrongDependencyVersion, error_param_stream.str());
}
// Prevent the dependency from being toggled at runtime, as it's required for this mod.
dep_mod.disable_runtime_toggle();
}
// Add an error for this mod if the dependency isn't optional.
else {
errors.emplace_back(ModLoadError::MissingDependency, cur_dep.mod_id);
}
}
}
}
recomp::mods::CodeModLoadError recomp::mods::ModContext::init_mod_code(uint8_t* rdram, const std::unordered_map<uint32_t, uint16_t>& section_vrom_map, ModHandle& mod, int32_t load_address, bool hooks_available, uint32_t& ram_used, std::string& error_param) {
// Load the mod symbol data from the file provided in the manifest.
bool binary_syms_exists = false;
std::vector<char> syms_data = mod.manifest.file_handle->read_file(std::string{ modpaths::binary_syms_path }, binary_syms_exists);
// Load the binary data from the file provided in the manifest.
bool binary_exists = false;
std::vector<char> binary_data = mod.manifest.file_handle->read_file(std::string{ modpaths::binary_path }, binary_exists);
if (binary_syms_exists && !binary_exists) {
return CodeModLoadError::HasSymsButNoBinary;
}
if (binary_exists && !binary_syms_exists) {
return CodeModLoadError::HasBinaryButNoSyms;
}
std::span<uint8_t> binary_span {reinterpret_cast<uint8_t*>(binary_data.data()), binary_data.size() };
// Parse the symbol file into the recompiler context.
N64Recomp::ModSymbolsError symbol_load_error = N64Recomp::parse_mod_symbols(syms_data, binary_span, section_vrom_map, *mod.recompiler_context);
if (symbol_load_error != N64Recomp::ModSymbolsError::Good) {
return CodeModLoadError::FailedToParseSyms;
}
// Prevent loading the mod if hooks aren't available and it has any hooks.
if (!hooks_available && !mod.recompiler_context->hooks.empty()) {
return CodeModLoadError::HooksUnavailable;
}
// Set all reference sections as relocatable, since the only relocations present in a mod's context
// are ones that target relocatable sections.
mod.recompiler_context->set_all_reference_sections_relocatable();
// Disable validation of reference symbols (so we can skip populating them). Validation will still happen
// later on in the live recompilation process.
mod.recompiler_context->skip_validating_reference_symbols = true;
// Populate the mod's export map.
mod.populate_exports();
// Populate the mod's event map and set its base event index.
mod.populate_events();
// Validate that the dependencies present in the symbol file are all present in the mod's manifest as well.
for (const auto& [cur_dep_id, cur_dep_index] : mod.recompiler_context->dependencies_by_name) {
// Handle special dependency names.
if (cur_dep_id == N64Recomp::DependencyBaseRecomp || cur_dep_id == N64Recomp::DependencySelf) {
continue;
}
// Find the dependency in the mod manifest to get its version.
auto find_manifest_dep_it = mod.manifest.dependencies_by_id.find(cur_dep_id);
if (find_manifest_dep_it == mod.manifest.dependencies_by_id.end()) {
return CodeModLoadError::MissingDependencyInManifest;
}
}
const std::vector<N64Recomp::Section>& mod_sections = mod.recompiler_context->sections;
mod.section_load_addresses.resize(mod_sections.size());
// Copy each section's binary into rdram, leaving room for the section's bss before the next one.
int32_t cur_section_addr = load_address;
for (size_t section_index = 0; section_index < mod_sections.size(); section_index++) {
const auto& section = mod_sections[section_index];
// Do not load fixed address sections into mod memory. Use their address as-is.
if (section.fixed_address) {
mod.section_load_addresses[section_index] = section.ram_addr;
}
else {
for (size_t i = 0; i < section.size; i++) {
MEM_B(i, (gpr)cur_section_addr) = binary_data[section.rom_addr + i];
}
mod.section_load_addresses[section_index] = cur_section_addr;
// Calculate the bss section's address based on the size of this section.
cur_section_addr += section.size;
// Zero the bss section.
for (size_t i = 0; i < section.bss_size; i++) {
MEM_B(i, (gpr)cur_section_addr) = 0;
}
// Calculate the next section's address based on the size of the bss section.
cur_section_addr += section.bss_size;
// Align the next section's address to 16 bytes.
cur_section_addr = (cur_section_addr + 15) & ~15;
// Add some empty space between mods to act as a buffer for misbehaving mods that have out of bounds accesses.
cur_section_addr += 0x400;
}
}
// Iterate over each section again after loading them to perform R_MIPS_32 relocations.
for (size_t section_index = 0; section_index < mod_sections.size(); section_index++) {
const auto& section = mod_sections[section_index];
uint32_t cur_section_original_vram = section.ram_addr;
uint32_t cur_section_loaded_vram = mod.section_load_addresses[section_index];
// Perform mips32 relocations for this section.
for (const auto& reloc : section.relocs) {
if (reloc.type == N64Recomp::RelocType::R_MIPS_32 && !reloc.reference_symbol) {
if (reloc.target_section >= mod_sections.size()) {
return CodeModLoadError::FailedToParseSyms;
}
// Get the ram address of the word that's being relocated and read its original value.
int32_t reloc_word_addr = reloc.address - cur_section_original_vram + cur_section_loaded_vram;
uint32_t reloc_word = MEM_W(0, reloc_word_addr);
// Determine the original and loaded addresses of the section that the relocation points to.
uint32_t target_section_original_vram = mod_sections[reloc.target_section].ram_addr;
uint32_t target_section_loaded_vram = mod.section_load_addresses[reloc.target_section];
// Recalculate the word and write it back into ram.
reloc_word += (target_section_loaded_vram - target_section_original_vram);
MEM_W(0, reloc_word_addr) = reloc_word;
}
}
}
ram_used = cur_section_addr - load_address;
// Allocate the event indices used by the mod.
num_events += mod.num_events();
// Read the mod's hooks and allocate hook slots as needed.
for (const N64Recomp::FunctionHook& hook : mod.recompiler_context->hooks) {
// Get the definition of this hook.
HookDefinition def {
.section_rom = hook.original_section_vrom,
.function_vram = hook.original_vram,
.at_return = (hook.flags & N64Recomp::HookFlags::AtReturn) == N64Recomp::HookFlags::AtReturn
};
// Check if the hook definition already exists in the hook slots.
auto find_it = hook_slots.find(def);
if (find_it == hook_slots.end()) {
// The hook definition is new, so assign a hook slot index and add it to the slots.
hook_slots.emplace(def, hook_slots.size());
}
}
// Copy the mod's binary into the recompiler context so it can be analyzed during code loading.
// TODO move it instead, right now the move can't be done because of a signedness difference in the types.
mod.recompiler_context->rom.assign(binary_span.begin(), binary_span.end());
return CodeModLoadError::Good;
}
recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* rdram, recomp::mods::ModHandle& mod, uint32_t base_event_index, std::string& error_param) {
// Build the hook list for this mod. Maps function index within mod to hook slot index.
std::unordered_map<size_t, size_t> entry_func_hooks{};
std::unordered_map<size_t, size_t> return_func_hooks{};
// Scan the replacements to handle hooks on the replaced functions.
for (const auto& replacement : mod.recompiler_context->replacements) {
// Check if there's a hook slot for the entry of this function.
HookDefinition entry_def {
.section_rom = replacement.original_section_vrom,
.function_vram = replacement.original_vram,
.at_return = false
};
auto find_entry_it = hook_slots.find(entry_def);
if (find_entry_it != hook_slots.end()) {
entry_func_hooks.emplace(replacement.func_index, find_entry_it->second);
processed_hook_slots[find_entry_it->second] = true;
}
// Check if there's a hook slot for the return of this function.
HookDefinition return_def {
.section_rom = replacement.original_section_vrom,
.function_vram = replacement.original_vram,
.at_return = true
};
auto find_return_it = hook_slots.find(return_def);
if (find_return_it != hook_slots.end()) {
return_func_hooks.emplace(replacement.func_index, find_return_it->second);
processed_hook_slots[find_return_it->second] = true;
}
}
// Build the inputs for the mod code handle.
std::string cur_error_param;
CodeModLoadError cur_error;
ModCodeHandleInputs handle_inputs{
.base_event_index = base_event_index,
.recomp_trigger_event = recomp_trigger_event,
.get_function = get_function,
.cop0_status_write = cop0_status_write,
.cop0_status_read = cop0_status_read,
.switch_error = switch_error,
.do_break = do_break,
.reference_section_addresses = section_addresses,
};
// Use a dynamic library code handle. This feature isn't meant to be used by end users, but provides a more debuggable
// experience than the live recompiler for mod developers.
// Enabled if the mod's filename ends with ".offline.nrm".
if (mod.manifest.mod_root_path.filename().string().ends_with(".offline.nrm")) {
// Hooks can't be generated for native mods, so return an error if any of the functions this mod replaces are also hooked by another mod.
if (!entry_func_hooks.empty() || !return_func_hooks.empty()) {
return CodeModLoadError::OfflineModHooked;
}
std::filesystem::path dll_path = mod.manifest.mod_root_path;
dll_path.replace_extension(DynamicLibrary::PlatformExtension);
mod.code_handle = std::make_unique<DynamicLibraryCodeHandle>(dll_path, *mod.recompiler_context, handle_inputs);
if (!mod.code_handle->good()) {
mod.code_handle.reset();
error_param = dll_path.string();
return CodeModLoadError::FailedToLoadNativeCode;
}
cur_error = validate_api_version(mod.code_handle->get_api_version(), cur_error_param);
if (cur_error != CodeModLoadError::Good) {
if (cur_error_param.empty()) {
error_param = dll_path.filename().string();
}
else {
error_param = dll_path.filename().string() + ":" + std::move(cur_error_param);
}
return cur_error;
}
}
// Live recompiler code handle.
else {
mod.code_handle = std::make_unique<LiveRecompilerCodeHandle>(*mod.recompiler_context, handle_inputs,
std::move(entry_func_hooks), std::move(return_func_hooks), std::vector<size_t>{}, false);
if (!mod.code_handle->good()) {
mod.code_handle.reset();
error_param = {};
return CodeModLoadError::FailedToRecompile;
}
}
// Load any native libraries specified by the mod and validate/register the expors.
std::filesystem::path parent_path = mod.manifest.mod_root_path.parent_path();
for (const recomp::mods::NativeLibraryManifest& cur_lib_manifest: mod.manifest.native_libraries) {
cur_error = mod.load_native_library(cur_lib_manifest, cur_error_param);
if (cur_error != CodeModLoadError::Good) {
error_param = std::move(cur_error_param);
return cur_error;
}
}
const std::vector<N64Recomp::Section>& mod_sections = mod.recompiler_context->sections;
// Add each function from the mod into the function lookup table.
for (size_t func_index = 0; func_index < mod.recompiler_context->functions.size(); func_index++) {
const auto& func = mod.recompiler_context->functions[func_index];
if (func.section_index >= mod_sections.size()) {
return CodeModLoadError::FailedToParseSyms;
}
// Calculate the loaded address of this function.
int32_t func_address = func.vram - mod_sections[func.section_index].ram_addr + mod.section_load_addresses[func.section_index];
// Get the handle to the function and add it to the lookup table based on its type.
recomp::mods::GenericFunction func_handle = mod.code_handle->get_function_handle(func_index);
std::visit(overloaded{
[func_address](recomp_func_t* native_func) {
recomp::overlays::add_loaded_function(func_address, native_func);
}
}, func_handle);
}
return CodeModLoadError::Good;
}
recomp::mods::CodeModLoadError recomp::mods::ModContext::resolve_code_dependencies(recomp::mods::ModHandle& mod, size_t mod_index, const std::unordered_map<recomp_func_t*, overlays::BasePatchedFunction>& base_patched_funcs, std::string& error_param) {
// Reference symbols.
std::string reference_syms_error_param{};
CodeModLoadError reference_syms_error = mod.code_handle->populate_reference_symbols(*mod.recompiler_context, reference_syms_error_param);
if (reference_syms_error != CodeModLoadError::Good) {
error_param = std::move(reference_syms_error_param);
return reference_syms_error;
}
// Create a list of dependencies ordered by their index in the recompiler context.
std::vector<std::string> dependencies_ordered{};
dependencies_ordered.resize(mod.recompiler_context->dependencies_by_name.size());
for (const auto& [dependency, dependency_index] : mod.recompiler_context->dependencies_by_name) {
dependencies_ordered[dependency_index] = dependency;
}
// Imported symbols.
for (size_t import_index = 0; import_index < mod.recompiler_context->import_symbols.size(); import_index++) {
const N64Recomp::ImportSymbol& imported_func = mod.recompiler_context->import_symbols[import_index];
const std::string& dependency_id = dependencies_ordered[imported_func.dependency_index];
GenericFunction func_handle{};
bool did_find_func = false;
if (dependency_id == N64Recomp::DependencyBaseRecomp) {
recomp_func_t* func_ptr = recomp::overlays::get_base_export(imported_func.base.name);
did_find_func = func_ptr != nullptr;
if (!did_find_func) {
recomp_func_ext_t* func_ext_ptr = recomp::overlays::get_ext_base_export(imported_func.base.name);
did_find_func = func_ext_ptr != nullptr;
if (did_find_func) {
func_ptr = shim_functions.emplace_back(std::make_unique<N64Recomp::ShimFunction>(func_ext_ptr, mod_index)).get()->get_func();
}
}
func_handle = func_ptr;
}
else if (dependency_id == N64Recomp::DependencySelf) {
did_find_func = mod.get_export_function(imported_func.base.name, func_handle);
}
else {
auto find_mod_it = loaded_mods_by_id.find(dependency_id);
if (find_mod_it != loaded_mods_by_id.end()) {
const auto& dependency = opened_mods[find_mod_it->second];
did_find_func = dependency.get_export_function(imported_func.base.name, func_handle);
}
else {
auto find_optional_it = mod.manifest.dependencies_by_id.find(dependency_id);
if (find_optional_it != mod.manifest.dependencies_by_id.end()) {
uintptr_t shim_argument = ((import_index & 0xFFFFFFFFu) << 32) | (mod_index & 0xFFFFFFFFu);
func_handle = shim_functions.emplace_back(std::make_unique<N64Recomp::ShimFunction>(unmet_dependency_handler, shim_argument)).get()->get_func();
did_find_func = true;
}
else {
error_param = "Failed to find import dependency while loading code: " + dependency_id;
// This should never happen, as dependencies are scanned before mod code is loaded and the symbol dependency list
// is validated against the manifest's.
return CodeModLoadError::InternalError;
}
}
}
if (!did_find_func) {
error_param = dependency_id + ":" + imported_func.base.name;
return CodeModLoadError::InvalidImport;
}
mod.code_handle->set_imported_function(import_index, func_handle);
}
// Register callbacks.
for (const N64Recomp::Callback& callback : mod.recompiler_context->callbacks) {
const N64Recomp::DependencyEvent& dependency_event = mod.recompiler_context->dependency_events[callback.dependency_event_index];
const std::string& dependency_id = dependencies_ordered[dependency_event.dependency_index];
GenericFunction func = mod.code_handle->get_function_handle(callback.function_index);
size_t event_index = 0;
bool did_find_event = false;
if (dependency_id == N64Recomp::DependencyBaseRecomp) {
event_index = recomp::overlays::get_base_event_index(dependency_event.event_name);
if (event_index != (size_t)-1) {
did_find_event = true;
}
}
else if (dependency_id == N64Recomp::DependencySelf) {
did_find_event = mod.get_global_event_index(dependency_event.event_name, event_index);
}
else {
auto find_mod_it = loaded_mods_by_id.find(dependency_id);
if (find_mod_it == loaded_mods_by_id.end()) {
error_param = "Failed to find callback dependency while loading code: " + dependency_id;
// This should never happen, as dependencies are scanned before mod code is loaded and the symbol dependency list
// is validated against the manifest's.
return CodeModLoadError::InternalError;
}
const auto& dependency_mod = opened_mods[find_mod_it->second];
did_find_event = dependency_mod.get_global_event_index(dependency_event.event_name, event_index);
}
if (!did_find_event) {
error_param = dependency_id + ":" + dependency_event.event_name;
return CodeModLoadError::InvalidCallbackEvent;
}
recomp::mods::register_event_callback(event_index, mod_index, func);
}
// Register hooks.
for (const auto& cur_hook : mod.recompiler_context->hooks) {
// Get the definition of this hook.
HookDefinition def {
.section_rom = cur_hook.original_section_vrom,
.function_vram = cur_hook.original_vram,
.at_return = (cur_hook.flags & N64Recomp::HookFlags::AtReturn) == N64Recomp::HookFlags::AtReturn
};
// Find the hook's slot from the definition.
auto find_it = hook_slots.find(def);
if (find_it == hook_slots.end()) {
error_param = "Failed to register hook";
// This should never happen, as hooks are scanned earlier to generate hook_slots.
return CodeModLoadError::InternalError;
}
// Register the function handle for this hook slot.
GenericFunction func = mod.code_handle->get_function_handle(cur_hook.func_index);
recomp::mods::register_hook(find_it->second, mod_index, func);
}
// Populate the relocated section addresses for the mod.
for (size_t section_index = 0; section_index < mod.section_load_addresses.size(); section_index++) {
mod.code_handle->set_local_section_address(section_index, mod.section_load_addresses[section_index]);
}
// Apply all the function replacements in the mod.
for (const auto& replacement : mod.recompiler_context->replacements) {
recomp_func_t* to_replace = recomp::overlays::get_func_by_section_rom_function_vram(replacement.original_section_vrom, replacement.original_vram);
if (to_replace == nullptr) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: 0x" << replacement.original_section_vrom <<
" func: 0x" << std::setfill('0') << std::setw(8) << replacement.original_vram;
error_param = error_param_stream.str();
return CodeModLoadError::InvalidFunctionReplacement;
}
// Check if this function has already been patched by the base recomp, but allow it if this is a force patch.
if ((replacement.flags & N64Recomp::ReplacementFlags::Force) == N64Recomp::ReplacementFlags(0)) {
auto find_it = base_patched_funcs.find(to_replace);
if (find_it != base_patched_funcs.end()) {
std::stringstream error_param_stream{};
error_param_stream << std::hex <<
"section: 0x" << replacement.original_section_vrom <<
" func: 0x" << std::setfill('0') << std::setw(8) << replacement.original_vram;
error_param = error_param_stream.str();
return CodeModLoadError::BaseRecompConflict;
}
}
// Check if this function has already been replaced.
auto find_patch_it = patched_funcs.find(to_replace);
if (find_patch_it != patched_funcs.end()) {
error_param = find_patch_it->second.mod_id;
return CodeModLoadError::ModConflict;
}
// Copy the original bytes so they can be restored later after the mod is unloaded.
PatchData& cur_replacement_data = patched_funcs[to_replace];
memcpy(cur_replacement_data.replaced_bytes.data(), reinterpret_cast<void*>(to_replace), cur_replacement_data.replaced_bytes.size());
cur_replacement_data.mod_id = mod.manifest.mod_id;
// Patch the function to redirect it to the replacement.
patch_func(to_replace, mod.code_handle->get_function_handle(replacement.func_index));
}
return CodeModLoadError::Good;
}
void recomp::mods::ModContext::unload_mods() {
for (auto& [replacement_func, replacement_data] : patched_funcs) {
unpatch_func(reinterpret_cast<void*>(replacement_func), replacement_data);
}
patched_funcs.clear();
loaded_mods_by_id.clear();
hook_slots.clear();
processed_hook_slots.clear();
shim_functions.clear();
recomp::mods::reset_events();
recomp::mods::reset_hooks();
num_events = recomp::overlays::num_base_events();
active_game = (size_t)-1;
}
void recomp::mods::unmet_dependency_handler(uint8_t* rdram, recomp_context* ctx, uintptr_t arg) {
size_t caller_mod_index = (arg >> 0) & uint64_t(0xFFFFFFFF);
size_t import_index = (arg >> 32) & uint64_t(0xFFFFFFFF);
std::string mod_name = recomp::mods::get_mod_display_name(caller_mod_index);
std::pair<std::string, std::string> import_info = recomp::mods::get_mod_import_info(caller_mod_index, import_index);
ultramodern::error_handling::message_box(
(
"Fatal error in mod \"" + mod_name + "\": Called function \"" + import_info.second + "\" in unmet optional dependency \"" + import_info.first + "\".\n"
).c_str()
);
ULTRAMODERN_QUICK_EXIT();
}