From ea7e538934d3f9de5a4596db3218da95f35344c6 Mon Sep 17 00:00:00 2001 From: KANAjetzt <41547570+KANAjetzt@users.noreply.github.com> Date: Sun, 26 Mar 2023 21:00:10 +0200 Subject: [PATCH] feat: :sparkles: Added optional dependencies (#188) * feat: :sparkles: Added optional dependencies Added new loop that iterates through each mod in `mod_data` and runs `_check_dependencies()` with a new `is_required` flag set to false. This flag is used to determine the dependency array to use and if a mod is still loaded if the depdendency is missing. * style: :art: fix double indents --- addons/mod_loader/classes/mod_manifest.gd | 12 +++- addons/mod_loader/mod_loader.gd | 87 +++++++++++++++-------- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/addons/mod_loader/classes/mod_manifest.gd b/addons/mod_loader/classes/mod_manifest.gd index baad9265..db61a33e 100644 --- a/addons/mod_loader/classes/mod_manifest.gd +++ b/addons/mod_loader/classes/mod_manifest.gd @@ -18,6 +18,8 @@ var description := "" var website_url := "" # Used to determine mod load order var dependencies: PoolStringArray = [] +# Used to determine mod load order +var optional_dependencies: PoolStringArray = [] var authors: PoolStringArray = [] # only used for information @@ -77,6 +79,7 @@ func _init(manifest: Dictionary) -> void: var godot_details: Dictionary = manifest.extra.godot authors = ModLoaderUtils.get_array_from_dict(godot_details, "authors") + optional_dependencies = ModLoaderUtils.get_array_from_dict(godot_details, "optional_dependencies") incompatibilities = ModLoaderUtils.get_array_from_dict(godot_details, "incompatibilities") load_before = ModLoaderUtils.get_array_from_dict(godot_details, "load_before") compatible_game_version = ModLoaderUtils.get_array_from_dict(godot_details, "compatible_game_version") @@ -86,7 +89,8 @@ func _init(manifest: Dictionary) -> void: config_defaults = godot_details.config_defaults var mod_id = get_mod_id() - if not validate_dependencies_and_incompatibilities(mod_id, dependencies, incompatibilities): + if (not validate_dependencies_and_incompatibilities(mod_id, dependencies, incompatibilities) or + not validate_optional_dependencies(mod_id, optional_dependencies)): return @@ -111,6 +115,7 @@ func get_as_dict() -> Dictionary: "description": description, "website_url": website_url, "dependencies": dependencies, + "optional_dependencies": optional_dependencies, "authors": authors, "compatible_game_version": compatible_game_version, "compatible_mod_loader_version": compatible_mod_loader_version, @@ -135,6 +140,7 @@ func to_json() -> String: "extra": { "godot":{ "authors": authors, + "optional_dependencies": optional_dependencies, "compatible_game_version": compatible_game_version, "compatible_mod_loader_version": compatible_mod_loader_version, "incompatibilities": incompatibilities, @@ -252,6 +258,10 @@ static func validate_dependencies_and_incompatibilities(mod_id: String, dependen return true +static func validate_optional_dependencies(mod_id: String, optional_dependencies: PoolStringArray, is_silent := false) -> bool: + return is_mod_id_array_valid(mod_id, optional_dependencies, "optional_dependency", is_silent) + + static func validate_dependencies(mod_id: String, dependencies: PoolStringArray, is_silent := false) -> bool: return is_mod_id_array_valid(mod_id, dependencies, "dependency", is_silent) diff --git a/addons/mod_loader/mod_loader.gd b/addons/mod_loader/mod_loader.gd index 5e8e61a6..490c0201 100644 --- a/addons/mod_loader/mod_loader.gd +++ b/addons/mod_loader/mod_loader.gd @@ -140,6 +140,16 @@ func _init() -> void: _check_load_before(mod) + # Run optional dependency checks after loading mod_manifest. + # If a mod depends on another mod that hasn't been loaded, + # that dependent mod will be loaded regardless. + for dir_name in mod_data: + var mod: ModData = mod_data[dir_name] + if not mod.is_loadable: + continue + var _is_circular := _check_dependencies(mod, false) + + # Run dependency checks after loading mod_manifest. If a mod depends on another # mod that hasn't been loaded, that dependent mod won't be loaded. for dir_name in mod_data: @@ -431,56 +441,71 @@ func _init_mod_data(mod_folder_path: String) -> void: # Run dependency checks on a mod, checking any dependencies it lists in its # mod_manifest (ie. its manifest.json file). If a mod depends on another mod that -# hasn't been loaded, the dependent mod won't be loaded. -func _check_dependencies(mod: ModData, dependency_chain := []) -> bool: - ModLoaderUtils.log_debug("Checking dependencies - mod_id: %s dependencies: %s" % [mod.dir_name, mod.manifest.dependencies], LOG_NAME) - - var is_circular := false +# hasn't been loaded, the dependent mod won't be loaded, if it is a required dependency. +# +# Parameters: +# - mod: A ModData object representing the mod being checked. +# - dependency_chain: An array that stores the IDs of the mods that have already +# been checked to avoid circular dependencies. +# - is_required: A boolean indicating whether the mod is a required or optional +# dependency. Optional dependencies will not prevent the dependent mod from +# loading if they are missing. +# +# Returns: A boolean indicating whether a circular dependency was detected. +func _check_dependencies(mod: ModData, is_required := true, dependency_chain := []) -> bool: + var dependency_type := "required" if is_required else "optional" + # Get the dependency array based on the is_required flag + var dependencies := mod.manifest.dependencies if is_required else mod.manifest.optional_dependencies + # Get the ID of the mod being checked var mod_id := mod.dir_name + ModLoaderUtils.log_debug("Checking dependencies - mod_id: %s %s dependencies: %s" % [mod_id, dependency_type, dependencies], LOG_NAME) + # Check for circular dependency if mod_id in dependency_chain: - is_circular = true - ModLoaderUtils.log_debug("Dependency check - circular dependency detected.", LOG_NAME) - return is_circular + ModLoaderUtils.log_debug("%s dependency check - circular dependency detected for mod with ID %s." % [dependency_type.capitalize(), mod_id], LOG_NAME) + return true - # Add mod_id to dependency_chain + # Add mod_id to dependency_chain to avoid circular dependencies dependency_chain.append(mod_id) - # loop through each dependency - for dependency_id in mod.manifest.dependencies: - # check if dependency is missing + # Loop through each dependency listed in the mod's manifest + for dependency_id in dependencies: + # Check if dependency is missing if not mod_data.has(dependency_id): + # Skip to the next dependency if it's optional + if not is_required: + ModLoaderUtils.log_info("Missing optional dependency - mod: -> %s dependency -> %s" % [mod_id, dependency_id], LOG_NAME) + continue _handle_missing_dependency(mod_id, dependency_id) # Flag the mod so it's not loaded later mod.is_loadable = false - continue - - var dependency: ModData = mod_data[dependency_id] - - # increase importance score by 1 - dependency.importance += 1 - ModLoaderUtils.log_debug("Dependency -> %s importance -> %s" % [dependency_id, dependency.importance], LOG_NAME) + else: + var dependency: ModData = mod_data[dependency_id] - # check if dependency has dependencies - if dependency.manifest.dependencies.size() > 0: - is_circular = _check_dependencies(dependency, dependency_chain) + # Increase the importance score of the dependency by 1 + dependency.importance += 1 + ModLoaderUtils.log_debug("%s dependency -> %s importance -> %s" % [dependency_type.capitalize(), dependency_id, dependency.importance], LOG_NAME) - if is_circular: - return is_circular + # Check if the dependency has any dependencies of its own + if dependency.manifest.dependencies.size() > 0: + if _check_dependencies(dependency, is_required, dependency_chain): + return true - return is_circular + # Return false if all dependencies have been resolved + return false -# Handle missing dependencies: Sets `is_loadable` to false and logs an error -func _handle_missing_dependency(mod_dir_name: String, dependency_id: String) -> void: - ModLoaderUtils.log_error("Missing dependency - mod: -> %s dependency -> %s" % [mod_dir_name, dependency_id], LOG_NAME) +# Handles a missing dependency for a given mod ID. Logs an error message indicating the missing dependency and adds +# the dependency ID to the mod_missing_dependencies dictionary for the specified mod. +func _handle_missing_dependency(mod_id: String, dependency_id: String) -> void: + ModLoaderUtils.log_error("Missing dependency - mod: -> %s dependency -> %s" % [mod_id, dependency_id], LOG_NAME) # if mod is not present in the missing dependencies array - if not mod_missing_dependencies.has(mod_dir_name): + if not mod_missing_dependencies.has(mod_id): # add it - mod_missing_dependencies[mod_dir_name] = [] + mod_missing_dependencies[mod_id] = [] - mod_missing_dependencies[mod_dir_name].append(dependency_id) + mod_missing_dependencies[mod_id].append(dependency_id) # Run load before check on a mod, checking any load_before entries it lists in its