Persist mod order and enable.

This commit is contained in:
Dario 2025-01-22 23:27:36 -03:00 committed by Mr-Wiseguy
parent 240c3f566d
commit 7ca672b196
4 changed files with 267 additions and 37 deletions

View file

@ -259,6 +259,7 @@ namespace recomp {
};
std::vector<ModDetails> get_mod_details(const std::string& mod_game_id);
void set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index);
// Internal functions, TODO move to an internal header.
struct PatchData {
@ -300,6 +301,20 @@ namespace recomp {
bool requires_manifest;
};
struct ModConfigQueueSaveMod {
std::string mod_id;
};
struct ModConfigQueueSave {
uint32_t pad;
};
struct ModConfigQueueEnd {
uint32_t pad;
};
typedef std::variant<ModConfigQueueSaveMod, ModConfigQueueSave, ModConfigQueueEnd> ModConfigQueueVariant;
class LiveRecompilerCodeHandle;
class ModContext {
public:
@ -308,16 +323,19 @@ namespace recomp {
void register_game(const std::string& mod_game_id);
std::vector<ModOpenErrorDetails> scan_mod_folder(const std::filesystem::path& mod_folder);
void enable_mod(const std::string& mod_id, bool enabled);
void load_mods_config();
void enable_mod(const std::string& mod_id, bool enabled, bool trigger_save);
bool is_mod_enabled(const std::string& mod_id);
size_t num_opened_mods();
std::vector<ModLoadErrorDetails> load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used);
void unload_mods();
std::vector<ModDetails> get_mod_details(const std::string& mod_game_id);
void set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index);
const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const;
void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value);
ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id);
void set_mod_config_path(const std::filesystem::path &path);
void set_mods_config_path(const std::filesystem::path &path);
void set_mod_config_directory(const std::filesystem::path &path);
ModContentTypeId register_content_type(const ModContentType& type);
bool register_container_type(const std::string& extension, const std::vector<ModContentTypeId>& content_types, bool requires_manifest);
ModContentTypeId get_code_content_type() const { return code_content_type_id; }
@ -346,14 +364,16 @@ namespace recomp {
std::unordered_map<std::string, size_t> mod_game_ids;
std::vector<ModHandle> opened_mods;
std::unordered_map<std::string, size_t> opened_mods_by_id;
std::vector<size_t> opened_mods_order;
std::mutex opened_mods_mutex;
std::unordered_set<std::string> mod_ids;
std::unordered_set<std::string> enabled_mods;
std::unordered_map<recomp_func_t*, PatchData> patched_funcs;
std::unordered_map<std::string, size_t> loaded_mods_by_id;
std::unique_ptr<std::thread> dirty_mod_configuration_thread;
moodycamel::BlockingConcurrentQueue<std::string> dirty_mod_configuration_thread_queue;
std::filesystem::path mod_config_path;
std::unique_ptr<std::thread> mod_configuration_thread;
moodycamel::BlockingConcurrentQueue<ModConfigQueueVariant> mod_configuration_thread_queue;
std::filesystem::path mods_config_path;
std::filesystem::path mod_config_directory;
std::mutex mod_config_storage_mutex;
std::vector<size_t> loaded_code_mods;
// Code handle for vanilla code that was regenerated to add hooks.

View file

@ -839,7 +839,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys
// Read the mod config if it exists.
ConfigStorage config_storage;
std::filesystem::path config_path = mod_config_path / (manifest.mod_id + ".json");
std::filesystem::path config_path = mod_config_directory / (manifest.mod_id + ".json");
parse_mod_config_storage(config_path, manifest.mod_id, config_storage, manifest.config_schema);
// Store the loaded mod manifest in a new mod handle.

View file

@ -10,6 +10,58 @@
#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
@ -546,6 +598,7 @@ void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStor
size_t mod_index = opened_mods.size();
opened_mods_by_id.emplace(manifest.mod_id, mod_index);
opened_mods.emplace_back(*this, std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types));
opened_mods_order.emplace_back(mod_index);
}
recomp::mods::ModLoadError recomp::mods::ModContext::load_mod(recomp::mods::ModHandle& mod, std::string& error_param) {
@ -573,17 +626,19 @@ void recomp::mods::ModContext::close_mods() {
std::unique_lock lock(opened_mods_mutex);
opened_mods_by_id.clear();
opened_mods.clear();
opened_mods_order.clear();
mod_ids.clear();
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) {
nlohmann::json config_json;
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();
nlohmann::json &storage_json = config_json["storage"];
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()) {
@ -618,34 +673,78 @@ bool save_mod_config_storage(const std::filesystem::path &path, const std::strin
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;
std::string mod_id;
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;
while (active) {
// Wait for at least one mod to require writing.
dirty_mod_configuration_thread_queue.wait_dequeue(mod_id);
if (!mod_id.empty()) {
pending_mods.emplace(mod_id);
}
else {
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;
}
};
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 && dirty_mod_configuration_thread_queue.wait_dequeue_timed(mod_id, 1s)) {
if (!mod_id.empty()) {
pending_mods.emplace(mod_id);
}
else {
active = false;
}
while (active && mod_configuration_thread_queue.wait_dequeue_timed(variant, 1s)) {
handle_variant(variant);
}
if (active && !pending_mods.empty()) {
@ -664,9 +763,26 @@ void recomp::mods::ModContext::dirty_mod_configuration_thread_process() {
}
for (const std::string &id : pending_mods) {
config_path = mod_config_path / std::string(id + ".json");
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;
}
}
}
@ -709,6 +825,38 @@ std::vector<recomp::mods::ModOpenErrorDetails> recomp::mods::ModContext::scan_mo
return ret;
}
void recomp::mods::ModContext::load_mods_config() {
std::unordered_set<std::string> config_enabled_mods;
std::vector<std::string> config_mod_order;
bool parsed = parse_mods_config(mods_config_path, config_enabled_mods, config_mod_order);
if (parsed) {
for (const std::string &mod_id : config_enabled_mods) {
enable_mod(mod_id, true, false);
}
{
std::unique_lock lock(opened_mods_mutex);
// 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());
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;
}
}
// 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];
});
}
}
}
recomp::mods::ModContext::ModContext() {
// Register the code content type.
ModContentType code_content_type {
@ -722,7 +870,7 @@ recomp::mods::ModContext::ModContext() {
// 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);
dirty_mod_configuration_thread = std::make_unique<std::thread>(&ModContext::dirty_mod_configuration_thread_process, this);
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) {
@ -736,9 +884,9 @@ void recomp::mods::ModContext::on_code_mod_enabled(ModContext& context, const Mo
}
recomp::mods::ModContext::~ModContext() {
dirty_mod_configuration_thread_queue.enqueue(std::string());
dirty_mod_configuration_thread->join();
dirty_mod_configuration_thread.reset();
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) {
@ -785,8 +933,9 @@ bool recomp::mods::ModContext::is_content_runtime_toggleable(ModContentTypeId co
return content_types[content_type.value].allow_runtime_toggle;
}
void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enabled) {
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;
@ -823,6 +972,10 @@ void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enable
}
}
}
if (trigger_save) {
mod_configuration_thread_queue.enqueue(ModConfigQueueSave());
}
}
bool recomp::mods::ModContext::is_mod_enabled(const std::string& mod_id) {
@ -833,7 +986,7 @@ size_t recomp::mods::ModContext::num_opened_mods() {
return opened_mods.size();
}
std::vector<recomp::mods::ModDetails> recomp::mods::ModContext::get_mod_details(const std::string& mod_game_id) {
std::vector<recomp::mods::ModDetails> recomp::mods::ModContext::get_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;
@ -843,7 +996,8 @@ std::vector<recomp::mods::ModDetails> recomp::mods::ModContext::get_mod_details(
game_index = find_game_it->second;
}
for (const ModHandle& mod : opened_mods) {
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{};
@ -984,6 +1138,50 @@ N64Recomp::Context context_from_regenerated_list(const RegeneratedList& regenlis
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);
}
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);
@ -1036,7 +1234,7 @@ void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, c
}
// Notify the asynchronous thread it should save the configuration for this mod.
dirty_mod_configuration_thread_queue.enqueue(mod_id);
mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod(mod_id));
}
recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) {
@ -1074,8 +1272,12 @@ recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(
}
}
void recomp::mods::ModContext::set_mod_config_path(const std::filesystem::path &path) {
mod_config_path = path;
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) {

View file

@ -86,7 +86,8 @@ void recomp::mods::initialize_mods() {
N64Recomp::live_recompiler_init();
std::filesystem::create_directories(config_path / mods_directory);
std::filesystem::create_directories(config_path / mod_config_directory);
mod_context->set_mod_config_path(config_path / mod_config_directory);
mod_context->set_mods_config_path(config_path / "mods.json");
mod_context->set_mod_config_directory(config_path / mod_config_directory);
}
void recomp::mods::scan_mods() {
@ -98,6 +99,8 @@ void recomp::mods::scan_mods() {
for (const auto& cur_error : mod_open_errors) {
printf("Error opening mod " PATHFMT ": %s (%s)\n", cur_error.mod_path.c_str(), recomp::mods::error_to_string(cur_error.error).c_str(), cur_error.error_param.c_str());
}
mod_context->load_mods_config();
}
recomp::mods::ModContentTypeId recomp::mods::register_mod_content_type(const ModContentType& type) {
@ -500,7 +503,7 @@ void ultramodern::quit() {
void recomp::mods::enable_mod(const std::string& mod_id, bool enabled) {
std::lock_guard lock { mod_context_mutex };
return mod_context->enable_mod(mod_id, enabled);
return mod_context->enable_mod(mod_id, enabled, true);
}
bool recomp::mods::is_mod_enabled(const std::string& mod_id) {
@ -533,6 +536,11 @@ std::vector<recomp::mods::ModDetails> recomp::mods::get_mod_details(const std::s
return mod_context->get_mod_details(mod_game_id);
}
void recomp::mods::set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index) {
std::lock_guard lock{ mod_context_mutex };
return mod_context->set_mod_index(mod_game_id, mod_id, index);
}
bool wait_for_game_started(uint8_t* rdram, recomp_context* context) {
game_status.wait(GameStatus::None);