gdharness 1.3.55 → 1.3.56

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.
@@ -89,7 +89,8 @@ var _unmade: Array[Dictionary] = []
89
89
  ##
90
90
  ## A write is refused while the editor holds unsaved changes to the scene: the file is reloaded
91
91
  ## into the editor after a write, which discards them without a prompt, and the editor saving its
92
- ## copy later would discard the write instead.
92
+ ## copy later would discard the write instead. One is refused too while the scene names a file
93
+ ## that is gone ([method missing_said]).
93
94
  func open(scene_path: String, for_writing: bool) -> Dictionary:
94
95
  path = scene_path
95
96
  if not FileAccess.file_exists(path):
@@ -98,6 +99,9 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
98
99
  var held_back: String = unsaved_in_editor(path)
99
100
  if not held_back.is_empty():
100
101
  return { "ok": false, "error": held_back }
102
+ var missing: String = missing_said(path, FileAccess.get_file_as_string(path))
103
+ if not missing.is_empty():
104
+ return { "ok": false, "error": missing }
101
105
  var packed: PackedScene = (
102
106
  ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene
103
107
  )
@@ -125,6 +129,92 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
125
129
  return {}
126
130
 
127
131
 
132
+ ## Why a write to the scene at [param scene_path], whose text is [param text], is refused, or "":
133
+ ## an [ext_resource] naming no file the project has, by its uid or by its path. The engine loads the
134
+ ## scene without it, so a save drops the entry and every value set from it, and the answer could
135
+ ## only call those values empty.
136
+ static func missing_said(scene_path: String, text: String) -> String:
137
+ var gone: Dictionary = {}
138
+ var entry: RegEx = RegEx.create_from_string("(?m)^\\[ext_resource\\b[^\\n]*\\]")
139
+ var field: RegEx = RegEx.create_from_string('\\b(path|uid|id)="([^"]*)"')
140
+ for found: RegExMatch in entry.search_all(text):
141
+ var fields: Dictionary = {}
142
+ for one: RegExMatch in field.search_all(found.get_string()):
143
+ fields[one.get_string(1)] = one.get_string(2)
144
+ var uid: String = str(fields.get("uid", ""))
145
+ var by_uid: String = ""
146
+ if uid.begins_with("uid://") and ResourceUID.has_id(ResourceUID.text_to_id(uid)):
147
+ by_uid = ResourceUID.get_id_path(ResourceUID.text_to_id(uid))
148
+ var named: String = str(fields.get("path", ""))
149
+ if FileAccess.file_exists(by_uid) or FileAccess.file_exists(named):
150
+ continue
151
+ gone[str(fields.get("id", ""))] = named if not named.is_empty() else uid
152
+ if gone.is_empty():
153
+ return ""
154
+ var uses: PackedStringArray = []
155
+ var on: String = ""
156
+ var lines: PackedStringArray = text.split("\n")
157
+ var header: RegEx = RegEx.create_from_string('^\\[node name="([^"]*)"')
158
+ var parented: RegEx = RegEx.create_from_string('\\bparent="([^"]*)"')
159
+ for index: int in lines.size():
160
+ var line: String = lines[index]
161
+ if line.begins_with("["):
162
+ var node: RegExMatch = header.search(line)
163
+ var under: RegExMatch = parented.search(line)
164
+ if node == null:
165
+ on = line.get_slice(" ", 0).trim_prefix("[")
166
+ elif under == null or under.get_string(1) == ".":
167
+ on = node.get_string(1)
168
+ else:
169
+ on = under.get_string(1).path_join(node.get_string(1))
170
+ continue
171
+ for id: String in gone:
172
+ if line.contains('ExtResource("%s")' % id):
173
+ var said: String = "line %d on %s, %s" % [index + 1, on, line.strip_edges()]
174
+ var _used: bool = uses.append(said)
175
+ break
176
+ var paths: PackedStringArray = []
177
+ for id: String in gone:
178
+ var _named: bool = paths.append(str(gone[id]))
179
+ var one_file: bool = paths.size() == 1
180
+ return (
181
+ (
182
+ "Nothing was written: %s names %s, which %s not there, so the engine loads the scene "
183
+ + "without %s, and a save would drop %s%s. Put the %s back at %s, or change the "
184
+ + "[ext_resource] %s in the scene to %s that exists."
185
+ )
186
+ % [
187
+ scene_path,
188
+ ", ".join(paths),
189
+ "is" if one_file else "are",
190
+ "it" if one_file else "them",
191
+ "it" if one_file else "them",
192
+ "" if uses.is_empty() else " and what is set from %s: %s" % [
193
+ "it" if one_file else "them", "; ".join(uses),
194
+ ],
195
+ "file" if one_file else "files",
196
+ "that path" if one_file else "those paths",
197
+ "line" if one_file else "lines",
198
+ "one" if one_file else "ones",
199
+ ]
200
+ )
201
+
202
+
203
+ ## The connections the file holds, as it was opened, by their paths from [member root].
204
+ func held_connections() -> Array[Dictionary]:
205
+ var held: Array[Dictionary] = []
206
+ for index: int in _state.get_connection_count():
207
+ held.append(
208
+ {
209
+ "sourceNodePath": str(_state.get_connection_source(index)),
210
+ "signalName": str(_state.get_connection_signal(index)),
211
+ "targetNodePath": str(_state.get_connection_target(index)),
212
+ "methodName": str(_state.get_connection_method(index)),
213
+ }
214
+ )
215
+ return held
216
+
217
+
128
218
  ## The connections in [param state] the engine did not make on [member root]: Object.connect
129
219
  ## refuses a callable that binds or unbinds arguments unless its target has the method, so the scene
130
220
  ## loads without them, in the editor as well, and every save drops them.
@@ -392,6 +392,7 @@ func delete_node(args: Dictionary) -> Dictionary:
392
392
  for under: Node in node.find_children("*", "", true, false):
393
393
  if under.owner == scene.root:
394
394
  went.append(scene.path_of(under))
395
+ var cut: Array[Dictionary] = _connections_with(scene, at)
395
396
  node.get_parent().remove_child(node)
396
397
  node.free()
397
398
  var carried: GdhPaths = GdhPaths.new()
@@ -409,10 +410,25 @@ func delete_node(args: Dictionary) -> Dictionary:
409
410
  var answer: Dictionary = { "ok": true, "deletedNodePath": at }
410
411
  if not went.is_empty():
411
412
  answer["deletedWithIt"] = went
413
+ if not cut.is_empty():
414
+ answer["connectionsRemoved"] = cut
412
415
  answer.merge(GdhKin.follow(scene_path, at, "", carried))
413
416
  return answer
414
417
 
415
418
 
419
+ ## The connections [param scene]'s file holds from or to [param at] or a node under it, which go
420
+ ## with the node; one an instanced scene makes inside itself is that scene's, and stays there.
421
+ static func _connections_with(scene: GdhSceneFile, at: String) -> Array[Dictionary]:
422
+ var cut: Array[Dictionary] = []
423
+ for held: Dictionary in scene.held_connections():
424
+ var ends: Array[String] = [str(held["sourceNodePath"]), str(held["targetNodePath"])]
425
+ for end: String in ends:
426
+ if end == at or end.begins_with(at + "/"):
427
+ cut.append(held)
428
+ break
429
+ return cut
430
+
431
+
416
432
  func duplicate_node(args: Dictionary) -> Dictionary:
417
433
  var project_path: String = str(args.get("projectPath", ""))
418
434
  var scene_path: String = GdhSceneFile.res_path(project_path, str(args.get("scenePath", "")))
@@ -405,7 +405,9 @@ func _add_inherited(answer: Dictionary, seen: Array[String]) -> void:
405
405
  if above.is_empty():
406
406
  break
407
407
  inherits_from.append(base_path)
