Skip to content

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

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7e01c48
✅added project.binary injection with gdpck-tool
KANAjetzt Jan 26, 2023
3befddc
✔ no longer injecting mod loader scripts
KANAjetzt Jan 26, 2023
cca3415
✔ added autoload order debug log
KANAjetzt Jan 27, 2023
27c7d4c
✔ added game execution before quitting setup
KANAjetzt Jan 27, 2023
ee51c3c
🧹 clean up path logging
KANAjetzt Jan 28, 2023
1eb0520
🔨 add vendor folder
KANAjetzt Jan 28, 2023
65fa5c9
✔ added --exe-name cli arg
KANAjetzt Jan 28, 2023
f5c2bc6
🔨 removed the backup process
KANAjetzt Jan 28, 2023
20b0a90
🧹 remove the autoload order logging
KANAjetzt Jan 28, 2023
7a8c627
✔ added restart notification
KANAjetzt Jan 29, 2023
fe936cf
🧹 move project.binary injection to dedicated func
KANAjetzt Jan 29, 2023
4de915d
🧹 moved project binary creation in dedicated func
KANAjetzt Jan 29, 2023
b1d993c
🔨 fixed linting errors and typos
KANAjetzt Jan 29, 2023
614691d
✔ added --silent argument
KANAjetzt Jan 29, 2023
8cc7596
🛠 WIP: --setup-create-override-cfg cli arg
KANAjetzt Jan 29, 2023
96ab3fa
🔨 added missing empty lines
KANAjetzt Jan 29, 2023
4c6901a
✔ create mods dir if not there
KANAjetzt Jan 29, 2023
71c3b4f
🔨 fix icon getting lost when using --script
KANAjetzt Jan 29, 2023
453496c
🧹 moved unnecessary if statements
KANAjetzt Jan 29, 2023
b9fc0bc
👀 accidentally pushed the project.binary file
KANAjetzt Jan 29, 2023
5b8f46c
✔ added project.binary file cleanup
KANAjetzt Jan 29, 2023
c81a828
✔ added --unattended cmd arg
KANAjetzt Feb 3, 2023
1e120e7
🔨 only setup UI if --unattended
KANAjetzt Feb 3, 2023
45fa127
✔ added cli arg --only-setup
KANAjetzt Feb 3, 2023
b327488
🔨 added comment why OS.set_icon() is required
KANAjetzt Feb 3, 2023
1906ca5
🧹 removed auto creation of mods dir -> see #34
KANAjetzt Feb 3, 2023
90223a1
🔨 fixed comments on restart handling
KANAjetzt Feb 3, 2023
d756abb
👀 fixed spacing
KANAjetzt Feb 3, 2023
9354b10
✔ added ModLoader autoload index check
KANAjetzt Feb 3, 2023
e636796
🧹 added modded_start() func
KANAjetzt Feb 3, 2023
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
230 changes: 195 additions & 35 deletions addons/mod_loader/mod_loader_setup.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Collaborator

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 if is_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


# 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)


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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are we sure this will always be on top of everything?
if for example some autoload has stuff that visualizes a game loading bar or something along those lines

Copy link
Collaborator

Choose a reason for hiding this comment

The 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()
21 changes: 21 additions & 0 deletions addons/mod_loader/vendor/godotpcktool/LICENSE
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.
Binary file not shown.