gdharness 1.3.7 → 1.3.9
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 +999 -864
- package/build/godot/addons/gdharness_editor/file_reads.gd +13 -0
- package/build/godot/addons/gdharness_editor/reading.gd +61 -5
- package/build/godot/addons/gdharness_editor/scene_file.gd +41 -35
- package/build/godot/addons/gdharness_editor/serialisation.gd +8 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +12 -2
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +244 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +105 -157
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +25 -1
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +72 -3
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +7 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/reading.gd +61 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +6 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +74 -54
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +38 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +2 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +26 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +105 -6
- package/build/godot/addons/gdharness_runtime/serialisation.gd +8 -7
- package/build/godot/operations/dependencies.gd +61 -10
- package/build/godot/operations/editor_tools.gd +8 -0
- package/build/godot/operations/file_reads.gd +13 -0
- package/build/godot/operations/gdscript_analysis.gd +76 -6
- package/build/godot/operations/gdscript_authoring.gd +5 -2
- package/build/godot/operations/godot_operations.gd +4 -0
- package/build/godot/operations/import_pipeline.gd +45 -1
- package/build/godot/operations/input_actions.gd +22 -7
- package/build/godot/operations/project_config.gd +31 -0
- package/build/godot/operations/project_diagnostics.gd +6 -5
- package/build/godot/operations/reading.gd +61 -5
- package/build/godot/operations/serialisation.gd +8 -7
- package/build/keeper.js +44 -13
- package/package.json +1 -1
|
@@ -119,6 +119,9 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
119
119
|
return {"ok": false, "error": refused}
|
|
120
120
|
if not nodes.has("."):
|
|
121
121
|
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
122
|
+
var base: SceneState = packed.get_state().get_base_scene_state()
|
|
123
|
+
if base != null:
|
|
124
|
+
nodes["."]["inherits"] = base.get_path()
|
|
122
125
|
return {"ok": true, "nodes": nodes}
|
|
123
126
|
|
|
124
127
|
|
|
@@ -175,6 +178,10 @@ static func _lay(
|
|
|
175
178
|
nodes[path] = _made(str(state.get_node_type(index)))
|
|
176
179
|
if ids[index] >= 0:
|
|
177
180
|
nodes[path]["uniqueId"] = ids[index]
|
|
181
|
+
# Laid at the root with no scene handed in is a scene the one being read inherits, and the
|
|
182
|
+
# deepest is laid first, so this is the file that makes the node.
|
|
183
|
+
if prefix == "." and loaded == null:
|
|
184
|
+
nodes[path]["inheritedFrom"] = state.get_path()
|
|
178
185
|
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
179
186
|
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
180
187
|
if not recovered:
|
|
@@ -324,6 +331,12 @@ static func _entry(
|
|
|
324
331
|
data["instanceOf"] = node["instanceOf"]
|
|
325
332
|
if node.has("editableChildren"):
|
|
326
333
|
data["editableChildren"] = node["editableChildren"]
|
|
334
|
+
# An inherited scene's file holds only what it changes, so the read says what it inherits and
|
|
335
|
+
# which nodes come from there, as the editor greys them.
|
|
336
|
+
if node.has("inherits"):
|
|
337
|
+
data["inherits"] = node["inherits"]
|
|
338
|
+
elif node.has("inheritedFrom"):
|
|
339
|
+
data["inheritedFrom"] = node["inheritedFrom"]
|
|
327
340
|
var groups: Array[String] = node["groups"]
|
|
328
341
|
if not groups.is_empty():
|
|
329
342
|
var sorted: Array[String] = groups.duplicate()
|
|
@@ -24,6 +24,23 @@ extends RefCounted
|
|
|
24
24
|
|
|
25
25
|
## How many of a node's children a refusal for a path missing below it names.
|
|
26
26
|
const NAMED_CHILDREN: int = 12
|
|
27
|
+
## The types holding their floats as the engine's real numbers, 32-bit unless it was built for 64.
|
|
28
|
+
const SHAPES_OF_REALS: Array[int] = [
|
|
29
|
+
TYPE_VECTOR2,
|
|
30
|
+
TYPE_VECTOR3,
|
|
31
|
+
TYPE_VECTOR4,
|
|
32
|
+
TYPE_RECT2,
|
|
33
|
+
TYPE_PLANE,
|
|
34
|
+
TYPE_QUATERNION,
|
|
35
|
+
TYPE_AABB,
|
|
36
|
+
TYPE_BASIS,
|
|
37
|
+
TYPE_TRANSFORM2D,
|
|
38
|
+
TYPE_TRANSFORM3D,
|
|
39
|
+
TYPE_PROJECTION,
|
|
40
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
41
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
42
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
43
|
+
]
|
|
27
44
|
|
|
28
45
|
|
|
29
46
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
@@ -190,18 +207,26 @@ static func offered(said: String, word: String, names: Array) -> String:
|
|
|
190
207
|
|
|
191
208
|
|
|
192
209
|
## [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
|
|
194
|
-
##
|
|
210
|
+
## names it declares nearest it when any are, as a misspelt one is usually one of them, or else the
|
|
211
|
+
## variables its script declares. The engine's own are not listed when none is near, since an object
|
|
212
|
+
## has a hundred, while a script's are the few a caller most likely meant.
|
|
195
213
|
static func near_property(said: String, target: Object, property: String) -> String:
|
|
196
214
|
var headings: int = PROPERTY_USAGE_CATEGORY | PROPERTY_USAGE_GROUP | PROPERTY_USAGE_SUBGROUP
|
|
197
215
|
var names: Array[String] = []
|
|
216
|
+
var scripted: Array[String] = []
|
|
198
217
|
for declared: Dictionary in target.get_property_list():
|
|
199
|
-
|
|
218
|
+
var usage: int = as_int(declared.get("usage", 0))
|
|
219
|
+
if not usage & headings:
|
|
200
220
|
names.append(str(declared.get("name", "")))
|
|
221
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE and not usage & headings:
|
|
222
|
+
scripted.append(str(declared.get("name", "")))
|
|
201
223
|
var close: PackedStringArray = nearest(property, PackedStringArray(names))
|
|
202
|
-
if close.is_empty():
|
|
224
|
+
if not close.is_empty():
|
|
225
|
+
return "%s; the nearest it has %s %s" % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
226
|
+
if scripted.is_empty():
|
|
203
227
|
return said
|
|
204
|
-
|
|
228
|
+
scripted.sort()
|
|
229
|
+
return "%s; its script declares %s" % [said, ", ".join(PackedStringArray(scripted))]
|
|
205
230
|
|
|
206
231
|
|
|
207
232
|
## The signals [param object] has that a caller can connect, its own class's first: the engine
|
|
@@ -219,3 +244,34 @@ static func signals_of(object: Object) -> Array:
|
|
|
219
244
|
## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
|
|
220
245
|
static func with_article(word: String) -> String:
|
|
221
246
|
return ("an " if word.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + word
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
## [param value], which the engine holds as a 32-bit float, as the shortest decimal that is still
|
|
250
|
+
## that float. JSON carries a double, and the double a 32-bit 0.2 widens to is 0.200000002980232:
|
|
251
|
+
## the same number, but not the one written or the one the file shows, so a caller comparing what it
|
|
252
|
+
## set against what it read saw them differ. The shorter one, sent back, stores the same 32 bits. A
|
|
253
|
+
## plain float is left alone by the serialiser, since nothing says whether the engine holds it in
|
|
254
|
+
## 32 bits or 64.
|
|
255
|
+
static func single(value: float) -> float:
|
|
256
|
+
if not is_finite(value) or value == 0.0:
|
|
257
|
+
return value
|
|
258
|
+
var held: float = PackedFloat32Array([value])[0]
|
|
259
|
+
var magnitude: int = floori(log(absf(held)) / log(10.0))
|
|
260
|
+
for digits: int in range(1, 10):
|
|
261
|
+
var shift: int = digits - 1 - magnitude
|
|
262
|
+
var tried: float = (
|
|
263
|
+
roundf(held * pow(10.0, shift)) / pow(10.0, shift)
|
|
264
|
+
if shift >= 0
|
|
265
|
+
else roundf(held / pow(10.0, -shift)) * pow(10.0, -shift)
|
|
266
|
+
)
|
|
267
|
+
if PackedFloat32Array([tried])[0] == held:
|
|
268
|
+
return tried
|
|
269
|
+
return held
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
## Whether a value of [param type] holds its floats in 32 bits: a colour always, and the vectors and
|
|
273
|
+
## the shapes built of them unless the engine was built for 64.
|
|
274
|
+
static func holds_singles(type: int) -> bool:
|
|
275
|
+
if type in [TYPE_COLOR, TYPE_PACKED_COLOR_ARRAY, TYPE_PACKED_FLOAT32_ARRAY]:
|
|
276
|
+
return true
|
|
277
|
+
return not OS.has_feature("double") and type in SHAPES_OF_REALS
|
|
@@ -50,6 +50,8 @@ static var by_path: PackedStringArray = []
|
|
|
50
50
|
|
|
51
51
|
var root: Node = null
|
|
52
52
|
var path: String = ""
|
|
53
|
+
## What the last write saved, for its answer to read what the file now stores.
|
|
54
|
+
var written_pack: PackedScene = null
|
|
53
55
|
## The path of each node a scene this one inherits gives, with the file that makes it.
|
|
54
56
|
var _base_paths: Dictionary = {}
|
|
55
57
|
var _state: SceneState = null
|
|
@@ -91,18 +93,7 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
91
93
|
if root == null:
|
|
92
94
|
return {"ok": false, "error": "Failed to instantiate " + path}
|
|
93
95
|
_state = packed.get_state()
|
|
94
|
-
|
|
95
|
-
# Each node a base gives, with the file that makes it: the deepest base listing it as more than
|
|
96
|
-
# values set on a node from further down.
|
|
97
|
-
while base != null:
|
|
98
|
-
for index: int in base.get_node_count():
|
|
99
|
-
var given: String = _relative(str(base.get_node_path(index)))
|
|
100
|
-
var made: bool = (
|
|
101
|
-
base.get_node_instance(index) != null or not str(base.get_node_type(index)).is_empty()
|
|
102
|
-
)
|
|
103
|
-
if made or not _base_paths.has(given):
|
|
104
|
-
_base_paths[given] = base.get_path()
|
|
105
|
-
base = base.get_base_scene_state()
|
|
96
|
+
_base_paths = makers(_state)
|
|
106
97
|
# On a read as well, which would otherwise answer what the scripts set as what the file holds.
|
|
107
98
|
if not Engine.is_editor_hint():
|
|
108
99
|
if for_writing:
|
|
@@ -179,8 +170,7 @@ func refuse(error: String) -> Dictionary:
|
|
|
179
170
|
return {"ok": false, "error": error}
|
|
180
171
|
|
|
181
172
|
|
|
182
|
-
## Each bit set in [param flags], a connection's, by name, and
|
|
183
|
-
## number.
|
|
173
|
+
## Each bit set in [param flags], a connection's, by name, and any bits left unnamed as one number.
|
|
184
174
|
static func flag_names(flags: int) -> Array[String]:
|
|
185
175
|
var names: Array[String] = []
|
|
186
176
|
var rest: int = flags
|
|
@@ -350,13 +340,20 @@ func _closed_around(node: Node) -> PackedStringArray:
|
|
|
350
340
|
func fixed(node: Node, deleting: bool = false) -> String:
|
|
351
341
|
if node == root:
|
|
352
342
|
return "The root of %s cannot be moved, renamed, copied or deleted." % path
|
|
343
|
+
if node.owner == null:
|
|
344
|
+
return foreign(node)
|
|
353
345
|
if node.owner != root:
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
346
|
+
# Editable children keep values here and no more; the scene instanced may have the node from
|
|
347
|
+
# a scene it inherits, which is the one to edit.
|
|
348
|
+
var instanced: String = node.owner.scene_file_path
|
|
349
|
+
var packed: PackedScene = load(instanced) as PackedScene
|
|
350
|
+
var made_by: Dictionary = makers(packed.get_state()) if packed != null else {}
|
|
351
|
+
var maker: String = str(made_by.get(str(node.owner.get_path_to(node)), instanced))
|
|
352
|
+
var inside: String = (
|
|
353
|
+
"%s is inside the scene instanced at %s (%s), which creates it again on load."
|
|
354
|
+
% [path_of(node), path_of(node.owner), instanced]
|
|
355
|
+
)
|
|
356
|
+
return inside + (_deleted_elsewhere(node, maker) if deleting else " Edit %s instead." % maker)
|
|
360
357
|
var at: String = path_of(node)
|
|
361
358
|
if _base_paths.has(at):
|
|
362
359
|
var maker: String = str(_base_paths[at])
|
|
@@ -367,6 +364,23 @@ func fixed(node: Node, deleting: bool = false) -> String:
|
|
|
367
364
|
return ""
|
|
368
365
|
|
|
369
366
|
|
|
367
|
+
## Each node the scenes [param state] inherits give it, by path, with the deepest file making it more
|
|
368
|
+
## than values; a node [param state] makes itself is not in it.
|
|
369
|
+
static func makers(state: SceneState) -> Dictionary:
|
|
370
|
+
var made_by: Dictionary = {}
|
|
371
|
+
var at: SceneState = state.get_base_scene_state()
|
|
372
|
+
while at != null:
|
|
373
|
+
for index: int in at.get_node_count():
|
|
374
|
+
var given: String = _relative(str(at.get_node_path(index)))
|
|
375
|
+
var made: bool = (
|
|
376
|
+
at.get_node_instance(index) != null or not str(at.get_node_type(index)).is_empty()
|
|
377
|
+
)
|
|
378
|
+
if made or not made_by.has(given):
|
|
379
|
+
made_by[given] = at.get_path()
|
|
380
|
+
at = at.get_base_scene_state()
|
|
381
|
+
return made_by
|
|
382
|
+
|
|
383
|
+
|
|
370
384
|
## What a caller can do with [param node], made by [param maker], instead of deleting it here.
|
|
371
385
|
static func _deleted_elsewhere(node: Node, maker: String) -> String:
|
|
372
386
|
var hidden: String = (
|
|
@@ -583,19 +597,12 @@ func write(check: Callable = Callable()) -> Dictionary:
|
|
|
583
597
|
return refuse("Nothing was written: " + str(settled[1]))
|
|
584
598
|
packed = settled[0]
|
|
585
599
|
if not path.ends_with(".tscn") and _holds_scripted_embedded():
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
"Nothing was written: %s is a binary scene holding an embedded resource with a script "
|
|
591
|
-
+ "that is not a tool, and what that script changed when a headless engine loaded it "
|
|
592
|
-
+ "cannot be told apart from the file here. "
|
|
593
|
-
)
|
|
594
|
-
% path
|
|
595
|
-
)
|
|
596
|
-
+ GdhHeadlessCheck.REMEDY
|
|
597
|
-
)
|
|
600
|
+
var binary: String = (
|
|
601
|
+
"Nothing was written: %s is a binary scene holding an embedded resource with a script "
|
|
602
|
+
+ "that is not a tool, and what that script changed when a headless engine loaded it "
|
|
603
|
+
+ "cannot be told apart from the file here. "
|
|
598
604
|
)
|
|
605
|
+
return refuse(binary % path + GdhHeadlessCheck.REMEDY)
|
|
599
606
|
if check.is_valid():
|
|
600
607
|
var saved: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
601
608
|
if saved == null:
|
|
@@ -608,6 +615,7 @@ func write(check: Callable = Callable()) -> Dictionary:
|
|
|
608
615
|
var previous: Variant = null
|
|
609
616
|
if FileAccess.file_exists(path):
|
|
610
617
|
previous = FileAccess.get_file_as_bytes(path)
|
|
618
|
+
written_pack = packed
|
|
611
619
|
var written: Dictionary = save_packed(packed, path)
|
|
612
620
|
if not written.is_empty() or not headless or not path.ends_with(".tscn"):
|
|
613
621
|
return written
|
|
@@ -988,7 +996,5 @@ static func _stored_at(state: SceneState, index: int) -> Dictionary:
|
|
|
988
996
|
return stored
|
|
989
997
|
|
|
990
998
|
|
|
991
|
-
func _relative(node_path: String) -> String:
|
|
992
|
-
if node_path == ".":
|
|
993
|
-
return node_path
|
|
999
|
+
static func _relative(node_path: String) -> String:
|
|
994
1000
|
return node_path.trim_prefix("./")
|
|
@@ -142,28 +142,29 @@ func serialize_value(value: Variant) -> Variant:
|
|
|
142
142
|
return value
|
|
143
143
|
var carried: Variant = call(serialiser, value)
|
|
144
144
|
# The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
|
|
145
|
-
# tagged here; containers reach their members through this
|
|
146
|
-
|
|
145
|
+
# tagged here, and a 32-bit one shortened; containers reach their members through this already.
|
|
146
|
+
var type: int = typeof(value)
|
|
147
|
+
return carried if type in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried, GdhRead.holds_singles(type))
|
|
147
148
|
|
|
148
149
|
|
|
149
150
|
# [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
|
|
150
151
|
# NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
|
|
151
152
|
# back out as null, so a property holding INF was answered as holding nothing.
|
|
152
|
-
func _finite(carried: Variant) -> Variant:
|
|
153
|
+
func _finite(carried: Variant, single: bool = false) -> Variant:
|
|
153
154
|
if carried is float:
|
|
154
155
|
var number: float = carried
|
|
155
|
-
return _serialize_float(number)
|
|
156
|
+
return _serialize_float(GdhRead.single(number) if single else number)
|
|
156
157
|
if carried is Dictionary:
|
|
157
158
|
var fields: Dictionary = carried
|
|
158
159
|
var tagged: Dictionary = {}
|
|
159
160
|
for key: Variant in fields:
|
|
160
|
-
tagged[key] = _finite(fields[key])
|
|
161
|
+
tagged[key] = _finite(fields[key], single)
|
|
161
162
|
return tagged
|
|
162
163
|
if carried is Array:
|
|
163
164
|
var items: Array = carried
|
|
164
165
|
var tagged_items: Array = []
|
|
165
166
|
for item: Variant in items:
|
|
166
|
-
tagged_items.append(_finite(item))
|
|
167
|
+
tagged_items.append(_finite(item, single))
|
|
167
168
|
return tagged_items
|
|
168
169
|
return carried
|
|
169
170
|
|
|
@@ -189,7 +190,7 @@ func _serialize_floats(value: Variant) -> Variant:
|
|
|
189
190
|
for each: float in numbers:
|
|
190
191
|
items.append(_serialize_float(each))
|
|
191
192
|
return items
|
|
192
|
-
return value
|
|
193
|
+
return numbers if value is PackedFloat32Array else value
|
|
193
194
|
|
|
194
195
|
|
|
195
196
|
# Rebuilds a Godot value from the shape serialize_value gave it.
|
|
@@ -8,6 +8,7 @@ extends Node
|
|
|
8
8
|
const GdhSceneTools = preload("tools/scene_tools.gd")
|
|
9
9
|
const GdhSignalTools = preload("tools/signal_tools.gd")
|
|
10
10
|
const GdhResourceTools = preload("tools/resource_tools.gd")
|
|
11
|
+
const GdhAnimationStates = preload("tools/animation_states.gd")
|
|
11
12
|
const GdhAnimationTools = preload("tools/animation_tools.gd")
|
|
12
13
|
const GdhPlayTools = preload("tools/play_tools.gd")
|
|
13
14
|
const GdhClassTools = preload("tools/class_tools.gd")
|
|
@@ -20,6 +21,7 @@ var _editor_plugin: EditorPlugin = null
|
|
|
20
21
|
var _scene_tools: GdhSceneTools = null
|
|
21
22
|
var _signal_tools: GdhSignalTools = null
|
|
22
23
|
var _resource_tools: GdhResourceTools = null
|
|
24
|
+
var _animation_states: GdhAnimationStates = null
|
|
23
25
|
var _animation_tools: GdhAnimationTools = null
|
|
24
26
|
var _play_tools: GdhPlayTools = null
|
|
25
27
|
var _class_tools: GdhClassTools = null
|
|
@@ -37,6 +39,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
37
39
|
_scene_tools.set_editor_plugin(plugin)
|
|
38
40
|
_resource_tools.set_editor_plugin(plugin)
|
|
39
41
|
_animation_tools.set_editor_plugin(plugin)
|
|
42
|
+
_animation_states.set_editor_plugin(plugin)
|
|
40
43
|
_play_tools.set_editor_plugin(plugin)
|
|
41
44
|
_class_tools.set_editor_plugin(plugin)
|
|
42
45
|
_filesystem_tools.set_editor_plugin(plugin)
|
|
@@ -64,6 +67,10 @@ func _init_tools() -> void:
|
|
|
64
67
|
_animation_tools.name = "AnimationTools"
|
|
65
68
|
add_child(_animation_tools)
|
|
66
69
|
|
|
70
|
+
_animation_states = GdhAnimationStates.new()
|
|
71
|
+
_animation_states.name = "AnimationStates"
|
|
72
|
+
add_child(_animation_states)
|
|
73
|
+
|
|
67
74
|
_play_tools = GdhPlayTools.new()
|
|
68
75
|
_play_tools.name = "PlayTools"
|
|
69
76
|
add_child(_play_tools)
|
|
@@ -131,11 +138,14 @@ func _init_tools() -> void:
|
|
|
131
138
|
"read_animation_states": [_animation_tools, "read_animation_states"],
|
|
132
139
|
"create_animation": [_animation_tools, "create_animation"],
|
|
133
140
|
"add_animation_track": [_animation_tools, "add_animation_track"],
|
|
141
|
+
"remove_animation_track": [_animation_tools, "remove_animation_track"],
|
|
134
142
|
"set_animation": [_animation_tools, "set_animation"],
|
|
135
143
|
"rename_animation": [_animation_tools, "rename_animation"],
|
|
136
144
|
"remove_animation": [_animation_tools, "remove_animation"],
|
|
137
|
-
"add_animation_state": [
|
|
138
|
-
"connect_animation_states": [
|
|
145
|
+
"add_animation_state": [_animation_states, "add_animation_state"],
|
|
146
|
+
"connect_animation_states": [_animation_states, "connect_animation_states"],
|
|
147
|
+
"remove_animation_state": [_animation_states, "remove_animation_state"],
|
|
148
|
+
"disconnect_animation_states": [_animation_states, "disconnect_animation_states"],
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends "animation_tools.gd"
|
|
3
|
+
|
|
4
|
+
## The states and transitions of an AnimationTree's state machines, beside the players and tracks
|
|
5
|
+
## the script it extends edits, whose helpers it shares.
|
|
6
|
+
|
|
7
|
+
## How far right of the rightmost state a new one goes in the editor's graph, wider than a state's
|
|
8
|
+
## box there.
|
|
9
|
+
const STATE_SPACING: float = 200.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# add_animation_state
|
|
14
|
+
# =============================================================================
|
|
15
|
+
func add_animation_state(args: Dictionary) -> Dictionary:
|
|
16
|
+
var state_name: String = str(args.get("stateName", ""))
|
|
17
|
+
var animation_name: String = str(args.get("animationName", ""))
|
|
18
|
+
if state_name.is_empty():
|
|
19
|
+
return {"ok": false, "error": "Missing stateName"}
|
|
20
|
+
if animation_name.is_empty():
|
|
21
|
+
return {"ok": false, "error": "Missing animationName"}
|
|
22
|
+
var machine: Array = _machine_at(args)
|
|
23
|
+
if machine[1] == null:
|
|
24
|
+
return machine[3]
|
|
25
|
+
var scene: GdhSceneFile = machine[0]
|
|
26
|
+
var sm: AnimationNodeStateMachine = machine[1]
|
|
27
|
+
var anim_tree: AnimationTree = machine[4]
|
|
28
|
+
# add_node fails with nothing but a line in the editor's log on a name in use, and Start and End
|
|
29
|
+
# are in use in every state machine.
|
|
30
|
+
if sm.has_node(StringName(state_name)):
|
|
31
|
+
return scene.refuse("The state machine already has a state named %s." % state_name)
|
|
32
|
+
var player: AnimationPlayer = anim_tree.get_node_or_null(anim_tree.anim_player) as AnimationPlayer
|
|
33
|
+
var found: bool = player != null and player.has_animation(StringName(animation_name))
|
|
34
|
+
|
|
35
|
+
var anim_node: AnimationNodeAnimation = AnimationNodeAnimation.new()
|
|
36
|
+
anim_node.animation = StringName(animation_name)
|
|
37
|
+
# Beside the rightmost state: add_node puts a state at the origin, where the editor's graph
|
|
38
|
+
# draws it over whatever is already there, Start included.
|
|
39
|
+
var rightmost: Vector2 = Vector2.ZERO
|
|
40
|
+
for state: StringName in sm.get_node_list():
|
|
41
|
+
var held: Vector2 = sm.get_node_position(state)
|
|
42
|
+
if held.x > rightmost.x:
|
|
43
|
+
rightmost = held
|
|
44
|
+
var position: Vector2 = rightmost + Vector2(STATE_SPACING, 0)
|
|
45
|
+
sm.add_node(StringName(state_name), anim_node, position)
|
|
46
|
+
if not sm.has_node(StringName(state_name)):
|
|
47
|
+
return scene.refuse("The state machine did not take the state %s." % state_name)
|
|
48
|
+
|
|
49
|
+
var answer: Dictionary = {
|
|
50
|
+
"ok": true,
|
|
51
|
+
"stateName": state_name,
|
|
52
|
+
"animationName": animation_name,
|
|
53
|
+
# False when the tree has no player to look the animation up in, or its player has none of
|
|
54
|
+
# that name.
|
|
55
|
+
"animationFound": found,
|
|
56
|
+
"position": GdhSerialisation.new().serialize_value(position),
|
|
57
|
+
}
|
|
58
|
+
# Written all the same, as the editor writes one: a state machine is often laid out before its
|
|
59
|
+
# animations are made, and refusing it stopped a tree being built in that order.
|
|
60
|
+
if player != null and not found:
|
|
61
|
+
answer["animationMissing"] = _missing_animation(scene, player, animation_name)
|
|
62
|
+
var files: Dictionary = machine[2]
|
|
63
|
+
return _finish(scene, Callable(), files, answer)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## Why the state playing [param animation_name] plays nothing yet. The player is named by its path
|
|
67
|
+
## from the scene root, as playerNodePath takes it; anim_player is relative to the tree ("../Anim"),
|
|
68
|
+
## which no argument takes.
|
|
69
|
+
static func _missing_animation(
|
|
70
|
+
scene: GdhSceneFile, player: AnimationPlayer, animation_name: String
|
|
71
|
+
) -> String:
|
|
72
|
+
var said: String = (
|
|
73
|
+
"The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
|
|
74
|
+
% [str(scene.root.get_path_to(player)), animation_name]
|
|
75
|
+
)
|
|
76
|
+
var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
|
|
77
|
+
if close.is_empty():
|
|
78
|
+
return said
|
|
79
|
+
return (
|
|
80
|
+
said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# =============================================================================
|
|
85
|
+
# connect_animation_states
|
|
86
|
+
# =============================================================================
|
|
87
|
+
func connect_animation_states(args: Dictionary) -> Dictionary:
|
|
88
|
+
var from_state: String = str(args.get("fromState", ""))
|
|
89
|
+
var to_state: String = str(args.get("toState", ""))
|
|
90
|
+
var transition_type: String = str(args.get("transitionType", "immediate"))
|
|
91
|
+
var advance_condition: String = str(args.get("advanceCondition", ""))
|
|
92
|
+
|
|
93
|
+
if from_state.is_empty() or to_state.is_empty():
|
|
94
|
+
return {"ok": false, "error": "Missing fromState or toState"}
|
|
95
|
+
var modes: Dictionary = {
|
|
96
|
+
"sync": AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC,
|
|
97
|
+
"at_end": AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END,
|
|
98
|
+
"immediate": AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE,
|
|
99
|
+
}
|
|
100
|
+
if not modes.has(transition_type):
|
|
101
|
+
return {"ok": false, "error": "Unsupported transitionType: " + transition_type}
|
|
102
|
+
var transition: AnimationNodeStateMachineTransition = AnimationNodeStateMachineTransition.new()
|
|
103
|
+
transition.switch_mode = (
|
|
104
|
+
GdhRead.as_int(modes[transition_type]) as AnimationNodeStateMachineTransition.SwitchMode
|
|
105
|
+
)
|
|
106
|
+
if not advance_condition.is_empty():
|
|
107
|
+
transition.advance_condition = StringName(advance_condition)
|
|
108
|
+
|
|
109
|
+
var found: Array = _machine_at(args)
|
|
110
|
+
if found[1] == null:
|
|
111
|
+
return found[3]
|
|
112
|
+
var scene: GdhSceneFile = found[0]
|
|
113
|
+
var sm: AnimationNodeStateMachine = found[1]
|
|
114
|
+
var problem: String = _transition_problem(sm, from_state, to_state)
|
|
115
|
+
if not problem.is_empty():
|
|
116
|
+
return scene.refuse(problem)
|
|
117
|
+
|
|
118
|
+
sm.add_transition(StringName(from_state), StringName(to_state), transition)
|
|
119
|
+
if not sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
120
|
+
return scene.refuse("The state machine did not take the transition.")
|
|
121
|
+
var files: Dictionary = found[2]
|
|
122
|
+
return _finish(scene, Callable(), files, {"ok": true, "from": from_state, "to": to_state})
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## Why [param sm] cannot take a transition from [param from_state] to [param to_state], or "".
|
|
126
|
+
## Each is a condition add_transition fails on with only a line in the editor's log, and the
|
|
127
|
+
## transition was answered as made.
|
|
128
|
+
func _transition_problem(sm: AnimationNodeStateMachine, from_state: String, to_state: String) -> String:
|
|
129
|
+
for state: String in [from_state, to_state]:
|
|
130
|
+
if not sm.has_node(StringName(state)):
|
|
131
|
+
return _no_state(sm, state)
|
|
132
|
+
if from_state == to_state:
|
|
133
|
+
return "A transition needs two different states."
|
|
134
|
+
if from_state == "End":
|
|
135
|
+
return "No transition can leave End."
|
|
136
|
+
if to_state == "Start":
|
|
137
|
+
return "No transition can arrive at Start."
|
|
138
|
+
if sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
139
|
+
return "There is already a transition from %s to %s." % [from_state, to_state]
|
|
140
|
+
return ""
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## The refusal for [param state_name], which [param sm] has no state of: with the nearest names it
|
|
144
|
+
## has, or else every state, since a machine has a handful.
|
|
145
|
+
static func _no_state(sm: AnimationNodeStateMachine, state_name: String) -> String:
|
|
146
|
+
var said: String = "The state machine has no state named %s" % state_name
|
|
147
|
+
var states: PackedStringArray = []
|
|
148
|
+
for state: StringName in sm.get_node_list():
|
|
149
|
+
var _listed: bool = states.append(str(state))
|
|
150
|
+
var close: PackedStringArray = GdhRead.nearest(state_name, states)
|
|
151
|
+
if not close.is_empty():
|
|
152
|
+
return (
|
|
153
|
+
"%s; the nearest it has %s %s." % [said, "is" if close.size() == 1 else "are", ", ".join(close)]
|
|
154
|
+
)
|
|
155
|
+
return "%s; its states are %s." % [said, ", ".join(states)]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
## The state machine [param args] names, as [scene, machine, files, refusal, tree]: the machine is
|
|
159
|
+
## null and the refusal says why when the scene, the tree or the machine is not there.
|
|
160
|
+
func _machine_at(args: Dictionary) -> Array:
|
|
161
|
+
var scene_path: String = GdhSceneFile.res_path("", str(args.get("scenePath", "")))
|
|
162
|
+
var anim_tree_path: String = str(args.get("animTreePath", ""))
|
|
163
|
+
if scene_path.strip_edges() == "res://":
|
|
164
|
+
return [null, null, {}, {"ok": false, "error": "Missing scenePath"}]
|
|
165
|
+
if anim_tree_path.is_empty():
|
|
166
|
+
return [null, null, {}, {"ok": false, "error": "Missing animTreePath"}]
|
|
167
|
+
var opened: Array = _open_at(scene_path, anim_tree_path, "AnimationTree")
|
|
168
|
+
var scene: GdhSceneFile = opened[0]
|
|
169
|
+
if opened[1] == null:
|
|
170
|
+
return [scene, null, {}, opened[2]]
|
|
171
|
+
var anim_tree: AnimationTree = opened[1]
|
|
172
|
+
var machine: Array = _machine(scene, anim_tree, str(args.get("stateMachinePath", "")))
|
|
173
|
+
if machine[0] == null:
|
|
174
|
+
return [scene, null, {}, scene.refuse(str(machine[2]))]
|
|
175
|
+
return [scene, machine[0], machine[1], {}, anim_tree]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# remove_animation_state, disconnect_animation_states
|
|
180
|
+
# =============================================================================
|
|
181
|
+
## Takes a state out of its state machine with every transition leaving or reaching it, which the
|
|
182
|
+
## engine drops with the state, and names those under transitionsRemoved. Start and End are part of
|
|
183
|
+
## every state machine.
|
|
184
|
+
func remove_animation_state(args: Dictionary) -> Dictionary:
|
|
185
|
+
var state_name: String = str(args.get("stateName", ""))
|
|
186
|
+
if state_name.is_empty():
|
|
187
|
+
return {"ok": false, "error": "Missing stateName"}
|
|
188
|
+
var found: Array = _machine_at(args)
|
|
189
|
+
if found[1] == null:
|
|
190
|
+
return found[3]
|
|
191
|
+
var scene: GdhSceneFile = found[0]
|
|
192
|
+
var sm: AnimationNodeStateMachine = found[1]
|
|
193
|
+
if state_name in ["Start", "End"]:
|
|
194
|
+
return scene.refuse("%s is part of every state machine, so it cannot be removed." % state_name)
|
|
195
|
+
if not sm.has_node(StringName(state_name)):
|
|
196
|
+
return scene.refuse(_no_state(sm, state_name))
|
|
197
|
+
var dropped: Array = []
|
|
198
|
+
for index: int in sm.get_transition_count():
|
|
199
|
+
var from: String = str(sm.get_transition_from(index))
|
|
200
|
+
var to: String = str(sm.get_transition_to(index))
|
|
201
|
+
if state_name in [from, to]:
|
|
202
|
+
dropped.append({"from": from, "to": to})
|
|
203
|
+
sm.remove_node(StringName(state_name))
|
|
204
|
+
if sm.has_node(StringName(state_name)):
|
|
205
|
+
return scene.refuse("The state machine did not let go of %s." % state_name)
|
|
206
|
+
for index: int in sm.get_transition_count():
|
|
207
|
+
if state_name in [str(sm.get_transition_from(index)), str(sm.get_transition_to(index))]:
|
|
208
|
+
return scene.refuse(
|
|
209
|
+
"The state machine kept a transition of %s after the state went." % state_name
|
|
210
|
+
)
|
|
211
|
+
var files: Dictionary = found[2]
|
|
212
|
+
return _finish(
|
|
213
|
+
scene, Callable(), files, {"ok": true, "removed": state_name, "transitionsRemoved": dropped}
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Takes out the transition from one state to another.
|
|
218
|
+
func disconnect_animation_states(args: Dictionary) -> Dictionary:
|
|
219
|
+
var from_state: String = str(args.get("fromState", ""))
|
|
220
|
+
var to_state: String = str(args.get("toState", ""))
|
|
221
|
+
if from_state.is_empty() or to_state.is_empty():
|
|
222
|
+
return {"ok": false, "error": "Missing fromState or toState"}
|
|
223
|
+
var found: Array = _machine_at(args)
|
|
224
|
+
if found[1] == null:
|
|
225
|
+
return found[3]
|
|
226
|
+
var scene: GdhSceneFile = found[0]
|
|
227
|
+
var sm: AnimationNodeStateMachine = found[1]
|
|
228
|
+
for state: String in [from_state, to_state]:
|
|
229
|
+
if not sm.has_node(StringName(state)):
|
|
230
|
+
return scene.refuse(_no_state(sm, state))
|
|
231
|
+
if not sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
232
|
+
var leaving: PackedStringArray = []
|
|
233
|
+
for index: int in sm.get_transition_count():
|
|
234
|
+
if str(sm.get_transition_from(index)) == from_state:
|
|
235
|
+
var _listed: bool = leaving.append(str(sm.get_transition_to(index)))
|
|
236
|
+
var said: String = "There is no transition from %s to %s" % [from_state, to_state]
|
|
237
|
+
if leaving.is_empty():
|
|
238
|
+
return scene.refuse("%s; none leaves %s." % [said, from_state])
|
|
239
|
+
return scene.refuse("%s; the ones leaving %s go to %s." % [said, from_state, ", ".join(leaving)])
|
|
240
|
+
sm.remove_transition(StringName(from_state), StringName(to_state))
|
|
241
|
+
if sm.has_transition(StringName(from_state), StringName(to_state)):
|
|
242
|
+
return scene.refuse("The state machine did not let go of the transition.")
|
|
243
|
+
var files: Dictionary = found[2]
|
|
244
|
+
return _finish(scene, Callable(), files, {"ok": true, "removed": {"from": from_state, "to": to_state}})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bu6foylc4333x
|