From c9d5af4fe85676d8935746f44149e3ca77dcb5b0 Mon Sep 17 00:00:00 2001 From: Qubus0 Date: Mon, 23 Jan 2023 02:03:48 +0100 Subject: [PATCH 1/2] add simple loader self setup through script flag --- addons/mod_loader/mod_loader_setup.gd | 93 +++++++++++++++++++++++++++ addons/mod_loader/mod_loader_utils.gd | 53 +++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 addons/mod_loader/mod_loader_setup.gd diff --git a/addons/mod_loader/mod_loader_setup.gd b/addons/mod_loader/mod_loader_setup.gd new file mode 100644 index 00000000..de6ffa1f --- /dev/null +++ b/addons/mod_loader/mod_loader_setup.gd @@ -0,0 +1,93 @@ +extends SceneTree + +const LOG_NAME := "ModLoader:Setup" + +const settings := { + "IS_LOADER_SETUP_APPLIED": "application/run/is_loader_setup_applied", + "IS_LOADER_SET_UP": "application/run/is_loader_set_up", + "MOD_LOADER_AUTOLOAD": "autoload/ModLoader", +} + +# see: [method ModLoaderUtils.register_global_classes_from_array] +const new_global_classes := [ + { + "base": "Resource", + "class": "ModData", + "language": "GDScript", + "path": "res://addons/mod_loader/mod_data.gd" + }, { + "base": "Node", + "class": "ModLoaderUtils", + "language": "GDScript", + "path": "res://addons/mod_loader/mod_loader_utils.gd" + }, { + "base": "Resource", + "class": "ModManifest", + "language": "GDScript", + "path": "res://addons/mod_loader/mod_manifest.gd" + } +] + +# IMPORTANT: use the ModLoaderUtils via this variable within this script! +# Otherwise, script compilation will break on first load since the class is not defined. +var modloaderutils: Node = load("res://addons/mod_loader/mod_loader_utils.gd").new() + + +func _init() -> void: + try_setup_modloader() + change_scene(ProjectSettings.get_setting("application/run/main_scene")) + + +# Set up the ModLoader, if it hasn't been set up yet +func try_setup_modloader() -> void: + # Avoid doubling the setup work + if is_loader_setup_applied(): + modloaderutils.log_info("ModLoader is available, mods can be loaded!", LOG_NAME) + OS.set_window_title("%s (Modded)" % ProjectSettings.get_setting("application/config/name")) + return + + setup_modloader() + + # If the loader is set up, but the override is not applied yet, + # prompt the user to quit and restart the game. + if is_loader_set_up() and not is_loader_setup_applied(): + modloaderutils.log_info("ModLoader is set up, but the game needs to be restarted", LOG_NAME) + OS.alert("The Godot ModLoader has been set up. Restart the game to apply the changes. Confirm to quit.") + ProjectSettings.set_setting(settings.IS_LOADER_SETUP_APPLIED, true) + ProjectSettings.save_custom(modloaderutils.get_override_path()) + quit() + + +# Set up the ModLoader as an autoload and register the other global classes. +# Saved as override.cfg besides the game executable to extend the existing project settings +func setup_modloader() -> void: + modloaderutils.log_info("Setting up ModLoader", LOG_NAME) + + # Register all new helper classes as global + modloaderutils.register_global_classes_from_array(new_global_classes) + + # Add ModLoader autoload (the * marks the path as autoload) + ProjectSettings.set_setting(settings.MOD_LOADER_AUTOLOAD, "*res://addons/mod_loader/mod_loader.gd") + ProjectSettings.set_setting(settings.IS_LOADER_SET_UP, true) + + # The game needs to be restarted first, bofore the loader is truly set up + # Set this here and check it elsewhere to prompt the user for a restart + ProjectSettings.set_setting(settings.IS_LOADER_SETUP_APPLIED, false) + + ProjectSettings.save_custom(ModLoaderUtils.get_override_path()) + modloaderutils.log_info("ModLoader setup complete", LOG_NAME) + + +static func is_project_setting_true(project_setting: String) -> bool: + return ProjectSettings.has_setting(project_setting) and\ + ProjectSettings.get_setting(project_setting) + + +static func is_loader_set_up() -> bool: + return is_project_setting_true(settings.IS_LOADER_SET_UP) + + +static func is_loader_setup_applied() -> bool: + return is_project_setting_true(settings.IS_LOADER_SETUP_APPLIED) + + diff --git a/addons/mod_loader/mod_loader_utils.gd b/addons/mod_loader/mod_loader_utils.gd index 07fe75e0..45c30e10 100644 --- a/addons/mod_loader/mod_loader_utils.gd +++ b/addons/mod_loader/mod_loader_utils.gd @@ -178,6 +178,20 @@ static func get_local_folder_dir(subfolder: String = "") -> String: return game_install_directory.plus_file(subfolder) +# Get the path where override.cfg will be stored. +# Not the same as the local folder dir (for mac) +static func get_override_path() -> String: + var base_path := "" + if OS.has_feature("editor"): + base_path = ProjectSettings.globalize_path("res://") + else: + # this is technically different to res:// in macos, but we want the + # executable dir anyway, so it is exactly what we need + base_path = OS.get_executable_path().get_base_dir() + + return base_path.plus_file("override.cfg") + + # Provide a path, get the file name at the end of the path static func get_file_name_from_path(path: String, make_lower_case := true, remove_extension := false) -> String: var file_name := path.get_file() @@ -223,6 +237,45 @@ static func get_json_string_as_dict(string: String) -> Dictionary: return parsed.result +# Register an array of classes to the global scope, since Godot only does that in the editor. +# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" } +# You can find these easily in the project.godot file under "_global_script_classes" +# (but you should only include classes belonging to your mod) +static func register_global_classes_from_array(new_global_classes: Array) -> void: + var registered_classes: Array = ProjectSettings.get_setting("_global_script_classes") + var registered_class_icons: Dictionary = ProjectSettings.get_setting("_global_script_class_icons") + + for new_class in new_global_classes: + if not is_valid_global_class_dict(new_class): + continue + if registered_classes.has(new_class): + continue + + registered_classes.append(new_class) + registered_class_icons[new_class.class] = "" # empty icon, does not matter + + ProjectSettings.set_setting("_global_script_classes", registered_classes) + ProjectSettings.set_setting("_global_script_class_icons", registered_class_icons) + ProjectSettings.save_custom(get_override_path()) + + +# Checks if all required fields are in the given [Dictionary] +# Format: { "base": "ParentClass", "class": "ClassName", "language": "GDScript", "path": "res://path/class_name.gd" } +static func is_valid_global_class_dict(global_class_dict: Dictionary) -> bool: + var required_fields := ["base", "class", "language", "path"] + if not global_class_dict.has_all(required_fields): + log_fatal("Global class to be registered is missing one of %s" % required_fields, LOG_NAME) + return false + + var file = File.new() + if not file.file_exists(global_class_dict.path): + log_fatal('Class "%s" to be registered as global could not be found at given path "%s"' % + [global_class_dict.class, global_class_dict.path], LOG_NAME) + return false + + return true + + # Get a flat array of all files in the target directory. This was needed in the # original version of this script, before becoming deprecated. It may still be # used if DEBUG_ENABLE_STORING_FILEPATHS is true. From 11db0127501215706f494664fbb1ebac1950bd0d Mon Sep 17 00:00:00 2001 From: Qubus0 Date: Mon, 23 Jan 2023 02:20:49 +0100 Subject: [PATCH 2/2] don't attempt setup if the autoload already exists --- addons/mod_loader/mod_loader_setup.gd | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/addons/mod_loader/mod_loader_setup.gd b/addons/mod_loader/mod_loader_setup.gd index de6ffa1f..a23a21f6 100644 --- a/addons/mod_loader/mod_loader_setup.gd +++ b/addons/mod_loader/mod_loader_setup.gd @@ -78,16 +78,21 @@ func setup_modloader() -> void: modloaderutils.log_info("ModLoader setup complete", LOG_NAME) -static func is_project_setting_true(project_setting: String) -> bool: - return ProjectSettings.has_setting(project_setting) and\ - ProjectSettings.get_setting(project_setting) +func is_loader_set_up() -> bool: + return is_project_setting_true(settings.IS_LOADER_SET_UP) -static func is_loader_set_up() -> bool: - return is_project_setting_true(settings.IS_LOADER_SET_UP) +func is_loader_setup_applied() -> bool: + if not root.get_node_or_null("/root/ModLoader") == null: + if not is_project_setting_true(settings.IS_LOADER_SETUP_APPLIED): + modloaderutils.log_info("ModLoader is already set up. No self setup required.", LOG_NAME) + return true + return false -static func is_loader_setup_applied() -> bool: - return is_project_setting_true(settings.IS_LOADER_SETUP_APPLIED) +static func is_project_setting_true(project_setting: String) -> bool: + return ProjectSettings.has_setting(project_setting) and\ + ProjectSettings.get_setting(project_setting) +