Skip to content

Commit

Permalink
Merge - v6.0.1
Browse files Browse the repository at this point in the history
v6.0.1
  • Loading branch information
KANAjetzt authored Jun 28, 2023
2 parents 1683759 + 9abd5c8 commit 279a012
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 97 deletions.
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
<div align="center">

# GDScript Mod Loader

A general purpose mod-loader for GDScript-based Godot Games.
<img alt="Godot Modding Logo" src="https://github.com/KANAjetzt/godot-mod-loader/assets/41547570/44df4f33-883e-4c1d-baac-06f87b0656f4" width="256" />

</div>

<br />

A generalized Mod Loader for GDScript-based Godot games.
The Mod Loader allows users to create mods for games and distribute them as zips.
Importantly, it provides methods to change existing scripts, scenes, and resources without modifying and distributing vanilla game files.

## Getting Started

View the [Wiki](https://github.com/GodotModding/godot-mod-loader/wiki/) for more information.
You can find detailed documentation, for game and mod developers, on the [Wiki](https://github.com/GodotModding/godot-mod-loader/wiki/) page.

1. Add ModLoader to your [Godot Project](https://github.com/GodotModding/godot-mod-loader/wiki/Godot-Project-Setup)
*Details on how to set up the Mod Loader in your Godot Project, relevant for game and mod developers.*
2. Create your [Mod Structure](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Structure)
*The mods loaded by the Mod Loader must follow a specific directory structure.*
3. Create your [Mod Files](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files)
*Learn about the required files to create your first mod.*
4. Use the [API Methods](https://github.com/GodotModding/godot-mod-loader/wiki/ModLoader-API)
*A list of all available API Methods.*

## Godot Version
The current version of the Mod Loader is developed for Godot 3.5. We have not yet ported it to Godot 4.0 due to lack of demand. If you require support for Godot 4.0, please let us know by opening an [issue](https://github.com/GodotModding/godot-mod-loader/issues) or joining [our Discord](https://discord.godotmodding.com).

## Development
The latest work-in-progress build can be found on the [development branch](https://github.com/GodotModding/godot-mod-loader/tree/development).

1. Add ModLoader to your [Godot Project](https://github.com/GodotModding/godot-mod-loader/wiki/Godot-Project-Setup).
1. Create your [Mod Structure](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Structure)
1. Create your [Mod Files](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files)
1. Use the [API Methods](https://github.com/GodotModding/godot-mod-loader/wiki/API-Methods)
## Compatibility
The Mod Loader supports the following platforms:
- Windows
- macOS
- Linux
- Android
- iOS
2 changes: 1 addition & 1 deletion addons/mod_loader/api/mod.gd
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,4 @@ static func is_mod_loaded(mod_id: String) -> bool:
if not ModLoaderStore.mod_data.has(mod_id) or not ModLoaderStore.mod_data[mod_id].is_loadable:
return false

return true
return true
8 changes: 4 additions & 4 deletions addons/mod_loader/api/mod_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ static func uninstall_script_extension(extension_script_path: String) -> void:
# Used to reload already present mods and load new ones*
#
# Returns: void
func reload_mods() -> void:
static func reload_mods() -> void:

# Currently this is the only thing we do, but it is better to expose
# this function like this for further changes
Expand All @@ -49,7 +49,7 @@ func reload_mods() -> void:
# handle removing all the changes that were not done through the Mod Loader*
#
# Returns: void
func disable_mods() -> void:
static 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
Expand All @@ -70,8 +70,8 @@ func disable_mods() -> void:
# - mod_data (ModData): The ModData object representing the mod to be disabled.
#
# Returns: void
func disable_mod(mod_data: ModData) -> void:
static 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)
ModLoader._disable_mod(mod_data)
78 changes: 52 additions & 26 deletions addons/mod_loader/api/profile.gd
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ static func create_profile(profile_name: String) -> bool:
if not new_profile:
return false

# Set it as the current profile
ModLoaderStore.current_user_profile = new_profile

# Store the new profile in the ModLoaderStore
ModLoaderStore.user_profiles[profile_name] = new_profile

# Set it as the current profile
ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[profile_name]

# Store the new profile in the json file
var is_save_success := _save()

Expand All @@ -108,7 +108,7 @@ static func set_profile(user_profile: ModUserProfile) -> bool:
return false

# Update the current_user_profile in the ModLoaderStore
ModLoaderStore.current_user_profile = user_profile
ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[user_profile.name]

# Save changes in the json file
var is_save_success := _save()
Expand Down Expand Up @@ -196,34 +196,33 @@ static func get_all_as_array() -> Array:
# Example: If "Mod-TestMod" is set in disabled_mods via the editor, the mod will appear disabled in the user profile.
# If the user then enables the mod in the profile the entry in disabled_mods will be removed.
static func _update_disabled_mods() -> void:
var user_profile_disabled_mods := []
var current_user_profile: ModUserProfile

current_user_profile = get_current()

# Check if a current user profile is set
if not ModLoaderStore.current_user_profile:
if not current_user_profile:
ModLoaderLog.info("There is no current user profile. The \"default\" profile will be created.", LOG_NAME)
return

current_user_profile = get_current()

# Iterate through the mod list in the current user profile to find disabled mods
for mod_id in current_user_profile.mod_list:
if not current_user_profile.mod_list[mod_id].is_active:
user_profile_disabled_mods.push_back(mod_id)

# Append the disabled mods to the global list of disabled mods
ModLoaderStore.ml_options.disabled_mods.append_array(user_profile_disabled_mods)
var mod_list_entry: Dictionary = current_user_profile.mod_list[mod_id]
if ModLoaderStore.mod_data.has(mod_id):
ModLoaderStore.mod_data[mod_id].is_active = mod_list_entry.is_active

ModLoaderLog.debug(
"Updated the global list of disabled mods \"%s\", based on the current user profile \"%s\""
% [ModLoaderStore.ml_options.disabled_mods, current_user_profile.name],
"Updated the active state of all mods, based on the current user profile \"%s\""
% current_user_profile.name,
LOG_NAME)


# This function updates the mod lists of all user profiles with newly loaded mods that are not already present.
# It does so by comparing the current set of loaded mods with the mod list of each user profile, and adding any missing mods.
# Additionally, it checks for and deletes any mods from each profile's mod list that are no longer installed on the system.
static func _update_mod_lists() -> bool:
# Generate a list of currently present mods by combining the mods
# in mod_data and ml_options.disabled_mods from ModLoaderStore.
var current_mod_list := _generate_mod_list()

# Iterate over all user profiles
Expand All @@ -246,18 +245,17 @@ static func _update_mod_lists() -> bool:
return is_save_success


# Updates the mod list by checking the validity of each mod entry and making necessary modifications.
# This function takes a mod_list dictionary and optional mod_data dictionary as input and returns
# an updated mod_list dictionary. It iterates over each mod ID in the mod list, checks if the mod
# is still installed and if the current_config is present. If the mod is not installed or the current
# config is missing, the mod is removed or its current_config is reset to the default configuration.
static func _update_mod_list(mod_list: Dictionary, mod_data := ModLoaderStore.mod_data) -> Dictionary:
var updated_mod_list := mod_list.duplicate(true)

# Iterate over each mod ID in the mod list
for mod_id in updated_mod_list:
var mod_list_entry: Dictionary = updated_mod_list[mod_id]

# If mod data is accessible and the mod is loaded
if not mod_data.empty() and mod_data.has(mod_id):
mod_list_entry = _generate_mod_list_entry(mod_id, true)

# If mod data is accessible and the mod is not loaded
if not mod_data.empty() and not mod_data.has(mod_id):
# Check if the mod_dir for the mod-id exists
Expand All @@ -273,6 +271,28 @@ static func _update_mod_list(mod_list: Dictionary, mod_data := ModLoaderStore.mo
# If the current config doesn't exist, reset it to the default configuration
mod_list_entry.current_config = ModLoaderConfig.DEFAULT_CONFIG_NAME

# If the mod is not loaded
if not mod_data.has(mod_id):
if (
# Check if the entry has a zip_path key
mod_list_entry.has("zip_path") and
# Check if the entry has a zip_path
not mod_list_entry.zip_path.empty() and
# Check if the zip file for the mod exists
not _ModLoaderFile.file_exists(mod_list_entry.zip_path)
):
# If the mod directory doesn't exist,
# the mod is no longer installed and can be removed from the mod list
ModLoaderLog.debug(
"Mod \"%s\" has been deleted from all user profiles as the corresponding zip file no longer exists at path \"%s\"."
% [mod_id, mod_list_entry.zip_path],
LOG_NAME,
true
)

updated_mod_list.erase(mod_id)
continue

updated_mod_list[mod_id] = mod_list_entry

return updated_mod_list
Expand All @@ -298,7 +318,13 @@ static func _generate_mod_list() -> Dictionary:
static func _generate_mod_list_entry(mod_id: String, is_active: bool) -> Dictionary:
var mod_list_entry := {}

# Set the mods active state
mod_list_entry.is_active = is_active

# Set the mods zip path if available
if ModLoaderStore.mod_data.has(mod_id):
mod_list_entry.zip_path = ModLoaderStore.mod_data[mod_id].zip_path

# Set the current_config if the mod has a config schema and is active
if is_active and not ModLoaderConfig.get_config_schema(mod_id).empty():
var current_config: ModConfig = ModLoaderStore.mod_data[mod_id].current_config
Expand Down Expand Up @@ -327,7 +353,10 @@ static func _set_mod_state(mod_id: String, profile_name: String, activate: bool)
return false

# Handle mod state
# Set state for user profile
ModLoaderStore.user_profiles[profile_name].mod_list[mod_id].is_active = activate
# Set state in the ModData
ModLoaderStore.mod_data[mod_id].is_active = activate

# Save profiles to the user profiles JSON file
var is_save_success := _save()
Expand Down Expand Up @@ -390,12 +419,6 @@ static func _load() -> bool:
ModLoaderLog.error("No profile file found at \"%s\"" % FILE_PATH_USER_PROFILES, LOG_NAME)
return false

# Set the current user profile to the one specified in the data
var current_user_profile: ModUserProfile = ModUserProfile.new()
current_user_profile.name = data.current_profile
current_user_profile.mod_list = data.profiles[data.current_profile].mod_list
ModLoaderStore.current_user_profile = current_user_profile

# Loop through each profile in the data and add them to ModLoaderStore
for profile_name in data.profiles.keys():
# Get the profile data from the JSON object
Expand All @@ -405,6 +428,9 @@ static func _load() -> bool:
var new_profile := _create_new_profile(profile_name, profile_data.mod_list)
ModLoaderStore.user_profiles[profile_name] = new_profile

# Set the current user profile to the one specified in the data
ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[data.current_profile]

return true


Expand Down
48 changes: 38 additions & 10 deletions addons/mod_loader/internal/file.gd
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,21 @@ static func _get_json_string_as_dict(string: String) -> Dictionary:


# Load the mod ZIP from the provided directory
static func load_zips_in_folder(folder_path: String) -> int:
var temp_zipped_mods_count := 0
static func load_zips_in_folder(folder_path: String) -> Dictionary:
var URL_MOD_STRUCTURE_DOCS := "https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Structure"
var zip_data := {}

var mod_dir := Directory.new()
var mod_dir_open_error := mod_dir.open(folder_path)
if not mod_dir_open_error == OK:
ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME)
return -1
return {}
var mod_dir_listdir_error := mod_dir.list_dir_begin()
if not mod_dir_listdir_error == OK:
ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME)
return -1
return {}



# Get all zip folders inside the game mod folder
while true:
Expand All @@ -76,9 +79,35 @@ static func load_zips_in_folder(folder_path: String) -> int:
# Go to the next file
continue

var mod_folder_path := folder_path.plus_file(mod_zip_file_name)
var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path)
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false)
var mod_zip_path := folder_path.plus_file(mod_zip_file_name)
var mod_zip_global_path := ProjectSettings.globalize_path(mod_zip_path)
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_zip_global_path, false)

# Get the current directories inside UNPACKED_DIR
# This array is used to determine which directory is new
var current_mod_dirs := _ModLoaderPath.get_dir_paths_in_dir(_ModLoaderPath.get_unpacked_mods_dir_path())

# Create a backup to reference when the next mod is loaded
var current_mod_dirs_backup := current_mod_dirs.duplicate()

# Remove all directory paths that existed before, leaving only the one added last
for previous_mod_dir in ModLoaderStore.previous_mod_dirs:
current_mod_dirs.erase(previous_mod_dir)

# If the mod zip is not structured correctly, it may not be in the UNPACKED_DIR.
if current_mod_dirs.empty():
ModLoaderLog.fatal(
"The mod zip at path \"%s\" does not have the correct file structure. For more information, please visit \"%s\"."
% [mod_zip_global_path, URL_MOD_STRUCTURE_DOCS],
LOG_NAME
)
continue

# The key is the mod_id of the latest loaded mod, and the value is the path to the zip file
zip_data[current_mod_dirs[0].get_slice("/", 3)] = mod_zip_global_path

# Update previous_mod_dirs in ModLoaderStore to use for the next mod
ModLoaderStore.previous_mod_dirs = current_mod_dirs_backup

# Notifies developer of an issue with Godot, where using `load_resource_pack`
# in the editor WIPES the entire virtual res:// directory the first time you
Expand All @@ -94,7 +123,7 @@ static func load_zips_in_folder(folder_path: String) -> int:
"Please unpack your mod ZIPs instead, and add them to ", _ModLoaderPath.get_unpacked_mods_dir_path()), LOG_NAME)
ModLoaderStore.has_shown_editor_zips_warning = true

ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME)
ModLoaderLog.debug("Found mod ZIP: %s" % mod_zip_global_path, LOG_NAME)

