mod_loader.gd 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. ## ModLoader - A mod loader for GDScript
  2. #
  3. # Written in 2021 by harrygiel <harrygiel@gmail.com>,
  4. # in 2021 by Mariusz Chwalba <mariusz@chwalba.net>,
  5. # in 2022 by Vladimir Panteleev <git@cy.md>,
  6. # in 2023 by KANA <kai@kana.jetzt>,
  7. # in 2023 by Darkly77,
  8. # in 2023 by otDan <otdanofficial@gmail.com>,
  9. # in 2023 by Qubus0/Ste
  10. #
  11. # To the extent possible under law, the author(s) have
  12. # dedicated all copyright and related and neighboring
  13. # rights to this software to the public domain worldwide.
  14. # This software is distributed without any warranty.
  15. #
  16. # You should have received a copy of the CC0 Public
  17. # Domain Dedication along with this software. If not, see
  18. # <http://creativecommons.org/publicdomain/zero/1.0/>.
  19. extends Node
  20. ## Emitted if something is logged with [ModLoaderLog]
  21. signal logged(entry: ModLoaderLog.ModLoaderLogEntry)
  22. ## Emitted if the [member ModData.current_config] of any mod changed.
  23. ## Use the [member ModConfig.mod_id] of the [ModConfig] to check if the config of your mod has changed.
  24. signal current_config_changed(config: ModConfig)
  25. ## Emitted when new mod hooks are created. A game restart is required to load them.
  26. signal new_hooks_created
  27. const LOG_NAME := "ModLoader"
  28. func _init() -> void:
  29. # if mods are not enabled - don't load mods
  30. if ModLoaderStore.REQUIRE_CMD_LINE and not _ModLoaderCLI.is_running_with_command_line_arg("--enable-mods"):
  31. return
  32. # Only load the hook pack if not in the editor
  33. # We can't use it in the editor - see https://github.com/godotengine/godot/issues/19815
  34. # Mod devs can use the Dev Tool to generate hooks in the editor.
  35. if not ModLoaderStore.has_feature.editor and _ModLoaderFile.file_exists(_ModLoaderPath.get_path_to_hook_pack()):
  36. _load_mod_hooks_pack()
  37. # Rotate the log files once on startup.
  38. ModLoaderLog._rotate_log_file()
  39. if not ModLoaderStore.ml_options.enable_mods:
  40. ModLoaderLog.info("Mods are currently disabled", LOG_NAME)
  41. return
  42. # Ensure the ModLoaderStore and ModLoader autoloads are in the correct position.
  43. _ModLoaderGodot.check_autoload_positions()
  44. # Log the autoloads order.
  45. ModLoaderLog.debug_json_print("Autoload order", _ModLoaderGodot.get_autoload_array(), LOG_NAME)
  46. # Log game install dir
  47. ModLoaderLog.info("game_install_directory: %s" % _ModLoaderPath.get_local_folder_dir(), LOG_NAME)
  48. # Load user profiles into ModLoaderStore
  49. if ModLoaderUserProfile.is_initialized():
  50. var _success_user_profile_load := ModLoaderUserProfile._load()
  51. # Create the default user profile if it does not already exist.
  52. # This should only occur on the first run or if the JSON file was manually edited.
  53. if not ModLoaderStore.user_profiles.has("default"):
  54. var _success_user_profile_create := ModLoaderUserProfile.create_profile("default")
  55. # --- Start loading mods ---
  56. var loaded_count := 0
  57. # mod_path can be a directory in mods-unpacked or a mod.zip
  58. var mod_paths := _ModLoaderPath.get_mod_paths_from_all_sources()
  59. ModLoaderLog.debug("Found %s mods at the following paths:\n\t - %s" % [mod_paths.size(), "\n\t - ".join(mod_paths)], LOG_NAME)
  60. for mod_path in mod_paths:
  61. var is_zip := _ModLoaderPath.is_zip(mod_path)
  62. # Load manifest file
  63. var manifest_data: Dictionary = _ModLoaderFile.load_manifest_file(mod_path)
  64. var manifest := ModManifest.new(manifest_data, mod_path)
  65. if not manifest.validation_messages_error.is_empty():
  66. ModLoaderLog.error(
  67. "The mod from path \"%s\" cannot be loaded. Manifest validation failed with the following errors:\n\t - %s" %
  68. [mod_path, "\n\t - ".join(manifest.validation_messages_error)], LOG_NAME
  69. )
  70. # Init ModData
  71. var mod := ModData.new(manifest, mod_path)
  72. if not mod.load_errors.is_empty():
  73. ModLoaderStore.ml_options.disabled_mods.append(mod.manifest.get_mod_id())
  74. ModLoaderLog.error(
  75. "The mod from path \"%s\" cannot be loaded. ModData initialization has failed with the following errors:\n\t - %s" %
  76. [mod_path, "\n\t - ".join(mod.load_errors)], LOG_NAME
  77. )
  78. # Using mod.dir_name here allows us to store the ModData even if manifest validation fails.
  79. ModLoaderStore.mod_data[mod.dir_name] = mod
  80. if mod.is_loadable:
  81. if is_zip:
  82. var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_path, false)
  83. if not is_mod_loaded_successfully:
  84. ModLoaderLog.error("Failed to load mod zip from path \"%s\" into the virtual filesystem." % mod_path, LOG_NAME)
  85. continue
  86. # Notifies developer of an issue with Godot, where using `load_resource_pack`
  87. # in the editor WIPES the entire virtual res:// directory the first time you
  88. # use it. This means that unpacked mods are no longer accessible, because they
  89. # no longer exist in the file system. So this warning basically says
  90. # "don't use ZIPs with unpacked mods!"
  91. # https://github.com/godotengine/godot/issues/19815
  92. # https://github.com/godotengine/godot/issues/16798
  93. if ModLoaderStore.has_feature.editor:
  94. ModLoaderLog.hint(
  95. "Loading any resource packs (.zip/.pck) with `load_resource_pack` will WIPE the entire virtual res:// directory. " +
  96. "If you have any unpacked mods in %s, they will not be loaded.Please unpack your mod ZIPs instead, and add them to %s" %
  97. [_ModLoaderPath.get_unpacked_mods_dir_path(), _ModLoaderPath.get_unpacked_mods_dir_path()], LOG_NAME, true
  98. )
  99. ModLoaderLog.success("%s loaded." % mod_path, LOG_NAME)
  100. loaded_count += 1
  101. ModLoaderLog.success("DONE: Loaded %s mod files into the virtual filesystem" % loaded_count, LOG_NAME)
  102. # Update the mod_list for each user profile
  103. var _success_update_mod_lists := ModLoaderUserProfile._update_mod_lists()
  104. # Update active state of mods based on the current user profile
  105. ModLoaderUserProfile._update_disabled_mods()
  106. # Load all Mod Configs
  107. for dir_name in ModLoaderStore.mod_data:
  108. var mod: ModData = ModLoaderStore.mod_data[dir_name]
  109. if not mod.is_loadable:
  110. continue
  111. if mod.manifest.get("config_schema") and not mod.manifest.config_schema.is_empty():
  112. mod.load_configs()
  113. ModLoaderLog.success("DONE: Loaded all mod configs", LOG_NAME)
  114. # Check for mods with load_before. If a mod is listed in load_before,
  115. # add the current mod to the dependencies of the the mod specified
  116. # in load_before.
  117. for dir_name in ModLoaderStore.mod_data:
  118. var mod: ModData = ModLoaderStore.mod_data[dir_name]
  119. if not mod.is_loadable:
  120. continue
  121. _ModLoaderDependency.check_load_before(mod)
  122. # Run optional dependency checks.
  123. # If a mod depends on another mod that hasn't been loaded,
  124. # the dependent mod will be loaded regardless.
  125. for dir_name in ModLoaderStore.mod_data:
  126. var mod: ModData = ModLoaderStore.mod_data[dir_name]
  127. if not mod.is_loadable:
  128. continue
  129. var _is_circular := _ModLoaderDependency.check_dependencies(mod, false)
  130. # Run dependency checks. If a mod depends on another
  131. # mod that hasn't been loaded, the dependent mod won't be loaded.
  132. for dir_name in ModLoaderStore.mod_data:
  133. var mod: ModData = ModLoaderStore.mod_data[dir_name]
  134. if not mod.is_loadable:
  135. continue
  136. var _is_circular := _ModLoaderDependency.check_dependencies(mod)
  137. # Sort mod_load_order by the importance score of the mod
  138. ModLoaderStore.mod_load_order = _ModLoaderDependency.get_load_order(ModLoaderStore.mod_data.values())
  139. # Log mod order
  140. for mod_index in ModLoaderStore.mod_load_order.size():
  141. var mod: ModData = ModLoaderStore.mod_load_order[mod_index]
  142. ModLoaderLog.info("mod_load_order -> %s) %s" % [mod_index + 1, mod.dir_name], LOG_NAME)
  143. # Instance every mod and add it as a node to the Mod Loader
  144. for mod in ModLoaderStore.mod_load_order:
  145. mod = mod as ModData
  146. # Continue if mod is disabled
  147. if not mod.is_active or not mod.is_loadable:
  148. continue
  149. ModLoaderLog.info("Initializing -> %s" % mod.manifest.get_mod_id(), LOG_NAME)
  150. _init_mod(mod)
  151. ModLoaderLog.debug_json_print("mod data", ModLoaderStore.mod_data, LOG_NAME)
  152. ModLoaderLog.success("DONE: Completely finished loading mods", LOG_NAME)
  153. _ModLoaderScriptExtension.handle_script_extensions()
  154. ModLoaderLog.success("DONE: Installed all script extensions", LOG_NAME)
  155. _ModLoaderSceneExtension.refresh_scenes()
  156. _ModLoaderSceneExtension.handle_scene_extensions()
  157. ModLoaderLog.success("DONE: Applied all scene extensions", LOG_NAME)
  158. ModLoaderStore.is_initializing = false
  159. new_hooks_created.connect(_ModLoaderHooks.on_new_hooks_created)
  160. func _ready():
  161. # Hooks must be generated after all autoloads are available.
  162. # Variables initialized with an autoload property cause errors otherwise.
  163. if _ModLoaderHooks.any_mod_hooked:
  164. if OS.has_feature("editor"):
  165. ModLoaderLog.hint("No mod hooks .zip will be created when running from the editor.", LOG_NAME)
  166. ModLoaderLog.hint("You can test mod hooks by running the preprocessor on the vanilla scripts once.", LOG_NAME)
  167. ModLoaderLog.hint("We recommend using the Mod Loader Dev Tool to process scripts in the editor. You can find it here: %s" % ModLoaderStore.MOD_LOADER_DEV_TOOL_URL, LOG_NAME)
  168. else:
  169. # Generate mod hooks
  170. _ModLoaderModHookPacker.start()
  171. func _load_mod_hooks_pack() -> void:
  172. # Load mod hooks
  173. var load_hooks_pack_success := ProjectSettings.load_resource_pack(_ModLoaderPath.get_path_to_hook_pack())
  174. if not load_hooks_pack_success:
  175. ModLoaderLog.error("Failed loading hooks pack from: %s" % _ModLoaderPath.get_path_to_hook_pack(), LOG_NAME)
  176. else:
  177. ModLoaderLog.debug("Successfully loaded hooks pack from: %s" % _ModLoaderPath.get_path_to_hook_pack(), LOG_NAME)
  178. # Instantiate every mod and add it as a node to the Mod Loader.
  179. func _init_mod(mod: ModData) -> void:
  180. var mod_main_path := mod.get_required_mod_file_path(ModData.RequiredModFiles.MOD_MAIN)
  181. var mod_overwrites_path := mod.get_optional_mod_file_path(ModData.OptionalModFiles.OVERWRITES)
  182. # If the mod contains overwrites initialize the overwrites script
  183. if mod.is_overwrite:
  184. ModLoaderLog.debug("Overwrite script detected -> %s" % mod_overwrites_path, LOG_NAME)
  185. var mod_overwrites_script := load(mod_overwrites_path)
  186. mod_overwrites_script.new()
  187. ModLoaderLog.debug("Initialized overwrite script -> %s" % mod_overwrites_path, LOG_NAME)
  188. ModLoaderLog.debug("Loading script from -> %s" % mod_main_path, LOG_NAME)
  189. var mod_main_script: GDScript = ResourceLoader.load(mod_main_path)
  190. ModLoaderLog.debug("Loaded script -> %s" % mod_main_script, LOG_NAME)
  191. var mod_main_instance: Node = mod_main_script.new()
  192. mod_main_instance.name = mod.manifest.get_mod_id()
  193. ModLoaderStore.saved_mod_mains[mod_main_path] = mod_main_instance
  194. ModLoaderLog.debug("Adding mod main instance to ModLoader -> %s" % mod_main_instance, LOG_NAME)
  195. add_child(mod_main_instance, true)