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

Self setup #75

Merged
merged 2 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
98 changes: 98 additions & 0 deletions addons/mod_loader/mod_loader_setup.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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)


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_project_setting_true(project_setting: String) -> bool:
return ProjectSettings.has_setting(project_setting) and\
ProjectSettings.get_setting(project_setting)



53 changes: 53 additions & 0 deletions addons/mod_loader/mod_loader_utils.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.
Expand Down