gdharness 1.2.36 → 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.
package/build/index.js CHANGED
@@ -3373,7 +3373,8 @@ import {
3373
3373
  readFileSync as readFileSync24,
3374
3374
  readSync as readSync2,
3375
3375
  rmdirSync as rmdirSync2,
3376
- statSync as statSync12
3376
+ statSync as statSync12,
3377
+ writeFileSync as writeFileSync16
3377
3378
  } from "node:fs";
3378
3379
  import { basename as basename7, dirname as dirname18, extname as extname4, isAbsolute as isAbsolute4, join as join26, normalize as normalize2, relative as relative5 } from "node:path";
3379
3380
  import { setTimeout as delay5 } from "node:timers/promises";
@@ -32974,6 +32975,7 @@ var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
32974
32975
  var waiting = new Set;
32975
32976
  var atExit = new Set;
32976
32977
  var KINDS = [
32978
+ "answers",
32977
32979
  "desktop",
32978
32980
  "export",
32979
32981
  "import",
@@ -33078,8 +33080,14 @@ function snakeCased(params) {
33078
33080
  }
33079
33081
  function engineExtras(outcome) {
33080
33082
  const after = outcome.afterAnswerMessages ?? [];
33083
+ const before = outcome.beforeOperationMessages ?? [];
33081
33084
  return {
33082
33085
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
33086
+ ...before.length > 0 ? {
33087
+ engine_messages_before_operation: before,
33088
+ engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
33089
+ } : {},
33090
+ ...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
33083
33091
  ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
33084
33092
  ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
33085
33093
  ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
@@ -33087,8 +33095,11 @@ function engineExtras(outcome) {
33087
33095
  };
33088
33096
  }
33089
33097
  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.";
33098
+ var 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.";
33090
33099
  var OWN_ERROR = "[ERROR] ";
33091
33100
  var ANSWERED = "[gdharness] answered";
33101
+ var WAITING = "[gdharness] waiting for the autoloads";
33102
+ var BEGUN = "[gdharness] begun";
33092
33103
  var MESSAGES_KEPT = 200;
33093
33104
  var ERRORS_NAMED = 3;
33094
33105
  function problemsIn(log) {
@@ -33229,12 +33240,22 @@ async function runOperation(engine, operation, params, projectPath) {
33229
33240
  discard(paramsDir);
33230
33241
  }
33231
33242
  const everything = ran.log.everything();
33232
- const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
33233
- const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
33243
+ const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
33244
+ const answered = marked(ANSWERED);
33245
+ const waiting = marked(WAITING);
33246
+ const begun = marked(BEGUN);
33247
+ const ended = answered === -1 ? everything.length : answered;
33248
+ const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
33249
+ ...everything.slice(0, waiting),
33250
+ ...begun === -1 ? [] : everything.slice(begun + 1, ended)
33251
+ ]);
33252
+ const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
33234
33253
  const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
33235
33254
  const afterAnswered = {
33236
33255
  ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
33237
- ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
33256
+ ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
33257
+ ...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
33258
+ ...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
33238
33259
  };
33239
33260
  if (answer.kind === "unreadable") {
33240
33261
  return {
@@ -33245,6 +33266,14 @@ async function runOperation(engine, operation, params, projectPath) {
33245
33266
  };
33246
33267
  }
33247
33268
  if (answer.kind === "none") {
33269
+ if (waiting !== -1 && begun === -1) {
33270
+ return {
33271
+ ok: false,
33272
+ 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)}`,
33273
+ ...reported2(problems),
33274
+ ...afterAnswered
33275
+ };
33276
+ }
33248
33277
  const refusals = refusalsIn(ran.log);
33249
33278
  if (refusals.length > 0) {
33250
33279
  const said = new Set(refusals.map((entry) => entry.index));
@@ -33686,6 +33715,31 @@ ${note}`;
33686
33715
  return detail === entry.detail ? entry : { ...entry, detail };
33687
33716
  });
33688
33717
  }
