Skip to content

Commit 6afe7ea

Browse files
authored
Merge branch 'develop' into improve_static_typing_and_style
2 parents 7ab5ff7 + 2d503e8 commit 6afe7ea

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

addons/mod_loader/mod_loader.gd

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,9 @@ func _init() -> void:
162162
# (UNPACKED_DIR)
163163
func _load_mod_zips() -> int:
164164
# Path to the games mod folder
165-
var game_mod_folder_path := ModLoaderUtils.get_local_folder_dir("mods")
165+
var game_mod_folder_path = ModLoaderUtils.get_local_folder_dir("mods")
166+
if not os_mods_path_override == "":
167+
game_mod_folder_path = os_mods_path_override
166168

167169
var dir := Directory.new()
168170
if not dir.open(game_mod_folder_path) == OK:

addons/mod_loader/mod_loader_setup.gd

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
extends SceneTree
2+
3+
const LOG_NAME := "ModLoader:Setup"
4+
5+
const settings := {
6+
"IS_LOADER_SETUP_APPLIED": "application/run/is_loader_setup_applied",
7+
"IS_LOADER_SET_UP": "application/run/is_loader_set_up",
8+
"MOD_LOADER_AUTOLOAD": "autoload/ModLoader",
9+
}
10+
11+
# see: [method ModLoaderUtils.register_global_classes_from_array]
12+
const new_global_classes := [
13+
{
14+
"base": "Resource",
15+
"class": "ModData",
16+
"language": "GDScript",
17+
"path": "res://addons/mod_loader/mod_data.gd"
18+
}, {
19+
"base": "Node",
20+
"class": "ModLoaderUtils",
21+
"language": "GDScript",
22+
"path": "res://addons/mod_loader/mod_loader_utils.gd"
23+
}, {
24+
"base": "Resource",
25+
"class": "ModManifest",
26+
"language": "GDScript",
27+
"path": "res://addons/mod_loader/mod_manifest.gd"
28+
}
29+
]
30+
31+
# IMPORTANT: use the ModLoaderUtils via this variable within this script!
32+
# Otherwise, script compilation will break on first load since the class is not defined.
33+
var modloaderutils: Node = load("res://addons/mod_loader/mod_loader_utils.gd").new()
34+
35+
36+
func _init() -> void:
37+
try_setup_modloader()
38+
change_scene(ProjectSettings.get_setting("application/run/main_scene"))
39+
40+
41+
# Set up the ModLoader, if it hasn't been set up yet
42+
func try_setup_modloader() -> void:
43+
# Avoid doubling the setup work
44+
if is_loader_setup_applied():
45+
modloaderutils.log_info("ModLoader is available, mods can be loaded!", LOG_NAME)
46+
OS.set_window_title("%s (Modded)" % ProjectSettings.get_setting("application/config/name"))
47+
return
48+
49+
setup_modloader()
50+
51+
# If the loader is set up, but the override is not applied yet,
52+
# prompt the user to quit and restart the game.
53+
if is_loader_set_up() and not is_loader_setup_applied():
54+
modloaderutils.log_info("ModLoader is set up, but the game needs to be restarted", LOG_NAME)
55+
OS.alert("The Godot ModLoader has been set up. Restart the game to apply the changes. Confirm to quit.")
56+
ProjectSettings.set_setting(settings.IS_LOADER_SETUP_APPLIED, true)
57+
ProjectSettings.save_custom(modloaderutils.get_override_path())
58+
quit()
59+
60+
61+
# Set up the ModLoader as an autoload and register the other global classes.
62+
# Saved as override.cfg besides the game executable to extend the existing project settings
63+
func setup_modloader() -> void:
64+
modloaderutils.log_info("Setting up ModLoader", LOG_NAME)
65+
66+
# Register all new helper classes as global
67+
modloaderutils.register_global_classes_from_array(new_global_classes)
68+
69+
# Add ModLoader autoload (the * marks the path as autoload)
70+
ProjectSettings.set_setting(settings.MOD_LOADER_AUTOLOAD, "*res://addons/mod_loader/mod_loader.gd")
71+
ProjectSettings.set_setting(settings.IS_LOADER_SET_UP, true)
72+
73+
# The game needs to be restarted first, bofore the loader is truly set up
74+
# Set this here and check it elsewhere to prompt the user for a restart
75+
ProjectSettings.set_setting(settings.IS_LOADER_SETUP_APPLIED, false)
76+
77+
ProjectSettings.save_custom(ModLoaderUtils.get_override_path())
78+
modloaderutils.log_info("ModLoader setup complete", LOG_NAME)
79+
80+
81+
func is_loader_set_up() -> bool:
82+
return is_project_setting_true(settings.IS_LOADER_SET_UP)
83+
84+
85+
func is_loader_setup_applied() -> bool:
86+
if not root.get_node_or_null("/root/ModLoader") == null:
87+
if not is_project_setting_true(settings.IS_LOADER_SETUP_APPLIED):
88+
modloaderutils.log_info("ModLoader is already set up. No self setup required.", LOG_NAME)
89+
return true
90+
return false
91+
92+
93+
static func is_project_setting_true(project_setting: String) -> bool:
94+
return ProjectSettings.has_setting(project_setting) and\
95+
ProjectSettings.get_setting(project_setting)
96+
97+
98+

