gdharness 1.3.43 → 1.3.44

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.
@@ -312,16 +312,25 @@ static func _bare(word: String) -> String:
312
312
  return word.to_lower().replace("_", "").replace(" ", "")
313
313
 
314
314
 
315
- ## The signals [param object] has that a caller can connect, its own class's first: the engine
316
- ## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
317
- ## starting with an underscore are the engine's own.
315
+ ## The signals [param object] has that a caller can connect, its own class's first: its script's,
316
+ ## as declared, then the engine's from the class down to Object. The engine lists its own from
317
+ ## Object down, which puts a Button's pressed after thirty a Control has, and its script's ahead of
318
+ ## those, so reversing the whole list put a game's own two signals after twenty-four of the
319
+ ## engine's. Names starting with an underscore are the engine's own.
318
320
  static func signals_of(object: Object) -> Array:
321
+ var declared: Array = []
322
+ var script: Script = object.get_script()
323
+ if script != null:
324
+ for entry: Dictionary in script.get_script_signal_list():
325
+ var name: String = str(entry["name"])
326
+ if not name.begins_with("_") and not name in declared:
327
+ declared.append(name)
319
328
  var names: Array = []
320
329
  for entry: Dictionary in object.get_signal_list():
321
330
  var name: String = str(entry["name"])
322
- if not name.begins_with("_"):
331
+ if not name.begins_with("_") and not name in declared:
323
332
  names.push_front(name)
324
- return names
333
+ return declared + names
325
334
 
326
335
 
327
336
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
@@ -526,10 +526,27 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
526
526
  if taken != null and taken != moving and taken.get_parent() == parent:
527
527
  # The root by its name, since its path is ".", and ". already has a child" read as a typo.
528
528
  var holder: String = "The root, %s," % parent.name if parent == root else path_of(parent)
529
- return "%s already has a child named %s." % [holder, child_name]
529
+ return (
530
+ "%s already has a child named %s; %s is free there."
531
+ % [holder, child_name, _free_name(parent, child_name)]
532
+ )
530
533
  return ""
531
534
 
532
535
 
536
+ ## The first name after [param taken] that no child of [param parent] has, numbered as Godot numbers
537
+ ## a duplicate: Crate2 after Crate, Crate4 after Crate3 when that is taken too. The refusal alone
538
+ ## left a caller guessing at a name that might be taken as well.
539
+ static func _free_name(parent: Node, taken: String) -> String:
540
+ var digits: int = 0
541
+ while digits < taken.length() and taken[taken.length() - 1 - digits] in "0123456789":
542
+ digits += 1
543
+ var base: String = taken.left(taken.length() - digits)
544
+ var at: int = 2 if digits == 0 else taken.right(digits).to_int() + 1
545
+ while parent.has_node(NodePath(base + str(at))):
546
+ at += 1
547
+ return base + str(at)
548
+
549
+
533
550
  ## The properties this scene's own file sets on [param node], each with the value the file gives it.
534
551
  func set_here(node: Node) -> Dictionary:
535
552
  return set_at(path_of(node))
@@ -51,7 +51,7 @@ func reload_script(args: Dictionary) -> Dictionary:
51
51
  var asked: String = str(args.get("scriptPath", ""))
52
52
  if asked.is_empty():
53
53
  return { "ok": false, "error": "scriptPath is required" }
54
- var path: String = _spelled_as_on_disk(asked)
54
+ var path: String = _spelled_as_on_disk(asked, {})
55
55
  if not ResourceLoader.exists(path):
56
56
  return { "ok": false, "error": "No script at " + path }
57
57
 
@@ -151,8 +151,9 @@ func scripts_behind(args: Dictionary) -> Dictionary:
151
151
  unsaved = EditorInterface.get_script_editor().get_unsaved_files()
152
152
  var behind: Array[String] = []
153
153
  var edited: Array[String] = []
154
+ var listings: Dictionary = {}
154
155
  for entry: Variant in asked:
155
- var path: String = _spelled_as_on_disk(str(entry))
156
+ var path: String = _spelled_as_on_disk(str(entry), listings)
156
157
  var script: GDScript = ResourceLoader.get_cached_ref(path) as GDScript
