Skip to content

Commit 279a012

Browse files
authored
Merge - v6.0.1
v6.0.1
2 parents 1683759 + 9abd5c8 commit 279a012

12 files changed

+228
-97
lines changed

README.md

+34-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,40 @@
1+
<div align="center">
2+
13
# GDScript Mod Loader
24

3-
A general purpose mod-loader for GDScript-based Godot Games.
5+
<img alt="Godot Modding Logo" src="https://github.com/KANAjetzt/godot-mod-loader/assets/41547570/44df4f33-883e-4c1d-baac-06f87b0656f4" width="256" />
6+
7+
</div>
8+
9+
<br />
10+
11+
A generalized Mod Loader for GDScript-based Godot games.
12+
The Mod Loader allows users to create mods for games and distribute them as zips.
13+
Importantly, it provides methods to change existing scripts, scenes, and resources without modifying and distributing vanilla game files.
414

515
## Getting Started
616

7-
View the [Wiki](https://github.com/GodotModding/godot-mod-loader/wiki/) for more information.
17+
You can find detailed documentation, for game and mod developers, on the [Wiki](https://github.com/GodotModding/godot-mod-loader/wiki/) page.
18+
19+
1. Add ModLoader to your [Godot Project](https://github.com/GodotModding/godot-mod-loader/wiki/Godot-Project-Setup)
20+
*Details on how to set up the Mod Loader in your Godot Project, relevant for game and mod developers.*
21+
2. Create your [Mod Structure](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Structure)
22+
*The mods loaded by the Mod Loader must follow a specific directory structure.*
23+
3. Create your [Mod Files](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files)
24+
*Learn about the required files to create your first mod.*
25+
4. Use the [API Methods](https://github.com/GodotModding/godot-mod-loader/wiki/ModLoader-API)
26+
*A list of all available API Methods.*
27+
28+
## Godot Version
29+
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).
30+
31+
## Development
32+
The latest work-in-progress build can be found on the [development branch](https://github.com/GodotModding/godot-mod-loader/tree/development).
833

9-
1. Add ModLoader to your [Godot Project](https://github.com/GodotModding/godot-mod-loader/wiki/Godot-Project-Setup).
10-
1. Create your [Mod Structure](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Structure)
11-
1. Create your [Mod Files](https://github.com/GodotModding/godot-mod-loader/wiki/Mod-Files)
12-
1. Use the [API Methods](https://github.com/GodotModding/godot-mod-loader/wiki/API-Methods)
34+
## Compatibility
35+
The Mod Loader supports the following platforms:
36+
- Windows
37+
- macOS
38+
- Linux
39+
- Android
40+
- iOS

addons/mod_loader/api/mod.gd

+1-1
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,4 @@ static func is_mod_loaded(mod_id: String) -> bool:
171171
if not ModLoaderStore.mod_data.has(mod_id) or not ModLoaderStore.mod_data[mod_id].is_loadable:
172172
return false
173173

174-
return true
174+
return true

addons/mod_loader/api/mod_manager.gd

+4-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ static func uninstall_script_extension(extension_script_path: String) -> void:
3131
# Used to reload already present mods and load new ones*
3232
#
3333
# Returns: void
34-
func reload_mods() -> void:
34+
static func reload_mods() -> void:
3535

3636
# Currently this is the only thing we do, but it is better to expose
3737
# this function like this for further changes
@@ -49,7 +49,7 @@ func reload_mods() -> void:
4949
# handle removing all the changes that were not done through the Mod Loader*
5050
#
5151
# Returns: void
52-
func disable_mods() -> void:
52+
static func disable_mods() -> void:
5353

5454
# Currently this is the only thing we do, but it is better to expose
5555
# this function like this for further changes
@@ -70,8 +70,8 @@ func disable_mods() -> void:
7070
# - mod_data (ModData): The ModData object representing the mod to be disabled.
7171
#
7272
# Returns: void
73-
func disable_mod(mod_data: ModData) -> void:
73+
static func disable_mod(mod_data: ModData) -> void:
7474

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

addons/mod_loader/api/profile.gd

+52-26
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ static func create_profile(profile_name: String) -> bool:
8080
if not new_profile:
8181
return false
8282

83-
# Set it as the current profile
84-
ModLoaderStore.current_user_profile = new_profile
85-
8683
# Store the new profile in the ModLoaderStore
8784
ModLoaderStore.user_profiles[profile_name] = new_profile
8885

86+
# Set it as the current profile
87+
ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[profile_name]
88+
8989
# Store the new profile in the json file
9090
var is_save_success := _save()
9191

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

110110
# Update the current_user_profile in the ModLoaderStore
111-
ModLoaderStore.current_user_profile = user_profile
111+
ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[user_profile.name]
112112

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

201+
current_user_profile = get_current()
202+
202203
# Check if a current user profile is set
203-
if not ModLoaderStore.current_user_profile:
204+
if not current_user_profile:
204205
ModLoaderLog.info("There is no current user profile. The \"default\" profile will be created.", LOG_NAME)
205206
return
206207

207-
current_user_profile = get_current()
208-
209208
# Iterate through the mod list in the current user profile to find disabled mods
210209
for mod_id in current_user_profile.mod_list:
211-
if not current_user_profile.mod_list[mod_id].is_active:
212-
user_profile_disabled_mods.push_back(mod_id)
213-
214-
# Append the disabled mods to the global list of disabled mods
215-
ModLoaderStore.ml_options.disabled_mods.append_array(user_profile_disabled_mods)
210+
var mod_list_entry: Dictionary = current_user_profile.mod_list[mod_id]
211+
if ModLoaderStore.mod_data.has(mod_id):
212+
ModLoaderStore.mod_data[mod_id].is_active = mod_list_entry.is_active
216213

217214
ModLoaderLog.debug(
218-
"Updated the global list of disabled mods \"%s\", based on the current user profile \"%s\""
219-
% [ModLoaderStore.ml_options.disabled_mods, current_user_profile.name],
215+
"Updated the active state of all mods, based on the current user profile \"%s\""
216+
% current_user_profile.name,
220217
LOG_NAME)
221218

222219

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

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

248247

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

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

257-
# If mod data is accessible and the mod is loaded
258-
if not mod_data.empty() and mod_data.has(mod_id):
259-
mod_list_entry = _generate_mod_list_entry(mod_id, true)
260-
261259
# If mod data is accessible and the mod is not loaded
262260
if not mod_data.empty() and not mod_data.has(mod_id):
263261
# Check if the mod_dir for the mod-id exists
@@ -273,6 +271,28 @@ static func _update_mod_list(mod_list: Dictionary, mod_data := ModLoaderStore.mo
273271
# If the current config doesn't exist, reset it to the default configuration
274272
mod_list_entry.current_config = ModLoaderConfig.DEFAULT_CONFIG_NAME
275273

274+
# If the mod is not loaded
275+
if not mod_data.has(mod_id):
276+
if (
277+
# Check if the entry has a zip_path key
278+
mod_list_entry.has("zip_path") and
279+
# Check if the entry has a zip_path
280+
not mod_list_entry.zip_path.empty() and
281+
# Check if the zip file for the mod exists
282+
not _ModLoaderFile.file_exists(mod_list_entry.zip_path)
283+
):
284+
# If the mod directory doesn't exist,
285+
# the mod is no longer installed and can be removed from the mod list
286+
ModLoaderLog.debug(
287+
"Mod \"%s\" has been deleted from all user profiles as the corresponding zip file no longer exists at path \"%s\"."
288+
% [mod_id, mod_list_entry.zip_path],
289+
LOG_NAME,
290+
true
291+
)
292+
293+
updated_mod_list.erase(mod_id)
294+
continue
295+
276296
updated_mod_list[mod_id] = mod_list_entry
277297

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

321+
# Set the mods active state
301322
mod_list_entry.is_active = is_active
323+
324+
# Set the mods zip path if available
325+
if ModLoaderStore.mod_data.has(mod_id):
326+
mod_list_entry.zip_path = ModLoaderStore.mod_data[mod_id].zip_path
327+
302328
# Set the current_config if the mod has a config schema and is active
303329
if is_active and not ModLoaderConfig.get_config_schema(mod_id).empty():
304330
var current_config: ModConfig = ModLoaderStore.mod_data[mod_id].current_config
@@ -327,7 +353,10 @@ static func _set_mod_state(mod_id: String, profile_name: String, activate: bool)
327353
return false
328354

329355
# Handle mod state
356+
# Set state for user profile
330357
ModLoaderStore.user_profiles[profile_name].mod_list[mod_id].is_active = activate
358+
# Set state in the ModData
359+
ModLoaderStore.mod_data[mod_id].is_active = activate
331360

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

393-
# Set the current user profile to the one specified in the data
394-
var current_user_profile: ModUserProfile = ModUserProfile.new()
395-
current_user_profile.name = data.current_profile
396-
current_user_profile.mod_list = data.profiles[data.current_profile].mod_list
397-
ModLoaderStore.current_user_profile = current_user_profile
398-
399422
# Loop through each profile in the data and add them to ModLoaderStore
400423
for profile_name in data.profiles.keys():
401424
# Get the profile data from the JSON object
@@ -405,6 +428,9 @@ static func _load() -> bool:
405428
var new_profile := _create_new_profile(profile_name, profile_data.mod_list)
406429
ModLoaderStore.user_profiles[profile_name] = new_profile
407430

431+
# Set the current user profile to the one specified in the data
432+
ModLoaderStore.current_user_profile = ModLoaderStore.user_profiles[data.current_profile]
433+
408434
return true
409435

410436

addons/mod_loader/internal/file.gd

+38-10
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,21 @@ static func _get_json_string_as_dict(string: String) -> Dictionary:
4444

4545

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

5051
var mod_dir := Directory.new()
5152
var mod_dir_open_error := mod_dir.open(folder_path)
5253
if not mod_dir_open_error == OK:
5354
ModLoaderLog.error("Can't open mod folder %s (Error: %s)" % [folder_path, mod_dir_open_error], LOG_NAME)
54-
return -1
55+
return {}
5556
var mod_dir_listdir_error := mod_dir.list_dir_begin()
5657
if not mod_dir_listdir_error == OK:
5758
ModLoaderLog.error("Can't read mod folder %s (Error: %s)" % [folder_path, mod_dir_listdir_error], LOG_NAME)
58-
return -1
59+
return {}
60+
61+
5962

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

79-
var mod_folder_path := folder_path.plus_file(mod_zip_file_name)
80-
var mod_folder_global_path := ProjectSettings.globalize_path(mod_folder_path)
81-
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_folder_global_path, false)
82+
var mod_zip_path := folder_path.plus_file(mod_zip_file_name)
83+
var mod_zip_global_path := ProjectSettings.globalize_path(mod_zip_path)
84+
var is_mod_loaded_successfully := ProjectSettings.load_resource_pack(mod_zip_global_path, false)
85+
86+
# Get the current directories inside UNPACKED_DIR
87+
# This array is used to determine which directory is new
88+
var current_mod_dirs := _ModLoaderPath.get_dir_paths_in_dir(_ModLoaderPath.get_unpacked_mods_dir_path())
89+
90+
# Create a backup to reference when the next mod is loaded
91+
var current_mod_dirs_backup := current_mod_dirs.duplicate()
92+
93+
# Remove all directory paths that existed before, leaving only the one added last
94+
for previous_mod_dir in ModLoaderStore.previous_mod_dirs:
95+
current_mod_dirs.erase(previous_mod_dir)
96+
97+
# If the mod zip is not structured correctly, it may not be in the UNPACKED_DIR.
98+
if current_mod_dirs.empty():
99+
ModLoaderLog.fatal(
100+
"The mod zip at path \"%s\" does not have the correct file structure. For more information, please visit \"%s\"."
101+
% [mod_zip_global_path, URL_MOD_STRUCTURE_DOCS],
102+
LOG_NAME
103+
)
104+
continue
105+
106+
# The key is the mod_id of the latest loaded mod, and the value is the path to the zip file
107+
zip_data[current_mod_dirs[0].get_slice("/", 3)] = mod_zip_global_path
108+
109+
# Update previous_mod_dirs in ModLoaderStore to use for the next mod
110+
ModLoaderStore.previous_mod_dirs = current_mod_dirs_backup
82111

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

97-
ModLoaderLog.debug("Found mod ZIP: %s" % mod_folder_global_path, LOG_NAME)
126+
ModLoaderLog.debug("Found mod ZIP: %s" % mod_zip_global_path, LOG_NAME)
98127

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

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

109137
mod_dir.list_dir_end()
110138

111-
return temp_zipped_mods_count
139+
return zip_data
112140

113141

114142
# Save Data

addons/mod_loader/internal/mod_loader_utils.gd

+7-7
Original file line numberDiff line numberDiff line change
@@ -126,47 +126,47 @@ static func get_string_in_between(string: String, initial: String, ending: Strin
126126
# Stops the execution in editor
127127
# Always logged
128128
static func log_fatal(message: String, mod_name: String) -> void:
129-
ModLoaderDeprecated.deprecated_changed("ModLoader.log_fatal", "ModLoaderLog.fatal", "6.0.0")
129+
ModLoaderDeprecated.deprecated_changed("ModLoaderUtils.log_fatal", "ModLoaderLog.fatal", "6.0.0")
130130
ModLoaderLog.fatal(message, mod_name)
131131

132132

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

139139

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

146146

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

153153

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

160160

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

167167

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

0 commit comments

Comments
 (0)