-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtest.gd
361 lines (290 loc) · 12.8 KB
/
test.gd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
extends SceneTree
const LOG_NAME := "ModLoader:Setup"
const settings := {
"IS_LOADER_SETUP_APPLIED": "application/run/is_loader_setup_applied",
"IS_LOADER_SET_UP": "application/run/is_loader_set_up",
"MOD_LOADER_AUTOLOAD": "autoload/ModLoader",
}
# IMPORTANT: use the ModLoaderLog via this variable within this script!
# Otherwise, script compilation will break on first load since the class is not defined.
var ModLoaderSetupLog: Object = load("res://addons/mod_loader/setup/setup_log.gd")
var ModLoaderSetupUtils: Object = load("res://addons/mod_loader/setup/setup_utils.gd")
var path := {}
var file_name := {}
var is_only_setup: bool = ModLoaderSetupUtils.is_running_with_command_line_arg("--only-setup")
var is_setup_create_override_cfg: bool = ModLoaderSetupUtils.is_running_with_command_line_arg(
"--setup-create-override-cfg"
)
func _init() -> void:
ModLoaderSetupLog.debug("ModLoader setup initialized", LOG_NAME)
var mod_loader_index: int = ModLoaderSetupUtils.get_autoload_index("ModLoader")
var mod_loader_store_index: int = ModLoaderSetupUtils.get_autoload_index("ModLoaderStore")
# Avoid doubling the setup work
# Checks if the ModLoaderStore is the first autoload and ModLoader the second
if mod_loader_store_index == 0 and mod_loader_index == 1:
modded_start()
return
# Check if --setup-create-override-cfg is passed,
# in that case the ModLoader and ModLoaderStore just have to be somewhere in the autoloads.
if is_setup_create_override_cfg and mod_loader_index != -1 and mod_loader_store_index != -1:
modded_start()
return
setup_modloader()
# ModLoader already setup - switch to the main scene
func modded_start() -> void:
ModLoaderSetupLog.info("ModLoader is available, mods can be loaded!", LOG_NAME)
root.set_title("%s (Modded)" % ProjectSettings.get_setting("application/config/name"))
change_scene_to_file.call_deferred(ProjectSettings.get_setting("application/run/main_scene"))
# Set up the ModLoader as an autoload and register the other global classes.
func setup_modloader() -> void:
ModLoaderSetupLog.info("Setting up ModLoader", LOG_NAME)
# Setup path and file_name dict with all required paths and file names.
setup_file_data()
# Add ModLoader autoload (the * marks the path as autoload)
reorder_autoloads()
ProjectSettings.set_setting(settings.IS_LOADER_SET_UP, true)
# The game needs to be restarted first, before 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 is_setup_create_override_cfg:
handle_override_cfg()
else:
handle_injection()
# ModLoader is set up. A game restart is required to apply the ProjectSettings.
ModLoaderSetupLog.info("ModLoader is set up, a game restart is required.", LOG_NAME)
match true:
# If the --only-setup cli argument is passed, quit with exit code 0
is_only_setup:
quit(0)
# 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. The game needs to be restarted to apply the changes. Confirm to restart."
)
restart()
# 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
ModLoaderSetupLog.info(
"Start reorder autoloads current state: %s" % JSON.stringify(original_autoloads, "\t")
)
for autoload in original_autoloads.keys():
ProjectSettings.set_setting(autoload, null)
# Add ModLoaderStore autoload (the * marks the path as autoload)
ProjectSettings.set_setting(
"autoload/ModLoaderStore", "*" + "res://addons/mod_loader/mod_loader_store.gd"
)
# 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])
var new_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)
new_autoloads[name] = value
ModLoaderSetupLog.info(
"Reorder autoloads completed - new state: %s" % JSON.stringify(new_autoloads, "\t")
)
# Saves the ProjectSettings to a override.cfg file in the base game directory.
func handle_override_cfg() -> void:
ModLoaderSetupLog.debug("using the override.cfg file", LOG_NAME)
# Make the '.godot' dir public as 'godot' and copy all files to the public dir.
make_project_data_public()
# Combine mod_loader and game global classes
var global_script_class_cache_combined := get_combined_global_script_class_cache()
global_script_class_cache_combined.save("res://godot/global_script_class_cache.cfg")
var _save_custom_error: int = ProjectSettings.save_custom(
ModLoaderSetupUtils.get_override_path()
)
# Creates the project.binary file, adds it to the pck and removes the no longer needed project.binary file.
func handle_injection() -> void:
ModLoaderSetupLog.debug("Start injection", LOG_NAME)
# Create temp dir
ModLoaderSetupLog.debug('Creating temp dir at "%s"' % path.temp_dir_path, LOG_NAME)
DirAccess.make_dir_recursive_absolute(path.temp_dir_path)
# Create project.binary
ModLoaderSetupLog.debug(
'Storing project.binary at "%s"' % path.temp_project_binary_path, LOG_NAME
)
var _error_save_custom_project_binary = ProjectSettings.save_custom(
path.temp_project_binary_path
)
# Create combined global class cache cfg
var combined_global_script_class_cache_file := get_combined_global_script_class_cache()
ModLoaderSetupLog.debug(
'Storing global_script_class_cache at "%s"' % path.temp_global_script_class_cache_path,
LOG_NAME
)
# Create the .godot dir inside the temp dir
DirAccess.make_dir_recursive_absolute(path.temp_dir_path.path_join(".godot"))
# Save the global class cache config file
combined_global_script_class_cache_file.save(path.temp_global_script_class_cache_path)
get_pck_version()
# Check if .pck is embedded split it from the .exe
if not FileAccess.file_exists(path.pck):
split_pck()
inject()
# Rename vanilla
DirAccess.rename_absolute(
path.pck,
path.pck.trim_suffix("%s.pck" % file_name.pck).path_join("%s-vanilla.pck" % file_name.pck)
)
ModLoaderSetupLog.debug(
(
'Renamed "%s" to "%s"'
% [
path.pck,
path.pck.trim_suffix("%s.pck" % file_name.pck).path_join(
"%s-vanilla.pck" % file_name.pck
)
]
),
LOG_NAME
)
# Rename modded
DirAccess.rename_absolute(
path.game_base_dir.path_join("%s-modded.pck" % file_name.pck),
"%s.pck" % path.game_base_dir.path_join(file_name.pck)
)
ModLoaderSetupLog.debug(
(
'Renamed "%s" to "%s"'
% [
path.game_base_dir.path_join("%s-modded.pck" % file_name.pck),
"%s.pck" % path.game_base_dir.path_join(file_name.pck)
]
),
LOG_NAME
)
clean_up()
func get_pck_version() -> String:
var engine_version_info := Engine.get_version_info()
# Godot 4 pck version always starts with a 2 (at least for now).
var pck_version := (
"2.%s.%s.%s"
% [engine_version_info.major, engine_version_info.minor, engine_version_info.patch]
)
ModLoaderSetupLog.debug("The pck version is: %s" % pck_version, LOG_NAME)
return pck_version
# Add modified binary to the pck
func inject(pck_version: String = get_pck_version()) -> void:
var arguments := [
"-pc",
path.pck,
path.temp_dir_path,
path.game_base_dir.path_join("%s-modded.pck" % file_name.pck),
pck_version
]
ModLoaderSetupLog.debug(
"Injecting temp dir content into .pck: %s %s", [path.pck_explorer, arguments], LOG_NAME
)
# For unknown reasons the output only displays a single "[" - so only the executed arguments are logged.
var _exit_code_inject := OS.execute(path.pck_explorer, arguments)
func split_pck() -> void:
var arguments := ["-s", path.exe]
ModLoaderSetupLog.debug(
"Splitting .pck from .exe: %s %s", [path.pck_explorer, arguments], LOG_NAME
)
# For unknown reasons the output only displays a single "[" - so only the executed arguments are logged.
var _exit_code_split_pck := OS.execute(path.pck_explorer, arguments)
# Removes the temp files
func clean_up() -> void:
ModLoaderSetupLog.debug("Start clean up", LOG_NAME)
DirAccess.remove_absolute(path.temp_project_binary_path)
ModLoaderSetupLog.debug('Removed: "%s"' % path.temp_project_binary_path, LOG_NAME)
DirAccess.remove_absolute(path.temp_global_script_class_cache_path)
ModLoaderSetupLog.debug('Removed: "%s"' % path.temp_global_script_class_cache_path, LOG_NAME)
DirAccess.remove_absolute(path.temp_dir_path.path_join(".godot"))
ModLoaderSetupLog.debug('Removed: "%s"' % path.temp_dir_path.path_join(".godot"), LOG_NAME)
DirAccess.remove_absolute(path.temp_dir_path)
ModLoaderSetupLog.debug('Removed: "%s"' % path.temp_dir_path, LOG_NAME)
ModLoaderSetupLog.debug("Clean up completed", LOG_NAME)
# Initialize the path and file_name dictionary
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 = ModLoaderSetupUtils.get_local_folder_dir()
# C:/path/to/game/addons/mod_loader
path.mod_loader_dir = path.game_base_dir + "addons/mod_loader/"
path.pck_explorer = (
path.mod_loader_dir
+ get_pck_explorer_path()
)
# ! pck explorer doesn't like trailing `/` in a path !
path.temp_dir_path = path.mod_loader_dir + "setup/temp"
path.temp_project_binary_path = path.temp_dir_path + "/project.binary"
path.temp_global_script_class_cache_path = (
path.temp_dir_path
+ "/.godot/global_script_class_cache.cfg"
)
# can be supplied to override the exe_name
file_name.cli_arg_exe = ModLoaderSetupUtils.get_cmd_line_arg_value("--exe-name")
# can be supplied to override the pck_name
file_name.cli_arg_pck = ModLoaderSetupUtils.get_cmd_line_arg_value("--pck-name")
# game - or use the value of cli_arg_exe_name if there is one
file_name.exe = (
ModLoaderSetupUtils.get_file_name_from_path(path.exe, false, 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 = (
ModLoaderSetupUtils.get_file_name_from_path(path.exe, false, 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.path_join(file_name.pck + ".pck")
ModLoaderSetupLog.debug_json_print("path: ", path, LOG_NAME)
ModLoaderSetupLog.debug_json_print("file_name: ", file_name, LOG_NAME)
func make_project_data_public() -> void:
ModLoaderSetupLog.info("Register Global Classes", LOG_NAME)
ProjectSettings.set_setting("application/config/use_hidden_project_data_directory", false)
var godot_files = ModLoaderSetupUtils.get_flat_view_dict("res://.godot")
ModLoaderSetupLog.info('Copying all files from "res://.godot" to "res://godot".', LOG_NAME)
for file in godot_files:
ModLoaderSetupUtils.copy_file(
file, file.trim_prefix("res://.godot").insert(0, "res://godot")
)
func get_combined_global_script_class_cache() -> ConfigFile:
ModLoaderSetupLog.info("Load mod loader class cache", LOG_NAME)
var global_script_class_cache_mod_loader := ConfigFile.new()
global_script_class_cache_mod_loader.load(
"res://addons/mod_loader/setup/global_script_class_cache_mod_loader.cfg"
)
ModLoaderSetupLog.info("Load game class cache", LOG_NAME)
var global_script_class_cache_game := ConfigFile.new()
global_script_class_cache_game.load("res://.godot/global_script_class_cache.cfg")
ModLoaderSetupLog.info("Create new class cache", LOG_NAME)
var global_classes_mod_loader := global_script_class_cache_mod_loader.get_value("", "list")
var global_classes_game := global_script_class_cache_game.get_value("", "list")
ModLoaderSetupLog.info("Combine class cache", LOG_NAME)
var global_classes_combined := []
global_classes_combined.append_array(global_classes_mod_loader)
global_classes_combined.append_array(global_classes_game)
ModLoaderSetupLog.info("Save combined class cache", LOG_NAME)
var global_script_class_cache_combined := ConfigFile.new()
global_script_class_cache_combined.set_value("", "list", global_classes_combined)
return global_script_class_cache_combined
func get_pck_explorer_path() -> String:
var pck_explorer_path := "vendor/GodotPCKExplorer/GodotPCKExplorer.Console"
if OS.get_name() == "Windows":
return "%s.exe" % pck_explorer_path
return pck_explorer_path
func restart() -> void:
OS.set_restart_on_exit(true)
quit()