gdharness 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +163 -85
- package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -11
- package/build/godot/addons/gdharness_editor/declared_values.gd +157 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +15 -3
- package/build/godot/addons/gdharness_editor/file_walk.gd +134 -0
- package/build/godot/addons/gdharness_editor/file_walk.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +44 -23
- package/build/godot/addons/gdharness_editor/property_paths.gd +225 -0
- package/build/godot/addons/gdharness_editor/property_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +249 -227
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/same_values.gd +177 -0
- package/build/godot/addons/gdharness_editor/same_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +100 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +75 -3
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +402 -55
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -2
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +91 -0
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +88 -5
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -337
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +363 -0
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +10 -26
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +3 -2
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +30 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +75 -3
- package/build/godot/operations/animation_reads.gd +14 -11
- package/build/godot/operations/declared_values.gd +157 -4
- package/build/godot/operations/dependencies.gd +14 -39
- package/build/godot/operations/editor_tools.gd +7 -3
- package/build/godot/operations/file_reads.gd +15 -3
- package/build/godot/operations/file_walk.gd +1 -0
- package/build/godot/operations/gdscript_analysis.gd +229 -75
- package/build/godot/operations/gdscript_authoring.gd +142 -34
- package/build/godot/operations/import_pipeline.gd +17 -6
- package/build/godot/operations/logger.gd +7 -0
- package/build/godot/operations/property_paths.gd +225 -0
- package/build/godot/operations/property_values.gd +249 -227
- package/build/godot/operations/reading.gd +15 -0
- package/build/godot/operations/same_values.gd +177 -0
- package/build/godot/operations/serialisation.gd +75 -3
- package/package.json +1 -1
|
@@ -7,6 +7,12 @@ const GdhPatterns = preload("patterns.gd")
|
|
|
7
7
|
const GdhScriptCheck = preload("script_check.gd")
|
|
8
8
|
|
|
9
9
|
const MODIFICATION_TYPES: Array[String] = ["add_constant", "add_variable", "add_signal", "add_function"]
|
|
10
|
+
## The class each template's code is written for, and the base a script made from it extends
|
|
11
|
+
## when given none: the resource template's code is a Resource's, and on the default of Node it
|
|
12
|
+
## does not parse.
|
|
13
|
+
const TEMPLATE_BASES: Dictionary = {
|
|
14
|
+
"singleton": "Node", "state_machine": "Node", "component": "Node", "resource": "Resource"
|
|
15
|
+
}
|
|
10
16
|
const BLANKS_AROUND_FUNCTIONS: int = 2
|
|
11
17
|
|
|
12
18
|
var _log: GdhLog
|
|
@@ -20,11 +26,11 @@ func _init(p_log: GdhLog) -> void:
|
|
|
20
26
|
func create_gdscript(params: Dictionary) -> Dictionary:
|
|
21
27
|
var script_path: String = str(params.get("script_path", ""))
|
|
22
28
|
var cls_name_param: String = str(params.get("class_name", ""))
|
|
23
|
-
var extends_class: String = str(params.get("extends", "Node"))
|
|
24
|
-
if extends_class.is_empty():
|
|
25
|
-
extends_class = "Node"
|
|
26
29
|
var content: String = str(params.get("content", ""))
|
|
27
30
|
var template: String = str(params.get("template", ""))
|
|
31
|
+
var extends_class: String = str(params.get("extends", ""))
|
|
32
|
+
if extends_class.is_empty():
|
|
33
|
+
extends_class = str(TEMPLATE_BASES.get(template, "Node"))
|
|
28
34
|
|
|
29
35
|
_log.info("Creating GDScript: " + script_path)
|
|
30
36
|
|
|
@@ -51,7 +57,7 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
51
57
|
full_script_path = "res://" + full_script_path
|
|
52
58
|
|
|
53
59
|
if not full_script_path.ends_with(".gd"):
|
|
54
|
-
return _log.failure(
|
|
60
|
+
return _log.failure(_not_gdscript(full_script_path, "makes"))
|
|
55
61
|
|
|
56
62
|
if FileAccess.file_exists(full_script_path):
|
|
57
63
|
return _log.failure(
|
|
@@ -62,6 +68,8 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
62
68
|
# and the first does not compile until the second exists.
|
|
63
69
|
if content.is_empty():
|
|
64
70
|
var header: String = _header_refusal(cls_name_param, extends_class)
|
|
71
|
+
if header.is_empty() and not template.is_empty():
|
|
72
|
+
header = _template_refusal(template, extends_class, full_script_path)
|
|
65
73
|
if not header.is_empty():
|
|
66
74
|
return _log.failure(header + " Nothing was written.")
|
|
67
75
|
|
|
@@ -80,8 +88,9 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
80
88
|
script_content += "extends " + extends_class + "\n"
|
|
81
89
|
# Only what was asked for: with no template the file was given an empty _ready beside a
|
|
82
90
|
# template_used of "none", which a caller had then to delete or leave lying in the script.
|
|
91
|
+
# Each template opens with the class's doc comment, which belongs straight below the header.
|
|
83
92
|
if not template.is_empty():
|
|
84
|
-
script_content +=
|
|
93
|
+
script_content += _script_template(template)
|
|
85
94
|
|
|
86
95
|
var file: FileAccess = FileAccess.open(full_script_path, FileAccess.WRITE)
|
|
87
96
|
if not file:
|
|
@@ -93,21 +102,56 @@ func create_gdscript(params: Dictionary) -> Dictionary:
|
|
|
93
102
|
return _log.failure("Failed to write to script file: " + full_script_path)
|
|
94
103
|
|
|
95
104
|
var written: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
105
|
+
var parses: bool = _parses(written)
|
|
96
106
|
var answer: Dictionary = {
|
|
97
107
|
"success": true,
|
|
98
108
|
"script_path": script_path,
|
|
99
109
|
"full_path": full_script_path,
|
|
100
110
|
"absolute_path": ProjectSettings.globalize_path(full_script_path),
|
|
101
111
|
"template_used": template if not template.is_empty() else "none",
|
|
102
|
-
"parses":
|
|
112
|
+
"parses": parses,
|
|
103
113
|
}
|
|
104
|
-
# Read off the file as the engine parsed it rather than off the arguments, which content leaves
|
|
105
|
-
|
|
114
|
+
# Read off the file as the engine parsed it rather than off the arguments, which content leaves
|
|
115
|
+
# out. A script that does not parse tells the engine nothing, and its empty base and name were
|
|
116
|
+
# answered for a file whose header says otherwise, so those are read off the text instead.
|
|
117
|
+
if parses:
|
|
106
118
|
answer["extends"] = _base_named(written)
|
|
107
119
|
answer["class_name"] = str(written.get_global_name())
|
|
120
|
+
else:
|
|
121
|
+
answer.merge(_header_written(script_content))
|
|
108
122
|
return answer
|
|
109
123
|
|
|
110
124
|
|
|
125
|
+
## The refusal for [param path], which is no GDScript file, naming what [param doing] it instead:
|
|
126
|
+
## a shader is made with resource_edit, and once it exists its text is edited as a file's.
|
|
127
|
+
static func _not_gdscript(path: String, doing: String) -> String:
|
|
128
|
+
var refusal: String = "%s is not a .gd file, and script_edit %s GDScript only." % [path, doing]
|
|
129
|
+
if path.get_extension() != "gdshader":
|
|
130
|
+
return refusal
|
|
131
|
+
if doing == "makes":
|
|
132
|
+
return refusal + " resource_edit create_shader makes a shader, with its source as code."
|
|
133
|
+
return refusal + " A shader's text is edited as a file."
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
## The class_name and extends [param text] declares at its margin, as written. A script with no
|
|
137
|
+
## extends extends RefCounted.
|
|
138
|
+
static func _header_written(text: String) -> Dictionary:
|
|
139
|
+
var header: Dictionary = {"extends": "RefCounted", "class_name": ""}
|
|
140
|
+
for line: String in text.split("\n"):
|
|
141
|
+
# An inner class's extends is indented, and is not the script's.
|
|
142
|
+
if line.begins_with(" ") or line.begins_with("\t"):
|
|
143
|
+
continue
|
|
144
|
+
var bare: String = GdhPatterns.without_annotations(GdhPatterns.without_comment(line))
|
|
145
|
+
var found: RegExMatch = GdhPatterns.declared_class(bare)
|
|
146
|
+
if found != null:
|
|
147
|
+
header["class_name"] = found.get_string(1)
|
|
148
|
+
if not found.get_string(2).is_empty():
|
|
149
|
+
header["extends"] = found.get_string(2)
|
|
150
|
+
elif bare.begins_with("extends "):
|
|
151
|
+
header["extends"] = bare.substr(8).strip_edges()
|
|
152
|
+
return header
|
|
153
|
+
|
|
154
|
+
|
|
111
155
|
# Modify an existing GDScript file by adding functions, variables, or signals
|
|
112
156
|
func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
113
157
|
var script_path: String = str(params.get("script_path", ""))
|
|
@@ -119,6 +163,9 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
119
163
|
if not full_script_path.begins_with("res://"):
|
|
120
164
|
full_script_path = "res://" + full_script_path
|
|
121
165
|
|
|
166
|
+
if not full_script_path.ends_with(".gd"):
|
|
167
|
+
return _log.failure(_not_gdscript(full_script_path, "changes"))
|
|
168
|
+
|
|
122
169
|
if not FileAccess.file_exists(full_script_path):
|
|
123
170
|
return _log.failure(
|
|
124
171
|
GdhFileWalk.new().with_nearest_files(
|
|
@@ -135,7 +182,11 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
|
|
|
135
182
|
|
|
136
183
|
var lines: Array[String] = []
|
|
137
184
|
lines.assign(original_content.split("\n"))
|
|
185
|
+
# Loaded only to learn whether it parsed as it was. What the engine says of that file carries its
|
|
186
|
+
# old line numbers, and beside the errors of the file written it named lines that had moved.
|
|
187
|
+
printerr(GdhLog.ASIDE)
|
|
138
188
|
var before: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
|
|
189
|
+
printerr(GdhLog.RESUMED)
|
|
139
190
|
var parsed_before: bool = _parses(before)
|
|
140
191
|
|
|
141
192
|
# All of them checked before any is made, so a call is made whole or not at all: one skipped for a
|
|
@@ -259,6 +310,44 @@ static func _header_refusal(named: String, base: String) -> String:
|
|
|
259
310
|
return said + "; the nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
260
311
|
|
|
261
312
|
|
|
313
|
+
## Why [param template] cannot be written on [param base], or "". Each template's code calls
|
|
314
|
+
## methods of the class it is written for, so a base outside it makes a script that does not parse.
|
|
315
|
+
## A base whose engine class cannot be read here is left to the engine's parse.
|
|
316
|
+
static func _template_refusal(template: String, base: String, script_path: String) -> String:
|
|
317
|
+
var wanted: String = str(TEMPLATE_BASES.get(template, ""))
|
|
318
|
+
var native: String = _engine_class_of(base, script_path)
|
|
319
|
+
if wanted.is_empty() or native.is_empty():
|
|
320
|
+
return ""
|
|
321
|
+
if native == wanted or ClassDB.is_parent_class(native, wanted):
|
|
322
|
+
return ""
|
|
323
|
+
var named: String = base if native == base else "%s, a %s," % [base, native]
|
|
324
|
+
return (
|
|
325
|
+
(
|
|
326
|
+
"The %s template is written for a %s, and extends %s is not one. Give extends a %s "
|
|
327
|
+
+ "class, or leave it out for %s."
|
|
328
|
+
)
|
|
329
|
+
% [template, wanted, named, wanted, wanted]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
## The engine class [param base] comes down to: itself, the one beneath the script a class_name or
|
|
334
|
+
## a quoted path names, or "" when that script cannot be read.
|
|
335
|
+
static func _engine_class_of(base: String, script_path: String) -> String:
|
|
336
|
+
if ClassDB.class_exists(base):
|
|
337
|
+
return base
|
|
338
|
+
var path: String = ""
|
|
339
|
+
if base.begins_with('"') or base.begins_with("'"):
|
|
340
|
+
path = base.substr(1, base.length() - 2)
|
|
341
|
+
if not path.begins_with("res://") and not path.begins_with("uid://"):
|
|
342
|
+
path = script_path.get_base_dir().path_join(path)
|
|
343
|
+
else:
|
|
344
|
+
path = str(_declared_classes().get(base, ""))
|
|
345
|
+
if path.is_empty() or not ResourceLoader.exists(path):
|
|
346
|
+
return ""
|
|
347
|
+
var script: Script = ResourceLoader.load(path, "Script") as Script
|
|
348
|
+
return "" if script == null else str(script.get_instance_base_type())
|
|
349
|
+
|
|
350
|
+
|
|
262
351
|
# The class_name each script of the project declares, by name, with its path.
|
|
263
352
|
static func _declared_classes() -> Dictionary:
|
|
264
353
|
var declared: Dictionary = {}
|
|
@@ -472,7 +561,18 @@ func _add_signal(lines: Array[String], mod: Dictionary, placed: Array[Dictionary
|
|
|
472
561
|
signal_line += "(" + signal_params + ")"
|
|
473
562
|
|
|
474
563
|
var insert_line: int = _signal_insertion_point(lines)
|
|
475
|
-
|
|
564
|
+
# Set apart by a blank line from the header above and from whatever follows that is not a
|
|
565
|
+
# signal, as the style guide and gdformat lay the signals out.
|
|
566
|
+
var block: Array[String] = [signal_line]
|
|
567
|
+
var above: String = lines[insert_line - 1].strip_edges() if insert_line > 0 else ""
|
|
568
|
+
var at: int = insert_line
|
|
569
|
+
if not above.is_empty() and not GdhPatterns.without_annotations(above).begins_with("signal "):
|
|
570
|
+
block.push_front("")
|
|
571
|
+
at += 1
|
|
572
|
+
var below: String = lines[insert_line].strip_edges() if insert_line < lines.size() else ""
|
|
573
|
+
if not below.is_empty() and not GdhPatterns.without_annotations(below).begins_with("signal "):
|
|
574
|
+
block.append("")
|
|
575
|
+
return at if _insert(lines, insert_line, block, placed) else -1
|
|
476
576
|
|
|
477
577
|
|
|
478
578
|
# Answers with the zero-based index of the `func` line, below the blank lines that separate it.
|
|
@@ -545,6 +645,7 @@ func _variable_insertion_point(lines: Array[String]) -> int:
|
|
|
545
645
|
before_func = i
|
|
546
646
|
break
|
|
547
647
|
|
|
648
|
+
after_header = _past_class_doc(lines, after_header)
|
|
548
649
|
for i: int in range(after_header, before_func):
|
|
549
650
|
if GdhPatterns.without_annotations(lines[i]).begins_with("var "):
|
|
550
651
|
after_header = i + 1
|
|
@@ -552,6 +653,20 @@ func _variable_insertion_point(lines: Array[String]) -> int:
|
|
|
552
653
|
return after_header
|
|
553
654
|
|
|
554
655
|
|
|
656
|
+
## [param at], or the line after the class's doc comment when [param at] follows the header and
|
|
657
|
+
## one opens there. Godot reads the `##` lines straight below `extends` as the class's own
|
|
658
|
+
## documentation, and a line put between them handed that documentation to the line put there.
|
|
659
|
+
static func _past_class_doc(lines: Array[String], at: int) -> int:
|
|
660
|
+
if at == 0 or at > lines.size():
|
|
661
|
+
return at
|
|
662
|
+
var above: String = GdhPatterns.without_annotations(lines[at - 1])
|
|
663
|
+
if not (above.begins_with("extends ") or above.begins_with("class_name ")):
|
|
664
|
+
return at
|
|
665
|
+
while at < lines.size() and lines[at].strip_edges().begins_with("##"):
|
|
666
|
+
at += 1
|
|
667
|
+
return at
|
|
668
|
+
|
|
669
|
+
|
|
555
670
|
func _signal_insertion_point(lines: Array[String]) -> int:
|
|
556
671
|
var after_header: int = 0
|
|
557
672
|
|
|
@@ -566,7 +681,7 @@ func _signal_insertion_point(lines: Array[String]) -> int:
|
|
|
566
681
|
elif line.begins_with("var ") or line.begins_with("func "):
|
|
567
682
|
break
|
|
568
683
|
|
|
569
|
-
return after_header
|
|
684
|
+
return _past_class_doc(lines, after_header)
|
|
570
685
|
|
|
571
686
|
|
|
572
687
|
func _function_insertion_point(lines: Array[String], position: String) -> int:
|
|
@@ -621,24 +736,15 @@ static func _indentation(line: String) -> String:
|
|
|
621
736
|
func _script_template(template_name: String) -> String:
|
|
622
737
|
match template_name:
|
|
623
738
|
"singleton":
|
|
624
|
-
return """##
|
|
625
|
-
##
|
|
626
|
-
|
|
627
|
-
var _instance: Object = null
|
|
739
|
+
return """## One instance for the whole game, which the project makes at start once
|
|
740
|
+
## project_settings add_autoload names it, and every script reaches by that name.
|
|
628
741
|
|
|
629
|
-
func _init() -> void:
|
|
630
|
-
\tif _instance != null:
|
|
631
|
-
\t\tpush_error("Singleton instance already exists!")
|
|
632
|
-
\t\treturn
|
|
633
|
-
\t_instance = self
|
|
634
742
|
|
|
635
743
|
func _ready() -> void:
|
|
636
744
|
\tpass
|
|
637
|
-
|
|
638
|
-
# Add your singleton methods here
|
|
639
745
|
"""
|
|
640
746
|
"state_machine":
|
|
641
|
-
return """##
|
|
747
|
+
return """## A finite state machine: one state at a time, entered and left through change_state.
|
|
642
748
|
|
|
643
749
|
signal state_changed(old_state: String, new_state: String)
|
|
644
750
|
|
|
@@ -647,22 +753,27 @@ var current_state: String = ""
|
|
|
647
753
|
## physics_process(delta); whichever it has are called.
|
|
648
754
|
var states: Dictionary = {}
|
|
649
755
|
|
|
756
|
+
|
|
650
757
|
func _ready() -> void:
|
|
651
758
|
\t_setup_states()
|
|
652
759
|
\tif states.size() > 0:
|
|
653
760
|
\t\tchange_state(str(states.keys()[0]))
|
|
654
761
|
|
|
762
|
+
|
|
655
763
|
func _setup_states() -> void:
|
|
656
764
|
\t# Override this to add states
|
|
657
765
|
\t# Example: states["idle"] = IdleState.new()
|
|
658
766
|
\tpass
|
|
659
767
|
|
|
768
|
+
|
|
660
769
|
func _process(delta: float) -> void:
|
|
661
770
|
\t_call_state(current_state, "process", [delta])
|
|
662
771
|
|
|
772
|
+
|
|
663
773
|
func _physics_process(delta: float) -> void:
|
|
664
774
|
\t_call_state(current_state, "physics_process", [delta])
|
|
665
775
|
|
|
776
|
+
|
|
666
777
|
func change_state(new_state: String) -> void:
|
|
667
778
|
\tif not states.has(new_state):
|
|
668
779
|
\t\tpush_error("State not found: " + new_state)
|
|
@@ -674,6 +785,7 @@ func change_state(new_state: String) -> void:
|
|
|
674
785
|
\t_call_state(current_state, "enter", [])
|
|
675
786
|
\tstate_changed.emit(old_state, new_state)
|
|
676
787
|
|
|
788
|
+
|
|
677
789
|
## Calls a method on the named state when it has one; a state without it is left alone.
|
|
678
790
|
func _call_state(state_name: String, method: String, args: Array) -> void:
|
|
679
791
|
\tif state_name.is_empty() or not states.has(state_name):
|
|
@@ -683,51 +795,47 @@ func _call_state(state_name: String, method: String, args: Array) -> void:
|
|
|
683
795
|
\t\tstate.callv(method, args)
|
|
684
796
|
"""
|
|
685
797
|
"component":
|
|
686
|
-
return """##
|
|
798
|
+
return """## A component: a child node that adds one behaviour to its parent, switched off with disable().
|
|
687
799
|
|
|
688
800
|
@export var enabled: bool = true
|
|
689
801
|
|
|
802
|
+
|
|
690
803
|
func _ready() -> void:
|
|
691
804
|
\tif not enabled:
|
|
692
805
|
\t\tset_process(false)
|
|
693
806
|
\t\tset_physics_process(false)
|
|
694
807
|
|
|
808
|
+
|
|
695
809
|
func _process(_delta: float) -> void:
|
|
696
810
|
\tif not enabled:
|
|
697
811
|
\t\treturn
|
|
698
812
|
\t# Component logic here
|
|
699
813
|
\tpass
|
|
700
814
|
|
|
815
|
+
|
|
701
816
|
func enable() -> void:
|
|
702
817
|
\tenabled = true
|
|
703
818
|
\tset_process(true)
|
|
704
819
|
\tset_physics_process(true)
|
|
705
820
|
|
|
821
|
+
|
|
706
822
|
func disable() -> void:
|
|
707
823
|
\tenabled = false
|
|
708
824
|
\tset_process(false)
|
|
709
825
|
\tset_physics_process(false)
|
|
710
826
|
"""
|
|
711
827
|
"resource":
|
|
712
|
-
return """##
|
|
828
|
+
return """## Data kept in a .tres file, which resource_edit create makes and the game loads.
|
|
713
829
|
|
|
714
830
|
@export var id: String = ""
|
|
715
831
|
@export var display_name: String = ""
|
|
716
832
|
@export var description: String = ""
|
|
717
833
|
|
|
834
|
+
|
|
718
835
|
func _init(p_id: String = "", p_name: String = "", p_desc: String = "") -> void:
|
|
719
836
|
\tid = p_id
|
|
720
837
|
\tdisplay_name = p_name
|
|
721
838
|
\tdescription = p_desc
|
|
722
|
-
|
|
723
|
-
func duplicate_resource() -> Resource:
|
|
724
|
-
\tvar new_resource: Resource = duplicate()
|
|
725
|
-
\treturn new_resource
|
|
726
839
|
"""
|
|
727
840
|
_:
|
|
728
|
-
return ""
|
|
729
|
-
\tpass
|
|
730
|
-
|
|
731
|
-
func _process(_delta: float) -> void:
|
|
732
|
-
\tpass
|
|
733
|
-
"""
|
|
841
|
+
return ""
|
|
@@ -224,18 +224,29 @@ func set_import_options(params: Dictionary) -> Dictionary:
|
|
|
224
224
|
if not refused.is_empty():
|
|
225
225
|
return _log.failure("Nothing was written: " + "; ".join(refused))
|
|
226
226
|
|
|
227
|
+
# One already holding the value given changes nothing, and listing it as updated said it had.
|
|
227
228
|
var updated_keys: Array[String] = []
|
|
229
|
+
var unchanged_keys: Array[String] = []
|
|
228
230
|
for name: String in fitted_values:
|
|
229
|
-
config.
|
|
231
|
+
var held: Variant = config.get_value("params", name, null)
|
|
232
|
+
var value: Variant = fitted_values[name]
|
|
233
|
+
if held != null and typeof(held) == typeof(value) and held == value:
|
|
234
|
+
unchanged_keys.append(name)
|
|
235
|
+
continue
|
|
236
|
+
config.set_value("params", name, value)
|
|
230
237
|
updated_keys.append(name)
|
|
231
|
-
_log.debug("Set " + name + " = " + str(
|
|
238
|
+
_log.debug("Set " + name + " = " + str(value))
|
|
232
239
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
if not updated_keys.is_empty():
|
|
241
|
+
err = config.save(import_file_path)
|
|
242
|
+
if err != OK:
|
|
243
|
+
return _log.failure("Failed to save import file: " + str(err))
|
|
236
244
|
|
|
237
245
|
# The reimport that applies them is the server's, through the editor or the engine's import.
|
|
238
|
-
|
|
246
|
+
var answer: Dictionary = {"resource_path": resource_path, "updated_options": updated_keys}
|
|
247
|
+
if not unchanged_keys.is_empty():
|
|
248
|
+
answer["unchanged_options"] = unchanged_keys
|
|
249
|
+
return answer
|
|
239
250
|
|
|
240
251
|
|
|
241
252
|
## [param given] as the option holds it. Every JSON number arrives as a float, and an enum option
|
|
@@ -6,6 +6,13 @@ extends RefCounted
|
|
|
6
6
|
# Every module preloads this as `GdhLog`. `Logger` is a native class from Godot 4.5 on, and a
|
|
7
7
|
# constant of that name is a parse error rather than a shadowing warning.
|
|
8
8
|
|
|
9
|
+
## Printed around engine output an operation provokes to answer a question of its own, such as
|
|
10
|
+
## whether a script parsed before a change, which src/headless.ts leaves out of the answer: its
|
|
11
|
+
## errors are about a file the answer does not describe. Kept in step with `ASIDE` and `RESUMED`
|
|
12
|
+
## there.
|
|
13
|
+
const ASIDE: String = "[gdharness] aside"
|
|
14
|
+
const RESUMED: String = "[gdharness] resumed"
|
|
15
|
+
|
|
9
16
|
var debug_mode: bool = false
|
|
10
17
|
|
|
11
18
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## Where a property a tool writes lives: on the object given, on a resource built into it, or on an
|
|
5
|
+
## element of one of its lists that the write has to make first.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
8
|
+
|
|
9
|
+
const HEADINGS: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## The entry [param target] declares for [param property], or {} when it declares none, headings
|
|
13
|
+
## left out.
|
|
14
|
+
static func declared_on(target: Object, property: String) -> Dictionary:
|
|
15
|
+
for declared: Dictionary in target.get_property_list():
|
|
16
|
+
if str(declared.get("name", "")) != property:
|
|
17
|
+
continue
|
|
18
|
+
if GdhRead.as_int(declared.get("usage", 0)) & HEADINGS:
|
|
19
|
+
continue
|
|
20
|
+
return declared
|
|
21
|
+
return {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Where [param key] is written: as [holder, name, through], the object holding the property, its
|
|
25
|
+
## name there, and the resources passed on the way. A key that is none of [param target]'s own may
|
|
26
|
+
## start with the name of one holding a resource built into it, then a slash and a key of that
|
|
27
|
+
## resource: "sources/0/0:0/0/probability" is the probability of tile 0:0 in the atlas source a
|
|
28
|
+
## TileSet keeps as sources/0. A colon path cannot reach it, since the tile's own name holds colons.
|
|
29
|
+
## A resource kept in a file of its own is not reached, nor, given the file [param home] that
|
|
30
|
+
## [param target] is saved in, one built into another file, since writing it here would leave that
|
|
31
|
+
## file as it was.
|
|
32
|
+
static func holder_of(target: Object, key: String, home: String = "") -> Array:
|
|
33
|
+
var through: Array[Resource] = []
|
|
34
|
+
var holder: Object = target
|
|
35
|
+
var name: String = key
|
|
36
|
+
while declared_on(holder, name).is_empty():
|
|
37
|
+
var reached: Resource = null
|
|
38
|
+
var at: int = name.find("/")
|
|
39
|
+
while at > 0:
|
|
40
|
+
var held: Resource = built_in_at(holder, name.left(at), home)
|
|
41
|
+
if held != null:
|
|
42
|
+
reached = held
|
|
43
|
+
break
|
|
44
|
+
at = name.find("/", at + 1)
|
|
45
|
+
if reached == null:
|
|
46
|
+
break
|
|
47
|
+
through.append(reached)
|
|
48
|
+
holder = reached
|
|
49
|
+
name = name.substr(at + 1)
|
|
50
|
+
return [holder, name, through]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## The resource [param target] holds under [param property] when it is built into a file, or null;
|
|
54
|
+
## given [param home], only one built into that file or into none yet.
|
|
55
|
+
static func built_in_at(target: Object, property: String, home: String = "") -> Resource:
|
|
56
|
+
var held: Resource = held_at(target, property)
|
|
57
|
+
if held == null or not held.is_built_in():
|
|
58
|
+
return null
|
|
59
|
+
var file: String = held.resource_path.get_slice("::", 0)
|
|
60
|
+
return held if home.is_empty() or file.is_empty() or file == home else null
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
## The file [param target] is saved in: its scene's for a node, as the nodes of an instance are
|
|
64
|
+
## saved in the scene holding it, and its own for a resource; "" when it has none yet.
|
|
65
|
+
static func home_of(target: Object) -> String:
|
|
66
|
+
if target is Node:
|
|
67
|
+
var top: Node = target
|
|
68
|
+
while top.owner != null:
|
|
69
|
+
top = top.owner
|
|
70
|
+
return top.scene_file_path
|
|
71
|
+
if target is Resource:
|
|
72
|
+
var resource: Resource = target
|
|
73
|
+
return resource.resource_path.get_slice("::", 0)
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## The resource [param target] holds under [param property], built in or kept in a file, or null.
|
|
78
|
+
static func held_at(target: Object, property: String) -> Resource:
|
|
79
|
+
if declared_on(target, property).is_empty():
|
|
80
|
+
return null
|
|
81
|
+
var held: Variant = target.get(property)
|
|
82
|
+
return held if held is Resource else null
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
## Makes the element of a list [param name] names on [param target] when it is the next one, as the
|
|
86
|
+
## inspector's Add Element does, and answers "" (also when [param name] is in no list), or why not,
|
|
87
|
+
## [param called] being what [param target] is called. The engine lists an element's properties
|
|
88
|
+
## only once the element exists, so a TileSet with no physics layer had no
|
|
89
|
+
## physics_layer_0/collision_layer to write. A list the engine declares as an array grows through
|
|
90
|
+
## the method named after its elements, add_physics_layer for physics_layer_, given the indices of
|
|
91
|
+
## the lists around it; a list counted by a property, as a tile's polygons are by polygons_count,
|
|
92
|
+
## grows by raising the count.
|
|
93
|
+
static func grow(target: Object, name: String, called: String) -> String:
|
|
94
|
+
var steps: PackedStringArray = name.split("/")
|
|
95
|
+
for depth: int in steps.size() - 1:
|
|
96
|
+
var step: String = steps[depth]
|
|
97
|
+
var cut: int = step.rfind("_")
|
|
98
|
+
if cut < 1 or not step.substr(cut + 1).is_valid_int():
|
|
99
|
+
continue
|
|
100
|
+
var index: int = step.substr(cut + 1).to_int()
|
|
101
|
+
var above: String = "/".join(steps.slice(0, depth))
|
|
102
|
+
var under: String = above + "/" if not above.is_empty() else ""
|
|
103
|
+
var prefix: String = under + step.left(cut + 1)
|
|
104
|
+
var element: String = step.left(cut)
|
|
105
|
+
var counted: String = under + element + "s_count"
|
|
106
|
+
var count: int = -1
|
|
107
|
+
var adding: Callable
|
|
108
|
+
if not declared_on(target, counted).is_empty():
|
|
109
|
+
count = GdhRead.as_int(target.get(counted))
|
|
110
|
+
adding = func() -> void: target.set(counted, count + 1)
|
|
111
|
+
elif _listed_array(target, prefix):
|
|
112
|
+
count = _elements(target, prefix)
|
|
113
|
+
var method: String = "add_" + element
|
|
114
|
+
var outer: Array = []
|
|
115
|
+
for enclosing: String in steps.slice(0, depth):
|
|
116
|
+
var at: int = enclosing.rfind("_")
|
|
117
|
+
if at > 0 and enclosing.substr(at + 1).is_valid_int():
|
|
118
|
+
outer.append(enclosing.substr(at + 1).to_int())
|
|
119
|
+
if target.has_method(method):
|
|
120
|
+
adding = func() -> void: target.callv(method, outer)
|
|
121
|
+
if count < 0 or index < count:
|
|
122
|
+
continue
|
|
123
|
+
var held: String = (
|
|
124
|
+
"%s has %d %s%s"
|
|
125
|
+
% [
|
|
126
|
+
above if not above.is_empty() else called,
|
|
127
|
+
count,
|
|
128
|
+
element.replace("_", " "),
|
|
129
|
+
"" if count == 1 else "s"
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
if index > count:
|
|
133
|
+
return (
|
|
134
|
+
"%s, and a new one goes after the last, so %s%d cannot be written yet: write %s%d first."
|
|
135
|
+
% [held, prefix, index, prefix, count]
|
|
136
|
+
)
|
|
137
|
+
if not adding.is_valid():
|
|
138
|
+
return "%s, and nothing makes another, so %s cannot be written." % [held, name]
|
|
139
|
+
adding.call()
|
|
140
|
+
return ""
|
|
141
|
+
return ""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## Whether [param target] declares a list whose elements' properties start with [param prefix],
|
|
145
|
+
## as TileSet declares physics_layers for physics_layer_0/collision_layer.
|
|
146
|
+
static func _listed_array(target: Object, prefix: String) -> bool:
|
|
147
|
+
for declared: Dictionary in target.get_property_list():
|
|
148
|
+
var usage: int = GdhRead.as_int(declared.get("usage", 0))
|
|
149
|
+
if usage & PROPERTY_USAGE_ARRAY and str(declared.get("class_name", "")) == prefix:
|
|
150
|
+
return true
|
|
151
|
+
return false
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## How many elements of the list whose properties start with [param prefix] [param target] holds.
|
|
155
|
+
static func _elements(target: Object, prefix: String) -> int:
|
|
156
|
+
var count: int = 0
|
|
157
|
+
for declared: Dictionary in target.get_property_list():
|
|
158
|
+
var name: String = str(declared.get("name", ""))
|
|
159
|
+
if not name.begins_with(prefix):
|
|
160
|
+
continue
|
|
161
|
+
var index: String = name.substr(prefix.length()).get_slice("/", 0)
|
|
162
|
+
if index.is_valid_int():
|
|
163
|
+
count = maxi(count, index.to_int() + 1)
|
|
164
|
+
return count
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
## What [param target] is called in a refusal: the class_name its script declares, or else its
|
|
168
|
+
## engine class. A resource made as a project class is a Resource to the engine, and "Resource.kind"
|
|
169
|
+
## named a property no Resource has.
|
|
170
|
+
static func class_called(target: Object) -> String:
|
|
171
|
+
var attached: Variant = target.get_script()
|
|
172
|
+
if attached is Script:
|
|
173
|
+
var script: Script = attached
|
|
174
|
+
if not str(script.get_global_name()).is_empty():
|
|
175
|
+
return str(script.get_global_name())
|
|
176
|
+
return target.get_class()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
## The refusal for [param name], which [param target] does not declare, when it runs through a
|
|
180
|
+
## resource saved in a file other than [param home], the one [param target] is saved in; or "". The
|
|
181
|
+
## write belongs in that file, since this one would be saved and that one left as it was: through a
|
|
182
|
+
## TileMapLayer's tile_set kept in a file of its own, or through the mesh of an instance's editable
|
|
183
|
+
## child, which is built into the scene the instance comes from.
|
|
184
|
+
static func held_elsewhere(target: Object, name: String, home: String) -> String:
|
|
185
|
+
var at: int = name.find("/")
|
|
186
|
+
while at > 0:
|
|
187
|
+
var head: String = name.left(at)
|
|
188
|
+
var rest: String = name.substr(at + 1)
|
|
189
|
+
var held: Resource = held_at(target, head)
|
|
190
|
+
var file: String = "" if held == null else held.resource_path.get_slice("::", 0)
|
|
191
|
+
if held != null and not held.is_built_in():
|
|
192
|
+
return (
|
|
193
|
+
"%s.%s is %s, a file of its own, so %s is written there, with resource_edit modify on %s."
|
|
194
|
+
% [class_called(target), head, file, rest, file]
|
|
195
|
+
)
|
|
196
|
+
if held != null and not file.is_empty() and not home.is_empty() and file != home:
|
|
197
|
+
return _built_in_elsewhere(target, head, rest, held, file)
|
|
198
|
+
at = name.find("/", at + 1)
|
|
199
|
+
return ""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
## The refusal for writing [param rest] into [param held], which [param target] holds under
|
|
203
|
+
## [param head] and which is built into [param file], the scene an instance in this one comes from.
|
|
204
|
+
static func _built_in_elsewhere(
|
|
205
|
+
target: Object, head: String, rest: String, held: Resource, file: String
|
|
206
|
+
) -> String:
|
|
207
|
+
var node: String = ""
|
|
208
|
+
if target is Node:
|
|
209
|
+
var placed: Node = target
|
|
210
|
+
var from: Node = placed
|
|
211
|
+
while from != null and from.scene_file_path != file:
|
|
212
|
+
from = from.owner
|
|
213
|
+
if from != null:
|
|
214
|
+
node = " on %s" % from.get_path_to(placed)
|
|
215
|
+
var text: String = (
|
|
216
|
+
(
|
|
217
|
+
"%s.%s is built into %s, so %s is written there, with scene_node set%s in %s, which changes "
|
|
218
|
+
+ "it wherever that scene is used. To change it here alone, give %s a %s of its own: a "
|
|
219
|
+
+ 'value with "_type" set to its class and its properties beside it.'
|
|
220
|
+
)
|
|
221
|
+
% [class_called(target), head, file, rest, node, file, head, held.get_class()]
|
|
222
|
+
)
|
|
223
|
+
if target is GeometryInstance3D and (held is Material or rest.contains("material")):
|
|
224
|
+
text += " A material for this instance alone goes in material_override."
|
|
225
|
+
return text
|