gdharness 1.3.40 → 1.3.42

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.
Files changed (32) hide show
  1. package/build/cli.js +343 -142
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +36 -1
  4. package/build/godot/addons/gdharness_editor/file_walk.gd +33 -1
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
  6. package/build/godot/addons/gdharness_editor/property_values.gd +6 -9
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +27 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -2
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -2
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +15 -0
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +7 -2
  12. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +59 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +75 -11
  15. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +77 -22
  16. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -1
  17. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +4 -3
  18. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +15 -8
  21. package/build/godot/operations/animation_reads.gd +31 -4
  22. package/build/godot/operations/dependencies.gd +5 -2
  23. package/build/godot/operations/file_reads.gd +36 -1
  24. package/build/godot/operations/file_walk.gd +33 -1
  25. package/build/godot/operations/gdscript_analysis.gd +19 -1
  26. package/build/godot/operations/gdscript_authoring.gd +65 -4
  27. package/build/godot/operations/import_pipeline.gd +1 -1
  28. package/build/godot/operations/input_actions.gd +1 -0
  29. package/build/godot/operations/plugins.gd +39 -1
  30. package/build/godot/operations/project_config.gd +104 -1
  31. package/build/godot/operations/property_values.gd +6 -9
  32. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -10478,7 +10478,12 @@ function ignoredLeftOut(report, ignored, declared, rowsIgnored = new Set) {
10478
10478
  function caseOf(suite, element) {
10479
10479
  const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
10480
10480
  const detail = child.text.trim();
10481
- return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
10481
+ const message = child.attributes["message"] ?? null;
10482
+ return {
10483
+ kind,
10484
+ message: kind === "error" ? raisedAt(message, detail) : message,
10485
+ detail: detail === "" ? null : detail
10486
+ };
10482
10487
  });
10483
10488
  const reports = [...of("failure"), ...of("error"), ...of("skipped")];
10484
10489
  const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
@@ -10492,6 +10497,17 @@ function caseOf(suite, element) {
10492
10497
  reports
10493
10498
  };
10494
10499
  }
10500
+ function raisedAt(message, detail) {
10501
+ if (message === null) {
10502
+ return null;
10503
+ }
10504
+ const named = /^ERROR: \S+:\d+(?=\s|$)/.exec(message);
10505
+ const raised = /^\s*at '[^']*' in (\S+):(\d+)\s*$/m.exec(detail);
10506
+ if (named === null || raised === null) {
10507
+ return message;
10508
+ }
10509
+ return `ERROR: ${raised[1] ?? ""}:${raised[2] ?? ""}${message.slice(named[0].length)}`;
10510
+ }
10495
10511
  function failedAnAssertion(entry) {
10496
10512
  return entry.reports.some((report) => report.kind === "failure");
10497
10513
  }
@@ -12910,7 +12926,7 @@ var init_doc_cache = __esm(() => {
12910
12926
 
12911
12927
  // src/project-scan.ts
12912
12928
  import { existsSync as existsSync9, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync9 } from "node:fs";
12913
- import { basename as basename5, extname, join as join11 } from "node:path";
12929
+ import { basename as basename5, extname, join as join11, relative as relative4, sep as sep3 } from "node:path";
12914
12930
  function skipped(name) {
12915
12931
  return name.startsWith(".") || SKIPPED.has(name);
12916
12932
  }
@@ -13070,6 +13086,20 @@ function pathsInUidCache(projectPath) {
13070
13086
  }
13071
13087
  return new Set(paths.values());
13072
13088
  }
13089
+ function steppedOverAround(projectPath, place) {
13090
+ let at = projectPath;
13091
+ for (const step of relative4(projectPath, place).split(sep3).filter((part) => part !== "")) {
13092
+ at = join11(at, step);
13093
+ if (!statSync9(at).isDirectory()) {
13094
+ break;
13095
+ }
13096
+ const why = skipped(step) ? step.startsWith(".") ? "its name starts with a dot" : `it is ${step}` : gdignored(at) ? "it holds a .gdignore" : installedByUs(at) ? "gdharness installed it" : null;
13097
+ if (why !== null) {
13098
+ return { folder: `res://${relative4(projectPath, at).split(sep3).join("/")}/`, why };
13099
+ }
13100
+ }
13101
+ return null;
13102
+ }
13073
13103
  function isBinary(bytes) {
13074
13104
  return bytes.subarray(0, 8000).includes(0);
13075
13105
  }
@@ -13195,8 +13225,8 @@ function searchProject(projectPath, options) {
13195
13225
  result.summary.files_with_matches += 1;
13196
13226
  }
13197
13227
  };