addons/mod_loader/mod_loader_utils.gd

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,20 @@ static func get_local_folder_dir(subfolder: String = "") -> String:
178178
return game_install_directory.plus_file(subfolder)
179179

180180

181+
# Get the path where override.cfg will be stored.
182+
# Not the same as the local folder dir (for mac)
183+
static func get_override_path() -> String:
184+
var base_path := ""
185+
if OS.has_feature("editor"):
186+
base_path = ProjectSettings.globalize_path("res://")
187+
else:
188+
# this is technically different to res:// in macos, but we want the
189+
# executable dir anyway, so it is exactly what we need
190+
base_path = OS.get_executable_path().get_base_dir()
191+
192+
return base_path.plus_file("override.cfg")
193+
194+
181195
# Provide a path, get the file name at the end of the path
182196
static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String:
183197
var file_name := path.get_file()
@@ -223,6 +237,45 @@ static func get_json_string_as_dict(string: String) -> Dictionary:
223237
return parsed.result
224238

225239

240+
# Register an array of classes to the global scope, since Godot only does that in the editor.
241+
# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" }
242+
# You can find these easily in the project.godot file under "_global_script_classes"
243+
# (but you should only include classes belonging to your mod)
244+
static func register_global_classes_from_array(new_global_classes: Array) -> void:
245+
var registered_classes: Array = ProjectSettings.get_setting("_global_script_classes")
246+
var registered_class_icons: Dictionary = ProjectSettings.get_setting("_global_script_class_icons")
247+
248+
for new_class in new_global_classes:
249+
if not is_valid_global_class_dict(new_class):
250+
continue
251+
if registered_classes.has(new_class):
252+
continue
253+
254+
registered_classes.append(new_class)
255+
registered_class_icons[new_class.class] = "" # empty icon, does not matter
256+
257+
ProjectSettings.set_setting("_global_script_classes", registered_classes)
258+
ProjectSettings.set_setting("_global_script_class_icons", registered_class_icons)
259+
ProjectSettings.save_custom(get_override_path())
260+
261+
262+
# Checks if all required fields are in the given [Dictionary]
263+
# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" }
264+
static func is_valid_global_class_dict(global_class_dict: Dictionary) -> bool:
265+
var required_fields := ["base", "class", "language", "path"]
266+
if not global_class_dict.has_all(required_fields):
267+
log_fatal("Global class to be registered is missing one of %s" % required_fields, LOG_NAME)
268+
return false
269+
270+
var file = File.new()
271+
if not file.file_exists(global_class_dict.path):
272+
log_fatal('Class "%s" to be registered as global could not be found at given path "%s"' %
273+
[global_class_dict.class, global_class_dict.path], LOG_NAME)
274+
return false
275+
276+
return true
277+
278+
226279
# Get a flat array of all files in the target directory. This was needed in the
227280
# original version of this script, before becoming deprecated. It may still be
228281
# used if DEBUG_ENABLE_STORING_FILEPATHS is true.

0 commit comments

Comments
 (0)