Skip to content

Commit b2bd7d4

Browse files
committed
refactor: 🚧 Adding ModConfig Resource
1 parent 8f92df8 commit b2bd7d4

File tree

5 files changed

+85
-165
lines changed

5 files changed

+85
-165
lines changed

addons/mod_loader/api/config.gd

Lines changed: 20 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -5,119 +5,34 @@ extends Object
55

66
const LOG_NAME := "ModLoader:Config"
77

8-
enum ML_CONFIG_STATUS {
9-
OK, # 0 = No errors
10-
NO_JSON_OK, # 1 = No custom JSON (file probably does not exist). Uses defaults from manifest, if available
11-
INVALID_MOD_ID, # 2 = Invalid mod ID
12-
NO_JSON_INVALID_KEY, # 3 = Invalid key, and no custom JSON was specified in the manifest defaults (`extra.godot.config_defaults`)
13-
INVALID_KEY # 4 = Invalid key, although config data does exists
14-
}
15-
16-
17-
# Retrieves the configuration data for a specific mod.
18-
# Returns a dictionary with three keys:
19-
# - "status_code": an integer indicating the status of the request
20-
# - "status_msg": a string containing a message explaining the status code
21-
# - "data": the configuration data
22-
#
23-
# Parameters:
24-
# - mod_dir_name: the name of the mod's directory / id
25-
#
26-
# Status Codes:
27-
# - ML_CONFIG_STATUS.OK: the request was successful and the configuration data is included in the "data" key
28-
# - ML_CONFIG_STATUS.INVALID_MOD_ID: the mod ID is not valid, and the "status_msg" key contains an error message
29-
# - ML_CONFIG_STATUS.NO_JSON_OK: there is no configuration file for the mod, the "data" key contains an empty dictionary
30-
#
31-
# Returns:
32-
# A dictionary with three keys: "status_code", "status_msg", and "data"
33-
static func get_mod_config(mod_dir_name: String) -> Dictionary:
34-
var status_code = ML_CONFIG_STATUS.OK
35-
var status_msg := ""
36-
var data = {} # can be anything
378

9+
# Retrieves the configuration data for a specific mod
10+
static func get_mod_config(mod_id: String) -> Dictionary:
3811
# Check if the mod ID is invalid
39-
if not ModLoaderStore.mod_data.has(mod_dir_name):
40-
status_code = ML_CONFIG_STATUS.INVALID_MOD_ID
41-
status_msg = "Mod ID was invalid: %s" % mod_dir_name
42-
43-
# Mod ID is valid
44-
if status_code == ML_CONFIG_STATUS.OK:
45-
var mod := ModLoaderStore.mod_data[mod_dir_name] as ModData
46-
var config_data := mod.config
47-
48-
# Check if there is no config file for the mod
49-
if config_data.size() == 0:
50-
status_code = ML_CONFIG_STATUS.NO_JSON_OK
51-
status_msg = "No config file for %s.json. " % mod_dir_name
52-
53-
# Config file exists
54-
if status_code == ML_CONFIG_STATUS.OK:
55-
data = config_data
56-
57-
# Log any errors that occurred
58-
if not status_code == ML_CONFIG_STATUS.OK:
59-
if status_code == ML_CONFIG_STATUS.NO_JSON_OK:
60-
# The mod has no user config file, which is not a critical error
61-
var full_msg = "Config JSON Notice: %s" % status_msg
62-
# Only log this once, to avoid flooding the log
63-
ModLoaderLog.debug(full_msg, mod_dir_name, true)
64-
else:
65-
# The error is critical (e.g. invalid mod ID)
66-
ModLoaderLog.fatal("Config JSON Error (%s): %s" % [status_code, status_msg], mod_dir_name)
67-
68-
return {
69-
"status_code": status_code,
70-
"status_msg": status_msg,
71-
"data": data,
72-
}
73-
74-
75-
# Returns a bool indicating if a retrieved mod config is valid.
76-
# Requires the full config object (ie. the dictionary that's returned by
77-
# `get_mod_config`)
78-
static func is_mod_config_data_valid(config_obj: Dictionary) -> bool:
79-
return config_obj.status_code <= ML_CONFIG_STATUS.NO_JSON_OK
80-
12+
if not ModLoaderStore.mod_data.has(mod_id):
13+
ModLoaderLog.fatal("Mod ID \"%s\" not found" % [mod_id], LOG_NAME)
14+
return {}
8115

