gdharness 1.2.41 → 1.3.0
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 +643 -378
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -4
- package/build/godot/addons/gdharness_editor/bridge_client.gd +1 -1
- package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
- package/build/godot/addons/gdharness_editor/headless_check.gd +34 -4
- package/build/godot/addons/gdharness_editor/property_values.gd +4 -9
- package/build/godot/addons/gdharness_editor/reading.gd +25 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +209 -15
- package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +9 -9
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +91 -49
- package/build/godot/addons/gdharness_runtime/reading.gd +25 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +29 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +2 -7
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +6 -14
- package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
- package/build/godot/operations/animation_reads.gd +7 -4
- package/build/godot/operations/editor_tools.gd +19 -1
- package/build/godot/operations/file_reads.gd +134 -10
- package/build/godot/operations/property_values.gd +4 -9
- package/build/godot/operations/reading.gd +25 -0
- package/build/godot/operations/serialisation.gd +5 -1
- package/build/keeper.js +10 -4
- package/package.json +1 -2
- package/build/index.js +0 -45043
|
@@ -15,9 +15,10 @@ const Read = preload("reading.gd")
|
|
|
15
15
|
const Serialisation = preload("serialisation.gd")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf
|
|
19
|
-
## instances another scene and every stored property when
|
|
20
|
-
## file's value or the default where it sets none.
|
|
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 every stored property when
|
|
20
|
+
## [param include_properties] is set, the file's value or the default where it sets none.
|
|
21
|
+
## [param depth] -1 reads every level.
|
|
21
22
|
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
23
|
var laid: Dictionary = _laid(scene_path)
|
|
23
24
|
if not laid.get("ok", false):
|
|
@@ -41,6 +42,31 @@ static func scene_node(scene_path: String, node_path: String) -> Dictionary:
|
|
|
41
42
|
return {"ok": true, "name": found["name"], "type": found["type"], "properties": found["properties"]}
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
## [param said], the refusal for [param node_path], which [param scene_path] does not have, with
|
|
46
|
+
## where the path leaves the scene as [method Read.missing_below] says it.
|
|
47
|
+
static func missing_from(said: String, scene_path: String, node_path: String) -> String:
|
|
48
|
+
var laid: Dictionary = _laid(scene_path)
|
|
49
|
+
if not laid.get("ok", false):
|
|
50
|
+
return said
|
|
51
|
+
var nodes: Dictionary = laid["nodes"]
|
|
52
|
+
var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
|
|
53
|
+
var at: String = "."
|
|
54
|
+
var reached: PackedStringArray = []
|
|
55
|
+
for part: String in parts:
|
|
56
|
+
var below: String = _joined(at, part)
|
|
57
|
+
if not nodes.has(below):
|
|
58
|
+
break
|
|
59
|
+
at = below
|
|
60
|
+
var _walked: bool = reached.append(part)
|
|
61
|
+
if reached.size() >= parts.size():
|
|
62
|
+
return said
|
|
63
|
+
var names: PackedStringArray = []
|
|
64
|
+
var children: Array[String] = nodes[at]["children"]
|
|
65
|
+
for child: String in children:
|
|
66
|
+
var _named: bool = names.append(str(nodes[child]["name"]))
|
|
67
|
+
return Read.missing_below(said, str(nodes["."]["name"]), reached, parts[reached.size()], names)
|
|
68
|
+
|
|
69
|
+
|
|
44
70
|
## Every node of [param scene_path] as {ok, nodes}, keyed by path, or a refusal.
|
|
45
71
|
static func _laid(scene_path: String) -> Dictionary:
|
|
46
72
|
if not FileAccess.file_exists(scene_path):
|
|
@@ -51,13 +77,11 @@ static func _laid(scene_path: String) -> Dictionary:
|
|
|
51
77
|
if loaded == null:
|
|
52
78
|
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
53
79
|
if not loaded is PackedScene:
|
|
80
|
+
var kind: String = loaded.get_class()
|
|
81
|
+
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
54
82
|
return {
|
|
55
83
|
"ok": false,
|
|
56
|
-
"error":
|
|
57
|
-
(
|
|
58
|
-
"%s is a %s, not a scene: resource_edit read reads its properties."
|
|
59
|
-
% [scene_path, loaded.get_class()]
|
|
60
|
-
)
|
|
84
|
+
"error": "%s is %s, not a scene: resource_edit read reads its properties." % [scene_path, sort]
|
|
61
85
|
}
|
|
62
86
|
var packed: PackedScene = loaded
|
|
63
87
|
var nodes: Dictionary = {}
|
|
@@ -82,8 +106,18 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
82
106
|
var refused: String = _lay(base, prefix, nodes, within)
|
|
83
107
|
if not refused.is_empty():
|
|
84
108
|
return refused
|
|
109
|
+
var ids: Array[int] = _unique_ids(state.get_path(), state.get_node_count())
|
|
85
110
|
for index: int in state.get_node_count():
|
|
86
111
|
var path: String = _joined(prefix, str(state.get_node_path(index)))
|
|
112
|
+
var recovered: bool = false
|
|
113
|
+
if not nodes.has(path) and index > 0 and _overrides(state, index) and ids[index] >= 0:
|
|
114
|
+
# The engine finds an entry whose node was renamed or moved in the scene it comes from by
|
|
115
|
+
# the node's unique_id, and keeps the node's new name.
|
|
116
|
+
var parent: String = _joined(prefix, str(state.get_node_path(index, true)))
|
|
117
|
+
var found: String = _by_id(nodes, _owner_of(nodes, parent, prefix), ids[index])
|
|
118
|
+
recovered = not found.is_empty()
|
|
119
|
+
if recovered:
|
|
120
|
+
path = found
|
|
87
121
|
var fresh: bool = not nodes.has(path)
|
|
88
122
|
if fresh:
|
|
89
123
|
var instance: PackedScene = state.get_node_instance(index)
|
|
@@ -98,6 +132,7 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
98
132
|
if not nodes.has(path):
|
|
99
133
|
return "%s holds no nodes" % instance.resource_path
|
|
100
134
|
nodes[path]["instanceOf"] = instance.resource_path
|
|
135
|
+
nodes[path]["editableChildren"] = false
|
|
101
136
|
elif state.is_node_instance_placeholder(index):
|
|
102
137
|
nodes[path] = _made("InstancePlaceholder")
|
|
103
138
|
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
@@ -106,9 +141,12 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
106
141
|
continue
|
|
107
142
|
else:
|
|
108
143
|
nodes[path] = _made(str(state.get_node_type(index)))
|
|
144
|
+
if ids[index] >= 0:
|
|
145
|
+
nodes[path]["uniqueId"] = ids[index]
|
|
109
146
|
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
110
147
|
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
111
|
-
|
|
148
|
+
if not recovered:
|
|
149
|
+
nodes[path]["name"] = str(state.get_node_name(index))
|
|
112
150
|
if fresh:
|
|
113
151
|
# The root of what is laid here is placed by whoever instanced it.
|
|
114
152
|
if path != prefix:
|
|
@@ -123,6 +161,86 @@ static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: A
|
|
|
123
161
|
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
124
162
|
index, property
|
|
125
163
|
)
|
|
164
|
+
# Only the scene being read and the scenes it inherits share its root, the owner whose editable
|
|
165
|
+
# instances a write here sets; a file instanced below makes its own owner's editable.
|
|
166
|
+
if prefix == ".":
|
|
167
|
+
for editable: String in _editable_in(state.get_path()):
|
|
168
|
+
var at: String = _joined(prefix, editable)
|
|
169
|
+
var marked: Dictionary = nodes.get(at, {})
|
|
170
|
+
if marked.has("editableChildren"):
|
|
171
|
+
marked["editableChildren"] = true
|
|
172
|
+
return ""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## The paths of the instances [param scene_path] gives editable children, from its root. SceneState
|
|
176
|
+
## does not give them to a script, so they are read off the scene's bundle.
|
|
177
|
+
static func _editable_in(scene_path: String) -> PackedStringArray:
|
|
178
|
+
var found: PackedStringArray = []
|
|
179
|
+
var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
180
|
+
if not loaded is PackedScene:
|
|
181
|
+
return found
|
|
182
|
+
var given: Variant = loaded.get("_bundled")
|
|
183
|
+
if not given is Dictionary:
|
|
184
|
+
return found
|
|
185
|
+
var bundled: Dictionary = given
|
|
186
|
+
var listed: Variant = bundled.get("editable_instances", [])
|
|
187
|
+
if listed is Array:
|
|
188
|
+
for one: Variant in listed:
|
|
189
|
+
var _kept: bool = found.append(str(one))
|
|
190
|
+
return found
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
## The unique_id of each node entry in the text scene at [param scene_path], in the order its state
|
|
194
|
+
## holds them, -1 where an entry has none. All -1 for a binary scene, and for a file whose entries do
|
|
195
|
+
## not number [param count], since SceneState does not give the ids to a script.
|
|
196
|
+
static func _unique_ids(scene_path: String, count: int) -> Array[int]:
|
|
197
|
+
var ids: Array[int] = []
|
|
198
|
+
var _sized: int = ids.resize(count)
|
|
199
|
+
ids.fill(-1)
|
|
200
|
+
if not scene_path.ends_with(".tscn") or not FileAccess.file_exists(scene_path):
|
|
201
|
+
return ids
|
|
202
|
+
var read: Array[int] = []
|
|
203
|
+
var pattern: RegEx = RegEx.create_from_string(" unique_id=(\\d+)")
|
|
204
|
+
for line: String in FileAccess.get_file_as_string(scene_path).split("\n"):
|
|
205
|
+
if line.begins_with("[node "):
|
|
206
|
+
var found: RegExMatch = pattern.search(line)
|
|
207
|
+
read.append(int(found.get_string(1)) if found != null else -1)
|
|
208
|
+
return read if read.size() == count else ids
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
## Whether entry [param index] of [param state] only sets values on a node another file makes.
|
|
212
|
+
static func _overrides(state: SceneState, index: int) -> bool:
|
|
213
|
+
return (
|
|
214
|
+
state.get_node_instance(index) == null
|
|
215
|
+
and not state.is_node_instance_placeholder(index)
|
|
216
|
+
and str(state.get_node_type(index)).is_empty()
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## The node whose file owns what sits under [param parent]: the nearest instance at or above it, or
|
|
221
|
+
## [param prefix], the root of what is being laid, as the engine searches.
|
|
222
|
+
static func _owner_of(nodes: Dictionary, parent: String, prefix: String) -> String:
|
|
223
|
+
var at: String = parent
|
|
224
|
+
while at != prefix and at != "." and nodes.has(at):
|
|
225
|
+
var entry: Dictionary = nodes[at]
|
|
226
|
+
if entry.has("instanceOf"):
|
|
227
|
+
break
|
|
228
|
+
at = "." if not at.contains("/") else at.get_base_dir()
|
|
229
|
+
return at if nodes.has(at) else prefix
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
## The path of the node [param owner] owns whose unique_id is [param id], or "": one under an instance
|
|
233
|
+
## nested below [param owner] belongs to that instance's file, whose ids are numbered apart.
|
|
234
|
+
static func _by_id(nodes: Dictionary, owner: String, id: int) -> String:
|
|
235
|
+
for path: String in nodes:
|
|
236
|
+
if path == owner or not (owner == "." or path.begins_with(owner + "/")):
|
|
237
|
+
continue
|
|
238
|
+
var entry: Dictionary = nodes[path]
|
|
239
|
+
if Read.as_int(entry.get("uniqueId", -1), -1) != id:
|
|
240
|
+
continue
|
|
241
|
+
var above: String = "." if not path.contains("/") else path.get_base_dir()
|
|
242
|
+
if _owner_of(nodes, above, owner) == owner:
|
|
243
|
+
return path
|
|
126
244
|
return ""
|
|
127
245
|
|
|
128
246
|
|
|
@@ -162,12 +280,18 @@ static func _entry(
|
|
|
162
280
|
# The root's file is the scene being read, so only another node's names a second scene.
|
|
163
281
|
if path != "." and node.has("instanceOf"):
|
|
164
282
|
data["instanceOf"] = node["instanceOf"]
|
|
283
|
+
if node.has("editableChildren"):
|
|
284
|
+
data["editableChildren"] = node["editableChildren"]
|
|
165
285
|
if include_properties:
|
|
166
286
|
var given: Dictionary = node["properties"]
|
|
167
287
|
data["properties"] = _stored(str(node["type"]), given)
|
|
288
|
+
var below: Array[String] = node["children"]
|
|
289
|
+
# Counted rather than left as an empty list, which reads as a node with no children.
|
|
168
290
|
if depth >= 0 and current_depth >= depth:
|
|
291
|
+
if not below.is_empty():
|
|
292
|
+
data["childrenNotListed"] = below.size()
|
|
169
293
|
return data
|
|
170
|
-
for child: String in
|
|
294
|
+
for child: String in below:
|
|
171
295
|
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
172
296
|
return data
|
|
173
297
|
|
|
@@ -600,18 +600,13 @@ func shown(value: Variant) -> String:
|
|
|
600
600
|
## not mine". A tag that names a type but lacks the keys to build it is left unhandled on purpose,
|
|
601
601
|
## so the caller can still read it against the type the property declares.
|
|
602
602
|
func _parse_tagged_dictionary(value: Dictionary) -> Array:
|
|
603
|
-
var type_tag: String = str(value.get("_type",
|
|
603
|
+
var type_tag: String = str(value.get("_type", ""))
|
|
604
604
|
if type_tag == "Resource":
|
|
605
605
|
return _parse_resource_tag(value)
|
|
606
606
|
|
|
607
607
|
# The shared builder is the same one the answer was written by, so every shape it writes comes
|
|
608
|
-
# back as itself.
|
|
609
|
-
|
|
610
|
-
var tagged: Dictionary = value
|
|
611
|
-
if not value.has("_type") and not type_tag.is_empty():
|
|
612
|
-
tagged = value.duplicate()
|
|
613
|
-
tagged["_type"] = type_tag
|
|
614
|
-
var rebuilt: Variant = _values.deserialize_value(tagged)
|
|
608
|
+
# back as itself.
|
|
609
|
+
var rebuilt: Variant = _values.deserialize_value(value)
|
|
615
610
|
if not rebuilt is Dictionary:
|
|
616
611
|
return [true, rebuilt]
|
|
617
612
|
|
|
@@ -660,7 +655,7 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
|
660
655
|
var wanted: Dictionary = {}
|
|
661
656
|
for key: Variant in value:
|
|
662
657
|
var property: String = str(key)
|
|
663
|
-
if property != "_type"
|
|
658
|
+
if property != "_type":
|
|
664
659
|
wanted[property] = value[key]
|
|
665
660
|
var refused: String = write_all(built, wanted)
|
|
666
661
|
if not refused.is_empty():
|
|
@@ -22,6 +22,9 @@ extends RefCounted
|
|
|
22
22
|
# number was expected. Absent keys never reach here: the callers pass their own default to
|
|
23
23
|
# `Dictionary.get`, so what arrives is that default.
|
|
24
24
|
|
|
25
|
+
## How many of a node's children a refusal for a path missing below it names.
|
|
26
|
+
const NAMED_CHILDREN: int = 12
|
|
27
|
+
|
|
25
28
|
|
|
26
29
|
static func as_int(value: Variant, fallback: int = 0) -> int:
|
|
27
30
|
if value is int:
|
|
@@ -114,3 +117,25 @@ static func from_the_root(said: String, node_path: String, root_name: String) ->
|
|
|
114
117
|
if not rest.is_empty():
|
|
115
118
|
how += ', so this one is "%s"' % rest
|
|
116
119
|
return "%s%s %s." % [said, "" if said.ends_with(".") else ".", how]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param said], the refusal for a path a scene does not have, with where the path leaves it: the
|
|
123
|
+
## last node of the path it has, [param reached] from the root, called [param root_name], when that
|
|
124
|
+
## is empty; the name below it that is missing; and [param children], the names that node has, as
|
|
125
|
+
## after a node was moved or renamed. Names up to [constant NAMED_CHILDREN] of them.
|
|
126
|
+
static func missing_below(
|
|
127
|
+
said: String, root_name: String, reached: PackedStringArray, missing: String, children: PackedStringArray
|
|
128
|
+
) -> String:
|
|
129
|
+
var shown: String = ", ".join(children.slice(0, NAMED_CHILDREN))
|
|
130
|
+
if children.size() > NAMED_CHILDREN:
|
|
131
|
+
shown += " and %d more" % (children.size() - NAMED_CHILDREN)
|
|
132
|
+
return (
|
|
133
|
+
"%s%s %s has no %s; %s."
|
|
134
|
+
% [
|
|
135
|
+
said,
|
|
136
|
+
"" if said.ends_with(".") else ".",
|
|
137
|
+
"The root, %s," % root_name if reached.is_empty() else "/".join(reached),
|
|
138
|
+
missing,
|
|
139
|
+
"its children are " + shown if not children.is_empty() else "it has no children",
|
|
140
|
+
]
|
|
141
|
+
)
|
|
@@ -664,8 +664,12 @@ func _serialize_array(value: Array) -> Array:
|
|
|
664
664
|
|
|
665
665
|
# Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
|
|
666
666
|
# enough that the list would be the whole answer, megabytes of it, and unreadable either way.
|
|
667
|
+
#
|
|
668
|
+
# The engine's encoder raises an error on an empty array, and an empty tile_map_data is every
|
|
669
|
+
# TileMapLayer with nothing painted yet, so a read of one carried an engine error in its answer.
|
|
667
670
|
func _serialize_bytes(value: PackedByteArray) -> Dictionary:
|
|
668
|
-
|
|
671
|
+
var text: String = "" if value.is_empty() else Marshalls.raw_to_base64(value)
|
|
672
|
+
return {"base64": text, "size": value.size(), "_type": "PackedByteArray"}
|
|
669
673
|
|
|
670
674
|
|
|
671
675
|
# The packed vector and colour arrays, each element serialised the way one of them on its own is.
|
package/build/keeper.js
CHANGED
|
@@ -1428,17 +1428,23 @@ function recordAt(path) {
|
|
|
1428
1428
|
return null;
|
|
1429
1429
|
}
|
|
1430
1430
|
const args = fields["arguments"];
|
|
1431
|
+
const startedBy = fields["startedBy"];
|
|
1432
|
+
const projectPath = fields["projectPath"];
|
|
1433
|
+
const command = fields["command"];
|
|
1434
|
+
if (typeof startedBy !== "number" || typeof projectPath !== "string" || projectPath === "" || typeof command !== "string" || !Array.isArray(args)) {
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1431
1437
|
return {
|
|
1432
1438
|
pid,
|
|
1433
1439
|
transcript,
|
|
1434
1440
|
startedAt,
|
|
1435
|
-
|
|
1436
|
-
projectPath
|
|
1441
|
+
startedBy,
|
|
1442
|
+
projectPath,
|
|
1437
1443
|
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
1438
1444
|
...typeof fields["keeper"] === "string" && fields["keeper"] !== "" ? { keeper: fields["keeper"] } : {},
|
|
1439
1445
|
...typeof fields["profile"] === "string" && fields["profile"] !== "" ? { profile: fields["profile"] } : {},
|
|
1440
|
-
arguments:
|
|
1441
|
-
|
|
1446
|
+
arguments: args.filter((value) => typeof value === "string"),
|
|
1447
|
+
command,
|
|
1442
1448
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
1443
1449
|
...typeof fields["exitSignal"] === "string" ? { exitSignal: fields["exitSignal"] } : {},
|
|
1444
1450
|
...typeof fields["endedAt"] === "number" ? { endedAt: fields["endedAt"] } : {}
|
package/package.json
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"main": "./build/index.js",
|
|
8
7
|
"bin": {
|
|
9
8
|
"gdharness": "build/cli.js"
|
|
10
9
|
},
|