mod_data.gd 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. class_name ModData
  2. extends Resource
  3. ##
  4. ## Stores and validates all Data required to load a mod successfully
  5. ## If some of the data is invalid, [member is_loadable] will be false
  6. const LOG_NAME := "ModLoader:ModData"
  7. const MOD_MAIN := "mod_main.gd"
  8. const MANIFEST := "manifest.json"
  9. const OVERWRITES := "overwrites.gd"
  10. # These 2 files are always required by mods.
  11. # [i]mod_main.gd[/i] = The main init file for the mod
  12. # [i]manifest.json[/i] = Meta data for the mod, including its dependencies
  13. enum RequiredModFiles {
  14. MOD_MAIN,
  15. MANIFEST,
  16. }
  17. enum OptionalModFiles {
  18. OVERWRITES
  19. }
  20. # Specifies the source from which the mod has been loaded:
  21. # UNPACKED = From the mods-unpacked directory ( only when in the editor ).
  22. # LOCAL = From the local mod zip directory, which by default is ../game_dir/mods.
  23. # STEAM_WORKSHOP = Loaded from ../Steam/steamapps/workshop/content/1234567/[..].
  24. enum Sources {
  25. UNPACKED,
  26. LOCAL,
  27. STEAM_WORKSHOP,
  28. }
  29. ## Name of the Mod's zip file
  30. var zip_name := ""
  31. ## Path to the Mod's zip file
  32. var zip_path := ""
  33. ## Directory of the mod. Has to be identical to [method ModManifest.get_mod_id]
  34. var dir_name := ""
  35. ## Path to the mod's unpacked directory
  36. var dir_path := ""
  37. ## False if any data is invalid
  38. var is_loadable := true
  39. ## True if overwrites.gd exists
  40. var is_overwrite := false
  41. ## True if mod can't be disabled or enabled in a user profile
  42. var is_locked := false
  43. ## Flag indicating whether the mod should be loaded
  44. var is_active := true
  45. ## Is increased for every mod depending on this mod. Highest importance is loaded first
  46. var importance := 0
  47. ## Contents of the manifest
  48. var manifest: ModManifest
  49. # Updated in load_configs
  50. ## All mod configs
  51. var configs := {}
  52. ## The currently applied mod config
  53. var current_config: ModConfig: set = _set_current_config
  54. ## Specifies the source from which the mod has been loaded
  55. var source: int
  56. var load_errors: Array[String] = []
  57. var load_warnings: Array[String] = []
  58. func _init(_manifest: ModManifest, path: String) -> void:
  59. manifest = _manifest
  60. if _ModLoaderPath.is_zip(path):
  61. zip_name = _ModLoaderPath.get_file_name_from_path(path)
  62. zip_path = path
  63. # Use the dir name of the passed path instead of the manifest data so we can validate
  64. # the mod dir has the same name as the mod id in the manifest
  65. dir_name = _ModLoaderFile.get_mod_dir_name_in_zip(zip_path)
  66. else:
  67. dir_name = path.split("/")[-1]
  68. dir_path = _ModLoaderPath.get_unpacked_mods_dir_path().path_join(dir_name)
  69. source = get_mod_source()
  70. _has_required_files()
  71. # We want to avoid checking if mod_dir_name == mod_id when manifest parsing has failed
  72. # to prevent confusing error messages.
  73. if not manifest.has_parsing_failed:
  74. _is_mod_dir_name_same_as_id(manifest)
  75. is_overwrite = _is_overwrite()
  76. is_locked = manifest.get_mod_id() in ModLoaderStore.ml_options.locked_mods
  77. if not load_errors.is_empty() or not manifest.validation_messages_error.is_empty():
  78. is_loadable = false
  79. # Load each mod config json from the mods config directory.
  80. func load_configs() -> void:
  81. # If the default values in the config schema are invalid don't load configs
  82. if not manifest.load_mod_config_defaults():
  83. return
  84. var config_dir_path := _ModLoaderPath.get_path_to_mod_configs_dir(dir_name)
  85. var config_file_paths := _ModLoaderPath.get_file_paths_in_dir(config_dir_path)
  86. for config_file_path in config_file_paths:
  87. _load_config(config_file_path)
  88. # Set the current_config based on the user profile
  89. if ModLoaderUserProfile.is_initialized() and ModLoaderConfig.has_current_config(dir_name):
  90. current_config = ModLoaderConfig.get_current_config(dir_name)
  91. else:
  92. current_config = ModLoaderConfig.get_config(dir_name, ModLoaderConfig.DEFAULT_CONFIG_NAME)
  93. # Create a new ModConfig instance for each Config JSON and add it to the configs dictionary.
  94. func _load_config(config_file_path: String) -> void:
  95. var config_data := _ModLoaderFile.get_json_as_dict(config_file_path)
  96. var mod_config = ModConfig.new(
  97. dir_name,
  98. config_data,
  99. config_file_path,
  100. manifest.config_schema
  101. )
  102. # Add the config to the configs dictionary
  103. configs[mod_config.name] = mod_config
  104. # Update the mod_list of the current user profile
  105. func _set_current_config(new_current_config: ModConfig) -> void:
  106. ModLoaderUserProfile.set_mod_current_config(dir_name, new_current_config)
  107. current_config = new_current_config
  108. # We can't emit the signal if the ModLoader is not initialized yet
  109. if ModLoader:
  110. ModLoader.current_config_changed.emit(new_current_config)
  111. func set_mod_state(should_activate: bool, force := false) -> bool:
  112. if is_locked and should_activate != is_active:
  113. ModLoaderLog.error(
  114. "Unable to toggle mod \"%s\" since it is marked as locked. Locked mods: %s"
  115. % [manifest.get_mod_id(), ModLoaderStore.ml_options.locked_mods], LOG_NAME)
  116. return false
  117. if should_activate and not is_loadable:
  118. ModLoaderLog.error(
  119. "Unable to activate mod \"%s\" since it has the following load errors: %s"
  120. % [manifest.get_mod_id(), ", ".join(load_errors)], LOG_NAME)
  121. return false
  122. if should_activate and manifest.validation_messages_warning.size() > 0:
  123. if not force:
  124. ModLoaderLog.warning(
  125. "Rejecting to activate mod \"%s\" since it has the following load warnings: %s"
  126. % [manifest.get_mod_id(), ", ".join(load_warnings)], LOG_NAME)
  127. return false
  128. ModLoaderLog.info(
  129. "Forced to activate mod \"%s\" despite the following load warnings: %s"
  130. % [manifest.get_mod_id(), ", ".join(load_warnings)], LOG_NAME)
  131. is_active = should_activate
  132. return true
  133. # Validates if [member dir_name] matches [method ModManifest.get_mod_id]
  134. func _is_mod_dir_name_same_as_id(mod_manifest: ModManifest) -> bool:
  135. var manifest_id := mod_manifest.get_mod_id()
  136. if not dir_name == manifest_id:
  137. load_errors.push_back('Mod directory name "%s" does not match the data in manifest.json. Expected "%s" (Format: {namespace}-{name})' % [ dir_name, manifest_id ])
  138. return false
  139. return true
  140. func _is_overwrite() -> bool:
  141. return _ModLoaderFile.file_exists(get_optional_mod_file_path(OptionalModFiles.OVERWRITES), zip_path)
  142. # Confirms that all files from [member required_mod_files] exist
  143. func _has_required_files() -> bool:
  144. var has_required_files := true
  145. for required_file in RequiredModFiles:
  146. var required_file_path := get_required_mod_file_path(RequiredModFiles[required_file])
  147. if not _ModLoaderFile.file_exists(required_file_path, zip_path):
  148. load_errors.push_back(
  149. "ERROR - %s is missing a required file: %s. For more information, please visit \"%s\"." %
  150. [dir_name, required_file_path, ModLoaderStore.URL_MOD_STRUCTURE_DOCS]
  151. )
  152. has_required_files = false
  153. return has_required_files
  154. # Converts enum indices [member RequiredModFiles] into their respective file paths
  155. # All required mod files should be in the root of the mod directory
  156. func get_required_mod_file_path(required_file: RequiredModFiles) -> String:
  157. match required_file:
  158. RequiredModFiles.MOD_MAIN:
  159. return dir_path.path_join(MOD_MAIN)
  160. RequiredModFiles.MANIFEST:
  161. return dir_path.path_join(MANIFEST)
  162. return ""
  163. func get_optional_mod_file_path(optional_file: OptionalModFiles) -> String:
  164. match optional_file:
  165. OptionalModFiles.OVERWRITES:
  166. return dir_path.path_join(OVERWRITES)
  167. return ""
  168. func get_mod_source() -> Sources:
  169. if zip_path.contains("workshop"):
  170. return Sources.STEAM_WORKSHOP
  171. if zip_path == "":
  172. return Sources.UNPACKED
  173. return Sources.LOCAL