82-
# Saves a full dictionary object to a mod's custom config file, as JSON.
83-
# Overwrites any existing data in the file.
84-
# Optionally updates the config object that's stored in memory (true by default).
85-
# Returns a bool indicating success or failure.
86-
# WARNING: Provides no validation
87-
static func save_mod_config_dictionary(mod_id: String, data: Dictionary, update_config: bool = true) -> bool:
88-
# Use `get_mod_config` to check if a custom JSON file already exists.
89-
# This has the added benefit of logging a fatal error if mod_name is
90-
# invalid (as it already happens in `get_mod_config`)
91-
var config_obj := get_mod_config(mod_id)
16+
var config_data = ModLoaderStore.mod_data[mod_id].config
9217

93-
if not is_mod_config_data_valid(config_obj):
94-
ModLoaderLog.warning("Could not save the config JSON file because the config data was invalid", mod_id)
95-
return false
18+
# Check if there is no config file for the mod
19+
if not config_data:
20+
ModLoaderLog.debug("No config for mod id \"%s\"" % mod_id, LOG_NAME, true)
21+
return {}
9622

97-
var data_original: Dictionary = config_obj.data
98-
var data_new := {}
23+
return config_data
9924

100-
# Merge
101-
if update_config:
102-
# Update the config held in memory
103-
data_original.merge(data, true)
104-
data_new = data_original
105-
else:
106-
# Don't update the config in memory
107-
data_new = data_original.duplicate(true)
108-
data_new.merge(data, true)
10925

110-
var configs_path := _ModLoaderPath.get_path_to_configs()
111-
var json_path := configs_path.plus_file(mod_id + ".json")
26+
static func is_mod_config_data_valid(config_data: ModConfig):
27+
var json_schema := JSONSchema.new()
28+
var error := json_schema.validate(config_data.get_data_as_string(), config_data.get_schema_as_string())
11229

113-
return _ModLoaderFile.save_dictionary_to_json_file(data_new, json_path)
11430

31+
static func update_mod_config(mod_id: String, data: Dictionary) -> void:
32+
# Update the config held in memory
33+
ModLoaderStore.mod_data[mod_id].config.merge(data, true)
11534

116-
# Saves a single settings to a mod's custom config file.
117-
# Returns a bool indicating success or failure.
118-
static func save_mod_config_setting(mod_id: String, key:String, value, update_config: bool = true) -> bool:
119-
var new_data = {
120-
key: value
121-
}
12235

123-
return save_mod_config_dictionary(mod_id, new_data, update_config)
36+
# Saves a full dictionary object to a mod's config file, as JSON.
37+
static func save_mod_config(config_data: ModConfig) -> bool:
38+
return _ModLoaderFile.save_dictionary_to_json_file(config_data.data, config_data.save_path)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class_name ModConfig
2+
extends Resource
3+
4+
5+
var mod_id: String
6+
var schema: Dictionary
7+
var data: Dictionary
8+
var save_path: String
9+
10+
11+
func get_data_as_string() -> String:
12+
return JSON.print(data)
13+
14+
15+
func get_schema_as_string() -> String:
16+
return JSON.print(schema)

addons/mod_loader/classes/mod_data.gd

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ var importance := 0
3838
# Contents of the manifest
3939
var manifest: ModManifest
4040
# Updated in _load_mod_configs
41-
var config := {}
41+
var config: ModConfig
4242

4343
# only set if DEBUG_ENABLE_STORING_FILEPATHS is enabled
4444
var file_paths: PoolStringArray = []
@@ -73,6 +73,51 @@ func load_manifest() -> void:
7373
manifest = mod_manifest
7474

7575

76+
func load_mod_config() -> void:
77+
config = ModConfig.new()
78+
config.save_path = _ModLoaderPath.get_path_to_configs().plus_file("%s.json" % dir_name)
79+
config.schema = manifest.config_schema
80+
81+
# Generate config_default based on the default values in config_schema
82+
_get_config_default_data(config.schema.properties)
83+
84+
# Validate the config defaults
85+
manifest.is_config_valid(
86+
config.get_data_as_string(),
87+
config.get_schema_as_string()
88+
)
89+
90+
# Save the default config to disk if there is no file yet
91+
if not _ModLoaderFile.file_exists(config.save_path):
92+
_ModLoaderFile.save_dictionary_to_json_file(config.data, config.save_path)
93+
94+
95+
# Recursively searches for default values
96+
func _get_config_default_data(property: Dictionary, current_prop := config.data) -> void:
97+
# Exit function if property is empty
98+
if property.empty():
99+
return
100+
101+
for property_key in property.keys():
102+
var prop = property[property_key]
103+
104+
# If this property contains nested properties, we recursively call this function
105+
if "properties" in prop:
106+
current_prop[property_key] = {}
107+
_get_config_default_data(prop.properties, current_prop[property_key])
108+
# Return early here because a object will not have a "default" key
109+
return
110+
111+
# If this property contains a default value, add it to the global config_defaults dictionary
112+
if "default" in prop:
113+
# Initialize the current_key if it is missing in config_defaults
114+
if not current_prop.has(property_key):
115+
current_prop[property_key] = {}
116+
117+
# Add the default value to the config_defaults
118+
current_prop[property_key] = prop.default
119+
120+
76121
# Validates if [member dir_name] matches [method ModManifest.get_mod_id]
77122
func _is_mod_dir_name_same_as_id(mod_manifest: ModManifest) -> bool:
78123
var manifest_id := mod_manifest.get_mod_id()

