Skip to content

Commit 2a22ac3

Browse files
committed
🚧 refactor: extract methods
1 parent 304be3e commit 2a22ac3

File tree

2 files changed

+301
-292
lines changed

2 files changed

+301
-292
lines changed
Lines changed: 9 additions & 292 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
11
extends EditorExportPlugin
22

3-
const REQUIRE_EXPLICIT_ADDITION := false
4-
const METHOD_PREFIX := "vanilla_"
5-
const HASH_COLLISION_ERROR := "MODDING EXPORT ERROR: Hash collision between %s and %s. The collision can be resolved by renaming one of the methods or changing their script's path."
6-
7-
static var regex_getter_setter: RegEx
8-
9-
var hashmap := {}
3+
const ModHookPreprocessorScript := preload("res://addons/mod_loader/_export_plugin/mod_hook_preprocessor.gd")
4+
static var ModHookPreprocessor
105

116

127
func _get_name() -> String:
138
return "Godot Mod Loader Export Plugin"
149

1510

1611
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
17-
process_begin()
12+
ModHookPreprocessor = ModHookPreprocessorScript.new()
13+
ModHookPreprocessor.process_begin()
1814

1915

2016
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
@@ -25,287 +21,8 @@ func _export_file(path: String, type: String, features: PackedStringArray) -> vo
2521
return
2622

