-
Notifications
You must be signed in to change notification settings - Fork 30
/
script_extension.gd
241 lines (194 loc) · 8.78 KB
/
script_extension.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
class_name _ModLoaderScriptExtension
extends RefCounted
# This Class provides methods for working with script extensions.
# Currently all of the included methods are internal and should only be used by the mod loader itself.
const LOG_NAME := "ModLoader:ScriptExtension"
# Sort script extensions by inheritance and apply them in order
static func handle_script_extensions() -> void:
var extension_paths := []
for extension_path in ModLoaderStore.script_extensions:
if FileAccess.file_exists(extension_path):
extension_paths.push_back(extension_path)
else:
ModLoaderLog.error(
"The child script path '%s' does not exist" % [extension_path], LOG_NAME
)
# Sort by inheritance
extension_paths.sort_custom(Callable(InheritanceSorting, "_check_inheritances"))
# Load and install all extensions
for extension in extension_paths:
var script: Script = apply_extension(extension)
_reload_vanilla_child_classes_for(script)
# Sorts script paths by their ancestors. Scripts are organized by their common
# ancestors then sorted such that scripts extending script A will be before
# a script extending script B if A is an ancestor of B.
class InheritanceSorting:
var stack_cache := {}
# Comparator function. return true if a should go before b. This may
# enforce conditions beyond the stated inheritance relationship.
func _check_inheritances(extension_a: String, extension_b: String) -> bool:
var a_stack := cached_inheritances_stack(extension_a)
var b_stack := cached_inheritances_stack(extension_b)
var last_index: int
for index in a_stack.size():
if index >= b_stack.size():
return false
if a_stack[index] != b_stack[index]:
return a_stack[index] < b_stack[index]
last_index = index
if last_index < b_stack.size():
return true
return extension_a < extension_b
# Returns a list of scripts representing all the ancestors of the extension
# script with the most recent ancestor last.
#
# Results are stored in a cache keyed by extension path
func cached_inheritances_stack(extension_path: String) -> Array:
if stack_cache.has(extension_path):
return stack_cache[extension_path]
var stack := []
var parent_script: Script = load(extension_path)
while parent_script:
stack.push_front(parent_script.resource_path)
parent_script = parent_script.get_base_script()
stack.pop_back()
stack_cache[extension_path] = stack
return stack
static func apply_extension(extension_path: String) -> Script:
# Check path to file exists
if not FileAccess.file_exists(extension_path):
ModLoaderLog.error("The child script path '%s' does not exist" % [extension_path], LOG_NAME)
return null
var child_script: Script = ResourceLoader.load(extension_path)
# Adding metadata that contains the extension script path
# We cannot get that path in any other way
# Passing the child_script as is would return the base script path
# Passing the .duplicate() would return a '' path
child_script.set_meta("extension_script_path", extension_path)
# Force Godot to compile the script now.
# We need to do this here to ensure that the inheritance chain is
# properly set up, and multiple mods can chain-extend the same
# class multiple times.
# This is also needed to make Godot instantiate the extended class
# when creating singletons.
# The actual instance is thrown away.
child_script.new()
var parent_script: Script = child_script.get_base_script()
var parent_script_path: String = parent_script.resource_path
# We want to save scripts for resetting later
# All the scripts are saved in order already
if not ModLoaderStore.saved_scripts.has(parent_script_path):
ModLoaderStore.saved_scripts[parent_script_path] = []
# The first entry in the saved script array that has the path
# used as a key will be the duplicate of the not modified script
ModLoaderStore.saved_scripts[parent_script_path].append(parent_script.duplicate())
ModLoaderStore.saved_scripts[parent_script_path].append(child_script)
ModLoaderLog.info(
"Installing script extension: %s <- %s" % [parent_script_path, extension_path], LOG_NAME
)
child_script.take_over_path(parent_script_path)
return child_script
# Reload all children classes of the vanilla class we just extended
# Calling reload() the children of an extended class seems to allow them to be extended
# e.g if B is a child class of A, reloading B after apply an extender of A allows extenders of B to properly extend B, taking A's extender(s) into account
static func _reload_vanilla_child_classes_for(script: Script) -> void:
if script == null:
return
var current_child_classes := []
var actual_path: String = script.get_base_script().resource_path
var classes: Array = ProjectSettings.get_setting("_global_script_classes")
for _class in classes:
if _class.path == actual_path:
current_child_classes.push_back(_class)
break
for _class in current_child_classes:
for child_class in classes:
if child_class.base == _class.get_class():
load(child_class.path).reload()
# Used to remove a specific extension
static func remove_specific_extension_from_script(extension_path: String) -> void:
# Check path to file exists
if not _ModLoaderFile.file_exists(extension_path):
ModLoaderLog.error(
'The extension script path "%s" does not exist' % [extension_path], LOG_NAME
)
return
var extension_script: Script = ResourceLoader.load(extension_path)
var parent_script: Script = extension_script.get_base_script()
var parent_script_path: String = parent_script.resource_path
# Check if the script to reset has been extended
if not ModLoaderStore.saved_scripts.has(parent_script_path):
ModLoaderLog.error(
'The extension parent script path "%s" has not been extended' % [parent_script_path],
LOG_NAME
)
return
# Check if the script to reset has anything actually saved
# If we ever encounter this it means something went very wrong in extending
if not ModLoaderStore.saved_scripts[parent_script_path].size() > 0:
ModLoaderLog.error(
(
'The extension script path "%s" does not have the base script saved, this should never happen, if you encounter this please create an issue in the github repository'
% [parent_script_path]
),
LOG_NAME
)
return
var parent_script_extensions: Array = ModLoaderStore.saved_scripts[parent_script_path].duplicate()
parent_script_extensions.remove_at(0)
# Searching for the extension that we want to remove
var found_script_extension: Script = null
for script_extension in parent_script_extensions:
if script_extension.get_meta("extension_script_path") == extension_path:
found_script_extension = script_extension
break
if found_script_extension == null:
ModLoaderLog.error(
(
'The extension script path "%s" has not been found in the saved extension of the base script'
% [parent_script_path]
),
LOG_NAME
)
return
parent_script_extensions.erase(found_script_extension)
# Preparing the script to have all other extensions reapllied
_remove_all_extensions_from_script(parent_script_path)
# Reapplying all the extensions without the removed one
for script_extension in parent_script_extensions:
apply_extension(script_extension.get_meta("extension_script_path"))
# Used to fully reset the provided script to a state prior of any extension
static func _remove_all_extensions_from_script(parent_script_path: String) -> void:
# Check path to file exists
if not _ModLoaderFile.file_exists(parent_script_path):
ModLoaderLog.error(
'The parent script path "%s" does not exist' % [parent_script_path], LOG_NAME
)
return
# Check if the script to reset has been extended
if not ModLoaderStore.saved_scripts.has(parent_script_path):
ModLoaderLog.error(
'The parent script path "%s" has not been extended' % [parent_script_path], LOG_NAME
)
return
# Check if the script to reset has anything actually saved
# If we ever encounter this it means something went very wrong in extending
if not ModLoaderStore.saved_scripts[parent_script_path].size() > 0:
ModLoaderLog.error(
(
'The parent script path "%s" does not have the base script saved, \nthis should never happen, if you encounter this please create an issue in the github repository'
% [parent_script_path]
),
LOG_NAME
)
return
var parent_script: Script = ModLoaderStore.saved_scripts[parent_script_path][0]
parent_script.take_over_path(parent_script_path)
# Remove the script after it has been reset so we do not do it again
ModLoaderStore.saved_scripts.erase(parent_script_path)
# Used to remove all extensions that are of a specific mod
static func remove_all_extensions_of_mod(mod: ModData) -> void:
var _to_remove_extension_paths: Array = ModLoaderStore.saved_extension_paths[mod.manifest.get_mod_id()]
for extension_path in _to_remove_extension_paths:
remove_specific_extension_from_script(extension_path)
ModLoaderStore.saved_extension_paths.erase(mod.manifest.get_mod_id())