gdharness 1.3.57 → 1.3.58

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
@@ -1763,6 +1763,9 @@ function nonEmpty(value) {
1763
1763
  function keptSlotsOf(count) {
1764
1764
  return Math.max(2, Math.floor(count / 4));
1765
1765
  }
1766
+ function reservedSlotOf(count) {
1767
+ return count - Math.max(1, count - keptSlotsOf(count)) >= 2 ? count - 1 : null;
1768
+ }
1766
1769
  function gamesKeptOf(kept) {
1767
1770
  return Math.max(0, Math.min(Math.floor(kept / 2), kept - GATE_KEPT - 1));
1768
1771
  }
@@ -1777,7 +1780,8 @@ function slotsDrawnOn(count, taker, queue) {
1777
1780
  return { numbers: every };
1778
1781
  }
1779
1782
  if (taker === "tier") {
1780
- return { numbers: [...kept].reverse(), limit: "tier" };
1783
+ const reserve = reservedSlotOf(count);
1784
+ return { numbers: [...kept].reverse().filter((slot) => slot !== reserve), limit: "tier" };
1781
1785
  }
1782
1786
  return { numbers: kept.slice(0, Math.max(1, kept.length - GATE_KEPT)), limit: "kept" };
1783
1787
  }
@@ -1926,7 +1930,7 @@ class EngineSlots {
1926
1930
  try {
1927
1931
  for (;; ) {
1928
1932
  signal?.throwIfAborted();
1929
- const lock = await this.takeFree(game);
1933
+ const lock = await this.takeFree(game, Date.now() - began);
1930
1934
  if (lock !== null) {
1931
1935
  return lock;
1932
1936
  }
@@ -1958,7 +1962,7 @@ class EngineSlots {
1958
1962
  }
1959
1963
  }
1960
1964
  }
1961
- async takeFree(game = false) {
1965
+ async takeFree(game = false, waitedMs = 0) {
1962
1966
  mkdirSync2(this.directory, { recursive: true });
1963
1967
  await this.sweep();
1964
1968
  const inherited = this.inherited === null ? await this.inheritedLock() : null;
@@ -1967,11 +1971,14 @@ class EngineSlots {
1967
1971
  return inherited;
1968
1972
  }
1969
1973
  const taker = this.taker();
1970
- this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" ? "none" : await this.gateQueue());
1974
+ const aged = taker === "other" && !game && waitedMs >= ONE_OFF_AGED_MS;
1975
+ this.drawn = slotsDrawnOn(this.count, taker, taker === "gate" || taker === "timing" || aged ? "none" : await this.gateQueue());
1971
1976
  if (game && this.drawn.limit === "kept") {
1972
- const share = slotsDrawnOn(this.count, "tier", "queued").numbers;
1973
- const games = await this.heldByGames(share);
1974
- if (games.length >= gamesKeptOf(share.length)) {
1977
+ const reserve = reservedSlotOf(this.count);
1978
+ this.drawn = { ...this.drawn, numbers: this.drawn.numbers.filter((slot) => slot !== reserve) };
1979
+ const kept = this.count - Math.max(1, this.count - keptSlotsOf(this.count));
1980
+ const games = await this.heldByGames(Array.from({ length: kept }, (_, at) => this.count - kept + at));
1981
+ if (games.length >= gamesKeptOf(kept)) {
1975
1982
  this.drawn = { numbers: games, limit: "games" };
1976
1983
  return null;
1977
1984
  }
@@ -2122,6 +2129,25 @@ class EngineSlots {
2122
2129
  await this.cleared(join5(this.directory, name), false);
2123
2130
  }
2124
2131
  }
2132
+ const marks = join5(this.directory, WAITING_FOLDER);
2133
+ let waits;
2134
+ try {
2135
+ waits = readdirSync(marks);
2136
+ } catch {
2137
+ return;
2138
+ }
2139
+ for (const name of waits) {
2140
+ let content;
2141
+ try {
2142
+ content = readWhole(join5(marks, name));
2143
+ } catch {
2144
+ continue;
2145
+ }
2146
+ const pid = content === null ? undefined : holderOf(content)?.pid;
2147
+ if (pid !== undefined && !alive(pid)) {
2148
+ removeWhole(join5(marks, name));
2149
+ }
2150
+ }
2125
2151
  }
2126
2152
  takenOver(lock, holder) {
2127
2153
  return this.cleared(lock, true, holder);
@@ -2344,7 +2370,7 @@ async function inASlot(body) {
2344
2370
  held.release();
2345
2371
  }
2346
2372
  }
2347
- var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2373
+ var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", GATE_KEPT = 1, ONE_OFF_AGED_MS = 120000, TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
2348
2374
  var init_engine_slots = __esm(() => {
2349
2375
  init_alive();
2350
2376
  init_cache_home();
@@ -18158,7 +18184,7 @@ var init_tool_definitions = __esm(() => {
18158
18184
  direction: {
18159
18185
  type: "string",
18160
18186
  enum: ["forward", "reverse"],
18161
- description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
18187
+ description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. A file whose header, or an import sidecar, claims the same uid as its own is no use either: it is listed under shares_uid, since the engine maps a uid to one file and loads that one wherever the uid is named. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
18162
18188
  },
18163
18189
  depth: {
18164
18190
  type: "integer",
@@ -16,8 +16,9 @@ const SETTLE_MS: int = 8000
16
16
  ## How long a window with nothing in it is walked before the client stops: a game building its tree
17
17
  ## fills the window within a few frames of being asked, and one building none never does.
18
18
  const EMPTY_MS: int = 4000
19
- ## How long starting PowerShell and loading UI Automation may take before the client is stopped. It
20
- ## waits on the machine alone: a warm start takes under a second, and a cold one on a loaded Windows
19
+ ## How long starting PowerShell, loading UI Automation and the client's other cold loads may take
20
+ ## before the client is stopped. It waits on the machine alone: a warm start takes under a second,
21
+ ## and a cold one on a loaded Windows
21
22
  ## runner took the whole of a single 23 s budget meant for the walk as well.
22
23
  const START_MS: int = 45000
23
24
  ## How long the walks may take beyond [constant SETTLE_MS] a window once UI Automation is loaded,
@@ -39,6 +40,7 @@ const NODE_ID: String = "_node_instance"
39
40
  const WALK: String = r"""
40
41
  $ErrorActionPreference = 'Stop'
41
42
  function Answer($value) {
43
+ Mark 'answering'
42
44
  [System.IO.File]::WriteAllText($Out, (ConvertTo-Json -InputObject $value -Depth 100 -Compress))
43
45
  Mark 'answered'
44
46
  }
@@ -48,7 +50,6 @@ function Mark($stage) {
48
50
  try {
49
51
  Mark 'began'
50
52
  Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes
51
- Mark 'loaded'
52
53
  $walker = [System.Windows.Automation.TreeWalker]::RawViewWalker
53
54
  function Patterned($element, $name) {
54
55
  $pattern = ("System.Windows.Automation.$($name)Pattern" -as [type])::Pattern
@@ -70,6 +71,10 @@ try {
70
71
  } catch {
71
72
  $script:unread = "$_"
72
73
  }
74
+ # Serialised once here, since the first ConvertTo-Json loads its serialiser, and on a loaded
75
+ # machine that cold load belongs to the start's budget, not to the walks'.
76
+ $null = ConvertTo-Json -InputObject @{ warm = @(1) } -Depth 100 -Compress
77
+ Mark 'loaded'
73
78
  function FullDescription($element) {
74
79
  if ($null -ne $script:unread) { return '' }
75
80
  try {
@@ -281,7 +286,9 @@ func get_accessibility(params: Dictionary) -> Dictionary:
281
286
  var looked_at: int = began_at
282
287
  while OS.is_process_running(pid):
283
288
  var now: int = Time.get_ticks_msec()
284
- if now - looked_at >= LOOK_MS:
289
+ # Read again at the deadline too, so an answer marked since the last look is taken rather
290
+ # than stopped.
291
+ if now - looked_at >= LOOK_MS or now > deadline:
285
292
  looked_at = now
286
293
  var marked: String = FileAccess.get_file_as_string(progress)
287
294
  if not walking and marked.contains("loaded "):
@@ -756,8 +763,8 @@ static func _marks(progress: String) -> Array:
756
763
 
757
764
  ## How far a client started at [param started_ms] and stopped at [param stopped_ms] got by the marks
758
765
  ## in [param progress], said for a caller whose read it stopped: the time went to starting
759
- ## PowerShell, loading UI Automation, the walks, or after the last walk, and which tells a loaded
760
- ## machine from a window that never answers.
766
+ ## PowerShell, loading UI Automation, the walks, after the last walk, or writing the answer, and
767
+ ## which tells a loaded machine from a window that never answers.
761
768
  static func _reached(progress: String, started_ms: int, stopped_ms: int) -> String:
762
769
  var marks: Array = _marks(progress)
763
770
  var at: Dictionary = {}
@@ -786,7 +793,7 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
786
793
  )
787
794
  % [began, loaded]
788
795
  )
789
- return (
796
+ var walked: String = (
790
797
  (
791
798
  "PowerShell began the client after %d ms and loaded UI Automation in %d ms, and the "
792
799
  + "windows were walked %d time%s, the last finding %s element%s and ending %d ms "
@@ -802,6 +809,10 @@ static func _reached(progress: String, started_ms: int, stopped_ms: int) -> Stri
802
809
  stopped_ms - last_at,
803
810
  ]
804
811
  )
812
+ if not at.has("answering"):
813
+ return walked
814
+ var answering: int = stopped_ms - at["answering"]
815
+ return walked + ", which had been writing the answer for %d ms" % answering
805
816
 
806
817
 
807
818
  ## Where the time of a client started at [param started_ms] and done at [param done_ms] went, by
@@ -281,6 +281,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
281
281
  var by_kind: Dictionary = {}
282
282
  var total: int = 0
283
283
  var in_code: int = 0
284
+ var shares_uid: Array[String] = []
285
+ var target_uid: String = target["uid"]
284
286
 
285
287
  for file_path: String in all_files:
286
288
  # Its own sidecar names it as the file it was imported from, which is not a use of it.
@@ -305,6 +307,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
305
307
  var references: Array[Dictionary] = []
306
308
  for i: int in range(lines.size()):
307
309
  var line: String = lines[i]
310
+ # A file declaring the target's uid as its own is not using it: a resource copied with
311
+ # its header claims the same uid, and its header line was answered as a use by path.
312
+ if not target_uid.is_empty() and _own_uid(line, i, file_path) == target_uid:
313
+ shares_uid.append(file_path)
314
+ continue
308
315
  var by_its_path: bool = _names_target(line, file_path, target)
309
316
  var by_its_name: bool = false
310
317
  var name_in_comment: bool = false
@@ -408,9 +415,30 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
408
415
  }
409
416
  if not autoload_named.is_empty():
410
417
  answer["autoload"] = autoload_named
418
+ if not shares_uid.is_empty():
419
+ answer["shares_uid"] = shares_uid
411
420
  return answer
412
421
 
413
422
 
423
+ # The uid [param line], line [param index] of [param file_path], declares as the file's own: a
424
+ # scene's or a resource's header, or an import sidecar's uid of the file it imports; empty for any
425
+ # other line.
426
+ static func _own_uid(line: String, index: int, file_path: String) -> String:
427
+ var trimmed: String = line.strip_edges()
428
+ var declares: bool = (
429
+ (
430
+ index == 0
431
+ and file_path.get_extension() in ["tscn", "tres"]
432
+ and (trimmed.begins_with("[gd_scene") or trimmed.begins_with("[gd_resource"))
433
+ )
434
+ or (file_path.get_extension() == "import" and trimmed.begins_with('uid="'))
435
+ )
436
+ if not declares:
437
+ return ""
438
+ var found: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(trimmed)
439
+ return "" if found == null else found.get_string(1)
440
+
441
+
414
442
  # The class_name a script declares, or empty for a scene, a resource or a script without one.
415
443
  func _declared_class_name(path: String) -> String:
416
444
  if not path.ends_with(".gd"):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.57",
3
+ "version": "1.3.58",
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",