2723
skip()
28-
add_file(path, process_script(path, type, features).to_utf8_buffer(), false)
29-
30-
31-
func process_begin() -> void:
32-
hashmap.clear()
33-
regex_getter_setter = RegEx.new()
34-
regex_getter_setter.compile("(.*?[sg]et\\s*=\\s*)(\\w+)(\\g<1>)?(\\g<2>)?")
35-
36-
37-
func process_script(path: String, type: String, features: PackedStringArray) -> String:
38-
var current_script := load(path) as GDScript
39-
var source_code := current_script.source_code
40-
var source_code_additions := ""
41-
42-
# We need to stop all vanilla methods from forming inheritance chains,
43-
# since the generated methods will fulfill inheritance requirements
44-
var class_prefix := str(hash(path))
45-
var method_store: Array[String] = []
46-
var mod_loader_hooks_start_string := \
47-
"\n# ModLoader Hooks - The following code has been automatically added by the Godot Mod Loader export plugin.\n"
48-
49-
var getters_setters := collect_getters_and_setters(source_code)
50-
51-
for method in current_script.get_script_method_list():
52-
var method_first_line_start := get_index_at_method_start(method.name, source_code)
53-
if method_first_line_start == -1 or method.name in method_store:
54-
continue
55-
56-
if getters_setters.has(method.name):
57-
continue
58-
59-
if not is_func_moddable(method_first_line_start, source_code):
60-
continue
61-
62-
var type_string := get_return_type_string(method.return)
63-
var is_static := true if method.flags == METHOD_FLAG_STATIC + METHOD_FLAG_NORMAL else false
64-
var method_arg_string_with_defaults_and_types := get_function_parameters(method.name, source_code, is_static)
65-
var method_arg_string_names_only := get_function_arg_name_string(method.args)
66-
67-
var hash_before := ModLoaderMod.get_hook_hash(path, method.name, true)
68-
var hash_after := ModLoaderMod.get_hook_hash(path, method.name, false)
69-
var hash_before_data := [path, method.name,true]
70-
var hash_after_data := [path, method.name,false]
71-
if hashmap.has(hash_before):
72-
push_error(HASH_COLLISION_ERROR%[hashmap[hash_before], hash_before_data])
73-
if hashmap.has(hash_after):
74-
push_error(HASH_COLLISION_ERROR %[hashmap[hash_after], hash_after_data])
75-
hashmap[hash_before] = hash_before_data
76-
hashmap[hash_after] = hash_after_data
77-
78-
var mod_loader_hook_string := get_mod_loader_hook(
79-
method.name,
80-
method_arg_string_names_only,
81-
method_arg_string_with_defaults_and_types,
82-
type_string,
83-
method.return.usage,
84-
is_static,
85-
path,
86-
hash_before,
87-
hash_after,
88-
METHOD_PREFIX + class_prefix,
89-
)
90-
91-
# Store the method name
92-
# Not sure if there is a way to get only the local methods in a script,
93-
# get_script_method_list() returns a full list,
94-
# including the methods from the scripts it extends,
95-
# which leads to multiple entries in the list if they are overridden by the child script.
96-
method_store.push_back(method.name)
97-
source_code = prefix_method_name(method.name, is_static, source_code, METHOD_PREFIX + class_prefix)
98-
source_code_additions += "\n%s" % mod_loader_hook_string
99-
100-
#if we have some additions to the code, append them at the end
101-
if source_code_additions != "":
102-
source_code = "%s\n%s\n%s" % [source_code,mod_loader_hooks_start_string, source_code_additions]
103-
104-
return source_code
105-
106-
107-
108-
static func get_function_arg_name_string(args: Array) -> String:
109-
var arg_string := ""
110-
for x in args.size():
111-
if x == args.size() -1:
112-
arg_string += args[x].name
113-
else:
114-
arg_string += "%s, " % args[x].name
115-
116-
return arg_string
117-
118-
119-
static func get_function_parameters(method_name: String, text: String, is_static: bool, offset := 0) -> String:
120-
var result := match_func_with_whitespace(method_name, text, offset)
121-
if result == null:
122-
return ""
123-
124-
# Find the index of the opening parenthesis
125-
var opening_paren_index := result.get_end() - 1
126-
if opening_paren_index == -1:
127-
return ""
128-
129-
if not is_top_level_func(text, result.get_start(), is_static):
130-
return get_function_parameters(method_name, text, is_static, result.get_end())
131-
132-
# Use a stack to match parentheses
133-
var stack := []
134-
var closing_paren_index := opening_paren_index
135-
while closing_paren_index < text.length():
136-
var char := text[closing_paren_index]
137-
if char == '(':
138-
stack.push_back('(')
139-
elif char == ')':
140-
stack.pop_back()
141-
if stack.size() == 0:
142-
break
143-
closing_paren_index += 1
144-
145-
# If the stack is not empty, that means there's no matching closing parenthesis
146-
if stack.size() != 0:
147-
return ""
148-
149-
# Extract the substring between the parentheses
150-
var param_string := text.substr(opening_paren_index + 1, closing_paren_index - opening_paren_index - 1)
151-
152-
# Clean whitespace characters (spaces, newlines, tabs)
153-
param_string = param_string.strip_edges()\
154-
.replace(" ", "")\
155-
.replace("\n", "")\
156-
.replace("\t", "")\
157-
.replace(",", ", ")\
158-
.replace(":", ": ")
159-
160-
return param_string
161-
162-
163-
static func prefix_method_name(method_name: String, is_static: bool, text: String, prefix := METHOD_PREFIX, offset := 0) -> String:
164-
var result := match_func_with_whitespace(method_name, text, offset)
165-
166-
if not result:
167-
return text
168-
169-
if not is_top_level_func(text, result.get_start(), is_static):
170-
return prefix_method_name(method_name, is_static, text, prefix, result.get_end())
171-
172-
text = text.erase(result.get_start(), result.get_end() - result.get_start())
173-
text = text.insert(result.get_start(), "func %s_%s(" % [prefix, method_name])
174-
175-
return text
176-
177-
178-
static func match_func_with_whitespace(method_name: String, text: String, offset := 0) -> RegExMatch:
179-
var func_with_whitespace := RegEx.new()
180-
func_with_whitespace.compile("func\\s+%s\\s*\\(" % method_name)
181-
182-
# Search for the function definition
183-
return func_with_whitespace.search(text, offset)
184-
185-
186-
static func get_mod_loader_hook(
187-
method_name: String,
188-
method_arg_string_names_only: String,
189-
method_arg_string_with_defaults_and_types: String,
190-
method_type: String,
191-
return_prop_usage: int,
192-
is_static: bool,
193-
script_path: String,
194-
hash_before:int,
195-
hash_after:int,
196-
method_prefix := METHOD_PREFIX) -> String:
197-
var type_string := " -> %s" % method_type if not method_type.is_empty() else ""
198-
var static_string := "static " if is_static else ""
199-
# Cannot use "self" inside a static function.
200-
var self_string := "null" if is_static else "self"
201-
var return_var := "var %s = " % "return_var" if not method_type.is_empty() or return_prop_usage == 131072 else ""
202-
var method_return := "return %s" % "return_var" if not method_type.is_empty() or return_prop_usage == 131072 else ""
203-
204-
return """
205-
{%STATIC%}func {%METHOD_NAME%}({%METHOD_PARAMS%}){%RETURN_TYPE_STRING%}:
206-
if ModLoaderStore.any_mod_hooked:
207-
ModLoaderMod.call_hooks({%SELF%}, [{%METHOD_ARGS%}], {%HOOK_ID_BEFORE%})
208-
{%METHOD_RETURN_VAR%}{%METHOD_PREFIX%}_{%METHOD_NAME%}({%METHOD_ARGS%})
209-
if ModLoaderStore.any_mod_hooked:
210-
ModLoaderMod.call_hooks({%SELF%}, [{%METHOD_ARGS%}], {%HOOK_ID_AFTER%})
211-
{%METHOD_RETURN%}
212-
""".format({
213-
"%METHOD_PREFIX%": method_prefix,
214-
"%METHOD_NAME%": method_name,
215-
"%METHOD_PARAMS%": method_arg_string_with_defaults_and_types,
216-
"%RETURN_TYPE_STRING%": type_string,
217-
"%METHOD_ARGS%": method_arg_string_names_only,
218-
"%SCRIPT_PATH%": script_path,
219-
"%METHOD_RETURN_VAR%": return_var,
220-
"%METHOD_RETURN%": method_return,
221-
"%STATIC%": static_string,
222-
"%SELF%": self_string,
223-
"%HOOK_ID_BEFORE%" : hash_before,
224-
"%HOOK_ID_AFTER%" : hash_after,
225-
})
226-
227-
228-
static func get_previous_line_to(text: String, index: int) -> String:
229-
if index <= 0 or index >= text.length():
230-
return ""
231-
232-
var start_index := index - 1
233-
# Find the end of the previous line
234-
while start_index > 0 and text[start_index] != "\n":
235-
start_index -= 1
236-
237-
if start_index == 0:
238-
return ""
239-
240-
start_index -= 1
241-
242-
# Find the start of the previous line
243-
var end_index := start_index
244-
while start_index > 0 and text[start_index - 1] != "\n":
245-
start_index -= 1
246-
247-
return text.substr(start_index, end_index - start_index + 1)
248-
249-
250-
static func is_func_moddable(method_start_idx, text) -> bool:
251-
var prevline := get_previous_line_to(text, method_start_idx)
252-
253-
if prevline.contains("@not-moddable"):
254-
return false
255-
if not REQUIRE_EXPLICIT_ADDITION:
256-
return true
257-
258-
return prevline.contains("@moddable")
259-
260-
261-
static func get_index_at_method_start(method_name: String, text: String) -> int:
262-
var result := match_func_with_whitespace(method_name, text)
263-
264-
if result:
265-
return text.find("\n", result.get_end())
266-
else:
267-
return -1
268-
269-
270-
static func is_top_level_func(text: String, result_start_index: int, is_static := false) -> bool:
271-
if is_static:
272-
result_start_index = text.rfind("static", result_start_index)
273-
274-
var line_start_index := text.rfind("\n", result_start_index) + 1
275-
var pre_func_length := result_start_index - line_start_index
276-
277-
if pre_func_length > 0:
278-
return false
279-
280-
return true
281-
282-
283-
static func get_return_type_string(return_data: Dictionary) -> String:
284-
if return_data.type == 0:
285-
return ""
286-
var type_base: String
287-
if return_data.has("class_name") and not str(return_data.class_name).is_empty():
288-
type_base = str(return_data.class_name)
289-
else:
290-
type_base = type_string(return_data.type)
291-
292-
var type_hint := "" if return_data.hint_string.is_empty() else ("[%s]" % return_data.hint_string)
293-
294-
return "%s%s" % [type_base, type_hint]
295-
296-
297-
static func collect_getters_and_setters(text: String) -> Dictionary:
298-
var result := {}
299-
# a valid match has 2 or 4 groups, split into the method names and the rest of the line
300-
# (var example: set = )(example_setter)(, get = )(example_getter)
301-
# if things between the names are empty or commented, exclude them
302-
for mat in regex_getter_setter.search_all(text):
303-
if mat.get_string(1).is_empty() or mat.get_string(1).contains("#"):
304-
continue
305-
result[mat.get_string(2)] = null
306-
307-
if mat.get_string(3).is_empty() or mat.get_string(3).contains("#"):
308-
continue
309-
result[mat.get_string(4)] = null
310-
311-
return result
24+
add_file(
25+
path,
26+
ModHookPreprocessor.process_script(path).to_utf8_buffer(),
27+
false
28+
)

0 commit comments

Comments
 (0)