Skip to content
Open
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
142 changes: 141 additions & 1 deletion addons/mod_loader/internal/mod_hook_packer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ extends RefCounted

const LOG_NAME := "ModLoader:ModHookPacker"

static var ModLoaderSetup: Object = load("res://addons/mod_loader/mod_loader_setup.gd")
static var ModLoaderSetupLog: Object = load("res://addons/mod_loader/setup/setup_log.gd")
static var ModLoaderSetupUtils: Object = load("res://addons/mod_loader/setup/setup_utils.gd")

static func start() -> void:
_decompile_and_load_if_needed()

ModLoaderLog.info("Generating mod hooks .zip", LOG_NAME)
var hook_pre_processor = _ModLoaderModHookPreProcessor.new()
hook_pre_processor.process_begin()
Expand Down Expand Up @@ -59,10 +64,19 @@ static func start() -> void:
ModLoaderLog.debug("No new hooks were created in \"%s\", skipping writing to hook pack." % path, LOG_NAME)
continue

zip_writer.start_file(path.trim_prefix("res://"))
var script_path: String = path.trim_prefix("res://")
zip_writer.start_file(script_path)
zip_writer.write_file(processed_source_code.to_utf8_buffer())
zip_writer.close_file()

if FileAccess.file_exists(path + ".remap"):
var remap_file = FileAccess.open(path + ".remap", FileAccess.READ)
var remap_content = remap_file.get_as_text()
remap_file.close()
zip_writer.start_file(script_path + ".remap")
zip_writer.write_file(remap_content.to_utf8_buffer())
zip_writer.close_file()

ModLoaderLog.debug("Hooks created for script: %s" % path, LOG_NAME)
new_hooks_created = true

Expand All @@ -72,3 +86,129 @@ static func start() -> void:
ModLoader.new_hooks_created.emit()

zip_writer.close()


static func _decompile_and_load_if_needed() -> bool:
var hooked_scripts := ModLoaderStore.hooked_script_paths
var scripts_to_decompile: Array[String] = []

for path: String in hooked_scripts.keys():
# if actual .gd script doesn't exist (which means it wasn't decompiled or hooked already), and there's a .gdc file instead
if path.ends_with(".gd") and not FileAccess.file_exists(path) and FileAccess.file_exists(path + "c"):
scripts_to_decompile.append(path)

if not scripts_to_decompile:
return false

ModLoaderLog.info("Decompiling scripts before hooking: %s" % [scripts_to_decompile], LOG_NAME)


# C:/path/to/game/game.exe
var exe_path := OS.get_executable_path()
# C:/path/to/game/
var game_base_dir: String = ModLoaderSetupUtils.get_local_folder_dir()
# C:/path/to/game/addons/mod_loader
var mod_loader_dir: String = game_base_dir + "addons/mod_loader/"
var gdre_path: String = mod_loader_dir + ModLoaderSetup.get_gdre_path()
var decomp_dir := mod_loader_dir + "decomp/"
var zip_path := decomp_dir + "inject.zip"

if not DirAccess.dir_exists_absolute(decomp_dir):
DirAccess.make_dir_recursive_absolute(decomp_dir)

