-
Notifications
You must be signed in to change notification settings - Fork 37
Project binary injection [old version] #81
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
Changes from all commits
7e01c48
3befddc
cca3415
27c7d4c
ee51c3c
1eb0520
65fa5c9
f5c2bc6
20b0a90
7a8c627
fe936cf
4de915d
b1d993c
614691d
8cc7596
96ab3fa
4c6901a
71c3b4f
453496c
b9fc0bc
5b8f46c
c81a828
1e120e7
45fa127
b327488
1906ca5
90223a1
d756abb
9354b10
e636796
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,67 +32,227 @@ const new_global_classes := [ | |
# 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() | ||
|
||
var info_label := Label.new() | ||
var restart_timer := Timer.new() | ||
|
||
func _init() -> void: | ||
try_setup_modloader() | ||
change_scene(ProjectSettings.get_setting("application/run/main_scene")) | ||
var path := {} | ||
var file_name := {} | ||
var is_only_setup: bool = modloaderutils.is_running_with_command_line_arg("--only-setup") | ||
var is_unattended : bool = modloaderutils.is_running_with_command_line_arg("--unattended") | ||
var is_silent : bool = modloaderutils.is_running_with_command_line_arg("--silent") | ||
var is_setup_create_override_cfg : bool = modloaderutils.is_running_with_command_line_arg("--setup-create-override-cfg") | ||
|
||
|
||
# Set up the ModLoader, if it hasn't been set up yet | ||
func try_setup_modloader() -> void: | ||
func _init() -> void: | ||
modloaderutils.log_debug("ModLoader setup initialized", LOG_NAME) | ||
|
||
# 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")) | ||
# Checks if the ModLoader Node is in the root of the scene tree | ||
# and if the IS_LOADER_SETUP_APPLIED project setting is there | ||
if get_mod_loader_position_index() == 0: | ||
modded_start() | ||
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() | ||
|
||
func _iteration(_delta): | ||
# If the restart timer is started update the label to show that the game will be restarted | ||
if !restart_timer.is_stopped(): | ||
info_label.text = "Mod Loader is installing - restarting in %s" % int(restart_timer.time_left) | ||
|
||
|
||
func modded_start() -> void: | ||
modloaderutils.log_info("ModLoader is available, mods can be loaded!", LOG_NAME) | ||
|
||
OS.set_window_title("%s (Modded)" % ProjectSettings.get_setting("application/config/name")) | ||
|
||
# For unknown reasons the icon gets reset to the default Godot icon when using the --script cli arg. | ||
# To fix this the icon defined in the ProjectSettings is set via OS.set_icon() | ||
var icon := Image.new() | ||
var _error_load_icon := icon.load(ProjectSettings.get_setting("application/config/icon")) | ||
OS.set_icon(icon) | ||
|
||
var _error_change_scene_main := change_scene(ProjectSettings.get_setting("application/run/main_scene")) | ||
|
||
|
||
# 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 | ||
# Set up the ModLoader, if it hasn't been set up yet | ||
func setup_modloader() -> void: | ||
modloaderutils.log_info("Setting up ModLoader", LOG_NAME) | ||
|
||
# Add info label and restart timer to the scene tree | ||
if is_unattended: | ||
setup_ui() | ||
|
||
setup_file_data() | ||
|
||
# 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) | ||
# Adds the ModLoader autoload at the top | ||
# Only works if the pck file is not embedded in the .exe | ||
# In that case create_override_cfg() is used and inject_project_binary() is skipped | ||
reorder_autoloads() | ||
|
||
# If a dedicated .pck file exists, we can inject the custom project.binary | ||
# If the --setup-create-override-cfg cli arg is passed always use the override.cfg | ||
if modloaderutils.file_exists(path.pck) and not is_setup_create_override_cfg: | ||
modloaderutils.log_debug("injecting the project.binary file", LOG_NAME) | ||
create_project_binary() | ||
inject_project_binary() | ||
clean_up_project_binary_file() | ||
# If no dedicated .pck file exists,it's most likely embedded into the .exe. | ||
# In that case we add a override.cfg file to the base game dir. | ||
else: | ||
modloaderutils.log_debug("using the override.cfg file", LOG_NAME) | ||
create_override_cfg() | ||
|
||
# ModLoader is set up. A game restart is required to apply the ProjectSettings. | ||
modloaderutils.log_info("ModLoader is set up, the game will be restarted", LOG_NAME) | ||
|
||
match true: | ||
# If the --only-setup cli argument is passed, quit with exit code 0 | ||
is_only_setup: | ||
quit(0) | ||
# If the --unattended cli argument is passed, show restart timer and auto restart | ||
is_unattended: | ||
restart_timer.start(4) | ||
# If the --silent cli argument is passed, restart immediately | ||
is_silent: | ||
restart_game() | ||
# If no cli argument is passed, show message with OS.alert() and user has to restart the game | ||
_: | ||
OS.alert("The Godot ModLoader has been set up. Restart the game to apply the changes. Confirm to quit.") | ||
quit() | ||
|
||
|
||
func get_mod_loader_position_index() -> int: | ||
# Log Autoload order | ||
var autoloads := {} | ||
for prop in ProjectSettings.get_property_list(): | ||
var name: String = prop.name | ||
if name.begins_with("autoload/"): | ||
var value: String = ProjectSettings.get_setting(name) | ||
autoloads[name] = value | ||
|
||
var autoload_keys = autoloads.keys() | ||
var mod_loader_index = autoload_keys.find("autoload/ModLoader") | ||
|
||
# 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) | ||
if(mod_loader_index != 0): | ||
modloaderutils.log_debug_json_print("Autoload order", autoloads, LOG_NAME) | ||
|
||
ProjectSettings.save_custom(ModLoaderUtils.get_override_path()) | ||
modloaderutils.log_info("ModLoader setup complete", LOG_NAME) | ||
return mod_loader_index | ||
|
||
|
||
func is_loader_set_up() -> bool: | ||
return is_project_setting_true(settings.IS_LOADER_SET_UP) | ||
# Saves the project settings to a project.binary file inside the addons/mod_loader/ directory. | ||
func create_project_binary() -> void: | ||
var _error_save_custom_project_binary = ProjectSettings.save_custom(path.game_base_dir + "addons/mod_loader/project.binary") | ||
|
||
|
||
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 | ||
# Saves the project settings to a overrides.cfg file inside the games base directory. | ||
func create_override_cfg() -> void: | ||
modloaderutils.log_debug("create_override_cfg func called", LOG_NAME) | ||
var _error_save_custom_override = ProjectSettings.save_custom(modloaderutils.get_override_path()) | ||
|
||
|
||
# Reorders the autoloads in the project settings, to get the ModLoader on top. | ||
func reorder_autoloads() -> void: | ||
# remove and re-add autoloads | ||
var original_autoloads := {} | ||
for prop in ProjectSettings.get_property_list(): | ||
var name: String = prop.name | ||
if name.begins_with("autoload/"): | ||
var value: String = ProjectSettings.get_setting(name) | ||
original_autoloads[name] = value | ||
|
||
for autoload in original_autoloads.keys(): | ||
ProjectSettings.set_setting(autoload, null) | ||
|
||
# add ModLoader autoload (the * marks the path as autoload) | ||
ProjectSettings.set_setting("autoload/ModLoader", "*" + "res://addons/mod_loader/mod_loader.gd") | ||
|
||
# add all previous autoloads back again | ||
for autoload in original_autoloads.keys(): | ||
ProjectSettings.set_setting(autoload, original_autoloads[autoload]) | ||
|
||
|
||
# Add modified binary to the pck | ||
func inject_project_binary() -> void: | ||
var output_add_project_binary := [] | ||
var _exit_code_add_project_binary := OS.execute(path.pck_tool, ["--pack", path.pck, "--action", "add", "--file", path.project_binary, "--remove-prefix", path.mod_loader_dir], true, output_add_project_binary) | ||
modloaderutils.log_debug_json_print("Adding custom project.binaray to res://", output_add_project_binary, LOG_NAME) | ||
|
||
|
||
# Removes the project.binary file | ||
func clean_up_project_binary_file() -> void: | ||
var dir = Directory.new() | ||
dir.remove(path.project_binary) | ||
|
||
KANAjetzt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
func setup_file_data() -> void: | ||
# C:/path/to/game/game.exe | ||
path.exe = OS.get_executable_path() | ||
# C:/path/to/game/ | ||
path.game_base_dir = modloaderutils.get_local_folder_dir() | ||
# C:/path/to/game/addons/mod_loader | ||
path.mod_loader_dir = path.game_base_dir + "addons/mod_loader/" | ||
# C:/path/to/game/addons/mod_loader/vendor/godotpcktool/godotpcktool.exe | ||
path.pck_tool = path.mod_loader_dir + "vendor/godotpcktool/godotpcktool.exe" | ||
# can be supplied to override the exe_name | ||
file_name.cli_arg_exe = modloaderutils.get_cmd_line_arg_value("--exe-name") | ||
# can be supplied to override the pck_name | ||
file_name.cli_arg_pck = modloaderutils.get_cmd_line_arg_value("--pck-name") | ||
# game - or use the value of cli_arg_exe_name if there is one | ||
file_name.exe = modloaderutils.get_file_name_from_path(path.exe, true, true) if file_name.cli_arg_exe == '' else file_name.cli_arg_exe | ||
# game - or use the value of cli_arg_pck_name if there is one | ||
# using exe_path.get_file() instead of exe_name | ||
# so you don't override the pck_name with the --exe-name cli arg | ||
# the main pack name is the same as the .exe name | ||
# if --main-pack cli arg is not set | ||
file_name.pck = modloaderutils.get_file_name_from_path(path.exe, true, true) if file_name.cli_arg_pck == '' else file_name.cli_arg_pck | ||
# C:/path/to/game/game.pck | ||
path.pck = path.game_base_dir.plus_file(file_name.pck + '.pck') | ||
# C:/path/to/game/addons/mod_loader/project.binary | ||
path.project_binary = path.mod_loader_dir + "project.binary" | ||
|
||
modloaderutils.log_debug_json_print("path: ", path, LOG_NAME) | ||
modloaderutils.log_debug_json_print("file_name: ", file_name, LOG_NAME) | ||
|
||
|
||
func setup_ui() -> void: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. are we sure this will always be on top of everything? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe make the GUI stuff optional via a CLI arg? Tbh though I think this GUI feature would be better in a separate subsequent PR, as this PR is focused on patching the binary. Best to keep things small and contained IMO, makes it easier to review, keeps discussions focused, and gets stuff merged faster |
||
# setup UI for installation visualization | ||
info_label.anchor_left = 0.5 | ||
info_label.anchor_top = 0.5 | ||
info_label.anchor_right = 0.5 | ||
info_label.anchor_bottom = 0.5 | ||
info_label.margin_left = -305.0 | ||
info_label.margin_top = -13.0 | ||
info_label.margin_right = 28.0 | ||
info_label.margin_bottom = 1.0 | ||
info_label.grow_horizontal = Label.GROW_DIRECTION_BOTH | ||
info_label.rect_scale = Vector2( 2, 2 ) | ||
info_label.align = Label.ALIGN_CENTER | ||
info_label.valign = Label.ALIGN_CENTER | ||
info_label.text = "Mod Loader is installing - please wait.." | ||
root.add_child(info_label) | ||
|
||
restart_timer.one_shot = true | ||
root.add_child(restart_timer) | ||
var _error_connect_restart_timer_timeout = restart_timer.connect("timeout", self, "_on_restart_timer_timeout") | ||
|
||
|
||
func restart_game() -> void: | ||
# run the game again to apply the changed project settings | ||
var _exit_code_game_start = OS.execute(path.exe, ["--script", path.mod_loader_dir + "mod_loader_setup.gd", "--log-debug"], false) | ||
# quit the current execution | ||
quit() | ||
|
||
|
||
static func is_project_setting_true(project_setting: String) -> bool: | ||
return ProjectSettings.has_setting(project_setting) and\ | ||
ProjectSettings.get_setting(project_setting) | ||
|
||
|
||
|
||
# restart the game | ||
func _on_restart_timer_timeout() -> void: | ||
restart_game() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2020-2022 Henri Hyyryläinen | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will trigger if either
is_setup_create_override_cfg
is true, or the PCK file doesn't exist. Is this intentional? Or shouldn't it only trigger ifis_setup_create_override_cfg
is true?It's a nice idea to have things happen automatically, but I think it's generally better for things to happen predictably -- eg. if a CLI arg exists to perform an action, that action should only be performed if you use that CLI arg.
This would also let you perform both actions, if (for whatever reason) you might want to. Atm this code only lets you create the override.cfg if the PCK file doesn't exist, but I don't think we need to add this extra level of conditional complexity. Best to just let users interact with the API without such hidden conditions.
I might be wrong here so I'm open to discussion