13198
- const visit = (directory) => {
13199
- if (steppedOver(directory)) {
13228
+ const visit = (directory, named = false) => {
13229
+ if (!named && steppedOver(directory)) {
13200
13230
  if (gdignored(directory)) {
13201
13231
  notSearched.push(`res://${directory.slice(projectPath.length + 1).replace(/\\/g, "/")}/`);
13202
13232
  }
@@ -13219,20 +13249,17 @@ function searchProject(projectPath, options) {
13219
13249
  };
13220
13250
  const within = options.within ?? projectPath;
13221
13251
  if (statSync9(within).isDirectory()) {
13222
- visit(within);
13252
+ visit(within, true);
13223
13253
  } else {
13224
13254
  searchFile(within, basename5(within));
13225
13255
  }
13226
13256
  const answered = notSearched.length === 0 ? result : { ...result, not_searched: notSearched };
13227
- if (extensions === null || result.summary.files_searched > 0) {
13228
- return answered;
13229
- }
13230
- const asked = [...extensions].join(", ");
13231
- const there = [...met].sort().join(", ");
13232
- return {
13233
- ...answered,
13234
- note: met.size === 0 ? `No file was searched: there is no file there to search.` : `No file was searched: none there has the extension ${asked}. The extensions there are ${there}.`
13235
- };
13257
+ const around = options.within === undefined ? null : steppedOverAround(projectPath, within);
13258
+ const notes = [
13259
+ around === null ? undefined : `${around.folder} is searched because path names it; a search of the whole project steps over it, since ${around.why}.`,
13260
+ extensions === null || result.summary.files_searched > 0 ? undefined : met.size === 0 ? "No file was searched: there is no file there to search." : `No file was searched: none there has the extension ${[...extensions].join(", ")}. The extensions there are ${[...met].sort().join(", ")}.`
13261
+ ].filter((note) => note !== undefined);
13262
+ return notes.length === 0 ? answered : { ...answered, note: notes.join(" ") };
13236
13263
  }
13237
13264
  var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, SIDECAR_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
13238
13265
  var init_project_scan = __esm(() => {
@@ -16423,7 +16450,7 @@ var init_tool_definitions = __esm(() => {
16423
16450
  returnType: { type: "string", description: "Functions: the return type." },
16424
16451
  body: {
16425
16452
  type: "string",
16426
- description: "Functions: the body, its lines separated by \\n. Indent shared by every line is taken off and one tab put on, so it can be written indented or not."
16453
+ description: "Functions: the body, its lines separated by \\n. Indent shared by every line is taken off, so it can be written indented or not, and each level is written in the script's own indentation, tabs or spaces."
16427
16454
  },
16428
16455
  varType: {
16429
16456
  type: "string",
@@ -16433,8 +16460,14 @@ var init_tool_definitions = __esm(() => {
16433
16460
  type: "string",
16434
16461
  description: "Variables: the initial value. Constants: the value, which a constant needs. As written in GDScript."
16435
16462
  },
16436
- isExport: { type: "boolean", description: "Variables: add @export." },
16437
- exportHint: { type: "string", description: 'Variables: the export hint, such as "range(0, 100)".' },
16463
+ isExport: {
16464
+ type: "boolean",
16465
+ description: "Variables: add @export. Default true when exportHint is given."
16466
+ },
16467
+ exportHint: {
16468
+ type: "string",
16469
+ description: 'Variables: the export hint, such as "range(0, 100)", written as @export_range(0, 100). Refused beside isExport false.'
16470
+ },
16438
16471
  isOnready: { type: "boolean", description: "Variables: add @onready." },
16439
16472
  position: {
16440
16473
  type: "string",
@@ -16537,7 +16570,7 @@ var init_tool_definitions = __esm(() => {
16537
16570
  {
16538
16571
  name: "project_settings",
16539
16572
  title: "Project settings",
16540
- description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events, and its deadzone when one is given, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot, the way back from a set of a misspelt name, and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
16573
+ description: "Reads or writes project.godot (settings, autoloads, the main scene, input actions and plugins) and the audio bus layout, default_bus_layout.tres unless audio/buses/default_bus_layout names another. project_info include lists the autoloads, input actions and audio buses as the project holds them. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, or one on another release's addon that was not asked, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start, and enable_plugin notes a script it would not load. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is, and overrides with overridesNote when a feature's override still replaces it. add_autoload refuses a name the editor's Autoload tab refuses: not an identifier, a keyword, a built-in type or an engine class. add_input_action on an existing action, a ui_ one included, replaces its events, and its deadzone when one is given, answering was_new false with old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. unset takes a setting's line out of project.godot and answers removed, old_value and default_value, the value that holds now (null for a setting the engine does not declare; for an override, name.feature, holds is the base setting's value its builds read now); on one the file does not set it answers removed false and writes nothing, its note naming the nearest settings when the engine does not declare it either. Every write changes only the lines of the settings it names and keeps every other line as the file had it, in its place: the engine's own save would sort a dictionary's keys and write what a project_settings_override file laid over the project. get answers exists, true for any setting the engine knows, at_default, true when the value is the engine's own default and so has no line in project.godot, and overrides, the value each feature's override holds. An override answers override_of, its base setting, and one project.godot does not set answers exists false with holds, the base setting's value. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting with no engine default, such as an input action the project added.",
16541
16574
  parameters: {
16542
16575
  projectPath: PROJECT_PATH,
16543
16576
  setting: {
@@ -16697,7 +16730,7 @@ var init_tool_definitions = __esm(() => {
16697
16730
  name: "project_search",
16698
16731
  title: "Search the project",
16699
16732
  effect: "read",
16700
- description: "Searches text or a regular expression across project files and returns file paths with line numbers, one entry per line that holds the query, however often it holds it; total_matches counts those lines. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
16733
+ description: "Searches text or a regular expression across project files and returns file paths with line numbers, one entry per line that holds the query, however often it holds it; total_matches counts those lines. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore, which not_searched names when the search passed one. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them. A path naming one of those folders, or a place inside one, is searched as asked, and note says the whole-project search steps over it. Each line is searched on its own, so a query holding a line break is refused.",
16701
16734
  parameters: {
16702
16735
  projectPath: PROJECT_PATH,
16703
16736
  query: { type: "string", description: "The text or pattern to find." },
@@ -16712,7 +16745,11 @@ var init_tool_definitions = __esm(() => {
16712
16745
  },
16713
16746
  regex: { type: "boolean", description: "Read the query as a regular expression. Default false." },
16714
16747
  caseSensitive: { type: "boolean", description: "Default false." },
16715
- maxResults: { type: "number", description: "Default 100." }
16748
+ maxResults: {
16749
+ type: "integer",
16750
+ minimum: 1,
16751
+ description: "How many matching lines to answer. Default 100."
16752
+ }
16716
16753
  },
16717
16754
  requires: ["projectPath", "query"]
16718
16755
  },
@@ -16729,7 +16766,11 @@ var init_tool_definitions = __esm(() => {
16729
16766
  enum: ["forward", "reverse"],
16730
16767
  description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
16731
16768
  },
16732
- depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
16769
+ depth: {
16770
+ type: "integer",
16771
+ minimum: 1,
16772
+ description: "forward: how many levels to follow, 1 being what the file loads itself. Default unlimited."
16773
+ },
16733
16774
  includeBuiltin: {
16734
16775
  type: "boolean",
16735
16776
  description: "forward: include the engine's own res://. resources. Default false."
@@ -16899,14 +16940,14 @@ var init_tool_definitions = __esm(() => {
16899
16940
  name: "project_test",
16900
16941
  title: "Run tests",
16901
16942
  effect: "additive",
16902
- description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and with script, line and function for the project script one points at, as editor_output's entries have. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
16943
+ description: "Runs the project's gdUnit4 tests and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and with script, line and function for the project script one points at, as editor_output's entries have. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes. What changed on disk since it was imported is then imported before any suite runs, and named under importedFirst. A suite it ran with no .uid beside it is named under uidsMissing, with uidsNote. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. The run is headless unless a suite in it needs a window as gdharness test reads it, and then has one, on a desktop of its own on Windows. A run that found nothing to run is never called a pass, and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but orphan nodes were left comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and is not listed there, and none are when the project turns off gdUnit4's report/godot/script_error setting; a suite declares them all the same. Needs gdUnit4 under addons/gdUnit4.",
16903
16944
  answer: "passed is the verdict to read. exitCode is the engine's own exit, gdUnit4's code for what it saw (100 a case failed, 101 only warnings, 105 script errors it detected), and is 0 beside passed false when the run is failed for something gdUnit4 does not judge, such as an engine error no suite declared. time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for, and tookMsBy splits it into the class rebuild (classes), the import after it (import) and the engine running the suites (engine), each with any wait for an engine slot or the editor's scan in it. The run is judged as gdharness test judges it: undeclaredErrors names each engine error that no suite declared in its GDHARNESS_EXPECTED_ERRORS constant, and each engine warning a suite printed and did not declare in its GDHARNESS_EXPECTED_WARNINGS, marked warning true, with the suite and the case that printed it, and declaredNotPrinted each declared error or warning its case did not print. gdUnit4 does not fail a case on push_error or push_warning, so a case that printed one undeclared is in neither failed nor errors: it is named here, and the run is failed for it. A declared text must be the whole line after ERROR: or WARNING: and its space, so a longer one its case printed holding the text is named under printedInstead. Either fails the run, and each suite holding one is listed with its count. leakedAtExit holds the engine's lines saying it exited still holding objects, resources, memory pages or a server's RIDs, which fail the run too, and the note names any script of the project Godot itself never frees and, on a run with engineArgs [\"--verbose\"], what the engine held. engineErrors counts every engine error the run printed but those a suite declared. orphans totals the orphan nodes gdUnit4 counted, and each suite that left some has its count, and a warning saying how to free them, under warnings, with left naming each case that left some (before or after for one of the suite's hooks, or the one value \"before or after\" where no case began and the two cannot be told apart) and the nodes, by class, name and script. suites lists only the suites with something to say (a case that failed, erred or was skipped, a hook that failed, orphans, or an error undeclared or declared and not printed), and suitesPassed counts the rest. A before() or after() that failed comes back under hookFailures with its suite, line and whole message; failures and errors count cases and leave it out, so the verdict names it as failed hooks, and the cases after a failed before() still run and are counted. Under classes, the classes each addon declares are counted under addedInAddons and changedInAddons rather than named, and a class the rebuild puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. A suite needs a window when it declares const GDHARNESS_NEEDS_WINDOW: bool = true, or calls scene_runner and declares nothing; a run holding one has a window, at the size project.godot gives its viewport and with no sound, and windowed is true and windowNote names those suites and any saying it runs headless that shared the window. On a machine with no display such a run is headless, windowed is false, and windowNote says so. " + IMPORTED_FIRST,
16904
16945
  parameters: {
16905
16946
  projectPath: PROJECT_PATH,
16906
16947
  path: {
16907
16948
  type: ["string", "array"],
16908
16949
  items: { type: "string" },
16909
- description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. Default tests and test, those the project has, which is where gdharness test looks."
16950
+ description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. Default tests and test, those the project has, which is where gdharness test looks.'
16910
16951
  },
16911
16952
  ignore: {
16912
16953
  type: "array",
@@ -16949,7 +16990,7 @@ var init_tool_definitions = __esm(() => {
16949
16990
  rootNodeType: {
16950
16991
  type: "string",
16951
16992
  ops: ["create"],
16952
- description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node."
16993
+ description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node. The answer names the root as scene_node get does: rootNodeType its engine class, with scriptClass for a class_name."
16953
16994
  },
16954
16995
  rootNodeName: {
16955
16996
  type: "string",
@@ -16985,8 +17026,9 @@ var init_tool_definitions = __esm(() => {
16985
17026
  projectPath: PROJECT_PATH,
16986
17027
  scenePath: SCENE_PATH,
16987
17028
  depth: {
16988
- type: "number",
16989
- description: "How many levels to descend. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
17029
+ type: "integer",
17030
+ minimum: 0,
17031
+ description: "How many levels to descend, 0 being the root alone. Default: all. A node at the last level that has children of its own lists none and says how many under childrenNotListed."
16990
17032
  },
16991
17033
  includeProperties: {
16992
17034
  type: "boolean",
@@ -16994,7 +17036,7 @@ var init_tool_definitions = __esm(() => {
16994
17036
  },
16995
17037
  includeDefaults: {
16996
17038
  type: "boolean",
16997
- description: "With includeProperties, list every stored property, defaults included: a Control has about a hundred, and ten of them ran to fifty-seven thousand characters. Default false."
17039
+ description: "List every stored property, defaults included, which turns includeProperties on when it is left out: a Control has about a hundred, and ten of them ran to fifty-seven thousand characters. Default false."
16998
17040
  }
16999
17041
  },
17000
17042
  requires: ["projectPath", "scenePath"]
@@ -17003,7 +17045,7 @@ var init_tool_definitions = __esm(() => {
17003
17045
  name: "scene_node",
17004
17046
  title: "Scene nodes",
17005
17047
  description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, paint TileMap cells, or bake a navigation region. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own, and answers each rewrite under retargeted and each path it left under notRetargeted. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A delete rewrites no path, and answers under stillNaming what named the deleted node and now names nothing. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
17006
- answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}; namesNoNode, each node path written naming no node. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
17048
+ answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
17007
17049
  parameters: {
17008
17050
  projectPath: PROJECT_PATH,
17009
17051
  scenePath: SCENE_PATH,
@@ -17019,7 +17061,7 @@ var init_tool_definitions = __esm(() => {
17019
17061
  nodeType: {
17020
17062
  type: "string",
17021
17063
  ops: ["add"],
17022
- description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer's nodeType is the class of the node made, for an instance the class of its scene's root.`
17064
+ description: `add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath. The answer names the node made as get does: nodeType its engine class, for an instance the class of its scene's root, with scriptClass when its script declares a class_name.`
17023
17065
  },
17024
17066
  instancePath: {
17025
17067
  type: "string",
@@ -17117,7 +17159,7 @@ var init_tool_definitions = __esm(() => {
17117
17159
  {
17118
17160
  name: "scene_signal",
17119
17161
  title: "Scene signals",
17120
- description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17162
+ description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17121
17163
  parameters: {
17122
17164
  projectPath: PROJECT_PATH,
17123
17165
  scenePath: SCENE_PATH,
@@ -17185,7 +17227,7 @@ var init_tool_definitions = __esm(() => {
17185
17227
  animationName: {
17186
17228
  type: "string",
17187
17229
  ops: ["read", "create", "add_track", "remove_track", "set", "rename", "remove", "add_state"],
17188
- description: "The animation in the player. A name before a slash is the library it is in, as moves/walk. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. create: a library the player does not have is made in the scene and named under libraryMade. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
17230
+ description: "The animation in the player. A name before a slash is the library it is in, as moves/walk. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. create: a library the player does not have is made in the scene and named under libraryMade, where an empty name is the default library of the player, the one a name without a slash is in. add_state: the one that state plays. One the player does not have yet is written all the same, answering animationFound false and why under animationMissing."
17189
17231
  },
17190
17232
  newName: {
17191
17233
  type: "string",
@@ -17330,7 +17372,7 @@ var init_tool_definitions = __esm(() => {
17330
17372
  {
17331
17373
  name: "script_edit",
17332
17374
  title: "Edit scripts",
17333
- description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read (class_name null for a script declaring none), and a script declaring a class_name is put in the project's class list, with registered saying whether it is there, and an editor serving the project scans for it, so it resolves the class at once (editorRescanned); a class_name another script or an engine class holds, or an extends naming no class, is refused with nothing written. modify makes every addition or none, refuses with nothing changed additions that would stop a script that parses from parsing, giving the engine's reasons, and answers with the line each one is on in the file written; with an editor serving the project it reloads the script there too, so the editor's answers read what was written, and editorReloaded says so (editorNote when it could not, or when a script tab holds unsaved edits that saving would write over the file). rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, of which the newest ten are kept, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
17375
+ description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read (class_name null for a script declaring none), and writes the .uid beside the script as the editor does, answering it under uid; a script declaring a class_name is put in the project's class list, with registered saying whether it is there, and an editor serving the project scans for it, so it resolves the class at once (editorRescanned); a class_name another script or an engine class holds, or an extends naming no class, is refused with nothing written. modify makes every addition or none, refuses with nothing changed additions that would stop a script that parses from parsing, giving the engine's reasons, and answers with the line each one is on in the file written; with an editor serving the project it reloads the script there too, so the editor's answers read what was written, and editorReloaded says so (editorNote when it could not, or when a script tab holds unsaved edits that saving would write over the file). rename renames a class or a member wherever the engine uses it and writes every file or none. A new name already taken is refused with nothing written, a member's included when a class inheriting it already uses the name in code, as a local or a parameter. changed lists each file with its changed lines as they now read, and leftAlone lists every other whole-word occurrence of the old name with what the tokenizer makes of it (code, string, comment, doc, nodePath, scene, text, or ignored for a file under a .gdignore directory, which is never changed) and, where it is not plain, why: a string that is the old name whole, such as find_children's type or a call's method name, which nothing renames or compiles and which is listed first; a node named like the class; a call on a value whose type the analyser could not tell. A member rename answers under languageServer how many requests it made and how long they took. A comment, documentation line, string or text file line with nothing more to say is given under leftAloneInProse by file, kind and line instead, and code that is certainly another symbol of the same name, a bare name in a file where nothing inherits the member or one the language server resolved to another declaration, under leftAloneOtherSymbols as each why with the files and lines it covers. summary.leftAlone counts each of the three lists beside the total. When those two grouped lists run past 300 lines, they are written with each line's text to a file under the project's .godot directory instead, of which the newest ten are kept, and leftAloneDetail names the file and counts what is in it. Read both before calling the rename done; nothing in them was changed. Afterwards an open editor scans the changed files and reloads every changed script, which is also the compile check, and its answer comes back under editor; with no editor the class cache is rebuilt and the changed scripts are compiled in a fresh engine, under classCache and compiled. written is true once the files are written, and ok is false when what came after the write failed: the files are left changed, and note says so.",
17334
17376
  parameters: {
17335
17377
  projectPath: PROJECT_PATH,
17336
17378
  scriptPath: SCRIPT_PATH,
@@ -17448,7 +17490,7 @@ var init_tool_definitions = __esm(() => {
17448
17490
  {
17449
17491
  name: "resource_edit",
17450
17492
  title: "Edit resources",
17451
- description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color", so a style box, a font or a constant is set through modify by that name, as "Button/styles/normal" given the res:// path of a StyleBox. A Curve, Curve2D or Curve3D reads as the inspector shows it, point_count and point_0/position and the rest, rather than the _data list the file stores, and so does one built into a resource or a scene. create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and a resource where that cannot be done is refused and the file left as it was.',
17493
+ description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, with resourceType the engine class and scriptClass the class_name of its script when it declares one, and a Theme lists its items there as "Button/colors/font_color", so a style box, a font or a constant is set through modify by that name, as "Button/styles/normal" given the res:// path of a StyleBox. A Curve, Curve2D or Curve3D reads as the inspector shows it, point_count and point_0/position and the rest, rather than the _data list the file stores, and so does one built into a resource or a scene. create and modify answer each property given under properties as the file holds it once saved, in the same form. A create or modify whose properties made the engine change others, as a Gradient given more colours than offsets gives each new point an offset of 0, names each of those under alsoChanged with what it held before and holds now. A property holding a resource takes its res:// path; a file never imported is refused with the reimport that imports it. move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before the save, and answered under setByScript rather than alsoChanged, with setByScriptNote; a resource where that cannot be done is refused and the file left as it was. A read with none leaves out what such a script set as the file loaded.',
17452
17494
  parameters: {
17453
17495
  projectPath: PROJECT_PATH,
17454
17496
  resourcePath: {
@@ -17615,7 +17657,7 @@ var init_tool_definitions = __esm(() => {
17615
17657
  type: ["string", "array"],
17616
17658
  items: { type: "string" },
17617
17659
  ops: ["start"],
17618
- description: "start: gdUnit4 suites to play instead of a scene, a suite file or a test directory inside the project or a list of them, as project_test's path takes them. They run in the tests addon's suites scene through the runner gdharness test uses, so with profile: true the profile names what the suites cost by function, which project_test and gdharness test cannot show, since the profiler never sees a script run. The run quits with gdUnit4's exit code; read it with editor_output until running is false, then profile. Takes no scene or executable, and needs gdUnit4 and the tests addon of this release in the project."
17660
+ description: "start: gdUnit4 suites to play instead of a scene, a suite file or a test directory inside the project or a list of them, as project_test's path takes them, though always whole: an entry naming one case is refused. They run in the tests addon's suites scene through the runner gdharness test uses, so with profile: true the profile names what the suites cost by function, which project_test and gdharness test cannot show, since the profiler never sees a script run. The run quits with gdUnit4's exit code; read it with editor_output until running is false, then profile. Takes no scene or executable, and needs gdUnit4 and the tests addon of this release in the project."
17619
17661
  },
17620
17662
  args: {
17621
17663
  type: "array",
@@ -18146,15 +18188,15 @@ var init_tool_definitions = __esm(() => {
18146
18188
  {
18147
18189
  name: "runtime_input",
18148
18190
  title: "Send input to the game",
18149
- 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.",
18150
- answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. A button answers pressed_signal, whether its pressed signal fired during the click. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), 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 and pressed whether it is left held down (true only after pressed true), 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. key, mouse_click, click, joypad_button and joypad_motion answer actions, the InputMap actions the event also presses; for a motion, those its direction reaches past their deadzone, and none for one inside it. mouse_click, mouse_motion and mouse_drag answer hovered, the control under the pointer once the events are handled, or null over none; the other events are aimed at nothing. mouse_click with a press, and mouse_drag, answer press_went_to, the path of what the press went to (a control, or a window embedded over the point), read before it, or null when nothing there takes the pointer; given nodePath naming a Control, landed says whether that is the control or inside it. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed, and one on a control with something drawn over it names that, a window stacked above the one the control is in among them. An action, a key, a joypad_button or a joypad_motion answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button, and a stick or dpad pressing ui_down moves a focused list's selection, or the focus, and never reaches the game's handler. Where the press moved the focus, focused_after names the Control holding it once the event was handled, or null when nothing does. A key that a focused LineEdit or TextEdit took as typing names that field under typed_into, with what it holds under holds, since a field being edited keeps the key from the game's _unhandled_input. editing_ended true, on a key or on text, is a LineEdit that stopped being edited, as Enter does by submitting it: it keeps the focus, and nothing typed into it lands until it is clicked again. text with the focus on a control that is no LineEdit or TextEdit answers holds null and a note saying so, since a focused button takes a space or Enter as a press. A mouse_click, mouse_motion or mouse_drag given nodePath answers it under path, its points in the pixels of the window they went to, and that window under window when it is not the game's own. One with a point outside the viewport it went to says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
18191
+ description: "Input to the running game: a whole click on a Control or a 3D node named by path or by the words on it, an item chosen out of a menu, typing into the focus, or a raw action, key, mouse button, mouse motion or drag, touch or finger 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 run's first pointer_note says why it matters: get_mouse_position() on the root window, and what derives from it, reads nonsense, 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.",
18192
+ answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. A button answers pressed_signal, whether its pressed signal fired during the click. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), 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 and pressed whether it is left held down (true only after pressed true), 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. touch: the finger's index, the position, whole and pressed as a mouse_click has them, and emulates_mouse; touch_drag: what mouse_drag answers, with index and emulates_mouse. joypad_motion: the axis by number and name, the value and the device. key, mouse_click, click, joypad_button and joypad_motion answer actions, the InputMap actions the event also presses; for a motion, those its direction reaches past their deadzone, and none for one inside it. mouse_click, mouse_motion and mouse_drag answer hovered, the control under the pointer once the events are handled, or null over none; the other events are aimed at nothing. mouse_click with a press, and mouse_drag, answer press_went_to, the path of what the press went to (a control, or a window embedded over the point), read before it, or null when nothing there takes the pointer; given nodePath naming a Control, landed says whether that is the control or inside it. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed, and one on a control with something drawn over it names that, a window stacked above the one the control is in among them. An action, a key, a joypad_button or a joypad_motion answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button, and a stick or dpad pressing ui_down moves a focused list's selection, or the focus, and never reaches the game's handler. Where the press moved the focus, focused_after names the Control holding it once the event was handled, or null when nothing does. A key that a focused LineEdit or TextEdit took as typing names that field under typed_into, with what it holds under holds, since a field being edited keeps the key from the game's _unhandled_input. editing_ended true, on a key or on text, is a LineEdit that stopped being edited, as Enter does by submitting it: it keeps the focus, and nothing typed into it lands until it is clicked again. text with the focus on a control that is no LineEdit or TextEdit answers holds null and a note saying so, since a focused button takes a space or Enter as a press. A mouse_click, mouse_motion or mouse_drag given nodePath answers it under path, its points in the pixels of the window they went to, and that window under window when it is not the game's own. One with a point outside the viewport it went to says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
18151
18193
  parameters: {
18152
18194
  projectPath: RUNNING_PROJECT_PATH,
18153
18195
  pid: RUNNING_PID,
18154
18196
  nodePath: {
18155
18197
  type: "string",
18156
- ops: ["click", "choose", "mouse_click", "mouse_motion", "mouse_drag"],
18157
- description: "click: the Control to click, at its centre, at x and y in its own space when those are given, or at the middle of the part on screen when no scroll brings its centre into view (said under pressed_on), or the 3D node to click, where it is drawn; with says, where to look for the control instead, default the whole tree. choose: the PopupMenu, or the OptionButton or MenuButton in front of one. mouse_click, mouse_motion, mouse_drag: the Control, Node2D or Window whose own space the points are in, a Window's being its own pixels; the events go to the window that shows it, a native one included, which the game's window pixels cannot reach, and the answer names it under window when it is not the game's own. Default the game's window."
18198
+ ops: ["click", "choose", "mouse_click", "mouse_motion", "mouse_drag", "touch", "touch_drag"],
18199
+ description: "click: the Control to click, at its centre, at x and y in its own space when those are given, or at the middle of the part on screen when no scroll brings its centre into view (said under pressed_on), or the 3D node to click, where it is drawn; with says, where to look for the control instead, default the whole tree. choose: the PopupMenu, or the OptionButton or MenuButton in front of one. mouse_click, mouse_motion, mouse_drag, touch, touch_drag: the Control, Node2D or Window whose own space the points are in, a Window's being its own pixels; the events go to the window that shows it, a native one included, which the game's window pixels cannot reach, and the answer names it under window when it is not the game's own. Default the game's window."
18158
18200
  },
18159
18201
  says: {
18160
18202
  type: "string",
@@ -18168,15 +18210,15 @@ var init_tool_definitions = __esm(() => {
18168
18210
  },
18169
18211
  pressed: {
18170
18212
  type: "boolean",
18171
- ops: ["action", "key", "mouse_click", "joypad_button"],
18172
- description: "action, key, mouse_click, joypad_button: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
18213
+ ops: ["action", "key", "mouse_click", "joypad_button", "touch"],
18214
+ description: "action, key, mouse_click, joypad_button, touch: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held, a finger lifted at x and y. A wheel button held down is still a whole step, since a wheel is never held."
18173
18215
  },
18174
18216
  holdFrames: {
18175
18217
  type: "integer",
18176
18218
  minimum: 1,
18177
18219
  maximum: 600,
18178
- ops: ["action", "key", "mouse_click", "joypad_button"],
18179
- description: "action, key, mouse_click, joypad_button: hold a whole press down for this many process frames and then let go, both inside the game, so a pour, a charge or a long press runs to a set length. A press, a frames wait and a release sent as three calls hold it for the time between the calls as well. The answer says held_frames, and under held_for the seconds the hold lasted and the physics_frames that ran in it: a process frame is no set time, and a headless game, drawing nothing, runs them far faster than the sixty physics ticks a second that _physics_process sees. Not with pressed, and not on a wheel step. Default 1."
18220
+ ops: ["action", "key", "mouse_click", "joypad_button", "touch"],
18221
+ description: "action, key, mouse_click, joypad_button, touch: hold a whole press down for this many process frames and then let go, both inside the game, so a pour, a charge or a long press runs to a set length. A press, a frames wait and a release sent as three calls hold it for the time between the calls as well. The answer says held_frames, and under held_for the seconds the hold lasted and the physics_frames that ran in it: a process frame is no set time, and a headless game, drawing nothing, runs them far faster than the sixty physics ticks a second that _physics_process sees. Not with pressed, and not on a wheel step. Default 1."
18180
18222
  },
18181
18223
  strength: {
18182
18224
  type: "number",
@@ -18203,40 +18245,44 @@ var init_tool_definitions = __esm(() => {
18203
18245
  },
18204
18246
  index: {
18205
18247
  type: "number",
18206
- ops: ["click", "choose"],
18207
- description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them, best matches first. choose: the item to take, by where it is in the list, when text will not do."
18248
+ ops: ["click", "choose", "touch", "touch_drag"],
18249
+ description: "click: which of several controls saying the words, counting from 0 in the order a refusal lists them, best matches first. choose: the item to take, by where it is in the list, when text will not do. touch, touch_drag: the finger, from 0, as InputEventScreenTouch.index; a second finger down at once is another index. Default 0."
18208
18250
  },
18209
18251
  shift: { type: "boolean", ops: ["key"], description: "key: hold Shift with it. Default false." },
18210
18252
  ctrl: { type: "boolean", ops: ["key"], description: "key: hold Ctrl with it. Default false." },
18211
18253
  alt: { type: "boolean", ops: ["key"], description: "key: hold Alt with it. Default false." },
18212
18254
  x: {
18213
18255
  type: "number",
18214
- ops: ["click", "mouse_click", "mouse_motion", "mouse_drag"],
18215
- description: "click: with y, the point to press in the control's own space instead of its centre, for a control that draws its parts itself; not with a 3D node, nor with says finding an item the control draws. mouse_click, mouse_motion: window pixels, or nodePath's own space when that is given. mouse_drag: where the press is, in the same space."
18256
+ ops: ["click", "mouse_click", "mouse_motion", "mouse_drag", "touch", "touch_drag"],
18257
+ description: "click: with y, the point to press in the control's own space instead of its centre, for a control that draws its parts itself; not with a 3D node, nor with says finding an item the control draws. mouse_click, mouse_motion, touch: window pixels, or nodePath's own space when that is given. mouse_drag, touch_drag: where the press is, in the same space."
18216
18258
  },
18217
18259
  y: {
18218
18260
  type: "number",
18219
- ops: ["click", "mouse_click", "mouse_motion", "mouse_drag"],
18220
- description: "click, mouse_click, mouse_motion, mouse_drag: as x."
18261
+ ops: ["click", "mouse_click", "mouse_motion", "mouse_drag", "touch", "touch_drag"],
18262
+ description: "click, mouse_click, mouse_motion, mouse_drag, touch, touch_drag: as x."
18221
18263
  },
18222
18264
  toX: {
18223
18265
  type: "number",
18224
- ops: ["mouse_drag"],
18225
- description: "mouse_drag: where the release is, in window pixels or nodePath's own space."
18266
+ ops: ["mouse_drag", "touch_drag"],
18267
+ description: "mouse_drag, touch_drag: where the release is, in window pixels or nodePath's own space."
18268
+ },
18269
+ toY: {
18270
+ type: "number",
18271
+ ops: ["mouse_drag", "touch_drag"],
18272
+ description: "mouse_drag, touch_drag: as toX."
18226
18273
  },
18227
- toY: { type: "number", ops: ["mouse_drag"], description: "mouse_drag: as toX." },
18228
18274
  via: {
18229
18275
  type: "array",
18230
18276
  items: { type: "array", items: { type: "number" }, minItems: 2, maxItems: 2 },
18231
- ops: ["mouse_drag"],
18232
- description: "mouse_drag: points the stroke passes through on the way, in order, each [x, y] in the space x and y are in. The motions are spread along the whole stroke by distance. Default none: a straight line."
18277
+ ops: ["mouse_drag", "touch_drag"],
18278
+ description: "mouse_drag, touch_drag: points the stroke passes through on the way, in order, each [x, y] in the space x and y are in. The motions are spread along the whole stroke by distance. Default none: a straight line."
18233
18279
  },
18234
18280
  frames: {
18235
18281
  type: "integer",
18236
18282
  minimum: 1,
18237
18283
  maximum: 600,
18238
- ops: ["mouse_drag"],
18239
- description: "mouse_drag: how many process frames the stroke takes, one motion each, as a real mouse sends one a frame; a hand takes about 20 to 60 for a stroke across a screen."
18284
+ ops: ["mouse_drag", "touch_drag"],
18285
+ description: "mouse_drag, touch_drag: how many process frames the stroke takes, one motion each, as a real mouse or finger sends one a frame; a hand takes about 20 to 60 for a stroke across a screen."
18240
18286
  },
18241
18287
  button: {
18242
18288
  type: ["string", "number"],
@@ -18281,28 +18327,28 @@ var init_tool_definitions = __esm(() => {
18281
18327
  operations: {
18282
18328
  click: {
18283
18329
  effect: "additive",
18284
- summary: "press and release on a Control, a frame apart, named by nodePath or by says with the words on it (found under nodePath when both are given), or on a 3D node where it is drawn. A control out of sight inside a ScrollContainer is scrolled to first, and a link in a RichTextLabel is pressed where its words are drawn. A disabled button, a control that does not process and one whose pointer mouse_behavior_recursive turned off are refused, since the press would do nothing; by says, one the pointer can reach is picked over it",
18330
+ summary: "press and release on a Control, a frame apart, named by nodePath or by says (found under nodePath when both are given), or on a 3D node where it is drawn. A control hidden in a ScrollContainer is scrolled to first; a RichTextLabel link is pressed where its words are drawn. A disabled button, a control that does not process and one mouse_behavior_recursive turned off are refused, since the press would do nothing; by says, a reachable one is picked over it",
18285
18331
  requires: []
18286
18332
  },
18287
18333
  choose: {
18288
18334
  effect: "additive",
18289
- summary: "take an item out of a menu, by what it says or by where it is in the list: the menu is opened, the item found in it as it stands then, and pressed through the engine's own path, which needs no window and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take",
18335
+ summary: "take an item out of a menu, by its words or its place in the list: the menu is opened, the item found as it stands then and pressed through the engine's own path, which needs no window and not ui_accept's binding. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take",
18290
18336
  requires: ["nodePath"]
18291
18337
  },
18292
18338
  action: { effect: "additive", summary: "press an action, or hold it", requires: ["action"] },
18293
18339
  key: {
18294
18340
  effect: "additive",
18295
- summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there. The limit is that menu's own navigation, not windows in general: an embedded dialog whose owner reads keys in _input gets them, and a binding catcher up in one takes the key first time",
18341
+ summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key never reaches, so arrows move nothing and Enter closes it unchosen; choose selects there. Other windows are not like it: an embedded dialog whose owner reads keys in _input gets them",
18296
18342
  requires: ["keycode"]
18297
18343
  },
18298
18344
  text: {
18299
18345
  effect: "additive",
18300
- summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask. The field is the focus of the dialog that has the focus when one does, which is where the keys go",
18346
+ summary: "type a string into the field being edited, a character at a time, saying what it landed in and what it then holds (a secret field as its mask). With a dialog focused, the field is that dialog's focus, where the keys go",
18301
18347
  requires: ["text"]
18302
18348
  },
18303
18349
  mouse_click: {
18304
18350
  effect: "additive",
18305
- summary: "click a mouse button at a position, pressed and released a frame apart, each event carrying the buttons held once it has happened; the answer says whole. A drag is mouse_drag. A wheel button is always a whole step, press and release together as a mouse sends one: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
18351
+ summary: "click a mouse button at a position, pressed and released a frame apart, each event carrying the buttons held after it; the answer says whole. A drag is mouse_drag. A wheel button is always a whole step, as a mouse sends one: a lone wheel press holds the viewport on the control that took it, so every later click lands there",
18306
18352
  requires: ["x", "y"]
18307
18353
  },
18308
18354
  mouse_motion: {
@@ -18312,17 +18358,27 @@ var init_tool_definitions = __esm(() => {
18312
18358
  },
18313
18359
  mouse_drag: {
18314
18360
  effect: "additive",
18315
- summary: "a whole drag in one call, as a hand makes one: press at x, y, a motion each frame for frames frames, and release at toX, toY; for a stir, a paint stroke or a slow slider",
18361
+ summary: "a whole drag in one call: press at x, y, a motion each frame, release at toX, toY; for a stir, a paint stroke or a slow slider",
18362
+ requires: ["x", "y", "toX", "toY", "frames"]
18363
+ },
18364
+ touch: {
18365
+ effect: "additive",
18366
+ summary: "press a finger at x, y and lift it a frame later, or hold it, as an InputEventScreenTouch; in the game's window the engine makes mouse events carrying DEVICE_ID_EMULATION from the first finger down, where emulate_mouse_from_touch is on (emulates_mouse)",
18367
+ requires: ["x", "y"]
18368
+ },
18369
+ touch_drag: {
18370
+ effect: "additive",
18371
+ summary: "mouse_drag for one finger: the touch, an InputEventScreenDrag each frame, and the lift",
18316
18372
  requires: ["x", "y", "toX", "toY", "frames"]
18317
18373
  },
18318
18374
  joypad_button: {
18319
18375
  effect: "additive",
18320
- summary: "press a controller button, or hold it, as an InputEventJoypadButton, so a game reading the pad in _input or _unhandled_input gets what a controller sends; an action bound to the button fires with it, as it would for a real one. Pressed and released a frame apart unless pressed says otherwise",
18376
+ summary: "press a controller button, or hold it, as an InputEventJoypadButton, as a controller sends it to _input and _unhandled_input; an action bound to the button fires with it. Pressed and released a frame apart unless pressed says otherwise",
18321
18377
  requires: ["button"]
18322
18378
  },
18323
18379
  joypad_motion: {
18324
18380
  effect: "additive",
18325
- summary: "move a stick or trigger axis to a value, as an InputEventJoypadMotion, which is what a game reading the sticks gets; the axis stays there until another motion moves it",
18381
+ summary: "move a stick or trigger axis to a value, as an InputEventJoypadMotion; the axis stays there until another motion moves it",
18326
18382
  requires: ["axis", "value"]
18327
18383
  }
18328
18384
  }
@@ -18419,7 +18475,7 @@ var init_tool_definitions = __esm(() => {
18419
18475
  {
18420
18476
  name: "debug_breakpoint",
18421
18477
  title: "Breakpoints",
18422
- description: "Sets, removes or lists breakpoints. A set on a script that is not there, past its last line, or on a line holding no code, blank or only a comment, is refused, as nothing would ever stop there; a remove takes any line, and with no editor connected answers removed false, saying why, when none was held there. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, for a headless run of a project whose editor plays its games with a window, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it, and the set answers once the game has taken it, which it does between frames, so a call made after the answer stops there (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
18478
+ description: "Sets, removes or lists breakpoints. A set on a script that is not there, past its last line, or on a line holding no code, blank or only a comment, is refused, as nothing would ever stop there; a remove takes any line, and with no editor connected answers removed false, saying why, when none was held there. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, for a headless run of a project whose editor plays its games with a window, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it, and the set answers once the game has taken it, which it does between frames, so a call made after the answer stops there (runningGame says so). Such a game does not stop on a script error, so a run that meets one carries on past it: editor_output has the error with where it happened, and a breakpoint on that line stops the game before the line runs. The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor where it was turned off again is said so under breakpointsAtRisk, here and in editor_status.",
18423
18479
  parameters: {
18424
18480
  projectPath: PROJECT_PATH,
18425
18481
  scriptPath: { ...SCRIPT_PATH, ops: ["set", "remove"] },
@@ -41431,7 +41487,7 @@ var init_boot_note = __esm(() => {
41431
41487
 
41432
41488
  // src/breakpoint-note.ts
41433
41489
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync13, writeFileSync as writeFileSync14 } from "node:fs";
41434
- import { dirname as dirname16, isAbsolute as isAbsolute7, join as join31, relative as relative5 } from "node:path";
41490
+ import { dirname as dirname16, isAbsolute as isAbsolute7, join as join31, relative as relative6 } from "node:path";
41435
41491
  function breakpointNotePath(projectPath) {
41436
41492
  return join31(projectPath, ".godot", "gdharness-breakpoints.json");
41437
41493
  }
@@ -41446,7 +41502,7 @@ function writeBreakpointNote(projectPath, held) {
41446
41502
  }
41447
41503
  const note = {
41448
41504
  breakpoints: inside.map((one) => ({
41449
- script: relative5(root, one.scriptPath).replaceAll("\\", "/"),
41505
+ script: relative6(root, one.scriptPath).replaceAll("\\", "/"),
41450
41506
  lines: [...one.lines]
41451
41507
  }))
41452
41508
  };
@@ -41483,7 +41539,7 @@ function readBreakpointNote(projectPath) {
41483
41539
  }
41484
41540
  }
41485
41541
  function withinProject(projectPath, scriptPath) {
41486
- const rel = relative5(projectPath, scriptPath);
41542
+ const rel = relative6(projectPath, scriptPath);
41487
41543
  return rel !== "" && !rel.startsWith("..") && !isAbsolute7(rel);
41488
41544
  }
41489
41545
  var init_breakpoint_note = __esm(() => {
@@ -43376,9 +43432,9 @@ var init_rename = __esm(() => {
43376
43432
 
43377
43433
  // src/rename-symbol.ts
43378
43434
  import { existsSync as existsSync30, mkdirSync as mkdirSync17, readdirSync as readdirSync16, realpathSync as realpathSync4, rmSync as rmSync16, writeFileSync as writeFileSync18 } from "node:fs";
43379
- import { basename as basename9, dirname as dirname21, join as join36, relative as relative6, resolve as resolve12 } from "node:path";
43435
+ import { basename as basename9, dirname as dirname21, join as join36, relative as relative7, resolve as resolve12 } from "node:path";
43380
43436
  function resourcePathOf(projectPath, absolute) {
43381
- const inside = relative6(onDiskSpelling(projectPath), onDiskSpelling(absolute));
43437
+ const inside = relative7(onDiskSpelling(projectPath), onDiskSpelling(absolute));
43382
43438
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
43383
43439
  return null;
43384
43440
  }
@@ -43884,7 +43940,7 @@ import {
43884
43940
  statSync as statSync17,
43885
43941
  writeFileSync as writeFileSync19
43886
43942
  } from "node:fs";
43887
- import { basename as basename10, dirname as dirname22, extname as extname3, join as join37, relative as relative7 } from "node:path";
43943
+ import { basename as basename10, dirname as dirname22, extname as extname3, join as join37, relative as relative8 } from "node:path";
43888
43944
  function filesUnder(directory) {
43889
43945
  return readdirSync17(directory, { withFileTypes: true }).flatMap((entry) => {
43890
43946
  const path = join37(directory, entry.name);
@@ -43945,7 +44001,7 @@ function binaryResources(projectPath) {
43945
44001
  return found;
43946
44002
  }
43947
44003
  function targetPathOf(projectPath, absolute) {
43948
- const inside = relative7(onDiskSpelling(projectPath), join37(onDiskSpelling(dirname22(absolute)), basename10(absolute)));
44004
+ const inside = relative8(onDiskSpelling(projectPath), join37(onDiskSpelling(dirname22(absolute)), basename10(absolute)));
43949
44005
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
43950
44006
  return null;
43951
44007
  }
@@ -47908,7 +47964,14 @@ function similarity(one, other) {
47908
47964
  const shared = ones.filter((pair) => others.includes(pair)).length;
47909
47965
  return 2 * shared / (ones.length + others.length);
47910
47966
  }
47911
- function withNearestFiles(said, wanted, files) {
47967
+ function withNearestFiles(said, wanted, files, beside) {
47968
+ const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
47969
+ const named = said.split(/[\s,;]+/).map((word) => word.replace(/[.:]+$/, ""));
47970
+ const namesakes = beside.filter((file) => file !== wanted && stem(file) === stem(wanted) && !named.includes(file)).sort();
47971
+ if (namesakes.length > 0) {
47972
+ const alone = namesakes.length === 1;
47973
+ return `${said}. The ${alone ? "file" : "files"} of that name beside it ${alone ? "is" : "are"} ${namesakes.join(", ")}.`;
47974
+ }
47912
47975
  const extension = /\.([^./]+)$/.exec(wanted)?.[1];
47913
47976
  if (extension === undefined) {
47914
47977
  return said;
@@ -47916,7 +47979,6 @@ function withNearestFiles(said, wanted, files) {
47916
47979
  if (files.length === 0) {
47917
47980
  return `${said}. The project holds no .${extension} file.`;
47918
47981
  }
47919
- const stem = (path) => (path.split("/").pop() ?? path).replace(/\.[^.]*$/, "");
47920
47982
  const close = nearest(stem(wanted), files.map((file) => stem(file)));
47921
47983
  const offered = files.filter((file) => close.includes(stem(file)));
47922
47984
  if (offered.length > 0) {
@@ -48894,7 +48956,7 @@ var init_server_instructions = __esm(() => {
48894
48956
 
48895
48957
  // src/suites-scene.ts
48896
48958
  import { existsSync as existsSync32, mkdirSync as mkdirSync22, writeFileSync as writeFileSync21 } from "node:fs";
48897
- import { join as join41, relative as relative8, sep as sep4 } from "node:path";
48959
+ import { join as join41, relative as relative9, sep as sep5 } from "node:path";
48898
48960
  function hasSuitesScene(projectPath) {
48899
48961
  return existsSync32(join41(projectPath, SUITES_SCENE));
48900
48962
  }
@@ -48918,7 +48980,7 @@ function suitesSceneArgs(projectPath, suites) {
48918
48980
  "-c",
48919
48981
  "--ignoreHeadlessMode",
48920
48982
  "-rd",
48921
- `res://${relative8(projectPath, join41(directory, "reports")).split(sep4).join("/")}`,
48983
+ `res://${relative9(projectPath, join41(directory, "reports")).split(sep5).join("/")}`,
48922
48984
  "-rc",
48923
48985
  "1"
48924
48986
  ];
@@ -48933,6 +48995,44 @@ var init_suites_scene = __esm(() => {
48933
48995
  // src/test-selection.ts
48934
48996
  import { existsSync as existsSync33 } from "node:fs";
48935
48997
  import { basename as basename12, join as join42 } from "node:path";
48998
+ function casesOutOfPath(projectPath, given, tests) {
48999
+ const entries = Array.isArray(given) ? given : [given];
49000
+ const cases = [];
49001
+ const whole = [];
49002
+ const path = entries.map((entry) => {
49003
+ const caseAt = typeof entry === "string" ? CASE_AFTER_SCRIPT.exec(entry.trim()) : null;
49004
+ const suite = caseAt === null ? null : resolveWithinProject(projectPath, caseAt[1] ?? "");
49005
+ if (caseAt === null || suite === null || !suite.ok) {
49006
+ if (typeof entry === "string") {
49007
+ whole.push(entry);
49008
+ }
49009
+ return entry;
49010
+ }
49011
+ cases.push(`res://${suite.relativePath}:${caseAt[2] ?? ""}`);
49012
+ return `res://${suite.relativePath}`;
49013
+ });
49014
+ if (cases.length === 0) {
49015
+ return { ok: true, path: given, tests };
49016
+ }
49017
+ if (whole.length > 0) {
49018
+ return {
49019
+ ok: false,
49020
+ reason: `path names ${cases.length === 1 ? "a case" : "cases"}, ${cases.join(", ")}, beside ${whole.join(", ")}, and a run with cases named runs only those: run the cases on their own, and the rest in a run of its own.`
49021
+ };
49022
+ }
49023
+ const named = tests === undefined || tests === null ? [] : Array.isArray(tests) ? tests : [tests];
49024
+ return { ok: true, path: [...new Set(path)], tests: [...named, ...cases] };
49025
+ }
49026
+ function caseInSuites(given) {
49027
+ const entries = Array.isArray(given) ? given : [given];
49028
+ const cased = entries.filter((entry) => typeof entry === "string" && CASE_AFTER_SCRIPT.test(entry.trim()));
49029
+ const first = cased[0];
49030
+ if (first === undefined) {
49031
+ return null;
49032
+ }
49033
+ const file = CASE_AFTER_SCRIPT.exec(first.trim())?.[1] ?? first;
49034
+ return `suites plays whole suites, and ${cased.join(", ")} ${cased.length === 1 ? "names" : "name"} one case: name the file, ${file}, to play every case in it, or run the case alone with project_test, whose path takes ${first}.`;
49035
+ }
48936
49036
  function casesIn(scripts, suite) {
48937
49037
  return scripts.suite(suite)?.cases ?? scripts.script(suite)?.funcs.filter((name) => name.startsWith("test_")) ?? [];
48938
49038
  }
@@ -48994,9 +49094,12 @@ function selectTests(projectPath, asked, given) {
48994
49094
  const ignore = declared.filter((one) => suites.includes(one.suite) && !chosen.has(`${one.suite}:${one.test}`)).map((one) => `${one.suite}:${one.test}`);
48995
49095
  return { ok: true, value: { suites, ignore, selected } };
48996
49096
  }
49097
+ var CASE_AFTER_SCRIPT;
48997
49098
  var init_test_selection = __esm(() => {
49099
+ init_paths();
48998
49100
  init_project_scan();
48999
49101
  init_suite_plan();
49102
+ CASE_AFTER_SCRIPT = /^(.*\.gd):(\w+)$/;
49000
49103
  });
49001
49104
 
49002
49105
  // src/server.ts
@@ -49018,7 +49121,7 @@ import {
49018
49121
  statSync as statSync20,
49019
49122
  writeFileSync as writeFileSync22
49020
49123
  } from "node:fs";
49021
- import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join43, normalize as normalize2, relative as relative9 } from "node:path";
49124
+ import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join43, normalize as normalize2, relative as relative10 } from "node:path";
49022
49125
  import { setTimeout as delay7 } from "node:timers/promises";
49023
49126
  import { fileURLToPath as fileURLToPath5 } from "node:url";
49024
49127
  function editorRestartTimeoutMs() {
@@ -49175,7 +49278,7 @@ function keptGameRequest(tool, op, args) {
49175
49278
  }
49176
49279
  }
49177
49280
  function projectSpelling(projectPath, absolutePath) {
49178
- return `res://${relative9(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
49281
+ return `res://${relative10(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
49179
49282
  }
49180
49283
  function whichGame(args) {
49181
49284
  return { projectPath: args["projectPath"], pid: args["pid"] };
@@ -50082,7 +50185,7 @@ function failedVerdict(report) {
50082
50185
  const last = parts.pop();
50083
50186
  return last === undefined ? "failures" : parts.length === 0 ? last : `${parts.join(", ")} and ${last}`;
50084
50187
  }
50085
- function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
50188
+ function timedOutVerdict(timeoutMs, silentForMs, repeating = null, printedAnything = true) {
50086
50189
  const quiet = Math.round(silentForMs);
50087
50190
  if (repeating !== null) {
50088
50191
  return {
@@ -50090,6 +50193,12 @@ function timedOutVerdict(timeoutMs, silentForMs, repeating = null) {
50090
50193
  hung: false
50091
50194
  };
50092
50195
  }
50196
+ if (!printedAnything && timeoutMs < STALLED_AFTER_MS) {
50197
+ return {
50198
+ verdict: `timed out after ${timeoutMs} ms before the engine printed anything, so it may still have been starting: timeoutMs counts milliseconds, and the default is ${TEST_TIMEOUT_MS}`,
50199
+ hung: false
50200
+ };
50201
+ }
50093
50202
  return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
50094
50203
  verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
50095
50204
  hung: true
@@ -51318,13 +51427,20 @@ class GodotServer {
51318
51427
  return await this.handleRunTests(args);
51319
51428
  case "scene_create":
51320
51429
  return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
51321
- case "scene_tree":
51430
+ case "scene_tree": {
51431
+ const defaults = readBoolean(args, "includeDefaults") ?? false;
51432
+ const asked = readBoolean(args, "includeProperties");
51433
+ if (defaults && asked === false) {
51434
+ return this.createErrorResponse("includeDefaults lists the defaults among the properties, and includeProperties false lists none: leave includeProperties out, or set it true.");
51435
+ }
51436
+ const properties = asked ?? defaults;
51322
51437
  return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
51323
51438
  scenePath: file,
51324
51439
  depth: readNumber(args, "depth") ?? -1,
51325
- includeProperties: readBoolean(args, "includeProperties") ?? false,
51326
- includeDefaults: readBoolean(args, "includeDefaults") ?? false
51327
- }), async () => await bridge("list_scene_nodes"));
51440
+ includeProperties: properties,
51441
+ includeDefaults: defaults
51442
+ }), async () => await bridge("list_scene_nodes", { includeProperties: properties }));
51443
+ }
51328
51444
  case "scene_node":
51329
51445
  switch (op) {
51330
51446
  case "add":
@@ -51546,7 +51662,7 @@ class GodotServer {
51546
51662
  }, { tool: "runtime_input", op });
51547
51663
  }
51548
51664
  {
51549
- const holding = op === "mouse_drag" ? (readPositiveNumber(args, "frames") ?? 1) + 2 : readPositiveNumber(args, "holdFrames");
51665
+ const holding = op === "mouse_drag" || op === "touch_drag" ? (readPositiveNumber(args, "frames") ?? 1) + 2 : readPositiveNumber(args, "holdFrames");
51550
51666
  return await this.handleRuntimeCommand(`inject_${op}`, args, {
51551
51667
  tool: "runtime_input",
51552
51668
  op,
@@ -52481,6 +52597,9 @@ class GodotServer {
52481
52597
  if (!project.ok) {
52482
52598
  return project.response;
52483
52599
  }
52600
+ if (/[\r\n]/.test(readString(args, "query") ?? "")) {
52601
+ return this.createErrorResponse("query holds a line break, and project_search matches one line at a time: search for one of its lines.");
52602
+ }
52484
52603
  const given = readNonEmptyString(args, "path");
52485
52604
  const place = given === undefined ? null : resolveWithinProject(project.value.path, given, { root: true });
52486
52605
  if (place !== null && !place.ok) {
@@ -52730,10 +52849,18 @@ ${platform}`;
52730
52849
  ticker.unref();
52731
52850
  return ticker;
52732
52851
  }
52733
- async handleRunTests(args) {
52852
+ async handleRunTests(given) {
52853
+ const project = this.project(given);
52854
+ if (!project.ok) {
52855
+ return project.response;
52856
+ }
52857
+ const split = casesOutOfPath(project.value.path, given["path"], given["tests"]);
52858
+ if (!split.ok) {
52859
+ return this.createErrorResponse(split.reason);
52860
+ }
52861
+ const args = split.path === given["path"] ? given : { ...given, path: split.path, tests: split.tests };
52734
52862
  const ran = await this.runTests(args);
52735
- const project = this.project(args);
52736
- if (!project.ok || callSignal()?.aborted === true) {
52863
+ if (callSignal()?.aborted === true) {
52737
52864
  return ran;
52738
52865
  }
52739
52866
  const paths = testPathsIn(project.value.path, args["path"]);
@@ -52892,7 +53019,7 @@ ${platform}`;
52892
53019
  "--expected-out",
52893
53020
  expectedFile
52894
53021
  ];
52895
- const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
53022
+ const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? TEST_TIMEOUT_MS;
52896
53023
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
52897
53024
  const helperFiles = {
52898
53025
  spec: join43(userData, "spec"),
@@ -52905,12 +53032,15 @@ ${platform}`;
52905
53032
  const run = await this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
52906
53033
  const console2 = [];
52907
53034
  let lastSaid = Date.now();
53035
+ let saidAnything = false;
52908
53036
  run.process.stdout?.on("data", (data) => {
52909
53037
  console2.push(data);
52910
53038
  lastSaid = Date.now();
53039
+ saidAnything = true;
52911
53040
  });
52912
53041
  run.process.stderr?.on("data", () => {
52913
53042
  lastSaid = Date.now();
53043
+ saidAnything = true;
52914
53044
  });
52915
53045
  const reporting = this.reportTestProgress(run.log);
52916
53046
  const autoloaded = scriptsAutoloaded(project.value.path);
@@ -53003,7 +53133,7 @@ ${platform}`;
53003
53133
  }).entries;
53004
53134
  const said = printed.map((entry) => entry.text);
53005
53135
  const nothingRan = timedOut || notCompiled !== null ? null : whyNoReport(said, asked.join(" and "));
53006
- const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry))) : null;
53136
+ const cutShort = timedOut && notCompiled === null ? timedOutVerdict(timeoutMs, silentForMs, repeatingScriptError(run.log.all, (entry) => run.log.raisedByScript(entry)), saidAnything) : null;
53007
53137
  const hung = cutShort?.hung ?? false;
53008
53138
  const printedErrors = notCompiled !== null ? compileErrorsIn(run.log.all) : timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
53009
53139
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
@@ -53065,7 +53195,8 @@ ${platform}`;
53065
53195
  ...printedLines,
53066
53196
  savesNote,
53067
53197
  ...scanned,
53068
- ...windowSaid
53198
+ ...windowSaid,
53199
+ classes: withAddonClassesCounted(classes.payload, cachedClasses(project.value.path) ?? new Map)
53069
53200
  }, null, 2)
53070
53201
  }
53071
53202
  ],
@@ -53356,7 +53487,10 @@ ${error.text}`;
53356
53487
  missingScript(projectPath, relativePath) {
53357
53488
  const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
53358
53489
  const wanted = `res://${relativePath}`;
53359
- return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts));
53490
+ const folder = dirname25(relativePath.replaceAll("\\", "/"));
53491
+ const inFolder = join43(projectPath, folder);
53492
+ const beside = existsSync34(inFolder) ? readdirSync19(inFolder, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `res://${folder === "." ? "" : `${folder}/`}${entry.name}`) : [];
53493
+ return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts, beside));
53360
53494
  }
53361
53495
  async compileScripts(projectPath, paths) {
53362
53496
  const payload = { checked: [], failed: [], scripts: [] };
@@ -54437,6 +54571,10 @@ ${error.text}`;
54437
54571
  if (args["scene"] !== undefined || args["executable"] !== undefined) {
54438
54572
  return refused("suites plays the suites in the tests addon's own scene, so it takes no scene and no executable: leave those out.");
54439
54573
  }
54574
+ const cased = caseInSuites(args["suites"]);
54575
+ if (cased !== null) {
54576
+ return refused(cased);
54577
+ }
54440
54578
  const paths = testPathsIn(projectPath, args["suites"], "suites");
54441
54579
  if (!paths.ok) {
54442
54580
  return refused(paths.reason, PATH_SOLUTIONS);
@@ -54626,6 +54764,9 @@ ${error.text}`;
54626
54764
  let editorAskedAt = 0;
54627
54765
  let editorSaysGoing = true;
54628
54766
  let editorAsks = 0;
54767
+ let holding = null;
54768
+ let keeperAskedAt = 0;
54769
+ let heldHere = null;
54629
54770
  const waitBegan = Date.now();
54630
54771
  const runBegan = Math.min(this.activeProcess?.startedAt ?? waitBegan, waitBegan);
54631
54772
  const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
@@ -54638,7 +54779,19 @@ ${error.text}`;
54638
54779
  }
54639
54780
  const run = this.activeProcess;
54640
54781
  if (run === null || !run.throughEditor) {
54641
- return !stillRunning(run);
54782
+ if (!stillRunning(run)) {
54783
+ return true;
54784
+ }
54785
+ const keeper = run?.keeper;
54786
+ if (keeper !== undefined && holding === null && Date.now() - keeperAskedAt >= RUN_POLL_MS) {
54787
+ keeperAskedAt = Date.now();
54788
+ holding = this.keptHold(keeper).then((held) => {
54789
+ heldHere = held;
54790
+ }).finally(() => {
54791
+ holding = null;
54792
+ });
54793
+ }
54794
+ return heldHere !== null;
54642
54795
  }
54643
54796
  if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
54644
54797
  editorAskedAt = Date.now();
@@ -54674,7 +54827,7 @@ ${error.text}`;
54674
54827
  addon: true,
54675
54828
  budgetMs,
54676
54829
  ...budget.sizedToMs === undefined ? {} : { sizedToMs: budget.sizedToMs },
54677
- heldAt: this.dapClient?.whereItStopped() ?? null,
54830
+ heldAt: going === null || going.throughEditor ? this.dapClient?.whereItStopped() ?? null : endpoint === null && running && going.keeper !== undefined ? await this.keptHold(going.keeper) : null,
54678
54831
  running,
54679
54832
  withArgs,
54680
54833
  servesAProject: this.ownProject !== null,
@@ -56762,7 +56915,12 @@ ${error.text}`;
56762
56915
  return contained.response;
56763
56916
  }
56764
56917
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
56765
- const created = await this.operation("create_script", params, project.value.path);
56918
+ const stale = await this.refreshStaleClasses(project.value.path);
56919
+ if (!stale.ok) {
56920
+ return stale.response;
56921
+ }
56922
+ const loaded = await this.operation("create_script", params, project.value.path);
56923
+ const created = loaded.ok && stale.value.length > 0 ? { ...loaded, payload: { ...loaded.payload, classesRebuiltFirst: stale.value } } : loaded;
56766
56924
  const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
56767
56925
  if (!created.ok || declared === "") {
56768
56926
  return this.answer(created);
@@ -56799,8 +56957,26 @@ ${error.text}`;
56799
56957
  });
56800
56958
  }
56801
56959
  async handleModifyScript(args) {
56802
- const written = await this.headless("modify_script", args);
56803
56960
  const project = this.project(args);
56961
+ const stale = project.ok ? await this.refreshStaleClasses(project.value.path) : null;
56962
+ if (stale !== null && !stale.ok) {
56963
+ return stale.response;
56964
+ }
56965
+ const loaded = await this.headless("modify_script", args);
56966
+ const first = loaded.content[0];
56967
+ const written = stale === null || stale.value.length === 0 || loaded.isError === true || first?.type !== "text" || !first.text.trimStart().startsWith("{") ? loaded : {
56968
+ ...loaded,
56969
+ content: [
56970
+ {
56971
+ ...first,
56972
+ text: answerJson({
56973
+ ...asParams(JSON.parse(first.text)),
56974
+ classesRebuiltFirst: stale.value
56975
+ })
56976
+ },
56977
+ ...loaded.content.slice(1)
56978
+ ]
56979
+ };
56804
56980
  const scriptPath = readNonEmptyString(args, "scriptPath");
56805
56981
  if (written.isError === true || !project.ok || scriptPath === undefined || !this.editorHasOpen(project.value.path)) {
56806
56982
  return written;
@@ -57777,7 +57953,7 @@ async function runGodotServer() {
57777
57953
  const server = new GodotServer;
57778
57954
  await server.run();
57779
57955
  }
57780
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
57956
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
57781
57957
  var init_server = __esm(() => {
57782
57958
  init_dist2();
57783
57959
  init_stdio();
@@ -58781,7 +58957,7 @@ import {
58781
58957
  writeFileSync as writeFileSync12
58782
58958
  } from "node:fs";
58783
58959
  import { availableParallelism as availableParallelism4 } from "node:os";
58784
- import { dirname as dirname13, join as join27, relative as relative4, resolve as resolve10, sep as sep3 } from "node:path";
58960
+ import { dirname as dirname13, join as join27, relative as relative5, resolve as resolve10, sep as sep4 } from "node:path";
58785
58961
  import process14 from "node:process";
58786
58962
 
58787
58963
  // src/suite-fingerprint.ts
@@ -58946,6 +59122,7 @@ function getLocalVersion() {
58946
59122
  }
58947
59123
 
58948
59124
  // src/suite-run.ts
59125
+ var LAST_RUN_FILE = "last-run.json";
58949
59126
  var SLOWEST_NAMED = 5;
58950
59127
  var SILENCE_TIMES_TAKEN = 4;
58951
59128
  function silenceWhile(running, times, floorSeconds) {
@@ -59116,14 +59293,14 @@ class RunLogs {
59116
59293
  return this.kept ?? this.directory;
59117
59294
  }
59118
59295
  named(path) {
59119
- return this.kept === null ? path : join27(this.kept, relative4(this.directory, path));
59296
+ return this.kept === null ? path : join27(this.kept, relative5(this.directory, path));
59120
59297
  }
59121
59298
  keep() {
59122
59299
  if (this.kept === null) {
59123
59300
  return;
59124
59301
  }
59125
59302
  for (const file of readdirSync14(this.directory, { recursive: true, encoding: "utf8" })) {
59126
- if (file.endsWith(".log") && file.split(sep3).length === 2) {
59303
+ if (file.endsWith(".log") && file.split(sep4).length === 2) {
59127
59304
  mkdirSync10(dirname13(join27(this.kept, file)), { recursive: true });
59128
59305
  copyFileSync(join27(this.directory, file), join27(this.kept, file));
59129
59306
  }
@@ -59141,6 +59318,7 @@ async function runTestCommand(options, godotPath) {
59141
59318
  const state = join27(home, ".godot", "gdharness");
59142
59319
  const timesFile = join27(state, "suite-times.json");
59143
59320
  const passedFile = join27(state, "suite-passed.json");
59321
+ const lastRunFile = join27(state, LAST_RUN_FILE);
59144
59322
  let suites = selectedSuites(scripts, options);
59145
59323
  const uncollected = uncollectedSaid(scripts.uncollectedUnder(searchedRoots(options)));
59146
59324
  const wholeSuites = suites.map((suite) => scripts.suite(suite.path) ?? suite);
@@ -59188,7 +59366,7 @@ async function runTestCommand(options, godotPath) {
59188
59366
  const imported = await importProject(launch);
59189
59367
  if (imported !== null) {
59190
59368
  logs.keep();
59191
- writeEarlyResult(options.resultFile, startedAt, [`the import failed: ${imported}`], logs.shown);
59369
+ writeEarlyResult({ resultFile: options.resultFile, lastRunFile, engines: options.engines }, startedAt, [`the import failed: ${imported}`], logs.shown);
59192
59370
  console.log(`The import failed, so no suite ran: ${imported}`);
59193
59371
  return 1;
59194
59372
  }
@@ -59213,7 +59391,7 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59213
59391
  if (suites.length === 0) {
59214
59392
  console.log("Nothing has changed since every suite last passed.");
59215
59393
  rmSync12(runDirectory, { recursive: true, force: true });
59216
- writeEarlyResult(options.resultFile, startedAt, uncollected, null);
59394
+ writeEarlyResult({ resultFile: options.resultFile, lastRunFile, engines: options.engines }, startedAt, uncollected, null);
59217
59395
  if (uncollected.length === 0) {
59218
59396
  return 0;
59219
59397
  }
@@ -59239,26 +59417,31 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59239
59417
  const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
59240
59418
  const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
59241
59419
  const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(asked, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
59242
- console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59420
+ const dealing = [];
59421
+ const say = (line) => {
59422
+ dealing.push(line);
59423
+ console.log(line);
59424
+ };
59425
+ say(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
59243
59426
  const windowedAnyway = headlessInAWindow(plans, units);
59244
59427
  if (windowedAnyway.length > 0) {
59245
59428
  const one = windowedAnyway.length === 1;
59246
- console.log(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
59429
+ say(`${named(windowedAnyway)} ${one ? "says it runs" : "say they run"} headless and ${one ? "runs" : "run"} with a window here, since an ordered run is one engine and a suite in it needs a window. Run ${one ? "it" : "them"} without the windowed suites to run ${one ? "it" : "them"} headless.`);
59247
59430
  }
59248
59431
  const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
59249
59432
  if (plans.length > 1 && split.length > 0) {
59250
- console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59433
+ say(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
59251
59434
  }
59252
59435
  const byRows = [
59253
59436
  ...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
59254
59437
  ];
59255
59438
  if (plans.length > 1 && byRows.length > 0) {
59256
- console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59439
+ say(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
59257
59440
  }
59258
59441
  const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
59259
59442
  if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
59260
59443
  const one = keptWhole.length === 1;
59261
- console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59444
+ say(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
59262
59445
  }
59263
59446
  const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
59264
59447
  const taken = [];
@@ -59271,11 +59454,11 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59271
59454
  }
59272
59455
  const shared = options.order === null && lanes.length > 1 && taken.length < lanes.length;
59273
59456
  if (shared) {
59274
- console.log(sharesSaid(taken.length, lanes.length));
59457
+ say(sharesSaid(taken.length, lanes.length));
59275
59458
  }
59276
59459
  let ran = shared ? await runShares(launch, units, declared, lanes.length, taken, times) : (await Promise.all(lanes.map((lane, at) => runLane(launch, lane, times, taken[at] ?? null)))).flat().sort((a, b) => a.index - b.index);
59277
59460
  if (ran.some((engine) => engine.verdict.starved) && ran.some((engine) => !engine.verdict.passed)) {
59278
- console.log("An engine ran out of memory, so every engine that failed runs again, one at a time.");
59461
+ say("An engine ran out of memory, so every engine that failed runs again, one at a time.");
59279
59462
  const again = [];
59280
59463
  for (const engine of ran) {
59281
59464
  again.push(engine.verdict.passed ? engine : await runEngine2(launch, engine.plan, engine.index, times, "again"));
@@ -59285,7 +59468,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
59285
59468
  writeJson(timesFile, timesAfter(readTimes(timesFile), wholeSuites, ran.map((engine) => engine.printed), (path) => scripts.suite(path), new Set(ran.flatMap((engine) => engine.plan.suites.filter((suite) => (engine.printed.suites.get(suite.path)?.closed ?? null) === null).flatMap((suite) => suite.cases.map((func) => `${suite.path}:${func}`))))));
59286
59469
  const across = rowsAcross(ran.map((engine) => ({ handed: engine.plan.suites, printed: engine.printed })));
59287
59470
  recordPassed(passedFile, ran, printsOf, new Set(across.map((fault) => fault.suite)));
59288
- return report(ran, across.map((fault) => fault.fault), uncollected, startedAt, logs, options.printOutput, options.resultFile);
59471
+ return report(ran, across.map((fault) => fault.fault), uncollected, startedAt, logs, options.printOutput, {
59472
+ resultFile: options.resultFile,
59473
+ lastRunFile,
59474
+ dealing,
59475
+ engines: options.engines,
59476
+ slotsFree: taken.length
59477
+ });
59289
59478
  } finally {
59290
59479
  process14.off("SIGINT", interrupted);
59291
59480
  process14.off("SIGTERM", interrupted);
@@ -59450,7 +59639,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
59450
59639
  ...plan.windowed ? [] : ["--ignoreHeadlessMode"],
59451
59640
  "-c",
59452
59641
  "-rd",
59453
- `res://${relative4(launch.projectPath, join27(directory, "reports")).split(sep3).join("/")}`,
59642
+ `res://${relative5(launch.projectPath, join27(directory, "reports")).split(sep4).join("/")}`,
59454
59643
  "-rc",
59455
59644
  "1",
59456
59645
  "--",
@@ -59492,6 +59681,7 @@ async function runEngine2(launch, plan, index, times, again = "", slot = null) {
59492
59681
  printed,
59493
59682
  verdict: written === null ? judged : { ...judged, passed: false, faults: [...judged.faults, written] },
59494
59683
  log,
59684
+ began,
59495
59685
  seconds: (Date.now() - began) / 1000,
59496
59686
  again: again !== ""
59497
59687
  };
@@ -59502,11 +59692,11 @@ function reportsFault(reports) {
59502
59692
  if (files.length === 0) {
59503
59693
  return null;
59504
59694
  }
59505
- const shown = files.slice(0, REPORT_FILES_NAMED).map((file) => file.split(sep3).join("/"));
59695
+ const shown = files.slice(0, REPORT_FILES_NAMED).map((file) => file.split(sep4).join("/"));
59506
59696
  const more = files.length - shown.length;
59507
59697
  return `gdUnit4 wrote ${plural(files.length, "file")} into ${reports} (${shown.join(", ")}${more > 0 ? ` and ${String(more)} more` : ""}), a report the runner had turned off, so the gdUnit4 installed no longer honours a session hook marked off; report it to gdharness with gdUnit4's version`;
59508
59698
  }
59509
- function report(ran, across, uncollected, startedAt, logs, printOutput, resultFile) {
59699
+ function report(ran, across, uncollected, startedAt, logs, printOutput, targets) {
59510
59700
  if (printOutput) {
59511
59701
  for (const engine of ran) {
59512
59702
  console.log("");
@@ -59583,27 +59773,31 @@ ${readFileSync22(engine.log, "utf8")}`);
59583
59773
  } else {
59584
59774
  logs.keep();
59585
59775
  }
59586
- if (resultFile !== null) {
59587
- writeJson(resultFile, {
59588
- passed,
59589
- seconds: took,
59590
- problems: [
59591
- ...failed.flatMap((engine) => engine.verdict.faults.map((fault) => `engine ${String(engine.index)}: ${fault}`)),
59592
- ...across,
59593
- ...uncollected
59594
- ],
59595
- suites: closed,
59596
- engines: ran.map((engine) => ({
59597
- index: engine.index,
59598
- seconds: engine.seconds,
59599
- dealt: engine.plan.cost,
59600
- passed: engine.verdict.passed,
59601
- windowed: engine.plan.windowed,
59602
- log: passed ? null : logs.named(engine.log)
59603
- })),
59604
- logs: passed ? null : logs.shown
59605
- });
59606
- }
59776
+ writeResult(targets, {
59777
+ passed,
59778
+ seconds: took,
59779
+ problems: [
59780
+ ...failed.flatMap((engine) => engine.verdict.faults.map((fault) => `engine ${String(engine.index)}: ${fault}`)),
59781
+ ...across,
59782
+ ...uncollected
59783
+ ],
59784
+ suites: closed,
59785
+ engines: ran.map((engine) => ({
59786
+ index: engine.index,
59787
+ lane: engine.plan.lane,
59788
+ startedAfter: (engine.began - startedAt) / 1000,
59789
+ seconds: engine.seconds,
59790
+ dealt: engine.plan.cost,
59791
+ passed: engine.verdict.passed,
59792
+ windowed: engine.plan.windowed,
59793
+ suites: engine.plan.suites.map((suite) => suite.path),
59794
+ log: passed ? null : logs.named(engine.log)
59795
+ })),
59796
+ logs: passed ? null : logs.shown,
59797
+ enginesAsked: targets.engines,
59798
+ slotsFree: targets.slotsFree,
59799
+ dealing: targets.dealing
59800
+ });
59607
59801
  if (passed) {
59608
59802
  console.log(`Passed: ${totals}`);
59609
59803
  return 0;
@@ -59613,16 +59807,23 @@ ${readFileSync22(engine.log, "utf8")}`);
59613
59807
  console.log(`Every engine's output is kept under ${logs.shown} until ${String(KEPT_RUN_DIRECTORIES)} later runs have failed.`);
59614
59808
  return 1;
59615
59809
  }
59616
- function writeEarlyResult(file, startedAt, problems, logs) {
59617
- if (file !== null) {
59618
- writeJson(file, {
59619
- passed: problems.length === 0,
59620
- seconds: (Date.now() - startedAt) / 1000,
59621
- problems,
59622
- suites: [],
59623
- engines: [],
59624
- logs
59625
- });
59810
+ function writeEarlyResult(targets, startedAt, problems, logs) {
59811
+ writeResult(targets, {
59812
+ passed: problems.length === 0,
59813
+ seconds: (Date.now() - startedAt) / 1000,
59814
+ problems,
59815
+ suites: [],
59816
+ engines: [],
59817
+ logs,
59818
+ enginesAsked: targets.engines,
59819
+ slotsFree: 0,
59820
+ dealing: []
59821
+ });
59822
+ }
59823
+ function writeResult(targets, result) {
59824
+ writeJson(targets.lastRunFile, result);
59825
+ if (targets.resultFile !== null) {
59826
+ writeJson(targets.resultFile, result);
59626
59827
  }
59627
59828
  }
59628
59829
  function printUncollected(uncollected) {