gdharness 1.2.34 → 1.2.36

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/index.js CHANGED
@@ -28717,9 +28717,14 @@ class GameLog {
28717
28717
  const owner = this.lastHeadline[source];
28718
28718
  if (owner !== null && /^\s/.test(line)) {
28719
28719
  owner.detail.push(line.trim());
28720
+ owner.continues = false;
28720
28721
  return;
28721
28722
  }
28722
28723
  const headline = announced(line);
28724
+ if (owner?.continues === true && headline === null) {
28725
+ owner.detail.push(line.trim());
28726
+ return;
28727
+ }
28723
28728
  const detail = [];
28724
28729
  const entry = {
28725
28730
  index: this.entries.length,
@@ -28732,7 +28737,7 @@ class GameLog {
28732
28737
  if (headline?.byScript === true) {
28733
28738
  this.byScript.add(entry.index);
28734
28739
  }
28735
- this.lastHeadline[source] = headline ? { detail } : null;
28740
+ this.lastHeadline[source] = headline ? { detail, continues: source === "stderr" && headline.text.endsWith(":") } : null;
28736
28741
  }
28737
28742
  raisedByScript(entry) {
28738
28743
  return this.byScript.has(entry.index);
@@ -33314,7 +33319,7 @@ var HEADLESS_OPERATIONS = dictionary({
33314
33319
  uid: "get_uid",
33315
33320
  refresh_classes: "refresh_class_cache"
33316
33321
  }),
33317
- project_export: dictionary({ list: "list_export_presets" }),
33322
+ project_export: dictionary({ list: "list_export_presets", add_preset: "add_export_preset" }),
33318
33323
  script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
33319
33324
  script_info: dictionary({ structure: "get_script_info" }),
33320
33325
  editor_classes: dictionary({
@@ -35210,7 +35215,7 @@ var TOOL_SPECS = [
35210
35215
  direction: {
35211
35216
  type: "string",
35212
35217
  enum: ["forward", "reverse"],
35213
- description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind, and two of them are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
35218
+ description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it, and total_references counts the entries in the tree. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; 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. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
35214
35219
  },
35215
35220
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
35216
35221
  includeBuiltin: {
@@ -35290,20 +35295,36 @@ var TOOL_SPECS = [
35290
35295
  {
35291
35296
  name: "project_export",
35292
35297
  title: "Export",
35293
- description: "Export presets and exports.",
35298
+ description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
35294
35299
  parameters: {
35295
35300
  projectPath: PROJECT_PATH2,
35296
- preset: { type: "string", description: "Preset name from export_presets.cfg." },
35301
+ preset: {
35302
+ type: "string",
35303
+ ops: ["run", "add_preset"],
35304
+ description: "Preset name from export_presets.cfg. add_preset: the name of the new one."
35305
+ },
35306
+ platform: {
35307
+ type: "string",
35308
+ ops: ["add_preset"],
35309
+ enum: ["Windows Desktop", "Linux", "macOS", "Android", "iOS", "Web"],
35310
+ description: "add_preset: the platform the preset exports for."
35311
+ },
35297
35312
  outputPath: {
35298
35313
  type: "string",
35299
- description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset.`
35314
+ ops: ["run", "add_preset"],
35315
+ description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset. add_preset: the path the preset exports to from the editor, optional.`
35300
35316
  },
35301
- debug: { type: "boolean", description: "run: a debug export. Default false." }
35317
+ debug: { type: "boolean", ops: ["run"], description: "run: a debug export. Default false." }
35302
35318
  },
35303
35319
  requires: ["projectPath"],
35304
35320
  operations: {
35305
35321
  list: { effect: "read", summary: "the presets in export_presets.cfg", requires: [] },
35306
- run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] }
35322
+ run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] },
35323
+ add_preset: {
35324
+ effect: "additive",
35325
+ summary: "add an export preset to export_presets.cfg",
35326
+ requires: ["preset", "platform"]
35327
+ }
35307
35328
  }
35308
35329
  },
35309
35330
  {
@@ -35671,7 +35692,7 @@ var TOOL_SPECS = [
35671
35692
  operations: {
35672
35693
  structure: {
35673
35694
  effect: "read",
35674
- summary: "functions, variables, signals, class_name and extends, read from the file",
35695
+ summary: "functions, variables, signals, class_name and extends, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class",
35675
35696
  requires: []
35676
35697
  },
35677
35698
  symbols: {
@@ -35692,7 +35713,7 @@ var TOOL_SPECS = [
35692
35713
  name: "script_diagnostics",
35693
35714
  title: "Script diagnostics",
35694
35715
  effect: "read",
35695
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
35716
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, and warnings null, since the engine reports warnings only to an editor. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
35696
35717
  parameters: {
35697
35718
  projectPath: PROJECT_PATH2,
35698
35719
  scriptPath: {
@@ -36150,7 +36171,7 @@ var TOOL_SPECS = [
36150
36171
  says: {
36151
36172
  type: "string",
36152
36173
  ops: ["find", "accessibility"],
36153
- description: 'accessibility: only the elements whose name or value says these words, matched as find matches them, with what they hold. find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
36174
+ description: 'accessibility: only the elements whose name, value or value_text says these words, matched as find matches them, with what they hold. find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
36154
36175
  },
36155
36176
  limit: {
36156
36177
  type: "integer",
@@ -36181,7 +36202,7 @@ var TOOL_SPECS = [
36181
36202
  tree: { effect: "read", summary: "the live scene tree", requires: [] },
36182
36203
  text: {
36183
36204
  effect: "read",
36184
- summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
36205
+ summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
36185
36206
  requires: []
36186
36207
  },
36187
36208
  find: {
@@ -36201,12 +36222,12 @@ var TOOL_SPECS = [
36201
36222
  },
36202
36223
  metrics: {
36203
36224
  effect: "read",
36204
- summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
36225
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. Memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
36205
36226
  requires: []
36206
36227
  },
36207
36228
  accessibility: {
36208
36229
  effect: "read",
36209
- summary: "what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one with the focus. node is the node that made the element; an element a node made as a part of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name is read as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed run on Windows started with engineArgs [\"--accessibility\", \"always\"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two",
36230
+ summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader does, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, value_text (a range's spoken words, such as "75%"), description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the focused one. node is the node that made the element; a part a node made of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name reads as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed Windows run started with engineArgs ["--accessibility", "always"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two`,
36210
36231
  requires: []
36211
36232
  }
36212
36233
  },
@@ -36215,7 +36236,7 @@ var TOOL_SPECS = [
36215
36236
  {
36216
36237
  name: "runtime_invoke",
36217
36238
  title: "Set or call in the running game",
36218
- description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
36239
+ description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors the engine reported while a called method ran come back under errors and warnings under warnings, the first 20 of each, with errors_not_shown and warnings_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
36219
36240
  parameters: {
36220
36241
  projectPath: RUNNING_PROJECT_PATH,
36221
36242
  pid: RUNNING_PID,
@@ -36335,7 +36356,7 @@ var TOOL_SPECS = [
36335
36356
  {
36336
36357
  name: "runtime_input",
36337
36358
  title: "Send input to the game",
36338
- description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
36359
+ description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
36339
36360
  answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
36340
36361
  parameters: {
36341
36362
  projectPath: RUNNING_PROJECT_PATH,
@@ -36532,10 +36553,16 @@ var TOOL_SPECS = [
36532
36553
  },
36533
36554
  nodePath: {
36534
36555
  type: "string",
36535
- ops: ["signal", "until"],
36536
- description: "signal, until: the node. Needed for a signal and for a property; for says it defaults to /root, the whole screen."
36556
+ ops: ["signal", "watch", "until"],
36557
+ description: "signal, watch, until: the node. Needed for a signal without a watch, a watch and a property; for says it defaults to /root, the whole screen."
36558
+ },
36559
+ signal: { type: "string", ops: ["signal", "watch"], description: "signal, watch: the signal name." },
36560
+ watch: {
36561
+ type: "integer",
36562
+ minimum: 1,
36563
+ ops: ["signal"],
36564
+ description: "signal: the watch an earlier watch answered, in place of nodePath and signal. Answers at once, with fired_before_wait, when the signal fired after the watch was armed, and otherwise waits for it; times counts how often it fired, and args is what the first carried. The watch goes with the answer."
36537
36565
  },
36538
- signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
36539
36566
  property: {
36540
36567
  type: "string",
36541
36568
  ops: ["until"],
@@ -36575,6 +36602,11 @@ var TOOL_SPECS = [
36575
36602
  signal: {
36576
36603
  effect: "read",
36577
36604
  summary: "wait for a signal and answer with what it carried",
36605
+ requires: []
36606
+ },
36607
+ watch: {
36608
+ effect: "read",
36609
+ summary: "start recording a signal and answer at once with a watch; arm it before the call meant to set the signal off, then collect it with signal and the watch. A client can run a call sent beside a wait only after the wait answers, so a wait sent with the call misses it",
36578
36610
  requires: ["nodePath", "signal"]
36579
36611
  },
36580
36612
  until: {
@@ -37228,6 +37260,12 @@ function endedWithoutACode(run) {
37228
37260
  }
37229
37261
  return `The editor stopped playing this run. ${kept} What it printed is below.`;
37230
37262
  }
37263
+ function ourRunEnded(run, now) {
37264
+ const which = `${run.throughEditor ? "played in the editor" : "started"}${run.pid === null ? "" : ` (pid ${run.pid})`}`;
37265
+ const when = run.endedAt === undefined ? "" : ` ${Math.max(0, Math.round((now - run.endedAt) / 1000))}s ago`;
37266
+ const how = run.endedThrough === "close" ? "this server asked it to close" : run.endedThrough === "keeper" ? "this server ended it through the keeper holding it" : run.endedThrough === "signal" ? "this server signalled it" : run.endedThrough === "editor" ? "this server asked the editor to stop it" : run.exitCode !== null ? `it exited by itself with code ${run.exitCode}` : run.exitSignal === null ? "it is no longer running" : `it was ended by ${run.exitSignal}`;
37267
+ return `No game with the runtime addon is running: the game this server ${which} ended${when}, and ${how}. editor_output has what it printed, and editor_run start runs it again.`;
37268
+ }
37231
37269
  function stopVerdict(stop) {
37232
37270
  const notClosed = stop.notClosedBecause === undefined ? "" : ` It was asked to close first and was not ended by that: ${stop.notClosedBecause}.`;
37233
37271
  if (!stop.wasRunning) {
@@ -37565,7 +37603,7 @@ function exportPresetRefusal(projectPath, preset) {
37565
37603
  try {
37566
37604
  text = readFileSync24(join26(projectPath, "export_presets.cfg"), "utf8");
37567
37605
  } catch {
37568
- return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. The editor writes the file when a preset is added in Project > Export.`;
37606
+ return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
37569
37607
  }
37570
37608
  const names = [];
37571
37609
  let inPreset = false;
@@ -37583,9 +37621,9 @@ function exportPresetRefusal(projectPath, preset) {
37583
37621
  if (names.includes(preset)) {
37584
37622
  return null;
37585
37623
  }
37586
- return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. The editor adds one in Project > Export.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
37624
+ return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
37587
37625
  }
37588
- function exportAnswer(ending, asked, before, after) {
37626
+ function exportAnswer(ending, asked, before, after, beside = []) {
37589
37627
  const { log } = ending;
37590
37628
  const written = after !== null && after !== before;
37591
37629
  const notCopied = librariesNotCopied(log.all);
@@ -37597,6 +37635,7 @@ function exportAnswer(ending, asked, before, after) {
37597
37635
  failure: ending.failure ?? undefined,
37598
37636
  errors: log.count("error"),
37599
37637
  warnings: log.count("warning"),
37638
+ ...beside.length === 0 ? {} : { alsoWritten: beside },
37600
37639
  ...notCopied.length === 0 ? {} : {
37601
37640
  librariesNotLoaded: notCopied,
37602
37641
  extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
@@ -37619,6 +37658,20 @@ function exportAnswer(ending, asked, before, after) {
37619
37658
  isError: true
37620
37659
  };
37621
37660
  }
37661
+ function writtenBeside(before, after, output, within) {
37662
+ return [...after].filter(([name, at]) => name !== output && before.get(name) !== at).map(([name]) => within === "." ? name : `${within}/${name}`).sort();
37663
+ }
37664
+ function filesIn(folder) {
37665
+ const found = new Map;
37666
+ try {
37667
+ for (const entry of readdirSync11(folder, { withFileTypes: true })) {
37668
+ if (entry.isFile()) {
37669
+ found.set(entry.name, statSync12(join26(folder, entry.name)).mtimeMs);
37670
+ }
37671
+ }
37672
+ } catch {}
37673
+ return found;
37674
+ }
37622
37675
  function scanWaitAnswer(scanned, started = "game") {
37623
37676
  if (scanned.waitedMs === 0) {
37624
37677
  return {};
@@ -37856,6 +37909,9 @@ var PATH_SOLUTIONS = [
37856
37909
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
37857
37910
  "Point projectPath at the project the file belongs to"
37858
37911
  ];
37912
+ var OUTPUT_PATH_SOLUTIONS = [
37913
+ 'Give outputPath inside the project, relative to it, such as "build/game.exe" or "res://build/game.exe"; copy the build elsewhere once it is exported'
37914
+ ];
37859
37915
  var PROJECT_FILE_ARGUMENTS = [
37860
37916
  "scenePath",
37861
37917
  "instancePath",
@@ -38213,6 +38269,13 @@ function scriptsToDiagnose(projectPath, given) {
38213
38269
  }
38214
38270
  return { ok: true, value: unique };
38215
38271
  }
38272
+ function engineChecked(why) {
38273
+ return {
38274
+ checkedBy: "engine",
38275
+ languageServer: why,
38276
+ note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, and no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both."
38277
+ };
38278
+ }
38216
38279
  var EXTERNAL_MEMBER = 'Could not resolve external class member "';
38217
38280
  function isDirectory(path) {
38218
38281
  try {
@@ -38910,6 +38973,9 @@ class GodotServer {
38910
38973
  if (tool === "project_import" && op === "set_options") {
38911
38974
  return await this.handleSetImportOptions(args);
38912
38975
  }
38976
+ if (tool === "project_export" && op === "add_preset") {
38977
+ return await this.handleAddExportPreset(args);
38978
+ }
38913
38979
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
38914
38980
  if (headless !== undefined) {
38915
38981
  const { from: asked, ...answerable } = args;
@@ -39973,6 +40039,30 @@ class GodotServer {
39973
40039
  return this.createErrorResponse(`Search failed: ${errorMessage(error)}`);
39974
40040
  }
39975
40041
  }
40042
+ async handleAddExportPreset(args) {
40043
+ const project = this.project(args);
40044
+ if (!project.ok) {
40045
+ return project.response;
40046
+ }
40047
+ const given = readNonEmptyString(args, "outputPath");
40048
+ const output = given === undefined ? null : resolveWithinProject(project.value.path, given);
40049
+ if (output !== null && !output.ok) {
40050
+ return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
40051
+ }
40052
+ const outcome = await this.operation("add_export_preset", {
40053
+ preset: readString(args, "preset") ?? "",
40054
+ platform: readString(args, "platform") ?? "",
40055
+ outputPath: output?.relativePath ?? ""
40056
+ }, project.value.path);
40057
+ if (!outcome.ok || !this.editorHasOpen(project.value.path)) {
40058
+ return this.answer(outcome);
40059
+ }
40060
+ return this.jsonTextResponse({
40061
+ ...outcome.payload,
40062
+ editorNote: "The editor open on this project read export_presets.cfg when it started and does not list this preset, and saving presets in its Export dialog writes its own list over the file. editor_launch restart loads the file; an export through project_export run reads the file and needs no restart.",
40063
+ ...engineExtras(outcome)
40064
+ });
40065
+ }
39976
40066
  async handleExportProject(args) {
39977
40067
  const project = this.project(args);
39978
40068
  if (!project.ok) {
@@ -39981,11 +40071,14 @@ class GodotServer {
39981
40071
  const preset = readNonEmptyString(args, "preset") ?? "";
39982
40072
  const output = resolveWithinProject(project.value.path, readString(args, "outputPath") ?? "");
39983
40073
  if (!output.ok) {
39984
- return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
40074
+ return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
39985
40075
  }
39986
40076
  const noPreset = exportPresetRefusal(project.value.path, preset);
39987
40077
  if (noPreset !== null) {
39988
- return this.createErrorResponse(noPreset, ["project_export list reads the presets the project has"]);
40078
+ return this.createErrorResponse(noPreset, [
40079
+ "project_export list reads the presets the project has",
40080
+ "project_export add_preset adds one"
40081
+ ]);
39989
40082
  }
39990
40083
  const engine = await this.engine();
39991
40084
  if (!engine.ok) {
@@ -40012,6 +40105,7 @@ class GodotServer {
40012
40105
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
40013
40106
  const writtenAt = () => existsSync20(output.absolutePath) ? statSync12(output.absolutePath).mtimeMs : null;
40014
40107
  const before = writtenAt();
40108
+ const listedBefore = filesIn(folder);
40015
40109
  const freed = freeLibraryCopies(project.value.path);
40016
40110
  if (freed.setAside.length > 0 || freed.swept.length > 0) {
40017
40111
  this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
@@ -40025,7 +40119,8 @@ class GodotServer {
40025
40119
  if (ending.exitSignal === null && ending.exitCode === null) {
40026
40120
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
40027
40121
  }
40028
- return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
40122
+ const beside = writtenBeside(listedBefore, filesIn(folder), basename7(output.absolutePath), dirname18(output.relativePath).replaceAll("\\", "/"));
40123
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
40029
40124
  }
40030
40125
  reportTestProgress(log) {
40031
40126
  const progress = callProgress();
@@ -40371,6 +40466,10 @@ class GodotServer {
40371
40466
  }
40372
40467
  if (scripts?.value == null) {
40373
40468
  const one = await this.diagnosticsOf(args);
40469
+ if ("noLanguageServer" in one) {
40470
+ const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
40471
+ return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
40472
+ }
40374
40473
  return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
40375
40474
  }
40376
40475
  const progress = callProgress();
@@ -40380,12 +40479,31 @@ class GodotServer {
40380
40479
  let errors = 0;
40381
40480
  let warnings = 0;
40382
40481
  let unread = 0;
40482
+ let byEngine = null;
40383
40483
  for (const [at, scriptPath] of scripts.value.entries()) {
40384
40484
  if (cancelled?.aborted === true) {
40385
40485
  return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
40386
40486
  }
40387
40487
  progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
40388
40488
  const one = await this.diagnosticsOf({ ...args, scriptPath });
40489
+ if ("noLanguageServer" in one) {
40490
+ const rest = scripts.value.slice(at);
40491
+ progress?.(at + 1, `${rest.length} script(s) left, compiled by a headless engine`);
40492
+ const checked = await this.engineDiagnostics(project.ok ? project.value.path : "", rest, one.noLanguageServer);
40493
+ if ("refusal" in checked) {
40494
+ return checked.refusal;
40495
+ }
40496
+ for (const answer of checked.answers) {
40497
+ errors += Number(answer["errors"]);
40498
+ if (readArray(answer, "diagnostics")?.length === 0) {
40499
+ nothingReported.push(String(answer["scriptPath"]));
40500
+ } else {
40501
+ reported.push(answer);
40502
+ }
40503
+ }
40504
+ byEngine = one.noLanguageServer.reason;
40505
+ break;
40506
+ }
40389
40507
  if ("refusal" in one) {
40390
40508
  if (!one.perScript) {
40391
40509
  return one.refusal;
@@ -40406,12 +40524,68 @@ class GodotServer {
40406
40524
  clean: errors === 0 && unread === 0,
40407
40525
  scriptsChecked: scripts.value.length,
40408
40526
  errors,
40409
- warnings,
40527
+ warnings: byEngine === null ? warnings : null,
40410
40528
  ...unread > 0 ? { scriptsNotRead: unread } : {},
40411
40529
  scripts: reported,
40412
- nothingReported
40530
+ nothingReported,
40531
+ ...byEngine === null ? {} : engineChecked(byEngine)
40413
40532
  }));
40414
40533
  }
40534
+ async compileScripts(projectPath, paths) {
40535
+ const payload = { checked: [], failed: [], scripts: [] };
40536
+ const messages = [];
40537
+ let left = [...paths];
40538
+ while (left.length > 0) {
40539
+ const ran = await this.operation("check_scripts", { paths: left }, projectPath);
40540
+ if (!ran.ok) {
40541
+ return { ok: false, message: ran.message };
40542
+ }
40543
+ const checked = (readArray(ran.payload, "checked") ?? []).map(String);
40544
+ if (checked.length === 0) {
40545
+ return { ok: false, message: `check_scripts read none of ${left.length} script(s)` };
40546
+ }
40547
+ payload.checked.push(...checked);
40548
+ payload.failed.push(...(readArray(ran.payload, "failed") ?? []).map(String));
40549
+ payload.scripts.push(...readArray(ran.payload, "scripts") ?? []);
40550
+ messages.push(...ran.messages);
40551
+ left = left.slice(checked.length);
40552
+ }
40553
+ return { ok: true, payload, extras: engineExtras({ messages }) };
40554
+ }
40555
+ async engineDiagnostics(projectPath, scripts, why) {
40556
+ const named = scripts.map((script) => {
40557
+ const contained = resolveWithinProject(projectPath, script);
40558
+ return contained.ok ? `res://${contained.relativePath}` : script;
40559
+ });
40560
+ const checked = await this.compileScripts(projectPath, named);
40561
+ if (!checked.ok) {
40562
+ return {
40563
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${why.reason.replace(/\.?$/, ".")} No headless engine could compile the scripts instead: ${checked.message}`, why.solutions)
40564
+ };
40565
+ }
40566
+ const failures = new Map((readArray(checked.payload, "scripts") ?? []).map((entry) => [
40567
+ String(asParams(entry)["path"]),
40568
+ asParams(entry)
40569
+ ]));
40570
+ return {
40571
+ answers: scripts.map((scriptPath, at) => {
40572
+ const failure = failures.get(named[at] ?? "");
40573
+ const diagnostics = (readArray(failure ?? {}, "errors") ?? []).map((entry) => {
40574
+ const { line, message } = asParams(entry);
40575
+ return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
40576
+ });
40577
+ const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
40578
+ return {
40579
+ scriptPath,
40580
+ clean: diagnostics.length === 0,
40581
+ errors: diagnostics.length,
40582
+ warnings: null,
40583
+ diagnostics,
40584
+ ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
40585
+ };
40586
+ })
40587
+ };
40588
+ }
40415
40589
  markedIfStale(answer) {
40416
40590
  return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
40417
40591
  }
@@ -40423,6 +40597,16 @@ class GodotServer {
40423
40597
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
40424
40598
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
40425
40599
  const refused = this.languageServerRefusal(payload, "Diagnostics");
40600
+ const project = this.project(args);
40601
+ const ownEditor = project.ok && this.editorHasOpen(project.value.path);
40602
+ if (refused !== null && !refused.perScript && payload["stillReading"] !== true && !ownEditor) {
40603
+ return {
40604
+ noLanguageServer: {
40605
+ reason: typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]),
40606
+ solutions: this.languageServerSolutions(payload)
40607
+ }
40608
+ };
40609
+ }
40426
40610
  if (refused !== null) {
40427
40611
  return refused;
40428
40612
  }
@@ -40582,24 +40766,24 @@ class GodotServer {
40582
40766
  if (payload["stillReading"] === true) {
40583
40767
  return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
40584
40768
  }
40585
- if (typeof payload["serves"] === "string") {
40586
- return {
40587
- refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
40588
- "editor_status names the editor on the bridge and the port it says it serves",
40589
- "editor_launch opens this project's own editor, on a language server port of its own",
40590
- "GDHARNESS_LSP_PORT points this server at the right one"
40591
- ]),
40592
- perScript: false
40593
- };
40594
- }
40595
40769
  return {
40596
- refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
40597
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
40598
- "GDHARNESS_LSP_PORT points this server at another one"
40599
- ]),
40600
- perScript: payload["aboutThisFile"] === true
40770
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, this.languageServerSolutions(payload)),
40771
+ perScript: typeof payload["serves"] !== "string" && payload["aboutThisFile"] === true
40601
40772
  };
40602
40773
  }
40774
+ languageServerSolutions(payload) {
40775
+ if (typeof payload["serves"] === "string") {
40776
+ return [
40777
+ ...this.godotBridge.getStatus().connected ? ["editor_status names the editor on the bridge and the port it says it serves"] : [],
40778
+ "editor_launch opens this project's own editor, on a language server port of its own",
40779
+ "GDHARNESS_LSP_PORT points this server at the right one"
40780
+ ];
40781
+ }
40782
+ return [
40783
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
40784
+ "GDHARNESS_LSP_PORT points this server at another one"
40785
+ ];
40786
+ }
40603
40787
  async lsp() {
40604
40788
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
40605
40789
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -43653,9 +43837,9 @@ class GodotServer {
43653
43837
  ok &&= rebuilt.ok;
43654
43838
  }
43655
43839
  if (changed.length > 0) {
43656
- const compiled = await this.operation("check_scripts", { paths: [...changed] }, projectPath);
43657
- report["compiled"] = compiled.ok ? { ...compiled.payload, ...engineExtras(compiled) } : { problem: compiled.message };
43658
- ok &&= compiled.ok && (readArray(compiled.payload, "failed") ?? []).length === 0;
43840
+ const compiled = await this.compileScripts(projectPath, [...changed]);
43841
+ report["compiled"] = compiled.ok ? { ...compiled.payload, ...compiled.extras } : { problem: compiled.message };
43842
+ ok &&= compiled.ok && compiled.payload.failed.length === 0;
43659
43843
  }
43660
43844
  return { ok, report };
43661
43845
  }
@@ -43921,6 +44105,11 @@ class GodotServer {
43921
44105
  const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
43922
44106
  const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
43923
44107
  if ("problem" in choice) {
44108
+ const ended = this.activeProcess;
44109
+ const nothingAnnounced = typeof pid !== "number" && announced.unspoken.length === 0 && !announced.running.some((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
44110
+ if (nothingAnnounced && ended !== null && !stillRunning(ended) && (wanted === undefined || ended.projectPath === null || isSameDirectory(ended.projectPath, wanted))) {
44111
+ return this.createErrorResponse(ourRunEnded({ ...ended, pid: ended.pid ?? ended.announcedPid ?? null }, Date.now()));
44112
+ }
43924
44113
  return this.createErrorResponse(choice.problem);
43925
44114
  }
43926
44115
  const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
@@ -44086,7 +44275,18 @@ ${staleNote}`}`
44086
44275
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
44087
44276
  return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
44088
44277
  }
44278
+ if (op === "watch") {
44279
+ return await this.handleRuntimeCommand("watch_signal", {
44280
+ ...whichGame(args),
44281
+ path: readNonEmptyString(args, "nodePath") ?? "",
44282
+ signal: readString(args, "signal") ?? ""
44283
+ }, { tool: "runtime_wait", op });
44284
+ }
44089
44285
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
44286
+ const watch = readPositiveNumber(args, "watch");
44287
+ if (op === "signal" && watch === undefined && (readNonEmptyString(args, "nodePath") === undefined || readNonEmptyString(args, "signal") === undefined)) {
44288
+ return this.createErrorResponse("runtime_wait signal needs nodePath and signal, or the watch an earlier runtime_wait watch answered.");
44289
+ }
44090
44290
  const asked = readNonEmptyString(args, "nodePath");
44091
44291
  if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
44092
44292
  return this.createErrorResponse("runtime_wait until needs nodePath to wait for a property: the property belongs to that node.", ["With says instead of property, nodePath can be left out and the whole game is searched"]);
@@ -44097,7 +44297,8 @@ ${staleNote}`}`
44097
44297
  ...whichGame(args),
44098
44298
  path: nodePath,
44099
44299
  signal: readString(args, "signal") ?? "",
44100
- timeout_ms: timeoutMs
44300
+ timeout_ms: timeoutMs,
44301
+ ...watch === undefined ? {} : { watch }
44101
44302
  }, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
44102
44303
  ...whichGame(args),
44103
44304
  path: nodePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.34",
3
+ "version": "1.2.36",
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",