# If there was an error loading the mod zip file
if not is_mod_loaded_successfully:
Expand All @@ -104,11 +133,10 @@ static func load_zips_in_folder(folder_path: String) -> int:

# Mod successfully loaded!
ModLoaderLog.success("%s loaded." % mod_zip_file_name, LOG_NAME)
temp_zipped_mods_count += 1

mod_dir.list_dir_end()

return temp_zipped_mods_count
return zip_data


# Save Data
Expand Down
14 changes: 7 additions & 7 deletions addons/mod_loader/internal/mod_loader_utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -126,47 +126,47 @@ static func get_string_in_between(string: String, initial: String, ending: Strin
# Stops the execution in editor
# Always logged
static func log_fatal(message: String, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_fatal", "ModLoaderLog.fatal", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_fatal", "ModLoaderLog.fatal", "6.0.0")
ModLoaderLog.fatal(message, mod_name)


# Logs the message and pushed an error. Prefixed ERROR
# Always logged
static func log_error(message: String, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_error", "ModLoaderLog.error", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_error", "ModLoaderLog.error", "6.0.0")
ModLoaderLog.error(message, mod_name)


# Logs the message and pushes a warning. Prefixed WARNING
# Logged with verbosity level at or above warning (-v)
static func log_warning(message: String, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_warning", "ModLoaderLog.warning", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_warning", "ModLoaderLog.warning", "6.0.0")
ModLoaderLog.warning(message, mod_name)


# Logs the message. Prefixed INFO
# Logged with verbosity level at or above info (-vv)
static func log_info(message: String, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_info", "ModLoaderLog.info", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_info", "ModLoaderLog.info", "6.0.0")
ModLoaderLog.info(message, mod_name)


# Logs the message. Prefixed SUCCESS
# Logged with verbosity level at or above info (-vv)
static func log_success(message: String, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_success", "ModLoaderLog.success", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_success", "ModLoaderLog.success", "6.0.0")
ModLoaderLog.success(message, mod_name)


# Logs the message. Prefixed DEBUG
# Logged with verbosity level at or above debug (-vvv)
static func log_debug(message: String, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_debug", "ModLoaderLog.debug", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_debug", "ModLoaderLog.debug", "6.0.0")
ModLoaderLog.debug(message, mod_name)


# Logs the message formatted with [method JSON.print]. Prefixed DEBUG
# Logged with verbosity level at or above debug (-vvv)
static func log_debug_json_print(message: String, json_printable, mod_name: String) -> void:
ModLoaderDeprecated.deprecated_changed("ModLoader.log_debug_json_print", "ModLoaderLog.debug_json_print", "6.0.0")
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_debug_json_print", "ModLoaderLog.debug_json_print", "6.0.0")
ModLoaderLog.debug_json_print(message, json_printable, mod_name)
Loading

0 comments on commit 279a012

Please sign in to comment.