33718
+ var MOST_REPORTS_SHOWN = 20;
33719
+ var LONGEST_DETAIL = 20000;
33720
+ function shortened(detail) {
33721
+ if (detail === null || detail.length <= LONGEST_DETAIL) {
33722
+ return detail;
33723
+ }
33724
+ return `${detail.slice(0, LONGEST_DETAIL)}
33725
+ [${detail.length - LONGEST_DETAIL} more characters left out]`;
33726
+ }
33727
+ function foldedReports(reports) {
33728
+ const first = new Map;
33729
+ for (const report of reports) {
33730
+ const key = JSON.stringify([report.kind, report.message, report.detail]);
33731
+ const seen = first.get(key);
33732
+ if (seen === undefined) {
33733
+ first.set(key, { report, times: 1 });
33734
+ } else {
33735
+ seen.times += 1;
33736
+ }
33737
+ }
33738
+ return [...first.values()].map(({ report, times }) => {
33739
+ const kept = { ...report, detail: shortened(report.detail) };
33740
+ return times === 1 ? kept : { ...kept, times };
33741
+ });
33742
+ }
33689
33743
  function failedCases(report, printed) {
33690
33744
  const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
33691
33745
  const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
@@ -33693,6 +33747,8 @@ function failedCases(report, printed) {
33693
33747
  return cases.map(({ entry, path }) => {
33694
33748
  const reports = read.slice(next, next + entry.reports.length);
33695
33749
  next += entry.reports.length;
33750
+ const folded = foldedReports(reports);
33751
+ const shown = folded.slice(0, MOST_REPORTS_SHOWN);
33696
33752
  return {
33697
33753
  suite: entry.suite,
33698
33754
  name: entry.name,
@@ -33700,11 +33756,34 @@ function failedCases(report, printed) {
33700
33756
  time: entry.time,
33701
33757
  path,
33702
33758
  message: reports[0]?.message ?? null,
33703
- detail: reports[0]?.detail ?? null,
33704
- ...reports.length > 1 ? { reports } : {}
33759
+ detail: shortened(reports[0]?.detail ?? null),
33760
+ ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
33761
+ ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
33705
33762
  };
33706
33763
  });
33707
33764
  }
33765
+ function failedWithin(failed, budget) {
33766
+ const sizes = failed.map((entry) => JSON.stringify(entry).length);
33767
+ const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
33768
+ const whole = new Set;
33769
+ let spent = 0;
33770
+ for (const index of smallestFirst) {
33771
+ const size = sizes[index] ?? 0;
33772
+ if (spent + size > budget) {
33773
+ break;
33774
+ }
33775
+ spent += size;
33776
+ whole.add(index);
33777
+ }
33778
+ const kept = failed.map((entry, index) => {
33779
+ if (whole.has(index)) {
33780
+ return entry;
33781
+ }
33782
+ const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
33783
+ return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
33784
+ });
33785
+ return { failed: kept, briefed: failed.length - whole.size };
33786
+ }
33708
33787
  var MASKED_BRACKET = "[lb]";
33709
33788
  function masked(expected, actual) {
33710
33789
  const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
@@ -34899,6 +34978,8 @@ var SCENE_PATH = {
34899
34978
  type: "string",
34900
34979
  description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
34901
34980
  };
34981
+ var 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.";
34982
+ var 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.';
34902
34983
  var SCRIPT_PATH = {
34903
34984
  type: "string",
34904
34985
  description: 'Script file inside the project, such as "scripts/player.gd".'
@@ -34911,7 +34992,7 @@ var NODE_PATH = {
34911
34992
  };
34912
34993
  var PROPERTIES = {
34913
34994
  type: "object",
34914
- 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.',
34995
+ 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}`,
34915
34996
  additionalProperties: true
34916
34997
  };
34917
34998
  var XY = {
@@ -34955,7 +35036,10 @@ var SCRIPT_MODIFICATIONS = {
34955
35036
  name: { type: "string" },
34956
35037
  params: { type: "string", description: 'Functions and signals: the parameter list, "delta: float".' },
34957
35038
  returnType: { type: "string", description: "Functions: the return type." },
34958
- body: { type: "string", description: "Functions: the body." },
35039
+ body: {
35040
+ type: "string",
35041
+ 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."
35042
+ },
34959
35043
  varType: {
34960
35044
  type: "string",
34961
35045
  description: "Variables and constants: the type annotation; without one, the type the value evaluates to."
@@ -35058,7 +35142,7 @@ var TOOL_SPECS = [
35058
35142
  {
35059
35143
  name: "project_settings",
35060
35144
  title: "Project settings",
35061
- 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.",
35145
+ 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.",
35062
35146
  parameters: {
35063
35147
  projectPath: PROJECT_PATH2,
35064
35148
  setting: {
@@ -35236,24 +35320,32 @@ var TOOL_SPECS = [
35236
35320
  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.",
35237
35321
  parameters: {
35238
35322
  projectPath: PROJECT_PATH2,
35239
- resourcePath: RESOURCE_PATH,
35323
+ resourcePath: { ...RESOURCE_PATH, ops: ["status", "options", "set_options", "uid", "reimport"] },
35240
35324
  includeUpToDate: {
35241
35325
  type: "boolean",
35326
+ ops: ["status"],
35242
35327
  description: "status: list resources that are current as well. Default false."
35243
35328
  },
35244
35329
  options: {
35245
35330
  type: "object",
35331
+ ops: ["set_options"],
35246
35332
  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.`,
35247
35333
  additionalProperties: true
35248
35334
  },
35249
35335
  reimport: {
35250
35336
  type: "boolean",
35337
+ ops: ["set_options"],
35251
35338
  description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
35252
35339
  },
35253
- force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
35340
+ force: {
35341
+ type: "boolean",
35342
+ ops: ["reimport"],
35343
+ description: "reimport: reimport even what is current. Default false."
35344
+ },
35254
35345
  timeoutMs: {
35255
35346
  type: "integer",
35256
35347
  minimum: 1,
35348
+ ops: ["reimport", "set_options"],
35257
35349
  description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
35258
35350
  }
35259
35351
  },
@@ -35331,7 +35423,7 @@ var TOOL_SPECS = [
35331
35423
  name: "project_test",
35332
35424
  title: "Run tests",
35333
35425
  effect: "additive",
35334
- 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.",
35426
+ 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.",
35335
35427
  parameters: {
35336
35428
  projectPath: PROJECT_PATH2,
35337
35429
  path: {
@@ -35368,7 +35460,7 @@ var TOOL_SPECS = [
35368
35460
  {
35369
35461
  name: "scene_create",
35370
35462
  title: "Create or save a scene",
35371
- 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.",
35463
+ 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}`,
35372
35464
  parameters: {
35373
35465
  projectPath: PROJECT_PATH2,
35374
35466
  scenePath: SCENE_PATH,
@@ -35413,17 +35505,19 @@ var TOOL_SPECS = [
35413
35505
  {
35414
35506
  name: "scene_node",
35415
35507
  title: "Scene nodes",
35416
- 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.",
35508
+ 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}`,
35417
35509
  parameters: {
35418
35510
  projectPath: PROJECT_PATH2,
35419
35511
  scenePath: SCENE_PATH,
35420
- nodePath: NODE_PATH,
35512
+ nodePath: { ...NODE_PATH, ops: ["get", "set", "duplicate", "reparent", "delete", "set_tilemap_cells"] },
35421
35513
  parentNodePath: {
35422
35514
  type: "string",
35515
+ ops: ["add", "duplicate"],
35423
35516
  description: "add: where the node goes. Default the root. duplicate: where the copy goes."
35424
35517
  },
35425
35518
  nodeType: {
35426
35519
  type: "string",
35520
+ ops: ["add"],
35427
35521
  description: 'add: the node class, such as "CharacterBody2D". Give this or instancePath.'
35428
35522
  },
35429
35523
  instancePath: {
@@ -35431,11 +35525,12 @@ var TOOL_SPECS = [
35431
35525
  ops: ["add"],
35432
35526
  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.`
35433
35527
  },
35434
- nodeName: { type: "string", description: "add: the new node's name." },
35435
- properties: PROPERTIES,
35436
- newName: { type: "string", description: "duplicate: the copy's name." },
35528
+ nodeName: { type: "string", ops: ["add"], description: "add: the new node's name." },
35529
+ properties: { ...PROPERTIES, ops: ["add", "set"] },
35530
+ newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
35437
35531
  newParentPath: {
35438
35532
  type: "string",
35533
+ ops: ["reparent"],
35439
35534
  description: "reparent: the new parent, which may be the one it has, to move it among its siblings."
35440
35535
  },
35441
35536
  index: {
@@ -35446,13 +35541,15 @@ var TOOL_SPECS = [
35446
35541
  },
35447
35542
  includeDefaults: {
35448
35543
  type: "boolean",
35544
+ ops: ["get"],
35449
35545
  description: "get: include properties still at their default. Default false."
35450
35546
  },
35451
35547
  layer: {
35452
35548
  type: "number",
35549
+ ops: ["set_tilemap_cells"],
35453
35550
  description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
35454
35551
  },
35455
- cells: TILEMAP_CELLS
35552
+ cells: { ...TILEMAP_CELLS, ops: ["set_tilemap_cells"] }
35456
35553
  },
35457
35554
  requires: ["projectPath", "scenePath"],
35458
35555
  operations: {
@@ -35492,16 +35589,32 @@ var TOOL_SPECS = [
35492
35589
  {
35493
35590
  name: "scene_signal",
35494
35591
  title: "Scene signals",
35495
- 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.",
35592
+ 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}`,
35496
35593
  parameters: {
35497
35594
  projectPath: PROJECT_PATH2,
35498
35595
  scenePath: SCENE_PATH,
35499
- sourceNodePath: { type: "string", description: "The node that emits." },
35500
- signalName: { type: "string", description: 'The signal it emits, such as "pressed".' },
35501
- targetNodePath: { type: "string", description: "The node whose method is called." },
35502
- methodName: { type: "string", description: "The method on that node, called when it emits." },
35503
- flags: { type: "number", description: "connect: Object.ConnectFlags, such as 1 for deferred." },
35504
- nodePath: { type: "string", description: "list: only connections involving this node." }
35596
+ sourceNodePath: { type: "string", ops: ["connect", "disconnect"], description: "The node that emits." },
35597
+ signalName: {
35598
+ type: "string",
35599
+ ops: ["connect", "disconnect"],
35600
+ description: 'The signal it emits, such as "pressed".'
35601
+ },
35602
+ targetNodePath: {
35603
+ type: "string",
35604
+ ops: ["connect", "disconnect"],
35605
+ description: "The node whose method is called."
35606
+ },
35607
+ methodName: {
35608
+ type: "string",
35609
+ ops: ["connect", "disconnect"],
35610
+ description: "The method on that node, called when it emits."
35611
+ },
35612
+ flags: {
35613
+ type: "number",
35614
+ ops: ["connect"],
35615
+ description: "connect: Object.ConnectFlags, such as 1 for deferred."
35616
+ },
35617
+ nodePath: { type: "string", ops: ["list"], description: "list: only connections involving this node." }
35505
35618
  },
35506
35619
  requires: ["projectPath", "scenePath"],
35507
35620
  operations: {
@@ -35626,18 +35739,28 @@ var TOOL_SPECS = [
35626
35739
  parameters: {
35627
35740
  projectPath: PROJECT_PATH2,
35628
35741
  scriptPath: SCRIPT_PATH,
35629
- className: { type: "string", description: "create: a class_name for the script. Not with content." },
35630
- extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
35742
+ className: {
35743
+ type: "string",
35744
+ ops: ["create"],
35745
+ description: "create: a class_name for the script. Not with content."
35746
+ },
35747
+ extends: {
35748
+ type: "string",
35749
+ ops: ["create"],
35750
+ description: "create: the base class. Default Node. Not with content."
35751
+ },
35631
35752
  content: {
35632
35753
  type: "string",
35754
+ ops: ["create"],
35633
35755
  description: "create: the whole file, written as given, instead of a template, className and extends."
35634
35756
  },
35635
35757
  template: {
35636
35758
  type: "string",
35637
35759
  enum: ["singleton", "state_machine", "component", "resource"],
35760
+ ops: ["create"],
35638
35761
  description: "create: a starting shape."
35639
35762
  },
35640
- modifications: SCRIPT_MODIFICATIONS,
35763
+ modifications: { ...SCRIPT_MODIFICATIONS, ops: ["modify"] },
35641
35764
  symbol: {
35642
35765
  type: "string",
35643
35766
  ops: ["rename"],
@@ -35675,16 +35798,19 @@ var TOOL_SPECS = [
35675
35798
  scriptPath: SCRIPT_PATH,
35676
35799
  includeInherited: {
35677
35800
  type: "boolean",
35801
+ ops: ["structure"],
35678
35802
  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."
35679
35803
  },
35680
35804
  line: {
35681
35805
  type: "integer",
35682
35806
  minimum: 1,
35807
+ ops: ["completion", "hover"],
35683
35808
  description: "completion, hover: the line, counted from 1 as the editor shows it."
35684
35809
  },
35685
35810
  character: {
35686
35811
  type: "integer",
35687
35812
  minimum: 1,
35813
+ ops: ["completion", "hover"],
35688
35814
  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."
35689
35815
  }
35690
35816
  },
@@ -35692,7 +35818,7 @@ var TOOL_SPECS = [
35692
35818
  operations: {
35693
35819
  structure: {
35694
35820
  effect: "read",
35695
- summary: "functions, variables, signals, class_name and extends, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class",
35821
+ 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",
35696
35822
  requires: []
35697
35823
  },
35698
35824
  symbols: {
@@ -35727,7 +35853,7 @@ var TOOL_SPECS = [
35727
35853
  {
35728
35854
  name: "resource_edit",
35729
35855
  title: "Edit resources",
35730
- 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.',
35856
+ 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.',
35731
35857
  parameters: {
35732
35858
  projectPath: PROJECT_PATH2,
35733
35859
  resourcePath: {
@@ -36078,9 +36204,10 @@ var TOOL_SPECS = [
36078
36204
  description: "The engine's ClassDB: find classes, read one in full, or walk an inheritance tree.",
36079
36205
  parameters: {
36080
36206
  projectPath: PROJECT_PATH2,
36081
- filter: { type: "string", description: "query: a substring of the class name." },
36207
+ filter: { type: "string", ops: ["query"], description: "query: a substring of the class name." },
36082
36208
  category: {
36083
36209
  type: "string",
36210
+ ops: ["query"],
36084
36211
  enum: [
36085
36212
  "node",
36086
36213
  "node2d",
@@ -36097,9 +36224,21 @@ var TOOL_SPECS = [
36097
36224
  ],
36098
36225
  description: "query: limit to one family."
36099
36226
  },
36100
- instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
36101
- className: { type: "string", description: "info, inheritance: the class." },
36102
- includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
36227
+ instantiableOnly: {
36228
+ type: "boolean",
36229
+ ops: ["query"],
36230
+ description: "query: leave out abstract classes. Default false."
36231
+ },
36232
+ className: {
36233
+ type: "string",
36234
+ ops: ["info", "inheritance"],
36235
+ description: "info, inheritance: the class."
36236
+ },
36237
+ includeInherited: {
36238
+ type: "boolean",
36239
+ ops: ["info"],
36240
+ description: "info: include inherited members. Default false."
36241
+ },
36103
36242
  member: {
36104
36243
  type: "string",
36105
36244
  ops: ["info"],
@@ -36253,7 +36392,7 @@ var TOOL_SPECS = [
36253
36392
  value: {
36254
36393
  ops: ["set"],
36255
36394
  blank: true,
36256
- 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.`
36395
+ 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.`
36257
36396
  },
36258
36397
  method: {
36259
36398
  type: "string",
@@ -36263,7 +36402,7 @@ var TOOL_SPECS = [
36263
36402
  args: {
36264
36403
  type: "array",
36265
36404
  ops: ["call"],
36266
- 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.`
36405
+ 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.`
36267
36406
  },
36268
36407
  properties: {
36269
36408
  type: ["boolean", "array"],
@@ -36992,6 +37131,8 @@ function argumentShape(schema) {
36992
37131
  // src/server.ts
36993
37132
  var UPDATE_NOTICE_EVERY = 500;
36994
37133
  var FEEDBACK_NOTICE_EVERY = 250;
37134
+ var FAILED_BUDGET = 1e6;
37135
+ var ANSWER_BUDGET = 4000000;
36995
37136
  var __dirname2 = dirname18(fileURLToPath4(import.meta.url));
36996
37137
  var PARENT_ASKED_AFTER_MS = 5000;
36997
37138
  function editorRestartTimeoutMs() {
@@ -38364,6 +38505,7 @@ class GodotServer {
38364
38505
  inFlight = new Map;
38365
38506
  callsOnGames = new Set;
38366
38507
  callsTaken = 0;
38508
+ answersDirectory;
38367
38509
  clientNoted = null;
38368
38510
  parentCommand = null;
38369
38511
  endNoted = false;
@@ -38762,12 +38904,66 @@ class GodotServer {
38762
38904
  this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
38763
38905
  try {
38764
38906
  const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
38765
- return this.withFeedbackNotice(this.withUpdateNotice(answer));
38907
+ return this.withinTheClientsLimit(spec.name, this.withFeedbackNotice(this.withUpdateNotice(answer)));
38766
38908
  } finally {
38767
38909
  this.inFlight.delete(call);
38768
38910
  }
38769
38911
  });
38770
38912
  }
38913
+ withinTheClientsLimit(tool, answer) {
38914
+ const sizeOf = (item) => item.type === "text" ? Buffer.byteLength(item.text) : item.data.length;
38915
+ const bytes = answer.content.reduce((sum, item) => sum + sizeOf(item), 0);
38916
+ if (bytes <= ANSWER_BUDGET) {
38917
+ return answer;
38918
+ }
38919
+ this.answersDirectory ??= scratchDirectory("answers");
38920
+ const directory = this.answersDirectory;
38921
+ const stem = `${tool}-${Date.now()}`;
38922
+ let savedAt;
38923
+ let notSaved;
38924
+ try {
38925
+ savedAt = answer.content.map((item, index) => {
38926
+ const file = join26(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
38927
+ writeFileSync16(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
38928
+ return file;
38929
+ });
38930
+ } catch (error) {
38931
+ notSaved = errorMessage(error);
38932
+ }
38933
+ const first = answer.content[0];
38934
+ let parsed = null;
38935
+ try {
38936
+ parsed = first?.type === "text" ? JSON.parse(first.text) : null;
38937
+ } catch {}
38938
+ const small = {};
38939
+ const leftOut = [];
38940
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
38941
+ for (const [key, value] of Object.entries(parsed)) {
38942
+ if (JSON.stringify(value).length <= 2000) {
38943
+ small[key] = value;
38944
+ } else {
38945
+ leftOut.push(key);
38946
+ }
38947
+ }
38948
+ }
38949
+ return {
38950
+ ...answer,
38951
+ content: [
38952
+ {
38953
+ type: "text",
38954
+ text: answerJson({
38955
+ ...small,
38956
+ answerTooLarge: {
38957
+ bytes,
38958
+ savedAt,
38959
+ leftOut: leftOut.length > 0 ? leftOut : undefined,
38960
+ 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.`
38961
+ }
38962
+ })
38963
+ }
38964
+ ]
38965
+ };
38966
+ }
38771
38967
  async answered(tool, op, args) {
38772
38968
  try {
38773
38969
  return await this.dispatch(tool, op, args);
@@ -39019,6 +39215,7 @@ class GodotServer {
39019
39215
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
39020
39216
  return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
39021
39217
  };
39218
+ const editorOrEngine = async (command, effect, extra = {}) => await this.editorOrEngine(command, { ...arguments_, ...extra }, `${tool} ${op}`, effect);
39022
39219
  switch (tool) {
39023
39220
  case "project_info":
39024
39221
  return await this.handleProjectInfo(args);
@@ -39031,7 +39228,7 @@ class GodotServer {
39031
39228
  case "project_test":
39032
39229
  return await this.handleRunTests(args);
39033
39230
  case "scene_create":
39034
- return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
39231
+ return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
39035
39232
  case "scene_tree":
39036
39233
  return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
39037
39234
  scenePath: file,
@@ -39041,28 +39238,28 @@ class GodotServer {
39041
39238
  case "scene_node":
39042
39239
  switch (op) {
39043
39240
  case "add":
39044
- return await bridge("add_node");
39241
+ return await editorOrEngine("add_node", "writes");
39045
39242
  case "get":
39046
- return await bridge("get_node_properties");
39243
+ return await editorOrEngine("get_node_properties", "reads");
39047
39244
  case "set":
39048
- return await bridge("set_node_properties");
39245
+ return await editorOrEngine("set_node_properties", "writes");
39049
39246
  case "duplicate":
39050
- return await bridge("duplicate_node", { parentPath: args["parentNodePath"] });
39247
+ return await editorOrEngine("duplicate_node", "writes", { parentPath: args["parentNodePath"] });
39051
39248
  case "reparent":
39052
- return await bridge("reparent_node");
39249
+ return await editorOrEngine("reparent_node", "writes");
39053
39250
  case "delete":
39054
- return await bridge("delete_node");
39251
+ return await editorOrEngine("delete_node", "writes");
39055
39252
  default:
39056
- return await bridge("set_tilemap_cells", { tilemapNodePath: args["nodePath"] });
39253
+ return await editorOrEngine("set_tilemap_cells", "writes", { tilemapNodePath: args["nodePath"] });
39057
39254
  }
39058
39255
  case "scene_signal":
39059
39256
  switch (op) {
39060
39257
  case "connect":
39061
- return await bridge("connect_signal");
39258
+ return await editorOrEngine("connect_signal", "writes");
39062
39259
  case "disconnect":
39063
- return await bridge("disconnect_signal");
39260
+ return await editorOrEngine("disconnect_signal", "writes");
39064
39261
  default:
39065
- return await bridge("list_connections");
39262
+ return await editorOrEngine("list_connections", "reads");
39066
39263
  }
39067
39264
  case "scene_animation":
39068
39265
  switch (op) {
@@ -39113,17 +39310,17 @@ class GodotServer {
39113
39310
  }), async () => await bridge("read_resource", kept));
39114
39311
  }
39115
39312
  case "create":
39116
- return await bridge("create_resource");
39313
+ return await editorOrEngine("create_resource", "writes");
39117
39314
  case "modify":
39118
- return await bridge("modify_resource");
39315
+ return await editorOrEngine("modify_resource", "writes");
39119
39316
  case "create_shader":
39120
- return await bridge("create_shader", { shaderPath: args["resourcePath"] });
39317
+ return await editorOrEngine("create_shader", "writes", { shaderPath: args["resourcePath"] });
39121
39318
  case "create_tileset":
39122
- return await bridge("create_tileset", { tilesetPath: args["resourcePath"] });
39319
+ return await editorOrEngine("create_tileset", "writes", { tilesetPath: args["resourcePath"] });
39123
39320
  case "set_theme_color":
39124
- return await bridge("set_theme_color", { themePath: args["resourcePath"] });
39321
+ return await editorOrEngine("set_theme_color", "writes", { themePath: args["resourcePath"] });
39125
39322
  default:
39126
- return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
39323
+ return await editorOrEngine("set_theme_font_size", "writes", { themePath: args["resourcePath"] });
39127
39324
  }
39128
39325
  case "editor_launch":
39129
39326
  if (op === "close") {
@@ -39989,6 +40186,10 @@ class GodotServer {
39989
40186
  if (unknown.length > 0) {
39990
40187
  return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
39991
40188
  }
40189
+ const forValidation = ["preset", "detail"].filter((name) => args[name] !== undefined);
40190
+ if (forValidation.length > 0 && !include.includes("validation")) {
40191
+ 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.`);
40192
+ }
39992
40193
  const project = this.project(args);
39993
40194
  if (!project.ok) {
39994
40195
  return project.response;
@@ -40373,7 +40574,7 @@ class GodotServer {
40373
40574
  isError: true
40374
40575
  };
40375
40576
  }
40376
- const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
40577
+ const { failed, briefed } = failedWithin(failedCases(report, Buffer.concat(console2).toString("utf8")), FAILED_BUDGET);
40377
40578
  const orphans = orphansPrinted(said);
40378
40579
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
40379
40580
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
@@ -40413,6 +40614,7 @@ class GodotServer {
40413
40614
  skipped: report.skipped,
40414
40615
  time: report.time,
40415
40616
  failed,
40617
+ failedWithoutDetail: briefed > 0 ? briefed : undefined,
40416
40618
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
40417
40619
  uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
40418
40620
  warnings: warnings.length > 0 ? warnings : undefined,
@@ -40424,6 +40626,7 @@ class GodotServer {
40424
40626
  ...scanned,
40425
40627
  note: [
40426
40628
  missingNote,
40629
+ 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,
40427
40630
  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,
40428
40631
  uncaughtNote(uncaught),
40429
40632
  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)
@@ -40587,7 +40790,11 @@ class GodotServer {
40587
40790
  };
40588
40791
  }
40589
40792
  markedIfStale(answer) {
40590
- return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
40793
+ const status = this.godotBridge.getStatus();
40794
+ if (!hasSaidWhoItIs(status)) {
40795
+ return answer;
40796
+ }
40797
+ return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
40591
40798
  }
40592
40799
  async diagnosticsOf(args) {
40593
40800
  const waited = await this.waitForTheEditorOpenedHere(args, []);
@@ -41408,7 +41615,7 @@ class GodotServer {
41408
41615
  return;
41409
41616
  }
41410
41617
  const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
41411
- const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
41618
+ const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected && alive(pid));
41412
41619
  const started = await startTimesOf(pids);
41413
41620
  return pids.map((pid) => {
41414
41621
  const at = started.get(pid) ?? tree.get(pid)?.startedAt;
@@ -43647,6 +43854,45 @@ class GodotServer {
43647
43854
  ...engineExtras(outcome)
43648
43855
  });
43649
43856
  }
43857
+ async editorOrEngine(command, args, askedAs, effect) {
43858
+ const contained = this.containProjectFiles(args);
43859
+ if (!contained.ok) {
43860
+ return contained.response;
43861
+ }
43862
+ const project = this.project(contained.value);
43863
+ if (!project.ok) {
43864
+ return project.response;
43865
+ }
43866
+ const projectPath = project.value.path;
43867
+ const waited = await this.waitForTheEditorOpenedHere(contained.value, []);
43868
+ if (this.editorHasOpen(projectPath)) {
43869
+ return await this.handleViaBridge(command, contained.value, askedAs);
43870
+ }
43871
+ if (effect === "writes") {
43872
+ if (waited !== null) {
43873
+ return waited;
43874
+ }
43875
+ const arriving = await this.editorsNotYetConnected(projectPath);
43876
+ if (arriving !== undefined && arriving.length > 0) {
43877
+ const named = arriving.map((one) => `pid ${one.pid}`).join(", ");
43878
+ 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.`, [
43879
+ "editor_status names it under editorsNotYetConnected and says when it has connected; the write then goes through it",
43880
+ `If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and ask again`
43881
+ ]);
43882
+ }
43883
+ }
43884
+ const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
43885
+ if (!outcome.ok) {
43886
+ return this.answer(outcome);
43887
+ }
43888
+ return this.jsonTextResponse({
43889
+ ...outcome.payload,
43890
+ ...effect === "reads" ? {
43891
+ readBy: "a headless engine, since no editor serves this project; unsaved changes in an editor are not seen"
43892
+ } : { writtenBy: "a headless engine, since no editor serves this project" },
43893
+ ...engineExtras(outcome)
43894
+ });
43895
+ }
43650
43896
  async handleMoveFile(given) {
43651
43897
  const project = this.project(given);
43652
43898
  if (!project.ok) {