11extends SceneTree
22
3-
43const LOG_NAME := "ModLoader:Setup"
54
5+ const settings := {
6+ "IS_LOADER_SETUP_APPLIED" : "application/run/is_loader_setup_applied" ,
7+ "IS_LOADER_SET_UP" : "application/run/is_loader_set_up" ,
8+ "MOD_LOADER_AUTOLOAD" : "autoload/ModLoader" ,
9+ }
10+
611# IMPORTANT: use the ModLoaderLog via this variable within this script!
712# Otherwise, script compilation will break on first load since the class is not defined.
813var ModLoaderSetupLog : Object = load ("res://addons/mod_loader/setup/setup_log.gd" )
914var ModLoaderSetupUtils : Object = load ("res://addons/mod_loader/setup/setup_utils.gd" )
1015
16+ var path := {}
17+ var file_name := {}
18+ var is_only_setup : bool = ModLoaderSetupUtils .is_running_with_command_line_arg ("--only-setup" )
1119var is_setup_create_override_cfg : bool = ModLoaderSetupUtils .is_running_with_command_line_arg (
1220 "--setup-create-override-cfg"
1321)
@@ -31,7 +39,7 @@ func _init() -> void:
3139 modded_start ()
3240 return
3341
34- change_scene_to_file ( "res://addons/mod_loader/setup/setup.tscn" )
42+ setup_modloader ( )
3543
3644
3745# ModLoader already setup - switch to the main scene
@@ -41,3 +49,290 @@ func modded_start() -> void:
4149 root .set_title ("%s (Modded)" % ProjectSettings .get_setting ("application/config/name" ))
4250
4351 change_scene_to_file .call_deferred (ProjectSettings .get_setting ("application/run/main_scene" ))
52+
53+
54+ # Set up the ModLoader as an autoload and register the other global classes.
55+ func setup_modloader () -> void :
56+ ModLoaderSetupLog .info ("Setting up ModLoader" , LOG_NAME )
57+
58+ # Setup path and file_name dict with all required paths and file names.
59+ setup_file_data ()
60+
61+ # Add ModLoader autoload (the * marks the path as autoload)
62+ reorder_autoloads ()
63+ ProjectSettings .set_setting (settings .IS_LOADER_SET_UP , true )
64+
65+ # The game needs to be restarted first, before the loader is truly set up
66+ # Set this here and check it elsewhere to prompt the user for a restart
67+ ProjectSettings .set_setting (settings .IS_LOADER_SETUP_APPLIED , false )
68+
69+ if is_setup_create_override_cfg :
70+ handle_override_cfg ()
71+ else :
72+ handle_injection ()
73+
74+ # ModLoader is set up. A game restart is required to apply the ProjectSettings.
75+ ModLoaderSetupLog .info ("ModLoader is set up, a game restart is required." , LOG_NAME )
76+
77+ match true :
78+ # If the --only-setup cli argument is passed, quit with exit code 0
79+ is_only_setup :
80+ quit (0 )
81+ # If no cli argument is passed, show message with OS.alert() and user has to restart the game
82+ _ :
83+ OS .alert (
84+ "The Godot ModLoader has been set up. The game needs to be restarted to apply the changes. Confirm to restart."
85+ )
86+ restart ()
87+
88+
89+ # Reorders the autoloads in the project settings, to get the ModLoader on top.
90+ func reorder_autoloads () -> void :
91+ # remove and re-add autoloads
92+ var original_autoloads := {}
93+ for prop in ProjectSettings .get_property_list ():
94+ var name : String = prop .name
95+ if name .begins_with ("autoload/" ):
96+ var value : String = ProjectSettings .get_setting (name )
97+ original_autoloads [name ] = value
98+
99+ ModLoaderSetupLog .info (
100+ "Start reorder autoloads current state: %s " % JSON .stringify (original_autoloads , "\t " ),
101+ LOG_NAME
102+ )
103+
104+ for autoload in original_autoloads .keys ():
105+ ProjectSettings .set_setting (autoload , null )
106+
107+ # Add ModLoaderStore autoload (the * marks the path as autoload)
108+ ProjectSettings .set_setting (
109+ "autoload/ModLoaderStore" , "*" + "res://addons/mod_loader/mod_loader_store.gd"
110+ )
111+
112+ # Add ModLoader autoload (the * marks the path as autoload)
113+ ProjectSettings .set_setting ("autoload/ModLoader" , "*" + "res://addons/mod_loader/mod_loader.gd" )
114+
115+ # add all previous autoloads back again
116+ for autoload in original_autoloads .keys ():
117+ ProjectSettings .set_setting (autoload , original_autoloads [autoload ])
118+
119+ var new_autoloads := {}
120+ for prop in ProjectSettings .get_property_list ():
121+ var name : String = prop .name
122+ if name .begins_with ("autoload/" ):
123+ var value : String = ProjectSettings .get_setting (name )
124+ new_autoloads [name ] = value
125+
126+ ModLoaderSetupLog .info (
127+ "Reorder autoloads completed - new state: %s " % JSON .stringify (new_autoloads , "\t " ),
128+ LOG_NAME
129+ )
130+
131+
132+ # Saves the ProjectSettings to a override.cfg file in the base game directory.
133+ func handle_override_cfg () -> void :
134+ ModLoaderSetupLog .debug ("using the override.cfg file" , LOG_NAME )
135+
136+ # Make the '.godot' dir public as 'godot' and copy all files to the public dir.
137+ make_project_data_public ()
138+
139+ # Combine mod_loader and game global classes
140+ var global_script_class_cache_combined := get_combined_global_script_class_cache ()
141+ global_script_class_cache_combined .save ("res://godot/global_script_class_cache.cfg" )
142+
143+ var _save_custom_error : int = ProjectSettings .save_custom (
144+ ModLoaderSetupUtils .get_override_path ()
145+ )
146+
147+
148+ # Creates the project.binary file, adds it to the pck and removes the no longer needed project.binary file.
149+ func handle_injection () -> void :
150+ var is_embedded : bool = not FileAccess .file_exists (path .pck )
151+ var injection_path : String = path .exe if is_embedded else path .pck
152+ var file_extension := injection_path .get_extension ()
153+
154+ ModLoaderSetupLog .debug ("Start injection" , LOG_NAME )
155+ # Create temp dir
156+ ModLoaderSetupLog .debug ('Creating temp dir at "%s "' % path .temp_dir_path , LOG_NAME )
157+ DirAccess .make_dir_recursive_absolute (path .temp_dir_path )
158+
159+ # Create project.binary
160+ ModLoaderSetupLog .debug (
161+ 'Storing project.binary at "%s "' % path .temp_project_binary_path , LOG_NAME
162+ )
163+ var _error_save_custom_project_binary = ProjectSettings .save_custom (
164+ path .temp_project_binary_path
165+ )
166+ # Create combined global class cache cfg
167+ var combined_global_script_class_cache_file := get_combined_global_script_class_cache ()
168+ ModLoaderSetupLog .debug (
169+ 'Storing global_script_class_cache at "%s "' % path .temp_global_script_class_cache_path ,
170+ LOG_NAME
171+ )
172+ # Create the .godot dir inside the temp dir
173+ DirAccess .make_dir_recursive_absolute (path .temp_dir_path .path_join (".godot" ))
174+ # Save the global class cache config file
175+ combined_global_script_class_cache_file .save (path .temp_global_script_class_cache_path )
176+
177+ inject (injection_path , is_embedded )
178+
179+ # Rename vanilla
180+ var modded_path := "%s -modded.%s " % [injection_path .get_basename (), file_extension ]
181+ var vanilla_path := "%s -vanilla.%s " % [injection_path .get_basename (), file_extension ]
182+
183+ DirAccess .rename_absolute (injection_path , vanilla_path )
184+ ModLoaderSetupLog .debug ('Renamed "%s " to "%s "' % [injection_path , vanilla_path ], LOG_NAME )
185+
186+ # Rename modded
187+ DirAccess .rename_absolute (modded_path , injection_path )
188+ ModLoaderSetupLog .debug ('Renamed "%s " to "%s "' % [modded_path , injection_path ], LOG_NAME )
189+
190+ clean_up ()
191+
192+
193+ # Add modified binary to the pck
194+ func inject (injection_path : String , is_embedded := false ) -> void :
195+ var arguments := []
196+ arguments .push_back ("--pck-patch=%s " % injection_path )
197+ if is_embedded :
198+ arguments .push_back ("--embed=%s " % injection_path )
199+ arguments .push_back (
200+ "--patch-file=%s =%s " % [path .temp_project_binary_path , path .project_binary_path_internal ]
201+ )
202+ arguments .push_back (
203+ (
204+ "--patch-file=%s =%s "
205+ % [
206+ path .temp_global_script_class_cache_path ,
207+ path .global_script_class_cache_path_internal
208+ ]
209+ )
210+ )
211+ arguments .push_back (
212+ (
213+ "--output=%s "
214+ % path .game_base_dir .path_join (
215+ (
216+ "%s -modded.%s "
217+ % [file_name [injection_path .get_extension ()], injection_path .get_extension ()]
218+ )
219+ )
220+ )
221+ )
222+
223+ # For unknown reasons the output only displays a single "[" - so only the executed arguments are logged.
224+ ModLoaderSetupLog .debug ("Injection started: %s %s " % [path .gdre , arguments ], LOG_NAME )
225+ var output := []
226+ var _exit_code_inject := OS .execute (path .gdre , arguments , output )
227+ ModLoaderSetupLog .debug ("Injection completed: %s " % output , LOG_NAME )
228+
229+
230+ # Removes the temp files
231+ func clean_up () -> void :
232+ ModLoaderSetupLog .debug ("Start clean up" , LOG_NAME )
233+ DirAccess .remove_absolute (path .temp_project_binary_path )
234+ ModLoaderSetupLog .debug ('Removed: "%s "' % path .temp_project_binary_path , LOG_NAME )
235+ DirAccess .remove_absolute (path .temp_global_script_class_cache_path )
236+ ModLoaderSetupLog .debug ('Removed: "%s "' % path .temp_global_script_class_cache_path , LOG_NAME )
237+ DirAccess .remove_absolute (path .temp_dir_path .path_join (".godot" ))
238+ ModLoaderSetupLog .debug ('Removed: "%s "' % path .temp_dir_path .path_join (".godot" ), LOG_NAME )
239+ DirAccess .remove_absolute (path .temp_dir_path )
240+ ModLoaderSetupLog .debug ('Removed: "%s "' % path .temp_dir_path , LOG_NAME )
241+ ModLoaderSetupLog .debug ("Clean up completed" , LOG_NAME )
242+
243+
244+ # Initialize the path and file_name dictionary
245+ func setup_file_data () -> void :
246+ # C:/path/to/game/game.exe
247+ path .exe = OS .get_executable_path ()
248+ # C:/path/to/game/
249+ path .game_base_dir = ModLoaderSetupUtils .get_local_folder_dir ()
250+ # C:/path/to/game/addons/mod_loader
251+ path .mod_loader_dir = path .game_base_dir + "addons/mod_loader/"
252+ path .gdre = path .mod_loader_dir + get_gdre_path ()
253+ path .temp_dir_path = path .mod_loader_dir + "setup/temp"
254+ path .temp_project_binary_path = path .temp_dir_path + "/project.binary"
255+ path .temp_global_script_class_cache_path = (
256+ path .temp_dir_path
257+ + "/.godot/global_script_class_cache.cfg"
258+ )
259+ path .global_script_class_cache_path_internal = "res://.godot/global_script_class_cache.cfg"
260+ path .project_binary_path_internal = "res://project.binary"
261+ # can be supplied to override the exe_name
262+ file_name .cli_arg_exe = ModLoaderSetupUtils .get_cmd_line_arg_value ("--exe-name" )
263+ # can be supplied to override the pck_name
264+ file_name .cli_arg_pck = ModLoaderSetupUtils .get_cmd_line_arg_value ("--pck-name" )
265+ # game - or use the value of cli_arg_exe_name if there is one
266+ file_name .exe = (
267+ ModLoaderSetupUtils .get_file_name_from_path (path .exe , false , true )
268+ if file_name .cli_arg_exe == ""
269+ else file_name .cli_arg_exe
270+ )
271+ # game - or use the value of cli_arg_pck_name if there is one
272+ # using exe_path.get_file() instead of exe_name
273+ # so you don't override the pck_name with the --exe-name cli arg
274+ # the main pack name is the same as the .exe name
275+ # if --main-pack cli arg is not set
276+ file_name .pck = (
277+ ModLoaderSetupUtils .get_file_name_from_path (path .exe , false , true )
278+ if file_name .cli_arg_pck == ""
279+ else file_name .cli_arg_pck
280+ )
281+ # C:/path/to/game/game.pck
282+ path .pck = path .game_base_dir .path_join (file_name .pck + ".pck" )
283+
284+ ModLoaderSetupLog .debug_json_print ("path: " , path , LOG_NAME )
285+ ModLoaderSetupLog .debug_json_print ("file_name: " , file_name , LOG_NAME )
286+
287+
288+ func make_project_data_public () -> void :
289+ ModLoaderSetupLog .info ("Register Global Classes" , LOG_NAME )
290+ ProjectSettings .set_setting ("application/config/use_hidden_project_data_directory" , false )
291+
292+ var godot_files = ModLoaderSetupUtils .get_flat_view_dict ("res://.godot" )
293+
294+ ModLoaderSetupLog .info ('Copying all files from "res://.godot" to "res://godot".' , LOG_NAME )
295+
296+ for file in godot_files :
297+ ModLoaderSetupUtils .copy_file (
298+ file , file .trim_prefix ("res://.godot" ).insert (0 , "res://godot" )
299+ )
300+
301+
302+ func get_combined_global_script_class_cache () -> ConfigFile :
303+ ModLoaderSetupLog .info ("Load mod loader class cache" , LOG_NAME )
304+ var global_script_class_cache_mod_loader := ConfigFile .new ()
305+ global_script_class_cache_mod_loader .load (
306+ "res://addons/mod_loader/setup/global_script_class_cache_mod_loader.cfg"
307+ )
308+
309+ ModLoaderSetupLog .info ("Load game class cache" , LOG_NAME )
310+ var global_script_class_cache_game := ConfigFile .new ()
311+ global_script_class_cache_game .load ("res://.godot/global_script_class_cache.cfg" )
312+
313+ ModLoaderSetupLog .info ("Create new class cache" , LOG_NAME )
314+ var global_classes_mod_loader := global_script_class_cache_mod_loader .get_value ("" , "list" )
315+ var global_classes_game := global_script_class_cache_game .get_value ("" , "list" )
316+
317+ ModLoaderSetupLog .info ("Combine class cache" , LOG_NAME )
318+ var global_classes_combined := []
319+ global_classes_combined .append_array (global_classes_mod_loader )
320+ global_classes_combined .append_array (global_classes_game )
321+
322+ ModLoaderSetupLog .info ("Save combined class cache" , LOG_NAME )
323+ var global_script_class_cache_combined := ConfigFile .new ()
324+ global_script_class_cache_combined .set_value ("" , "list" , global_classes_combined )
325+
326+ return global_script_class_cache_combined
327+
328+
329+ func get_gdre_path () -> String :
330+ if OS .get_name () == "Windows" :
331+ return "vendor/GDRE/gdre_tools.exe"
332+
333+ return ""
334+
335+
336+ func restart () -> void :
337+ OS .set_restart_on_exit (true )
338+ quit ()
0 commit comments