gdharness 1.2.35 → 1.2.37

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 (28) hide show
  1. package/build/cli.js +478 -100
  2. package/build/godot/addons/gdharness_editor/headless_check.gd +374 -0
  3. package/build/godot/addons/gdharness_editor/headless_check.gd.uid +1 -0
  4. package/build/godot/addons/gdharness_editor/property_values.gd +3 -0
  5. package/build/godot/addons/gdharness_editor/scene_file.gd +345 -10
  6. package/build/godot/addons/gdharness_editor/serialisation.gd +103 -5
  7. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -18
  8. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +24 -13
  9. package/build/godot/addons/gdharness_runtime/call_errors.gd +7 -2
  10. package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +9 -5
  11. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +33 -0
  12. package/build/godot/addons/gdharness_runtime/runtime_changes.gd +21 -4
  13. package/build/godot/addons/gdharness_runtime/runtime_targets.gd +7 -5
  14. package/build/godot/addons/gdharness_runtime/runtime_values.gd +5 -0
  15. package/build/godot/addons/gdharness_runtime/runtime_waits.gd +116 -3
  16. package/build/godot/addons/gdharness_runtime/serialisation.gd +103 -5
  17. package/build/godot/operations/classdb_queries.gd +22 -0
  18. package/build/godot/operations/dependencies.gd +90 -20
  19. package/build/godot/operations/editor_tools.gd +57 -0
  20. package/build/godot/operations/gdscript_analysis.gd +45 -2
  21. package/build/godot/operations/gdscript_authoring.gd +5 -2
  22. package/build/godot/operations/godot_operations.gd +53 -2
  23. package/build/godot/operations/property_values.gd +3 -0
  24. package/build/godot/operations/rename_checks.gd +0 -19
  25. package/build/godot/operations/script_check.gd +98 -0
  26. package/build/godot/operations/serialisation.gd +103 -5
  27. package/build/index.js +466 -90
  28. package/package.json +1 -1
