gdharness 1.3.69 → 1.3.71
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 +2 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +16 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +77 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +36 -1
- package/build/godot/operations/property_values.gd +16 -1
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -20498,7 +20498,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
20498
20498
|
};
|
|
20499
20499
|
PROPERTIES = {
|
|
20500
20500
|
type: "object",
|
|
20501
|
-
description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. One carrying a field its type has not, such as a z for a Vector2, is refused naming it. A colour may also be given as its html code, "#ff8000" or "#ff800080". A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y". A component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". A property holding a Resource takes the res:// path of a file, which a read answers with its class and, when its script declares one, scriptClass, or a resource built into the scene as {"_type": "CircleShape2D", "radius": 8}, its class (an engine class or a script's class_name) beside its properties. A typed list or map of resources or nodes, such as Array[Item], takes each in the same forms, a node by its path. A read answers a resource built into the scene in that form, with what it holds away from the defaults, so it too can be written to another node. script takes the res:// path of a script to attach, one with no class_name included, and is attached before the rest are set, so the properties it declares can be given beside it. Each value is read back once set, and one the engine changed or refused is refused with what it holds. An enum property takes its number or the name of a value: as the script spells it, as the inspector shows it, or for an engine enum as its constant, so Add or BLEND_MODE_ADD, and Bottom or VERTICAL_ALIGNMENT_BOTTOM for a global one. A value past an export range, or a number no value of an enum has, is written as given and named under outsideDeclared. A key may reach into a resource built into this one: the property holding it, a slash, then one of its own, so a TileSet's first atlas source takes "sources/0/0:0/0/probability". A list of such resources is stepped into by an element's index, so "lines/1/text" is the text of the second resource in lines and the others are left as they were. A resource kept in a file of its own, or built into the scene an instance comes from, is written in that file, or replaced here by one of this file's own. The next element of a list is made by writing to it, as the inspector's Add Element makes it: physics_layer_0/collision_layer on a TileSet with no physics layer makes its first, and "sources/0/0:0/0/physics_layer_0/polygon_0/points" a tile's first collision polygon on that layer. ${WHOLE_NUMBERS}`,
|
|
20501
|
+
description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. One carrying a field its type has not, such as a z for a Vector2, is refused naming it. A colour may also be given as its html code, "#ff8000" or "#ff800080". A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y". A component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". A property holding a Resource takes the res:// path of a file, which a read answers with its class and, when its script declares one, scriptClass, or a resource built into the scene as {"_type": "CircleShape2D", "radius": 8}, its class (an engine class or a script's class_name) beside its properties. A typed list or map of resources or nodes, such as Array[Item], takes each in the same forms, a node by its path. A read answers a resource built into the scene in that form, with what it holds away from the defaults, so it too can be written to another node. script takes the res:// path of a script to attach, one with no class_name included, and is attached before the rest are set, so the properties it declares can be given beside it. Each value is read back once set, and one the engine changed or refused is refused with what it holds. An enum property takes its number or the name of a value: as the script spells it, as the inspector shows it, or for an engine enum as its constant, so Add or BLEND_MODE_ADD, and Bottom or VERTICAL_ALIGNMENT_BOTTOM for a global one. A value past an export range, or a number no value of an enum has, is written as given and named under outsideDeclared. A key may reach into a resource built into this one: the property holding it, a slash, then one of its own, so a TileSet's first atlas source takes "sources/0/0:0/0/probability". A list of such resources is stepped into by an element's index, so "lines/1/text" is the text of the second resource in lines and the others are left as they were. A resource kept in a file of its own, or built into the scene an instance comes from, is written in that file, or replaced here by one of this file's own. The next element of a list is made by writing to it, as the inspector's Add Element makes it: physics_layer_0/collision_layer on a TileSet with no physics layer makes its first, and "sources/0/0:0/0/physics_layer_0/polygon_0/points" a tile's first collision polygon on that layer. null for a "metadata/" key takes that entry off, as the inspector's delete button does, and the answer names it under metadataRemoved. ${WHOLE_NUMBERS}`,
|
|
20502
20502
|
additionalProperties: true
|
|
20503
20503
|
};
|
|
20504
20504
|
XY = {
|
|
@@ -21259,7 +21259,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
21259
21259
|
placeholder: {
|
|
21260
21260
|
type: "boolean",
|
|
21261
21261
|
ops: ["add", "set"],
|
|
21262
|
-
description: "add, with instancePath, or set, on an instance this scene places: whether it is saved as the editor's Load as Placeholder saves it, as `instance_placeholder` in the file. A game then gets an InstancePlaceholder there and loads the scene itself, while the editor still shows it. The answer says placeholder as the file now holds it."
|
|
21262
|
+
description: "add, with instancePath, or set, on an instance this scene places: whether it is saved as the editor's Load as Placeholder saves it, as `instance_placeholder` in the file. A game then gets an InstancePlaceholder there and loads the scene itself, while the editor still shows it. A placeholder loads without its children, so it never has editableChildren too. Passing editableChildren false with placeholder true drops the children's saved values. The answer says placeholder as the file now holds it."
|
|
21263
21263
|
},
|
|
21264
21264
|
addGroups: {
|
|
21265
21265
|
type: "array",
|
|
@@ -69,6 +69,9 @@ var _outside_declared: Dictionary = {}
|
|
|
69
69
|
# The node paths the last write_all wrote that name no node from the node written, by property, as
|
|
70
70
|
# a sentence saying so.
|
|
71
71
|
var _names_no_node: Dictionary = {}
|
|
72
|
+
## The metadata entries the last write_all took off, by the property names given. Godot reads a null
|
|
73
|
+
## set on a metadata entry as its removal, as the inspector's delete button does.
|
|
74
|
+
var metadata_removed: Array[String] = []
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
## Turns what arrived over the wire into the Godot value a property wants.
|
|
@@ -204,6 +207,7 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
204
207
|
_names_no_node = {}
|
|
205
208
|
moved_by_engine = {}
|
|
206
209
|
unheld = ""
|
|
210
|
+
metadata_removed = []
|
|
207
211
|
var named: Dictionary = {}
|
|
208
212
|
for key: Variant in properties:
|
|
209
213
|
named[str(key).get_slice(":", 0)] = true
|
|
@@ -238,6 +242,15 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
238
242
|
var place: Array = GdhPropertyPaths.holder_of(target, property, home)
|
|
239
243
|
var holder: Object = place[0]
|
|
240
244
|
var name: String = place[1]
|
|
245
|
+
var entry: String = name.trim_prefix("metadata/")
|
|
246
|
+
if (
|
|
247
|
+
properties[key] == null
|
|
248
|
+
and name.begins_with("metadata/")
|
|
249
|
+
and holder.has_meta(entry)
|
|
250
|
+
):
|
|
251
|
+
holder.remove_meta(entry)
|
|
252
|
+
metadata_removed.append(property)
|
|
253
|
+
continue
|
|
241
254
|
var declared: Dictionary = declaration(holder, name)
|
|
242
255
|
if declared.is_empty():
|
|
243
256
|
var elsewhere: String = GdhPropertyPaths.held_elsewhere(holder, name, home)
|
|
@@ -329,7 +342,7 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
329
342
|
)
|
|
330
343
|
for property: String in before:
|
|
331
344
|
var now: Variant = target.get(property)
|
|
332
|
-
if not GdhSame.same(now, before[property]):
|
|
345
|
+
if not metadata_removed.has(property) and not GdhSame.same(now, before[property]):
|
|
333
346
|
_also_changed[property] = [before[property], now]
|
|
334
347
|
if target is Node:
|
|
335
348
|
_names_no_node = GdhDeclared.naming_nothing(target as Node, given)
|
|
@@ -384,6 +397,8 @@ func _laid_out(control: Control, name: String) -> String:
|
|
|
384
397
|
## as a scene answer names one. Called before the scene is written, since the write frees the nodes
|
|
385
398
|
## these values can hold.
|
|
386
399
|
func note_write(answer: Dictionary, relative_to: Node = null) -> void:
|
|
400
|
+
if not metadata_removed.is_empty():
|
|
401
|
+
answer["metadataRemoved"] = metadata_removed.duplicate()
|
|
387
402
|
if not _outside_declared.is_empty():
|
|
388
403
|
answer["outsideDeclared"] = _outside_declared.duplicate()
|
|
389
404
|
if not _names_no_node.is_empty():
|
|
@@ -79,6 +79,10 @@ var _copied_from: Dictionary = {}
|
|
|
79
79
|
var _reached: Dictionary = {}
|
|
80
80
|
## The file as it was opened, to compare its embedded resources with what the write saves.
|
|
81
81
|
var _text: String = ""
|
|
82
|
+
## The unique_id each placeholder put back as its instance had in the file, by the instance's object
|
|
83
|
+
## id. Scripts cannot set one, the engine gives the instance a new one as it packs it, and a scene
|
|
84
|
+
## inheriting this one finds a moved node by it.
|
|
85
|
+
var _held_ids: Dictionary = {}
|
|
82
86
|
## The engine properties a put back did not take on a node, as "id:name": the engine derives them
|
|
83
87
|
## from where the node is, as it keeps layout_mode at Container under a container, and stores the
|
|
84
88
|
## same from an editor.
|
|
@@ -155,6 +159,8 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
155
159
|
func _unhold_placeholders() -> void:
|
|
156
160
|
var held: Array[InstancePlaceholder] = []
|
|
157
161
|
_placeholders_under(root, held)
|
|
162
|
+
var as_text: bool = path.ends_with(".tscn") and not held.is_empty()
|
|
163
|
+
var text: String = FileAccess.get_file_as_string(path) if as_text else ""
|
|
158
164
|
for one: InstancePlaceholder in held:
|
|
159
165
|
var packed: PackedScene = (
|
|
160
166
|
ResourceLoader.load(one.get_instance_path(), "PackedScene") as PackedScene
|
|
@@ -168,15 +174,78 @@ func _unhold_placeholders() -> void:
|
|
|
168
174
|
var values: Dictionary = one.get_stored_values()
|
|
169
175
|
for key: Variant in values:
|
|
170
176
|
instance.set(str(key), values[key])
|
|
177
|
+
var filed_id: String = _filed_id(text, _header_of(root.get_path_to(one)))
|
|
171
178
|
# replace_by moves the place and the groups.
|
|
172
179
|
one.replace_by(instance, true)
|
|
173
180
|
instance.owner = root
|
|
174
181
|
instance.scene_file_path = one.get_instance_path()
|
|
175
182
|
instance.set_scene_instance_load_placeholder(true)
|
|
183
|
+
if not filed_id.is_empty():
|
|
184
|
+
_held_ids[instance.get_instance_id()] = filed_id
|
|
176
185
|
_repointed(root, one, instance)
|
|
177
186
|
one.free()
|
|
178
187
|
|
|
179
188
|
|
|
189
|
+
## How a text scene opens the entry of the node at [param at], a path from the root.
|
|
190
|
+
static func _header_of(at: NodePath) -> String:
|
|
191
|
+
var named: String = str(at.get_name(at.get_name_count() - 1))
|
|
192
|
+
var parent: String = "." if at.get_name_count() == 1 else str(at).get_base_dir()
|
|
193
|
+
return '[node name="%s" parent="%s"' % [named.c_escape(), parent.c_escape()]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
## The unique_id [param text] gives the node whose entry opens with [param header], or "".
|
|
197
|
+
static func _filed_id(text: String, header: String) -> String:
|
|
198
|
+
var id: RegEx = RegEx.create_from_string(" unique_id=(\\d+)")
|
|
199
|
+
for line: String in text.split("\n"):
|
|
200
|
+
if line.begins_with(header + " ") or line.begins_with(header + "]"):
|
|
201
|
+
var found: RegExMatch = id.search(line)
|
|
202
|
+
return "" if found == null else found.get_string(1)
|
|
203
|
+
return ""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## The unique_id to write back on each placeholder put back as its instance, by its entry's header,
|
|
207
|
+
## for those still in the scene.
|
|
208
|
+
func _ids_to_keep() -> Dictionary:
|
|
209
|
+
var keep: Dictionary = {}
|
|
210
|
+
for key: Variant in _held_ids:
|
|
211
|
+
var node: Node = instance_from_id(GdhRead.as_int(key)) as Node
|
|
212
|
+
if node != null and root.is_ancestor_of(node):
|
|
213
|
+
keep[_header_of(root.get_path_to(node))] = _held_ids[key]
|
|
214
|
+
return keep
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
## Saves [param packed] at [param file_path] as [method save_packed] does, with the unique_ids in
|
|
218
|
+
## [param keep].
|
|
219
|
+
static func _saved_keeping_ids(packed: PackedScene, file_path: String, keep: Dictionary) -> Error:
|
|
220
|
+
var saving: Error = GdhSceneUids.save_keeping_uid(packed, file_path)
|
|
221
|
+
return saving if saving != OK else _keep_ids(file_path, keep)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## Writes back on the text scene at [param file_path] the unique_id of each entry in [param keep],
|
|
225
|
+
## by its header.
|
|
226
|
+
static func _keep_ids(file_path: String, keep: Dictionary) -> Error:
|
|
227
|
+
if keep.is_empty():
|
|
228
|
+
return OK
|
|
229
|
+
var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
|
|
230
|
+
var id: RegEx = RegEx.create_from_string(" unique_id=\\d+")
|
|
231
|
+
var changed: bool = false
|
|
232
|
+
for index: int in lines.size():
|
|
233
|
+
for header: Variant in keep:
|
|
234
|
+
var opening: String = str(header)
|
|
235
|
+
if lines[index].begins_with(opening + " ") or lines[index].begins_with(opening + "]"):
|
|
236
|
+
lines[index] = id.sub(lines[index], " unique_id=" + str(keep[header]))
|
|
237
|
+
changed = true
|
|
238
|
+
if not changed:
|
|
239
|
+
return OK
|
|
240
|
+
var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
|
|
241
|
+
if file == null:
|
|
242
|
+
return FileAccess.get_open_error()
|
|
243
|
+
if not file.store_string("\n".join(lines)):
|
|
244
|
+
return ERR_FILE_CANT_WRITE
|
|
245
|
+
file.close()
|
|
246
|
+
return OK
|
|
247
|
+
|
|
248
|
+
|
|
180
249
|
## Every InstancePlaceholder below [param top] that this scene places, into [param into].
|
|
181
250
|
func _placeholders_under(top: Node, into: Array[InstancePlaceholder]) -> void:
|
|
182
251
|
for child: Node in top.get_children():
|
|
@@ -1077,6 +1146,7 @@ func _write(check: Callable) -> Dictionary:
|
|
|
1077
1146
|
if not missing.is_empty():
|
|
1078
1147
|
return refuse("Nothing was written: " + missing)
|
|
1079
1148
|
var also: Array[Dictionary] = _also_saved(packed.get_state())
|
|
1149
|
+
var keep: Dictionary = _ids_to_keep() if headless and path.ends_with(".tscn") else {}
|
|
1080
1150
|
close()
|
|
1081
1151
|
var previous: Variant = null
|
|
1082
1152
|
if FileAccess.file_exists(path):
|
|
@@ -1088,12 +1158,18 @@ func _write(check: Callable) -> Dictionary:
|
|
|
1088
1158
|
values.append_array(_stored_at(state, index).values())
|
|
1089
1159
|
var carried: Dictionary = GdhHeadlessCheck.embedded_in(values, {})
|
|
1090
1160
|
var written: Dictionary = save_packed(packed, path)
|
|
1161
|
+
if written.is_empty():
|
|
1162
|
+
var ids_kept: Error = _keep_ids(path, keep)
|
|
1163
|
+
if ids_kept != OK:
|
|
1164
|
+
written = {
|
|
1165
|
+
"ok": false, "error": "Failed to save %s: %s" % [path, error_string(ids_kept)],
|
|
1166
|
+
}
|
|
1091
1167
|
if not written.is_empty() or not headless or not path.ends_with(".tscn"):
|
|
1092
1168
|
if written.is_empty():
|
|
1093
1169
|
also_saved.append_array(also)
|
|
1094
1170
|
return written
|
|
1095
1171
|
var just_saved: GdhHeadlessCheck.GdhSaved = GdhHeadlessCheck.GdhSaved.new(
|
|
1096
|
-
path, _text, func() -> Error: return
|
|
1172
|
+
path, _text, func() -> Error: return _saved_keeping_ids(packed, path, keep)
|
|
1097
1173
|
)
|
|
1098
1174
|
var unkept: String = GdhHeadlessCheck.settle_saved(
|
|
1099
1175
|
just_saved,
|
|
@@ -338,7 +338,10 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
338
338
|
)
|
|
339
339
|
if not also.is_empty():
|
|
340
340
|
answer["alsoSaved"] = also
|
|
341
|
-
|
|
341
|
+
var kept: Array = properties.keys().filter(
|
|
342
|
+
func(key: Variant) -> bool: return not _properties.metadata_removed.has(str(key))
|
|
343
|
+
)
|
|
344
|
+
answer["properties"] = _held(resource, kept)
|
|
342
345
|
return answer
|
|
343
346
|
|
|
344
347
|
|
|
@@ -1181,6 +1181,39 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1181
1181
|
if not foreign.is_empty():
|
|
1182
1182
|
return scene.refuse(foreign)
|
|
1183
1183
|
|
|
1184
|
+
# A placeholder loads without its children, so the editor never keeps both settings on.
|
|
1185
|
+
var wants_placeholder: bool = placeholder != null and GdhRead.as_bool(placeholder)
|
|
1186
|
+
var wants_editable: bool = editable != null and GdhRead.as_bool(editable)
|
|
1187
|
+
if wants_placeholder and wants_editable:
|
|
1188
|
+
return scene.refuse(
|
|
1189
|
+
(
|
|
1190
|
+
"placeholder and editableChildren cannot both be true, "
|
|
1191
|
+
+ "since a placeholder loads without its children."
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
# The engine asks the root whether it is an editable instance of itself only with an error.
|
|
1195
|
+
var children_open: bool = node != scene.root and scene.root.is_editable_instance(node)
|
|
1196
|
+
if wants_placeholder and editable == null and children_open:
|
|
1197
|
+
return scene.refuse(
|
|
1198
|
+
(
|
|
1199
|
+
(
|
|
1200
|
+
"%s has editable children, which a placeholder cannot keep. Pass "
|
|
1201
|
+
+ "editableChildren false with placeholder true to drop their saved values."
|
|
1202
|
+
)
|
|
1203
|
+
% scene.path_of(node)
|
|
1204
|
+
)
|
|
1205
|
+
)
|
|
1206
|
+
if wants_editable and placeholder == null and node.get_scene_instance_load_placeholder():
|
|
1207
|
+
return scene.refuse(
|
|
1208
|
+
(
|
|
1209
|
+
(
|
|
1210
|
+
"%s is a placeholder, which loads without its children. "
|
|
1211
|
+
+ "Pass placeholder false with editableChildren true."
|
|
1212
|
+
)
|
|
1213
|
+
% scene.path_of(node)
|
|
1214
|
+
)
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1184
1217
|
var dropped: Dictionary = {}
|
|
1185
1218
|
if editable != null:
|
|
1186
1219
|
var not_an_instance: String = GdhKin.set_editable(scene, node, GdhRead.as_bool(editable))
|
|
@@ -1214,8 +1247,10 @@ func set_node_properties(args: Dictionary) -> Dictionary:
|
|
|
1214
1247
|
return scene.refuse(ungrouped)
|
|
1215
1248
|
|
|
1216
1249
|
var held: Dictionary = {}
|
|
1250
|
+
var removed: Array[String] = _properties.metadata_removed.duplicate()
|
|
1217
1251
|
for key: Variant in properties:
|
|
1218
|
-
|
|
1252
|
+
if not removed.has(str(key)):
|
|
1253
|
+
held[str(key)] = _readable(node, _properties.read_at(node, str(key)))
|
|
1219
1254
|
var new_path: String = scene.path_of(node)
|
|
1220
1255
|
var answer: Dictionary = { "ok": true, "nodePath": new_path, "properties": held }
|
|
1221
1256
|
_properties.note_write(answer, node)
|
|
@@ -69,6 +69,9 @@ var _outside_declared: Dictionary = {}
|
|
|
69
69
|
# The node paths the last write_all wrote that name no node from the node written, by property, as
|
|
70
70
|
# a sentence saying so.
|
|
71
71
|
var _names_no_node: Dictionary = {}
|
|
72
|
+
## The metadata entries the last write_all took off, by the property names given. Godot reads a null
|
|
73
|
+
## set on a metadata entry as its removal, as the inspector's delete button does.
|
|
74
|
+
var metadata_removed: Array[String] = []
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
## Turns what arrived over the wire into the Godot value a property wants.
|
|
@@ -204,6 +207,7 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
204
207
|
_names_no_node = {}
|
|
205
208
|
moved_by_engine = {}
|
|
206
209
|
unheld = ""
|
|
210
|
+
metadata_removed = []
|
|
207
211
|
var named: Dictionary = {}
|
|
208
212
|
for key: Variant in properties:
|
|
209
213
|
named[str(key).get_slice(":", 0)] = true
|
|
@@ -238,6 +242,15 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
238
242
|
var place: Array = GdhPropertyPaths.holder_of(target, property, home)
|
|
239
243
|
var holder: Object = place[0]
|
|
240
244
|
var name: String = place[1]
|
|
245
|
+
var entry: String = name.trim_prefix("metadata/")
|
|
246
|
+
if (
|
|
247
|
+
properties[key] == null
|
|
248
|
+
and name.begins_with("metadata/")
|
|
249
|
+
and holder.has_meta(entry)
|
|
250
|
+
):
|
|
251
|
+
holder.remove_meta(entry)
|
|
252
|
+
metadata_removed.append(property)
|
|
253
|
+
continue
|
|
241
254
|
var declared: Dictionary = declaration(holder, name)
|
|
242
255
|
if declared.is_empty():
|
|
243
256
|
var elsewhere: String = GdhPropertyPaths.held_elsewhere(holder, name, home)
|
|
@@ -329,7 +342,7 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
329
342
|
)
|
|
330
343
|
for property: String in before:
|
|
331
344
|
var now: Variant = target.get(property)
|
|
332
|
-
if not GdhSame.same(now, before[property]):
|
|
345
|
+
if not metadata_removed.has(property) and not GdhSame.same(now, before[property]):
|
|
333
346
|
_also_changed[property] = [before[property], now]
|
|
334
347
|
if target is Node:
|
|
335
348
|
_names_no_node = GdhDeclared.naming_nothing(target as Node, given)
|
|
@@ -384,6 +397,8 @@ func _laid_out(control: Control, name: String) -> String:
|
|
|
384
397
|
## as a scene answer names one. Called before the scene is written, since the write frees the nodes
|
|
385
398
|
## these values can hold.
|
|
386
399
|
func note_write(answer: Dictionary, relative_to: Node = null) -> void:
|
|
400
|
+
if not metadata_removed.is_empty():
|
|
401
|
+
answer["metadataRemoved"] = metadata_removed.duplicate()
|
|
387
402
|
if not _outside_declared.is_empty():
|
|
388
403
|
answer["outsideDeclared"] = _outside_declared.duplicate()
|
|
389
404
|
if not _names_no_node.is_empty():
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.71",
|
|
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",
|