addons/mod_loader/classes/mod_manifest.gd

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ var incompatibilities: PoolStringArray = []
3232
var load_before: PoolStringArray = []
3333
var tags : PoolStringArray = []
3434
var config_schema := {}
35-
# Is generated based on the default values in config_schema
36-
var config_defaults := {}
3735
var description_rich := ""
3836
var image: StreamTexture
3937

@@ -136,9 +134,6 @@ func _init(manifest: Dictionary) -> void:
136134
):
137135
return
138136

139-
if not config_schema.empty():
140-
_handle_mod_config()
141-
142137

143138
# Mod ID used in the mod loader
144139
# Format: {namespace}-{name}
@@ -200,59 +195,6 @@ func to_json() -> String:
200195
}, "\t")
201196

202197

203-
func _handle_mod_config() -> void:
204-
var config_path := _ModLoaderPath.get_path_to_configs().plus_file("%s-%s.json" % [namespace, name])
205-
206-
# Generate config_default based on the default values in config_schema
207-
_get_config_default_data(config_schema.properties)
208-
209-
# Validate the config defaults
210-
is_config_valid(
211-
get_config_default_data_as_string(),
212-
get_config_schema_as_string()
213-
)
214-
215-
# Save the default config to disk if there is no file yet
216-
if not _ModLoaderFile.file_exists(config_path):
217-
_ModLoaderFile.save_dictionary_to_json_file(config_defaults, config_path)
218-
219-
220-
# Recursively searches for default values
221-
func _get_config_default_data(property: Dictionary, current_prop := config_defaults) -> void:
222-
# Exit function if property is empty
223-
if property.empty():
224-
return
225-
226-
for property_key in property.keys():
227-
var prop = property[property_key]
228-
229-
# If this property contains nested properties, we recursively call this function
230-
if "properties" in prop:
231-
current_prop[property_key] = {}
232-
_get_config_default_data(prop.properties, current_prop[property_key])
233-
# Return early here because a object will not have a "default" key
234-
return
235-
236-
# If this property contains a default value, add it to the global config_defaults dictionary
237-
if "default" in prop:
238-
# Initialize the current_key if it is missing in config_defaults
239-
if not current_prop.has(property_key):
240-
current_prop[property_key] = {}
241-
242-
# Add the default value to the config_defaults
243-
current_prop[property_key] = prop.default
244-
245-
246-
# Returns a JSON string representing the default configuration data
247-
func get_config_default_data_as_string() -> String:
248-
return JSON.print(config_defaults)
249-
250-
251-
# Returns a JSON string representing the configuration schema
252-
func get_config_schema_as_string() -> String:
253-
return JSON.print(config_schema)
254-
255-
256198
func is_config_valid(config_data: String, config_schema: String) -> bool:
257199
var json_schema := JSONSchema.new()
258200
var error := json_schema.validate(config_data, config_schema)

addons/mod_loader/mod_loader.gd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ func _load_mods() -> void:
9898
for dir_name in ModLoaderStore.mod_data:
9999
var mod: ModData = ModLoaderStore.mod_data[dir_name]
100100
mod.load_manifest()
101+
if not mod.manifest.get("config_schema") and not mod.manifest.config_schema.empty():
102+
mod.load_mod_config()
101103

102104
# Set up mod configs. If a mod's JSON file is found, its data gets added
103105
# to mod_data.{dir_name}.config
@@ -485,7 +487,7 @@ func save_scene(modified_scene: Node, scene_path: String) -> void:
485487

486488
func get_mod_config(mod_dir_name: String = "", key: String = "") -> Dictionary:
487489
ModLoaderDeprecated.deprecated_changed("ModLoader.get_mod_config", "ModLoaderConfig.get_mod_config", "6.0.0")
488-
return ModLoaderConfig.get_mod_config(mod_dir_name, key)
490+
return ModLoaderConfig.get_mod_config(mod_dir_name)
489491

490492

491493
func deprecated_direct_access_UNPACKED_DIR() -> String:

0 commit comments

Comments
 (0)