1
1
extends SceneTree
2
2
3
-
4
3
const LOG_NAME := "ModLoader:Setup"
5
4
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
+
6
11
# IMPORTANT: use the ModLoaderLog via this variable within this script!
7
12
# Otherwise, script compilation will break on first load since the class is not defined.
8
13
var ModLoaderSetupLog : Object = load ("res://addons/mod_loader/setup/setup_log.gd" )
9
14
var ModLoaderSetupUtils : Object = load ("res://addons/mod_loader/setup/setup_utils.gd" )
10
15
16
+ var path := {}
17
+ var file_name := {}
18
+ var is_only_setup : bool = ModLoaderSetupUtils .is_running_with_command_line_arg ("--only-setup" )
11
19
var is_setup_create_override_cfg : bool = ModLoaderSetupUtils .is_running_with_command_line_arg (
12
20
"--setup-create-override-cfg"
13
21
)
@@ -31,7 +39,7 @@ func _init() -> void:
31
39
modded_start ()
32
40
return
33
41
34
- change_scene_to_file ( "res://addons/mod_loader/setup/setup.tscn" )
42
+ setup_modloader ( )
35
43
36
44
37
45
# ModLoader already setup - switch to the main scene
@@ -41,3 +49,290 @@ func modded_start() -> void:
41
49
root .set_title ("%s (Modded)" % ProjectSettings .get_setting ("application/config/name" ))
42
50
43
51
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