package/build/cli.js CHANGED
@@ -8487,6 +8487,7 @@ var init_scratch = __esm(() => {
8487
8487
  waiting = new Set;
8488
8488
  atExit = new Set;
8489
8489
  KINDS = [
8490
+ "answers",
8490
8491
  "desktop",
8491
8492
  "export",
8492
8493
  "import",
@@ -8509,8 +8510,14 @@ function snakeCased(params) {
8509
8510
  }
8510
8511
  function engineExtras(outcome) {
8511
8512
  const after = outcome.afterAnswerMessages ?? [];
8513
+ const before = outcome.beforeOperationMessages ?? [];
8512
8514
  return {
8513
8515
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
8516
+ ...before.length > 0 ? {
8517
+ engine_messages_before_operation: before,
8518
+ engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
8519
+ } : {},
8520
+ ...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
8514
8521
  ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
8515
8522
  ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
8516
8523
  ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
@@ -8655,12 +8662,22 @@ async function runOperation(engine, operation, params, projectPath) {
8655
8662
  discard(paramsDir);
8656
8663
  }
8657
8664
  const everything = ran.log.everything();
8658
- const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
8659
- const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
8665
+ const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
8666
+ const answered = marked(ANSWERED);
8667
+ const waiting = marked(WAITING);
8668
+ const begun = marked(BEGUN);
8669
+ const ended = answered === -1 ? everything.length : answered;
8670
+ const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
8671
+ ...everything.slice(0, waiting),
8672
+ ...begun === -1 ? [] : everything.slice(begun + 1, ended)
8673
+ ]);
8674
+ const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
8660
8675
  const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
8661
8676
  const afterAnswered = {
8662
8677
  ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
8663
- ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
8678
+ ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
8679
+ ...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
8680
+ ...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
8664
8681
  };
8665
8682
  if (answer.kind === "unreadable") {
8666
8683
  return {
@@ -8671,6 +8688,14 @@ async function runOperation(engine, operation, params, projectPath) {
8671
8688
  };
8672
8689
  }
8673
8690
  if (answer.kind === "none") {
8691
+ if (waiting !== -1 && begun === -1) {
8692
+ return {
8693
+ ok: false,
8694
+ message: `${operation} never ran: the engine ended (${howItEnded(ran)}) while it started the project's ` + `autoloads, which this operation waits for. The last it reported: ${reason(ran.log, startup)}`,
8695
+ ...reported2(problems),
8696
+ ...afterAnswered
8697
+ };
8698
+ }
8674
8699
  const refusals = refusalsIn(ran.log);
8675
8700
  if (refusals.length > 0) {
8676
8701
  const said = new Set(refusals.map((entry) => entry.index));
@@ -8720,7 +8745,7 @@ function answerIn(file) {
8720
8745
  };
8721
8746
  }
8722
8747
  }
8723
- var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8748
+ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", MESSAGES_KEPT = 200, ERRORS_NAMED = 3;
8724
8749
  var init_headless = __esm(() => {
8725
8750
  init_engine_run();
8726
8751
  init_library_copies();
@@ -11141,7 +11166,7 @@ function annotationsOf(spec) {
11141
11166
  openWorldHint: false
11142
11167
  };
11143
11168
  }
11144
- var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
11169
+ var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, 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 anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
11145
11170
  var init_tool_definitions = __esm(() => {
11146
11171
  init_launch();
11147
11172
  PROJECT_INFO_SECTIONS = [
@@ -11179,7 +11204,7 @@ var init_tool_definitions = __esm(() => {
11179
11204
  };
11180
11205
  PROPERTIES = {
11181
11206
  type: "object",
11182
- description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
11207
+ description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds. ${WHOLE_NUMBERS}`,
11183
11208
  additionalProperties: true
11184
11209
  };
11185
11210
  XY = {
@@ -11223,7 +11248,10 @@ var init_tool_definitions = __esm(() => {
11223
11248
  name: { type: "string" },
11224
11249
  params: { type: "string", description: 'Functions and signals: the parameter list, "delta: float".' },
11225
11250
  returnType: { type: "string", description: "Functions: the return type." },
11226
- body: { type: "string", description: "Functions: the body." },
11251
+ body: {
11252
+ type: "string",
11253
+ 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."
11254
+ },
11227
11255
  varType: {
11228
11256
  type: "string",
11229
11257
  description: "Variables and constants: the type annotation; without one, the type the value evaluates to."
@@ -11326,7 +11354,7 @@ var init_tool_definitions = __esm(() => {
11326
11354
  {
11327
11355
  name: "project_settings",
11328
11356
  title: "Project settings",
11329
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. 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, one on an addon from before this, 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 deadzone, 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. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. 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.",
11357
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. 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, one on an addon from before this, 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 deadzone, 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. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. 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.",
11330
11358
  parameters: {
11331
11359
  projectPath: PROJECT_PATH,
11332
11360
  setting: {
@@ -11483,7 +11511,7 @@ var init_tool_definitions = __esm(() => {
11483
11511
  direction: {
11484
11512
  type: "string",
11485
11513
  enum: ["forward", "reverse"],
11486
- description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind, and two of them are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
11514
+ description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it, and total_references counts the entries in the tree. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
11487
11515
  },
11488
11516
  depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
11489
11517
  includeBuiltin: {
@@ -11504,24 +11532,32 @@ var init_tool_definitions = __esm(() => {
11504
11532
  description: "The import pipeline: what needs importing, how a resource is imported, reimports, UIDs, and the global class list the editor and the engine read.",
11505
11533
  parameters: {
11506
11534
  projectPath: PROJECT_PATH,
11507
- resourcePath: RESOURCE_PATH,
11535
+ resourcePath: { ...RESOURCE_PATH, ops: ["status", "options", "set_options", "uid", "reimport"] },
11508
11536
  includeUpToDate: {
11509
11537
  type: "boolean",
11538
+ ops: ["status"],
11510
11539
  description: "status: list resources that are current as well. Default false."
11511
11540
  },
11512
11541
  options: {
11513
11542
  type: "object",
11543
+ ops: ["set_options"],
11514
11544
  description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
11515
11545
  additionalProperties: true
11516
11546
  },
11517
11547
  reimport: {
11518
11548
  type: "boolean",
11549
+ ops: ["set_options"],
11519
11550
  description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
11520
11551
  },
11521
- force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
11552
+ force: {
11553
+ type: "boolean",
11554
+ ops: ["reimport"],
11555
+ description: "reimport: reimport even what is current. Default false."
11556
+ },
11522
11557
  timeoutMs: {
11523
11558
  type: "integer",
11524
11559
  minimum: 1,
11560
+ ops: ["reimport", "set_options"],
11525
11561
  description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
11526
11562
  }
11527
11563
  },
@@ -11599,7 +11635,7 @@ var init_tool_definitions = __esm(() => {
11599
11635
  name: "project_test",
11600
11636
  title: "Run tests",
11601
11637
  effect: "additive",
11602
- description: "Runs the project's gdUnit4 tests headless 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, every report 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. 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 rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 nodes were left in the tree 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
11638
+ description: "Runs the project's gdUnit4 tests headless 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 rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it 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. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so 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. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. 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 nodes were left in the tree 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 does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
11603
11639
  parameters: {
11604
11640
  projectPath: PROJECT_PATH,
11605
11641
  path: {
@@ -11636,7 +11672,7 @@ var init_tool_definitions = __esm(() => {
11636
11672
  {
11637
11673
  name: "scene_create",
11638
11674
  title: "Create or save a scene",
11639
- description: "Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. Needs the editor connected.",
11675
+ description: `Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. ${WITHOUT_AN_EDITOR}`,
11640
11676
  parameters: {
11641
11677
  projectPath: PROJECT_PATH,
11642
11678
  scenePath: SCENE_PATH,
@@ -11681,17 +11717,19 @@ var init_tool_definitions = __esm(() => {
11681
11717
  {
11682
11718
  name: "scene_node",
11683
11719
  title: "Scene nodes",
11684
- description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type 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. set with name renames a node. 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, and for a node that belongs to a scene this one instances or inherits. Needs the editor connected.",
11720
+ description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type 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. 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, and for a node that belongs to a scene this one instances or inherits. ${WITHOUT_AN_EDITOR}`,
11685
11721
  parameters: {
11686
11722
  projectPath: PROJECT_PATH,
11687
11723
  scenePath: SCENE_PATH,
11688
- nodePath: NODE_PATH,
11724
+ nodePath: { ...NODE_PATH, ops: ["get", "set", "duplicate", "reparent", "delete", "set_tilemap_cells"] },
11689
11725
  parentNodePath: {
11690
11726
  type: "string",
11727
+ ops: ["add", "duplicate"],
11691
11728
  description: "add: where the node goes. Default the root. duplicate: where the copy goes."
11692
11729
  },
11693
11730
  nodeType: {
11694
11731
  type: "string",
11732
+ ops: ["add"],
11695
11733
  description: 'add: the node class, such as "CharacterBody2D". Give this or instancePath.'
11696
11734
  },
11697
11735
  instancePath: {
@@ -11699,11 +11737,12 @@ var init_tool_definitions = __esm(() => {
11699
11737
  ops: ["add"],
11700
11738
  description: `add: a scene to instance as the new node instead of a class, such as "ui/panel.tscn". It is saved as an instance of that file, the way the editor's Instantiate Child Scene saves one, so changes to that scene reach it; properties set its root. A scene that is the target or instances it is refused.`
11701
11739
  },
11702
- nodeName: { type: "string", description: "add: the new node's name." },
11703
- properties: PROPERTIES,
11704
- newName: { type: "string", description: "duplicate: the copy's name." },
11740
+ nodeName: { type: "string", ops: ["add"], description: "add: the new node's name." },
11741
+ properties: { ...PROPERTIES, ops: ["add", "set"] },
11742
+ newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
11705
11743
  newParentPath: {
11706
11744
  type: "string",
11745
+ ops: ["reparent"],
11707
11746
  description: "reparent: the new parent, which may be the one it has, to move it among its siblings."
11708
11747
  },
11709
11748
  index: {
@@ -11714,13 +11753,15 @@ var init_tool_definitions = __esm(() => {
11714
11753
  },
11715
11754
  includeDefaults: {
11716
11755
  type: "boolean",
11756
+ ops: ["get"],
11717
11757
  description: "get: include properties still at their default. Default false."
11718
11758
  },
11719
11759
  layer: {
11720
11760
  type: "number",
11761
+ ops: ["set_tilemap_cells"],
11721
11762
  description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
11722
11763
  },
11723
- cells: TILEMAP_CELLS
11764
+ cells: { ...TILEMAP_CELLS, ops: ["set_tilemap_cells"] }
11724
11765
  },
11725
11766
  requires: ["projectPath", "scenePath"],
11726
11767
  operations: {
@@ -11760,16 +11801,32 @@ var init_tool_definitions = __esm(() => {
11760
11801
  {
11761
11802
  name: "scene_signal",
11762
11803
  title: "Scene signals",
11763
- 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 connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. Needs the editor connected.",
11804
+ 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 connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. ${WITHOUT_AN_EDITOR}`,
11764
11805
  parameters: {
11765
11806
  projectPath: PROJECT_PATH,
11766
11807
  scenePath: SCENE_PATH,
11767
- sourceNodePath: { type: "string", description: "The node that emits." },
11768
- signalName: { type: "string", description: 'The signal it emits, such as "pressed".' },
11769
- targetNodePath: { type: "string", description: "The node whose method is called." },
11770
- methodName: { type: "string", description: "The method on that node, called when it emits." },
11771
- flags: { type: "number", description: "connect: Object.ConnectFlags, such as 1 for deferred." },
11772
- nodePath: { type: "string", description: "list: only connections involving this node." }
11808
+ sourceNodePath: { type: "string", ops: ["connect", "disconnect"], description: "The node that emits." },
11809
+ signalName: {
11810
+ type: "string",
11811
+ ops: ["connect", "disconnect"],
11812
+ description: 'The signal it emits, such as "pressed".'
11813
+ },
11814
+ targetNodePath: {
11815
+ type: "string",
11816
+ ops: ["connect", "disconnect"],
11817
+ description: "The node whose method is called."
11818
+ },
11819
+ methodName: {
11820
+ type: "string",
11821
+ ops: ["connect", "disconnect"],
11822
+ description: "The method on that node, called when it emits."
11823
+ },
11824
+ flags: {
11825
+ type: "number",
11826
+ ops: ["connect"],
11827
+ description: "connect: Object.ConnectFlags, such as 1 for deferred."
11828
+ },
11829
+ nodePath: { type: "string", ops: ["list"], description: "list: only connections involving this node." }
11773
11830
  },
11774
11831
  requires: ["projectPath", "scenePath"],
11775
11832
  operations: {
@@ -11894,18 +11951,28 @@ var init_tool_definitions = __esm(() => {
11894
11951
  parameters: {
11895
11952
  projectPath: PROJECT_PATH,
11896
11953
  scriptPath: SCRIPT_PATH,
11897
- className: { type: "string", description: "create: a class_name for the script. Not with content." },
11898
- extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
11954
+ className: {
11955
+ type: "string",
11956
+ ops: ["create"],
11957
+ description: "create: a class_name for the script. Not with content."
11958
+ },
11959
+ extends: {
11960
+ type: "string",
11961
+ ops: ["create"],
11962
+ description: "create: the base class. Default Node. Not with content."
11963
+ },
11899
11964
  content: {
11900
11965
  type: "string",
11966
+ ops: ["create"],
11901
11967
  description: "create: the whole file, written as given, instead of a template, className and extends."
11902
11968
  },
11903
11969
  template: {
11904
11970
  type: "string",
11905
11971
  enum: ["singleton", "state_machine", "component", "resource"],
11972
+ ops: ["create"],
11906
11973
  description: "create: a starting shape."
11907
11974
  },
11908
- modifications: SCRIPT_MODIFICATIONS,
11975
+ modifications: { ...SCRIPT_MODIFICATIONS, ops: ["modify"] },
11909
11976
  symbol: {
11910
11977
  type: "string",
11911
11978
  ops: ["rename"],
@@ -11943,16 +12010,19 @@ var init_tool_definitions = __esm(() => {
11943
12010
  scriptPath: SCRIPT_PATH,
11944
12011
  includeInherited: {
11945
12012
  type: "boolean",
12013
+ ops: ["structure"],
11946
12014
  description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
11947
12015
  },
11948
12016
  line: {
11949
12017
  type: "integer",
11950
12018
  minimum: 1,
12019
+ ops: ["completion", "hover"],
11951
12020
  description: "completion, hover: the line, counted from 1 as the editor shows it."
11952
12021
  },
11953
12022
  character: {
11954
12023
  type: "integer",
11955
12024
  minimum: 1,
12025
+ ops: ["completion", "hover"],
11956
12026
  description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
11957
12027
  }
11958
12028
  },
@@ -11960,7 +12030,7 @@ var init_tool_definitions = __esm(() => {
11960
12030
  operations: {
11961
12031
  structure: {
11962
12032
  effect: "read",
11963
- summary: "functions, variables, signals, class_name and extends, read from the file",
12033
+ summary: "functions, variables, signals, class_name and extends, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
11964
12034
  requires: []
11965
12035
  },
11966
12036
  symbols: {
@@ -11981,7 +12051,7 @@ var init_tool_definitions = __esm(() => {
11981
12051
  name: "script_diagnostics",
11982
12052
  title: "Script diagnostics",
11983
12053
  effect: "read",
11984
- description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. Needs the editor running. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
12054
+ description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, and warnings null, since the engine reports warnings only to an editor. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
11985
12055
  parameters: {
11986
12056
  projectPath: PROJECT_PATH,
11987
12057
  scriptPath: {
@@ -11995,7 +12065,7 @@ var init_tool_definitions = __esm(() => {
11995
12065
  {
11996
12066
  name: "resource_edit",
11997
12067
  title: "Edit resources",
11998
- 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, and a Theme lists its items there as "Button/colors/font_color". 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 need the editor connected.',
12068
+ 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, and a Theme lists its items there as "Button/colors/font_color". 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.',
11999
12069
  parameters: {
12000
12070
  projectPath: PROJECT_PATH,
12001
12071
  resourcePath: {
@@ -12346,9 +12416,10 @@ var init_tool_definitions = __esm(() => {
12346
12416
  description: "The engine's ClassDB: find classes, read one in full, or walk an inheritance tree.",
12347
12417
  parameters: {
12348
12418
  projectPath: PROJECT_PATH,
12349
- filter: { type: "string", description: "query: a substring of the class name." },
12419
+ filter: { type: "string", ops: ["query"], description: "query: a substring of the class name." },
12350
12420
  category: {
12351
12421
  type: "string",
12422
+ ops: ["query"],
12352
12423
  enum: [
12353
12424
  "node",
12354
12425
  "node2d",
@@ -12365,9 +12436,21 @@ var init_tool_definitions = __esm(() => {
12365
12436
  ],
12366
12437
  description: "query: limit to one family."
12367
12438
  },
12368
- instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
12369
- className: { type: "string", description: "info, inheritance: the class." },
12370
- includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
12439
+ instantiableOnly: {
12440
+ type: "boolean",
12441
+ ops: ["query"],
12442
+ description: "query: leave out abstract classes. Default false."
12443
+ },
12444
+ className: {
12445
+ type: "string",
12446
+ ops: ["info", "inheritance"],
12447
+ description: "info, inheritance: the class."
12448
+ },
12449
+ includeInherited: {
12450
+ type: "boolean",
12451
+ ops: ["info"],
12452
+ description: "info: include inherited members. Default false."
12453
+ },
12371
12454
  member: {
12372
12455
  type: "string",
12373
12456
  ops: ["info"],
@@ -12439,7 +12522,7 @@ var init_tool_definitions = __esm(() => {
12439
12522
  says: {
12440
12523
  type: "string",
12441
12524
  ops: ["find", "accessibility"],
12442
- description: 'accessibility: only the elements whose name or value says these words, matched as find matches them, with what they hold. find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
12525
+ description: 'accessibility: only the elements whose name, value or value_text says these words, matched as find matches them, with what they hold. find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
12443
12526
  },
12444
12527
  limit: {
12445
12528
  type: "integer",
@@ -12470,7 +12553,7 @@ var init_tool_definitions = __esm(() => {
12470
12553
  tree: { effect: "read", summary: "the live scene tree", requires: [] },
12471
12554
  text: {
12472
12555
  effect: "read",
12473
- summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
12556
+ summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
12474
12557
  requires: []
12475
12558
  },
12476
12559
  find: {
@@ -12490,12 +12573,12 @@ var init_tool_definitions = __esm(() => {
12490
12573
  },
12491
12574
  metrics: {
12492
12575
  effect: "read",
12493
- summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
12576
+ summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. Memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
12494
12577
  requires: []
12495
12578
  },
12496
12579
  accessibility: {
12497
12580
  effect: "read",
12498
- summary: "what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one with the focus. node is the node that made the element; an element a node made as a part of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name is read as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed run on Windows started with engineArgs [\"--accessibility\", \"always\"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two",
12581
+ summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader does, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, value_text (a range's spoken words, such as "75%"), description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the focused one. node is the node that made the element; a part a node made of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name reads as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed Windows run started with engineArgs ["--accessibility", "always"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two`,
12499
12582
  requires: []
12500
12583
  }
12501
12584
  },
@@ -12504,7 +12587,7 @@ var init_tool_definitions = __esm(() => {
12504
12587
  {
12505
12588
  name: "runtime_invoke",
12506
12589
  title: "Set or call in the running game",
12507
- description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
12590
+ description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors the engine reported while a called method ran come back under errors and warnings under warnings, the first 20 of each, with errors_not_shown and warnings_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
12508
12591
  parameters: {
12509
12592
  projectPath: RUNNING_PROJECT_PATH,
12510
12593
  pid: RUNNING_PID,
@@ -12521,7 +12604,7 @@ var init_tool_definitions = __esm(() => {
12521
12604
  value: {
12522
12605
  ops: ["set"],
12523
12606
  blank: true,
12524
- description: `set: the value, fitted to the property's type. A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12607
+ description: `set: the value, fitted to the property's type. ${WHOLE_NUMBERS} A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
12525
12608
  },
12526
12609
  method: {
12527
12610
  type: "string",
@@ -12531,7 +12614,7 @@ var init_tool_definitions = __esm(() => {
12531
12614
  args: {
12532
12615
  type: "array",
12533
12616
  ops: ["call"],
12534
- description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12617
+ description: `call: the arguments, fitted to the method's parameter types. An RID is passed as the {"_type": "RID", "id": ...} a read answers it with, such as a viewport's for RenderingServer's render timing. ${WHOLE_NUMBERS} A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
12535
12618
  },
12536
12619
  properties: {
12537
12620
  type: ["boolean", "array"],
@@ -12624,7 +12707,7 @@ var init_tool_definitions = __esm(() => {
12624
12707
  {
12625
12708
  name: "runtime_input",
12626
12709
  title: "Send input to the game",
12627
- description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
12710
+ description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
12628
12711
  answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
12629
12712
  parameters: {
12630
12713
  projectPath: RUNNING_PROJECT_PATH,
@@ -12821,10 +12904,16 @@ var init_tool_definitions = __esm(() => {
12821
12904
  },
12822
12905
  nodePath: {
12823
12906
  type: "string",
12824
- ops: ["signal", "until"],
12825
- description: "signal, until: the node. Needed for a signal and for a property; for says it defaults to /root, the whole screen."
12907
+ ops: ["signal", "watch", "until"],
12908
+ description: "signal, watch, until: the node. Needed for a signal without a watch, a watch and a property; for says it defaults to /root, the whole screen."
12909
+ },
12910
+ signal: { type: "string", ops: ["signal", "watch"], description: "signal, watch: the signal name." },
12911
+ watch: {
12912
+ type: "integer",
12913
+ minimum: 1,
12914
+ ops: ["signal"],
12915
+ description: "signal: the watch an earlier watch answered, in place of nodePath and signal. Answers at once, with fired_before_wait, when the signal fired after the watch was armed, and otherwise waits for it; times counts how often it fired, and args is what the first carried. The watch goes with the answer."
12826
12916
  },
12827
- signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
12828
12917
  property: {
12829
12918
  type: "string",
12830
12919
  ops: ["until"],
@@ -12864,6 +12953,11 @@ var init_tool_definitions = __esm(() => {
12864
12953
  signal: {
12865
12954
  effect: "read",
12866
12955
  summary: "wait for a signal and answer with what it carried",
12956
+ requires: []
12957
+ },
12958
+ watch: {
12959
+ effect: "read",
12960
+ summary: "start recording a signal and answer at once with a watch; arm it before the call meant to set the signal off, then collect it with signal and the watch. A client can run a call sent beside a wait only after the wait answers, so a wait sent with the call misses it",
12867
12961
  requires: ["nodePath", "signal"]
12868
12962
  },
12869
12963
  until: {
@@ -13024,8 +13118,11 @@ function headlessSection() {
13024
13118
  "had none. The reading ops are the ones to gate on, and which is which is in each",
13025
13119
  "op's own summary below rather than assumed from this list.",
13026
13120
  "",
13027
- "Everything else needs the editor (`editor_*`, `debug_*`, `scene_*`, `resource_*`) or a running",
13028
- "game (`runtime_*`).",
13121
+ "`scene_create`, `scene_node`, `scene_signal` and `resource_edit` go through the editor when one",
13122
+ "serves the project and through a headless engine when none does, so they need nothing open",
13123
+ "either; a gate can call them, and the reads among them are the ones to gate on. Everything else",
13124
+ "needs the editor (`editor_*`, `debug_*`, `scene_animation` but its reads) or a running game",
13125
+ "(`runtime_*`).",
13029
13126
  "",
13030
13127
  'One of these can be pointed the other way: `project_settings get` takes `from: "editor"` to ask',
13031
13128
  "the open editor instead of the file, which is a different reading and costs no engine start. A",
@@ -43091,6 +43188,29 @@ ${note}`;
43091
43188
  return detail === entry.detail ? entry : { ...entry, detail };
43092
43189
  });
43093
43190
  }
43191
+ function shortened(detail) {
43192
+ if (detail === null || detail.length <= LONGEST_DETAIL) {
43193
+ return detail;
43194
+ }
43195
+ return `${detail.slice(0, LONGEST_DETAIL)}
43196
+ [${detail.length - LONGEST_DETAIL} more characters left out]`;
43197
+ }
43198
+ function foldedReports(reports) {
43199
+ const first = new Map;
43200
+ for (const report of reports) {
43201
+ const key = JSON.stringify([report.kind, report.message, report.detail]);
43202
+ const seen = first.get(key);
43203
+ if (seen === undefined) {
43204
+ first.set(key, { report, times: 1 });
43205
+ } else {
43206
+ seen.times += 1;
43207
+ }
43208
+ }
43209
+ return [...first.values()].map(({ report, times }) => {
43210
+ const kept = { ...report, detail: shortened(report.detail) };
43211
+ return times === 1 ? kept : { ...kept, times };
43212
+ });
43213
+ }
43094
43214
  function failedCases(report, printed) {
43095
43215
  const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
43096
43216
  const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
@@ -43098,6 +43218,8 @@ function failedCases(report, printed) {
43098
43218
  return cases.map(({ entry, path }) => {
43099
43219
  const reports = read.slice(next, next + entry.reports.length);
43100
43220
  next += entry.reports.length;
43221
+ const folded = foldedReports(reports);
43222
+ const shown = folded.slice(0, MOST_REPORTS_SHOWN);
43101
43223
  return {
43102
43224
  suite: entry.suite,
43103
43225
  name: entry.name,
@@ -43105,11 +43227,34 @@ function failedCases(report, printed) {
43105
43227
  time: entry.time,
43106
43228
  path,
43107
43229
  message: reports[0]?.message ?? null,
43108
- detail: reports[0]?.detail ?? null,
43109
- ...reports.length > 1 ? { reports } : {}
43230
+ detail: shortened(reports[0]?.detail ?? null),
43231
+ ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
43232
+ ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
43110
43233
  };
43111
43234
  });
43112
43235
  }
43236
+ function failedWithin(failed, budget) {
43237
+ const sizes = failed.map((entry) => JSON.stringify(entry).length);
43238
+ const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
43239
+ const whole = new Set;
43240
+ let spent = 0;
43241
+ for (const index of smallestFirst) {
43242
+ const size = sizes[index] ?? 0;
43243
+ if (spent + size > budget) {
43244
+ break;
43245
+ }
43246
+ spent += size;
43247
+ whole.add(index);
43248
+ }
43249
+ const kept = failed.map((entry, index) => {
43250
+ if (whole.has(index)) {
43251
+ return entry;
43252
+ }
43253
+ const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
43254
+ return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
43255
+ });
43256
+ return { failed: kept, briefed: failed.length - whole.size };
43257
+ }
43113
43258
  function masked(expected, actual) {
43114
43259
  const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
43115
43260
  return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
@@ -43188,7 +43333,7 @@ function testProgressMessage(printed, elapsedMs) {
43188
43333
  const done = finished.length === 0 ? "No suite has finished" : `${finished.length === 1 ? "One suite has" : `${finished.length} suites have`} finished${failing === 0 ? "" : `, ${failing} with failures`}`;
43189
43334
  return `${done}${running === null ? "" : `; running ${running}`}; ${took} in.`;
43190
43335
  }
43191
- var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
43336
+ var MalformedReportError, ENTITIES, ATTRIBUTE, HOOK_FAILURE, NOTHING_RAN, NO_SUCH_PATH, DISCOVERY_FAILED = "Script errors were detected during test discovery!", ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, IGNORING_CASE = " \\(ignoring case\\)", PRINTED_ACTUAL, QUOTED_EQUALITY, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RENDERED_AWAY_NOTE = '[gdharness: the two sides print alike, so what tells them apart is something gdUnit4 does not print. The usual cause is BBCode in the expected string, which gdUnit4 renders as formatting instead of showing; the value after "but was" is the one the assertion found.]', MOST_REPORTS_SHOWN = 20, LONGEST_DETAIL = 20000, MASKED_BRACKET = "[lb]", PRINTED_DICTIONARY, RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary", SUITE_COUNTS, CASE_FAILED;
43192
43337
  var init_junit = __esm(() => {
43193
43338
  MalformedReportError = class MalformedReportError extends Error {
43194
43339
  constructor(message) {
@@ -44334,7 +44479,8 @@ import {
44334
44479
  readFileSync as readFileSync26,
44335
44480
  readSync as readSync2,
44336
44481
  rmdirSync as rmdirSync2,
44337
- statSync as statSync13
44482
+ statSync as statSync13,
44483
+ writeFileSync as writeFileSync18
44338
44484
  } from "node:fs";
44339
44485
  import { basename as basename9, dirname as dirname22, extname as extname4, isAbsolute as isAbsolute4, join as join30, normalize as normalize2, relative as relative6 } from "node:path";
44340
44486
  import { setTimeout as delay5 } from "node:timers/promises";
@@ -44595,6 +44741,12 @@ function endedWithoutACode(run) {
44595
44741
  }
44596
44742
  return `The editor stopped playing this run. ${kept} What it printed is below.`;
44597
44743
  }
44744
+ function ourRunEnded(run, now) {
44745
+ const which = `${run.throughEditor ? "played in the editor" : "started"}${run.pid === null ? "" : ` (pid ${run.pid})`}`;
44746
+ const when = run.endedAt === undefined ? "" : ` ${Math.max(0, Math.round((now - run.endedAt) / 1000))}s ago`;
44747
+ const how = run.endedThrough === "close" ? "this server asked it to close" : run.endedThrough === "keeper" ? "this server ended it through the keeper holding it" : run.endedThrough === "signal" ? "this server signalled it" : run.endedThrough === "editor" ? "this server asked the editor to stop it" : run.exitCode !== null ? `it exited by itself with code ${run.exitCode}` : run.exitSignal === null ? "it is no longer running" : `it was ended by ${run.exitSignal}`;
44748
+ return `No game with the runtime addon is running: the game this server ${which} ended${when}, and ${how}. editor_output has what it printed, and editor_run start runs it again.`;
44749
+ }
44598
44750
  function stopVerdict(stop) {
44599
44751
  const notClosed = stop.notClosedBecause === undefined ? "" : ` It was asked to close first and was not ended by that: ${stop.notClosedBecause}.`;
44600
44752
  if (!stop.wasRunning) {
@@ -45521,6 +45673,13 @@ function scriptsToDiagnose(projectPath, given) {
45521
45673
  }
45522
45674
  return { ok: true, value: unique };
45523
45675
  }
45676
+ function engineChecked(why) {
45677
+ return {
45678
+ checkedBy: "engine",
45679
+ languageServer: why,
45680
+ note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, and no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both."
45681
+ };
45682
+ }
45524
45683
  function isDirectory(path) {
45525
45684
  try {
45526
45685
  return statSync13(path).isDirectory();
@@ -45607,6 +45766,7 @@ class GodotServer {
45607
45766
  inFlight = new Map;
45608
45767
  callsOnGames = new Set;
45609
45768
  callsTaken = 0;
45769
+ answersDirectory;
45610
45770
  clientNoted = null;
45611
45771
  parentCommand = null;
45612
45772
  endNoted = false;
@@ -46005,12 +46165,66 @@ class GodotServer {
46005
46165
  this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
46006
46166
  try {
46007
46167
  const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
46008
- return this.withFeedbackNotice(this.withUpdateNotice(answer));
46168
+ return this.withinTheClientsLimit(spec.name, this.withFeedbackNotice(this.withUpdateNotice(answer)));
46009
46169
  } finally {
46010
46170
  this.inFlight.delete(call);
46011
46171
  }
46012
46172
  });
46013
46173
  }
46174
+ withinTheClientsLimit(tool, answer) {
46175
+ const sizeOf = (item) => item.type === "text" ? Buffer.byteLength(item.text) : item.data.length;
46176
+ const bytes = answer.content.reduce((sum, item) => sum + sizeOf(item), 0);
46177
+ if (bytes <= ANSWER_BUDGET) {
46178
+ return answer;
46179
+ }
46180
+ this.answersDirectory ??= scratchDirectory("answers");
46181
+ const directory = this.answersDirectory;
46182
+ const stem = `${tool}-${Date.now()}`;
46183
+ let savedAt;
46184
+ let notSaved;
46185
+ try {
46186
+ savedAt = answer.content.map((item, index) => {
46187
+ const file = join30(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
46188
+ writeFileSync18(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
46189
+ return file;
46190
+ });
46191
+ } catch (error) {
46192
+ notSaved = errorMessage(error);
46193
+ }
46194
+ const first = answer.content[0];
46195
+ let parsed = null;
46196
+ try {
46197
+ parsed = first?.type === "text" ? JSON.parse(first.text) : null;
46198
+ } catch {}
46199
+ const small = {};
46200
+ const leftOut = [];
46201
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
46202
+ for (const [key, value] of Object.entries(parsed)) {
46203
+ if (JSON.stringify(value).length <= 2000) {
46204
+ small[key] = value;
46205
+ } else {
46206
+ leftOut.push(key);
46207
+ }
46208
+ }
46209
+ }
46210
+ return {
46211
+ ...answer,
46212
+ content: [
46213
+ {
46214
+ type: "text",
46215
+ text: answerJson({
46216
+ ...small,
46217
+ answerTooLarge: {
46218
+ bytes,
46219
+ savedAt,
46220
+ leftOut: leftOut.length > 0 ? leftOut : undefined,
46221
+ note: savedAt === undefined ? `The answer was ${bytes} bytes, past the ${ANSWER_BUDGET} this server sends in one message (Claude Code closes the connection past 16 MB), and writing it to a file failed: ${notSaved ?? "no reason given"}. The fields of its top level that are short are kept here.` : `The answer was ${bytes} bytes, past the ${ANSWER_BUDGET} this server sends in one message (Claude Code closes the connection past 16 MB), so it was written whole to savedAt; read it from there. The fields of its top level that are short are kept here. The files are removed once this server has stopped.`
46222
+ }
46223
+ })
46224
+ }
46225
+ ]
46226
+ };
46227
+ }
46014
46228
  async answered(tool, op, args) {
46015
46229
  try {
46016
46230
  return await this.dispatch(tool, op, args);
@@ -46262,6 +46476,7 @@ class GodotServer {
46262
46476
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
46263
46477
  return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
46264
46478
  };
46479
+ const editorOrEngine = async (command, effect, extra = {}) => await this.editorOrEngine(command, { ...arguments_, ...extra }, `${tool} ${op}`, effect);
46265
46480
  switch (tool) {
46266
46481
  case "project_info":
46267
46482
  return await this.handleProjectInfo(args);
@@ -46274,7 +46489,7 @@ class GodotServer {
46274
46489
  case "project_test":
46275
46490
  return await this.handleRunTests(args);
46276
46491
  case "scene_create":
46277
- return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
46492
+ return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
46278
46493
  case "scene_tree":
46279
46494
  return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
46280
46495
  scenePath: file,
@@ -46284,28 +46499,28 @@ class GodotServer {
46284
46499
  case "scene_node":
46285
46500
  switch (op) {
46286
46501
  case "add":
46287
- return await bridge("add_node");
46502
+ return await editorOrEngine("add_node", "writes");
46288
46503
  case "get":
46289
- return await bridge("get_node_properties");
46504
+ return await editorOrEngine("get_node_properties", "reads");
46290
46505
  case "set":
46291
- return await bridge("set_node_properties");
46506
+ return await editorOrEngine("set_node_properties", "writes");
46292
46507
  case "duplicate":
46293
- return await bridge("duplicate_node", { parentPath: args["parentNodePath"] });
46508
+ return await editorOrEngine("duplicate_node", "writes", { parentPath: args["parentNodePath"] });
46294
46509
  case "reparent":
46295
- return await bridge("reparent_node");
46510
+ return await editorOrEngine("reparent_node", "writes");
46296
46511
  case "delete":
46297
- return await bridge("delete_node");
46512
+ return await editorOrEngine("delete_node", "writes");
46298
46513
  default:
46299
- return await bridge("set_tilemap_cells", { tilemapNodePath: args["nodePath"] });
46514
+ return await editorOrEngine("set_tilemap_cells", "writes", { tilemapNodePath: args["nodePath"] });
46300
46515
  }
46301
46516
  case "scene_signal":
46302
46517
  switch (op) {
46303
46518
  case "connect":
46304
- return await bridge("connect_signal");
46519
+ return await editorOrEngine("connect_signal", "writes");
46305
46520
  case "disconnect":
46306
- return await bridge("disconnect_signal");
46521
+ return await editorOrEngine("disconnect_signal", "writes");
46307
46522
  default:
46308
- return await bridge("list_connections");
46523
+ return await editorOrEngine("list_connections", "reads");
46309
46524
  }
46310
46525
  case "scene_animation":
46311
46526
  switch (op) {
@@ -46356,17 +46571,17 @@ class GodotServer {
46356
46571
  }), async () => await bridge("read_resource", kept));
46357
46572
  }
46358
46573
  case "create":
46359
- return await bridge("create_resource");
46574
+ return await editorOrEngine("create_resource", "writes");
46360
46575
  case "modify":
46361
- return await bridge("modify_resource");
46576
+ return await editorOrEngine("modify_resource", "writes");
46362
46577
  case "create_shader":
46363
- return await bridge("create_shader", { shaderPath: args["resourcePath"] });
46578
+ return await editorOrEngine("create_shader", "writes", { shaderPath: args["resourcePath"] });
46364
46579
  case "create_tileset":
46365
- return await bridge("create_tileset", { tilesetPath: args["resourcePath"] });
46580
+ return await editorOrEngine("create_tileset", "writes", { tilesetPath: args["resourcePath"] });
46366
46581
  case "set_theme_color":
46367
- return await bridge("set_theme_color", { themePath: args["resourcePath"] });
46582
+ return await editorOrEngine("set_theme_color", "writes", { themePath: args["resourcePath"] });
46368
46583
  default:
46369
- return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
46584
+ return await editorOrEngine("set_theme_font_size", "writes", { themePath: args["resourcePath"] });
46370
46585
  }
46371
46586
  case "editor_launch":
46372
46587
  if (op === "close") {
@@ -47232,6 +47447,10 @@ class GodotServer {
47232
47447
  if (unknown.length > 0) {
47233
47448
  return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
47234
47449
  }
47450
+ const forValidation = ["preset", "detail"].filter((name) => args[name] !== undefined);
47451
+ if (forValidation.length > 0 && !include.includes("validation")) {
47452
+ return this.createErrorResponse(`${forValidation.join(" and ")} ${forValidation.length === 1 ? "is" : "are"} read only by the validation section, which this call does not include: add "validation" to include, or leave ${forValidation.length === 1 ? "it" : "them"} out.`);
47453
+ }
47235
47454
  const project = this.project(args);
47236
47455
  if (!project.ok) {
47237
47456
  return project.response;
@@ -47616,7 +47835,7 @@ class GodotServer {
47616
47835
  isError: true
47617
47836
  };
47618
47837
  }
47619
- const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
47838
+ const { failed, briefed } = failedWithin(failedCases(report, Buffer.concat(console2).toString("utf8")), FAILED_BUDGET);
47620
47839
  const orphans = orphansPrinted(said);
47621
47840
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
47622
47841
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
@@ -47656,6 +47875,7 @@ class GodotServer {
47656
47875
  skipped: report.skipped,
47657
47876
  time: report.time,
47658
47877
  failed,
47878
+ failedWithoutDetail: briefed > 0 ? briefed : undefined,
47659
47879
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
47660
47880
  uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
47661
47881
  warnings: warnings.length > 0 ? warnings : undefined,
@@ -47667,6 +47887,7 @@ class GodotServer {
47667
47887
  ...scanned,
47668
47888
  note: [
47669
47889
  missingNote,
47890
+ briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
47670
47891
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
47671
47892
  uncaughtNote(uncaught),
47672
47893
  ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
@@ -47709,6 +47930,10 @@ class GodotServer {
47709
47930
  }
47710
47931
  if (scripts?.value == null) {
47711
47932
  const one = await this.diagnosticsOf(args);
47933
+ if ("noLanguageServer" in one) {
47934
+ const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
47935
+ return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
47936
+ }
47712
47937
  return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
47713
47938
  }
47714
47939
  const progress = callProgress();
@@ -47718,12 +47943,31 @@ class GodotServer {
47718
47943
  let errors = 0;
47719
47944
  let warnings = 0;
47720
47945
  let unread = 0;
47946
+ let byEngine = null;
47721
47947
  for (const [at, scriptPath] of scripts.value.entries()) {
47722
47948
  if (cancelled?.aborted === true) {
47723
47949
  return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
47724
47950
  }
47725
47951
  progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
47726
47952
  const one = await this.diagnosticsOf({ ...args, scriptPath });
47953
+ if ("noLanguageServer" in one) {
47954
+ const rest = scripts.value.slice(at);
47955
+ progress?.(at + 1, `${rest.length} script(s) left, compiled by a headless engine`);
47956
+ const checked = await this.engineDiagnostics(project.ok ? project.value.path : "", rest, one.noLanguageServer);
47957
+ if ("refusal" in checked) {
47958
+ return checked.refusal;
47959
+ }
47960
+ for (const answer of checked.answers) {
47961
+ errors += Number(answer["errors"]);
47962
+ if (readArray(answer, "diagnostics")?.length === 0) {
47963
+ nothingReported.push(String(answer["scriptPath"]));
47964
+ } else {
47965
+ reported.push(answer);
47966
+ }
47967
+ }
47968
+ byEngine = one.noLanguageServer.reason;
47969
+ break;
47970
+ }
47727
47971
  if ("refusal" in one) {
47728
47972
  if (!one.perScript) {
47729
47973
  return one.refusal;
@@ -47744,14 +47988,74 @@ class GodotServer {
47744
47988
  clean: errors === 0 && unread === 0,
47745
47989
  scriptsChecked: scripts.value.length,
47746
47990
  errors,
47747
- warnings,
47991
+ warnings: byEngine === null ? warnings : null,
47748
47992
  ...unread > 0 ? { scriptsNotRead: unread } : {},
47749
47993
  scripts: reported,
47750
- nothingReported
47994
+ nothingReported,
47995
+ ...byEngine === null ? {} : engineChecked(byEngine)
47751
47996
  }));
47752
47997
  }
47998
+ async compileScripts(projectPath, paths) {
47999
+ const payload = { checked: [], failed: [], scripts: [] };
48000
+ const messages = [];
48001
+ let left = [...paths];
48002
+ while (left.length > 0) {
48003
+ const ran = await this.operation("check_scripts", { paths: left }, projectPath);
48004
+ if (!ran.ok) {
48005
+ return { ok: false, message: ran.message };
48006
+ }
48007
+ const checked = (readArray(ran.payload, "checked") ?? []).map(String);
48008
+ if (checked.length === 0) {
48009
+ return { ok: false, message: `check_scripts read none of ${left.length} script(s)` };
48010
+ }
48011
+ payload.checked.push(...checked);
48012
+ payload.failed.push(...(readArray(ran.payload, "failed") ?? []).map(String));
48013
+ payload.scripts.push(...readArray(ran.payload, "scripts") ?? []);
48014
+ messages.push(...ran.messages);
48015
+ left = left.slice(checked.length);
48016
+ }
48017
+ return { ok: true, payload, extras: engineExtras({ messages }) };
48018
+ }
48019
+ async engineDiagnostics(projectPath, scripts, why) {
48020
+ const named = scripts.map((script) => {
48021
+ const contained = resolveWithinProject(projectPath, script);
48022
+ return contained.ok ? `res://${contained.relativePath}` : script;
48023
+ });
48024
+ const checked = await this.compileScripts(projectPath, named);
48025
+ if (!checked.ok) {
48026
+ return {
48027
+ refusal: this.createErrorResponse(`Diagnostics unavailable: ${why.reason.replace(/\.?$/, ".")} No headless engine could compile the scripts instead: ${checked.message}`, why.solutions)
48028
+ };
48029
+ }
48030
+ const failures = new Map((readArray(checked.payload, "scripts") ?? []).map((entry) => [
48031
+ String(asParams(entry)["path"]),
48032
+ asParams(entry)
48033
+ ]));
48034
+ return {
48035
+ answers: scripts.map((scriptPath, at) => {
48036
+ const failure = failures.get(named[at] ?? "");
48037
+ const diagnostics = (readArray(failure ?? {}, "errors") ?? []).map((entry) => {
48038
+ const { line, message } = asParams(entry);
48039
+ return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
48040
+ });
48041
+ const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
48042
+ return {
48043
+ scriptPath,
48044
+ clean: diagnostics.length === 0,
48045
+ errors: diagnostics.length,
48046
+ warnings: null,
48047
+ diagnostics,
48048
+ ...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
48049
+ };
48050
+ })
48051
+ };
48052
+ }
47753
48053
  markedIfStale(answer) {
47754
- return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
48054
+ const status = this.godotBridge.getStatus();
48055
+ if (!hasSaidWhoItIs(status)) {
48056
+ return answer;
48057
+ }
48058
+ return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
47755
48059
  }
47756
48060
  async diagnosticsOf(args) {
47757
48061
  const waited = await this.waitForTheEditorOpenedHere(args, []);
@@ -47761,6 +48065,16 @@ class GodotServer {
47761
48065
  const answer = await this.handleLSP("lsp_get_diagnostics", args);
47762
48066
  const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
47763
48067
  const refused = this.languageServerRefusal(payload, "Diagnostics");
48068
+ const project = this.project(args);
48069
+ const ownEditor = project.ok && this.editorHasOpen(project.value.path);
48070
+ if (refused !== null && !refused.perScript && payload["stillReading"] !== true && !ownEditor) {
48071
+ return {
48072
+ noLanguageServer: {
48073
+ reason: typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]),
48074
+ solutions: this.languageServerSolutions(payload)
48075
+ }
48076
+ };
48077
+ }
47764
48078
  if (refused !== null) {
47765
48079
  return refused;
47766
48080
  }
@@ -47920,24 +48234,24 @@ class GodotServer {
47920
48234
  if (payload["stillReading"] === true) {
47921
48235
  return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
47922
48236
  }
47923
- if (typeof payload["serves"] === "string") {
47924
- return {
47925
- refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
47926
- "editor_status names the editor on the bridge and the port it says it serves",
47927
- "editor_launch opens this project's own editor, on a language server port of its own",
47928
- "GDHARNESS_LSP_PORT points this server at the right one"
47929
- ]),
47930
- perScript: false
47931
- };
47932
- }
47933
48237
  return {
47934
- refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
47935
- `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
47936
- "GDHARNESS_LSP_PORT points this server at another one"
47937
- ]),
47938
- perScript: payload["aboutThisFile"] === true
48238
+ refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, this.languageServerSolutions(payload)),
48239
+ perScript: typeof payload["serves"] !== "string" && payload["aboutThisFile"] === true
47939
48240
  };
47940
48241
  }
48242
+ languageServerSolutions(payload) {
48243
+ if (typeof payload["serves"] === "string") {
48244
+ return [
48245
+ ...this.godotBridge.getStatus().connected ? ["editor_status names the editor on the bridge and the port it says it serves"] : [],
48246
+ "editor_launch opens this project's own editor, on a language server port of its own",
48247
+ "GDHARNESS_LSP_PORT points this server at the right one"
48248
+ ];
48249
+ }
48250
+ return [
48251
+ `Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
48252
+ "GDHARNESS_LSP_PORT points this server at another one"
48253
+ ];
48254
+ }
47941
48255
  async lsp() {
47942
48256
  const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
47943
48257
  if (this.lspClient !== null && this.lspClient.port !== port) {
@@ -48562,7 +48876,7 @@ class GodotServer {
48562
48876
  return;
48563
48877
  }
48564
48878
  const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
48565
- const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
48879
+ const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected && alive(pid));
48566
48880
  const started = await startTimesOf(pids);
48567
48881
  return pids.map((pid) => {
48568
48882
  const at = started.get(pid) ?? tree.get(pid)?.startedAt;
@@ -50801,6 +51115,45 @@ class GodotServer {
50801
51115
  ...engineExtras(outcome)
50802
51116
  });
50803
51117
  }
51118
+ async editorOrEngine(command, args, askedAs, effect) {
51119
+ const contained = this.containProjectFiles(args);
51120
+ if (!contained.ok) {
51121
+ return contained.response;
51122
+ }
51123
+ const project = this.project(contained.value);
51124
+ if (!project.ok) {
51125
+ return project.response;
51126
+ }
51127
+ const projectPath = project.value.path;
51128
+ const waited = await this.waitForTheEditorOpenedHere(contained.value, []);
51129
+ if (this.editorHasOpen(projectPath)) {
51130
+ return await this.handleViaBridge(command, contained.value, askedAs);
51131
+ }
51132
+ if (effect === "writes") {
51133
+ if (waited !== null) {
51134
+ return waited;
51135
+ }
51136
+ const arriving = await this.editorsNotYetConnected(projectPath);
51137
+ if (arriving !== undefined && arriving.length > 0) {
51138
+ const named = arriving.map((one) => `pid ${one.pid}`).join(", ");
51139
+ return this.createErrorResponse(`${askedAs} was not run: an editor of this project is running (${named}) and has not reached this server, and saving a scene or resource it holds would put back what a write made around it.`, [
51140
+ "editor_status names it under editorsNotYetConnected and says when it has connected; the write then goes through it",
51141
+ `If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and ask again`
51142
+ ]);
51143
+ }
51144
+ }
51145
+ const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
51146
+ if (!outcome.ok) {
51147
+ return this.answer(outcome);
51148
+ }
51149
+ return this.jsonTextResponse({
51150
+ ...outcome.payload,
51151
+ ...effect === "reads" ? {
51152
+ readBy: "a headless engine, since no editor serves this project; unsaved changes in an editor are not seen"
51153
+ } : { writtenBy: "a headless engine, since no editor serves this project" },
51154
+ ...engineExtras(outcome)
51155
+ });
51156
+ }
50804
51157
  async handleMoveFile(given) {
50805
51158
  const project = this.project(given);
50806
51159
  if (!project.ok) {
@@ -50991,9 +51344,9 @@ class GodotServer {
50991
51344
  ok &&= rebuilt.ok;
50992
51345
  }
50993
51346
  if (changed.length > 0) {
50994
- const compiled = await this.operation("check_scripts", { paths: [...changed] }, projectPath);
50995
- report["compiled"] = compiled.ok ? { ...compiled.payload, ...engineExtras(compiled) } : { problem: compiled.message };
50996
- ok &&= compiled.ok && (readArray(compiled.payload, "failed") ?? []).length === 0;
51347
+ const compiled = await this.compileScripts(projectPath, [...changed]);
51348
+ report["compiled"] = compiled.ok ? { ...compiled.payload, ...compiled.extras } : { problem: compiled.message };
51349
+ ok &&= compiled.ok && compiled.payload.failed.length === 0;
50997
51350
  }
50998
51351
  return { ok, report };
50999
51352
  }
@@ -51259,6 +51612,11 @@ class GodotServer {
51259
51612
  const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
51260
51613
  const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
51261
51614
  if ("problem" in choice) {
51615
+ const ended = this.activeProcess;
51616
+ const nothingAnnounced = typeof pid !== "number" && announced.unspoken.length === 0 && !announced.running.some((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
51617
+ if (nothingAnnounced && ended !== null && !stillRunning(ended) && (wanted === undefined || ended.projectPath === null || isSameDirectory(ended.projectPath, wanted))) {
51618
+ return this.createErrorResponse(ourRunEnded({ ...ended, pid: ended.pid ?? ended.announcedPid ?? null }, Date.now()));
51619
+ }
51262
51620
  return this.createErrorResponse(choice.problem);
51263
51621
  }
51264
51622
  const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
@@ -51424,7 +51782,18 @@ ${staleNote}`}`
51424
51782
  const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
51425
51783
  return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
51426
51784
  }
51785
+ if (op === "watch") {
51786
+ return await this.handleRuntimeCommand("watch_signal", {
51787
+ ...whichGame(args),
51788
+ path: readNonEmptyString(args, "nodePath") ?? "",
51789
+ signal: readString(args, "signal") ?? ""
51790
+ }, { tool: "runtime_wait", op });
51791
+ }
51427
51792
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
51793
+ const watch = readPositiveNumber(args, "watch");
51794
+ if (op === "signal" && watch === undefined && (readNonEmptyString(args, "nodePath") === undefined || readNonEmptyString(args, "signal") === undefined)) {
51795
+ return this.createErrorResponse("runtime_wait signal needs nodePath and signal, or the watch an earlier runtime_wait watch answered.");
51796
+ }
51428
51797
  const asked = readNonEmptyString(args, "nodePath");
51429
51798
  if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
51430
51799
  return this.createErrorResponse("runtime_wait until needs nodePath to wait for a property: the property belongs to that node.", ["With says instead of property, nodePath can be left out and the whole game is searched"]);
@@ -51435,7 +51804,8 @@ ${staleNote}`}`
51435
51804
  ...whichGame(args),
51436
51805
  path: nodePath,
51437
51806
  signal: readString(args, "signal") ?? "",
51438
- timeout_ms: timeoutMs
51807
+ timeout_ms: timeoutMs,
51808
+ ...watch === undefined ? {} : { watch }
51439
51809
  }, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
51440
51810
  ...whichGame(args),
51441
51811
  path: nodePath,
@@ -51453,7 +51823,7 @@ async function runGodotServer() {
51453
51823
  const server = new GodotServer;
51454
51824
  await server.run();
51455
51825
  }
51456
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __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, 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_START_MS = 2000, 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", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, 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, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
51826
+ 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, 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_START_MS = 2000, 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", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, 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, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
51457
51827
  var init_server = __esm(() => {
51458
51828
  init_dist();
51459
51829
  init_stdio();
@@ -52669,9 +53039,15 @@ so take it off when the question it was for is answered.
52669
53039
 
52670
53040
  \`scene_tree\` for what a scene holds, rather than what the \`.tscn\` text implies, and
52671
53041
  \`resource_edit read\` for a resource: both read the file through the editor when one serves the
52672
- project and through a headless engine when none does, with the same answer. \`scene_node\`,
52673
- \`scene_signal\` and the other \`resource_edit\` ops but \`move\` change it through the editor, so the
52674
- files keep their formatting. \`script_diagnostics\` and \`script_info\` come from the editor's own language server.
53042
+ project and through a headless engine when none does, with the same answer. \`scene_create\`,
53043
+ \`scene_node\`, \`scene_signal\` and the other \`resource_edit\` ops but \`move\` change it through the
53044
+ editor when one serves the project, so the files keep their formatting, and through a headless
53045
+ engine running the editor's own tool when none does. That engine runs the scene's scripts, which an
53046
+ editor runs only as tools, so the answer puts back what they changed as the file loaded, and refuses a
53047
+ file where it cannot, such as a scene whose script does not compile; open the editor
53048
+ (\`editor_launch\` with \`hidden: true\`) and ask again for those. \`script_info\` comes from the editor's own language server, and so does
53049
+ \`script_diagnostics\` when this project's editor is open; without it a headless engine compiles the
53050
+ scripts and reports their errors, with lines but no columns and no warnings.
52675
53051
 
52676
53052
  \`project_info\`, \`project_dependencies\`, \`project_settings\`, \`project_import\`,
52677
53053
  \`project_export\`, \`project_test\`, \`script_edit\`, \`script_info structure\` and
@@ -52746,7 +53122,9 @@ control outside a headless viewport. Read the refusal instead of retrying the ca
52746
53122
  Every answer is read back from the engine after the change rather than echoed from the request, and
52747
53123
  whatever the engine said on stderr comes back under \`engine_messages\`. A headless engine runs the
52748
53124
  project's autoloads after the operation has answered, and what it reports from then until it exits
52749
- comes back separately, under \`engine_messages_after_answer\`: none of it changes the answer.
53125
+ comes back separately, under \`engine_messages_after_answer\`: none of it changes the answer. A
53126
+ check or write of a script waits for the autoloads to start, so the scripts naming them compile, and
53127
+ what they report while starting comes back under \`engine_messages_before_operation\`.
52750
53128
 
52751
53129
  ## More
52752
53130