Skip to content

feat: ✨ Added mod disabling #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 9, 2023
45 changes: 44 additions & 1 deletion addons/mod_loader/api/mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ const LOG_NAME := "ModLoader:Mod"
# {target} is the vanilla path, eg: `extends "res://singletons/utils.gd"`.
# Note that your extender script doesn't have to follow the same directory path
# as the vanilla file, but it's good practice to do so.
static func install_script_extension(child_script_path:String) -> void:
static func install_script_extension(child_script_path: String) -> void:

var mod_id: String = ModLoaderUtils.get_string_in_between(child_script_path, "res://mods-unpacked/", "/")
var mod_data: ModData = get_mod_data_from_mod_id(mod_id)
if not ModLoaderStore.saved_extension_paths.has(mod_data.manifest.get_mod_id()):
ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()] = []
ModLoaderStore.saved_extension_paths[mod_data.manifest.get_mod_id()].append(child_script_path)

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


# This function should be called only when actually necessary
# as it can break the game and require a restart for mods
# that do not fully use the systems put in place by the mod loader,
# so anything that just uses add_node, move_node ecc...
# To not have your mod break on disable please use provided functions
# and implement a _disable function in your mod_main.gd that will
# handle removing all the changes that were not done through the Mod Loader
func disable_mods() -> void:

# Currently this is the only thing we do, but it is better to expose
# this function like this for further changes
ModLoader._disable_mods()


# This function should be called only when actually necessary
# as it can break the game and require a restart for mods
# that do not fully use the systems put in place by the mod loader,
# so anything that just uses add_node, move_node ecc...
# To not have your mod break on disable please use provided functions
# and implement a _disable function in your mod_main.gd that will
# handle removing all the changes that were not done through the Mod Loader
func disable_mod(mod_data: ModData) -> void:

# Currently this is the only thing we do, but it is better to expose
# this function like this for further changes
ModLoader._disable_mod(mod_data)


# Register an array of classes to the global scope, since Godot only does that in the editor.
# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" }
# You can find these easily in the project.godot file under "_global_script_classes"
Expand All @@ -67,6 +101,15 @@ static func add_translation_from_resource(resource_path: String) -> void:
ModLoaderLog.info("Added Translation from Resource -> %s" % resource_path, LOG_NAME)


# Gets the ModData from the provided namespace
static func get_mod_data_from_mod_id(mod_id: String) -> ModData:
if not ModLoaderStore.mod_data.has(mod_id):
ModLoaderLog.error("%s is an invalid mod_id" % mod_id, LOG_NAME)
return null

return ModLoaderStore.mod_data[mod_id]


static func append_node_in_scene(modified_scene: Node, node_name: String = "", node_parent = null, instance_path: String = "", is_visible: bool = true) -> void:
var new_node: Node
if not instance_path == "":
Expand Down
19 changes: 19 additions & 0 deletions addons/mod_loader/internal/mod_loader_utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ static func _is_valid_global_class_dict(global_class_dict: Dictionary) -> bool:
return true


# Returns the string in between two strings in a provided string
static func get_string_in_between(string: String, initial: String, ending: String) -> String:
var start_index: int = string.find(initial)
if start_index == -1:
ModLoaderLog.error("Initial string not found.", LOG_NAME)
return ""

start_index += initial.length()

var end_index: int = string.find(ending, start_index)
if end_index == -1:
ModLoaderLog.error("Ending string not found.", LOG_NAME)
return ""

var found_string: String = string.substr(start_index, end_index - start_index)

return found_string


# Deprecated
# =============================================================================

Expand Down
21 changes: 12 additions & 9 deletions addons/mod_loader/internal/script_extension.gd
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ static func apply_extension(extension_path: String) -> Script:
# The first entry in the saved script array that has the path
# used as a key will be the duplicate of the not modified script
ModLoaderStore.saved_scripts[parent_script_path].append(parent_script.duplicate())
ModLoaderStore.saved_scripts[parent_script_path].append(child_script)

ModLoaderStore.saved_scripts[parent_script_path].append(child_script)

ModLoaderLog.info("Installing script extension: %s <- %s" % [parent_script_path, extension_path], LOG_NAME)
child_script.take_over_path(parent_script_path)
Expand Down Expand Up @@ -146,16 +147,10 @@ static func _reload_vanilla_child_classes_for(script: Script) -> void:
load(child_class.path).reload()


