mirror of
https://github.com/N64Recomp/N64ModernRuntime.git
synced 2025-10-30 08:02:29 +00:00
Add support for config schema.
This commit is contained in:
parent
985e600f7a
commit
0b18a35292
5 changed files with 292 additions and 31 deletions
|
|
@ -1,11 +0,0 @@
|
|||
#ifndef __RECOMP_CONFIG_H__
|
||||
#define __RECOMP_CONFIG_H__
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <mutex>
|
||||
#include "json/json.hpp"
|
||||
|
||||
#endif
|
||||
|
|
@ -65,6 +65,9 @@ namespace recomp {
|
|||
FailedToParseManifest,
|
||||
InvalidManifestSchema,
|
||||
IncorrectManifestFieldType,
|
||||
MissingConfigSchemaField,
|
||||
IncorrectConfigSchemaType,
|
||||
InvalidConfigSchemaDefault,
|
||||
InvalidVersionString,
|
||||
InvalidMinimumRecompVersionString,
|
||||
InvalidDependencyString,
|
||||
|
|
@ -115,6 +118,13 @@ namespace recomp {
|
|||
|
||||
std::string error_to_string(CodeModLoadError);
|
||||
|
||||
enum class ConfigOptionType {
|
||||
None,
|
||||
Enum,
|
||||
Number,
|
||||
String
|
||||
};
|
||||
|
||||
struct ModFileHandle {
|
||||
virtual ~ModFileHandle() = default;
|
||||
virtual std::vector<char> read_file(const std::string& filepath, bool& exists) const = 0;
|
||||
|
|
@ -154,6 +164,38 @@ namespace recomp {
|
|||
Version version;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
typedef std::variant<ConfigOptionEnum, ConfigOptionNumber, ConfigOptionString> ConfigOptionVariant;
|
||||
|
||||
struct ConfigOption {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
ConfigOptionType type;
|
||||
ConfigOptionVariant variant;
|
||||
};
|
||||
|
||||
struct ConfigSchema {
|
||||
std::vector<ConfigOption> options;
|
||||
};
|
||||
|
||||
struct ModDetails {
|
||||
std::string mod_id;
|
||||
std::string display_name;
|
||||
|
|
@ -176,6 +218,7 @@ 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;
|
||||
|
|
@ -258,6 +301,7 @@ namespace recomp {
|
|||
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);
|
||||
const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const;
|
||||
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; }
|
||||
|
|
@ -299,6 +343,7 @@ namespace recomp {
|
|||
// Tracks which hook slots have already been processed. Used to regenerate vanilla functions as needed
|
||||
// to add hooks to any functions that weren't already replaced by a mod.
|
||||
std::vector<bool> processed_hook_slots;
|
||||
ConfigSchema empty_schema;
|
||||
size_t num_events = 0;
|
||||
ModContentTypeId code_content_type_id;
|
||||
size_t active_game = (size_t)-1;
|
||||
|
|
@ -462,6 +507,7 @@ 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);
|
||||
ModContentTypeId register_mod_content_type(const ModContentType& type);
|
||||
bool register_mod_container_type(const std::string& extension, const std::vector<ModContentTypeId>& content_types, bool requires_manifest);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,16 +131,6 @@ bool recomp::mods::LooseModFileHandle::file_exists(const std::string& filepath)
|
|||
return true;
|
||||
}
|
||||
|
||||
enum class ManifestField {
|
||||
GameModId,
|
||||
Id,
|
||||
Version,
|
||||
Authors,
|
||||
MinimumRecompVersion,
|
||||
Dependencies,
|
||||
NativeLibraries,
|
||||
};
|
||||
|
||||
const std::string game_mod_id_key = "game_id";
|
||||
const std::string mod_id_key = "id";
|
||||
const std::string display_name_key = "display_name";
|
||||
|
|
@ -151,16 +141,7 @@ const std::string authors_key = "authors";
|
|||
const std::string minimum_recomp_version_key = "minimum_recomp_version";
|
||||
const std::string dependencies_key = "dependencies";
|
||||
const std::string native_libraries_key = "native_libraries";
|
||||
|
||||
std::unordered_map<std::string, ManifestField> field_map {
|
||||
{ game_mod_id_key, ManifestField::GameModId },
|
||||
{ mod_id_key, ManifestField::Id },
|
||||
{ version_key, ManifestField::Version },
|
||||
{ authors_key, ManifestField::Authors },
|
||||
{ minimum_recomp_version_key, ManifestField::MinimumRecompVersion },
|
||||
{ dependencies_key, ManifestField::Dependencies },
|
||||
{ native_libraries_key, ManifestField::NativeLibraries },
|
||||
};
|
||||
const std::string config_schema_key = "config_schema";
|
||||
|
||||
template <typename T1, typename T2>
|
||||
bool get_to(const nlohmann::json& val, T2& out) {
|
||||
|
|
@ -298,6 +279,200 @@ recomp::mods::ModOpenError try_get_vec(std::vector<T2>& out, const nlohmann::jso
|
|||
return recomp::mods::ModOpenError::Good;
|
||||
}
|
||||
|
||||
constexpr std::string_view config_schema_id_key = "id";
|
||||
constexpr std::string_view config_schema_name_key = "name";
|
||||
constexpr std::string_view config_schema_description_key = "description";
|
||||
constexpr std::string_view config_schema_type_key = "type";
|
||||
constexpr std::string_view config_schema_min_key = "min";
|
||||
constexpr std::string_view config_schema_max_key = "max";
|
||||
constexpr std::string_view config_schema_step_key = "step";
|
||||
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 = "min";
|
||||
|
||||
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},
|
||||
};
|
||||
|
||||
recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::json &config_schema_json, recomp::mods::ModManifest &ret, std::string &error_param) {
|
||||
using json = nlohmann::json;
|
||||
recomp::mods::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)) {
|
||||
error_param = config_schema_id_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_id_key;
|
||||
return recomp::mods::ModOpenError::MissingConfigSchemaField;
|
||||
}
|
||||
|
||||
auto name = config_schema_json.find(config_schema_name_key);
|
||||
if (name != config_schema_json.end()) {
|
||||
if (!get_to<json::string_t>(*name, option.name)) {
|
||||
error_param = config_schema_name_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_name_key;
|
||||
return recomp::mods::ModOpenError::MissingConfigSchemaField;
|
||||
}
|
||||
|
||||
auto description = config_schema_json.find(config_schema_description_key);
|
||||
if (description != config_schema_json.end()) {
|
||||
if (!get_to<json::string_t>(*description, option.description)) {
|
||||
error_param = config_schema_description_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
auto type = config_schema_json.find(config_schema_type_key);
|
||||
if (type != config_schema_json.end()) {
|
||||
std::string type_string;
|
||||
if (!get_to<json::string_t>(*type, type_string)) {
|
||||
error_param = config_schema_type_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
else {
|
||||
auto it = config_option_map.find(type_string);
|
||||
if (it != config_option_map.end()) {
|
||||
option.type = it->second;
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_type_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_type_key;
|
||||
return recomp::mods::ModOpenError::MissingConfigSchemaField;
|
||||
}
|
||||
|
||||
switch (option.type) {
|
||||
case recomp::mods::ConfigOptionType::Enum:
|
||||
{
|
||||
recomp::mods::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)) {
|
||||
error_param = config_schema_options_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (it != option_enum.options.end()) {
|
||||
option_enum.default_value = uint32_t(it - option_enum.options.begin());
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_default_key;
|
||||
return recomp::mods::ModOpenError::InvalidConfigSchemaDefault;
|
||||
}
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_default_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
option.variant = option_enum;
|
||||
|
||||
}
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::Number:
|
||||
{
|
||||
recomp::mods::ConfigOptionNumber option_number;
|
||||
|
||||
auto min = config_schema_json.find(config_schema_min_key);
|
||||
if (min != config_schema_json.end()) {
|
||||
if (!get_to<double>(*min, option_number.min)) {
|
||||
error_param = config_schema_min_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
auto max = config_schema_json.find(config_schema_max_key);
|
||||
if (max != config_schema_json.end()) {
|
||||
if (!get_to<double>(*max, option_number.max)) {
|
||||
error_param = config_schema_max_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
auto step = config_schema_json.find(config_schema_step_key);
|
||||
if (step != config_schema_json.end()) {
|
||||
if (!get_to<double>(*step, option_number.step)) {
|
||||
error_param = config_schema_step_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
auto precision = config_schema_json.find(config_schema_precision_key);
|
||||
if (precision != config_schema_json.end()) {
|
||||
int64_t precision_int64;
|
||||
if (get_to<int64_t>(*precision, precision_int64)) {
|
||||
option_number.precision = precision_int64;
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_precision_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
auto percent = config_schema_json.find(config_schema_percent_key);
|
||||
if (percent != config_schema_json.end()) {
|
||||
if (!get_to<bool>(*percent, option_number.percent)) {
|
||||
error_param = config_schema_percent_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
auto default_value = config_schema_json.find(config_schema_default_key);
|
||||
if (default_value != config_schema_json.end()) {
|
||||
if (!get_to<double>(*default_value, option_number.default_value)) {
|
||||
error_param = config_schema_default_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
option.variant = option_number;
|
||||
}
|
||||
break;
|
||||
case recomp::mods::ConfigOptionType::String:
|
||||
{
|
||||
recomp::mods::ConfigOptionString option_string;
|
||||
|
||||
auto default_value = config_schema_json.find(config_schema_default_key);
|
||||
if (default_value != config_schema_json.end()) {
|
||||
if (!get_to<json::string_t>(*default_value, option_string.default_value)) {
|
||||
error_param = config_schema_default_key;
|
||||
return recomp::mods::ModOpenError::IncorrectConfigSchemaType;
|
||||
}
|
||||
}
|
||||
|
||||
option.variant = option_string;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
ret.config_schema.options.push_back(option);
|
||||
return recomp::mods::ModOpenError::Good;
|
||||
}
|
||||
|
||||
recomp::mods::ModOpenError parse_manifest(recomp::mods::ModManifest& ret, const std::vector<char>& manifest_data, std::string& error_param) {
|
||||
using json = nlohmann::json;
|
||||
json manifest_json = json::parse(manifest_data.begin(), manifest_data.end(), nullptr, false);
|
||||
|
|
@ -399,6 +574,35 @@ recomp::mods::ModOpenError parse_manifest(recomp::mods::ModManifest& ret, const
|
|||
}
|
||||
}
|
||||
|
||||
// Config schema (optional)
|
||||
auto find_config_schema_it = manifest_json.find(config_schema_key);
|
||||
if (find_config_schema_it != manifest_json.end()) {
|
||||
auto& val = *find_config_schema_it;
|
||||
if (!val.is_object()) {
|
||||
error_param = config_schema_key;
|
||||
return recomp::mods::ModOpenError::IncorrectManifestFieldType;
|
||||
}
|
||||
|
||||
auto options = val.find(config_schema_options_key);
|
||||
if (options != val.end()) {
|
||||
if (!options->is_array()) {
|
||||
error_param = config_schema_options_key;
|
||||
return recomp::mods::ModOpenError::IncorrectManifestFieldType;
|
||||
}
|
||||
|
||||
for (const json &option : *options) {
|
||||
recomp::mods::ModOpenError open_error = parse_manifest_config_schema_option(option, ret, error_param);
|
||||
if (open_error != recomp::mods::ModOpenError::Good) {
|
||||
return open_error;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
error_param = config_schema_options_key;
|
||||
return recomp::mods::ModOpenError::MissingConfigSchemaField;
|
||||
}
|
||||
}
|
||||
|
||||
return recomp::mods::ModOpenError::Good;
|
||||
}
|
||||
|
||||
|
|
@ -547,6 +751,12 @@ std::string recomp::mods::error_to_string(ModOpenError error) {
|
|||
return "Mod's mod.json has an invalid schema";
|
||||
case ModOpenError::IncorrectManifestFieldType:
|
||||
return "Incorrect type for field in mod.json";
|
||||
case ModOpenError::MissingConfigSchemaField:
|
||||
return "Missing required field in config schema in mod.json";
|
||||
case ModOpenError::IncorrectConfigSchemaType:
|
||||
return "Incorrect type for field in config schema in mod.json";
|
||||
case ModOpenError::InvalidConfigSchemaDefault:
|
||||
return "Invalid default for option in config schema in mod.json";
|
||||
case ModOpenError::InvalidVersionString:
|
||||
return "Invalid version string in mod.json";
|
||||
case ModOpenError::InvalidMinimumRecompVersionString:
|
||||
|
|
|
|||
|
|
@ -881,6 +881,17 @@ N64Recomp::Context context_from_regenerated_list(const RegeneratedList& regenlis
|
|||
return ret;
|
||||
}
|
||||
|
||||
const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schema(const std::string &mod_id) const {
|
||||
// Check that the mod exists.
|
||||
auto find_it = opened_mods_by_id.find(mod_id);
|
||||
if (find_it == opened_mods_by_id.end()) {
|
||||
return empty_schema;
|
||||
}
|
||||
|
||||
const ModHandle &mod = opened_mods[find_it->second];
|
||||
return mod.manifest.config_schema;
|
||||
}
|
||||
|
||||
std::vector<recomp::mods::ModLoadErrorDetails> recomp::mods::ModContext::load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used) {
|
||||
std::vector<recomp::mods::ModLoadErrorDetails> ret{};
|
||||
ram_used = 0;
|
||||
|
|
|
|||
|
|
@ -515,6 +515,11 @@ bool recomp::mods::is_mod_auto_enabled(const std::string& mod_id) {
|
|||
return false; // TODO
|
||||
}
|
||||
|
||||
const recomp::mods::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);
|
||||
}
|
||||
|
||||
std::vector<recomp::mods::ModDetails> recomp::mods::get_mod_details(const std::string& mod_game_id) {
|
||||
std::lock_guard lock { mod_context_mutex };
|
||||
return mod_context->get_mod_details(mod_game_id);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue