gdharness 1.1.26 → 1.1.28

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
@@ -17617,7 +17617,7 @@ var init_tool_definitions = __esm(() => {
17617
17617
  },
17618
17618
  {
17619
17619
  name: "editor_status",
17620
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
17620
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
17621
17621
  parameters: {},
17622
17622
  requires: []
17623
17623
  },
@@ -17724,7 +17724,7 @@ var init_tool_definitions = __esm(() => {
17724
17724
  namePattern: {
17725
17725
  type: "string",
17726
17726
  ops: ["find"],
17727
- description: 'find: a case-insensitive glob on the node name, such as "Enemy*". Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
17727
+ description: 'find: a case-insensitive glob on the node name, such as "Enemy*", or several separated by | the way says takes them, such as "Enemy*|Boss", with \\| for a bar in a name. Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
17728
17728
  },
17729
17729
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
17730
17730
  says: {
@@ -42183,6 +42183,13 @@ class GodotServer {
42183
42183
  return null;
42184
42184
  }
42185
42185
  }
42186
+ runNamed(run) {
42187
+ return {
42188
+ pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
42189
+ ...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
42190
+ ...run.pickedUp === true ? { adopted: true } : {}
42191
+ };
42192
+ }
42186
42193
  async handleEditorStatus() {
42187
42194
  const located = await this.locator.find();
42188
42195
  const announced = runtimesAnnounced();
@@ -42215,10 +42222,11 @@ class GodotServer {
42215
42222
  return {
42216
42223
  playing,
42217
42224
  active,
42225
+ run: current === null ? null : this.runNamed(current),
42218
42226
  hold: current === null ? {} : await this.holdOf(current, active)
42219
42227
  };
42220
42228
  })();
42221
- const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
42229
+ const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
42222
42230
  pinged,
42223
42231
  asked,
42224
42232
  this.editorScanState(),
@@ -42237,6 +42245,7 @@ class GodotServer {
42237
42245
  },
42238
42246
  game: {
42239
42247
  processActive: active,
42248
+ run,
42240
42249
  playingInEditor: playing,
42241
42250
  ...hold,
42242
42251
  runtimeConnected: games.some((game) => game.reachable && game.ofThisProject !== false),
@@ -70,7 +70,8 @@ func get_tree(params: Dictionary) -> Dictionary:
70
70
 
71
71
  ## Nodes matching every filter given, as paths, so a caller can name what it wants without
72
72
  ## reading the whole tree to find it. `class` matches native classes and their subclasses, and
73
- ## the global name of a script class; `name` is a case-insensitive glob; `script` is a path;
73
+ ## the global name of a script class; `name` is a case-insensitive glob, or several separated by `|`
74
+ ## the way `says` takes them, with `\|` for a bar in a name; `script` is a path;
74
75
  ## `says` is what the node has written on it.
75
76
  ##
76
77
  ## `property` names one to read off each of them, which is the difference between one question and
@@ -103,6 +104,12 @@ func find_nodes(params: Dictionary) -> Dictionary:
103
104
  if not wanted["script"].is_empty() and not wanted["script"].begins_with("res://"):
104
105
  wanted["script"] = "res://" + wanted["script"]
105
106
 
107
+ # Split once here rather than for each node the walk passes.
108
+ var names: Array[String] = Says.split_at_bars(wanted["name"])
109
+ if not wanted["name"].is_empty() and names.is_empty():
110
+ var bars_only: String = 'name "%s" is only bars, so it names no node; write the names between them'
111
+ return {"type": "error", "message": bars_only % wanted["name"]}
112
+
106
113
  var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
107
114
  if reached.has("message"):
108
115
  return reached
@@ -113,7 +120,10 @@ func find_nodes(params: Dictionary) -> Dictionary:
113
120
  var truncated: bool = false
114
121
  # Counted while the tree is already being walked, for the answer below: how many nodes this
115
122
  # find would have matched if the name had been read the way it was probably meant.
116
- var literal: bool = Says.is_plain(wanted["name"])
123
+ var plain_names: Array[String] = []
124
+ for named: String in names:
125
+ if Says.is_plain(named):
126
+ plain_names.append(named)
117
127
  var nearly: int = 0
118
128
  # The same for the words: a glob is matched against the whole text, and one written as a prefix
119
129
  # by habit, "Taken together*", missed the label whose sentence those words are in the middle of.
@@ -132,7 +142,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
132
142
  while not pending.is_empty():
133
143
  var node: Node = pending.pop_front()
134
144
  var rest: bool = _matches_apart_from_name(node, wanted)
135
- if rest and _named(node, wanted["name"]):
145
+ if rest and _named(node, names):
136
146
  if not include_hidden and not shown(node):
137
147
  hidden += 1
138
148
  elif found.size() >= limit:
@@ -140,16 +150,12 @@ func find_nodes(params: Dictionary) -> Dictionary:
140
150
  break
141
151
  else:
142
152
  found.append(_found(node, wanted_property))
143
- elif rest and literal and str(node.name).containsn(wanted["name"]):
153
+ elif rest and _contains_any(str(node.name), plain_names):
144
154
  if include_hidden or shown(node):
145
155
  nearly += 1
146
156
  else:
147
157
  nearly_hidden += 1
148
- elif (
149
- not widened_says.is_empty()
150
- and _named(node, wanted["name"])
151
- and _matches_apart_from_name(node, widened)
152
- ):
158
+ elif not widened_says.is_empty() and _named(node, names) and _matches_apart_from_name(node, widened):
153
159
  if include_hidden or shown(node):
154
160
  nearly_said += 1
155
161
  else:
@@ -172,17 +178,23 @@ func find_nodes(params: Dictionary) -> Dictionary:
172
178
  if found.is_empty() and nearly + nearly_hidden > 0:
173
179
  var named_near: int = nearly if nearly > 0 else nearly_hidden
174
180
  var holding: String = "names contain" if named_near > 1 else "name contains"
181
+ var near_words: Array[String] = []
182
+ for named: String in plain_names:
183
+ near_words.append('"%s"' % named)
184
+ var opened: Array[String] = []
185
+ for named: String in names:
186
+ opened.append("*%s*" % named if Says.is_plain(named) else named)
175
187
  (
176
188
  notes
177
189
  . append(
178
190
  (
179
- 'name is matched as a glob against the whole name; %d %snode %s "%s", which "*%s*"%s would find'
191
+ 'name is matched as a glob against the whole name; %d %snode %s %s, which "%s"%s would find'
180
192
  % [
181
193
  named_near,
182
194
  "" if nearly > 0 else "hidden ",
183
195
  holding,
184
- wanted["name"],
185
- wanted["name"],
196
+ " or ".join(PackedStringArray(near_words)),
197
+ Says.joined_at_bars(opened),
186
198
  "" if nearly > 0 else " with includeHidden true",
187
199
  ]
188
200
  )
@@ -308,10 +320,23 @@ static func _anything_asked(wanted: Dictionary[String, String]) -> bool:
308
320
  return false
309
321
 
310
322
 
311
- ## Whether [param node] carries the name a find asked for. Empty matches everything, which is what
312
- ## makes leaving a filter out the same as not having one.
313
- static func _named(node: Node, pattern: String) -> bool:
314
- return pattern.is_empty() or str(node.name).matchn(pattern)
323
+ ## Whether [param node] carries one of the names a find asked for. None matches everything, which is
324
+ ## what makes leaving a filter out the same as not having one.
325
+ static func _named(node: Node, names: Array[String]) -> bool:
326
+ if names.is_empty():
327
+ return true
328
+ var called: String = str(node.name)
329
+ for pattern: String in names:
330
+ if called.matchn(pattern):
331
+ return true
332
+ return false
333
+
334
+
335
+ static func _contains_any(called: String, words: Array[String]) -> bool:
336
+ for word: String in words:
337
+ if called.containsn(word):
338
+ return true
339
+ return false
315
340
 
316
341
 
317
342
  ## Every filter but the name, so a find that came back empty can say how many nodes the name was
@@ -54,18 +54,34 @@ static func alternatives(wanted: String) -> Array[String]:
54
54
  if kept != null:
55
55
  var split: Array[String] = kept
56
56
  return split
57
- var held: String = as_said(wanted).replace("\\|", ESCAPED_BAR)
57
+ var found: Array[String] = split_at_bars(as_said(wanted))
58
+ if _splits.size() >= SPLITS_KEPT:
59
+ _splits.clear()
60
+ _splits[wanted] = found
61
+ return found
62
+
63
+
64
+ ## [param written] split at every `|` not written as `\|`, empty alternatives dropped. Shared with a
65
+ ## find's name, which takes alternatives by the same rule so the two arguments beside each other do
66
+ ## not read a bar two ways: written in a name, `A|B` was one glob no node matched, and the empty
67
+ ## answer came with no note.
68
+ static func split_at_bars(written: String) -> Array[String]:
58
69
  var found: Array[String] = []
59
- for part: String in held.split("|"):
70
+ for part: String in written.replace("\\|", ESCAPED_BAR).split("|"):
60
71
  var words: String = part.replace(ESCAPED_BAR, "|")
61
72
  if not words.is_empty():
62
73
  found.append(words)
63
- if _splits.size() >= SPLITS_KEPT:
64
- _splits.clear()
65
- _splits[wanted] = found
66
74
  return found
67
75
 
68
76
 
77
+ ## [param parts] joined back into one pattern that [method split_at_bars] reads as them.
78
+ static func joined_at_bars(parts: Array[String]) -> String:
79
+ var escaped: Array[String] = []
80
+ for words: String in parts:
81
+ escaped.append(words.replace("|", "\\|"))
82
+ return "|".join(PackedStringArray(escaped))
83
+
84
+
69
85
  ## Whether [param pattern] is words written whole rather than as a glob, which is how a caller
70
86
  ## writes them when they mean "contains". [method String.matchn] answers nothing to it, and nothing
71
87
  ## is also what words that are simply not there answer.
package/build/index.js CHANGED
@@ -31601,7 +31601,7 @@ var TOOL_SPECS = [
31601
31601
  },
31602
31602
  {
31603
31603
  name: "editor_status",
31604
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
31604
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
31605
31605
  parameters: {},
31606
31606
  requires: []
31607
31607
  },
@@ -31708,7 +31708,7 @@ var TOOL_SPECS = [
31708
31708
  namePattern: {
31709
31709
  type: "string",
31710
31710
  ops: ["find"],
31711
- description: 'find: a case-insensitive glob on the node name, such as "Enemy*". Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
31711
+ description: 'find: a case-insensitive glob on the node name, such as "Enemy*", or several separated by | the way says takes them, such as "Enemy*|Boss", with \\| for a bar in a name. Matched against the whole name, so a bare word finds only a node called exactly that; an answer of none says how many names contain it and what glob would have found them.'
31712
31712
  },
31713
31713
  group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
31714
31714
  says: {
@@ -34763,6 +34763,13 @@ class GodotServer {
34763
34763
  return null;
34764
34764
  }
34765
34765
  }
34766
+ runNamed(run) {
34767
+ return {
34768
+ pid: run.pid ?? this.announcedPidOf(run) ?? run.announcedPid ?? null,
34769
+ ...run.throughEditor || run.projectPath === null ? {} : { project: run.projectPath },
34770
+ ...run.pickedUp === true ? { adopted: true } : {}
34771
+ };
34772
+ }
34766
34773
  async handleEditorStatus() {
34767
34774
  const located = await this.locator.find();
34768
34775
  const announced = runtimesAnnounced();
@@ -34795,10 +34802,11 @@ class GodotServer {
34795
34802
  return {
34796
34803
  playing,
34797
34804
  active,
34805
+ run: current === null ? null : this.runNamed(current),
34798
34806
  hold: current === null ? {} : await this.holdOf(current, active)
34799
34807
  };
34800
34808
  })();
34801
- const [games, { playing, active, hold }, scan, editorStatus] = await Promise.all([
34809
+ const [games, { playing, active, run, hold }, scan, editorStatus] = await Promise.all([
34802
34810
  pinged,
34803
34811
  asked,
34804
34812
  this.editorScanState(),
@@ -34817,6 +34825,7 @@ class GodotServer {
34817
34825
  },
34818
34826
  game: {
34819
34827
  processActive: active,
34828
+ run,
34820
34829
  playingInEditor: playing,
34821
34830
  ...hold,
34822
34831
  runtimeConnected: games.some((game) => game.reachable && game.ofThisProject !== false),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.1.26",
3
+ "version": "1.1.28",
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",