gdharness 1.3.9 → 1.3.11
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 +4740 -3212
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +102 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +51 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -140
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +28 -5
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +39 -20
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -362
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +147 -492
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +4 -3
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +79 -5
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +183 -37
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +51 -1
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +185 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +86 -79
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +309 -22
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +163 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +5 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +146 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +11 -22
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +22 -14
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +102 -18
- package/build/godot/operations/gdscript_authoring.gd +11 -1
- package/build/godot/operations/godot_operations.gd +16 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +108 -47
- package/build/godot/operations/project_config.gd +83 -69
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +51 -1
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +11 -22
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +73 -13
- package/build/keeper.js +1371 -706
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
6
|
|
|
7
7
|
const GdhFileReads = preload("file_reads.gd")
|
|
8
|
+
const GdhRead = preload("reading.gd")
|
|
8
9
|
const GdhSerialisation = preload("serialisation.gd")
|
|
9
10
|
|
|
10
11
|
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
@@ -28,6 +29,11 @@ const SWITCH_MODE_NAMES: Dictionary[int, String] = {
|
|
|
28
29
|
AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
|
|
29
30
|
AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
|
|
30
31
|
}
|
|
32
|
+
const ADVANCE_MODE_NAMES: Dictionary[int, String] = {
|
|
33
|
+
AnimationNodeStateMachineTransition.ADVANCE_MODE_DISABLED: "disabled",
|
|
34
|
+
AnimationNodeStateMachineTransition.ADVANCE_MODE_ENABLED: "enabled",
|
|
35
|
+
AnimationNodeStateMachineTransition.ADVANCE_MODE_AUTO: "auto",
|
|
36
|
+
}
|
|
31
37
|
|
|
32
38
|
|
|
33
39
|
## [method animations] of the AnimationPlayer at [param player_path] in the file [param scene_path].
|
|
@@ -47,8 +53,21 @@ static func scene_states(scene_path: String, tree_path: String, state_machine_pa
|
|
|
47
53
|
if made[0] == null:
|
|
48
54
|
return made[1]
|
|
49
55
|
var tree: AnimationTree = made[0]
|
|
50
|
-
|
|
56
|
+
# The player the tree plays from, read from the file the same way, since a bare tree has no
|
|
57
|
+
# siblings to find it among.
|
|
58
|
+
# A path above the root names no node the file has, and finds none, as the editor finds none.
|
|
59
|
+
var player: AnimationPlayer = null
|
|
60
|
+
var player_path: String = ""
|
|
61
|
+
if not str(tree.anim_player).is_empty():
|
|
62
|
+
player_path = (tree_path + "/" + str(tree.anim_player)).simplify_path()
|
|
63
|
+
if player_path.is_empty():
|
|
64
|
+
player_path = "."
|
|
65
|
+
var found: Array = _bare(scene_path, player_path, "AnimationPlayer")
|
|
66
|
+
player = found[0]
|
|
67
|
+
var answer: Dictionary = states(tree, state_machine_path, player, player_path)
|
|
51
68
|
tree.free()
|
|
69
|
+
if player != null:
|
|
70
|
+
player.free()
|
|
52
71
|
return answer
|
|
53
72
|
|
|
54
73
|
|
|
@@ -200,8 +219,28 @@ static func no_machine(tree: AnimationTree, state_machine_path: String) -> Strin
|
|
|
200
219
|
return "No state machine at " + state_machine_path
|
|
201
220
|
|
|
202
221
|
|
|
222
|
+
## Why a state playing [param animation_name] from [param player], at [param player_path] from the
|
|
223
|
+
## scene root as playerNodePath takes it, plays nothing yet. anim_player is relative to the tree
|
|
224
|
+
## ("../Anim"), which no argument takes.
|
|
225
|
+
static func missing_animation(player: AnimationPlayer, player_path: String, animation_name: String) -> String:
|
|
226
|
+
var said: String = (
|
|
227
|
+
"The AnimationPlayer at %s has no animation %s yet, so the state plays nothing until it has one."
|
|
228
|
+
% [player_path, animation_name]
|
|
229
|
+
)
|
|
230
|
+
var close: PackedStringArray = GdhRead.nearest(animation_name, player.get_animation_list())
|
|
231
|
+
if close.is_empty():
|
|
232
|
+
return said
|
|
233
|
+
return (
|
|
234
|
+
said + " The nearest %s %s." % ["it has is" if close.size() == 1 else "it has are", ", ".join(close)]
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
203
238
|
## The states and transitions of the state machine at [param state_machine_path] in [param tree].
|
|
204
|
-
|
|
239
|
+
## A state playing an animation says whether [param player], at [param player_path], has it, as
|
|
240
|
+
## add_state answers: a state naming one it lacks plays nothing, and says so only in a log.
|
|
241
|
+
static func states(
|
|
242
|
+
tree: AnimationTree, state_machine_path: String, player: AnimationPlayer, player_path: String
|
|
243
|
+
) -> Dictionary:
|
|
205
244
|
var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
|
|
206
245
|
if sm == null:
|
|
207
246
|
return {"ok": false, "error": no_machine(tree, state_machine_path)}
|
|
@@ -212,6 +251,10 @@ static func states(tree: AnimationTree, state_machine_path: String) -> Dictionar
|
|
|
212
251
|
if node is AnimationNodeAnimation:
|
|
213
252
|
var plays: AnimationNodeAnimation = node
|
|
214
253
|
entry["animationName"] = str(plays.animation)
|
|
254
|
+
var found: bool = player != null and player.has_animation(plays.animation)
|
|
255
|
+
entry["animationFound"] = found
|
|
256
|
+
if player != null and not found:
|
|
257
|
+
entry["animationMissing"] = missing_animation(player, player_path, str(plays.animation))
|
|
215
258
|
listed.append(entry)
|
|
216
259
|
var transitions: Array[Dictionary] = []
|
|
217
260
|
for index: int in sm.get_transition_count():
|
|
@@ -220,8 +263,76 @@ static func states(tree: AnimationTree, state_machine_path: String) -> Dictionar
|
|
|
220
263
|
"from": str(sm.get_transition_from(index)),
|
|
221
264
|
"to": str(sm.get_transition_to(index)),
|
|
222
265
|
"transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
|
|
266
|
+
"advanceMode": ADVANCE_MODE_NAMES.get(transition.advance_mode, str(transition.advance_mode)),
|
|
223
267
|
}
|
|
224
|
-
|
|
225
|
-
|
|
268
|
+
var condition: String = str(transition.advance_condition)
|
|
269
|
+
if not condition.is_empty():
|
|
270
|
+
shaped["advanceCondition"] = condition
|
|
271
|
+
# A condition is checked only on an auto transition, and the default is enabled: a machine
|
|
272
|
+
# with its conditions set true stayed where it was, and nothing said why.
|
|
273
|
+
if not condition.is_empty() and transition.advance_mode != transition.ADVANCE_MODE_AUTO:
|
|
274
|
+
shaped["conditionNote"] = (
|
|
275
|
+
(
|
|
276
|
+
"advanceMode is %s, so %s never advances it: the engine checks a condition only on an"
|
|
277
|
+
+ " auto transition%s. disconnect_states and connect_states again with the condition"
|
|
278
|
+
+ " write it as auto"
|
|
279
|
+
)
|
|
280
|
+
% [
|
|
281
|
+
shaped["advanceMode"],
|
|
282
|
+
condition,
|
|
283
|
+
(
|
|
284
|
+
", and travel() still takes this one"
|
|
285
|
+
if transition.advance_mode == transition.ADVANCE_MODE_ENABLED
|
|
286
|
+
else ""
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
)
|
|
226
290
|
transitions.append(shaped)
|
|
227
291
|
return {"ok": true, "states": listed, "transitions": transitions}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
## The states of the AnimationTrees under [param root] that play [param animation_name] from
|
|
295
|
+
## [param player], as [[node, {animTreePath, state}]], a state named by the path of the nodes leading
|
|
296
|
+
## to it.
|
|
297
|
+
static func states_playing(root: Node, player: AnimationPlayer, animation_name: String) -> Array:
|
|
298
|
+
var found: Array = []
|
|
299
|
+
var trees: Array[Node] = root.find_children("*", "AnimationTree", true, false)
|
|
300
|
+
if root is AnimationTree:
|
|
301
|
+
trees.append(root)
|
|
302
|
+
for node: Node in trees:
|
|
303
|
+
var tree: AnimationTree = node
|
|
304
|
+
if tree.tree_root == null or tree.get_node_or_null(tree.anim_player) != player:
|
|
305
|
+
continue
|
|
306
|
+
var tree_path: String = str(root.get_path_to(tree))
|
|
307
|
+
_collect_playing(tree.tree_root, "", tree_path, animation_name, found)
|
|
308
|
+
return found
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
static func _collect_playing(
|
|
312
|
+
node: AnimationNode, at: String, tree_path: String, animation_name: String, found: Array
|
|
313
|
+
) -> void:
|
|
314
|
+
if node is AnimationNodeAnimation:
|
|
315
|
+
var plays: AnimationNodeAnimation = node
|
|
316
|
+
if plays.animation == StringName(animation_name):
|
|
317
|
+
found.append([plays, {"animTreePath": tree_path, "state": "." if at.is_empty() else at}])
|
|
318
|
+
return
|
|
319
|
+
var children: Dictionary = {}
|
|
320
|
+
if node is AnimationNodeStateMachine:
|
|
321
|
+
var machine: AnimationNodeStateMachine = node
|
|
322
|
+
for state: StringName in machine.get_node_list():
|
|
323
|
+
children[str(state)] = machine.get_node(state)
|
|
324
|
+
elif node is AnimationNodeBlendTree:
|
|
325
|
+
var blend: AnimationNodeBlendTree = node
|
|
326
|
+
for input: StringName in blend.get_node_list():
|
|
327
|
+
children[str(input)] = blend.get_node(input)
|
|
328
|
+
elif node is AnimationNodeBlendSpace1D:
|
|
329
|
+
var line: AnimationNodeBlendSpace1D = node
|
|
330
|
+
for point: int in line.get_blend_point_count():
|
|
331
|
+
children[str(point)] = line.get_blend_point_node(point)
|
|
332
|
+
elif node is AnimationNodeBlendSpace2D:
|
|
333
|
+
var plane: AnimationNodeBlendSpace2D = node
|
|
334
|
+
for point: int in plane.get_blend_point_count():
|
|
335
|
+
children[str(point)] = plane.get_blend_point_node(point)
|
|
336
|
+
for step: String in children:
|
|
337
|
+
var child: AnimationNode = children[step]
|
|
338
|
+
_collect_playing(child, step if at.is_empty() else at + "/" + step, tree_path, animation_name, found)
|
|
@@ -6,6 +6,36 @@ extends RefCounted
|
|
|
6
6
|
|
|
7
7
|
const GdhRead = preload("reading.gd")
|
|
8
8
|
|
|
9
|
+
## The section of layer_names in project.godot naming the layers of each layers hint.
|
|
10
|
+
const LAYER_NAMES: Dictionary[int, String] = {
|
|
11
|
+
PROPERTY_HINT_LAYERS_2D_PHYSICS: "2d_physics",
|
|
12
|
+
PROPERTY_HINT_LAYERS_2D_RENDER: "2d_render",
|
|
13
|
+
PROPERTY_HINT_LAYERS_2D_NAVIGATION: "2d_navigation",
|
|
14
|
+
PROPERTY_HINT_LAYERS_3D_PHYSICS: "3d_physics",
|
|
15
|
+
PROPERTY_HINT_LAYERS_3D_RENDER: "3d_render",
|
|
16
|
+
PROPERTY_HINT_LAYERS_3D_NAVIGATION: "3d_navigation",
|
|
17
|
+
PROPERTY_HINT_LAYERS_AVOIDANCE: "avoidance",
|
|
18
|
+
}
|
|
19
|
+
## How many layers a mask holds, one bit each, whatever the editor names.
|
|
20
|
+
const MASK_LAYERS: int = 32
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## The layers [param mask] holds under a layers [param hint], each with the name project.godot
|
|
24
|
+
## gives it where it gives one.
|
|
25
|
+
static func layers_in(hint: int, mask: int) -> Array[Dictionary]:
|
|
26
|
+
var layers: Array[Dictionary] = []
|
|
27
|
+
for layer: int in range(1, MASK_LAYERS + 1):
|
|
28
|
+
if mask & (1 << (layer - 1)) == 0:
|
|
29
|
+
continue
|
|
30
|
+
var entry: Dictionary = {"layer": layer}
|
|
31
|
+
var name: String = str(
|
|
32
|
+
ProjectSettings.get_setting("layer_names/%s/layer_%d" % [LAYER_NAMES[hint], layer], "")
|
|
33
|
+
)
|
|
34
|
+
if not name.is_empty():
|
|
35
|
+
entry["name"] = name
|
|
36
|
+
layers.append(entry)
|
|
37
|
+
return layers
|
|
38
|
+
|
|
9
39
|
|
|
10
40
|
## The values an enum hint names, by name, from its hint string: "SWORD:0,SHIELD:1" as a script
|
|
11
41
|
## declares one, or "Add,Subtract" as an engine property does, each name then taking the number
|
|
@@ -127,6 +157,77 @@ static func enum_refusal(
|
|
|
127
157
|
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
128
158
|
|
|
129
159
|
|
|
160
|
+
## Why [param word], given as argument [param index] of [param called] for the enum [param named],
|
|
161
|
+
## names none of its [param choices].
|
|
162
|
+
static func enum_argument_refusal(
|
|
163
|
+
called: String, index: int, named: String, word: String, choices: Dictionary
|
|
164
|
+
) -> String:
|
|
165
|
+
var said: String = (
|
|
166
|
+
"%s takes the enum %s as argument %d, and %s is none of its values: send %s by name or by number"
|
|
167
|
+
% [called, named, index + 1, word, _listed(choices)]
|
|
168
|
+
)
|
|
169
|
+
var close: PackedStringArray = GdhRead.nearest(word, PackedStringArray(choices.keys()))
|
|
170
|
+
if close.is_empty():
|
|
171
|
+
return said + "."
|
|
172
|
+
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Why [param value], given as argument [param index] for the enum [param named], is none of its
|
|
176
|
+
## [param choices], or "" when it is one. The engine passes any number on.
|
|
177
|
+
static func outside_enum_argument(index: int, named: String, value: Variant, choices: Dictionary) -> String:
|
|
178
|
+
if typeof(value) != TYPE_INT or choices.values().has(value):
|
|
179
|
+
return ""
|
|
180
|
+
return (
|
|
181
|
+
"argument %d, %d, is none of the values %s declares: %s."
|
|
182
|
+
% [index + 1, value, named, _listed(choices)]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
## The values of the enum the engine names [param qualified], by name: an engine class's, as
|
|
187
|
+
## Node.ProcessMode, or a script's, as Named.Size for a class_name, res://main.gd.Mood for a script
|
|
188
|
+
## without one, and either followed by inner classes. {} when it names nothing this can read.
|
|
189
|
+
static func enum_declared_as(qualified: String) -> Dictionary:
|
|
190
|
+
var cut: int = qualified.rfind(".")
|
|
191
|
+
if cut <= 0:
|
|
192
|
+
return {}
|
|
193
|
+
var owner: String = qualified.substr(0, cut)
|
|
194
|
+
var named: String = qualified.substr(cut + 1)
|
|
195
|
+
if ClassDB.class_exists(owner):
|
|
196
|
+
var constants: Dictionary = {}
|
|
197
|
+
for constant: String in ClassDB.class_get_enum_constants(owner, named):
|
|
198
|
+
constants[constant] = ClassDB.class_get_integer_constant(owner, constant)
|
|
199
|
+
return constants
|
|
200
|
+
var script: Script = _script_named_as(owner)
|
|
201
|
+
if script == null:
|
|
202
|
+
return {}
|
|
203
|
+
var found: Variant = script.get_script_constant_map().get(named)
|
|
204
|
+
if not found is Dictionary:
|
|
205
|
+
return {}
|
|
206
|
+
var values: Dictionary = found
|
|
207
|
+
return values
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## The script [param owner] names: a class_name, a res:// path, or an inner class of either after a
|
|
211
|
+
## dot, or null.
|
|
212
|
+
static func _script_named_as(owner: String) -> Script:
|
|
213
|
+
var by_name: Script = script_class(owner)
|
|
214
|
+
if by_name != null:
|
|
215
|
+
return by_name
|
|
216
|
+
if owner.begins_with("res://") and ResourceLoader.exists(owner):
|
|
217
|
+
return load(owner) as Script
|
|
218
|
+
var cut: int = owner.rfind(".")
|
|
219
|
+
if cut <= 0:
|
|
220
|
+
return null
|
|
221
|
+
var outer: Script = _script_named_as(owner.substr(0, cut))
|
|
222
|
+
if outer == null:
|
|
223
|
+
return null
|
|
224
|
+
var inner: Variant = outer.get_script_constant_map().get(owner.substr(cut + 1))
|
|
225
|
+
if not inner is Script:
|
|
226
|
+
return null
|
|
227
|
+
var inner_script: Script = inner
|
|
228
|
+
return inner_script
|
|
229
|
+
|
|
230
|
+
|
|
130
231
|
## Why [param value], shown as [param shown], lies outside what [param declared] allows, an export
|
|
131
232
|
## range or an enum's values, or "" when it does not. A range marked or_greater or or_less is open
|
|
132
233
|
## on that side.
|
|
@@ -153,6 +254,19 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
|
|
|
153
254
|
if choices.is_empty() or choices.values().has(value):
|
|
154
255
|
return ""
|
|
155
256
|
return "%d is none of the values %s declares: %s." % [value, property, _listed(choices)]
|
|
257
|
+
if hint == PROPERTY_HINT_FLAGS and typeof(value) == TYPE_INT:
|
|
258
|
+
var flags: Dictionary = flags_of(hint_string)
|
|
259
|
+
var known: int = 0
|
|
260
|
+
for bit: int in flags.values():
|
|
261
|
+
known |= bit
|
|
262
|
+
var whole: int = value
|
|
263
|
+
var stray: int = whole & ~known
|
|
264
|
+
if flags.is_empty() or stray == 0:
|
|
265
|
+
return ""
|
|
266
|
+
return (
|
|
267
|
+
"%d holds bits no flag of %s has (%d): its flags are %s; the engine stores it as given."
|
|
268
|
+
% [whole, property, stray, _listed(flags)]
|
|
269
|
+
)
|
|
156
270
|
if hint != PROPERTY_HINT_RANGE:
|
|
157
271
|
return ""
|
|
158
272
|
var parts: PackedStringArray = []
|
|
@@ -173,6 +287,20 @@ static func outside(target: Object, declared: Dictionary, value: Variant, shown:
|
|
|
173
287
|
)
|
|
174
288
|
|
|
175
289
|
|
|
290
|
+
## The flags a flags hint names, each with its bit: "Fill:1,Expand:2" gives its values, and a name
|
|
291
|
+
## with none takes the bit of its place, as @export_flags("A", "B") does.
|
|
292
|
+
static func flags_of(hint_string: String) -> Dictionary:
|
|
293
|
+
var flags: Dictionary = {}
|
|
294
|
+
var parts: PackedStringArray = hint_string.split(",", false)
|
|
295
|
+
for index: int in parts.size():
|
|
296
|
+
var part: String = parts[index].strip_edges()
|
|
297
|
+
if ":" in part:
|
|
298
|
+
flags[part.get_slice(":", 0)] = part.get_slice(":", 1).to_int()
|
|
299
|
+
else:
|
|
300
|
+
flags[part] = 1 << index
|
|
301
|
+
return flags
|
|
302
|
+
|
|
303
|
+
|
|
176
304
|
static func _listed(choices: Dictionary) -> String:
|
|
177
305
|
var listed: PackedStringArray = []
|
|
178
306
|
for name: String in choices:
|
|
@@ -291,3 +419,147 @@ static func is_a(value: Object, name: String) -> bool:
|
|
|
291
419
|
return true
|
|
292
420
|
script = script.get_base_script()
|
|
293
421
|
return false
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
## Whether writing [param name] on [param target] runs code of its script: the script itself, a
|
|
425
|
+
## property it declares, or any property once it takes sets through [code]_set[/code].
|
|
426
|
+
static func runs_script_code(target: Object, name: String) -> bool:
|
|
427
|
+
var script: Script = target.get_script()
|
|
428
|
+
if name == "script":
|
|
429
|
+
return true
|
|
430
|
+
if script == null:
|
|
431
|
+
return false
|
|
432
|
+
for method: Dictionary in script.get_script_method_list():
|
|
433
|
+
if str(method.get("name", "")) == "_set":
|
|
434
|
+
return true
|
|
435
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
436
|
+
if str(declared.get("name", "")) == name:
|
|
437
|
+
return true
|
|
438
|
+
return false
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
## Why [param property] of a [param holder], an [param engine_class], declared as [param declared],
|
|
442
|
+
## holds [param held], as shown, after it was given [param given], the value and how it is shown. A
|
|
443
|
+
## text property with a list of names holds only one of them, and its getter answers its default
|
|
444
|
+
## for any other. A player's bus is one, though the engine fills its list only in the inspector, so
|
|
445
|
+
## the buses are read from the AudioServer, which holds the project's layout; "the engine changed or
|
|
446
|
+
## refused the value" left a caller to guess the bus did not exist.
|
|
447
|
+
static func not_held(
|
|
448
|
+
holder: String, engine_class: String, property: String, declared: Dictionary, given: Array, held: String
|
|
449
|
+
) -> String:
|
|
450
|
+
var value: Variant = given[0]
|
|
451
|
+
var said: String = "%s.%s was given %s and holds %s after it was set" % [holder, property, given[1], held]
|
|
452
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL))
|
|
453
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
454
|
+
if hint in LAYER_NAMES and typeof(value) == TYPE_INT:
|
|
455
|
+
return (
|
|
456
|
+
(
|
|
457
|
+
"%s: it is a mask of the %s layers, one bit each from layer 1 (1) to layer %d (%d), so it "
|
|
458
|
+
+ "takes 0 to %d and the engine keeps only those bits. Nothing was written."
|
|
459
|
+
)
|
|
460
|
+
% [
|
|
461
|
+
said,
|
|
462
|
+
LAYER_NAMES[hint],
|
|
463
|
+
MASK_LAYERS,
|
|
464
|
+
1 << (MASK_LAYERS - 1),
|
|
465
|
+
(1 << MASK_LAYERS) - 1,
|
|
466
|
+
]
|
|
467
|
+
)
|
|
468
|
+
var names: PackedStringArray = []
|
|
469
|
+
if hint == PROPERTY_HINT_ENUM and (type == TYPE_STRING or type == TYPE_STRING_NAME):
|
|
470
|
+
names = PackedStringArray(enum_values(str(declared.get("hint_string", ""))).keys())
|
|
471
|
+
var bus: bool = property == "bus" and engine_class.begins_with("AudioStreamPlayer")
|
|
472
|
+
if bus:
|
|
473
|
+
names = []
|
|
474
|
+
for index: int in AudioServer.bus_count:
|
|
475
|
+
var _added: bool = names.append(AudioServer.get_bus_name(index))
|
|
476
|
+
if names.is_empty() or names.has(str(value)):
|
|
477
|
+
return said + ": the engine changed or refused the value, so nothing was written."
|
|
478
|
+
var close: PackedStringArray = GdhRead.nearest(str(value), names)
|
|
479
|
+
var nearest: String = (
|
|
480
|
+
"%s %s" % ["is" if close.size() == 1 else "are", ", ".join(close)] if not close.is_empty() else ""
|
|
481
|
+
)
|
|
482
|
+
if bus:
|
|
483
|
+
# Not "holds Master after it was set": the getter answers the first bus for any name the
|
|
484
|
+
# layout lacks, and nothing was kept.
|
|
485
|
+
return (
|
|
486
|
+
(
|
|
487
|
+
"%s.bus takes the name of one of the project's audio buses, and %s is none of them, so it "
|
|
488
|
+
+ "would play on %s; %s. project_info include audio_buses lists them, and "
|
|
489
|
+
+ "project_settings add_audio_bus adds one. Nothing was written."
|
|
490
|
+
)
|
|
491
|
+
% [
|
|
492
|
+
holder,
|
|
493
|
+
given[1],
|
|
494
|
+
AudioServer.get_bus_name(0),
|
|
495
|
+
"the buses are " + ", ".join(names) if nearest.is_empty() else "the nearest " + nearest
|
|
496
|
+
]
|
|
497
|
+
)
|
|
498
|
+
return (
|
|
499
|
+
"%s: it takes one of the names it lists, and %s is none of them; %s. Nothing was written."
|
|
500
|
+
% [
|
|
501
|
+
said,
|
|
502
|
+
given[1],
|
|
503
|
+
"it lists " + ", ".join(names) if nearest.is_empty() else "the nearest it lists " + nearest
|
|
504
|
+
]
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
## The sentence naming the children of [param node] that have [param property], or "" when none
|
|
509
|
+
## does. A property of a helper node set on the body it serves, as navigation_layers on the
|
|
510
|
+
## CharacterBody2D rather than its NavigationAgent2D, was refused with the nearest physics layer.
|
|
511
|
+
static func held_by_children(node: Node, property: String) -> String:
|
|
512
|
+
var root: Node = node.owner if node.owner != null else node
|
|
513
|
+
var holders: PackedStringArray = []
|
|
514
|
+
for child: Node in node.get_children():
|
|
515
|
+
if property in child:
|
|
516
|
+
var _added: bool = holders.append("%s (a %s)" % [root.get_path_to(child), child.get_class()])
|
|
517
|
+
if holders.is_empty():
|
|
518
|
+
return ""
|
|
519
|
+
if holders.size() == 1:
|
|
520
|
+
return " The node at %s has it." % holders[0]
|
|
521
|
+
var last: String = holders[holders.size() - 1]
|
|
522
|
+
return " The nodes at %s and %s have it." % [", ".join(holders.slice(0, -1)), last]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
## A sentence for each node path in [param given], by property, that names no node from
|
|
526
|
+
## [param node]. A path is written whatever it names, as the inspector writes one, since the node may
|
|
527
|
+
## be added next; a joint whose node_a named nothing then joined nothing, and no answer said so.
|
|
528
|
+
static func naming_nothing(node: Node, given: Dictionary) -> Dictionary:
|
|
529
|
+
var said: Dictionary = {}
|
|
530
|
+
for property: String in given:
|
|
531
|
+
if "/" in property or ":" in property:
|
|
532
|
+
continue
|
|
533
|
+
# What the node holds, since a path given as text is held as text until the engine stores it.
|
|
534
|
+
var path: Variant = node.get(property)
|
|
535
|
+
if not path is NodePath:
|
|
536
|
+
continue
|
|
537
|
+
var named: NodePath = path
|
|
538
|
+
if named.is_empty() or node.get_node_or_null(named) != null:
|
|
539
|
+
continue
|
|
540
|
+
said[property] = (
|
|
541
|
+
(
|
|
542
|
+
"%s names no node from %s. It is written, as the editor writes one, since that node may "
|
|
543
|
+
+ "be added next; until it is, %s finds nothing."
|
|
544
|
+
)
|
|
545
|
+
% [str(named), node.name, property]
|
|
546
|
+
)
|
|
547
|
+
return said
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
## Why [param material] has no [param property]: a ShaderMaterial's parameters are its shader's
|
|
551
|
+
## uniforms, so the shader is what lacks one, and "ShaderMaterial has no property" named the
|
|
552
|
+
## material.
|
|
553
|
+
static func no_uniform(material: ShaderMaterial, property: String) -> String:
|
|
554
|
+
var uniform: String = property.trim_prefix("shader_parameter/")
|
|
555
|
+
if material.shader == null:
|
|
556
|
+
return "The ShaderMaterial has no shader, so it has no parameter %s yet; set shader first." % uniform
|
|
557
|
+
var shader: String = material.shader.resource_path
|
|
558
|
+
var named: String = shader if not shader.is_empty() else "Its shader"
|
|
559
|
+
var uniforms: Array = []
|
|
560
|
+
for entry: Dictionary in material.shader.get_shader_uniform_list():
|
|
561
|
+
uniforms.append(str(entry["name"]))
|
|
562
|
+
var said: String = "%s declares no uniform %s, which %s would set" % [named, uniform, property]
|
|
563
|
+
if uniforms.is_empty():
|
|
564
|
+
return said + "; it declares no uniforms."
|
|
565
|
+
return GdhRead.offered(said, uniform, uniforms) + "."
|
|
@@ -16,15 +16,19 @@ const GdhSerialisation = preload("serialisation.gd")
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf and
|
|
19
|
-
## editableChildren on a node that instances another scene and
|
|
20
|
-
## [param include_properties] is set, the file's value or the default where it sets none
|
|
19
|
+
## editableChildren on a node that instances another scene and its stored properties when
|
|
20
|
+
## [param include_properties] is set, the file's value or the default where it sets none, and on a
|
|
21
|
+
## node another file makes setHere, the properties this file sets on it. Defaults the file leaves
|
|
22
|
+
## alone are counted as defaultsLeftOut rather than listed unless [param include_defaults] is set.
|
|
21
23
|
## [param depth] -1 reads every level.
|
|
22
|
-
static func scene_tree(
|
|
24
|
+
static func scene_tree(
|
|
25
|
+
scene_path: String, depth: int, include_properties: bool, include_defaults: bool
|
|
26
|
+
) -> Dictionary:
|
|
23
27
|
var laid: Dictionary = _laid(scene_path)
|
|
24
28
|
if not laid.get("ok", false):
|
|
25
29
|
return laid
|
|
26
30
|
var nodes: Dictionary = laid["nodes"]
|
|
27
|
-
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
31
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, include_defaults, depth, 0)}
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
## The node at [param node_path] in [param scene_path] as {ok, name, type, properties}, laid as
|
|
@@ -176,6 +180,7 @@ static func _lay(
|
|
|
176
180
|
continue
|
|
177
181
|
else:
|
|
178
182
|
nodes[path] = _made(str(state.get_node_type(index)))
|
|
183
|
+
nodes[path]["madeHere"] = loaded != null
|
|
179
184
|
if ids[index] >= 0:
|
|
180
185
|
nodes[path]["uniqueId"] = ids[index]
|
|
181
186
|
# Laid at the root with no scene handed in is a scene the one being read inherits, and the
|
|
@@ -202,10 +207,15 @@ static func _lay(
|
|
|
202
207
|
if not grouped.has(str(group)):
|
|
203
208
|
grouped.append(str(group))
|
|
204
209
|
var held: Dictionary = nodes[path]["properties"]
|
|
210
|
+
# What the file being read sets, kept apart for a node another file makes, whose values read
|
|
211
|
+
# the same whether this scene changed them or that one gave them.
|
|
212
|
+
var laid: Dictionary = nodes[path]
|
|
213
|
+
var set_here: Array = laid.get_or_add("setHere", [])
|
|
205
214
|
for property: int in state.get_node_property_count(index):
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
215
|
+
var named: String = str(state.get_node_property_name(index, property))
|
|
216
|
+
held[named] = state.get_node_property_value(index, property)
|
|
217
|
+
if loaded != null and not set_here.has(named):
|
|
218
|
+
set_here.append(named)
|
|
209
219
|
# Only the scene being read and the scenes it inherits share its root, the owner whose editable
|
|
210
220
|
# instances a write here sets; a file instanced below makes its own owner's editable.
|
|
211
221
|
if prefix == ".":
|
|
@@ -321,7 +331,12 @@ static func _joined(prefix: String, given: String) -> String:
|
|
|
321
331
|
|
|
322
332
|
|
|
323
333
|
static func _entry(
|
|
324
|
-
nodes: Dictionary,
|
|
334
|
+
nodes: Dictionary,
|
|
335
|
+
path: String,
|
|
336
|
+
include_properties: bool,
|
|
337
|
+
include_defaults: bool,
|
|
338
|
+
depth: int,
|
|
339
|
+
current_depth: int
|
|
325
340
|
) -> Dictionary:
|
|
326
341
|
var node: Dictionary = nodes[path]
|
|
327
342
|
var children: Array[Dictionary] = []
|
|
@@ -344,7 +359,21 @@ static func _entry(
|
|
|
344
359
|
data["groups"] = sorted
|
|
345
360
|
if include_properties:
|
|
346
361
|
var given: Dictionary = node["properties"]
|
|
347
|
-
|
|
362
|
+
var stored: Dictionary = _stored(str(node["type"]), given, include_defaults)
|
|
363
|
+
data["properties"] = stored["properties"]
|
|
364
|
+
if stored["left_out"] > 0:
|
|
365
|
+
data["defaultsLeftOut"] = stored["left_out"]
|
|
366
|
+
var cells: int = _cell_count(str(node["type"]), given)
|
|
367
|
+
if cells >= 0:
|
|
368
|
+
data["cellCount"] = cells
|
|
369
|
+
data["cellsListedBy"] = (
|
|
370
|
+
"scene_node get with nodePath %s, which lists them as set_tilemap_cells takes them" % path
|
|
371
|
+
)
|
|
372
|
+
if not node.get("madeHere", false):
|
|
373
|
+
var kept: Array = node.get("setHere", [])
|
|
374
|
+
var set_here: Array = kept.duplicate()
|
|
375
|
+
set_here.sort()
|
|
376
|
+
data["setHere"] = set_here
|
|
348
377
|
var below: Array[String] = node["children"]
|
|
349
378
|
# Counted rather than left as an empty list, which reads as a node with no children.
|
|
350
379
|
if depth >= 0 and current_depth >= depth:
|
|
@@ -352,19 +381,56 @@ static func _entry(
|
|
|
352
381
|
data["childrenNotListed"] = below.size()
|
|
353
382
|
return data
|
|
354
383
|
for child: String in below:
|
|
355
|
-
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
384
|
+
children.append(_entry(nodes, child, include_properties, include_defaults, depth, current_depth + 1))
|
|
356
385
|
return data
|
|
357
386
|
|
|
358
387
|
|
|
388
|
+
## How many cells a TileMapLayer or TileMap of [param type] holds under the values [param given], or
|
|
389
|
+
## -1 when it is another node or the file sets none of its cells. The file keeps them as packed bytes,
|
|
390
|
+
## which a read of the tree answered with alone, so a caller decoded the base64 to learn what was
|
|
391
|
+
## painted. Only cells the file sets are counted: an instance's come from its own scene.
|
|
392
|
+
static func _cell_count(type: String, given: Dictionary) -> int:
|
|
393
|
+
if not ClassDB.class_exists(type) or not ClassDB.can_instantiate(type):
|
|
394
|
+
return -1
|
|
395
|
+
var single: bool = ClassDB.is_parent_class(type, "TileMapLayer")
|
|
396
|
+
if not single and not ClassDB.is_parent_class(type, "TileMap"):
|
|
397
|
+
return -1
|
|
398
|
+
var painted: bool = false
|
|
399
|
+
for property: Variant in given:
|
|
400
|
+
var named: String = str(property)
|
|
401
|
+
if named == "tile_map_data" or (named.begins_with("layer_") and named.ends_with("/tile_data")):
|
|
402
|
+
painted = true
|
|
403
|
+
if not painted:
|
|
404
|
+
return -1
|
|
405
|
+
var bare: Node = ClassDB.instantiate(type)
|
|
406
|
+
for property: Variant in given:
|
|
407
|
+
if property != "script":
|
|
408
|
+
bare.set(str(property), given[property])
|
|
409
|
+
var count: int = 0
|
|
410
|
+
if single:
|
|
411
|
+
count = (bare as TileMapLayer).get_used_cells().size()
|
|
412
|
+
else:
|
|
413
|
+
var layered: TileMap = bare as TileMap
|
|
414
|
+
for layer: int in layered.get_layers_count():
|
|
415
|
+
count += layered.get_used_cells(layer).size()
|
|
416
|
+
bare.free()
|
|
417
|
+
return count
|
|
418
|
+
|
|
419
|
+
|
|
359
420
|
## Every property a node of [param type] stores, with the value [param given] sets or the default,
|
|
360
|
-
## read as a node holding those values would answer them.
|
|
421
|
+
## read as a node holding those values would answer them, as {properties, left_out}.
|
|
361
422
|
##
|
|
362
423
|
## The engine's half is read off a bare node of the class with the file's values set on it, which
|
|
363
424
|
## runs no script: what a node lists depends on its values (a thread group's settings appear only
|
|
364
425
|
## once it has one) and some setters set others (a Control's anchors preset sets how it grows), so
|
|
365
426
|
## the class's defaults and the file's entries alone answered neither. The script's half is its
|
|
366
427
|
## declared defaults under the file's values.
|
|
367
|
-
|
|
428
|
+
##
|
|
429
|
+
## Without [param include_defaults], properties keeps only what the file sets and what differs from
|
|
430
|
+
## a node of the class holding none of its values, and left_out counts the rest. Every Control
|
|
431
|
+
## carries a hundred properties, and ten of them came to 57 KB of defaults around the few values a
|
|
432
|
+
## menu scene set.
|
|
433
|
+
static func _stored(type: String, given: Dictionary, include_defaults: bool) -> Dictionary:
|
|
368
434
|
var attached: Variant = given.get("script")
|
|
369
435
|
var script: Script = attached if attached is Script else null
|
|
370
436
|
var scripted: Dictionary = {}
|
|
@@ -374,8 +440,10 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
374
440
|
if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
375
441
|
scripted[property] = script.get_property_default_value(property)
|
|
376
442
|
var values: Dictionary = {}
|
|
443
|
+
var defaults: Dictionary = {}
|
|
377
444
|
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
|
|
378
445
|
var bare: Object = ClassDB.instantiate(type)
|
|
446
|
+
var plain: Object = ClassDB.instantiate(type)
|
|
379
447
|
for property: Variant in given:
|
|
380
448
|
if property != "script" and not scripted.has(property):
|
|
381
449
|
bare.set(str(property), given[property])
|
|
@@ -383,18 +451,30 @@ static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
|
383
451
|
var property: String = str(declared.get("name", ""))
|
|
384
452
|
if not property.is_empty() and GdhRead.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
385
453
|
values[property] = bare.get(property)
|
|
386
|
-
|
|
387
|
-
|
|
454
|
+
defaults[property] = plain.get(property)
|
|
455
|
+
for made: Object in [bare, plain]:
|
|
456
|
+
if made is Node:
|
|
457
|
+
made.free()
|
|
388
458
|
for property: String in scripted:
|
|
389
459
|
values[property] = scripted[property]
|
|
460
|
+
defaults[property] = scripted[property]
|
|
390
461
|
# Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
|
|
391
462
|
# for a property that holds the node once it is in a tree.
|
|
392
463
|
values.merge(given, true)
|
|
393
464
|
var serialiser: GdhSerialisation = GdhSerialisation.new()
|
|
394
465
|
var answered: Dictionary = {}
|
|
466
|
+
var left_out: int = 0
|
|
395
467
|
for property: Variant in values:
|
|
468
|
+
var as_default: bool = (
|
|
469
|
+
not given.has(property)
|
|
470
|
+
and defaults.has(property)
|
|
471
|
+
and GdhSame.same(values[property], defaults[property])
|
|
472
|
+
)
|
|
473
|
+
if as_default and not include_defaults:
|
|
474
|
+
left_out += 1
|
|
475
|
+
continue
|
|
396
476
|
answered[property] = serialiser.serialize_value(values[property])
|
|
397
|
-
return answered
|
|
477
|
+
return {"properties": answered, "left_out": left_out}
|
|
398
478
|
|
|
399
479
|
|
|
400
480
|
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
@@ -407,12 +487,16 @@ static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
|
407
487
|
var file: String = res_path.get_slice("::", 0)
|
|
408
488
|
if not FileAccess.file_exists(file):
|
|
409
489
|
return {"ok": false, "error": "No resource at " + file}
|
|
410
|
-
var
|
|
411
|
-
if
|
|
490
|
+
var whole: Resource = ResourceLoader.load(file, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
491
|
+
if whole == null:
|
|
412
492
|
return {"ok": false, "error": "%s did not load as a resource." % file}
|
|
493
|
+
# The file's resource is held until the read is done, since what it keeps can depend on it: a
|
|
494
|
+
# TileSet that goes unlinks its sources, and an atlas source with no TileSet lists none of its
|
|
495
|
+
# tiles' physics, navigation or custom data.
|
|
496
|
+
var loaded: Resource = whole
|
|
413
497
|
if file != res_path:
|
|
414
498
|
var id: String = res_path.trim_prefix(file + "::")
|
|
415
|
-
var inner: Resource = _kept_in(
|
|
499
|
+
var inner: Resource = _kept_in(whole, file, id, {})
|
|
416
500
|
if inner == null:
|
|
417
501
|
return {"ok": false, "error": "%s keeps no resource with the id %s." % [file, id]}
|
|
418
502
|
loaded = inner
|