gdharness 1.3.69 → 1.3.70

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 CHANGED
@@ -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",
@@ -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 GdhSceneUids.save_keeping_uid(packed, path)
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,
@@ -1181,6 +1181,37 @@ 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
+ if wants_placeholder and editable == null and scene.root.is_editable_instance(node):
1195
+ return scene.refuse(
1196
+ (
1197
+ (
1198
+ "%s has editable children, which a placeholder cannot keep. Pass "
1199
+ + "editableChildren false with placeholder true to drop their saved values."
1200
+ )
1201
+ % scene.path_of(node)
1202
+ )
1203
+ )
1204
+ if wants_editable and placeholder == null and node.get_scene_instance_load_placeholder():
1205
+ return scene.refuse(
1206
+ (
1207
+ (
1208
+ "%s is a placeholder, which loads without its children. "
1209
+ + "Pass placeholder false with editableChildren true."
1210
+ )
1211
+ % scene.path_of(node)
1212
+ )
1213
+ )
1214
+
1184
1215
  var dropped: Dictionary = {}
1185
1216
  if editable != null:
1186
1217
  var not_an_instance: String = GdhKin.set_editable(scene, node, GdhRead.as_bool(editable))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.69",
3
+ "version": "1.3.70",
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",