157
158
  if script == null:
158
159
  continue
@@ -196,28 +197,46 @@ func _reread_open_tabs(path: String, answer: Dictionary) -> void:
196
197
  ## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
197
198
  ## another spelling misses that copy and compiles a new one: reloading that answers with a fresh
198
199
  ## compile's members and leaves the copy the editor holds as it was.
199
- static func _spelled_as_on_disk(path: String) -> String:
200
+ ##
201
+ ## [param listings] keeps each directory's entries for the length of one call that spells many
202
+ ## paths. Listed again for every path, a check of the 416 classes of one project listed a folder of
203
+ ## eight hundred files twice per class, and held an editor's main thread for close to a minute on a
204
+ ## CI runner, with every request behind it waiting.
205
+ static func _spelled_as_on_disk(path: String, listings: Dictionary) -> String:
200
206
  var local: String = ProjectSettings.localize_path(path)
201
207
  if not local.begins_with("res://"):
202
208
  return local
203
209
  var spelled: String = "res://"
204
210
  for step: String in local.trim_prefix("res://").split("/", false):
205
- var listing: DirAccess = DirAccess.open(spelled)
206
- if listing == null:
211
+ if not listings.has(spelled):
212
+ listings[spelled] = _entries_by_spelling(spelled)
213
+ var entries: Variant = listings[spelled]
214
+ if entries == null:
207
215
  return local
208
- var found: String = ""
209
- for entry: String in listing.get_directories() + listing.get_files():
210
- if entry == step:
211
- found = entry
212
- break
213
- if found.is_empty() and entry.nocasecmp_to(step) == 0:
214
- found = entry
216
+ var exact: Dictionary = entries[0]
217
+ var folded: Dictionary = entries[1]
218
+ var found: String = step if exact.has(step) else str(folded.get(step.to_lower(), ""))
215
219
  if found.is_empty():
216
220
  return local
217
221
  spelled = spelled.path_join(found)
218
222
  return spelled
219
223
 
220
224
 
225
+ ## The entries of [param directory] by their own spelling and by their spelling in lower case, the
226
+ ## first of those that fold together winning, or null for a directory that cannot be opened.
227
+ static func _entries_by_spelling(directory: String) -> Variant:
228
+ var listing: DirAccess = DirAccess.open(directory)
229
+ if listing == null:
230
+ return null
231
+ var exact: Dictionary = {}
232
+ var folded: Dictionary = {}
233
+ for entry: String in listing.get_directories() + listing.get_files():
234
+ exact[entry] = true
235
+ if not folded.has(entry.to_lower()):
236
+ folded[entry.to_lower()] = entry
237
+ return [exact, folded]
238
+
239
+
221
240
  func _method_names(script: GDScript) -> Array[String]:
222
241
  var names: Array[String] = []
223
242
  for entry: Dictionary in script.get_script_method_list():
@@ -312,16 +312,25 @@ static func _bare(word: String) -> String:
312
312
  return word.to_lower().replace("_", "").replace(" ", "")
313
313
 
314
314
 
315
- ## The signals [param object] has that a caller can connect, its own class's first: the engine
316
- ## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
317
- ## starting with an underscore are the engine's own.
315
+ ## The signals [param object] has that a caller can connect, its own class's first: its script's,
316
+ ## as declared, then the engine's from the class down to Object. The engine lists its own from
317
+ ## Object down, which puts a Button's pressed after thirty a Control has, and its script's ahead of
318
+ ## those, so reversing the whole list put a game's own two signals after twenty-four of the
319
+ ## engine's. Names starting with an underscore are the engine's own.
318
320
  static func signals_of(object: Object) -> Array:
321
+ var declared: Array = []
322
+ var script: Script = object.get_script()
323
+ if script != null:
324
+ for entry: Dictionary in script.get_script_signal_list():
325
+ var name: String = str(entry["name"])
326
+ if not name.begins_with("_") and not name in declared:
327
+ declared.append(name)
319
328
  var names: Array = []
320
329
  for entry: Dictionary in object.get_signal_list():
321
330
  var name: String = str(entry["name"])
