gdharness 1.2.35 → 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/cli.js CHANGED
@@ -11483,7 +11483,7 @@ var init_tool_definitions = __esm(() => {
11483
11483
  direction: {
11484
11484
  type: "string",
11485
11485
  enum: ["forward", "reverse"],
11486
- 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."
11486
+ 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."
11487
11487
  },
11488
11488
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
11489
11489
  includeBuiltin: {
@@ -11960,7 +11960,7 @@ var init_tool_definitions = __esm(() => {
11960
11960
  operations: {
11961
11961
  structure: {
11962
11962
  effect: "read",
11963
- summary: "functions, variables, signals, class_name and extends, read from the file",
11963
+ 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",
11964
11964
  requires: []
11965
11965
  },
11966
11966
  symbols: {
@@ -11981,7 +11981,7 @@ var init_tool_definitions = __esm(() => {
11981
11981
  name: "script_diagnostics",
11982
11982
  title: "Script diagnostics",
11983
11983
  effect: "read",
11984
- 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.",
11984
+ 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.",
11985
11985
  parameters: {
11986
11986
  projectPath: PROJECT_PATH,
11987
11987
  scriptPath: {
@@ -12439,7 +12439,7 @@ var init_tool_definitions = __esm(() => {
12439
12439
  says: {
12440
12440
  type: "string",
12441
12441
  ops: ["find", "accessibility"],
12442
- 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.'
12442
+ 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.'
12443
12443
  },
12444
12444
  limit: {
12445
12445
  type: "integer",
@@ -12470,7 +12470,7 @@ var init_tool_definitions = __esm(() => {
12470
12470
  tree: { effect: "read", summary: "the live scene tree", requires: [] },
12471
12471
  text: {
12472
12472
  effect: "read",
12473
- 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",
12473
+ 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",
12474
12474
  requires: []
12475
12475
  },
12476
12476
  find: {
@@ -12490,12 +12490,12 @@ var init_tool_definitions = __esm(() => {
12490
12490
  },
12491
12491
  metrics: {
12492
12492
  effect: "read",
12493
- 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",
12493
+ 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",
12494
12494
  requires: []
12495
12495
  },
12496
12496
  accessibility: {
12497
12497
  effect: "read",
12498
- 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",
12498
+ 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`,
12499
12499
  requires: []
12500
12500
  }
12501
12501
  },
@@ -12504,7 +12504,7 @@ var init_tool_definitions = __esm(() => {
12504
12504
  {
12505
12505
  name: "runtime_invoke",
12506
12506
  title: "Set or call in the running game",
12507
- 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.",
12507
+ 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.",
12508
12508
  parameters: {
12509
12509
  projectPath: RUNNING_PROJECT_PATH,
12510
12510
  pid: RUNNING_PID,
@@ -12624,7 +12624,7 @@ var init_tool_definitions = __esm(() => {
12624
12624
  {
12625
12625
  name: "runtime_input",
12626
12626
  title: "Send input to the game",
12627
- 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.",
12627
+ 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.",
12628
12628
  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.',
12629
12629
  parameters: {
12630
12630
  projectPath: RUNNING_PROJECT_PATH,
@@ -12821,10 +12821,16 @@ var init_tool_definitions = __esm(() => {
12821
12821
  },
12822
12822
  nodePath: {
12823
12823
  type: "string",
12824
- ops: ["signal", "until"],
12825
- description: "signal, until: the node. Needed for a signal and for a property; for says it defaults to /root, the whole screen."
12824
+ ops: ["signal", "watch", "until"],
12825
+ 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."
12826
+ },
12827
+ signal: { type: "string", ops: ["signal", "watch"], description: "signal, watch: the signal name." },
12828
+ watch: {
12829
+ type: "integer",
12830
+ minimum: 1,
12831
+ ops: ["signal"],
12832
+ 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."
12826
12833
  },
12827
- signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
12828
12834
  property: {
12829
12835
  type: "string",
12830
12836
  ops: ["until"],
@@ -12864,6 +12870,11 @@ var init_tool_definitions = __esm(() => {
12864
12870
  signal: {
12865
12871
  effect: "read",
12866
12872
  summary: "wait for a signal and answer with what it carried",
12873
+ requires: []
12874
+ },
12875
+ watch: {
12876
+ effect: "read",
12877
+ 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",
12867
12878
  requires: ["nodePath", "signal"]
12868
12879
  },
12869
12880
  until: {
@@ -44595,6 +44606,12 @@ function endedWithoutACode(run) {
44595
44606
  }
44596
44607
  return `The editor stopped playing this run. ${kept} What it printed is below.`;
44597
44608
  }
44609
+ function ourRunEnded(run, now) {
44610
+ const which = `${run.throughEditor ? "played in the editor" : "started"}${run.pid === null ? "" : ` (pid ${run.pid})`}`;
44611
+ const when = run.endedAt === undefined ? "" : ` ${Math.max(0, Math.round((now - run.endedAt) / 1000))}s ago`;
44612
+ 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}`;
44613
+ 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.`;
44614
+ }
44598
44615
  function stopVerdict(stop) {
44599
44616
  const notClosed = stop.notClosedBecause === undefined ? "" : ` It was asked to close first and was not ended by that: ${stop.notClosedBecause}.`;
44600
44617
  if (!stop.wasRunning) {
@@ -45521,6 +45538,13 @@ function scriptsToDiagnose(projectPath, given) {
45521
45538
  }
45522
45539
  return { ok: true, value: unique };
45523
45540
  }
45541
+ function engineChecked(why) {
45542
+ return {
45543
+ checkedBy: "engine",
45544
+ languageServer: why,
45545
+ 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."
45546
+ };
45547
+ }
45524
45548
  function isDirectory(path) {
45525
45549
  try {
45526
45550
  return statSync13(path).isDirectory();
@@ -47709,6 +47733,10 @@ class GodotServer {
47709
47733
  }
47710
47734
  if (scripts?.value == null) {
47711
47735
  const one = await this.diagnosticsOf(args);
47736
+ if ("noLanguageServer" in one) {
47737
+ const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
47738
+ return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
47739
+ }
47712
47740
  return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
47713
47741
  }
47714
47742
  const progress = callProgress();
@@ -47718,12 +47746,31 @@ class GodotServer {
47718
47746
  let errors = 0;
47719
47747
  let warnings = 0;
47720
47748
  let unread = 0;
47749
+ let byEngine = null;
47721
47750
  for (const [at, scriptPath] of scripts.value.entries()) {
47722
47751
  if (cancelled?.aborted === true) {
47723
47752
  return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
47724
47753
  }
47725
47754
  progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
47726
47755
  const one = await this.diagnosticsOf({ ...args, scriptPath });
47756
+ if ("noLanguageServer" in one) {
47757
+ const rest = scripts.value.slice(at);
47758
+ progress?.(at + 1, `${rest.length} script(s) left, compiled by a headless engine`);
47759
+ const checked = await this.engineDiagnostics(project.ok ? project.value.path : "", rest, one.noLanguageServer);
47760
+ if ("refusal" in checked) {
47761
+ return checked.refusal;
47762
+ }
47763
+ for (const answer of checked.answers) {
47764
+ errors += Number(answer["errors"]);
47765
+ if (readArray(answer, "diagnostics")?.length === 0) {
47766
+ nothingReported.push(String(answer["scriptPath"]));
47767
+ } else {
47768
+ reported.push(answer);
47769
+ }
47770
+ }
47771
+ byEngine = one.noLanguageServer.reason;
47772
+ break;
47773
+ }
47727
47774
  if ("refusal" in one) {
47728
47775
  if (!one.perScript) {
47729
47776
  return one.refusal;
@@ -47744,12 +47791,68 @@ class GodotServer {
47744
47791
  clean: errors === 0 && unread === 0,
47745
47792
  scriptsChecked: scripts.value.length,
47746
47793
  errors,
47747
- warnings,
47794
+ warnings: byEngine === null ? warnings : null,
47748
47795
  ...unread > 0 ? { scriptsNotRead: unread } : {},
47749
47796
  scripts: reported,
47750
- nothingReported
47797
+ nothingReported,
47798
+ ...byEngine === null ? {} : engineChecked(byEngine)
47751
47799
  }));
47752
47800
  }
47801
+ async compileScripts(projectPath, paths) {
47802
+ const payload = { checked: [], failed: [], scripts: [] };
47803
+ const messages = [];
47804
+ let left = [...paths];
47805
+ while (left.length > 0) {
47806
+ const ran = await this.operation("check_scripts", { paths: left }, projectPath);
47807
+ if (!ran.ok) {
47808
+ return { ok: false, message: ran.message };
47809
+ }
47810
+ const checked = (readArray(ran.payload, "checked") ?? []).map(String);
47811
+ if (checked.length === 0) {
47812
+ return { ok: false, message: `check_scripts read none of ${left.length} script(s)` };
47813
+ }
47814
+ payload.checked.push(...checked);
47815
+ payload.failed.push(...(readArray(ran.payload, "failed") ?? []).map(String));
47816
+ payload.scripts.push(...readArray(ran.payload, "scripts") ?? []);
47817
+ messages.push(...ran.messages);
47818
+ left = left.slice(checked.length);
47819
+ }
47820
+ return { ok: true, payload, extras: engineExtras({ messages }) };
47821
+ }
47822
+ async engineDiagnostics(projectPath, scripts, why) {
47823
+ const named = scripts.map((script) => {
47824
+ const contained = resolveWithinProject(projectPath, script);
47825
+ return contained.ok ? `res://${contained.relativePath}` : script;
47826
+ });
47827
+ const checked = await this.compileScripts(projectPath, named);
47828
+ if (!checked.ok) {
47829
+ return {
47830
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${why.reason.replace(/\.?$/, ".")} No headless engine could compile the scripts instead: ${checked.message}`, why.solutions)
47831
+ };
47832
+ }
47833
+ const failures = new Map((readArray(checked.payload, "scripts") ?? []).map((entry) => [
47834
+ String(asParams(entry)["path"]),
47835
+ asParams(entry)
47836
+ ]));
47837
+ return {
47838
+ answers: scripts.map((scriptPath, at) => {
47839
+ const failure = failures.get(named[at] ?? "");
47840
+ const diagnostics = (readArray(failure ?? {}, "errors") ?? []).map((entry) => {
47841
+ const { line, message } = asParams(entry);
47842
+ return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
47843
+ });
47844
+ const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
47845
+ return {
47846
+ scriptPath,
47847
+ clean: diagnostics.length === 0,
47848
+ errors: diagnostics.length,
47849
+ warnings: null,
47850
+ diagnostics,
47851
+ ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
47852
+ };
47853
+ })
47854
+ };
47855
+ }
47753
47856
  markedIfStale(answer) {
47754
47857
  return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
47755
47858
  }
@@ -47761,6 +47864,16 @@ class GodotServer {
47761
47864
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
47762
47865
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
47763
47866
  const refused = this.languageServerRefusal(payload, "Diagnostics");
47867
+ const project = this.project(args);
47868
+ const ownEditor = project.ok && this.editorHasOpen(project.value.path);
47869
+ if (refused !== null && !refused.perScript && payload["stillReading"] !== true && !ownEditor) {
47870
+ return {
47871
+ noLanguageServer: {
47872
+ reason: typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]),
47873
+ solutions: this.languageServerSolutions(payload)
47874
+ }
47875
+ };
47876
+ }
47764
47877
  if (refused !== null) {
47765
47878
  return refused;
47766
47879
  }
@@ -47920,24 +48033,24 @@ class GodotServer {
47920
48033
  if (payload["stillReading"] === true) {
47921
48034
  return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
47922
48035
  }
47923
- if (typeof payload["serves"] === "string") {
47924
- return {
47925
- refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
47926
- "editor_status names the editor on the bridge and the port it says it serves",
47927
- "editor_launch opens this project's own editor, on a language server port of its own",
47928
- "GDHARNESS_LSP_PORT points this server at the right one"
47929
- ]),
47930
- perScript: false
47931
- };
47932
- }
47933
48036
  return {
47934
- refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
47935
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
47936
- "GDHARNESS_LSP_PORT points this server at another one"
47937
- ]),
47938
- perScript: payload["aboutThisFile"] === true
48037
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, this.languageServerSolutions(payload)),
48038
+ perScript: typeof payload["serves"] !== "string" && payload["aboutThisFile"] === true
47939
48039
  };
47940
48040
  }
48041
+ languageServerSolutions(payload) {
48042
+ if (typeof payload["serves"] === "string") {
48043
+ return [
48044
+ ...this.godotBridge.getStatus().connected ? ["editor_status names the editor on the bridge and the port it says it serves"] : [],
48045
+ "editor_launch opens this project's own editor, on a language server port of its own",
48046
+ "GDHARNESS_LSP_PORT points this server at the right one"
48047
+ ];
48048
+ }
48049
+ return [
48050
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
48051
+ "GDHARNESS_LSP_PORT points this server at another one"
48052
+ ];
48053
+ }
47941
48054
  async lsp() {
47942
48055
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
47943
48056
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -50991,9 +51104,9 @@ class GodotServer {
50991
51104
  ok &&= rebuilt.ok;
50992
51105
  }
50993
51106
  if (changed.length > 0) {
50994
- const compiled = await this.operation("check_scripts", { paths: [...changed] }, projectPath);
50995
- report["compiled"] = compiled.ok ? { ...compiled.payload, ...engineExtras(compiled) } : { problem: compiled.message };
50996
- ok &&= compiled.ok && (readArray(compiled.payload, "failed") ?? []).length === 0;
51107
+ const compiled = await this.compileScripts(projectPath, [...changed]);
51108
+ report["compiled"] = compiled.ok ? { ...compiled.payload, ...compiled.extras } : { problem: compiled.message };
51109
+ ok &&= compiled.ok && compiled.payload.failed.length === 0;
50997
51110
  }
50998
51111
  return { ok, report };
50999
51112
  }
@@ -51259,6 +51372,11 @@ class GodotServer {
51259
51372
  const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
51260
51373
  const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
51261
51374
  if ("problem" in choice) {
51375
+ const ended = this.activeProcess;
51376
+ const nothingAnnounced = typeof pid !== "number" && announced.unspoken.length === 0 && !announced.running.some((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
51377
+ if (nothingAnnounced && ended !== null && !stillRunning(ended) && (wanted === undefined || ended.projectPath === null || isSameDirectory(ended.projectPath, wanted))) {
51378
+ return this.createErrorResponse(ourRunEnded({ ...ended, pid: ended.pid ?? ended.announcedPid ?? null }, Date.now()));
51379
+ }
51262
51380
  return this.createErrorResponse(choice.problem);
51263
51381
  }
51264
51382
  const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
@@ -51424,7 +51542,18 @@ ${staleNote}`}`
51424
51542
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
51425
51543
  return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
51426
51544
  }
51545
+ if (op === "watch") {
51546
+ return await this.handleRuntimeCommand("watch_signal", {
51547
+ ...whichGame(args),
51548
+ path: readNonEmptyString(args, "nodePath") ?? "",
51549
+ signal: readString(args, "signal") ?? ""
51550
+ }, { tool: "runtime_wait", op });
51551
+ }
51427
51552
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
51553
+ const watch = readPositiveNumber(args, "watch");
51554
+ if (op === "signal" && watch === undefined && (readNonEmptyString(args, "nodePath") === undefined || readNonEmptyString(args, "signal") === undefined)) {
51555
+ return this.createErrorResponse("runtime_wait signal needs nodePath and signal, or the watch an earlier runtime_wait watch answered.");
51556
+ }
51428
51557
  const asked = readNonEmptyString(args, "nodePath");
51429
51558
  if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
51430
51559
  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"]);
@@ -51435,7 +51564,8 @@ ${staleNote}`}`
51435
51564
  ...whichGame(args),
51436
51565
  path: nodePath,
51437
51566
  signal: readString(args, "signal") ?? "",
51438
- timeout_ms: timeoutMs
51567
+ timeout_ms: timeoutMs,
51568
+ ...watch === undefined ? {} : { watch }
51439
51569
  }, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
51440
51570
  ...whichGame(args),
51441
51571
  path: nodePath,
@@ -52671,7 +52801,9 @@ so take it off when the question it was for is answered.
52671
52801
  \`resource_edit read\` for a resource: both read the file through the editor when one serves the
52672
52802
  project and through a headless engine when none does, with the same answer. \`scene_node\`,
52673
52803
  \`scene_signal\` and the other \`resource_edit\` ops but \`move\` change it through the editor, so the
52674
- files keep their formatting. \`script_diagnostics\` and \`script_info\` come from the editor's own language server.
52804
+ files keep their formatting. \`script_info\` comes from the editor's own language server, and so does
52805
+ \`script_diagnostics\` when this project's editor is open; without it a headless engine compiles the
52806
+ scripts and reports their errors, with lines but no columns and no warnings.
52675
52807
 
52676
52808
  \`project_info\`, \`project_dependencies\`, \`project_settings\`, \`project_import\`,
52677
52809
  \`project_export\`, \`project_test\`, \`script_edit\`, \`script_info structure\` and
@@ -12,8 +12,10 @@ const REFUSED: String = "Error calling method from 'callv': "
12
12
 
13
13
  ## The engine's reason for refusing the call, without [constant REFUSED], or "" when it ran.
14
14
  var refused: String = ""
15
- ## What was reported while it ran, a line each, warnings marked as such.
15
+ ## The errors reported while it ran, a line each.
16
16
  var raised: Array[String] = []
17
+ ## The warnings reported while it ran, a line each.
18
+ var warned: Array[String] = []
17
19
 
18
20
 
19
21
  func _log_message(_message: String, _error: bool) -> void:
@@ -39,4 +41,7 @@ func _log_error(
39
41
  if function == "callv" and text.begins_with(REFUSED) and refused.is_empty():
40
42
  refused = text.trim_prefix(REFUSED)
41
43
  return
42
- raised.append(("WARNING: " if error_type == ERROR_TYPE_WARNING else "") + text)
44
+ if error_type == ERROR_TYPE_WARNING:
45
+ warned.append(text)
46
+ else:
47
+ raised.append(text)
@@ -108,6 +108,9 @@ try {
108
108
  }
109
109
  $p = Patterned $element 'RangeValue'
110
110
  if ($p) {
111
+ # A reader speaks the Value pattern's words, such as "75%" for a slider whose game sets them,
112
+ # and the number would otherwise replace them.
113
+ if ($node.Contains('value') -and "$($node.value)") { $node.value_text = $node.value }
111
114
  $node.value = $p.Current.Value
112
115
  $node.range = @($p.Current.Minimum, $p.Current.Maximum)
113
116
  if (-not $p.Current.IsReadOnly) { $actions += 'set_value' }
@@ -389,17 +392,18 @@ static func _narrow(result: Dictionary, params: Dictionary) -> void:
389
392
 
390
393
 
391
394
  ## Whether [param element] is made by the node at [param under] or one below it, and says
392
- ## [param said], each where asked: words as find matches them, in the element's name or value.
395
+ ## [param said], each where asked: words as find matches them, in the element's name, value or
396
+ ## value_text.
393
397
  static func _matches(element: Dictionary, under: String, said: String) -> bool:
394
398
  if not under.is_empty():
395
399
  var path: String = str(element.get("node", element.get("part_of", "")))
396
400
  if path != under and not path.begins_with(under + "/"):
397
401
  return false
398
402
  if not said.is_empty():
399
- var name: String = str(element.get("name", ""))
400
- var value: String = str(element.get("value", ""))
401
- if not Says.matches(name, said) and not Says.matches(value, said):
402
- return false
403
+ for field: String in ["name", "value", "value_text"]:
404
+ if element.has(field) and Says.matches(str(element[field]), said):
405
+ return true
406
+ return false
403
407
  return true
404
408
 
405
409
 
@@ -18,6 +18,7 @@ const InputCommands = preload("runtime_input.gd")
18
18
  const Pad = preload("runtime_pad.gd")
19
19
  const Queries = preload("runtime_queries.gd")
20
20
  const Read = preload("reading.gd")
21
+ const Targets = preload("runtime_targets.gd")
21
22
  const Typing = preload("runtime_typing.gd")
22
23
  const Values = preload("runtime_values.gd")
23
24
  const Waits = preload("runtime_waits.gd")
@@ -153,6 +154,7 @@ func _init() -> void:
153
154
  "choose": _typing.choose,
154
155
  "wait_frames": _waits.wait_frames,
155
156
  "wait_signal": _waits.wait_signal,
157
+ "watch_signal": _waits.watch_signal,
156
158
  "wait_until": _waits.wait_until,
157
159
  "send_profile": _send_profile_now,
158
160
  "close_game": _close_game,
@@ -160,6 +162,36 @@ func _init() -> void:
160
162
  _wait_for_the_profiler()
161
163
 
162
164
 
165
+ ## Gives a game with no window the size the project opens its window at. Measured: a
166
+ ## headless engine's root viewport is 64 by 64 whatever the project settings or --resolution say, so
167
+ ## a headless game laid its interface out in 64 pixels, and a button every windowed run shows was
168
+ ## refused as off screen. The root takes a size set once the autoloads are in the tree, before the
169
+ ## main scene enters it, and the scene lays out at it from its first frame. A size set at the
170
+ ## autoload's construction does not hold: the root is a 100 by 100 placeholder then, and the engine
171
+ ## gives it the 64 by 64 afterwards. A script run is not a game and keeps the viewport its own code
172
+ ## gives it, and a release build is left as the engine made it.
173
+ func _size_a_windowless_game() -> void:
174
+ if DisplayServer.get_name() != "headless" or _script_run() or not OS.is_debug_build():
175
+ return
176
+ var root: Window = get_tree().root
177
+ if Vector2(root.size) != Targets.HEADLESS_VIEWPORT:
178
+ return
179
+ root.size = Vector2i(_window_dimension("width", 1152), _window_dimension("height", 648))
180
+
181
+
182
+ ## The window's [param axis] as the project opens it: the override when one is set, otherwise the
183
+ ## viewport's size, whose engine default is [param fallback].
184
+ static func _window_dimension(axis: String, fallback: int) -> int:
185
+ var override: int = Read.as_int(
186
+ ProjectSettings.get_setting("display/window/size/window_%s_override" % axis, 0)
187
+ )
188
+ if override > 0:
189
+ return override
190
+ return Read.as_int(
191
+ ProjectSettings.get_setting("display/window/size/viewport_%s" % axis, fallback), fallback
192
+ )
193
+
194
+
163
195
  ## Holds the game's start until the profiler is on, when whatever started the game asked for a
164
196
  ## profile from its start. The debugger switches the profiler on with a message the game reads only
165
197
  ## on a poll, which comes every [constant POLLS_PER_READ] lines of script, so the main scene's _ready
@@ -197,6 +229,7 @@ func _ready() -> void:
197
229
  # even be un-paused over the socket. A debug server has to stay responsive while the game
198
230
  # is frozen, to inspect, capture, inject or resume it.
199
231
  process_mode = Node.PROCESS_MODE_ALWAYS
232
+ _size_a_windowless_game()
200
233
  var watched: int = get_tree().root.tree_exiting.connect(_send_the_profile)
201
234
  if watched != OK:
202
235
  push_error("gdharness could not watch for the game quitting: %d" % watched)
@@ -441,16 +441,24 @@ func call_method(params: Dictionary) -> Dictionary:
441
441
  "result": _values.serialize(result),
442
442
  "elapsed_usec": elapsed
443
443
  }
444
- if not caught.raised.is_empty():
445
- answer["errors"] = caught.raised.slice(0, ERRORS_KEPT)
446
- if caught.raised.size() > ERRORS_KEPT:
447
- answer["errors_not_shown"] = caught.raised.size() - ERRORS_KEPT
444
+ _keep_reported(answer, "errors", caught.raised)
445
+ _keep_reported(answer, "warnings", caught.warned)
448
446
  var asked: Variant = params.get("properties")
449
447
  if _reads_the_result(asked):
450
448
  answer.merge(_read_off_result(result, asked))
451
449
  return answer
452
450
 
453
451
 
452
+ ## Puts the first [constant ERRORS_KEPT] of [param reported] in [param answer] under [param field],
453
+ ## with how many more there were, when anything was reported.
454
+ static func _keep_reported(answer: Dictionary, field: String, reported: Array[String]) -> void:
455
+ if reported.is_empty():
456
+ return
457
+ answer[field] = reported.slice(0, ERRORS_KEPT)
458
+ if reported.size() > ERRORS_KEPT:
459
+ answer[field + "_not_shown"] = reported.size() - ERRORS_KEPT
460
+
461
+
454
462
  ## Why [param named], the last step of a call's method, names no method of [param holder] to call, or
455
463
  ## "" when it does. Brackets on it holding arguments are refused rather than read: the arguments are
456
464
  ## taken from args alone, so a call is never made with half of them in one place and half in the
@@ -412,17 +412,19 @@ static func _outside_note(outside: int, root: Node) -> String:
412
412
  ## headless game's button twenty pixels in from the corner was refused as off screen, and the
413
413
  ## refusal read as though the button were not there.
414
414
  ##
415
- ## The size is only claimed where it is true: the rect is printed beside this, so naming 64 by 64
416
- ## over a viewport somebody has resized says two different things in one sentence.
415
+ ## The size named is the one in force, which the rect printed beside it agrees with. A game the
416
+ ## runtime started headless has the size the project opens its window at, and a window of that size
417
+ ## would not reach the control either, so only the engine's own 64 by 64 is answered with a window.
417
418
  static func no_window_note(viewport: Viewport, root: Window) -> String:
418
419
  if viewport != root or root.can_draw():
419
420
  return ""
420
- if viewport.get_visible_rect().size == HEADLESS_VIEWPORT:
421
+ var size: Vector2 = viewport.get_visible_rect().size
422
+ if size == HEADLESS_VIEWPORT:
421
423
  return (
422
424
  ". This game has no window, and a game with no window has a 64 by 64 viewport whatever "
423
425
  + "the project settings say: run it with a window to reach this control"
424
426
  )
425
- return ". This game has no window: run it with a window to reach this control"
427
+ return ". This game has no window, and its viewport is %d by %d" % [roundi(size.x), roundi(size.y)]
426
428
 
427
429
 
428
430
  ## What to add to a refusal that found nothing on screen, when the controls saying it are covered.
@@ -771,7 +773,7 @@ static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[Scrol
771
773
  ## Whether a click could not reach [param target] however it scrolled: its centre, where the click
772
774
  ## would bring it, outside its viewport or the one showing that on the screen, or clipped away by a
773
775
  ## container the click does not scroll. A drawer parked off the side of the screen, or anything
774
- ## outside the 64 by 64 viewport of a game with no window, was listed among the controls on screen.
776
+ ## outside the viewport of a game with no window, was listed among the controls on screen.
775
777
  static func _off_screen(target: Control) -> bool:
776
778
  var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
777
779
  var point: Vector2 = _centre_of(target) + _moved_by(target, moves)