profile.gd 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. class_name ModLoaderUserProfile
  2. extends Object
  3. ##
  4. ## This Class provides methods for working with user profiles.
  5. const LOG_NAME := "ModLoader:UserProfile"
  6. # The path where the Mod User Profiles data is stored.
  7. const FILE_PATH_USER_PROFILES := "user://mod_user_profiles.json"
  8. # API profile functions
  9. # =============================================================================
  10. ## Enables a mod - it will be loaded on the next game start[br]
  11. ## [br]
  12. ## [b]Parameters:[/b][br]
  13. ## - [param mod_id] ([String]): The ID of the mod to enable.[br]
  14. ## - [param user_profile] ([ModUserProfile]): (Optional) The user profile to enable the mod for. Default is the current user profile.[br]
  15. ## [br]
  16. ## [b]Returns:[/b][br]
  17. ## - [bool]: True on success.
  18. static func enable_mod(mod_id: String, user_profile:= ModLoaderStore.current_user_profile) -> bool:
  19. return _set_mod_state(mod_id, user_profile.name, true)
  20. ## Forces a mod to enable, ensuring it loads at the next game start, regardless of load warnings.[br]
  21. ## [br]
  22. ## [b]Parameters:[/b][br]
  23. ## - [param mod_id] ([String]): The ID of the mod to enable.[br]
  24. ## - [param user_profile] ([ModUserProfile]): (Optional) The user profile for which the mod will be enabled. Defaults to the current user profile.[br]
  25. ## [br]
  26. ## [b]Returns:[/b][br]
  27. ## - [bool]: True on success.
  28. static func force_enable_mod(mod_id: String, user_profile:= ModLoaderStore.current_user_profile) -> bool:
  29. return _set_mod_state(mod_id, user_profile.name, true, true)
  30. ## Disables a mod - it will not be loaded on the next game start[br]
  31. ## [br]
  32. ## [b]Parameters:[/b][br]
  33. ## - [param mod_id] ([String]): The ID of the mod to disable.[br]
  34. ## - [param user_profile] ([ModUserProfile]): (Optional) The user profile to disable the mod for. Default is the current user profile.[br]
  35. ## [br]
  36. ## [b]Returns:[/b][br]
  37. ## - [bool]: True on success.
  38. static func disable_mod(mod_id: String, user_profile := ModLoaderStore.current_user_profile) -> bool:
  39. return _set_mod_state(mod_id, user_profile.name, false)
  40. ## Sets the current config for a mod in a user profile's mod_list.[br]
  41. ## [br]
  42. ## [b]Parameters:[/b][br]
  43. ## - [param mod_id] ([String]): The ID of the mod.[br]
  44. ## - [param mod_config] ([ModConfig]): The mod config to set as the current config.[br]
  45. ## - [param user_profile] ([ModUserProfile]): (Optional) The user profile to update. Default is the current user profile.[br]
  46. ## [br]
  47. ## [b]Returns:[/b][br]
  48. ## - [bool]: True on success.
  49. static func set_mod_current_config(mod_id: String, mod_config: ModConfig, user_profile := ModLoaderStore.current_user_profile) -> bool:
  50. # Verify whether the mod_id is present in the profile's mod_list.
  51. if not _is_mod_id_in_mod_list(mod_id, user_profile.name):
  52. return false
  53. # Update the current config in the mod_list of the user profile
  54. user_profile.mod_list[mod_id].current_config = mod_config.name
  55. # Store the new profile in the json file
  56. var is_save_success := _save()
  57. if is_save_success:
  58. ModLoaderLog.debug("Set the \"current_config\" of \"%s\" to \"%s\" in user profile \"%s\" " % [mod_id, mod_config.name, user_profile.name], LOG_NAME)
  59. return is_save_success
  60. ## Creates a new user profile with the given name, using the currently loaded mods as the mod list.[br]
  61. ## [br]
  62. ## [b]Parameters:[/b][br]
  63. ## - [param profile_name] ([String]): The name of the new user profile (must be unique).[br]
  64. ## [br]
  65. ## [b]Returns:[/b][br]
  66. ## - [bool]: True on success.
  67. static func create_profile(profile_name: String) -> bool:
  68. # Verify that the profile name is not already in use
  69. if ModLoaderStore.user_profiles.has(profile_name):
  70. ModLoaderLog.error("User profile with the name of \"%s\" already exists." % profile_name, LOG_NAME)
  71. return false
  72. var mod_list := _generate_mod_list()
  73. var new_profile := _create_new_profile(profile_name, mod_list)
  74. # If there was an error creating the new user profile return
  75. if not new_profile:
  76. return false
  77. # Store the new profile in the ModLoaderStore
  78. ModLoaderStore.user_profiles[profile_name] = new_profile
  79. # Set it as the current profile
  80. ModLoaderStore.current_user_profile = new_profile
  81. # Store the new profile in the json file
  82. var is_save_success := _save()
  83. if is_save_success:
  84. ModLoaderLog.debug("Created new user profile \"%s\"" % profile_name, LOG_NAME)
  85. return is_save_success
  86. ## Sets the current user profile to the given user profile.[br]
  87. ## [br]
  88. ## [b]Parameters:[/b][br]
  89. ## - [param user_profile] ([ModUserProfile]): The user profile to set as the current profile.[br]
  90. ## [br]
  91. ## [b]Returns:[/b][br]
  92. ## - [bool]: True on success.
  93. static func set_profile(user_profile: ModUserProfile) -> bool:
  94. # Check if the profile name is unique
  95. if not ModLoaderStore.user_profiles.has(user_profile.name):
  96. ModLoaderLog.error("User profile with name \"%s\" not found." % user_profile.name, LOG_NAME)
  97. return false
  98. # Update the current_user_profile in the ModLoaderStore
  99. ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[user_profile.name]
  100. # Save changes in the json file
  101. var is_save_success := _save()
  102. if is_save_success:
  103. ModLoaderLog.debug("Current user profile set to \"%s\"" % user_profile.name, LOG_NAME)
  104. return is_save_success
  105. ## Deletes the given user profile.[br]
  106. ## [br]
  107. ## [b]Parameters:[/b][br]
  108. ## - [param user_profile] ([ModUserProfile]): The user profile to delete.[br]
  109. ## [br]
  110. ## [b]Returns:[/b][br]
  111. ## - [bool]: True on success.
  112. static func delete_profile(user_profile: ModUserProfile) -> bool:
  113. # If the current_profile is about to get deleted log an error
  114. if ModLoaderStore.current_user_profile.name == user_profile.name:
  115. ModLoaderLog.error(str(
  116. "You cannot delete the currently selected user profile \"%s\" " +
  117. "because it is currently in use. Please switch to a different profile before deleting this one.") % user_profile.name,
  118. LOG_NAME)
  119. return false
  120. # Deleting the default profile is not allowed
  121. if user_profile.name == "default":
  122. ModLoaderLog.error("You can't delete the default profile", LOG_NAME)
  123. return false
  124. # Delete the user profile
  125. if not ModLoaderStore.user_profiles.erase(user_profile.name):
  126. # Erase returns false if the the key is not present in user_profiles
  127. ModLoaderLog.error("User profile with name \"%s\" not found." % user_profile.name, LOG_NAME)
  128. return false
  129. # Save profiles to the user profiles JSON file
  130. var is_save_success := _save()
  131. if is_save_success:
  132. ModLoaderLog.debug("Deleted user profile \"%s\"" % user_profile.name, LOG_NAME)
  133. return is_save_success
  134. ## Returns the current user profile.[br]
  135. ## [br]
  136. ## [b]Returns:[/b][br]
  137. ## - [ModUserProfile]: The current profile or [code]null[/code] if not set.
  138. static func get_current() -> ModUserProfile:
  139. return ModLoaderStore.current_user_profile
  140. ## Returns the user profile with the given name.[br]
  141. ## [br]
  142. ## [b]Parameters:[/b][br]
  143. ## - [param profile_name] ([String]): The name of the user profile to retrieve.[br]
  144. ## [br]
  145. ## [b]Returns:[/b][br]
  146. ## - [ModUserProfile]: The profile or [code]null[/code] if not found
  147. static func get_profile(profile_name: String) -> ModUserProfile:
  148. if not ModLoaderStore.user_profiles.has(profile_name):
  149. ModLoaderLog.error("User profile with name \"%s\" not found." % profile_name, LOG_NAME)
  150. return null
  151. return ModLoaderStore.user_profiles[profile_name]
  152. ## Returns an array containing all user profiles stored in ModLoaderStore.[br]
  153. ## [br]
  154. ## [b]Returns:[/b][br]
  155. ## - [Array]: A list of [ModUserProfile] Objects
  156. static func get_all_as_array() -> Array:
  157. var user_profiles := []
  158. for user_profile_name in ModLoaderStore.user_profiles.keys():
  159. user_profiles.push_back(ModLoaderStore.user_profiles[user_profile_name])
  160. return user_profiles
  161. ## Returns true if the Mod User Profiles are initialized.
  162. ## [br]
  163. ## [b]Returns:[/b][br]
  164. ## - [bool]: True if profiles are ready.
  165. ## [br]
  166. ## On the first execution of the game, user profiles might not yet be created.
  167. ## Use this method to check if everything is ready to interact with the ModLoaderUserProfile API.
  168. static func is_initialized() -> bool:
  169. return _ModLoaderFile.file_exists(FILE_PATH_USER_PROFILES)
  170. # Internal profile functions
  171. # =============================================================================
  172. # Update the global list of disabled mods based on the current user profile
  173. # The user profile will override the disabled_mods property that can be set via the options resource in the editor.
  174. # Example: If "Mod-TestMod" is set in disabled_mods via the editor, the mod will appear disabled in the user profile.
  175. # If the user then enables the mod in the profile the entry in disabled_mods will be removed.
  176. static func _update_disabled_mods() -> void:
  177. var current_user_profile: ModUserProfile = get_current()
  178. # Check if a current user profile is set
  179. if not current_user_profile:
  180. ModLoaderLog.info("There is no current user profile. The \"default\" profile will be created.", LOG_NAME)
  181. return
  182. # Iterate through the mod list in the current user profile to find disabled mods
  183. for mod_id in current_user_profile.mod_list:
  184. var mod_list_entry: Dictionary = current_user_profile.mod_list[mod_id]
  185. if ModLoaderStore.mod_data.has(mod_id):
  186. ModLoaderStore.mod_data[mod_id].set_mod_state(mod_list_entry.is_active, true)
  187. ModLoaderLog.debug(
  188. "Updated the active state of all mods, based on the current user profile \"%s\""
  189. % current_user_profile.name,
  190. LOG_NAME)
  191. # This function updates the mod lists of all user profiles with newly loaded mods that are not already present.
  192. # It does so by comparing the current set of loaded mods with the mod list of each user profile, and adding any missing mods.
  193. # Additionally, it checks for and deletes any mods from each profile's mod list that are no longer installed on the system.
  194. static func _update_mod_lists() -> bool:
  195. # Generate a list of currently present mods by combining the mods
  196. # in mod_data and ml_options.disabled_mods from ModLoaderStore.
  197. var current_mod_list := _generate_mod_list()
  198. # Iterate over all user profiles
  199. for profile_name in ModLoaderStore.user_profiles.keys():
  200. var profile: ModUserProfile = ModLoaderStore.user_profiles[profile_name]
  201. # Merge the profiles mod_list with the previously created current_mod_list
  202. profile.mod_list.merge(current_mod_list)
  203. var update_mod_list := _update_mod_list(profile.mod_list)
  204. profile.mod_list = update_mod_list
  205. # Save the updated user profiles to the JSON file
  206. var is_save_success := _save()
  207. if is_save_success:
  208. ModLoaderLog.debug("Updated the mod lists of all user profiles", LOG_NAME)
  209. return is_save_success
  210. # This function takes a mod_list dictionary and optional mod_data dictionary as input and returns
  211. # an updated mod_list dictionary. It iterates over each mod ID in the mod list, checks if the mod
  212. # is still installed and if the current_config is present. If the mod is not installed or the current
  213. # config is missing, the mod is removed or its current_config is reset to the default configuration.
  214. static func _update_mod_list(mod_list: Dictionary, mod_data := ModLoaderStore.mod_data) -> Dictionary:
  215. var updated_mod_list := mod_list.duplicate(true)
  216. # Iterate over each mod ID in the mod list
  217. for mod_id in updated_mod_list.keys():
  218. var mod_list_entry: Dictionary = updated_mod_list[mod_id]
  219. # Check if the current config doesn't exist
  220. # This can happen if the config file was manually deleted
  221. if mod_list_entry.has("current_config") and _ModLoaderPath.get_path_to_mod_config_file(mod_id, mod_list_entry.current_config).is_empty():
  222. # If the current config doesn't exist, reset it to the default configuration
  223. mod_list_entry.current_config = ModLoaderConfig.DEFAULT_CONFIG_NAME
  224. if (
  225. # If the mod is not loaded
  226. not mod_data.has(mod_id) and
  227. # Check if the entry has a zip_path key
  228. mod_list_entry.has("zip_path") and
  229. # Check if the entry has a zip_path
  230. not mod_list_entry.zip_path.is_empty() and
  231. # Check if the zip file for the mod doesn't exist
  232. not _ModLoaderFile.file_exists(mod_list_entry.zip_path)
  233. ):
  234. # If the mod directory doesn't exist,
  235. # the mod is no longer installed and can be removed from the mod list
  236. ModLoaderLog.debug(
  237. "Mod \"%s\" has been deleted from all user profiles as the corresponding zip file no longer exists at path \"%s\"."
  238. % [mod_id, mod_list_entry.zip_path],
  239. LOG_NAME,
  240. true
  241. )
  242. updated_mod_list.erase(mod_id)
  243. continue
  244. updated_mod_list[mod_id] = mod_list_entry
  245. return updated_mod_list
  246. # Generates a dictionary with data to be stored for each mod.
  247. static func _generate_mod_list() -> Dictionary:
  248. var mod_list := {}
  249. # Create a mod_list with the currently loaded mods
  250. for mod_id in ModLoaderStore.mod_data.keys():
  251. mod_list[mod_id] = _generate_mod_list_entry(mod_id, true)
  252. # Add the deactivated mods to the list
  253. for mod_id in ModLoaderStore.ml_options.disabled_mods:
  254. mod_list[mod_id] = _generate_mod_list_entry(mod_id, false)
  255. return mod_list
  256. # Generates a mod list entry dictionary with the given mod ID and active status.
  257. # If the mod has a config schema, sets the 'current_config' key to the current_config stored in the Mods ModData.
  258. static func _generate_mod_list_entry(mod_id: String, is_active: bool) -> Dictionary:
  259. var mod_list_entry := {}
  260. # Set the mods active state
  261. mod_list_entry.is_active = is_active
  262. # Set the mods zip path if available
  263. if ModLoaderStore.mod_data.has(mod_id):
  264. mod_list_entry.zip_path = ModLoaderStore.mod_data[mod_id].zip_path
  265. # Set the current_config if the mod has a config schema and is active
  266. if is_active and not ModLoaderConfig.get_config_schema(mod_id).is_empty():
  267. var current_config: ModConfig = ModLoaderStore.mod_data[mod_id].current_config
  268. if current_config and current_config.is_valid:
  269. # Set to the current_config name if valid
  270. mod_list_entry.current_config = current_config.name
  271. else:
  272. # If not valid revert to the default config
  273. mod_list_entry.current_config = ModLoaderConfig.DEFAULT_CONFIG_NAME
  274. return mod_list_entry
  275. # Handles the activation or deactivation of a mod in a user profile.
  276. static func _set_mod_state(mod_id: String, profile_name: String, should_activate: bool, force := false) -> bool:
  277. # Verify whether the mod_id is present in the profile's mod_list.
  278. if not _is_mod_id_in_mod_list(mod_id, profile_name):
  279. return false
  280. # Handle mod state
  281. # Set state in the ModData
  282. var was_toggled: bool = ModLoaderStore.mod_data[mod_id].set_mod_state(should_activate, force)
  283. if not was_toggled:
  284. return false
  285. # Set state for user profile
  286. ModLoaderStore.user_profiles[profile_name].mod_list[mod_id].is_active = should_activate
  287. # Save profiles to the user profiles JSON file
  288. var is_save_success := _save()
  289. if is_save_success:
  290. ModLoaderLog.debug("Mod activation state changed: mod_id=%s should_activate=%s profile_name=%s" % [mod_id, should_activate, profile_name], LOG_NAME)
  291. return is_save_success
  292. # Checks whether a given mod_id is present in the mod_list of the specified user profile.
  293. # Returns True if the mod_id is present, False otherwise.
  294. static func _is_mod_id_in_mod_list(mod_id: String, profile_name: String) -> bool:
  295. # Get the user profile
  296. var user_profile := get_profile(profile_name)
  297. if not user_profile:
  298. # Return false if there is an error getting the user profile
  299. return false
  300. # Return false if the mod_id is not in the profile's mod_list
  301. if not user_profile.mod_list.has(mod_id):
  302. ModLoaderLog.error("Mod id \"%s\" not found in the \"mod_list\" of user profile \"%s\"." % [mod_id, profile_name], LOG_NAME)
  303. return false
  304. # Return true if the mod_id is in the profile's mod_list
  305. return true
  306. # Creates a new Profile with the given name and mod list.
  307. # Returns the newly created Profile object.
  308. static func _create_new_profile(profile_name: String, mod_list: Dictionary) -> ModUserProfile:
  309. var new_profile := ModUserProfile.new()
  310. # If no name is provided, log an error and return null
  311. if profile_name == "":
  312. ModLoaderLog.error("Please provide a name for the new profile", LOG_NAME)
  313. return null
  314. # Set the profile name
  315. new_profile.name = profile_name
  316. # If no mods are specified in the mod_list, log a warning and return the new profile
  317. if mod_list.keys().size() == 0:
  318. ModLoaderLog.info("No mod_ids inside \"mod_list\" for user profile \"%s\" " % profile_name, LOG_NAME)
  319. return new_profile
  320. # Set the mod_list
  321. new_profile.mod_list = _update_mod_list(mod_list)
  322. return new_profile
  323. # Loads user profiles from the JSON file and adds them to ModLoaderStore.
  324. static func _load() -> bool:
  325. # Load JSON data from the user profiles file
  326. var data := _ModLoaderFile.get_json_as_dict(FILE_PATH_USER_PROFILES)
  327. # If there is no data, log an error and return
  328. if data.is_empty():
  329. ModLoaderLog.error("No profile file found at \"%s\"" % FILE_PATH_USER_PROFILES, LOG_NAME)
  330. return false
  331. # Loop through each profile in the data and add them to ModLoaderStore
  332. for profile_name in data.profiles.keys():
  333. # Get the profile data from the JSON object
  334. var profile_data: Dictionary = data.profiles[profile_name]
  335. # Create a new profile object and add it to ModLoaderStore.user_profiles
  336. var new_profile := _create_new_profile(profile_name, profile_data.mod_list)
  337. ModLoaderStore.user_profiles[profile_name] = new_profile
  338. # Set the current user profile to the one specified in the data
  339. ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[data.current_profile]
  340. return true
  341. # Saves the user profiles in the ModLoaderStore to the user profiles JSON file.
  342. static func _save() -> bool:
  343. # Initialize a dictionary to hold the serialized user profiles data
  344. var save_dict := {
  345. "current_profile": "",
  346. "profiles": {}
  347. }
  348. # Set the current profile name in the save_dict
  349. save_dict.current_profile = ModLoaderStore.current_user_profile.name
  350. # Serialize the mod_list data for each user profile and add it to the save_dict
  351. for profile_name in ModLoaderStore.user_profiles.keys():
  352. var profile: ModUserProfile = ModLoaderStore.user_profiles[profile_name]
  353. # Init the profile dict
  354. save_dict.profiles[profile.name] = {}
  355. # Init the mod_list dict
  356. save_dict.profiles[profile.name].mod_list = profile.mod_list
  357. # Save the serialized user profiles data to the user profiles JSON file
  358. return _ModLoaderFile.save_dictionary_to_json_file(save_dict, FILE_PATH_USER_PROFILES)