Skip to content

Commit ca40996

Browse files
authored
feat: Added mod disabling (#241)
* fix: Missed ModLoaderUtils to _ModLoaderFile change * feat: Implemented mod disabling * fix: Not saving the extenders after the first one of a script * style: Change log message to provide more info * fix: Remove unused log * feat: Expose `disable_mods` and `disable_mod` for usage * style: Rename `namespace` to proper name `mod_id` * style: Add `_` in front of method as it should not be used in most cases * style: Take care of review for styling * style: Change log text
1 parent 3358adc commit ca40996

File tree

5 files changed

+113
-9
lines changed

5 files changed

+113
-9
lines changed

addons/mod_loader/api/mod.gd

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ const LOG_NAME := "ModLoader:Mod"
1212
# {target} is the vanilla path, eg: `extends "res://singletons/utils.gd"`.
1313
# Note that your extender script doesn't have to follow the same directory path
1414
# as the vanilla file, but it's good practice to do so.
15-
static func install_script_extension(child_script_path:String) -> void:
15+
static func install_script_extension(child_script_path: String) -> void:
16+
17+
var mod_id: String = ModLoaderUtils.get_string_in_between(child_script_path, "res://mods-unpacked/", "/")
18+
var mod_data: ModData = get_mod_data_from_mod_id(mod_id)
19+
if not ModLoaderStore.saved_extension_paths.has(mod_data.manifest.get_mod_id()):
20+
ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()] = []
21+
ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()].append(child_script_path)
1622

1723
# If this is called during initialization, add it with the other
1824
# extensions to be installed taking inheritance chain into account
@@ -45,6 +51,34 @@ func reload_mods() -> void:
4551
ModLoader._reload_mods()
4652

4753

54+
# This function should be called only when actually necessary
55+
# as it can break the game and require a restart for mods
56+
# that do not fully use the systems put in place by the mod loader,
57+
# so anything that just uses add_node, move_node ecc...
58+
# To not have your mod break on disable please use provided functions
59+
# and implement a _disable function in your mod_main.gd that will
60+
# handle removing all the changes that were not done through the Mod Loader
61+
func disable_mods() -> void:
62+
63+
# Currently this is the only thing we do, but it is better to expose
64+
# this function like this for further changes
65+
ModLoader._disable_mods()
66+
67+
68+
# This function should be called only when actually necessary
69+
# as it can break the game and require a restart for mods
70+
# that do not fully use the systems put in place by the mod loader,
71+
# so anything that just uses add_node, move_node ecc...
72+
# To not have your mod break on disable please use provided functions
73+
# and implement a _disable function in your mod_main.gd that will
74+
# handle removing all the changes that were not done through the Mod Loader
75+
func disable_mod(mod_data: ModData) -> void:
76+
77+
# Currently this is the only thing we do, but it is better to expose
78+
# this function like this for further changes
79+
ModLoader._disable_mod(mod_data)
80+
81+
4882
# Register an array of classes to the global scope, since Godot only does that in the editor.
4983
# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" }
5084
# You can find these easily in the project.godot file under "_global_script_classes"
@@ -67,6 +101,15 @@ static func add_translation_from_resource(resource_path: String) -> void:
67101
ModLoaderLog.info("Added Translation from Resource -> %s" % resource_path, LOG_NAME)
68102

69103

104+
# Gets the ModData from the provided namespace
105+
static func get_mod_data_from_mod_id(mod_id: String) -> ModData:
106+
if not ModLoaderStore.mod_data.has(mod_id):
107+
ModLoaderLog.error("%s is an invalid mod_id" % mod_id, LOG_NAME)
108+
return null
109+
110+
return ModLoaderStore.mod_data[mod_id]
111+
112+
70113
static func append_node_in_scene(modified_scene: Node, node_name: String = "", node_parent = null, instance_path: String = "", is_visible: bool = true) -> void:
71114
var new_node: Node
72115
if not instance_path == "":

addons/mod_loader/internal/mod_loader_utils.gd

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,25 @@ static func _is_valid_global_class_dict(global_class_dict: Dictionary) -> bool:
8989
return true
9090

9191

92+
# Returns the string in between two strings in a provided string
93+
static func get_string_in_between(string: String, initial: String, ending: String) -> String:
94+
var start_index: int = string.find(initial)
95+
if start_index == -1:
96+
ModLoaderLog.error("Initial string not found.", LOG_NAME)
97+
return ""
98+
99+
start_index += initial.length()
100+
101+
var end_index: int = string.find(ending, start_index)
102+
if end_index == -1:
103+
ModLoaderLog.error("Ending string not found.", LOG_NAME)
104+
return ""
105+
106+
var found_string: String = string.substr(start_index, end_index - start_index)
107+
108+
return found_string
109+
110+
92111
# Deprecated
93112
# =============================================================================
94113

addons/mod_loader/internal/script_extension.gd

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ static func apply_extension(extension_path: String) -> Script:
104104
# The first entry in the saved script array that has the path
105105
# used as a key will be the duplicate of the not modified script
106106
ModLoaderStore.saved_scripts[parent_script_path].append(parent_script.duplicate())
107-
ModLoaderStore.saved_scripts[parent_script_path].append(child_script)
107+
108+
ModLoaderStore.saved_scripts[parent_script_path].append(child_script)
108109