# can be supplied to override the exe_name
var cli_arg_exe: String = ModLoaderSetupUtils.get_cmd_line_arg_value("--exe-name")
# can be supplied to override the pck_name
var cli_arg_pck: String = ModLoaderSetupUtils.get_cmd_line_arg_value("--pck-name")
# game - or use the value of cli_arg_exe_name if there is one
var exe: String = (
ModLoaderSetupUtils.get_file_name_from_path(exe_path, false, true)
if cli_arg_exe == ""
else 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
var pck: String = exe if cli_arg_pck == "" else cli_arg_pck

# C:/path/to/game/game.pck
var pck_path := game_base_dir.path_join(pck + ".pck")

var is_embedded: bool = not FileAccess.file_exists(pck_path)
var injection_path: String = exe_path if is_embedded else pck_path


# Decompile scripts
var arguments := []
var output := []
arguments.push_back("--headless")
arguments.push_back("--recover=%s" % injection_path)
for script_to_decompile in scripts_to_decompile:
var compressed_script_path := script_to_decompile + "c"
arguments.push_back("--include=%s" % compressed_script_path)
arguments.push_back("--output=%s" % decomp_dir)
ModLoaderSetupLog.debug("Script recovery started: %s %s" % [gdre_path, arguments], LOG_NAME)
var _exit_code_recover := OS.execute(gdre_path, arguments, output)
ModLoaderSetupLog.debug("Script recovery: %s" % output, LOG_NAME)

# Extract remap files
arguments.clear()
output.clear()
arguments.push_back("--headless")
arguments.push_back("--extract=%s" % injection_path)
for script_to_decompile in scripts_to_decompile:
var remap_path := script_to_decompile + ".remap"
arguments.push_back("--include=%s" % remap_path)
arguments.push_back("--output=%s" % decomp_dir)
ModLoaderSetupLog.debug("Remap extract started: %s %s" % [gdre_path, arguments], LOG_NAME)
var _exit_code_extract := OS.execute(gdre_path, arguments, output)
ModLoaderSetupLog.debug("Remap extract: %s" % output, LOG_NAME)


# Create inject zip
var zip_writer := ZIPPacker.new()
var error: Error

error = zip_writer.open(zip_path, ZIPPacker.APPEND_CREATE)
if not error == OK:
ModLoaderLog.error("Error (%s) writing to inject zip, consider deleting this file: %s" % [error, zip_path], LOG_NAME)
DirAccess.remove_absolute(decomp_dir) # Clean up
return false

for script_to_decompile in scripts_to_decompile:
var script_path := script_to_decompile.trim_prefix("res://")
var file_path := decomp_dir + script_path

# Script
var script_file := FileAccess.open(file_path, FileAccess.READ)
var script_content := script_file.get_as_text()
script_file.close()
zip_writer.start_file(script_path)
zip_writer.write_file(script_content.to_utf8_buffer())
zip_writer.close_file()

# Remap
var remap_path := file_path + ".remap"
if FileAccess.file_exists(remap_path):
var remap_file := FileAccess.open(remap_path, FileAccess.READ)
var remap_content = remap_file.get_as_text().replace(".gdc", ".gd")
remap_file.close()
zip_writer.start_file(script_path + ".remap")
zip_writer.write_file(remap_content.to_utf8_buffer())
zip_writer.close_file()

zip_writer.close()

# Inject decompiled scripts and remaps
ModLoaderLog.info("Injecting decompiled scripts before hooking: %s" % [scripts_to_decompile], LOG_NAME)
var inject_success := ProjectSettings.load_resource_pack(zip_path) # TODO this returns true, but seems not to do anything ;d

if not inject_success:
ModLoaderLog.error("Error injecting decompiled scripts: %s" % [zip_path], LOG_NAME)

# Clean up
DirAccess.remove_absolute(decomp_dir)

return inject_success
3 changes: 2 additions & 1 deletion addons/mod_loader/internal/mod_hook_preprocessor.gd
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ func process_script_verbose(path: String, enable_hook_check := false, method_mas
## [param enable_hook_check]: Adds a check that _ModLoaderHooks.any_mod_hooked is [code]true[/code] to the processed method, reducing hash checks.[br]
## [param method_mask]: If provided, only methods in this [Array] will be processed.[br]
func process_script(path: String, enable_hook_check := false, method_mask: Array[String] = []) -> String:
var current_script := load(path) as GDScript
# Ignoring cache in case decompiled scripts were injected
var current_script := ResourceLoader.load(path, "", ResourceLoader.CacheMode.CACHE_MODE_IGNORE) as GDScript
var source_code := current_script.source_code
var source_code_additions := ""

Expand Down
2 changes: 1 addition & 1 deletion addons/mod_loader/mod_loader_setup.gd
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ func get_combined_global_script_class_cache() -> ConfigFile:
return global_script_class_cache_combined


func get_gdre_path() -> String:
static func get_gdre_path() -> String:
if OS.get_name() == "Windows":
return "vendor/GDRE/gdre_tools.exe"

Expand Down