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
|
@@ -189,6 +189,21 @@ static func offered(said: String, word: String, names: Array) -> String:
|
|
|
189
189
|
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
190
190
|
|
|
191
191
|
|
|
192
|
+
## [param said], the refusal for [param property], which [param target] does not declare, with the
|
|
193
|
+
## names it declares nearest it when any are, as a misspelt one is usually one of them. Unlike
|
|
194
|
+
## [method offered], nothing is listed when none is near, since an object declares a hundred.
|
|
195
|
+
static func near_property(said: String, target: Object, property: String) -> String:
|
|
196
|
+
var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
197
|
+
var names: Array[String] = []
|
|
198
|
+
for declared: Dictionary in target.get_property_list():
|
|
199
|
+
if not as_int(declared.get("usage", 0)) & headings:
|
|
200
|
+
names.append(str(declared.get("name", "")))
|
|
201
|
+
var close: PackedStringArray = nearest(property, PackedStringArray(names))
|
|
202
|
+
if close.is_empty():
|
|
203
|
+
return said
|
|
204
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
205
|
+
|
|
206
|
+
|
|
192
207
|
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
193
208
|
## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
|
|
194
209
|
## starting with an underscore are the engine's own.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## Whether a value read back is the value that was given, as the tools that write properties judge
|
|
5
|
+
## what a write kept and what it changed besides.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
8
|
+
const GdhSerialisation = preload("serialisation.gd")
|
|
9
|
+
|
|
10
|
+
# The types made of floats, which a read back compares within float precision: a Vector2 property
|
|
11
|
+
# stores 32-bit components, so 0.1 comes back as 0.100000001.
|
|
12
|
+
const FLOAT_SHAPES: Array[int] = [
|
|
13
|
+
TYPE_VECTOR2,
|
|
14
|
+
TYPE_VECTOR3,
|
|
15
|
+
TYPE_VECTOR4,
|
|
16
|
+
TYPE_QUATERNION,
|
|
17
|
+
TYPE_COLOR,
|
|
18
|
+
TYPE_RECT2,
|
|
19
|
+
TYPE_TRANSFORM2D,
|
|
20
|
+
TYPE_BASIS,
|
|
21
|
+
TYPE_TRANSFORM3D,
|
|
22
|
+
TYPE_PLANE,
|
|
23
|
+
TYPE_AABB,
|
|
24
|
+
TYPE_PROJECTION,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
const PACKED: Array[int] = [
|
|
28
|
+
TYPE_PACKED_BYTE_ARRAY,
|
|
29
|
+
TYPE_PACKED_INT32_ARRAY,
|
|
30
|
+
TYPE_PACKED_INT64_ARRAY,
|
|
31
|
+
TYPE_PACKED_FLOAT32_ARRAY,
|
|
32
|
+
TYPE_PACKED_FLOAT64_ARRAY,
|
|
33
|
+
TYPE_PACKED_STRING_ARRAY,
|
|
34
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
35
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
36
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
37
|
+
TYPE_PACKED_COLOR_ARRAY,
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# The pairs of built-in resources being compared, so a resource that holds itself ends the
|
|
41
|
+
# comparison instead of recursing into it for ever.
|
|
42
|
+
static var _comparing: Array[Array] = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Whether [param held] is [param given], as a property read back after a set answers it.
|
|
46
|
+
##
|
|
47
|
+
## Close enough rather than equal for anything made of floats: a Vector2 property stores 32-bit
|
|
48
|
+
## components, so 0.1 comes back as 0.100000001. A packed array read back against the list it was
|
|
49
|
+
## given compares element by element, since `==` between the two is an error rather than false.
|
|
50
|
+
static func same(held: Variant, given: Variant) -> bool:
|
|
51
|
+
var held_type: int = typeof(held)
|
|
52
|
+
var given_type: int = typeof(given)
|
|
53
|
+
# An empty object property reads back as an Object that is null, not as nil, and the two are
|
|
54
|
+
# the same nothing.
|
|
55
|
+
var held_none: bool = held_type == TYPE_NIL or (held_type == TYPE_OBJECT and held == null)
|
|
56
|
+
var given_none: bool = given_type == TYPE_NIL or (given_type == TYPE_OBJECT and given == null)
|
|
57
|
+
if held_none or given_none:
|
|
58
|
+
return held_none == given_none
|
|
59
|
+
if held_type == TYPE_OBJECT or given_type == TYPE_OBJECT:
|
|
60
|
+
return _same_object(held, given)
|
|
61
|
+
if GdhSerialisation.NUMBERS.has(held_type) and GdhSerialisation.NUMBERS.has(given_type):
|
|
62
|
+
if held_type == TYPE_BOOL or given_type == TYPE_BOOL:
|
|
63
|
+
return held_type == given_type and held == given
|
|
64
|
+
var held_number: float = GdhRead.as_float(held)
|
|
65
|
+
var given_number: float = GdhRead.as_float(given)
|
|
66
|
+
return (is_nan(held_number) and is_nan(given_number)) or is_equal_approx(held_number, given_number)
|
|
67
|
+
if GdhSerialisation.TEXTS.has(held_type) and GdhSerialisation.TEXTS.has(given_type):
|
|
68
|
+
return str(held) == str(given)
|
|
69
|
+
if _listed(held_type) and _listed(given_type):
|
|
70
|
+
var held_items: Array = type_convert(held, TYPE_ARRAY)
|
|
71
|
+
var given_items: Array = type_convert(given, TYPE_ARRAY)
|
|
72
|
+
if held_items.size() != given_items.size():
|
|
73
|
+
return false
|
|
74
|
+
for index: int in held_items.size():
|
|
75
|
+
if not same(held_items[index], given_items[index]):
|
|
76
|
+
return false
|
|
77
|
+
return true
|
|
78
|
+
if held_type == TYPE_DICTIONARY and given_type == TYPE_DICTIONARY:
|
|
79
|
+
var held_entries: Dictionary = held
|
|
80
|
+
var given_entries: Dictionary = given
|
|
81
|
+
return _same_entries(held_entries, given_entries)
|
|
82
|
+
if held_type != given_type:
|
|
83
|
+
return false
|
|
84
|
+
return _same_shape(held, given)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
static func _same_object(held: Variant, given: Variant) -> bool:
|
|
88
|
+
if typeof(held) != TYPE_OBJECT or typeof(given) != TYPE_OBJECT:
|
|
89
|
+
return false
|
|
90
|
+
if is_same(held, given):
|
|
91
|
+
return true
|
|
92
|
+
if not held is Resource or not given is Resource:
|
|
93
|
+
return false
|
|
94
|
+
var held_resource: Resource = held
|
|
95
|
+
var given_resource: Resource = given
|
|
96
|
+
if not _built_in(held_resource):
|
|
97
|
+
return held_resource.resource_path == given_resource.resource_path
|
|
98
|
+
if held_resource.get_class() != given_resource.get_class():
|
|
99
|
+
return false
|
|
100
|
+
# A resource local to the scene is duplicated for every instance, so the copy read back is
|
|
101
|
+
# another object of the same class.
|
|
102
|
+
if held_resource.resource_local_to_scene:
|
|
103
|
+
return true
|
|
104
|
+
return _built_in(given_resource) and _same_stored(held_resource, given_resource)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
## A resource no other file can refer to: one with no path, or one of a file's own sub-resources,
|
|
108
|
+
## whose path is the file's with "::" and an id after it.
|
|
109
|
+
static func _built_in(resource: Resource) -> bool:
|
|
110
|
+
return resource.resource_path.is_empty() or resource.resource_path.contains("::")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
## Two built-in resources of one class, by every property they store. Neither has a path to be
|
|
114
|
+
## known by, and a file loaded again holds a new object for each, so a Curve written into a
|
|
115
|
+
## resource file and read back was judged lost by the save that had just kept it.
|
|
116
|
+
static func _same_stored(held: Resource, given: Resource) -> bool:
|
|
117
|
+
for pair: Array in _comparing:
|
|
118
|
+
if is_same(pair[0], held) and is_same(pair[1], given):
|
|
119
|
+
return true
|
|
120
|
+
_comparing.append([held, given])
|
|
121
|
+
var alike: bool = true
|
|
122
|
+
for property: Dictionary in held.get_property_list():
|
|
123
|
+
var usage: int = GdhRead.as_int(property.get("usage", 0))
|
|
124
|
+
var name: String = str(property.get("name", ""))
|
|
125
|
+
if (
|
|
126
|
+
not (usage & PROPERTY_USAGE_STORAGE)
|
|
127
|
+
or (name.begins_with("resource_") and name != "resource_name")
|
|
128
|
+
):
|
|
129
|
+
continue
|
|
130
|
+
if not same(held.get(name), given.get(name)):
|
|
131
|
+
alike = false
|
|
132
|
+
break
|
|
133
|
+
_comparing.pop_back()
|
|
134
|
+
return alike
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
static func _same_entries(held: Dictionary, given: Dictionary) -> bool:
|
|
138
|
+
if held.size() != given.size():
|
|
139
|
+
return false
|
|
140
|
+
for key: Variant in given:
|
|
141
|
+
var found: bool = false
|
|
142
|
+
for held_key: Variant in held:
|
|
143
|
+
if same(held_key, key):
|
|
144
|
+
found = same(held[held_key], given[key])
|
|
145
|
+
break
|
|
146
|
+
if not found:
|
|
147
|
+
return false
|
|
148
|
+
return true
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Two values of one type made of floats, compared component by component through the type's own
|
|
152
|
+
## text form, which lists every component: each type has its own is_equal_approx, and reaching it
|
|
153
|
+
## through a Variant is a call the typed gate refuses.
|
|
154
|
+
static func _same_shape(held: Variant, given: Variant) -> bool:
|
|
155
|
+
if typeof(held) not in FLOAT_SHAPES:
|
|
156
|
+
return held == given
|
|
157
|
+
var held_parts: Array[float] = _components(held)
|
|
158
|
+
var given_parts: Array[float] = _components(given)
|
|
159
|
+
if held_parts.size() != given_parts.size():
|
|
160
|
+
return false
|
|
161
|
+
for index: int in held_parts.size():
|
|
162
|
+
if not same(held_parts[index], given_parts[index]):
|
|
163
|
+
return false
|
|
164
|
+
return true
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
static func _components(value: Variant) -> Array[float]:
|
|
168
|
+
var parts: Array[float] = []
|
|
169
|
+
var text: String = var_to_str(value)
|
|
170
|
+
var inner: String = text.substr(text.find("(") + 1).trim_suffix(")")
|
|
171
|
+
for part: String in inner.split(",", false):
|
|
172
|
+
parts.append(part.strip_edges().to_float())
|
|
173
|
+
return parts
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
static func _listed(type: int) -> bool:
|
|
177
|
+
return type == TYPE_ARRAY or PACKED.has(type)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c1a7btkxcss02
|
|
@@ -29,8 +29,8 @@ const GdhHeadlessCheck = preload("headless_check.gd")
|
|
|
29
29
|
|
|
30
30
|
## How many times what scripts changed is put back before a write is refused for it.
|
|
31
31
|
const PUTTING_BACK_PASSES: int = 4
|
|
32
|
-
const GdhPropertyValues = preload("property_values.gd")
|
|
33
32
|
const GdhRead = preload("reading.gd")
|
|
33
|
+
const GdhSame = preload("same_values.gd")
|
|
34
34
|
## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
|
|
35
35
|
## the persistence every connection in a saved scene carries.
|
|
36
36
|
const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
@@ -62,7 +62,8 @@ var _asked: Dictionary = {}
|
|
|
62
62
|
var _before: Dictionary = {}
|
|
63
63
|
## Each copy a tool made, by instance id, to the instance id of the node it copies.
|
|
64
64
|
var _copied_from: Dictionary = {}
|
|
65
|
-
##
|
|
65
|
+
## What a tool wrote in the embedded resources it reached into, by their id in the file, as
|
|
66
|
+
## [method GdhHeadlessCheck.reached] answers it.
|
|
66
67
|
var _reached: Dictionary = {}
|
|
67
68
|
## The file as it was opened, to compare its embedded resources with what the write saves.
|
|
68
69
|
var _text: String = ""
|
|
@@ -140,7 +141,7 @@ func asked(node: Node, names: Array) -> void:
|
|
|
140
141
|
var property: String = str(name)
|
|
141
142
|
on[property] = true
|
|
142
143
|
on[property.get_slice(":", 0)] = true
|
|
143
|
-
|
|
144
|
+
GdhHeadlessCheck.spare(_reached, GdhHeadlessCheck.reached(node, property))
|
|
144
145
|
|
|
145
146
|
|
|
146
147
|
## Takes [param resource], built into the scene, as one the tool changed, which a write saves as it is.
|
|
@@ -251,6 +252,51 @@ func path_of(node: Node) -> String:
|
|
|
251
252
|
return str(root.get_path_to(node))
|
|
252
253
|
|
|
253
254
|
|
|
255
|
+
## The class of [param node] as a read answers it: nodeType the engine class, and scriptClass the
|
|
256
|
+
## class its script declares, when it declares one.
|
|
257
|
+
static func class_of(node: Node) -> Dictionary:
|
|
258
|
+
var named: Dictionary = {"nodeType": node.get_class()}
|
|
259
|
+
var script: Script = node.get_script()
|
|
260
|
+
if script != null and not str(script.get_global_name()).is_empty():
|
|
261
|
+
named["scriptClass"] = str(script.get_global_name())
|
|
262
|
+
return named
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
## [param given] as a res:// path, one inside [param project_path] taken relative to it.
|
|
266
|
+
static func res_path(project_path: String, given: String) -> String:
|
|
267
|
+
var p: String = given.strip_edges()
|
|
268
|
+
if p.begins_with("res://"):
|
|
269
|
+
return p
|
|
270
|
+
if not project_path.strip_edges().is_empty():
|
|
271
|
+
var normalized_project: String = project_path.replace("\\", "/")
|
|
272
|
+
var normalized: String = p.replace("\\", "/")
|
|
273
|
+
if normalized.begins_with(normalized_project):
|
|
274
|
+
p = normalized.substr(normalized_project.length()).trim_prefix("/")
|
|
275
|
+
return "res://" + p
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
## The node at [param at] under [param top], "." or "" naming [param top] itself, or null.
|
|
279
|
+
static func node_at(top: Node, at: String) -> Node:
|
|
280
|
+
if at == "." or at.is_empty():
|
|
281
|
+
return top
|
|
282
|
+
return top.get_node_or_null(at)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
## Why the property or signal [param missing] could not be found on [param node], adding the one
|
|
286
|
+
## cause the name alone does not show: a script that does not compile leaves the editor holding a
|
|
287
|
+
## placeholder with no properties or signals at all, and the refusal named the property instead.
|
|
288
|
+
## Such a script has no engine class beneath it. Compiling a copy of the source to find out failed on
|
|
289
|
+
## every script with a class_name, since the copy hides the global class the original declares.
|
|
290
|
+
static func missing_on(node: Node, missing: String) -> String:
|
|
291
|
+
var script: Script = node.get_script()
|
|
292
|
+
if script == null or not str(script.get_instance_base_type()).is_empty():
|
|
293
|
+
return missing
|
|
294
|
+
return (
|
|
295
|
+
"%s. Its script, %s, does not compile, so none of what it declares is known here."
|
|
296
|
+
% [missing.trim_suffix("."), script.resource_path]
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
254
300
|
## Why [param node]'s own properties and connections cannot be written, or "". A node inside an
|
|
255
301
|
## instanced scene belongs to that scene's file unless its children were made editable here.
|
|
256
302
|
func foreign(node: Node) -> String:
|
|
@@ -299,27 +345,42 @@ func _closed_around(node: Node) -> PackedStringArray:
|
|
|
299
345
|
|
|
300
346
|
|
|
301
347
|
## Why [param node] cannot be moved, renamed, copied or deleted, or "": the scene may only
|
|
302
|
-
## restructure nodes it declares itself, not ones its base scene or an instance creates.
|
|
303
|
-
|
|
348
|
+
## restructure nodes it declares itself, not ones its base scene or an instance creates. With
|
|
349
|
+
## [param deleting], the refusal says what can be done instead, since nothing in this scene can.
|
|
350
|
+
func fixed(node: Node, deleting: bool = false) -> String:
|
|
304
351
|
if node == root:
|
|
305
352
|
return "The root of %s cannot be moved, renamed, copied or deleted." % path
|
|
306
353
|
if node.owner != root:
|
|
307
354
|
var why: String = foreign(node)
|
|
308
355
|
if why.is_empty():
|
|
309
356
|
why = "%s is inside an instanced scene, which creates it again on load." % path_of(node)
|
|
357
|
+
if deleting:
|
|
358
|
+
why += _deleted_elsewhere(node, node.owner.scene_file_path)
|
|
310
359
|
return why
|
|
311
360
|
var at: String = path_of(node)
|
|
312
361
|
if _base_paths.has(at):
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
+ "scene instead."
|
|
317
|
-
)
|
|
318
|
-
% [at, _base_paths[at], path]
|
|
362
|
+
var maker: String = str(_base_paths[at])
|
|
363
|
+
var made: String = (
|
|
364
|
+
"%s is made by %s, a scene %s inherits, which creates it again on load." % [at, maker, path]
|
|
319
365
|
)
|
|
366
|
+
return made + (_deleted_elsewhere(node, maker) if deleting else " Edit that scene instead.")
|
|
320
367
|
return ""
|
|
321
368
|
|
|
322
369
|
|
|
370
|
+
## What a caller can do with [param node], made by [param maker], instead of deleting it here.
|
|
371
|
+
static func _deleted_elsewhere(node: Node, maker: String) -> String:
|
|
372
|
+
var hidden: String = (
|
|
373
|
+
"set its visible to false here to hide it in this scene alone, or " if "visible" in node else ""
|
|
374
|
+
)
|
|
375
|
+
return (
|
|
376
|
+
(
|
|
377
|
+
" No scene can delete a node another scene makes: %sdelete it in %s, which removes it from "
|
|
378
|
+
+ "every scene using that one."
|
|
379
|
+
)
|
|
380
|
+
% [hidden, maker]
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
|
|
323
384
|
## Why the folder a new file at [param file_path] goes in cannot be made, or "" once it is there. The
|
|
324
385
|
## engine makes no folder when it saves, so a create into one not yet made failed with no more than
|
|
325
386
|
## the engine's error, while script_edit create made its folder and went ahead.
|
|
@@ -437,7 +498,9 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
|
|
|
437
498
|
return unfit
|
|
438
499
|
var taken: Node = parent.get_node_or_null(NodePath(child_name))
|
|
439
500
|
if taken != null and taken != moving and taken.get_parent() == parent:
|
|
440
|
-
|
|
501
|
+
# The root by its name, since its path is ".", and ". already has a child" read as a typo.
|
|
502
|
+
var holder: String = "The root, %s," % parent.name if parent == root else path_of(parent)
|
|
503
|
+
return "%s already has a child named %s." % [holder, child_name]
|
|
441
504
|
return ""
|
|
442
505
|
|
|
443
506
|
|
|
@@ -457,6 +520,17 @@ func values_from(node: Node, at: String) -> String:
|
|
|
457
520
|
return str(_base_paths.get(at, ""))
|
|
458
521
|
|
|
459
522
|
|
|
523
|
+
## The groups this scene's own file puts the node at [param at] in.
|
|
524
|
+
func groups_at(at: String) -> PackedStringArray:
|
|
525
|
+
var named: PackedStringArray = []
|
|
526
|
+
if _state == null:
|
|
527
|
+
return named
|
|
528
|
+
for index: int in _state.get_node_count():
|
|
529
|
+
if _relative(str(_state.get_node_path(index))) == at:
|
|
530
|
+
named.append_array(_state.get_node_groups(index))
|
|
531
|
+
return named
|
|
532
|
+
|
|
533
|
+
|
|
460
534
|
## The names of the properties this scene's own file sets on the node at [param at].
|
|
461
535
|
func set_at(at: String) -> Dictionary:
|
|
462
536
|
var names: Dictionary = {}
|
|
@@ -471,13 +545,24 @@ func set_at(at: String) -> Dictionary:
|
|
|
471
545
|
|
|
472
546
|
|
|
473
547
|
## Whether [param loaded], read off [param saved] (a copy instantiated from the pack), is
|
|
474
|
-
## [param given], held by the node in [member root]. A node is compared by where it sits in each
|
|
548
|
+
## [param given], held by the node in [member root]. A node is compared by where it sits in each,
|
|
549
|
+
## in a list of nodes as much as alone: the copy's nodes are others with the same places.
|
|
475
550
|
func kept(saved: Node, loaded: Variant, given: Variant) -> bool:
|
|
476
551
|
if loaded is Node and given is Node:
|
|
477
552
|
var loaded_node: Node = loaded
|
|
478
553
|
var given_node: Node = given
|
|
479
554
|
return str(saved.get_path_to(loaded_node)) == str(root.get_path_to(given_node))
|
|
480
|
-
|
|
555
|
+
if loaded is Array and given is Array:
|
|
556
|
+
var loaded_items: Array = loaded
|
|
557
|
+
var given_items: Array = given
|
|
558
|
+
if given_items.any(func(item: Variant) -> bool: return item is Node):
|
|
559
|
+
if loaded_items.size() != given_items.size():
|
|
560
|
+
return false
|
|
561
|
+
for index: int in given_items.size():
|
|
562
|
+
if not kept(saved, loaded_items[index], given_items[index]):
|
|
563
|
+
return false
|
|
564
|
+
return true
|
|
565
|
+
return GdhSame.same(loaded, given)
|
|
481
566
|
|
|
482
567
|
|
|
483
568
|
## Packs [member root], has [param check] read the result as the file will hold it, and writes the
|
|
@@ -805,7 +890,7 @@ func _strays(state: SceneState) -> Array[Array]:
|
|
|
805
890
|
for key: Variant in names:
|
|
806
891
|
var name: String = str(key)
|
|
807
892
|
var held: bool = filed.has(name)
|
|
808
|
-
if held == stored.has(name) and (not held or
|
|
893
|
+
if held == stored.has(name) and (not held or GdhSame.same(stored[name], filed[name])):
|
|
809
894
|
continue
|
|
810
895
|
if not held and GdhHeadlessCheck.at_default(node, name):
|
|
811
896
|
continue
|
|
@@ -121,6 +121,10 @@ const BUILDERS: Dictionary = {
|
|
|
121
121
|
"Dictionary": "_build_dictionary",
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
## The built-in resources being described, outermost first, so one holding itself somewhere inside
|
|
125
|
+
## is answered by its class rather than without end.
|
|
126
|
+
var _describing: Array[Resource] = []
|
|
127
|
+
|
|
124
128
|
|
|
125
129
|
# Converts a Godot value into something JSON can carry. A type with no entry in the table
|
|
126
130
|
# passes through as itself, which is what the JSON-native ones want.
|
|
@@ -665,9 +669,21 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
665
669
|
return _describe_input_event(event)
|
|
666
670
|
if value is Resource:
|
|
667
671
|
var resource: Resource = value
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
672
|
+
# A file's built-in resources load with a path of the file's own and "::", which no other
|
|
673
|
+
# file can refer to.
|
|
674
|
+
if resource.resource_path.is_empty() or resource.resource_path.contains("::"):
|
|
675
|
+
return _describe_built_in(resource)
|
|
676
|
+
var filed: Dictionary = {
|
|
677
|
+
"path": resource.resource_path, "class": resource.get_class(), "_type": "Resource"
|
|
678
|
+
}
|
|
679
|
+
# The engine class of a resource with a script is the one its script extends, so a Stats
|
|
680
|
+
# file read as a Resource and nothing said it was a Stats.
|
|
681
|
+
var attached: Variant = resource.get_script()
|
|
682
|
+
if attached is Script:
|
|
683
|
+
var script: Script = attached
|
|
684
|
+
if not script.get_global_name().is_empty():
|
|
685
|
+
filed["scriptClass"] = str(script.get_global_name())
|
|
686
|
+
return filed
|
|
671
687
|
if value is Node:
|
|
672
688
|
var node: Node = value
|
|
673
689
|
if node.is_inside_tree():
|
|
@@ -675,6 +691,62 @@ func _serialize_object(value: Object) -> Variant:
|
|
|
675
691
|
return _describe_object(value)
|
|
676
692
|
|
|
677
693
|
|
|
694
|
+
## A resource built into the file it was read from, in the form a write builds one from: its class
|
|
695
|
+
## under _type beside every property it stores away from its default. As its class alone it said
|
|
696
|
+
## nothing of what it held, so a CollisionShape2D's radius could not be read, and it could not be
|
|
697
|
+
## written back.
|
|
698
|
+
func _describe_built_in(resource: Resource) -> Dictionary:
|
|
699
|
+
if _describing.has(resource):
|
|
700
|
+
return {"class": resource.get_class(), "_type": "Resource"}
|
|
701
|
+
_describing.append(resource)
|
|
702
|
+
var described: Dictionary = {"_type": resource.get_class()}
|
|
703
|
+
var attached: Variant = resource.get_script()
|
|
704
|
+
var script: Script = null
|
|
705
|
+
if attached is Script:
|
|
706
|
+
script = attached
|
|
707
|
+
described["script"] = serialize_value(script)
|
|
708
|
+
var fresh: Object = _fresh(script)
|
|
709
|
+
for property: Dictionary in resource.get_property_list():
|
|
710
|
+
var usage: int = property.get("usage", 0)
|
|
711
|
+
var name: String = str(property.get("name", ""))
|
|
712
|
+
if (
|
|
713
|
+
not (usage & PROPERTY_USAGE_STORAGE)
|
|
714
|
+
or name == "script"
|
|
715
|
+
or (name.begins_with("resource_") and name != "resource_name")
|
|
716
|
+
):
|
|
717
|
+
continue
|
|
718
|
+
var held: Variant = resource.get(name)
|
|
719
|
+
var default: Variant = ClassDB.class_get_property_default_value(resource.get_class(), name)
|
|
720
|
+
if fresh != null:
|
|
721
|
+
default = fresh.get(name)
|
|
722
|
+
elif script != null and usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
723
|
+
default = script.get_property_default_value(name)
|
|
724
|
+
if typeof(held) == typeof(default) and held == default:
|
|
725
|
+
continue
|
|
726
|
+
described[name] = serialize_value(held)
|
|
727
|
+
_describing.pop_back()
|
|
728
|
+
return described
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
## A new instance of [param script] to read defaults off, or null where none should be made. What
|
|
732
|
+
## the script's own code sets as it is made (a resource_name in _init, a list built by a
|
|
733
|
+
## constructor, which the engine reports no default for) is then a default rather than a value
|
|
734
|
+
## the file holds. None is made for a script whose _init needs arguments, nor in the editor for one
|
|
735
|
+
## that is not a tool: the editor never runs those, and can_instantiate says so.
|
|
736
|
+
static func _fresh(script: Script) -> Object:
|
|
737
|
+
if script == null or not script.can_instantiate():
|
|
738
|
+
return null
|
|
739
|
+
for method: Dictionary in script.get_script_method_list():
|
|
740
|
+
if str(method.get("name", "")) == "_init":
|
|
741
|
+
var arguments: Array = method.get("args", [])
|
|
742
|
+
var defaults: Array = method.get("default_args", [])
|
|
743
|
+
if arguments.size() > defaults.size():
|
|
744
|
+
return null
|
|
745
|
+
# Called by name: new() is declared on each kind of script rather than on Script.
|
|
746
|
+
var made: Variant = script.call("new")
|
|
747
|
+
return made if made is Object else null
|
|
748
|
+
|
|
749
|
+
|
|
678
750
|
## An input event, by what it is bound to: the engine's own words for it and every field it stores
|
|
679
751
|
## away from its default. As a bare resource it was its class and nothing else, so an input action
|
|
680
752
|
## read back from project.godot could not say whether it was bound to Space or to a joypad button.
|
|
@@ -6,6 +6,7 @@ extends Node
|
|
|
6
6
|
## instead of leaving a command silently unanswered.
|
|
7
7
|
|
|
8
8
|
const GdhSceneTools = preload("tools/scene_tools.gd")
|
|
9
|
+
const GdhSignalTools = preload("tools/signal_tools.gd")
|
|
9
10
|
const GdhResourceTools = preload("tools/resource_tools.gd")
|
|
10
11
|
const GdhAnimationTools = preload("tools/animation_tools.gd")
|
|
11
12
|
const GdhPlayTools = preload("tools/play_tools.gd")
|
|
@@ -17,6 +18,7 @@ const GdhProfileTools = preload("tools/profile_tools.gd")
|
|
|
17
18
|
var _editor_plugin: EditorPlugin = null
|
|
18
19
|
|
|
19
20
|
var _scene_tools: GdhSceneTools = null
|
|
21
|
+
var _signal_tools: GdhSignalTools = null
|
|
20
22
|
var _resource_tools: GdhResourceTools = null
|
|
21
23
|
var _animation_tools: GdhAnimationTools = null
|
|
22
24
|
var _play_tools: GdhPlayTools = null
|
|
@@ -50,6 +52,10 @@ func _init_tools() -> void:
|
|
|
50
52
|
_scene_tools.name = "SceneTools"
|
|
51
53
|
add_child(_scene_tools)
|
|
52
54
|
|
|
55
|
+
_signal_tools = GdhSignalTools.new()
|
|
56
|
+
_signal_tools.name = "SignalTools"
|
|
57
|
+
add_child(_signal_tools)
|
|
58
|
+
|
|
53
59
|
_resource_tools = GdhResourceTools.new()
|
|
54
60
|
_resource_tools.name = "ResourceTools"
|
|
55
61
|
add_child(_resource_tools)
|
|
@@ -89,9 +95,9 @@ func _init_tools() -> void:
|
|
|
89
95
|
"set_node_properties": [_scene_tools, "set_node_properties"],
|
|
90
96
|
"get_node_properties": [_scene_tools, "get_node_properties"],
|
|
91
97
|
"save_scene": [_scene_tools, "save_scene"],
|
|
92
|
-
"connect_signal": [
|
|
93
|
-
"disconnect_signal": [
|
|
94
|
-
"list_connections": [
|
|
98
|
+
"connect_signal": [_signal_tools, "connect_signal"],
|
|
99
|
+
"disconnect_signal": [_signal_tools, "disconnect_signal"],
|
|
100
|
+
"list_connections": [_signal_tools, "list_connections"],
|
|
95
101
|
"rescan_filesystem": [_filesystem_tools, "rescan_filesystem"],
|
|
96
102
|
"scan_status": [_filesystem_tools, "scan_status"],
|
|
97
103
|
"reimport_files": [_filesystem_tools, "reimport_files"],
|
|
@@ -124,6 +130,9 @@ func _init_tools() -> void:
|
|
|
124
130
|
"read_animation_states": [_animation_tools, "read_animation_states"],
|
|
125
131
|
"create_animation": [_animation_tools, "create_animation"],
|
|
126
132
|
"add_animation_track": [_animation_tools, "add_animation_track"],
|
|
133
|
+
"set_animation": [_animation_tools, "set_animation"],
|
|
134
|
+
"rename_animation": [_animation_tools, "rename_animation"],
|
|
135
|
+
"remove_animation": [_animation_tools, "remove_animation"],
|
|
127
136
|
"add_animation_state": [_animation_tools, "add_animation_state"],
|
|
128
137
|
"connect_animation_states": [_animation_tools, "connect_animation_states"],
|
|
129
138
|
}
|