mirror of
https://github.com/N64Recomp/N64ModernRuntime.git
synced 2025-10-30 08:02:29 +00:00
Separate configuration into librecomp/config
This commit is contained in:
parent
442b466985
commit
58b023750e
10 changed files with 1293 additions and 297 deletions
|
|
@ -9,6 +9,8 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||
add_library(librecomp STATIC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/ai.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/cont.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/config_option.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/config.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/dp.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/euc-jp.cpp"
|
||||
|
|
|
|||
326
librecomp/include/librecomp/config.hpp
Normal file
326
librecomp/include/librecomp/config.hpp
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
#ifndef __RECOMP_CONFIG_HPP__
|
||||
#define __RECOMP_CONFIG_HPP__
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <functional>
|
||||
|
||||
#include <json/json.hpp>
|
||||
|
||||
#include "recomp.h"
|
||||
|
||||
namespace recomp {
|
||||
namespace config {
|
||||
enum class ConfigOptionType {
|
||||
None,
|
||||
Enum,
|
||||
Number,
|
||||
String,
|
||||
Bool
|
||||
};
|
||||
|
||||
struct ConfigOptionEnumOption {
|
||||
uint32_t value;
|
||||
std::string key;
|
||||
std::string name;
|
||||
|
||||
template <typename ENUM_TYPE = uint32_t>
|
||||
ConfigOptionEnumOption(ENUM_TYPE value, std::string key, std::string name)
|
||||
: value(static_cast<uint32_t>(value)), key(key), name(name) {}
|
||||
|
||||
template <typename ENUM_TYPE = uint32_t>
|
||||
ConfigOptionEnumOption(ENUM_TYPE value, std::string key)
|
||||
: value(static_cast<uint32_t>(value)), key(key), name(key) {}
|
||||
};
|
||||
|
||||
struct ConfigOptionEnum {
|
||||
std::vector<ConfigOptionEnumOption> options;
|
||||
uint32_t default_value = 0;
|
||||
|
||||
// Case insensitive search for an option based on a key string. (Matches against options[n].key)
|
||||
std::vector<ConfigOptionEnumOption>::const_iterator find_option_from_string(const std::string& option_key) const;
|
||||
// Search for an option that has a specific value. (Matches against options[n].value)
|
||||
std::vector<ConfigOptionEnumOption>::const_iterator find_option_from_value(uint32_t value) const;
|
||||
// Verify an option has a unique key and a unique value
|
||||
bool can_add_option(const std::string& option_key, uint32_t option_value) const;
|
||||
};
|
||||
|
||||
struct ConfigOptionNumber {
|
||||
double min = 0.0;
|
||||
double max = 0.0;
|
||||
double step = 0.0;
|
||||
int precision = 0;
|
||||
bool percent = false;
|
||||
double default_value = 0.0;
|
||||
};
|
||||
|
||||
struct ConfigOptionString {
|
||||
std::string default_value;
|
||||
};
|
||||
|
||||
struct ConfigOptionBool {
|
||||
bool default_value;
|
||||
};
|
||||
|
||||
typedef std::variant<ConfigOptionEnum, ConfigOptionNumber, ConfigOptionString, ConfigOptionBool> ConfigOptionVariant;
|
||||
|
||||
struct ConfigOption {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
bool hidden = false;
|
||||
ConfigOptionType type;
|
||||
ConfigOptionVariant variant;
|
||||
};
|
||||
|
||||
typedef std::variant<std::monostate, uint32_t, double, std::string, bool> ConfigValueVariant;
|
||||
|
||||
// Manages value dependencies between config options (e.g. option is hidden or disabled from other option being a certain value) .
|
||||
class ConfigOptionDependency {
|
||||
private:
|
||||
// Maps options to the options that are affected by their values
|
||||
std::unordered_map<size_t, std::unordered_set<size_t>> option_to_dependencies = {};
|
||||
// Maps dependent options to the values that the source option can be
|
||||
std::unordered_map<size_t, std::vector<ConfigValueVariant>> dependency_to_values = {};
|
||||
public:
|
||||
ConfigOptionDependency() = default;
|
||||
|
||||
// Add dependency. When <source_option> is one of the <values>, <dependent_option> is affected.
|
||||
void add_option_dependency(size_t dependent_option_index, size_t source_option_index, std::vector<ConfigValueVariant> &values);
|
||||
|
||||
// Check which dependent options are affected by the value of the source option.
|
||||
// Returns a map of dependent options and if they are a match
|
||||
std::unordered_map<size_t, bool> check_option_dependencies(size_t source_option_index, ConfigValueVariant value);
|
||||
};
|
||||
|
||||
struct ConfigSchema {
|
||||
std::vector<ConfigOption> options;
|
||||
std::unordered_map<std::string, size_t> options_by_id;
|
||||
ConfigOptionDependency disable_dependencies;
|
||||
ConfigOptionDependency hidden_dependencies;
|
||||
};
|
||||
|
||||
struct ConfigStorage {
|
||||
std::unordered_map<std::string, ConfigValueVariant> value_map;
|
||||
};
|
||||
|
||||
enum class ConfigOptionUpdateType {
|
||||
Disabled,
|
||||
Hidden,
|
||||
EnumDetails,
|
||||
EnumDisabled,
|
||||
Value,
|
||||
Description
|
||||
};
|
||||
|
||||
struct ConfigOptionUpdateContext {
|
||||
size_t option_index;
|
||||
std::vector<ConfigOptionUpdateType> updates = {};
|
||||
};
|
||||
|
||||
enum class OptionChangeContext {
|
||||
Load,
|
||||
Temporary,
|
||||
Permanent
|
||||
};
|
||||
|
||||
using on_option_change_callback = std::function<void(
|
||||
ConfigValueVariant cur_value,
|
||||
ConfigValueVariant prev_value,
|
||||
OptionChangeContext change_context
|
||||
)>;
|
||||
|
||||
using parse_option_func = std::function<ConfigValueVariant(const nlohmann::json&)>;
|
||||
using serialize_option_func = std::function<nlohmann::json(const ConfigValueVariant&)>;
|
||||
|
||||
class Config {
|
||||
public:
|
||||
std::string name;
|
||||
// id is used for the file name (e.g. general.json) and storing keys
|
||||
std::string id;
|
||||
// If true, any configuration changes are temporarily stored until Apply is pressed.
|
||||
// Changing the tab will prompt the user to either apply or cancel changes.
|
||||
bool requires_confirmation = false;
|
||||
|
||||
std::unordered_set<size_t> modified_options = {};
|
||||
|
||||
// For base game configs
|
||||
Config(std::string name, std::string id, bool requires_confirmation = false);
|
||||
// For mod configs
|
||||
Config();
|
||||
|
||||
void set_id(const std::string &id);
|
||||
void set_mod_version(const std::string &mod_version);
|
||||
|
||||
void add_option(const ConfigOption& option);
|
||||
|
||||
void add_enum_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
const std::vector<ConfigOptionEnumOption> &options,
|
||||
uint32_t default_value,
|
||||
bool hidden = false
|
||||
);
|
||||
|
||||
template <typename ENUM_TYPE>
|
||||
void add_enum_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
const std::vector<ConfigOptionEnumOption> &options,
|
||||
ENUM_TYPE default_value,
|
||||
bool hidden = false
|
||||
) {
|
||||
add_enum_option(id, name, description, options, static_cast<uint32_t>(default_value), hidden);
|
||||
};
|
||||
|
||||
void add_number_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
double min = 0,
|
||||
double max = 0,
|
||||
double step = 1,
|
||||
int precision = 0,
|
||||
bool percent = false,
|
||||
double default_value = 0,
|
||||
bool hidden = false
|
||||
);
|
||||
|
||||
void add_string_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
const std::string &default_value,
|
||||
bool hidden = false
|
||||
);
|
||||
|
||||
void add_bool_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
bool default_value = false,
|
||||
bool hidden = false
|
||||
);
|
||||
|
||||
const ConfigValueVariant get_option_value(const std::string& option_id) const;
|
||||
const ConfigValueVariant get_temp_option_value(const std::string& option_id) const;
|
||||
// This should only be used internally to recompui. Other changes to values should be done through update_option_value
|
||||
// so rendering can be updated with your new set value.
|
||||
void set_option_value(const std::string& option_id, ConfigValueVariant value);
|
||||
bool get_enum_option_disabled(size_t option_index, uint32_t enum_index);
|
||||
void add_option_change_callback(const std::string& option_id, on_option_change_callback callback);
|
||||
void set_apply_callback(std::function<void()> callback) {
|
||||
apply_callback = callback;
|
||||
}
|
||||
void set_save_callback(std::function<void()> callback) {
|
||||
save_callback = callback;
|
||||
}
|
||||
|
||||
void report_config_option_update(size_t option_index, ConfigOptionUpdateType update_type);
|
||||
void update_option_disabled(size_t option_index, bool disabled);
|
||||
void update_option_disabled(const std::string& option_id, bool disabled);
|
||||
void update_option_hidden(size_t option_index, bool hidden);
|
||||
void update_option_hidden(const std::string& option_id, bool hidden);
|
||||
void update_option_enum_details(const std::string& option_id, const std::string& enum_details);
|
||||
void update_option_value(const std::string& option_id, ConfigValueVariant value);
|
||||
void update_option_description(const std::string& option_id, const std::string& new_description);
|
||||
void update_enum_option_disabled(const std::string& option_id, uint32_t enum_index, bool disabled);
|
||||
|
||||
// Makes the dependent option disabled when the source option is set to any of the specified values.
|
||||
void add_option_disable_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector<ConfigValueVariant> &values);
|
||||
template <typename... ENUM_TYPE>
|
||||
void add_option_disable_dependency(const std::string& dependent_option_id, const std::string& source_option_id, ENUM_TYPE... enum_values) {
|
||||
std::vector<ConfigValueVariant> values;
|
||||
for (const auto& value : {enum_values...}) {
|
||||
values.push_back(static_cast<uint32_t>(value));
|
||||
}
|
||||
add_option_disable_dependency(dependent_option_id, source_option_id, values);
|
||||
};
|
||||
// Makes the dependent option hidden when the source option is set to any of the specified values.
|
||||
// Does not override the option's inherent hidden property if set.
|
||||
void add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector<ConfigValueVariant> &values);
|
||||
template <typename... ENUM_TYPE>
|
||||
void add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, ENUM_TYPE... enum_values) {
|
||||
std::vector<ConfigValueVariant> values;
|
||||
for (const auto& value : {enum_values...}) {
|
||||
values.push_back(static_cast<uint32_t>(value));
|
||||
}
|
||||
add_option_hidden_dependency(dependent_option_id, source_option_id, values);
|
||||
};
|
||||
void add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, bool bool_val) {
|
||||
std::vector<ConfigValueVariant> values = { bool_val };
|
||||
add_option_hidden_dependency(dependent_option_id, source_option_id, values);
|
||||
};
|
||||
|
||||
bool load_config(std::function<bool(nlohmann::json &)> validate_callback = nullptr);
|
||||
bool save_config();
|
||||
bool save_config_json(nlohmann::json config_json) const;
|
||||
nlohmann::json get_json_config() const;
|
||||
|
||||
|
||||
void revert_temp_config();
|
||||
bool is_dirty();
|
||||
|
||||
std::vector<ConfigOptionUpdateContext> get_config_option_updates() { return config_option_updates; }
|
||||
bool is_config_option_disabled(size_t option_index) { return disabled_options.contains(option_index); }
|
||||
bool is_config_option_hidden(size_t option_index);
|
||||
void clear_config_option_updates() {
|
||||
config_option_updates.clear();
|
||||
}
|
||||
std::string get_enum_option_details(size_t option_index);
|
||||
void on_json_parse_option(const std::string& option_id, parse_option_func callback) {
|
||||
json_parse_option_map[option_id] = callback;
|
||||
}
|
||||
void on_json_serialize_option(const std::string& option_id, serialize_option_func callback) {
|
||||
json_serialize_option_map[option_id] = callback;
|
||||
}
|
||||
|
||||
const ConfigStorage& get_config_storage() const;
|
||||
const ConfigSchema& get_config_schema() const;
|
||||
|
||||
private:
|
||||
bool loaded_config = false;
|
||||
bool is_mod_config = false;
|
||||
|
||||
std::string config_file_name;
|
||||
std::string mod_version; // only used if mod
|
||||
|
||||
ConfigSchema schema;
|
||||
ConfigStorage storage;
|
||||
ConfigStorage temp_storage;
|
||||
|
||||
std::unordered_map<size_t, on_option_change_callback> option_change_callbacks = {};
|
||||
std::function<void()> apply_callback = nullptr;
|
||||
std::function<void()> save_callback = nullptr;
|
||||
std::vector<ConfigOptionUpdateContext> config_option_updates = {};
|
||||
std::unordered_set<size_t> disabled_options = {};
|
||||
std::unordered_set<size_t> hidden_options = {};
|
||||
std::unordered_map<size_t, std::string> enum_option_details = {};
|
||||
std::unordered_map<size_t, std::unordered_set<size_t>> enum_options_disabled = {};
|
||||
|
||||
std::unordered_map<std::string, parse_option_func> json_parse_option_map = {};
|
||||
std::unordered_map<std::string, serialize_option_func> json_serialize_option_map = {};
|
||||
|
||||
const ConfigValueVariant get_option_value_from_storage(const std::string& option_id, const ConfigStorage& src) const;
|
||||
|
||||
void derive_all_config_option_dependencies();
|
||||
void derive_option_dependencies(size_t option_index);
|
||||
void try_call_option_change_callback(const std::string& option_id, ConfigValueVariant value, ConfigValueVariant prev_value, OptionChangeContext change_context);
|
||||
const ConfigValueVariant get_option_default_value(const std::string& option_id) const;
|
||||
void determine_changed_option(const std::string& option_id);
|
||||
ConfigValueVariant parse_config_option_json_value(const nlohmann::json& json_value, const ConfigOption &option);
|
||||
|
||||
// Return pointer to the root of where the config values should be stored in the json.
|
||||
nlohmann::json *get_config_storage_root(nlohmann::json* json);
|
||||
nlohmann::json get_storage_json() const;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#endif // __RECOMP_CONFIG_HPP__
|
||||
|
|
@ -70,6 +70,7 @@ namespace recomp {
|
|||
OtherError
|
||||
};
|
||||
void register_config_path(std::filesystem::path path);
|
||||
std::filesystem::path get_config_path();
|
||||
bool register_game(const recomp::GameEntry& entry);
|
||||
void check_all_stored_roms();
|
||||
bool load_stored_rom(std::u8string& game_id);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include "librecomp/game.hpp"
|
||||
#include "librecomp/sections.h"
|
||||
#include "librecomp/overlays.hpp"
|
||||
#include "librecomp/config.hpp"
|
||||
|
||||
namespace N64Recomp {
|
||||
class Context;
|
||||
|
|
@ -81,7 +82,10 @@ namespace recomp {
|
|||
InvalidDependencyString,
|
||||
MissingManifestField,
|
||||
DuplicateMod,
|
||||
WrongGame
|
||||
WrongGame,
|
||||
InvalidDisableOptionDependency,
|
||||
InvalidHiddenOptionDependency,
|
||||
DuplicateEnumStrings,
|
||||
};
|
||||
|
||||
std::string error_to_string(ModOpenError);
|
||||
|
|
@ -127,14 +131,6 @@ namespace recomp {
|
|||
|
||||
std::string error_to_string(CodeModLoadError);
|
||||
|
||||
enum class ConfigOptionType {
|
||||
None,
|
||||
Enum,
|
||||
Number,
|
||||
String,
|
||||
Bool
|
||||
};
|
||||
|
||||
enum class DependencyStatus {
|
||||
// Do not change these values as they're exposed in the mod API!
|
||||
|
||||
|
|
@ -189,49 +185,6 @@ namespace recomp {
|
|||
bool optional;
|
||||
};
|
||||
|
||||
struct ConfigOptionEnum {
|
||||
std::vector<std::string> options;
|
||||
uint32_t default_value = 0;
|
||||
};
|
||||
|
||||
struct ConfigOptionNumber {
|
||||
double min = 0.0;
|
||||
double max = 0.0;
|
||||
double step = 0.0;
|
||||
int precision = 0;
|
||||
bool percent = false;
|
||||
double default_value = 0.0;
|
||||
};
|
||||
|
||||
struct ConfigOptionString {
|
||||
std::string default_value;
|
||||
};
|
||||
|
||||
struct ConfigOptionBool {
|
||||
bool default_value;
|
||||
};
|
||||
|
||||
typedef std::variant<ConfigOptionEnum, ConfigOptionNumber, ConfigOptionString, ConfigOptionBool> ConfigOptionVariant;
|
||||
|
||||
struct ConfigOption {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
ConfigOptionType type;
|
||||
ConfigOptionVariant variant;
|
||||
};
|
||||
|
||||
struct ConfigSchema {
|
||||
std::vector<ConfigOption> options;
|
||||
std::unordered_map<std::string, size_t> options_by_id;
|
||||
};
|
||||
|
||||
typedef std::variant<std::monostate, uint32_t, double, std::string, bool> ConfigValueVariant;
|
||||
|
||||
struct ConfigStorage {
|
||||
std::unordered_map<std::string, ConfigValueVariant> value_map;
|
||||
};
|
||||
|
||||
struct ModDetails {
|
||||
std::string mod_id;
|
||||
std::string display_name;
|
||||
|
|
@ -255,7 +208,6 @@ namespace recomp {
|
|||
std::vector<std::string> authors;
|
||||
std::vector<Dependency> dependencies;
|
||||
std::unordered_map<std::string, size_t> dependencies_by_id;
|
||||
ConfigSchema config_schema;
|
||||
Version minimum_recomp_version;
|
||||
Version version;
|
||||
bool runtime_toggleable;
|
||||
|
|
@ -371,12 +323,12 @@ namespace recomp {
|
|||
recomp::Version get_mod_version(size_t mod_index);
|
||||
std::string get_mod_id(size_t mod_index);
|
||||
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;
|
||||
const config::ConfigSchema &get_mod_config_schema(const std::string &mod_id) const;
|
||||
const std::vector<char> &get_mod_thumbnail(const std::string &mod_id) const;
|
||||
void set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value);
|
||||
void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value);
|
||||
ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id) const;
|
||||
ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id) const;
|
||||
void set_mod_config_value(size_t mod_index, const std::string &option_id, const config::ConfigValueVariant &value);
|
||||
void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const config::ConfigValueVariant &value);
|
||||
config::ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id) const;
|
||||
config::ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id) const;
|
||||
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);
|
||||
|
|
@ -396,7 +348,7 @@ namespace recomp {
|
|||
CodeModLoadError 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);
|
||||
CodeModLoadError load_mod_code(uint8_t* rdram, ModHandle& mod, uint32_t base_event_index, std::string& error_param);
|
||||
CodeModLoadError resolve_code_dependencies(ModHandle& mod, size_t mod_index, const std::unordered_map<recomp_func_t*, recomp::overlays::BasePatchedFunction>& base_patched_funcs, std::string& error_param);
|
||||
void add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types, std::vector<char>&& thumbnail);
|
||||
void add_opened_mod(ModManifest&& manifest, config::Config&& config, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types, std::vector<char>&& thumbnail);
|
||||
std::vector<ModLoadErrorDetails> 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,
|
||||
|
|
@ -440,7 +392,7 @@ namespace recomp {
|
|||
std::vector<bool> processed_hook_slots;
|
||||
// Generated shim functions to use for implementing shim exports.
|
||||
std::vector<std::unique_ptr<N64Recomp::ShimFunction>> shim_functions;
|
||||
ConfigSchema empty_schema;
|
||||
config::ConfigSchema empty_schema;
|
||||
std::vector<char> empty_bytes;
|
||||
size_t num_events = 0;
|
||||
ModContentTypeId code_content_type_id;
|
||||
|
|
@ -464,7 +416,7 @@ namespace recomp {
|
|||
public:
|
||||
// TODO make these private and expose methods for the functionality they're currently used in.
|
||||
ModManifest manifest;
|
||||
ConfigStorage config_storage;
|
||||
config::Config config;
|
||||
std::unique_ptr<ModCodeHandle> code_handle;
|
||||
std::unique_ptr<N64Recomp::Context> recompiler_context;
|
||||
std::vector<uint32_t> section_load_addresses;
|
||||
|
|
@ -472,7 +424,7 @@ namespace recomp {
|
|||
std::vector<ModContentTypeId> content_types;
|
||||
std::vector<char> thumbnail;
|
||||
|
||||
ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types, std::vector<char>&& thumbnail);
|
||||
ModHandle(const ModContext& context, ModManifest&& manifest, config::Config&& config, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types, std::vector<char>&& thumbnail);
|
||||
ModHandle(const ModHandle& rhs) = delete;
|
||||
ModHandle& operator=(const ModHandle& rhs) = delete;
|
||||
ModHandle(ModHandle&& rhs);
|
||||
|
|
@ -618,7 +570,7 @@ namespace recomp {
|
|||
void register_hook_exports();
|
||||
void run_hook(uint8_t* rdram, recomp_context* ctx, size_t hook_slot_index);
|
||||
|
||||
ModOpenError parse_manifest(ModManifest &ret, const std::vector<char> &manifest_data, std::string &error_param);
|
||||
ModOpenError parse_manifest(ModManifest &ret, const std::vector<char> &manifest_data, std::string &error_param, recomp::config::Config *config);
|
||||
CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param);
|
||||
|
||||
void initialize_mods();
|
||||
|
|
@ -633,12 +585,12 @@ namespace recomp {
|
|||
void enable_mod(const std::string& mod_id, bool enabled);
|
||||
bool is_mod_enabled(const std::string& mod_id);
|
||||
bool is_mod_auto_enabled(const std::string& mod_id);
|
||||
const ConfigSchema &get_mod_config_schema(const std::string &mod_id);
|
||||
const config::ConfigSchema &get_mod_config_schema(const std::string &mod_id);
|
||||
const std::vector<char> &get_mod_thumbnail(const std::string &mod_id);
|
||||
void set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value);
|
||||
void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value);
|
||||
ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id);
|
||||
ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id);
|
||||
void set_mod_config_value(size_t mod_index, const std::string &option_id, const config::ConfigValueVariant &value);
|
||||
void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const config::ConfigValueVariant &value);
|
||||
config::ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id);
|
||||
config::ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id);
|
||||
std::string get_mod_id_from_filename(const std::filesystem::path& mod_filename);
|
||||
std::filesystem::path get_mod_filename(const std::string& mod_id);
|
||||
size_t get_mod_order_index(const std::string& mod_id);
|
||||
|
|
|
|||
638
librecomp/src/config.cpp
Normal file
638
librecomp/src/config.cpp
Normal file
|
|
@ -0,0 +1,638 @@
|
|||
#include <fstream>
|
||||
#include "librecomp/files.hpp"
|
||||
#include "librecomp/config.hpp"
|
||||
#include "librecomp/game.hpp"
|
||||
#include "librecomp/mods.hpp"
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) {
|
||||
{
|
||||
std::ofstream output_file = recomp::open_output_file_with_backup(path);
|
||||
if (!output_file.good()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output_file << std::setw(4) << json_data;
|
||||
}
|
||||
return recomp::finalize_output_file_with_backup(path);
|
||||
}
|
||||
|
||||
static std::filesystem::path get_path_to_config(bool is_mod_config) {
|
||||
if (is_mod_config) {
|
||||
return recomp::get_config_path() / recomp::mods::mod_config_directory;
|
||||
}
|
||||
return recomp::get_config_path();
|
||||
}
|
||||
|
||||
namespace recomp::config {
|
||||
Config::Config(std::string name, std::string id, bool requires_confirmation) {
|
||||
this->name = name;
|
||||
this->id = id;
|
||||
this->requires_confirmation = requires_confirmation;
|
||||
schema.options.clear();
|
||||
schema.options_by_id.clear();
|
||||
storage.value_map.clear();
|
||||
temp_storage.value_map.clear();
|
||||
|
||||
config_file_name = this->id + ".json";
|
||||
}
|
||||
|
||||
Config::Config() {
|
||||
is_mod_config = true;
|
||||
requires_confirmation = false;
|
||||
name = "Mod Config";
|
||||
|
||||
schema.options.clear();
|
||||
schema.options_by_id.clear();
|
||||
storage.value_map.clear();
|
||||
temp_storage.value_map.clear();
|
||||
}
|
||||
|
||||
void Config::set_id(const std::string &id) {
|
||||
this->id = id;
|
||||
config_file_name = this->id + ".json";
|
||||
}
|
||||
void Config::set_mod_version(const std::string &mod_version) {
|
||||
this->mod_version = mod_version;
|
||||
}
|
||||
|
||||
const ConfigStorage& Config::get_config_storage() const {
|
||||
return storage;
|
||||
}
|
||||
|
||||
const ConfigSchema& Config::get_config_schema() const {
|
||||
return schema;
|
||||
}
|
||||
|
||||
nlohmann::json *Config::get_config_storage_root(nlohmann::json* json) {
|
||||
if (is_mod_config) {
|
||||
return &(*json)["storage"];
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
void Config::add_option(const ConfigOption& option) {
|
||||
if (loaded_config) {
|
||||
assert(false && "Cannot add options after config has been loaded.");
|
||||
}
|
||||
schema.options.push_back(option);
|
||||
schema.options_by_id[option.id] = schema.options.size() - 1;
|
||||
|
||||
ConfigValueVariant default_value = std::monostate();
|
||||
switch (option.type) {
|
||||
case ConfigOptionType::None:
|
||||
assert(false && "Cannot add option with type None.");
|
||||
break;
|
||||
case ConfigOptionType::Enum:
|
||||
default_value = std::get<ConfigOptionEnum>(option.variant).default_value;
|
||||
break;
|
||||
case ConfigOptionType::Number:
|
||||
default_value = std::get<ConfigOptionNumber>(option.variant).default_value;
|
||||
break;
|
||||
case ConfigOptionType::String:
|
||||
default_value = std::get<ConfigOptionString>(option.variant).default_value;
|
||||
break;
|
||||
case ConfigOptionType::Bool:
|
||||
default_value = std::get<ConfigOptionBool>(option.variant).default_value;
|
||||
break;
|
||||
}
|
||||
|
||||
storage.value_map[option.id] = default_value;
|
||||
if (requires_confirmation) {
|
||||
temp_storage.value_map[option.id] = default_value;
|
||||
}
|
||||
}
|
||||
|
||||
void Config::add_enum_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
const std::vector<ConfigOptionEnumOption> &options,
|
||||
uint32_t default_value,
|
||||
bool hidden
|
||||
) {
|
||||
ConfigOption option;
|
||||
option.id = id;
|
||||
option.name = name;
|
||||
option.description = description;
|
||||
option.type = ConfigOptionType::Enum;
|
||||
option.hidden = hidden;
|
||||
|
||||
ConfigOptionEnum option_enum = {{}, default_value};
|
||||
|
||||
// Note: this is a bit too predictive since this calls add_option
|
||||
size_t option_index = schema.options.size();
|
||||
|
||||
for (const auto &option : options) {
|
||||
assert(option_enum.can_add_option(option.key, option.value) && "Duplicate enum option key or value.");
|
||||
option_enum.options.push_back(option);
|
||||
}
|
||||
|
||||
if (option_enum.find_option_from_value(default_value) == option_enum.options.end()) {
|
||||
assert(false && "Default value must match to an option.");
|
||||
}
|
||||
|
||||
option.variant = option_enum;
|
||||
|
||||
add_option(option);
|
||||
}
|
||||
|
||||
void Config::add_number_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
double min,
|
||||
double max,
|
||||
double step,
|
||||
int precision,
|
||||
bool percent,
|
||||
double default_value,
|
||||
bool hidden
|
||||
) {
|
||||
ConfigOption option;
|
||||
option.id = id;
|
||||
option.name = name;
|
||||
option.description = description;
|
||||
option.type = ConfigOptionType::Number;
|
||||
option.variant = ConfigOptionNumber{min, max, step, precision, percent, default_value};
|
||||
option.hidden = hidden;
|
||||
|
||||
add_option(option);
|
||||
}
|
||||
|
||||
void Config::add_string_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
const std::string &default_value,
|
||||
bool hidden
|
||||
) {
|
||||
ConfigOption option;
|
||||
option.id = id;
|
||||
option.name = name;
|
||||
option.description = description;
|
||||
option.type = ConfigOptionType::String;
|
||||
option.variant = ConfigOptionString{default_value};
|
||||
option.hidden = hidden;
|
||||
|
||||
add_option(option);
|
||||
}
|
||||
|
||||
void Config::add_bool_option(
|
||||
const std::string &id,
|
||||
const std::string &name,
|
||||
const std::string &description,
|
||||
bool default_value,
|
||||
bool hidden
|
||||
) {
|
||||
ConfigOption option;
|
||||
option.id = id;
|
||||
option.name = name;
|
||||
option.description = description;
|
||||
option.type = ConfigOptionType::Bool;
|
||||
option.variant = ConfigOptionBool{default_value};
|
||||
option.hidden = hidden;
|
||||
|
||||
add_option(option);
|
||||
}
|
||||
|
||||
const ConfigValueVariant Config::get_option_default_value(const std::string& option_id) const {
|
||||
auto option_by_id_it = schema.options_by_id.find(option_id);
|
||||
if (option_by_id_it == schema.options_by_id.end()) {
|
||||
assert(false && "Option not found.");
|
||||
return std::monostate();
|
||||
}
|
||||
|
||||
const ConfigOption &option = 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;
|
||||
case ConfigOptionType::Bool:
|
||||
return std::get<ConfigOptionBool>(option.variant).default_value;
|
||||
default:
|
||||
assert(false && "Unknown config option type.");
|
||||
return std::monostate();
|
||||
}
|
||||
}
|
||||
|
||||
const ConfigValueVariant Config::get_option_value_from_storage(const std::string& option_id, const ConfigStorage& src) const {
|
||||
auto it = src.value_map.find(option_id);
|
||||
if (it != src.value_map.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return get_option_default_value(option_id);
|
||||
}
|
||||
|
||||
const ConfigValueVariant Config::get_option_value(const std::string& option_id) const {
|
||||
return get_option_value_from_storage(option_id, storage);
|
||||
}
|
||||
|
||||
const ConfigValueVariant Config::get_temp_option_value(const std::string& option_id) const {
|
||||
return get_option_value_from_storage(option_id, temp_storage);
|
||||
}
|
||||
|
||||
void Config::determine_changed_option(const std::string& option_id) {
|
||||
if (get_option_value(option_id) != get_temp_option_value(option_id)) {
|
||||
modified_options.insert(schema.options_by_id[option_id]);
|
||||
} else {
|
||||
modified_options.erase(schema.options_by_id[option_id]);
|
||||
}
|
||||
}
|
||||
|
||||
void Config::try_call_option_change_callback(const std::string& option_id, ConfigValueVariant value, ConfigValueVariant prev_value, OptionChangeContext change_context) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
auto callback_it = option_change_callbacks.find(option_index);
|
||||
bool is_load = (change_context == OptionChangeContext::Load);
|
||||
bool value_changed = (value != prev_value);
|
||||
if (callback_it != option_change_callbacks.end() && (is_load || value_changed)) {
|
||||
callback_it->second(value, prev_value, change_context);
|
||||
}
|
||||
}
|
||||
|
||||
void Config::set_option_value(const std::string& option_id, ConfigValueVariant value) {
|
||||
ConfigStorage &storage = requires_confirmation ? temp_storage : storage;
|
||||
|
||||
auto it = storage.value_map.find(option_id);
|
||||
if (it != storage.value_map.end()) {
|
||||
ConfigValueVariant prev_value = it->second;
|
||||
it->second = value;
|
||||
|
||||
if (requires_confirmation) {
|
||||
determine_changed_option(option_id);
|
||||
try_call_option_change_callback(option_id, value, prev_value, OptionChangeContext::Temporary);
|
||||
} else {
|
||||
try_call_option_change_callback(option_id, value, prev_value, OptionChangeContext::Permanent);
|
||||
}
|
||||
|
||||
derive_option_dependencies(schema.options_by_id[option_id]);
|
||||
}
|
||||
}
|
||||
|
||||
bool Config::get_enum_option_disabled(size_t option_index, uint32_t enum_index) {
|
||||
auto enum_it = enum_options_disabled.find(option_index);
|
||||
if (enum_it != enum_options_disabled.end()) {
|
||||
return enum_it->second.contains(enum_index);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json Config::get_json_config() const {
|
||||
if (is_mod_config) {
|
||||
nlohmann::json config_json;
|
||||
if (id.empty()) {
|
||||
assert(false && "Mod ID does not exist for this config.");
|
||||
}
|
||||
if (mod_version.empty()) {
|
||||
assert(false && "Mod version does not exist for this config.");
|
||||
}
|
||||
config_json["mod_id"] = id;
|
||||
config_json["mod_version"] = mod_version;
|
||||
config_json["recomp_version"] = recomp::get_project_version().to_string();
|
||||
config_json["storage"] = get_storage_json();
|
||||
return config_json;
|
||||
}
|
||||
return get_storage_json();
|
||||
}
|
||||
|
||||
nlohmann::json Config::get_storage_json() const {
|
||||
nlohmann::json json;
|
||||
for (const auto& option : schema.options) {
|
||||
const ConfigValueVariant value = get_option_value(option.id);
|
||||
|
||||
if (json_serialize_option_map.contains(option.id)) {
|
||||
auto cb = json_serialize_option_map.at(option.id);
|
||||
json[option.id] = cb(value);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case ConfigOptionType::Enum: {
|
||||
auto &option_enum = std::get<ConfigOptionEnum>(option.variant);
|
||||
auto found_opt = option_enum.find_option_from_value(std::get<uint32_t>(value));
|
||||
if (found_opt != option_enum.options.end()) {
|
||||
json[option.id] = found_opt->key;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConfigOptionType::Number: {
|
||||
auto &option_number = std::get<ConfigOptionNumber>(option.variant);
|
||||
if (option_number.precision == 0) {
|
||||
json[option.id] = static_cast<int>(std::get<double>(value));
|
||||
} else {
|
||||
json[option.id] = std::get<double>(value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ConfigOptionType::String:
|
||||
json[option.id] = std::get<std::string>(value);
|
||||
break;
|
||||
case ConfigOptionType::Bool:
|
||||
json[option.id] = std::get<bool>(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
bool Config::save_config_json(nlohmann::json config_json) const {
|
||||
std::filesystem::path file_path = get_path_to_config(is_mod_config) / config_file_name;
|
||||
|
||||
bool result = save_json_with_backups(file_path, config_json);
|
||||
if (save_callback) {
|
||||
save_callback();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Config::save_config() {
|
||||
if (requires_confirmation) {
|
||||
for (const auto& option : schema.options) {
|
||||
ConfigValueVariant prev_value = get_option_value(option.id);
|
||||
ConfigValueVariant cur_value = get_temp_option_value(option.id);
|
||||
storage.value_map[option.id] = cur_value;
|
||||
try_call_option_change_callback(option.id, cur_value, prev_value, OptionChangeContext::Permanent);
|
||||
}
|
||||
|
||||
if (apply_callback && is_dirty()) {
|
||||
apply_callback();
|
||||
}
|
||||
|
||||
modified_options.clear();
|
||||
}
|
||||
|
||||
return save_config_json(get_json_config());
|
||||
}
|
||||
|
||||
void Config::derive_option_dependencies(size_t option_index) {
|
||||
auto &option_id = schema.options[option_index].id;
|
||||
auto value = requires_confirmation ? get_temp_option_value(option_id) : get_option_value(option_id);
|
||||
|
||||
auto disable_result = schema.disable_dependencies.check_option_dependencies(option_index, value);
|
||||
for (auto &option_res : disable_result) {
|
||||
update_option_disabled(option_res.first, option_res.second);
|
||||
}
|
||||
|
||||
auto hidden_result = schema.hidden_dependencies.check_option_dependencies(option_index, value);
|
||||
for (auto &option_res : hidden_result) {
|
||||
update_option_hidden(option_res.first, option_res.second);
|
||||
}
|
||||
}
|
||||
|
||||
void Config::derive_all_config_option_dependencies() {
|
||||
for (size_t option_index = 0; option_index < schema.options.size(); option_index++) {
|
||||
derive_option_dependencies(option_index);
|
||||
}
|
||||
}
|
||||
|
||||
ConfigValueVariant Config::parse_config_option_json_value(const nlohmann::json& json_value, const ConfigOption &option) {
|
||||
if (json_parse_option_map.contains(option.id)) {
|
||||
return json_parse_option_map[option.id](json_value);
|
||||
}
|
||||
|
||||
bool is_null = json_value.is_null();
|
||||
|
||||
switch (option.type) {
|
||||
case ConfigOptionType::None:
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
case ConfigOptionType::Enum: {
|
||||
if (is_null) {
|
||||
return std::get<ConfigOptionEnum>(option.variant).default_value;
|
||||
}
|
||||
std::string enum_string_value = json_value.get<std::string>();
|
||||
auto option_variant = std::get<ConfigOptionEnum>(option.variant);
|
||||
auto found_opt = option_variant.find_option_from_string(enum_string_value);
|
||||
if (found_opt != option_variant.options.end()) {
|
||||
return found_opt->value;
|
||||
} else {
|
||||
return std::get<ConfigOptionEnum>(option.variant).default_value;
|
||||
}
|
||||
}
|
||||
case ConfigOptionType::Number:
|
||||
if (is_null) {
|
||||
return std::get<ConfigOptionNumber>(option.variant).default_value;
|
||||
}
|
||||
return json_value.get<double>();
|
||||
case ConfigOptionType::String:
|
||||
if (is_null) {
|
||||
return std::get<ConfigOptionString>(option.variant).default_value;
|
||||
}
|
||||
return json_value.get<std::string>();
|
||||
case ConfigOptionType::Bool:
|
||||
if (is_null) {
|
||||
return std::get<ConfigOptionBool>(option.variant).default_value;
|
||||
break;
|
||||
}
|
||||
return json_value.get<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
bool Config::load_config(std::function<bool(nlohmann::json &)> validate_callback) {
|
||||
std::filesystem::path file_path = get_path_to_config(is_mod_config) / config_file_name;
|
||||
nlohmann::json config_json{};
|
||||
|
||||
if (!read_json_with_backups(file_path, config_json)) {
|
||||
if (requires_confirmation) {
|
||||
revert_temp_config();
|
||||
}
|
||||
save_config();
|
||||
derive_all_config_option_dependencies();
|
||||
clear_config_option_updates();
|
||||
loaded_config = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (validate_callback != nullptr && !validate_callback(config_json)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nlohmann::json *json_config_root = get_config_storage_root(&config_json);
|
||||
|
||||
for (const auto& option : schema.options) {
|
||||
auto json_value = (*json_config_root)[option.id];
|
||||
|
||||
auto value = parse_config_option_json_value(json_value, option);
|
||||
storage.value_map[option.id] = value;
|
||||
|
||||
if (requires_confirmation) {
|
||||
temp_storage.value_map[option.id] = value;
|
||||
}
|
||||
try_call_option_change_callback(option.id, value, value, OptionChangeContext::Load);
|
||||
}
|
||||
|
||||
derive_all_config_option_dependencies();
|
||||
clear_config_option_updates();
|
||||
|
||||
loaded_config = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Config::revert_temp_config() {
|
||||
if (!requires_confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
modified_options.clear();
|
||||
|
||||
for (const auto& option : schema.options) {
|
||||
temp_storage.value_map[option.id] = get_option_value(option.id);
|
||||
}
|
||||
derive_all_config_option_dependencies();
|
||||
}
|
||||
|
||||
bool Config::is_dirty() {
|
||||
return !modified_options.empty();
|
||||
}
|
||||
|
||||
void Config::add_option_change_callback(const std::string& option_id, on_option_change_callback callback) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
option_change_callbacks[option_index] = callback;
|
||||
}
|
||||
|
||||
void Config::report_config_option_update(size_t option_index, ConfigOptionUpdateType update_type) {
|
||||
ConfigOptionUpdateContext *update_context = nullptr;
|
||||
for (auto &context : config_option_updates) {
|
||||
if (context.option_index == option_index) {
|
||||
update_context = &context;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (update_context == nullptr) {
|
||||
config_option_updates.push_back({option_index, {}});
|
||||
update_context = &config_option_updates.back();
|
||||
}
|
||||
|
||||
update_context->updates.push_back(update_type);
|
||||
}
|
||||
|
||||
void Config::update_option_disabled(size_t option_index, bool disabled) {
|
||||
bool was_disabled = is_config_option_disabled(option_index);
|
||||
if (was_disabled == disabled) return;
|
||||
|
||||
if (disabled) {
|
||||
disabled_options.insert(option_index);
|
||||
} else {
|
||||
disabled_options.erase(option_index);
|
||||
}
|
||||
report_config_option_update(option_index, ConfigOptionUpdateType::Disabled);
|
||||
};
|
||||
|
||||
void Config::update_option_disabled(const std::string& option_id, bool disabled) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
update_option_disabled(option_index, disabled);
|
||||
};
|
||||
|
||||
void Config::update_option_hidden(size_t option_index, bool hidden) {
|
||||
if (schema.options[option_index].hidden) {
|
||||
// unchangeable - always hidden
|
||||
return;
|
||||
}
|
||||
bool was_hidden = is_config_option_hidden(option_index);
|
||||
if (was_hidden == hidden) {
|
||||
return;
|
||||
}
|
||||
if (hidden) {
|
||||
hidden_options.insert(option_index);
|
||||
} else {
|
||||
hidden_options.erase(option_index);
|
||||
}
|
||||
report_config_option_update(option_index, ConfigOptionUpdateType::Hidden);
|
||||
};
|
||||
|
||||
void Config::update_option_hidden(const std::string& option_id, bool hidden) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
update_option_hidden(option_index, hidden);
|
||||
};
|
||||
|
||||
void Config::update_option_enum_details(const std::string& option_id, const std::string& enum_details) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
enum_option_details[option_index] = enum_details;
|
||||
report_config_option_update(option_index, ConfigOptionUpdateType::EnumDetails);
|
||||
};
|
||||
|
||||
void Config::update_option_value(const std::string& option_id, ConfigValueVariant value) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
// This could potentially cause an update loop due to set_option_value calling change callbacks, which could call this function.
|
||||
// It seems more important to call change callbacks AND respect requires_confirmation
|
||||
set_option_value(option_id, value);
|
||||
report_config_option_update(option_index, ConfigOptionUpdateType::Value);
|
||||
};
|
||||
|
||||
void Config::update_option_description(const std::string& option_id, const std::string& new_description) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
schema.options[option_index].description = new_description;
|
||||
report_config_option_update(option_index, ConfigOptionUpdateType::Description);
|
||||
}
|
||||
|
||||
void Config::update_enum_option_disabled(const std::string& option_id, uint32_t enum_index, bool disabled) {
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
if (!enum_options_disabled.contains(option_index)) {
|
||||
enum_options_disabled[option_index] = {};
|
||||
}
|
||||
if (disabled) {
|
||||
enum_options_disabled[option_index].insert(enum_index);
|
||||
} else {
|
||||
enum_options_disabled[option_index].erase(enum_index);
|
||||
}
|
||||
report_config_option_update(option_index, ConfigOptionUpdateType::EnumDisabled);
|
||||
}
|
||||
|
||||
void Config::add_option_disable_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector<ConfigValueVariant> &values) {
|
||||
size_t dependent_index = schema.options_by_id[dependent_option_id];
|
||||
size_t source_index = schema.options_by_id[source_option_id];
|
||||
schema.disable_dependencies.add_option_dependency(dependent_index, source_index, values);
|
||||
}
|
||||
|
||||
void Config::add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector<ConfigValueVariant> &values) {
|
||||
size_t dependent_index = schema.options_by_id[dependent_option_id];
|
||||
size_t source_index = schema.options_by_id[source_option_id];
|
||||
schema.hidden_dependencies.add_option_dependency(dependent_index, source_index, values);
|
||||
}
|
||||
|
||||
std::string Config::get_enum_option_details(size_t option_index) {
|
||||
if (!enum_option_details.contains(option_index)) {
|
||||
return std::string();
|
||||
}
|
||||
return enum_option_details[option_index];
|
||||
}
|
||||
|
||||
bool Config::is_config_option_hidden(size_t option_index) {
|
||||
return schema.options[option_index].hidden || hidden_options.contains(option_index);
|
||||
}
|
||||
|
||||
}
|
||||
73
librecomp/src/config_option.cpp
Normal file
73
librecomp/src/config_option.cpp
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#include "librecomp/config.hpp"
|
||||
|
||||
static char make_char_upper(char c) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c -= 'a' - 'A';
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
static bool case_insensitive_compare(const std::string& a, const std::string& b) {
|
||||
if (a.size() != b.size()) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < a.size(); i++) {
|
||||
if (make_char_upper(a[i]) != make_char_upper(b[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace recomp::config {
|
||||
|
||||
// ConfigOptionEnum
|
||||
std::vector<ConfigOptionEnumOption>::const_iterator ConfigOptionEnum::find_option_from_string(const std::string& option_key) const {
|
||||
return std::find_if(options.begin(), options.end(), [option_key](const ConfigOptionEnumOption& opt) {
|
||||
return case_insensitive_compare(opt.key, option_key);
|
||||
});
|
||||
};
|
||||
|
||||
std::vector<ConfigOptionEnumOption>::const_iterator ConfigOptionEnum::find_option_from_value(uint32_t value) const {
|
||||
return std::find_if(options.begin(), options.end(), [value](const ConfigOptionEnumOption& opt) {
|
||||
return opt.value == value;
|
||||
});
|
||||
}
|
||||
|
||||
bool ConfigOptionEnum::can_add_option(const std::string& option_key, uint32_t option_value) const {
|
||||
return options.size() == 0 || (
|
||||
find_option_from_string(option_key) == options.end() &&
|
||||
find_option_from_value(option_value) == options.end());
|
||||
}
|
||||
|
||||
// ConfigOptionDependency
|
||||
void ConfigOptionDependency::add_option_dependency(size_t dependent_option_index, size_t source_option_index, std::vector<ConfigValueVariant> &values) {
|
||||
if (!option_to_dependencies.contains(source_option_index)) {
|
||||
option_to_dependencies[source_option_index] = {};
|
||||
}
|
||||
option_to_dependencies[source_option_index].insert(dependent_option_index);
|
||||
dependency_to_values[dependent_option_index] = values;
|
||||
}
|
||||
|
||||
std::unordered_map<size_t, bool> ConfigOptionDependency::check_option_dependencies(size_t source_option_index, ConfigValueVariant value) {
|
||||
std::unordered_map<size_t, bool> result{};
|
||||
if (!option_to_dependencies.contains(source_option_index)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unordered_set<size_t> &dependencies = option_to_dependencies[source_option_index];
|
||||
for (auto &dep : dependencies) {
|
||||
bool is_match = false;
|
||||
for (auto &check_value : dependency_to_values[dep]) {
|
||||
if (value == check_value) {
|
||||
is_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
result[dep] = is_match;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
#include "librecomp/addresses.hpp"
|
||||
|
||||
void recomp_get_config_u32(uint8_t* rdram, recomp_context* ctx, size_t mod_index) {
|
||||
recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx));
|
||||
recomp::config::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx));
|
||||
if (uint32_t* as_u32 = std::get_if<uint32_t>(&val)) {
|
||||
_return(ctx, *as_u32);
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ void recomp_get_config_u32(uint8_t* rdram, recomp_context* ctx, size_t mod_index
|
|||
}
|
||||
|
||||
void recomp_get_config_double(uint8_t* rdram, recomp_context* ctx, size_t mod_index) {
|
||||
recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx));
|
||||
recomp::config::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx));
|
||||
if (uint32_t* as_u32 = std::get_if<uint32_t>(&val)) {
|
||||
ctx->f0.d = double(*as_u32);
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ void return_string(uint8_t* rdram, recomp_context* ctx, const StringType& str) {
|
|||
}
|
||||
|
||||
void recomp_get_config_string(uint8_t* rdram, recomp_context* ctx, size_t mod_index) {
|
||||
recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx));
|
||||
recomp::config::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx));
|
||||
if (std::string* as_string = std::get_if<std::string>(&val)) {
|
||||
return_string(rdram, ctx, *as_string);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -332,17 +332,20 @@ constexpr std::string_view config_schema_precision_key = "precision";
|
|||
constexpr std::string_view config_schema_percent_key = "percent";
|
||||
constexpr std::string_view config_schema_options_key = "options";
|
||||
constexpr std::string_view config_schema_default_key = "default";
|
||||
constexpr std::string_view config_schema_matches_key = "matches";
|
||||
constexpr std::string_view config_schema_hidden_from_key = "hidden_from";
|
||||
constexpr std::string_view config_schema_disabled_from_key = "disabled_from";
|
||||
|
||||
std::unordered_map<std::string, recomp::mods::ConfigOptionType> config_option_map{
|
||||
{ "Enum", recomp::mods::ConfigOptionType::Enum},
|
||||
{ "Number", recomp::mods::ConfigOptionType::Number},
|
||||
{ "String", recomp::mods::ConfigOptionType::String},
|
||||
{ "Bool", recomp::mods::ConfigOptionType::Bool},
|
||||
std::unordered_map<std::string, recomp::config::ConfigOptionType> config_option_map{
|
||||
{ "Enum", recomp::config::ConfigOptionType::Enum},
|
||||
{ "Number", recomp::config::ConfigOptionType::Number},
|
||||
{ "String", recomp::config::ConfigOptionType::String},
|
||||
{ "Bool", recomp::config::ConfigOptionType::Bool},
|
||||
};
|
||||
|
||||
recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::json &config_schema_json, recomp::mods::ModManifest &ret, std::string &error_param) {
|
||||
recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::json &config_schema_json, recomp::config::Config *ret, std::string &error_param) {
|
||||
using json = nlohmann::json;
|
||||
recomp::mods::ConfigOption option;
|
||||
recomp::config::ConfigOption option;
|
||||
auto id = config_schema_json.find(config_schema_id_key);
|
||||
if (id != config_schema_json.end()) {
|
||||
if (!get_to<json::string_t>(*id, option.id)) {
|
||||
|
|
@ -399,25 +402,34 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j
|
|||
}
|
||||
|
||||
switch (option.type) {
|
||||
case recomp::mods::ConfigOptionType::Enum:
|
||||
case recomp::config::ConfigOptionType::Enum:
|
||||
{
|
||||
recomp::mods::ConfigOptionEnum option_enum;
|
||||
recomp::config::ConfigOptionEnum option_enum;
|
||||
|
||||
auto options = config_schema_json.find(config_schema_options_key);
|
||||
if (options != config_schema_json.end()) {
|
||||
if (!get_to_vec<std::string>(*options, option_enum.options)) {
|
||||
std::vector<std::string> option_key_list;
|
||||
if (!get_to_vec<std::string>(*options, option_key_list)) {
|
||||
error_param = config_schema_options_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(option_key_list.size()); i++) {
|
||||
if (!option_enum.can_add_option(option_key_list[i], i)) {
|
||||
error_param = config_schema_options_key;
|
||||
return recomp::mods::ModOpenError::DuplicateEnumStrings;
|
||||
}
|
||||
option_enum.options.push_back({i, option_key_list[i]});
|
||||
}
|
||||
}
|
||||
|
||||
auto default_value = config_schema_json.find(config_schema_default_key);
|
||||
if (default_value != config_schema_json.end()) {
|
||||
std::string default_value_string;
|
||||
if (get_to<json::string_t>(*default_value, default_value_string)) {
|
||||
auto it = std::find(option_enum.options.begin(), option_enum.options.end(), default_value_string);
|
||||
auto it = option_enum.find_option_from_string(default_value_string);
|
||||
if (it != option_enum.options.end()) {
|
||||
option_enum.default_value = uint32_t(it - option_enum.options.begin());
|
||||
option_enum.default_value = it->value;
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_default_key;
|
||||
|
|
@ -434,9 +446,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j
|
|||
|
||||
}
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::Number:
|
||||
case recomp::config::ConfigOptionType::Number:
|
||||
{
|
||||
recomp::mods::ConfigOptionNumber option_number;
|
||||
recomp::config::ConfigOptionNumber option_number;
|
||||
|
||||
auto min = config_schema_json.find(config_schema_min_key);
|
||||
if (min != config_schema_json.end()) {
|
||||
|
|
@ -497,9 +509,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j
|
|||
option.variant = option_number;
|
||||
}
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::String:
|
||||
case recomp::config::ConfigOptionType::String:
|
||||
{
|
||||
recomp::mods::ConfigOptionString option_string;
|
||||
recomp::config::ConfigOptionString option_string;
|
||||
|
||||
auto default_value = config_schema_json.find(config_schema_default_key);
|
||||
if (default_value != config_schema_json.end()) {
|
||||
|
|
@ -512,9 +524,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j
|
|||
option.variant = option_string;
|
||||
}
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::Bool:
|
||||
case recomp::config::ConfigOptionType::Bool:
|
||||
{
|
||||
recomp::mods::ConfigOptionBool option_bool;
|
||||
recomp::config::ConfigOptionBool option_bool;
|
||||
|
||||
auto default_value = config_schema_json.find(config_schema_default_key);
|
||||
if (default_value != config_schema_json.end()) {
|
||||
|
|
@ -531,13 +543,129 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j
|
|||
break;
|
||||
}
|
||||
|
||||
ret.config_schema.options_by_id.emplace(option.id, ret.config_schema.options.size());
|
||||
ret.config_schema.options.emplace_back(option);
|
||||
if (ret != nullptr) {
|
||||
ret->add_option(option);
|
||||
}
|
||||
|
||||
return recomp::mods::ModOpenError::Good;
|
||||
}
|
||||
|
||||
recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const std::vector<char>& manifest_data, std::string& error_param) {
|
||||
std::vector<recomp::config::ConfigValueVariant> parse_config_option_dependency_matches(const nlohmann::json &matches_object, const recomp::config::ConfigOption &source_option) {
|
||||
std::vector<recomp::config::ConfigValueVariant> matches = {};
|
||||
|
||||
switch (source_option.type) {
|
||||
case recomp::config::ConfigOptionType::None:
|
||||
default:
|
||||
//! ERROR: Source option type is invalid
|
||||
return matches;
|
||||
case recomp::config::ConfigOptionType::Enum: {
|
||||
std::vector<std::string> enum_values = {};
|
||||
if (!get_to_vec<std::string>(matches_object, enum_values)) {
|
||||
//! ERROR: failed to get array of strings in matches
|
||||
return matches;
|
||||
}
|
||||
|
||||
auto &option_enum = get<recomp::config::ConfigOptionEnum>(source_option.variant);
|
||||
for (auto &value_str : enum_values) {
|
||||
auto it = option_enum.find_option_from_string(value_str);
|
||||
if (it == option_enum.options.end()) {
|
||||
//! ERROR: one of the matches specified doesn't exist in the source enum
|
||||
return matches;
|
||||
}
|
||||
matches.push_back(it->value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case recomp::config::ConfigOptionType::String:
|
||||
//! ERROR: string dependencies are unsupported
|
||||
return matches;
|
||||
case recomp::config::ConfigOptionType::Number:
|
||||
//! ERROR: numerical dependencies are unsupported
|
||||
return matches;
|
||||
case recomp::config::ConfigOptionType::Bool: {
|
||||
bool bool_condition;
|
||||
if (!get_to<nlohmann::json::boolean_t>(matches_object, bool_condition)) {
|
||||
//! ERROR: Failed to get boolean from dependency matches
|
||||
return matches;
|
||||
}
|
||||
matches.push_back(bool_condition);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
recomp::mods::ModOpenError parse_config_option_dependencies(const nlohmann::json &config_schema_json, recomp::config::Config &config) {
|
||||
using json = nlohmann::json;
|
||||
|
||||
auto id = config_schema_json.find(config_schema_id_key);
|
||||
std::string option_id;
|
||||
if (id != config_schema_json.end()) {
|
||||
if (!get_to<json::string_t>(*id, option_id)) {
|
||||
return recomp::mods::ModOpenError::Good;
|
||||
}
|
||||
}
|
||||
|
||||
recomp::config::ConfigSchema schema = config.get_config_schema();
|
||||
|
||||
size_t option_index = schema.options_by_id[option_id];
|
||||
recomp::config::ConfigOption &option = schema.options[option_index];
|
||||
|
||||
auto conditional_add_dependency = [config_schema_json, schema, option, option_index](recomp::config::ConfigOptionDependency &dependency, const std::string_view &dep_key) -> bool {
|
||||
auto find_it = config_schema_json.find(dep_key);
|
||||
if (find_it == config_schema_json.end()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto& dependency_json = *find_it;
|
||||
if (!dependency_json.is_object()) {
|
||||
//! ERROR: Dependency malformed
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string source_dependency_id;
|
||||
auto find_dep_id = dependency_json.find(config_schema_id_key);
|
||||
if (find_dep_id == dependency_json.end()) {
|
||||
//! ERROR: Could not find source dependency id
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!get_to<json::string_t>(*find_dep_id, source_dependency_id)) {
|
||||
//! ERROR: Failed to get source dependency id
|
||||
return false;
|
||||
}
|
||||
if (schema.options_by_id.contains(source_dependency_id) == false) {
|
||||
//! ERROR: Failed to find specified source dependency in schema
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t source_dependency_index = schema.options_by_id.at(source_dependency_id);
|
||||
auto &source_option = schema.options.at(source_dependency_index);
|
||||
|
||||
auto find_matches = dependency_json.find(config_schema_matches_key);
|
||||
if (find_matches == dependency_json.end()) {
|
||||
//! ERROR: Failed to find matches
|
||||
return false;
|
||||
}
|
||||
auto matches = parse_config_option_dependency_matches(*find_matches, source_option);
|
||||
if (matches.empty()) {
|
||||
//! ERROR: Could not find valid data in matches
|
||||
return false;
|
||||
}
|
||||
|
||||
dependency.add_option_dependency(option_index, source_dependency_index, matches);
|
||||
return true;
|
||||
};
|
||||
|
||||
bool disable_success = conditional_add_dependency(schema.disable_dependencies, config_schema_disabled_from_key);
|
||||
if (!disable_success) return recomp::mods::ModOpenError::InvalidDisableOptionDependency;
|
||||
bool hidden_success = conditional_add_dependency(schema.hidden_dependencies, config_schema_hidden_from_key);
|
||||
if (!hidden_success) return recomp::mods::ModOpenError::InvalidHiddenOptionDependency;
|
||||
|
||||
return recomp::mods::ModOpenError::Good;
|
||||
}
|
||||
|
||||
recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const std::vector<char>& manifest_data, std::string& error_param, recomp::config::Config *config) {
|
||||
using json = nlohmann::json;
|
||||
json manifest_json = json::parse(manifest_data.begin(), manifest_data.end(), nullptr, false);
|
||||
|
||||
|
|
@ -589,6 +717,11 @@ recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const
|
|||
return current_error;
|
||||
}
|
||||
|
||||
if (config != nullptr) {
|
||||
config->set_id(ret.mod_id);
|
||||
config->set_mod_version(ret.version.to_string());
|
||||
}
|
||||
|
||||
// Authors
|
||||
current_error = try_get_vec<json::string_t>(ret.authors, manifest_json, authors_key, true, error_param);
|
||||
if (current_error != ModOpenError::Good) {
|
||||
|
|
@ -681,11 +814,23 @@ recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const
|
|||
}
|
||||
|
||||
for (const json &option : *options) {
|
||||
ModOpenError open_error = parse_manifest_config_schema_option(option, ret, error_param);
|
||||
ModOpenError open_error = parse_manifest_config_schema_option(option, config, error_param);
|
||||
if (open_error != ModOpenError::Good) {
|
||||
return open_error;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse option dependencies after all options have been added
|
||||
// Requires config to not be null
|
||||
if (config != nullptr) {
|
||||
for (const json &option : *options) {
|
||||
ModOpenError dep_error = parse_config_option_dependencies(option, *config);
|
||||
if (dep_error != ModOpenError::Good) {
|
||||
error_param = dep_error == ModOpenError::InvalidDisableOptionDependency ? config_schema_disabled_from_key : config_schema_hidden_from_key;
|
||||
return dep_error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_options_key;
|
||||
|
|
@ -696,93 +841,44 @@ recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const
|
|||
return ModOpenError::Good;
|
||||
}
|
||||
|
||||
bool parse_mod_config_storage(const std::filesystem::path &path, const std::string &expected_mod_id, recomp::mods::ConfigStorage &config_storage, const recomp::mods::ConfigSchema &config_schema) {
|
||||
using json = nlohmann::json;
|
||||
json config_json;
|
||||
if (!read_json_with_backups(path, config_json)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto mod_id = config_json.find("mod_id");
|
||||
if (mod_id != config_json.end()) {
|
||||
std::string mod_id_str;
|
||||
if (get_to<json::string_t>(*mod_id, mod_id_str)) {
|
||||
if (*mod_id != expected_mod_id) {
|
||||
// The mod's ID doesn't match.
|
||||
bool parse_mod_config_storage(const std::string &expected_mod_id, recomp::config::Config &config) {
|
||||
return config.load_config([expected_mod_id](nlohmann::json &config_json) {
|
||||
auto mod_id = config_json.find("mod_id");
|
||||
if (mod_id != config_json.end()) {
|
||||
std::string mod_id_str;
|
||||
if (get_to<nlohmann::json::string_t>(*mod_id, mod_id_str)) {
|
||||
if (*mod_id != expected_mod_id) {
|
||||
// The mod's ID doesn't match.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The mod ID is not a string.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The mod ID is not a string.
|
||||
// The configuration file doesn't have a mod ID.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// The configuration file doesn't have a mod ID.
|
||||
return false;
|
||||
}
|
||||
|
||||
auto storage_json = config_json.find("storage");
|
||||
if (storage_json == config_json.end()) {
|
||||
// The configuration file doesn't have a storage object.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!storage_json->is_object()) {
|
||||
// The storage key does not correspond to an object.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only parse the object for known option types based on the schema.
|
||||
std::string value_str;
|
||||
for (const recomp::mods::ConfigOption &option : config_schema.options) {
|
||||
auto option_json = storage_json->find(option.id);
|
||||
if (option_json == storage_json->end()) {
|
||||
// Option doesn't exist in storage.
|
||||
continue;
|
||||
|
||||
auto storage_json = config_json.find("storage");
|
||||
if (storage_json == config_json.end()) {
|
||||
// The configuration file doesn't have a storage object.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!storage_json->is_object()) {
|
||||
// The storage key does not correspond to an object.
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case recomp::mods::ConfigOptionType::Enum:
|
||||
if (get_to<json::string_t>(*option_json, value_str)) {
|
||||
const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(option.variant);
|
||||
auto option_it = std::find(option_enum.options.begin(), option_enum.options.end(), value_str);
|
||||
if (option_it != option_enum.options.end()) {
|
||||
config_storage.value_map[option.id] = uint32_t(option_it - option_enum.options.begin());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::Number:
|
||||
if (option_json->is_number()) {
|
||||
config_storage.value_map[option.id] = option_json->template get<double>();
|
||||
}
|
||||
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::String: {
|
||||
if (get_to<json::string_t>(*option_json, value_str)) {
|
||||
config_storage.value_map[option.id] = value_str;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case recomp::mods::ConfigOptionType::Bool: {
|
||||
if (option_json->is_boolean()) {
|
||||
config_storage.value_map[option.id] = option_json->get<bool>();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
assert(false && "Unknown option type.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModManifest& manifest, std::string& error_param, const std::vector<ModContentTypeId>& supported_content_types, bool requires_manifest) {
|
||||
recomp::config::Config mod_config;
|
||||
{
|
||||
bool exists;
|
||||
std::vector<char> manifest_data = manifest.file_handle->read_file("mod.json", exists);
|
||||
|
|
@ -820,7 +916,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModM
|
|||
}
|
||||
}
|
||||
else {
|
||||
ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param);
|
||||
ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param, &mod_config);
|
||||
if (parse_error != ModOpenError::Good) {
|
||||
return parse_error;
|
||||
}
|
||||
|
|
@ -869,9 +965,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModM
|
|||
}
|
||||
|
||||
// Read the mod config if it exists.
|
||||
ConfigStorage config_storage;
|
||||
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);
|
||||
parse_mod_config_storage(manifest.mod_id, mod_config);
|
||||
|
||||
// Read the mod thumbnail if it exists.
|
||||
static const std::string thumbnail_dds_name = "thumb.dds";
|
||||
|
|
@ -883,7 +977,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModM
|
|||
}
|
||||
|
||||
// Store the loaded mod manifest in a new mod handle.
|
||||
add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data));
|
||||
add_opened_mod(std::move(manifest), std::move(mod_config), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data));
|
||||
|
||||
return ModOpenError::Good;
|
||||
}
|
||||
|
|
@ -978,6 +1072,8 @@ std::string recomp::mods::error_to_string(ModOpenError error) {
|
|||
return "Duplicate mod found";
|
||||
case ModOpenError::WrongGame:
|
||||
return "Mod is for a different game";
|
||||
case ModOpenError::DuplicateEnumStrings:
|
||||
return "Duplicate enum strings found in mod.json (enum strings are case insensitive)";
|
||||
}
|
||||
return "Unknown mod opening error: " + std::to_string((int)error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -266,9 +266,9 @@ recomp::mods::CodeModLoadError recomp::mods::validate_api_version(uint32_t api_v
|
|||
}
|
||||
}
|
||||
|
||||
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) :
|
||||
recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, recomp::config::Config&& config, 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)),
|
||||
config(std::move(config)),
|
||||
code_handle(),
|
||||
recompiler_context{std::make_unique<N64Recomp::Context>()},
|
||||
content_types{std::move(content_types)},
|
||||
|
|
@ -597,12 +597,12 @@ void unpatch_func(void* target_func, const recomp::mods::PatchData& data) {
|
|||
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) {
|
||||
void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, config::Config&& config, 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.emplace_back(*this, std::move(manifest), std::move(config), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail));
|
||||
opened_mods_order.emplace_back(mod_index);
|
||||
}
|
||||
|
||||
|
|
@ -646,49 +646,14 @@ void recomp::mods::ModContext::close_mods() {
|
|||
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) {
|
||||
bool save_mod_config_storage(const std::string &mod_id, const recomp::Version &mod_version, const recomp::config::Config *config, nlohmann::json &storage_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();
|
||||
|
||||
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;
|
||||
case recomp::mods::ConfigOptionType::Bool:
|
||||
storage_json[it.first] = std::get<bool>(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);
|
||||
config_json["storage"] = storage_json;
|
||||
return config->save_config_json(config_json);
|
||||
}
|
||||
|
||||
bool parse_mods_config(const std::filesystem::path &path, std::unordered_set<std::string> &enabled_mods, std::vector<std::string> &mod_order) {
|
||||
|
|
@ -734,12 +699,13 @@ bool save_mods_config(const std::filesystem::path &path, const std::unordered_se
|
|||
|
||||
void recomp::mods::ModContext::dirty_mod_configuration_thread_process() {
|
||||
using namespace std::chrono_literals;
|
||||
using json = nlohmann::json;
|
||||
|
||||
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_map<std::string, const recomp::config::Config*> pending_mod_configs;
|
||||
std::unordered_map<std::string, json> pending_mod_storage_json;
|
||||
std::unordered_set<std::string> config_enabled_mods;
|
||||
std::vector<std::string> config_mod_order;
|
||||
bool pending_config_save = false;
|
||||
|
|
@ -776,16 +742,14 @@ void recomp::mods::ModContext::dirty_mod_configuration_thread_process() {
|
|||
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;
|
||||
pending_mod_configs[id] = &mod.config;
|
||||
pending_mod_storage_json[id] = mod.config.get_json_config();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_mod_configs[id]->save_config_json(pending_mod_storage_json[id]);
|
||||
}
|
||||
|
||||
pending_mods.clear();
|
||||
|
|
@ -1422,7 +1386,7 @@ void recomp::mods::ModContext::set_mod_index(const std::string &mod_game_id, con
|
|||
mod_configuration_thread_queue.enqueue(ModConfigQueueSave());
|
||||
}
|
||||
|
||||
const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schema(const std::string &mod_id) const {
|
||||
const recomp::config::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()) {
|
||||
|
|
@ -1430,7 +1394,7 @@ const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schem
|
|||
}
|
||||
|
||||
const ModHandle &mod = opened_mods[find_it->second];
|
||||
return mod.manifest.config_schema;
|
||||
return mod.config.get_config_schema();
|
||||
}
|
||||
|
||||
const std::vector<char> &recomp::mods::ModContext::get_mod_thumbnail(const std::string &mod_id) const {
|
||||
|
|
@ -1444,7 +1408,7 @@ const std::vector<char> &recomp::mods::ModContext::get_mod_thumbnail(const std::
|
|||
return mod.thumbnail;
|
||||
}
|
||||
|
||||
void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value) {
|
||||
void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std::string &option_id, const recomp::config::ConfigValueVariant &value) {
|
||||
// Check that the mod exists.
|
||||
if (mod_index >= opened_mods.size()) {
|
||||
return;
|
||||
|
|
@ -1452,48 +1416,13 @@ void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std:
|
|||
|
||||
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;
|
||||
case ConfigOptionType::Bool:
|
||||
if (std::holds_alternative<bool>(value)) {
|
||||
mod.config_storage.value_map[option_id] = value;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
assert(false && "Unknown config option type.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
mod.config.set_option_value(option_id, value);
|
||||
|
||||
// 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) {
|
||||
void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const recomp::config::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()) {
|
||||
|
|
@ -1503,7 +1432,7 @@ void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, c
|
|||
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 {
|
||||
recomp::config::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();
|
||||
|
|
@ -1511,35 +1440,10 @@ recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(
|
|||
|
||||
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;
|
||||
case ConfigOptionType::Bool:
|
||||
return std::get<ConfigOptionBool>(option.variant).default_value;
|
||||
default:
|
||||
assert(false && "Unknown config option type.");
|
||||
return std::monostate();
|
||||
}
|
||||
}
|
||||
return mod.config.get_option_value(option_id);
|
||||
}
|
||||
|
||||
recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) const {
|
||||
recomp::config::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()) {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ void recomp::register_config_path(std::filesystem::path path) {
|
|||
config_path = path;
|
||||
}
|
||||
|
||||
std::filesystem::path recomp::get_config_path() {
|
||||
return config_path;
|
||||
}
|
||||
|
||||
bool recomp::register_game(const recomp::GameEntry& entry) {
|
||||
// TODO verify that there's no game with this ID already.
|
||||
{
|
||||
|
|
@ -560,7 +564,7 @@ bool recomp::mods::is_mod_auto_enabled(const std::string& mod_id) {
|
|||
return mod_context->is_mod_auto_enabled(mod_id);
|
||||
}
|
||||
|
||||
const recomp::mods::ConfigSchema &recomp::mods::get_mod_config_schema(const std::string &mod_id) {
|
||||
const recomp::config::ConfigSchema &recomp::mods::get_mod_config_schema(const std::string &mod_id) {
|
||||
std::lock_guard lock{ mod_context_mutex };
|
||||
return mod_context->get_mod_config_schema(mod_id);
|
||||
}
|
||||
|
|
@ -570,22 +574,22 @@ const std::vector<char> &recomp::mods::get_mod_thumbnail(const std::string &mod_
|
|||
return mod_context->get_mod_thumbnail(mod_id);
|
||||
}
|
||||
|
||||
void recomp::mods::set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value) {
|
||||
void recomp::mods::set_mod_config_value(size_t mod_index, const std::string &option_id, const recomp::config::ConfigValueVariant &value) {
|
||||
std::lock_guard lock{ mod_context_mutex };
|
||||
return mod_context->set_mod_config_value(mod_index, option_id, value);
|
||||
}
|
||||
|
||||
void recomp::mods::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) {
|
||||
void recomp::mods::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const recomp::config::ConfigValueVariant &value) {
|
||||
std::lock_guard lock{ mod_context_mutex };
|
||||
return mod_context->set_mod_config_value(mod_id, option_id, value);
|
||||
}
|
||||
|
||||
recomp::mods::ConfigValueVariant recomp::mods::get_mod_config_value(size_t mod_index, const std::string &option_id) {
|
||||
recomp::config::ConfigValueVariant recomp::mods::get_mod_config_value(size_t mod_index, const std::string &option_id) {
|
||||
std::lock_guard lock{ mod_context_mutex };
|
||||
return mod_context->get_mod_config_value(mod_index, option_id);
|
||||
}
|
||||
|
||||
recomp::mods::ConfigValueVariant recomp::mods::get_mod_config_value(const std::string &mod_id, const std::string &option_id) {
|
||||
recomp::config::ConfigValueVariant recomp::mods::get_mod_config_value(const std::string &mod_id, const std::string &option_id) {
|
||||
std::lock_guard lock{ mod_context_mutex };
|
||||
return mod_context->get_mod_config_value(mod_id, option_id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue