gdharness 1.3.55 → 1.3.57

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,15 +8833,29 @@ function holderOf(content) {
8834
8833
  if (typeof parsed !== "object" || parsed === null) {
8835
8834
  return null;
8836
8835
  }
8837
- const { pid, started, by } = parsed;
8838
- if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof started !== "number") {
8836
+ const { by, game, engine } = parsed;
8837
+ const named = namedProcess(parsed);
8838
+ if (named === null) {
8839
8839
  return null;
8840
8840
  }
8841
- return typeof by === "string" && by !== "" ? { pid, started, by } : { pid, started };
8841
+ const runs = namedProcess(engine);
8842
+ return {
8843
+ ...named,
8844
+ ...typeof by === "string" && by !== "" ? { by } : {},
8845
+ ...game === true ? { game } : {},
8846
+ ...runs === null ? {} : { engine: runs }
8847
+ };
8842
8848
  } catch {
8843
8849
  return null;
8844
8850
  }
8845
8851
  }
8852
+ function namedProcess(value) {
8853
+ if (typeof value !== "object" || value === null) {
8854
+ return null;
8855
+ }
8856
+ const { pid, started } = value;
8857
+ return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && typeof started === "number" ? { pid, started } : null;
8858
+ }
8846
8859
  async function ownStart() {
8847
8860
  const said = (await startTimesOf([process4.pid])).get(process4.pid);
8848
8861
  return said ?? Math.round(Date.now() - process4.uptime() * 1000);
@@ -8854,7 +8867,18 @@ function handSlot(lock, from, to) {
8854
8867
  } catch {
8855
8868
  return false;
8856
8869
  }
8857
- return content !== null && holderOf(content)?.pid === from && writeWhole(lock, JSON.stringify(to));
8870
+ const holder = content === null ? null : holderOf(content);
8871
+ return holder?.pid === from && writeWhole(lock, JSON.stringify(holder.game === true ? { ...to, game: true } : to));
8872
+ }
8873
+ function nameSlotEngine(lock, holder, engine) {
8874
+ let content;
8875
+ try {
8876
+ content = readWhole(lock);
8877
+ } catch {
8878
+ return false;
8879
+ }
8880
+ const named = content === null ? null : holderOf(content);
8881
+ return named?.pid === holder && writeWhole(lock, JSON.stringify({ ...named, engine }));
8858
8882
  }
8859
8883
  function giveBackSlot(lock, pid) {
8860
8884
  try {
@@ -9314,7 +9338,7 @@ var IMPORTABLE = new Set(IMPORTABLE_EXTENSIONS);
9314
9338
  var hashes = new Map;
9315
9339
 
9316
9340
  // src/scratch.ts
9317
- import { existsSync as existsSync3, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
9341
+ import { existsSync as existsSync2, mkdtempSync as mkdtempSync2, readdirSync as readdirSync2, rmSync as rmSync4 } from "node:fs";
9318
9342
  import { tmpdir as tmpdir5 } from "node:os";
9319
9343
  import { join as join4 } from "node:path";
9320
9344
  import process6 from "node:process";
@@ -9359,7 +9383,7 @@ function tryAgain(path, attempt) {
9359
9383
  lastTryOnExit();
9360
9384
  }
9361
9385
  const after = AGAIN_AFTER_MS[attempt];
9362
- if (after === undefined || !existsSync3(path)) {
9386
+ if (after === undefined || !existsSync2(path)) {
9363
9387
  waiting.delete(path);
9364
9388
  if (after === undefined) {
9365
9389
  atExit.add(path);
@@ -10864,7 +10888,7 @@ async function startThroughHelper(spec, output, stopFile) {
10864
10888
  exited.now = true;
10865
10889
  });
10866
10890
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
10867
- const pid = existsSync4(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
10891
+ const pid = existsSync3(pidFile) ? Number(readFileSync9(pidFile, "utf8").trim()) : Number.NaN;
10868
10892
  if (Number.isInteger(pid) && pid > 0) {
10869
10893
  return { child: started.child, pid };
10870
10894
  }
@@ -11062,10 +11086,12 @@ async function keep() {
11062
11086
  });
11063
11087
  const slot = run?.slot;
11064
11088
  if (slot !== undefined && keeperStarted !== undefined) {
11089
+ const gameStarted = (await startTimesOf([pid])).get(pid);
11065
11090
  handSlot(slot.lock, slot.from, {
11066
11091
  pid: process.pid,
11067
11092
  started: await keeperStarted,
11068
- by: `${slot.by}, through the keeper process that runs it`
11093
+ by: `${slot.by}, through the keeper process that runs it`,
11094
+ ...gameStarted === undefined ? {} : { engine: { pid, started: gameStarted } }
11069
11095
  });
11070
11096
  }
11071
11097
  let { code, signal } = await exited;
@@ -11101,6 +11127,9 @@ async function keep() {
11101
11127
  restartedFrom: [...note.restartedFrom ?? [], before]
11102
11128
  };
11103
11129
  writeNoteIfItCan(note);
11130
+ if (slot !== undefined) {
11131
+ nameSlotEngine(slot.lock, process.pid, { pid: next.pid, started: next.startedAt });
11132
+ }
11104
11133
  end = () => {
11105
11134
  try {
11106
11135
  process.kill(next.pid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.55",
3
+ "version": "1.3.57",
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",