gdharness 1.2.2 → 1.2.4

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
@@ -42117,6 +42117,30 @@ function projectKey(projectPath) {
42117
42117
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
42118
42118
  return join24(directory, "runs", `note-${projectKey(projectPath)}.json`);
42119
42119
  }
42120
+ function latestRunPath(servedBy, directory = runtimeDirectory()) {
42121
+ return join24(directory, "runs", `latest-${projectKey(servedBy)}.json`);
42122
+ }
42123
+ function writeLatestRun(servedBy, latest) {
42124
+ try {
42125
+ mkdirSync14(runsDirectory(), { recursive: true });
42126
+ writeFileSync16(latestRunPath(servedBy), JSON.stringify(latest, null, 2), "utf8");
42127
+ } catch {}
42128
+ }
42129
+ function readLatestRun(servedBy, directory = runtimeDirectory()) {
42130
+ try {
42131
+ const fields = JSON.parse(readFileSync21(latestRunPath(servedBy, directory), "utf8"));
42132
+ if (typeof fields !== "object" || fields === null) {
42133
+ return null;
42134
+ }
42135
+ const { projectPath, pid, startedAt } = fields;
42136
+ if (typeof projectPath !== "string" || typeof startedAt !== "number") {
42137
+ return null;
42138
+ }
42139
+ return { projectPath, pid: typeof pid === "number" ? pid : null, startedAt };
42140
+ } catch {
42141
+ return null;
42142
+ }
42143
+ }
42120
42144
  function openTranscript(startedAt) {
42121
42145
  mkdirSync14(runsDirectory(), { recursive: true });
42122
42146
  const path = join24(runsDirectory(), `run-${startedAt}.log`);
@@ -42866,6 +42890,9 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
42866
42890
  function noCodeWillCome(run) {
42867
42891
  return !exited(run);
42868
42892
  }
42893
+ function transcriptNote(transcript, going) {
42894
+ return going ? `Everything this run has printed is in ${transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.` : `Everything this run printed is in ${transcript}, uncapped and complete, since the run is over.`;
42895
+ }
42869
42896
  function stopWaitMs() {
42870
42897
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
42871
42898
  }
@@ -46560,6 +46587,7 @@ class GodotServer {
46560
46587
  ...which === null ? {} : { play: which }
46561
46588
  };
46562
46589
  this.activeProcess = played;
46590
+ this.noteTheLatestRun({ projectPath, pid: null, startedAt });
46563
46591
  this.watchThePlayedConsole();
46564
46592
  writeEditorRunNote({
46565
46593
  projectPath,
@@ -46912,6 +46940,7 @@ class GodotServer {
46912
46940
  return launched;
46913
46941
  }
46914
46942
  const startedBy = Date.now();
46943
+ this.noteTheLatestRun({ projectPath, pid: launched.pid, startedAt });
46915
46944
  sweepTranscripts();
46916
46945
  const noted = readRunRecord(projectPath);
46917
46946
  const keeper = noted?.pid === launched.pid ? noted.keeper : undefined;
@@ -47150,7 +47179,8 @@ class GodotServer {
47150
47179
  async adoptRecordedRun() {
47151
47180
  const project = this.ourProject();
47152
47181
  const found = project === null ? [] : [readRunRecord(project), this.startedForUs(project)];
47153
- const record = found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null);
47182
+ const latest = project === null ? null : readLatestRun(project);
47183
+ const record = latest === null ? found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null) : found.find((one) => one !== null && one.pid === latest.pid && one.startedAt === latest.startedAt) ?? null;
47154
47184
  if (record === null || !(this.couldBeOurs(record.projectPath) || this.couldBeOurs(record.servedBy ?? ""))) {
47155
47185
  return null;
47156
47186
  }
@@ -47190,6 +47220,12 @@ class GodotServer {
47190
47220
  const mine = this.ourProject();
47191
47221
  return mine === null ? {} : { servedBy: mine };
47192
47222
  }
47223
+ noteTheLatestRun(latest) {
47224
+ const mine = this.ourProject();
47225
+ if (mine !== null) {
47226
+ writeLatestRun(mine, latest);
47227
+ }
47228
+ }
47193
47229
  startedForUs(project) {
47194
47230
  return everyRunRecord().find((one) => one.servedBy !== undefined && isSameDirectory(one.servedBy, project) && !(one.projectPath !== "" && isSameDirectory(one.projectPath, project))) ?? null;
47195
47231
  }
@@ -47601,7 +47637,7 @@ class GodotServer {
47601
47637
  notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
47602
47638
  }
47603
47639
  if (run.transcript !== null && selected.omitted > 0) {
47604
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
47640
+ notes.push(transcriptNote(run.transcript, going));
47605
47641
  }
47606
47642
  return this.jsonTextResponse({
47607
47643
  running: going,
@@ -377,10 +377,15 @@ static func drawn_over(target: Control) -> Node:
377
377
 
378
378
 
379
379
  ## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
380
- ## which nodes hide it, nearest first, and what is shown beside the nearest. Reading visible off
381
- ## the node itself says true when only an ancestor is hidden, so without this a caller had nothing
382
- ## to unhide; every hidden one is named because unhiding only the nearest leaves the rest hiding it.
383
- ## "" when nothing on the way up says so.
380
+ ## which nodes hide it, nearest first, and what has taken its place. Reading visible off the node
381
+ ## itself says true when only an ancestor is hidden, so without this a caller had nothing to unhide;
382
+ ## every hidden one is named because unhiding only the nearest leaves the rest hiding it.
383
+ ##
384
+ ## What has taken its place is what a click at its centre reaches now, which is the screen a game
385
+ ## shows instead. Without a control there, as for a 3D node, it is what is shown beside the
386
+ ## outermost hidden node: a game hides a screen by hiding the screen's root, and the nearest hidden
387
+ ## node can be a panel deep inside it whose siblings are hidden with it. "" when nothing on the way
388
+ ## up says the node is hidden.
384
389
  static func why_hidden(node: Node) -> String:
385
390
  var hiding: Array[Node] = _hiding(node)
386
391
  if hiding.is_empty():
@@ -391,27 +396,71 @@ static func why_hidden(node: Node) -> String:
391
396
  named.append("its own visible is false")
392
397
  else:
393
398
  named.append("%s is hidden%s" % [one.get_path(), "" if named.is_empty() else " too"])
399
+ var control: Control = node as Control
400
+ var instead: Node = null if control == null else drawn_at(control)
401
+ if instead != null:
402
+ return " (%s; a click where it would be reaches %s)" % [", and ".join(named), instead.get_path()]
403
+ var outermost: Node = hiding.back()
394
404
  var beside: Array[String] = []
395
- var parent: Node = hiding[0].get_parent()
405
+ var parent: Node = outermost.get_parent()
396
406
  if parent != null:
397
407
  for sibling: Node in parent.get_children():
398
- if sibling != hiding[0] and _shown(sibling):
408
+ if sibling != outermost and _shown(sibling):
399
409
  beside.append(str(sibling.get_path()))
400
- return " (%s%s)" % [", and ".join(named), _shown_beside(beside)]
410
+ return " (%s%s)" % [", and ".join(named), _shown_beside(beside, outermost)]
411
+
412
+
413
+ ## What a click at the centre of [param target], which is hidden, would reach instead, or null.
414
+ ##
415
+ ## The topmost thing there that takes the pointer, whatever order it is drawn in relative to the
416
+ ## target, since the target is not drawn at all; and never a node holding the target, which covers
417
+ ## its place by containing it and is not what took it.
418
+ static func drawn_at(target: Control) -> Node:
419
+ var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
420
+ var point: Vector2 = _centre_of(target)
421
+ var none: Dictionary[ScrollContainer, Vector2] = {}
422
+ var viewport: Viewport = target.get_viewport()
423
+ var best: Control = null
424
+ for each: Node in drawn:
425
+ var window: Window = each as Window
426
+ if window != null:
427
+ if _window_over(target, point, window):
428
+ return window
429
+ continue
430
+ var control: Control = each
431
+ if control.is_ancestor_of(target) or target.is_ancestor_of(control):
432
+ continue
433
+ if control.get_viewport() != viewport or not _holds(control, point):
434
+ continue
435
+ if _clipped_away(control, point, none):
436
+ continue
437
+ if best == null or _drawn_above(control, best):
438
+ best = control
439
+ return best
440
+
441
+
442
+ ## Whether [param control] is drawn above [param other]: on a higher canvas layer, or later on the
443
+ ## same one.
444
+ static func _drawn_above(control: Control, other: Control) -> bool:
445
+ var layer: int = _layer_of(control)
446
+ var other_layer: int = _layer_of(other)
447
+ return layer > other_layer or (layer == other_layer and control.is_greater_than(other))
401
448
 
402
449
 
403
- ## "; A, B and C beside it are shown" for [param beside], naming three at most and counting the rest.
404
- static func _shown_beside(beside: Array[String]) -> String:
450
+ ## "; A, B and C are shown beside N" for [param beside] and the hidden node [param hidden], naming
451
+ ## three at most and counting the rest.
452
+ static func _shown_beside(beside: Array[String], hidden: Node) -> String:
405
453
  if beside.is_empty():
406
454
  return ""
455
+ var at: String = str(hidden.get_path())
407
456
  if beside.size() == 1:
408
- return "; %s beside it is shown" % beside[0]
457
+ return "; %s is shown beside %s" % [beside[0], at]
409
458
  var listed: Array[String] = beside.slice(0, 3)
410
459
  var rest: int = beside.size() - listed.size()
411
460
  if rest > 0:
412
- return "; %s and %d more beside it are shown" % [", ".join(listed), rest]
461
+ return "; %s and %d more are shown beside %s" % [", ".join(listed), rest, at]
413
462
  var last: String = listed.pop_back()
414
- return "; %s and %s beside it are shown" % [", ".join(listed), last]
463
+ return "; %s and %s are shown beside %s" % [", ".join(listed), last, at]
415
464
 
416
465
 
417
466
  ## The nodes whose visible is false and keeps [param node] from being drawn, nearest first. Walked
package/build/index.js CHANGED
@@ -32922,6 +32922,30 @@ function projectKey(projectPath) {
32922
32922
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
32923
32923
  return join19(directory, "runs", `note-${projectKey(projectPath)}.json`);
32924
32924
  }
32925
+ function latestRunPath(servedBy, directory = runtimeDirectory()) {
32926
+ return join19(directory, "runs", `latest-${projectKey(servedBy)}.json`);
32927
+ }
32928
+ function writeLatestRun(servedBy, latest) {
32929
+ try {
32930
+ mkdirSync11(runsDirectory(), { recursive: true });
32931
+ writeFileSync13(latestRunPath(servedBy), JSON.stringify(latest, null, 2), "utf8");
32932
+ } catch {}
32933
+ }
32934
+ function readLatestRun(servedBy, directory = runtimeDirectory()) {
32935
+ try {
32936
+ const fields = JSON.parse(readFileSync18(latestRunPath(servedBy, directory), "utf8"));
32937
+ if (typeof fields !== "object" || fields === null) {
32938
+ return null;
32939
+ }
32940
+ const { projectPath, pid, startedAt } = fields;
32941
+ if (typeof projectPath !== "string" || typeof startedAt !== "number") {
32942
+ return null;
32943
+ }
32944
+ return { projectPath, pid: typeof pid === "number" ? pid : null, startedAt };
32945
+ } catch {
32946
+ return null;
32947
+ }
32948
+ }
32925
32949
  var SHARED_NOTE = "run.json";
32926
32950
  function openTranscript(startedAt) {
32927
32951
  mkdirSync11(runsDirectory(), { recursive: true });
@@ -35294,6 +35318,9 @@ function runIsUp(run, editorSays, editorPlay = null, now = Date.now()) {
35294
35318
  function noCodeWillCome(run) {
35295
35319
  return !exited(run);
35296
35320
  }
35321
+ function transcriptNote(transcript, going) {
35322
+ return going ? `Everything this run has printed is in ${transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.` : `Everything this run printed is in ${transcript}, uncapped and complete, since the run is over.`;
35323
+ }
35297
35324
  function stopWaitMs() {
35298
35325
  return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
35299
35326
  }
@@ -39057,6 +39084,7 @@ class GodotServer {
39057
39084
  ...which === null ? {} : { play: which }
39058
39085
  };
39059
39086
  this.activeProcess = played;
39087
+ this.noteTheLatestRun({ projectPath, pid: null, startedAt });
39060
39088
  this.watchThePlayedConsole();
39061
39089
  writeEditorRunNote({
39062
39090
  projectPath,
@@ -39409,6 +39437,7 @@ class GodotServer {
39409
39437
  return launched;
39410
39438
  }
39411
39439
  const startedBy = Date.now();
39440
+ this.noteTheLatestRun({ projectPath, pid: launched.pid, startedAt });
39412
39441
  sweepTranscripts();
39413
39442
  const noted = readRunRecord(projectPath);
39414
39443
  const keeper = noted?.pid === launched.pid ? noted.keeper : undefined;
@@ -39647,7 +39676,8 @@ class GodotServer {
39647
39676
  async adoptRecordedRun() {
39648
39677
  const project = this.ourProject();
39649
39678
  const found = project === null ? [] : [readRunRecord(project), this.startedForUs(project)];
39650
- const record = found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null);
39679
+ const latest = project === null ? null : readLatestRun(project);
39680
+ const record = latest === null ? found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null) : found.find((one) => one !== null && one.pid === latest.pid && one.startedAt === latest.startedAt) ?? null;
39651
39681
  if (record === null || !(this.couldBeOurs(record.projectPath) || this.couldBeOurs(record.servedBy ?? ""))) {
39652
39682
  return null;
39653
39683
  }
@@ -39687,6 +39717,12 @@ class GodotServer {
39687
39717
  const mine = this.ourProject();
39688
39718
  return mine === null ? {} : { servedBy: mine };
39689
39719
  }
39720
+ noteTheLatestRun(latest) {
39721
+ const mine = this.ourProject();
39722
+ if (mine !== null) {
39723
+ writeLatestRun(mine, latest);
39724
+ }
39725
+ }
39690
39726
  startedForUs(project) {
39691
39727
  return everyRunRecord().find((one) => one.servedBy !== undefined && isSameDirectory(one.servedBy, project) && !(one.projectPath !== "" && isSameDirectory(one.projectPath, project))) ?? null;
39692
39728
  }
@@ -40098,7 +40134,7 @@ class GodotServer {
40098
40134
  notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
40099
40135
  }
40100
40136
  if (run.transcript !== null && selected.omitted > 0) {
40101
- notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
40137
+ notes.push(transcriptNote(run.transcript, going));
40102
40138
  }
40103
40139
  return this.jsonTextResponse({
40104
40140
  running: going,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
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",