config.gd 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. class_name ModLoaderConfig
  2. extends Object
  3. ##
  4. ## Class for managing per-mod configurations.
  5. ##
  6. ## @tutorial(Creating a Mod Config Schema with JSON-Schemas): https://wiki.godotmodding.com/guides/modding/config_json/
  7. const LOG_NAME := "ModLoader:Config"
  8. const DEFAULT_CONFIG_NAME := "default"
  9. ## Creates a new configuration for a mod.[br]
  10. ## [br]
  11. ## [b]Parameters:[/b][br]
  12. ## - [param mod_id] ([String]): The ID of the mod.[br]
  13. ## - [param config_name] ([String]): The name of the configuration.[br]
  14. ## - [param config_data] ([Dictionary]): The configuration data to be stored.[br]
  15. ## [br]
  16. ## [b]Returns:[/b][br]
  17. ## - [ModConfig]: The created [ModConfig] object if successful, or null otherwise.
  18. static func create_config(mod_id: String, config_name: String, config_data: Dictionary) -> ModConfig:
  19. var default_config: ModConfig = get_default_config(mod_id)
  20. if not default_config:
  21. ModLoaderLog.error(
  22. "Failed to create config \"%s\". No config schema found for \"%s\"."
  23. % [config_name, mod_id], LOG_NAME
  24. )
  25. return null
  26. # Make sure the config name is not empty
  27. if config_name == "":
  28. ModLoaderLog.error(
  29. "Failed to create config \"%s\". The config name cannot be empty."
  30. % config_name, LOG_NAME
  31. )
  32. return null
  33. # Make sure the config name is unique
  34. if ModLoaderStore.mod_data[mod_id].configs.has(config_name):
  35. ModLoaderLog.error(
  36. "Failed to create config \"%s\". A config with the name \"%s\" already exists."
  37. % [config_name, config_name], LOG_NAME
  38. )
  39. return null
  40. # Create the config save path based on the config_name
  41. var config_file_path := _ModLoaderPath.get_path_to_mod_configs_dir(mod_id).path_join("%s.json" % config_name)
  42. # Initialize a new ModConfig object with the provided parameters
  43. var mod_config := ModConfig.new(
  44. mod_id,
  45. config_data,
  46. config_file_path
  47. )
  48. # Check if the mod_config is valid
  49. if not mod_config.is_valid:
  50. return null
  51. # Store the mod_config in the mod's ModData
  52. ModLoaderStore.mod_data[mod_id].configs[config_name] = mod_config
  53. # Save the mod_config to a new config JSON file in the mod's config directory
  54. var is_save_success := mod_config.save_to_file()
  55. if not is_save_success:
  56. return null
  57. ModLoaderLog.debug("Created new config \"%s\" for mod \"%s\"" % [config_name, mod_id], LOG_NAME)
  58. return mod_config
  59. ## Updates an existing [ModConfig] object with new data and saves the config file.[br]
  60. ## [br]
  61. ## [b]Parameters:[/b][br]
  62. ## - [param config] ([ModConfig]): The [ModConfig] object to be updated.[br]
  63. ## [br]
  64. ## [b]Returns:[/b][br]
  65. ## - [ModConfig]: The updated [ModConfig] object if successful, or null otherwise.
  66. static func update_config(config: ModConfig) -> ModConfig:
  67. # Validate the config and check for any validation errors
  68. var error_message := config.validate()
  69. # Check if the config is the "default" config, which cannot be modified
  70. if config.name == DEFAULT_CONFIG_NAME:
  71. ModLoaderLog.error("The \"default\" config cannot be modified. Please create a new config instead.", LOG_NAME)
  72. return null
  73. # Check if the config passed validation
  74. if not config.is_valid:
  75. ModLoaderLog.error("Update for config \"%s\" failed validation with error message \"%s\"" % [config.name, error_message], LOG_NAME)
  76. return null
  77. # Save the updated config to the config file
  78. var is_save_success := config.save_to_file()
  79. if not is_save_success:
  80. ModLoaderLog.error("Failed to save config \"%s\" to \"%s\"." % [config.name, config.save_path], LOG_NAME)
  81. return null
  82. # Return the updated config
  83. return config
  84. ## Deletes a [ModConfig] object and performs cleanup operations.[br]
  85. ## [br]
  86. ## [b]Parameters:[/b][br]
  87. ## - [param config] ([ModConfig]): The [ModConfig] object to be deleted.[br]
  88. ## [br]
  89. ## [b]Returns:[/b][br]
  90. ## - [bool]: True if the deletion was successful, False otherwise.
  91. static func delete_config(config: ModConfig) -> bool:
  92. # Check if the config is the "default" config, which cannot be deleted
  93. if config.name == DEFAULT_CONFIG_NAME:
  94. ModLoaderLog.error("Deletion of the default configuration is not allowed.", LOG_NAME)
  95. return false
  96. # Change the current config to the "default" config
  97. set_current_config(get_default_config(config.mod_id))
  98. # Remove the config file from the Mod Config directory
  99. var is_remove_success := config.remove_file()
  100. if not is_remove_success:
  101. return false
  102. # Remove the config from ModData
  103. ModLoaderStore.mod_data[config.mod_id].configs.erase(config.name)
  104. return true
  105. ## Sets the current configuration of a mod to the specified configuration.[br]
  106. ## [br]
  107. ## [b]Parameters:[/b][br]
  108. ## - [param config] ([ModConfig]): The [ModConfig] object to be set as current config.
  109. static func set_current_config(config: ModConfig) -> void:
  110. ModLoaderStore.mod_data[config.mod_id].current_config = config
  111. ## Returns the schema for the specified mod id.[br]
  112. ## [br]
  113. ## [b]Parameters:[/b][br]
  114. ## - [param mod_id] ([String]): The ID of the mod.[br]
  115. ## [br]
  116. ## [b]Returns:[/b][br]
  117. ## - A dictionary representing the schema for the mod's configuration file.
  118. static func get_config_schema(mod_id: String) -> Dictionary:
  119. # Get all config files for the specified mod
  120. var mod_configs := get_configs(mod_id)
  121. # If no config files were found, return an empty dictionary
  122. if mod_configs.is_empty():
  123. return {}
  124. # The schema is the same for all config files, so we just return the schema of the default config file
  125. return mod_configs.default.schema
  126. ## Retrieves the schema for a specific property key.[br]
  127. ## [br]
  128. ## [b]Parameters:[/b][br]
  129. ## - [param config] ([ModConfig]): The [ModConfig] object from which to retrieve the schema.[br]
  130. ## - [param prop] ([String]): The property key for which to retrieve the schema.[br]
  131. ## [br]
  132. ## [b]Returns:[/b][br]
  133. ## - [Dictionary]: The schema dictionary for the specified property.
  134. static func get_schema_for_prop(config: ModConfig, prop: String) -> Dictionary:
  135. # Split the property string into an array of property keys
  136. var prop_array := prop.split(".")
  137. # If the property array is empty, return the schema for the root property
  138. if prop_array.is_empty():
  139. return config.schema.properties[prop]
  140. # Traverse the schema dictionary to find the schema for the specified property
  141. var schema_for_prop := _traverse_schema(config.schema.properties, prop_array)
  142. # If the schema for the property is empty, log an error and return an empty dictionary
  143. if schema_for_prop.is_empty():
  144. ModLoaderLog.error("No Schema found for property \"%s\" in config \"%s\" for mod \"%s\"" % [prop, config.name, config.mod_id], LOG_NAME)
  145. return {}
  146. return schema_for_prop
  147. # Recursively traverses the schema dictionary based on the provided [code]prop_key_array[/code]
  148. # and returns the corresponding schema for the target property.[br]
  149. # [br]
  150. # [b]Parameters:[/b][br]
  151. # - [param schema_prop]: The current schema dictionary to traverse.[br]
  152. # - [param prop_key_array]: An array containing the property keys representing the path to the target property.[br]
  153. # [br]
  154. # [b]Returns:[/b][br]
  155. # - [Dictionary]: The schema dictionary corresponding to the target property specified by the [code]prop_key_array[/code].
  156. # If the target property is not found, an empty dictionary is returned.
  157. static func _traverse_schema(schema_prop: Dictionary, prop_key_array: Array) -> Dictionary:
  158. # Return the current schema_prop if the prop_key_array is empty (reached the destination property)
  159. if prop_key_array.is_empty():
  160. return schema_prop
  161. # Get and remove the first prop_key in the array
  162. var prop_key: String = prop_key_array.pop_front()
  163. # Check if the searched property exists
  164. if not schema_prop.has(prop_key):
  165. return {}
  166. schema_prop = schema_prop[prop_key]
  167. # If the schema_prop has a 'type' key, is of type 'object', and there are more property keys remaining
  168. if schema_prop.has("type") and schema_prop.type == "object" and not prop_key_array.is_empty():
  169. # Set the properties of the object as the current 'schema_prop'
  170. schema_prop = schema_prop.properties
  171. schema_prop = _traverse_schema(schema_prop, prop_key_array)
  172. return schema_prop
  173. ## Retrieves an Array of mods that have configuration files.[br]
  174. ## [br]
  175. ## [b]Returns:[/b][br]
  176. ## - [Array]: An Array containing the mod data of mods that have configuration files.
  177. static func get_mods_with_config() -> Array:
  178. # Create an empty array to store mods with configuration files
  179. var mods_with_config := []
  180. # Iterate over each mod in ModLoaderStore.mod_data
  181. for mod_id in ModLoaderStore.mod_data:
  182. # Retrieve the mod data for the current mod ID
  183. # *The ModData type cannot be used because ModData is not fully loaded when this code is executed.*
  184. var mod_data = ModLoaderStore.mod_data[mod_id]
  185. # Check if the mod has any configuration files
  186. if not mod_data.configs.is_empty():
  187. mods_with_config.push_back(mod_data)
  188. # Return the array of mods with configuration files
  189. return mods_with_config
  190. ## Retrieves the configurations dictionary for a given mod ID.[br]
  191. ## [br]
  192. ## [b]Parameters:[/b][br]
  193. ## - [param mod_id]: The ID of the mod.[br]
  194. ## [br]
  195. ## [b]Returns:[/b][br]
  196. ## - [Dictionary]: A dictionary containing the configurations for the specified mod.
  197. ## If the mod ID is invalid or no configurations are found, an empty dictionary is returned.
  198. static func get_configs(mod_id: String) -> Dictionary:
  199. # Check if the mod ID is invalid
  200. if not ModLoaderStore.mod_data.has(mod_id):
  201. ModLoaderLog.fatal("Mod ID \"%s\" not found" % [mod_id], LOG_NAME)
  202. return {}
  203. var config_dictionary: Dictionary = ModLoaderStore.mod_data[mod_id].configs
  204. # Check if there is no config file for the mod
  205. if config_dictionary.is_empty():
  206. ModLoaderLog.debug("No config for mod id \"%s\"" % mod_id, LOG_NAME, true)
  207. return {}
  208. return config_dictionary
  209. ## Retrieves the configuration for a specific mod and configuration name.[br]
  210. ## [br]
  211. ## [b]Parameters:[/b][br]
  212. ## - [param mod_id] ([String]): The ID of the mod.[br]
  213. ## - [param config_name] ([String]): The name of the configuration.[br]
  214. ## [br]
  215. ## [b]Returns:[/b][br]
  216. ## - [ModConfig]: The configuration as a [ModConfig] object or null if not found.
  217. static func get_config(mod_id: String, config_name: String) -> ModConfig:
  218. var configs := get_configs(mod_id)
  219. if not configs.has(config_name):
  220. ModLoaderLog.error("No config with name \"%s\" found for mod_id \"%s\" " % [config_name, mod_id], LOG_NAME)
  221. return null
  222. return configs[config_name]
  223. ## Checks whether a mod has a current configuration set.[br]
  224. ## [br]
  225. ## [b]Parameters:[/b][br]
  226. ## - [param mod_id] ([String]): The ID of the mod.[br]
  227. ## [br]
  228. ## [b]Returns:[/b][br]
  229. ## - [bool]: True if the mod has a current configuration, false otherwise.
  230. static func has_current_config(mod_id: String) -> bool:
  231. var mod_data := ModLoaderMod.get_mod_data(mod_id)
  232. return not mod_data.current_config == null
  233. ## Checks whether a mod has a configuration with the specified name.[br]
  234. ## [br]
  235. ## [b]Parameters:[/b][br]
  236. ## - [param mod_id] ([String]): The ID of the mod.[br]
  237. ## - [param config_name] ([String]): The name of the configuration.[br]
  238. ## [br]
  239. ## [b]Returns:[/b][br]
  240. ## - [bool]: True if the mod has a configuration with the specified name, False otherwise.
  241. static func has_config(mod_id: String, config_name: String) -> bool:
  242. var mod_data := ModLoaderMod.get_mod_data(mod_id)
  243. return mod_data.configs.has(config_name)
  244. ## Retrieves the default configuration for a specified mod ID.[br]
  245. ## [br]
  246. ## [b]Parameters:[/b][br]
  247. ## - [param mod_id] ([String]): The ID of the mod.[br]
  248. ## [br]
  249. ## [b]Returns:[/b][br]
  250. ## - [ModConfig]: The [ModConfig] object representing the default configuration for the specified mod.
  251. ## If the mod ID is invalid or no configuration is found, returns null.
  252. static func get_default_config(mod_id: String) -> ModConfig:
  253. return get_config(mod_id, DEFAULT_CONFIG_NAME)
  254. ## Retrieves the currently active configuration for a specific mod.[br]
  255. ## [br]
  256. ## [b]Parameters:[/b][br]
  257. ## - [param mod_id] ([String]): The ID of the mod.[br]
  258. ## [br]
  259. ## [b]Returns:[/b][br]
  260. ## - [ModConfig]: The configuration as a [ModConfig] object or [code]null[/code] if not found.
  261. static func get_current_config(mod_id: String) -> ModConfig:
  262. var current_config_name := get_current_config_name(mod_id)
  263. var current_config: ModConfig
  264. # Load the default configuration if there is no configuration set as current yet
  265. # Otherwise load the corresponding configuration
  266. if current_config_name.is_empty():
  267. current_config = get_default_config(mod_id)
  268. else:
  269. current_config = get_config(mod_id, current_config_name)
  270. return current_config
  271. ## Retrieves the name of the current configuration for a specific mod.[br]
  272. ## [br]
  273. ## [b]Parameters:[/b][br]
  274. ## - [param mod_id] ([String]): The ID of the mod.[br]
  275. ## [br]
  276. ## [b]Returns:[/b][br]
  277. ## - [String] The currently active configuration name for the given mod id or an empty string if not found.
  278. static func get_current_config_name(mod_id: String) -> String:
  279. # Check if user profile has been loaded
  280. if not ModLoaderStore.current_user_profile or not ModLoaderStore.user_profiles.has(ModLoaderStore.current_user_profile.name):
  281. # Warn and return an empty string if the user profile has not been loaded
  282. ModLoaderLog.warning("Can't get current mod config name for \"%s\", because no current user profile is present." % mod_id, LOG_NAME)
  283. return ""
  284. # Retrieve the current user profile from ModLoaderStore
  285. # *Can't use ModLoaderUserProfile because it causes a cyclic dependency*
  286. var current_user_profile = ModLoaderStore.current_user_profile
  287. # Check if the mod exists in the user profile's mod list and if it has a current config
  288. if not current_user_profile.mod_list.has(mod_id) or not current_user_profile.mod_list[mod_id].has("current_config"):
  289. # Log an error and return an empty string if the mod has no config file
  290. ModLoaderLog.error("Can't get current mod config name for \"%s\" because no config file exists." % mod_id, LOG_NAME)
  291. return ""
  292. # Return the name of the current configuration for the mod
  293. return current_user_profile.mod_list[mod_id].current_config
  294. ## Refreshes the data of the provided configuration by reloading it from the config file.[br]
  295. ## [br]
  296. ## [b]Parameters:[/b][br]
  297. ## - [param config] ([ModConfig]): The [ModConfig] object whose data needs to be refreshed.[br]
  298. ## [br]
  299. ## [b]Returns:[/b][br]
  300. ## - [ModConfig]: The [ModConfig] object with refreshed data if successful, or the original object otherwise.
  301. static func refresh_config_data(config: ModConfig) -> ModConfig:
  302. # Retrieve updated configuration data from the config file
  303. var new_config_data := _ModLoaderFile.get_json_as_dict(config.save_path)
  304. # Update the data property of the ModConfig object with the refreshed data
  305. config.data = new_config_data
  306. return config
  307. ## Iterates over all mods to refresh the data of their current configurations, if available.[br]
  308. ## [br]
  309. ## [b]Returns:[/b][br]
  310. ## - No return value[br]
  311. ## [br]
  312. ## Compares the previous configuration data with the refreshed data and emits the [signal ModLoader.current_config_changed]
  313. ## signal if changes are detected.[br]
  314. ## This function ensures that any changes made to the configuration files outside the application
  315. ## are reflected within the application's runtime, allowing for dynamic updates without the need for a restart.
  316. static func refresh_current_configs() -> void:
  317. for mod_id in ModLoaderMod.get_mod_data_all().keys():
  318. # Skip if the mod has no config
  319. if not has_current_config(mod_id):
  320. return
  321. # Retrieve the current configuration for the mod
  322. var config := get_current_config(mod_id)
  323. # Create a deep copy of the current configuration data for comparison
  324. var config_data_previous := config.data.duplicate(true)
  325. # Refresh the configuration data
  326. var config_new := refresh_config_data(config)
  327. # Compare previous data with refreshed data
  328. if not config_data_previous == config_new.data:
  329. # Emit signal indicating that the current configuration has changed
  330. ModLoader.current_config_changed.emit(config)