Skip to content
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

feat: ✨ Added optional dependencies #188

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Qubus0 marked this conversation as resolved.
Show resolved Hide resolved
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
105 changes: 65 additions & 40 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
KANAjetzt marked this conversation as resolved.
Show resolved Hide resolved

# 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
if not mod_data.has(dependency_id):
_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)

# check if dependency has dependencies
if dependency.manifest.dependencies.size() > 0:
is_circular = _check_dependencies(dependency, dependency_chain)

if is_circular:
return is_circular

return is_circular


# 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)
# 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):
KANAjetzt marked this conversation as resolved.
Show resolved Hide resolved
# 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
else:
var dependency: ModData = mod_data[dependency_id]

# 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)

# 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 false if all dependencies have been resolved
return false


# 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