109110
ModLoaderLog.info("Installing script extension: %s <- %s" % [parent_script_path, extension_path], LOG_NAME)
110111
child_script.take_over_path(parent_script_path)
@@ -146,12 +147,6 @@ static func _reload_vanilla_child_classes_for(script: Script) -> void:
146147
load(child_class.path).reload()
147148

148149

149-
static func remove_all_extensions_from_all_scripts() -> void:
150-
var _to_remove_scripts: Dictionary = ModLoaderStore.saved_scripts.duplicate()
151-
for script in _to_remove_scripts:
152-
_remove_all_extensions_from_script(script)
153-
154-
155150
# Used to remove a specific extension
156151
static func remove_specific_extension_from_script(extension_path: String) -> void:
157152
# Check path to file exists
@@ -220,3 +215,11 @@ static func _remove_all_extensions_from_script(parent_script_path: String) -> vo
220215

221216
# Remove the script after it has been reset so we do not do it again
222217
ModLoaderStore.saved_scripts.erase(parent_script_path)
218+
219+
220+
# Used to remove all extensions that are of a specific mod
221+
static func remove_all_extensions_of_mod(mod: ModData) -> void:
222+
var _to_remove_extension_paths: Array = ModLoaderStore.saved_extension_paths[mod.manifest.get_mod_id()]
223+
for extension_path in _to_remove_extension_paths:
224+
remove_specific_extension_from_script(extension_path)
225+
ModLoaderStore.saved_extension_paths.erase(mod.manifest.get_mod_id())

addons/mod_loader/mod_loader.gd

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,17 @@ func _reload_mods() -> void:
169169

170170
# Internal call that handles the resetting of all mod related data
171171
func _reset_mods() -> void:
172+
_disable_mods()
172173
ModLoaderStore.mod_data.clear()
173174
ModLoaderStore.mod_load_order.clear()
174175
ModLoaderStore.mod_missing_dependencies.clear()
175176
ModLoaderStore.script_extensions.clear()
176-
_ModLoaderScriptExtension.remove_all_extensions_from_all_scripts()
177+
178+
179+
# Internal call that handles the disabling of all mods
180+
func _disable_mods() -> void:
181+
for mod in ModLoaderStore.mod_data:
182+
_disable_mod(ModLoaderStore.mod_data[mod])
177183

178184

179185
# Check autoload positions:
@@ -451,10 +457,37 @@ func _init_mod(mod: ModData) -> void:
451457
var mod_main_instance: Node = mod_main_script.new(self)
452458
mod_main_instance.name = mod.manifest.get_mod_id()
453459

460+
ModLoaderStore.saved_mod_mains[mod_main_path] = mod_main_instance
461+
454462
ModLoaderLog.debug("Adding child -> %s" % mod_main_instance, LOG_NAME)
455463
add_child(mod_main_instance, true)
456464

457465

466+
# Call the disable method in every mod if present.
467+
# This way developers can implement their own disable handling logic,
468+
# that is needed if there are actions that are not done through the Mod Loader.
469+
func _disable_mod(mod: ModData) -> void:
470+
if mod == null:
471+
ModLoaderLog.error("The provided ModData does not exist", LOG_NAME)
472+
return
473+
var mod_main_path := mod.get_required_mod_file_path(ModData.required_mod_files.MOD_MAIN)
474+
475+
if not ModLoaderStore.saved_mod_mains.has(mod_main_path):
476+
ModLoaderLog.error("The provided Mod %s has no saved mod main" % mod.manifest.get_mod_id(), LOG_NAME)
477+
return
478+
479+
var mod_main_instance: Node = ModLoaderStore.saved_mod_mains[mod_main_path]
480+
if mod_main_instance.has_method("_disable"):
481+
mod_main_instance._disable()
482+
else:
483+
ModLoaderLog.warning("The provided Mod %s does not have a \"_disable\" method" % mod.manifest.get_mod_id(), LOG_NAME)
484+
485+
ModLoaderStore.saved_mod_mains.erase(mod_main_path)
486+
_ModLoaderScriptExtension.remove_all_extensions_of_mod(mod)
487+
488+
remove_child(mod_main_instance)
489+
490+
458491
# Deprecated
459492
# =============================================================================
460493

addons/mod_loader/mod_loader_store.gd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ var loaded_vanilla_parents_cache := {}
5959
# Stores all the taken over scripts for restoration
6060
var saved_scripts := {}
6161

62+
# Stores main scripts for mod disabling
63+
var saved_mod_mains := {}
64+
65+
# Stores script extension paths with the key being the namespace of a mod
66+
var saved_extension_paths := {}
67+
6268
# Keeps track of logged messages, to avoid flooding the log with duplicate notices
6369
# Can also be used by mods, eg. to create an in-game developer console that
6470
# shows messages

0 commit comments

Comments
 (0)