322
- if not name.begins_with("_"):
331
+ if not name.begins_with("_") and not name in declared:
323
332
  names.push_front(name)
324
- return names
333
+ return declared + names
325
334
 
326
335
 
327
336
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
@@ -36,6 +36,8 @@ var _files: GdhFileWalk = GdhFileWalk.new()
36
36
  ## script again made the whole-project answer quadratic in the number of scripts.
37
37
  var _classes_declared: Dictionary = {}
38
38
  var _classes_read: bool = false
39
+ var _uids_declared: Dictionary = {}
40
+ var _uids_read: bool = false
39
41
 
40
42
 
41
43
  func _init(p_log: GdhLog) -> void:
@@ -709,7 +711,7 @@ func _depend(
709
711
 
710
712
  ## Every autoload the project registers as a global name, with its script. One that is not global
711
713
  ## is no name code can use.
712
- static func autoloads_declared() -> Dictionary:
714
+ func autoloads_declared() -> Dictionary:
713
715
  var autoloads: Dictionary = {}
714
716
  for property: Dictionary in ProjectSettings.get_property_list():
715
717
  var setting: String = str(property.get("name", ""))
@@ -762,27 +764,63 @@ static func classes_used_in(content: String, classes: Dictionary) -> Array[Strin
762
764
  return used
763
765
 
764
766
 
765
- # [param written] as the res:// path the engine would load from [param from]: a uid:// by what the
766
- # project's uid cache says it names, and a relative path against the referring file's directory.
767
- # Both were passed over as not being res:// paths, so a script's `preload("sibling.gd")` was no
768
- # dependency at all.
769
- static func _resolved(written: String, from: String) -> String:
767
+ # [param written] as the res:// path the engine would load from [param from]: a uid:// by the file
768
+ # it names, and a relative path against the referring file's directory. Both were passed over as not
769
+ # being res:// paths, so a script's `preload("sibling.gd")` was no dependency at all.
770
+ func _resolved(written: String, from: String) -> String:
770
771
  if written.begins_with("res://"):
771
772
  return written
772
773
  if written.begins_with("uid://"):
773
- var id: int = ResourceUID.text_to_id(written)
774
- return ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else written
774
+ var found: String = _uid_path(written)
775
+ return written if found.is_empty() else found
775
776
  if written.contains("://"):
776
777
  return written
777
778
  return from.get_base_dir().path_join(written).simplify_path()
778
779
 
779
780
 
781
+ # The file the uid:// [param text] names, or empty when nothing does: the engine's uid cache first,
782
+ # then the uids the project's files declare. Only an import writes the cache, so on a fresh clone
783
+ # every uid was answered as naming no file, and a scene moved outside the editor was missing under
784
+ # its old path, although the first import loads it by its uid. A cached path with no file there
785
+ # gives way to a file declaring the uid, and is the answer when none does.
786
+ func _uid_path(text: String) -> String:
787
+ var id: int = ResourceUID.text_to_id(text)
788
+ if id == ResourceUID.INVALID_ID:
789
+ return ""
790
+ var cached: String = ResourceUID.get_id_path(id) if ResourceUID.has_id(id) else ""
791
+ if FileAccess.file_exists(cached):
792
+ return cached
793
+ return str(_files_by_uid().get(id, cached))
794
+
795
+
796
+ # Every file of the project that declares a uid, by its uid's id: in a .uid sidecar, an import
797
+ # sidecar or the header of a text resource, as GdhResourceFiles reads them. Read once for the call,
798
+ # the first time the cache does not know a uid.
799
+ func _files_by_uid() -> Dictionary:
800
+ if _uids_read:
801
+ return _uids_declared
802
+ _uids_read = true
803
+ var declaring: Dictionary = {}
804
+ for file: String in _files.find_files_with_extensions(
805
+ "res://", ["uid", "import"] + GdhResourceFiles.TEXT_RESOURCE_KINDS
806
+ ):
807
+ var sidecar: bool = file.get_extension() in ["uid", "import"]
808
+ declaring[file.get_basename() if sidecar else file] = true
809
+ for file: String in declaring:
810
+ if not FileAccess.file_exists(file):
811
+ continue
812
+ var id: int = ResourceUID.text_to_id(str(GdhResourceFiles.uid_of(file).get("uid", "")))
813
+ if id != ResourceUID.INVALID_ID:
814
+ _uids_declared[id] = file
815
+ return _uids_declared
816
+
817
+
780
818
  # The ext_resource paths in [param content] whose uid leads elsewhere, each with the file the uid
781
819
  # names. The engine loads an ext_resource by its uid whenever the uid is known, whatever the path
782
820
  # says, so a file moved outside the editor still loads, and the walk called it missing; every
783
821
  # pattern reads the old path, the bare res:// one included, so the paths are mapped before any of
784
822
  # them runs.
785
- static func _moved_by_uid(content: String) -> Dictionary:
823
+ func _moved_by_uid(content: String) -> Dictionary:
786
824
  var moved: Dictionary = {}
787
825
  for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
788
826
  var found: String = _loaded_by_uid(line.get_string())
@@ -795,7 +833,7 @@ static func _moved_by_uid(content: String) -> Dictionary:
795
833
  # The ext_resource paths in [param content], read from [param from], whose uid names no file the
796
834
  # project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
797
835
  # the walk followed the path saying nothing, so an export was the first to mention it.
798
- static func _unknown_uids(content: String, from: String) -> Dictionary:
836
+ func _unknown_uids(content: String, from: String) -> Dictionary:
799
837
  var unknown: Dictionary = {}
800
838
  for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
801
839
  var uid: String = _unknown_uid_of(line.get_string())
@@ -806,21 +844,18 @@ static func _unknown_uids(content: String, from: String) -> Dictionary:
806
844
 
807
845
 
808
846
  # The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
809
- static func _unknown_uid_of(entry: String) -> String:
847
+ func _unknown_uid_of(entry: String) -> String:
810
848
  var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
811
849
  return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
812
850
 
813
851
 
814
852
  # The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
815
853
  # the project knows, and the engine falls back to the path.
816
- static func _loaded_by_uid(entry: String) -> String:
854
+ func _loaded_by_uid(entry: String) -> String:
817
855
  var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
818
856
  if uid == null:
819
857
  return ""
820
- var id: int = ResourceUID.text_to_id(uid.get_string(1))
821
- if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id):
822
- return ""
823
- var found: String = ResourceUID.get_id_path(id)
858
+ var found: String = _uid_path(uid.get_string(1))
824
859
  return found if FileAccess.file_exists(found) else ""
825
860
 
826
861
 
@@ -85,15 +85,16 @@ static func run(params: Dictionary) -> Dictionary:
85
85
  answered["namedByPath"] = (
86
86
  (
87
87
  "What this saved names %s by path rather than by uid, which loads the same. A "
88
- + "uid in a file's own header is resolved through the project's uid cache, "
89
- + "which holds none for %s yet, as with a file made since the project was last "
90
- + "scanned, so a uid written now would warn on every load. project_import "
91
- + "refresh_uids scans the project, and writes after it name %s by uid."
88
+ + "reference by uid is resolved through the project's uid cache, which holds "
89
+ + "none for %s yet, as with a file made since the project was last scanned, so "
90
+ + "naming %s by uid now would warn on every load. project_import refresh_uids "
91
+ + "scans the project, and writes after it name %s by uid."
92
92
  )
93
93
  % [
94
94
  ", ".join(GdhSceneUids.by_path),
95
95
  "it" if GdhSceneUids.by_path.size() == 1 else "them",
96
96
  "it" if GdhSceneUids.by_path.size() == 1 else "them",
97
+ "it" if GdhSceneUids.by_path.size() == 1 else "them",
97
98
  ]
98
99
  )
99
100
  return answered
@@ -387,8 +387,7 @@ func add_autoload(params: Dictionary) -> Dictionary:
387
387
  var global: bool = GdhRead.as_bool(params.get("global", true), true)
388
388
 
389
389
  # There is no disabled autoload in Godot 4: every entry in the list is loaded under the root at
390
- # every start. `enabled: false` used to write the entry without its asterisk and answer that it
391
- # was disabled, and the autoload ran all the same.
390
+ # every start. An entry without its asterisk is one whose name is not global, and it still runs.
392
391
  if not GdhRead.as_bool(params.get("enabled", true), true):
393
392
  return (
394
393
  _log
@@ -433,12 +432,15 @@ func add_autoload(params: Dictionary) -> Dictionary:
433
432
  "action": "unchanged",
434
433
  "saved": false,
435
434
  }
435
+ # The editor's Add replaces an entry of the same name too; said here, since the replaced script
436
+ # is no longer loaded and nothing else in the answer names it.
437
+ var held: String = str(ProjectSettings.get_setting(setting)) if was_updated else ""
436
438
  ProjectSettings.set_setting(setting, entry)
437
439
  var err: Error = GdhProjectFile.save([setting])
438
440
  if err != OK:
439
441
  return _log.failure("Failed to save project.godot: " + error_string(err))
440
442
 
441
- return {
443
+ var answer: Dictionary = {
442
444
  "name": name,
443
445
  "path": path,
444
446
  "enabled": true,
@@ -446,6 +448,9 @@ func add_autoload(params: Dictionary) -> Dictionary:
446
448
  "action": "updated" if was_updated else "added",
447
449
  "saved": true,
448
450
  }
451
+ if was_updated:
452
+ answer["old_path"] = held.trim_prefix("*")
453
+ return answer
449
454
 
450
455
 
451
456
  ## Why [param name] cannot name an autoload, or empty when it can. The editor's Autoload tab refuses
@@ -312,16 +312,25 @@ static func _bare(word: String) -> String:
312
312
  return word.to_lower().replace("_", "").replace(" ", "")
313
313
 
314
314
 
315
- ## The signals [param object] has that a caller can connect, its own class's first: the engine
316
- ## lists them from Object down, which puts a Button's pressed after thirty a Control has, and names
317
- ## starting with an underscore are the engine's own.
315
+ ## The signals [param object] has that a caller can connect, its own class's first: its script's,
316
+ ## as declared, then the engine's from the class down to Object. The engine lists its own from
317
+ ## Object down, which puts a Button's pressed after thirty a Control has, and its script's ahead of
318
+ ## those, so reversing the whole list put a game's own two signals after twenty-four of the
319
+ ## engine's. Names starting with an underscore are the engine's own.
318
320
  static func signals_of(object: Object) -> Array:
321
+ var declared: Array = []
322
+ var script: Script = object.get_script()
323
+ if script != null:
324
+ for entry: Dictionary in script.get_script_signal_list():
325
+ var name: String = str(entry["name"])
326
+ if not name.begins_with("_") and not name in declared:
327
+ declared.append(name)
319
328
  var names: Array = []
320
329
  for entry: Dictionary in object.get_signal_list():
321
330
  var name: String = str(entry["name"])
322
- if not name.begins_with("_"):
331
+ if not name.begins_with("_") and not name in declared:
323
332
  names.push_front(name)
324
- return names
333
+ return declared + names
325
334
 
326
335
 
327
336
  ## [param word] after the article it takes, "an AnimationTree" or "a Node2D".
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 closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync6, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
6949
+ import { closeSync as closeSync2, existsSync as existsSync3, mkdtempSync as mkdtempSync4, openSync as openSync3, readFileSync as readFileSync7, rmSync as rmSync6, writeFileSync as writeFileSync3 } from "node:fs";
6950
6950
  import { tmpdir as tmpdir5 } 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,7 @@ function crashesReported() {
7435
7435
  }
7436
7436
 
7437
7437
  // src/game-debugger.ts
7438
- import { readFileSync as readFileSync4 } from "node:fs";
7438
+ import { readFileSync as readFileSync5 } from "node:fs";
7439
7439
  import { createServer } from "node:net";
7440
7440
  import { join as join6 } from "node:path";
7441
7441
 
@@ -7802,6 +7802,36 @@ var KEYWORDS = new Set([
7802
7802
  "TAU"
7803
7803
  ]);
7804
7804
 
7805
+ // src/whole-file.ts
7806
+ import { readFileSync as readFileSync2, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
7807
+ var HELD_FILE_MS = 1000;
7808
+ var HELD_FILE_RETRY_MS = 10;
7809
+ var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
7810
+ function writeWhole(path, text) {
7811
+ const partial = `${path}.${process.pid}.partial`;
7812
+ try {
7813
+ writeFileSync2(partial, text, "utf8");
7814
+ } catch {
7815
+ return false;
7816
+ }
7817
+ const until = Date.now() + HELD_FILE_MS;
7818
+ for (;; ) {
7819
+ try {
7820
+ renameSync(partial, path);
7821
+ return true;
7822
+ } catch (error) {
7823
+ const code = error.code ?? "";
7824
+ if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
7825
+ try {
7826
+ rmSync2(partial, { force: true });
7827
+ } catch {}
7828
+ return false;
7829
+ }
7830
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
7831
+ }
7832
+ }
7833
+ }
7834
+
7805
7835
  // src/dictionary.ts
7806
7836
  function emptyRecord() {
7807
7837
  return Object.create(null);
@@ -8399,14 +8429,14 @@ var MEMORY_KEPT = 4 * 1024 ** 3;
8399
8429
  var MEMORY_PER_ENGINE = 1.5 * 1024 ** 3;
8400
8430
 
8401
8431
  // src/process-children.ts
8402
- import { readFileSync as readFileSync2 } from "node:fs";
8432
+ import { readFileSync as readFileSync3 } from "node:fs";
8403
8433
  import { basename as basename3 } from "node:path";
8404
8434
  async function linuxStartTimes(pids) {
8405
8435
  const began = new Map;
8406
8436
  const hertz = await ticksPerSecond();
8407
8437
  let booted;
8408
8438
  try {
8409
- booted = Number(/^btime\s+(\d+)$/m.exec(readFileSync2("/proc/stat", "utf8"))?.[1] ?? Number.NaN) * 1000;
8439
+ booted = Number(/^btime\s+(\d+)$/m.exec(readFileSync3("/proc/stat", "utf8"))?.[1] ?? Number.NaN) * 1000;
8410
8440
  } catch {
8411
8441
  return began;
8412
8442
  }
@@ -8416,7 +8446,7 @@ async function linuxStartTimes(pids) {
8416
8446
  for (const pid of pids) {
8417
8447
  let stat;
8418
8448
  try {
8419
- stat = readFileSync2(`/proc/${String(pid)}/stat`, "utf8");
8449
+ stat = readFileSync3(`/proc/${String(pid)}/stat`, "utf8");
8420
8450
  } catch {
8421
8451
  continue;
8422
8452
  }
@@ -8516,36 +8546,6 @@ function commandWords(command) {
8516
8546
  return words;
8517
8547
  }
8518
8548
 
8519
- // src/whole-file.ts
8520
- import { renameSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
8521
- var HELD_FILE_MS = 1000;
8522
- var HELD_FILE_RETRY_MS = 10;
8523
- var HELD_BY_A_READER = new Set(["EPERM", "EACCES", "EBUSY"]);
8524
- function writeWhole(path, text) {
8525
- const partial = `${path}.${process.pid}.partial`;
8526
- try {
8527
- writeFileSync2(partial, text, "utf8");
8528
- } catch {
8529
- return false;
8530
- }
8531
- const until = Date.now() + HELD_FILE_MS;
8532
- for (;; ) {
8533
- try {
8534
- renameSync(partial, path);
8535
- return true;
8536
- } catch (error) {
8537
- const code = error.code ?? "";
8538
- if (!HELD_BY_A_READER.has(code) || Date.now() >= until) {
8539
- try {
8540
- rmSync2(partial, { force: true });
8541
- } catch {}
8542
- return false;
8543
- }
8544
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HELD_FILE_RETRY_MS);
8545
- }
8546
- }
8547
- }
8548
-
8549
8549
  // src/game-log.ts
8550
8550
  var COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
8551
8551
  var ADMITTED = {
@@ -8847,7 +8847,7 @@ function parseIniLikeValue(value) {
8847
8847
  }
8848
8848
 
8849
8849
  // src/server-version.ts
8850
- import { readFileSync as readFileSync3 } from "node:fs";
8850
+ import { readFileSync as readFileSync4 } from "node:fs";
8851
8851
 
8852
8852
  // src/update-check.ts
8853
8853
  var CACHE_MS = 10 * 60 * 1000;
@@ -8858,7 +8858,7 @@ var MAX_RETRY_MS = 60 * 60 * 1000;
8858
8858
  var DEBUG_MODE = process.env["GDHARNESS_DEBUG"] === "1";
8859
8859
  var SERVER_VERSION = (() => {
8860
8860
  try {
8861
- const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
8861
+ const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
8862
8862
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
8863
8863
  } catch {
8864
8864
  return "0.0.0";
@@ -8884,7 +8884,7 @@ var RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON}runtime_autoload.gd`;
8884
8884
  var frameEnd = null;
8885
8885
  function frameEndLine() {
8886
8886
  if (frameEnd === null) {
8887
- const lines = readFileSync4(join6(shippedAddonsDirectory(), "gdharness_runtime", "runtime_autoload.gd"), "utf8").split(`
8887
+ const lines = readFileSync5(join6(shippedAddonsDirectory(), "gdharness_runtime", "runtime_autoload.gd"), "utf8").split(`
8888
8888
  `);
8889
8889
  const declared = lines.findIndex((line) => line.startsWith("func _notification("));
8890
8890
  if (declared === -1) {
@@ -9642,7 +9642,7 @@ function readLaunched(printed) {
9642
9642
 
9643
9643
  // src/run-record.ts
9644
9644
  import { createHash as createHash2 } from "node:crypto";
9645
- import { mkdirSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, rmSync as rmSync5, statSync as statSync3 } from "node:fs";
9645
+ import { mkdirSync, openSync as openSync2, readdirSync as readdirSync2, readFileSync as readFileSync6, rmSync as rmSync5, statSync as statSync3 } from "node:fs";
9646
9646
  import { basename as basename4, join as join9, resolve as resolve4 } from "node:path";
9647
9647
 
9648
9648
  // src/runtime-client.ts
@@ -9738,7 +9738,7 @@ function recordRunEnded(projectPath, pid, ending) {
9738
9738
  function recordAt(path) {
9739
9739
  let parsed;
9740
9740
  try {
9741
- parsed = JSON.parse(readFileSync5(path, "utf8"));
9741
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
9742
9742
  } catch {
9743
9743
  return null;
9744
9744
  }
@@ -10186,7 +10186,7 @@ async function startThroughHelper(spec, output, stopFile) {
10186
10186
  exited.now = true;
10187
10187
  });
10188
10188
  for (let waited = 0;waited < DESKTOP_START_MS; waited += 50) {
10189
- const pid = existsSync3(pidFile) ? Number(readFileSync6(pidFile, "utf8").trim()) : Number.NaN;
10189
+ const pid = existsSync3(pidFile) ? Number(readFileSync7(pidFile, "utf8").trim()) : Number.NaN;
10190
10190
  if (Number.isInteger(pid) && pid > 0) {
10191
10191
  return { child: started.child, pid };
10192
10192
  }
@@ -10217,7 +10217,7 @@ function profiledBy(path, projectPath) {
10217
10217
  };
10218
10218
  let projectGodot = null;
10219
10219
  try {
10220
- projectGodot = readFileSync6(join10(projectPath, "project.godot"), "utf8");
10220
+ projectGodot = readFileSync7(join10(projectPath, "project.godot"), "utf8");
10221
10221
  } catch {}
10222
10222
  const observer = new ProfileObserver((totals) => {
10223
10223
  const first = latest === null;
@@ -10456,7 +10456,7 @@ function restartSaid(notes, pid) {
10456
10456
  return null;
10457
10457
  }
10458
10458
  try {
10459
- const said = JSON.parse(readFileSync6(join10(notes, `exit-${String(pid)}.json`), "utf8"));
10459
+ const said = JSON.parse(readFileSync7(join10(notes, `exit-${String(pid)}.json`), "utf8"));
10460
10460
  return typeof said === "object" && said !== null && "restart" in said && typeof said.restart === "boolean" ? said.restart : null;
10461
10461
  } catch {
10462
10462
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.43",
3
+ "version": "1.3.44",
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",