static func remove_all_extensions_from_all_scripts() -> void:
var _to_remove_scripts: Dictionary = ModLoaderStore.saved_scripts.duplicate()
for script in _to_remove_scripts:
_remove_all_extensions_from_script(script)


# Used to remove a specific extension
static func remove_specific_extension_from_script(extension_path: String) -> void:
# Check path to file exists
if not ModLoaderUtils.file_exists(extension_path):
if not _ModLoaderFile.file_exists(extension_path):
ModLoaderLog.error("The extension script path \"%s\" does not exist" % [extension_path], LOG_NAME)
return

Expand Down Expand Up @@ -200,7 +195,7 @@ static func remove_specific_extension_from_script(extension_path: String) -> voi
# Used to fully reset the provided script to a state prior of any extension
static func _remove_all_extensions_from_script(parent_script_path: String) -> void:
# Check path to file exists
if not ModLoaderUtils.file_exists(parent_script_path):
if not _ModLoaderFile.file_exists(parent_script_path):
ModLoaderLog.error("The parent script path \"%s\" does not exist" % [parent_script_path], LOG_NAME)
return

Expand All @@ -220,3 +215,11 @@ static func _remove_all_extensions_from_script(parent_script_path: String) -> vo

# Remove the script after it has been reset so we do not do it again
ModLoaderStore.saved_scripts.erase(parent_script_path)


# Used to remove all extensions that are of a specific mod
static func remove_all_extensions_of_mod(mod: ModData) -> void:
var _to_remove_extension_paths: Array = ModLoaderStore.saved_extension_paths[mod.manifest.get_mod_id()]
for extension_path in _to_remove_extension_paths:
remove_specific_extension_from_script(extension_path)
ModLoaderStore.saved_extension_paths.erase(mod.manifest.get_mod_id())
35 changes: 34 additions & 1 deletion addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,17 @@ func _reload_mods() -> void:

# Internal call that handles the resetting of all mod related data
func _reset_mods() -> void:
_disable_mods()
ModLoaderStore.mod_data.clear()
ModLoaderStore.mod_load_order.clear()
ModLoaderStore.mod_missing_dependencies.clear()
ModLoaderStore.script_extensions.clear()
_ModLoaderScriptExtension.remove_all_extensions_from_all_scripts()


# Internal call that handles the disabling of all mods
func _disable_mods() -> void:
for mod in ModLoaderStore.mod_data:
_disable_mod(ModLoaderStore.mod_data[mod])


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

ModLoaderStore.saved_mod_mains[mod_main_path] = mod_main_instance

ModLoaderLog.debug("Adding child -> %s" % mod_main_instance, LOG_NAME)
add_child(mod_main_instance, true)


# Call the disable method in every mod if present.
# This way developers can implement their own disable handling logic,
# that is needed if there are actions that are not done through the Mod Loader.
func _disable_mod(mod: ModData) -> void:
if mod == null:
ModLoaderLog.error("The provided ModData does not exist", LOG_NAME)
return
var mod_main_path := mod.get_required_mod_file_path(ModData.required_mod_files.MOD_MAIN)

if not ModLoaderStore.saved_mod_mains.has(mod_main_path):
ModLoaderLog.error("The provided Mod %s has no saved mod main" % mod.manifest.get_mod_id(), LOG_NAME)
return

var mod_main_instance: Node = ModLoaderStore.saved_mod_mains[mod_main_path]
if mod_main_instance.has_method("_disable"):
mod_main_instance._disable()
else:
ModLoaderLog.warning("The provided Mod %s does not have a \"_disable\" method" % mod.manifest.get_mod_id(), LOG_NAME)

ModLoaderStore.saved_mod_mains.erase(mod_main_path)
_ModLoaderScriptExtension.remove_all_extensions_of_mod(mod)

remove_child(mod_main_instance)


# Deprecated
# =============================================================================

Expand Down
6 changes: 6 additions & 0 deletions addons/mod_loader/mod_loader_store.gd
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ var loaded_vanilla_parents_cache := {}
# Stores all the taken over scripts for restoration
var saved_scripts := {}

# Stores main scripts for mod disabling
var saved_mod_mains := {}

# Stores script extension paths with the key being the namespace of a mod
var saved_extension_paths := {}

# Keeps track of logged messages, to avoid flooding the log with duplicate notices
# Can also be used by mods, eg. to create an in-game developer console that
# shows messages
Expand Down