408
- for list_name: String in ["signals", "variables", "functions", "constants", "enums"]:
408
+ for list_name: String in [
409
+ "signals", "variables", "functions", "constants", "enums", "inner_classes",
410
+ ]:
409
411
  var mine: Array[Dictionary] = answer[list_name]
410
412
  var theirs: Array[Dictionary] = above[list_name]
411
413
  for member: Dictionary in theirs:
package/build/keeper.js CHANGED
@@ -6946,7 +6946,7 @@ var require_public_api = __commonJS(function(exports) {
6946
6946
  });
6947
6947
 
6948
6948
  // src/keeper.ts
6949
- import { closeSync as closeSync3, existsSync as existsSync4, mkdtempSync as mkdtempSync4, openSync as openSync4, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync4 } from "node:fs";
6949
+ import { closeSync as closeSync3, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync4, readFileSync as readFileSync9, rmSync as rmSync7, writeFileSync as writeFileSync4 } from "node:fs";
6950
6950
  import { tmpdir as tmpdir8 } from "node:os";
6951
6951
  import { join as join10 } from "node:path";
6952
6952
  import { setTimeout as delay3 } from "node:timers/promises";
@@ -7435,7 +7435,6 @@ function throughHelper(how, command, args, files, assembly = helperAssembly()) {
7435
7435
  // src/engine-slots.ts
7436
7436
  import {
7437
7437
  closeSync as closeSync2,
7438
- existsSync as existsSync2,
7439
7438
  mkdirSync,
7440
7439
  openSync as openSync2,
7441
7440
  readdirSync,
@@ -8834,11 +8833,16 @@ function holderOf(content) {
8834
8833
  if (typeof parsed !== "object" || parsed === null) {
8835
8834
  return null;
8836
8835
  }
8837
- const { pid, started, by } = parsed;
8836
+ const { pid, started, by, game } = parsed;
8838
8837
  if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof started !== "number") {
8839
8838
  return null;
8840
8839
  }
8841
- return typeof by === "string" && by !== "" ? { pid, started, by } : { pid, started };
8840
+ return {
8841
+ pid,
8842
+ started,
8843
+ ...typeof by === "string" && by !== "" ? { by } : {},
8844
+ ...game === true ? { game } : {}
8845
+ };
8842
8846
  } catch {
8843
8847
  return null;
8844
8848
  }
@@ -8854,7 +8858,8 @@ function handSlot(lock, from, to) {
8854
8858
  } catch {
8855
8859
  return false;
8856
8860
  }
8857
- return content !== null && holderOf(content)?.pid === from && writeWhole(lock, JSON.stringify(to));
8861
+ const holder = content === null ? null : holderOf(content);
8862
+ return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
8858
8863
  }
8859
8864
  function giveBackSlot(lock, pid) {
8860
8865
  try {
@@ -9314,7 +9319,7 @@ var IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
9314
9319
  var hashes = new Map;
9315
9320
 
9316
9321
  // src/scratch.ts
9317
- import { existsSync as existsSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
9322
+ import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
9318
9323
  import { tmpdir as tmpdir5 } from "node:os";
9319
9324
  import { join as join4 } from "node:path";
9320
9325
  import process6 from "node:process";
@@ -9359,7 +9364,7 @@ function tryAgain(path, attempt) {
9359
9364
  lastTryOnExit();
9360
9365
  }
9361
9366
  const after = AGAIN_AFTER_MS[attempt];
9362
- if (after === undefined || !existsSync3(path)) {
9367
+ if (after === undefined || !existsSync2(path)) {
9363
9368
  waiting.delete(path);
9364
9369
  if (after === undefined) {
9365
9370
  atExit.add(path);
@@ -10864,7 +10869,7 @@ async function startThroughHelper(spec, output, stopFile) {
10864
10869
  exited.now = true;
10865
10870
  });
10866
10871
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
10867
- const pid = existsSync4(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
10872
+ const pid = existsSync3(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
10868
10873
  if (Number.isInteger(pid) && pid > 0) {
10869
10874
  return { child: started.child, pid };
10870
10875
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.55",
3
+ "version": "1.3.56",
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",