Skip to content

Commit

Permalink
feat: ✨ Added optional dependencies (#188)
Browse files Browse the repository at this point in the history
* feat: ✨ 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: 🎨 fix double indents
  • Loading branch information
KANAjetzt authored Mar 26, 2023
1 parent 7649406 commit ea7e538
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 32 deletions.
12 changes: 11 additions & 1 deletion addons/mod_loader/classes/mod_manifest.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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


Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
87 changes: 56 additions & 31 deletions addons/mod_loader/mod_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit ea7e538

Please sign in to comment.