gdharness 1.2.14 → 1.2.16

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
@@ -3807,6 +3807,18 @@ function prefixIssues(path, issues) {
3807
3807
  function unwrapMessage(message) {
3808
3808
  return typeof message === "string" ? message : message?.message;
3809
3809
  }
3810
+ function sizeCaps(defChecks) {
3811
+ const caps = [];
3812
+ for (const ch of defChecks ?? []) {
3813
+ const d = ch._zod.def;
3814
+ if (!/_(length|size)$|_equals$/.test(d.check))
3815
+ break;
3816
+ const max = d.maximum ?? d.length ?? d.size;
3817
+ if (max >= 0)
3818
+ caps.push([max, ch]);
3819
+ }
3820
+ return caps;
3821
+ }
3810
3822
  function attachSchema(issues, start, inst) {
3811
3823
  var _a;
3812
3824
  for (let i = start;i < issues.length; i++) {
@@ -4808,7 +4820,7 @@ ${content.join(`
4808
4820
  var version = {
4809
4821
  major: 4,
4810
4822
  minor: 6,
4811
- patch: 5
4823
+ patch: 7
4812
4824
  };
4813
4825
 
4814
4826
  // node_modules/zod/v4/core/schemas.js
@@ -5381,6 +5393,29 @@ var $ZodNever = /* @__PURE__ */ $constructor("$ZodNever", (inst, def) => {
5381
5393
  return payload;
5382
5394
  };
5383
5395
  });
5396
+ function capSize(inst, size) {
5397
+ const caps = sizeCaps(inst._zod.def.checks);
5398
+ if (!caps.length)
5399
+ return;
5400
+ const cap = Math.min(...caps.map((c) => c[0]));
5401
+ const parse = inst._zod.parse;
5402
+ inst._zod.parse = (payload, ctx) => {
5403
+ const n = size(payload.value);
5404
+ if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
5405
+ for (const [max, ch] of sizeCaps(inst._zod.def.checks)) {
5406
+ if (n > max && ch._zod.def.when(payload)) {
5407
+ const start = payload.issues.length;
5408
+ ch._zod.check(payload);
5409
+ payload.issues[start].continue = false;
5410
+ attachSchema(payload.issues, start, inst);
5411
+ return payload;
5412
+ }
5413
+ }
5414
+ }
5415
+ return parse(payload, ctx);
5416
+ };
5417
+ }
5418
+ var arraySize = (input) => Array.isArray(input) ? input.length : -1;
5384
5419
  function handleArrayResult(result, final, index) {
5385
5420
  if (result.issues.length) {
5386
5421
  final.issues.push(...prefixIssues(index, result.issues));
@@ -5424,6 +5459,7 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
5424
5459
  }
5425
5460
  return payload;
5426
5461
  };
5462
+ capSize(inst, arraySize);
5427
5463
  });
5428
5464
  function handlePropertyResult(result, final, key, input, optin, optout) {
5429
5465
  const isPresent = key in input;
@@ -28316,10 +28352,58 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
28316
28352
  }
28317
28353
  const log = new GameLog;
28318
28354
  const { text, of } = theFirstOf(path);
28319
- log.append("transcript", text);
28355
+ log.append("transcript", foldProgress(text));
28320
28356
  log.finish();
28321
28357
  return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
28322
28358
  }
28359
+ var PROGRESS_STEP = /^\[\s*(\d+)% \] (\S+) \| (.*)$/;
28360
+ var PROGRESS_DONE = /^\[ DONE \] (\S+)\s*$/;
28361
+ function foldProgress(text) {
28362
+ const out = [];
28363
+ const open = new Map;
28364
+ for (const raw of text.split(`
28365
+ `)) {
28366
+ const line = raw.replace(/\r$/, "").replace(COLOUR_CODE, "");
28367
+ const step = PROGRESS_STEP.exec(line);
28368
+ if (step !== null) {
28369
+ const [, percent = "", name = "", said = ""] = step;
28370
+ const started = /^Started (.*)$/.exec(said);
28371
+ if (started !== null) {
28372
+ const task = { name, label: started[1] ?? "", percent, done: false };
28373
+ open.set(name, task);
28374
+ out.push(task);
28375
+ continue;
28376
+ }
28377
+ const task = open.get(name);
28378
+ if (task !== undefined) {
28379
+ task.percent = percent;
28380
+ if (said.trim() !== "") {
28381
+ task.last = said.trim();
28382
+ }
28383
+ continue;
28384
+ }
28385
+ }
28386
+ const finished = PROGRESS_DONE.exec(line);
28387
+ const task = finished === null ? undefined : open.get(finished[1] ?? "");
28388
+ if (task !== undefined) {
28389
+ task.done = true;
28390
+ open.delete(task.name);
28391
+ continue;
28392
+ }
28393
+ out.push(raw);
28394
+ }
28395
+ return out.map((entry) => {
28396
+ if (typeof entry === "string") {
28397
+ return entry;
28398
+ }
28399
+ if (entry.done) {
28400
+ return `[ DONE ] ${entry.name} | ${entry.label}`;
28401
+ }
28402
+ const last = entry.last === undefined ? "" : `, and the last step it named was "${entry.last}"`;
28403
+ return `[ ${entry.percent}% ] ${entry.name} | ${entry.label}, which has not finished${last}`;
28404
+ }).join(`
28405
+ `);
28406
+ }
28323
28407
  var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
28324
28408
  function theFirstOf(path) {
28325
28409
  const whole = statSync3(path).size;
@@ -28579,6 +28663,12 @@ function theEditorHasComeBack(status, startedAt) {
28579
28663
  function hasSaidWhoItIs(status) {
28580
28664
  return status.connected && status.addonVersion !== undefined;
28581
28665
  }
28666
+ function withoutTrailingSeparator(path) {
28667
+ if (path.length <= 1 || !/[\\/]$/.test(path) || /^[A-Za-z]:[\\/]$/.test(path)) {
28668
+ return path;
28669
+ }
28670
+ return path.slice(0, -1);
28671
+ }
28582
28672
  function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
28583
28673
  return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
28584
28674
  }
@@ -28879,7 +28969,7 @@ class GodotBridge extends EventEmitter {
28879
28969
  this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
28880
28970
  return;
28881
28971
  }
28882
- this.connectionInfo.projectPath = message.project_path;
28972
+ this.connectionInfo.projectPath = withoutTrailingSeparator(message.project_path);
28883
28973
  this.connectionInfo.addonVersion = message.addon_version ?? "";
28884
28974
  this.connectionInfo.addonDigest = message.addon_digest;
28885
28975
  this.connectionInfo.editorPid = message.editor_pid;
@@ -29628,9 +29718,11 @@ var HEADLESS_OPERATIONS = dictionary({
29628
29718
  remove_autoload: "remove_autoload",
29629
29719
  set_main_scene: "set_main_scene",
29630
29720
  add_input_action: "add_input_action",
29721
+ remove_input_action: "remove_input_action",
29631
29722
  enable_plugin: "enable_plugin",
29632
29723
  disable_plugin: "disable_plugin",
29633
29724
  add_audio_bus: "create_audio_bus",
29725
+ remove_audio_bus: "remove_audio_bus",
29634
29726
  set_audio_bus_effect: "set_audio_bus_effect",
29635
29727
  set_audio_bus_volume: "set_audio_bus_volume"
29636
29728
  }),
@@ -30847,6 +30939,29 @@ function shownSymbol(entry) {
30847
30939
  ...children.length > 0 ? { children: children.map(shownSymbol) } : {}
30848
30940
  };
30849
30941
  }
30942
+ var MOST_COMPLETIONS = 100;
30943
+ function typedBefore(content, line, character) {
30944
+ const text = content.split(/\r?\n/)[line] ?? "";
30945
+ return /[\p{L}\p{N}_]*$/u.exec(text.slice(0, character))?.[0] ?? "";
30946
+ }
30947
+ function narrowedCompletions(completions, typed) {
30948
+ const wanted = typed.toLowerCase();
30949
+ const matching = wanted === "" ? completions : completions.filter((entry) => {
30950
+ const record = asRecord(entry);
30951
+ const name = typeof record["filterText"] === "string" ? record["filterText"] : record["label"];
30952
+ return typeof name === "string" && name.toLowerCase().startsWith(wanted);
30953
+ });
30954
+ const shown = matching.slice(0, MOST_COMPLETIONS);
30955
+ return {
30956
+ ...typed === "" ? {} : { typed },
30957
+ completions: shown.map(shownCompletion),
30958
+ total: matching.length,
30959
+ ...matching.length > shown.length ? {
30960
+ omitted: matching.length - shown.length,
30961
+ note: `${matching.length - shown.length} more are offered here than one answer carries; ask at a position after more of the name to narrow them.`
30962
+ } : {}
30963
+ };
30964
+ }
30850
30965
  function shownCompletion(entry) {
30851
30966
  const { data: _request, kind, ...rest } = asRecord(entry);
30852
30967
  return { ...rest, kind: named(kind, COMPLETION_KINDS) };
@@ -30917,7 +31032,7 @@ async function handleLSPTool(client, toolName, args) {
30917
31032
  case "lsp_get_completions": {
30918
31033
  const { line, character } = protocolPosition(parsedArgs);
30919
31034
  const completions = await client.getCompletions(scriptPath, content, line, character);
30920
- return asToolResponse({ completions: completions.map(shownCompletion) });
31035
+ return asToolResponse(narrowedCompletions(completions, typedBefore(content, line, character)));
30921
31036
  }
30922
31037
  case "lsp_get_hover": {
30923
31038
  const { line, character } = protocolPosition(parsedArgs);
@@ -31087,9 +31202,16 @@ var SKIPPED = new Set(["node_modules"]);
31087
31202
  function skipped(name) {
31088
31203
  return name.startsWith(".") || SKIPPED.has(name);
31089
31204
  }
31205
+ var VERSION_MARKER = ".gdharness-version";
31090
31206
  function steppedOver(directory) {
31207
+ return gdignored(directory) || installedByUs(directory);
31208
+ }
31209
+ function gdignored(directory) {
31091
31210
  return existsSync13(join16(directory, ".gdignore"));
31092
31211
  }
31212
+ function installedByUs(directory) {
31213
+ return existsSync13(join16(directory, VERSION_MARKER));
31214
+ }
31093
31215
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
31094
31216
  var ASSET_EXTENSIONS = new Set([
31095
31217
  "png",
@@ -31245,7 +31367,10 @@ function projectTexts(projectPath) {
31245
31367
  function ignoredTexts(projectPath) {
31246
31368
  const found = [];
31247
31369
  const visit = (directory, prefix, ignored) => {
31248
- const under = ignored || steppedOver(directory);
31370
+ if (installedByUs(directory)) {
31371
+ return;
31372
+ }
31373
+ const under = ignored || gdignored(directory);
31249
31374
  for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31250
31375
  if (skipped(entry.name)) {
31251
31376
  continue;
@@ -31348,7 +31473,11 @@ var PROMPT_SPECS = [
31348
31473
  }
31349
31474
  ],
31350
31475
  steps: [
31351
- { tool: "editor_status", passes: [], why: "see whether an editor is connected and its addon matches" },
31476
+ {
31477
+ tool: "editor_status",
31478
+ passes: [],
31479
+ why: "see whether an editor is connected and its addon matches; without one the run is a game of its own, not a fault"
31480
+ },
31352
31481
  { tool: "editor_run", op: "start", passes: ["projectPath", "scene"], why: "start the run" },
31353
31482
  {
31354
31483
  tool: "editor_run",
@@ -31366,7 +31495,11 @@ var PROMPT_SPECS = [
31366
31495
  description: "Check the editor connection, the project health report, export validation and the import state.",
31367
31496
  arguments: [PROJECT_PATH],
31368
31497
  steps: [
31369
- { tool: "editor_status", passes: [], why: "see whether an editor is connected and its addon matches" },
31498
+ {
31499
+ tool: "editor_status",
31500
+ passes: [],
31501
+ why: "see whether an editor is connected and, if one is, whether its addon matches; no editor is no fault in the project, so leave it out of what is wrong"
31502
+ },
31370
31503
  {
31371
31504
  tool: "project_info",
31372
31505
  passes: ["projectPath"],
@@ -33895,10 +34028,14 @@ var TOOL_SPECS = [
33895
34028
  {
33896
34029
  name: "project_settings",
33897
34030
  title: "Project settings",
33898
- 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.",
34031
+ 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. 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.",
33899
34032
  parameters: {
33900
34033
  projectPath: PROJECT_PATH2,
33901
- setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
34034
+ setting: {
34035
+ type: "string",
34036
+ ops: ["get", "set"],
34037
+ description: 'Setting path, such as "display/window/size/viewport_width".'
34038
+ },
33902
34039
  from: {
33903
34040
  type: "string",
33904
34041
  enum: ["disk", "editor"],
@@ -33912,40 +34049,66 @@ var TOOL_SPECS = [
33912
34049
  },
33913
34050
  value: {
33914
34051
  blank: true,
34052
+ ops: ["set"],
33915
34053
  description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with. "" writes an empty string.'
33916
34054
  },
33917
- name: { type: "string", description: "Autoload name." },
33918
- path: { type: "string", description: "Autoload script or scene inside the project." },
34055
+ name: { type: "string", ops: ["add_autoload", "remove_autoload"], description: "Autoload name." },
34056
+ path: {
34057
+ type: "string",
34058
+ ops: ["add_autoload"],
34059
+ description: "Autoload script or scene inside the project."
34060
+ },
33919
34061
  enabled: {
33920
34062
  type: "boolean",
33921
- description: "Autoloads: Godot 4 loads every autoload in the list, so false is refused. Use global: false to keep the name out of the global scope, or remove_autoload to stop it loading."
34063
+ ops: ["add_autoload", "set_audio_bus_effect"],
34064
+ description: "add_autoload: Godot 4 loads every autoload in the list, so false is refused. Use global: false to keep the name out of the global scope, or remove_autoload to stop it loading. set_audio_bus_effect: whether the effect is on. Default true."
33922
34065
  },
33923
34066
  global: {
33924
34067
  type: "boolean",
34068
+ ops: ["add_autoload"],
33925
34069
  description: `Autoloads: make the name a global variable, the * in project.godot and the editor's "Global Variable" column. Default true. The autoload loads either way.`
33926
34070
  },
33927
- scenePath: SCENE_PATH,
33928
- actionName: { type: "string", description: 'Input action name, such as "jump".' },
33929
- events: INPUT_EVENTS,
34071
+ scenePath: { ...SCENE_PATH, ops: ["set_main_scene"] },
34072
+ actionName: {
34073
+ type: "string",
34074
+ ops: ["add_input_action", "remove_input_action"],
34075
+ description: 'Input action name, such as "jump".'
34076
+ },
34077
+ events: { ...INPUT_EVENTS, ops: ["add_input_action"] },
33930
34078
  deadzone: {
33931
34079
  type: "number",
34080
+ ops: ["add_input_action"],
33932
34081
  minimum: 0,
33933
34082
  maximum: 1,
33934
34083
  description: "Input actions: analogue deadzone, 0 to 1. Default 0.5."
33935
34084
  },
33936
- pluginName: { type: "string", description: "Folder name under addons/." },
33937
- busName: { type: "string", description: "Audio bus name." },
34085
+ pluginName: {
34086
+ type: "string",
34087
+ ops: ["enable_plugin", "disable_plugin"],
34088
+ description: "Folder name under addons/."
34089
+ },
34090
+ busName: { type: "string", ops: ["add_audio_bus", "remove_audio_bus"], description: "Audio bus name." },
33938
34091
  parentBusIndex: {
33939
34092
  type: "number",
34093
+ ops: ["add_audio_bus"],
33940
34094
  description: "Audio buses: the bus to send to. Default 0, Master. The new bus goes right after it, so every later bus moves one along, and the answer lists them all."
33941
34095
  },
33942
- busIndex: { type: "number", description: "Audio bus index." },
34096
+ busIndex: {
34097
+ type: "number",
34098
+ ops: ["set_audio_bus_effect", "set_audio_bus_volume"],
34099
+ description: "Audio bus index."
34100
+ },
33943
34101
  effectIndex: {
33944
34102
  type: "number",
34103
+ ops: ["set_audio_bus_effect"],
33945
34104
  description: "Slot on the bus for the effect: the effect there is replaced, and one past the last adds one. Any other slot is refused."
33946
34105
  },
33947
- effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
33948
- volumeDb: { type: "number", description: "Bus volume in decibels." }
34106
+ effectType: {
34107
+ type: "string",
34108
+ ops: ["set_audio_bus_effect"],
34109
+ description: 'Effect class, such as "AudioEffectReverb".'
34110
+ },
34111
+ volumeDb: { type: "number", ops: ["set_audio_bus_volume"], description: "Bus volume in decibels." }
33949
34112
  },
33950
34113
  requires: ["projectPath"],
33951
34114
  operations: {
@@ -33967,9 +34130,19 @@ var TOOL_SPECS = [
33967
34130
  summary: "register an input action and its events",
33968
34131
  requires: ["actionName", "events"]
33969
34132
  },
34133
+ remove_input_action: {
34134
+ effect: "destructive",
34135
+ summary: "remove an input action the project defines",
34136
+ requires: ["actionName"]
34137
+ },
33970
34138
  enable_plugin: { effect: "destructive", summary: "enable an addon", requires: ["pluginName"] },
33971
34139
  disable_plugin: { effect: "destructive", summary: "disable an addon", requires: ["pluginName"] },
33972
34140
  add_audio_bus: { effect: "additive", summary: "add an audio bus", requires: ["busName"] },
34141
+ remove_audio_bus: {
34142
+ effect: "destructive",
34143
+ summary: "remove an audio bus, sending what sent to it on to where it sent",
34144
+ requires: ["busName"]
34145
+ },
33973
34146
  set_audio_bus_effect: {
33974
34147
  effect: "destructive",
33975
34148
  summary: "put an effect in a slot on a bus",
@@ -33986,7 +34159,7 @@ var TOOL_SPECS = [
33986
34159
  name: "project_search",
33987
34160
  title: "Search the project",
33988
34161
  effect: "read",
33989
- description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs.",
34162
+ description: "Searches text or a regular expression across project files and returns file paths with line numbers. What the engine steps over is not searched: directories spelled with a dot, node_modules, and any directory holding a .gdignore. A vendored engine or an export directory is therefore absent from the results rather than matching the code you are looking for in a copy nobody runs. The addons gdharness installed are not searched either, since nobody on the project wrote them.",
33990
34163
  parameters: {
33991
34164
  projectPath: PROJECT_PATH2,
33992
34165
  query: { type: "string", description: "The text or pattern to find." },
@@ -34148,8 +34321,18 @@ var TOOL_SPECS = [
34148
34321
  parameters: {
34149
34322
  projectPath: PROJECT_PATH2,
34150
34323
  scenePath: SCENE_PATH,
34151
- rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
34152
- newPath: { type: "string", description: "save_as: where the copy goes." }
34324
+ rootNodeType: {
34325
+ type: "string",
34326
+ ops: ["create"],
34327
+ description: "create: the root node class. Default Node."
34328
+ },
34329
+ rootNodeName: {
34330
+ type: "string",
34331
+ ops: ["create"],
34332
+ description: "create: the root node name. Default the class name, as the editor names a new root."
34333
+ },
34334
+ script: { type: "string", ops: ["create"], description: "create: a script to attach to the root." },
34335
+ newPath: { type: "string", ops: ["save_as"], description: "save_as: where the copy goes." }
34153
34336
  },
34154
34337
  requires: ["projectPath", "scenePath"],
34155
34338
  operations: {
@@ -34229,7 +34412,7 @@ var TOOL_SPECS = [
34229
34412
  {
34230
34413
  name: "scene_signal",
34231
34414
  title: "Scene signals",
34232
- description: "Signal connections in a scene file. Needs the editor connected.",
34415
+ 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. Needs the editor connected.",
34233
34416
  parameters: {
34234
34417
  projectPath: PROJECT_PATH2,
34235
34418
  scenePath: SCENE_PATH,
@@ -34258,43 +34441,78 @@ var TOOL_SPECS = [
34258
34441
  {
34259
34442
  name: "scene_animation",
34260
34443
  title: "Animations",
34261
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. Needs the editor connected.",
34444
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. Needs the editor connected.",
34262
34445
  parameters: {
34263
34446
  projectPath: PROJECT_PATH2,
34264
34447
  scenePath: SCENE_PATH,
34265
- playerNodePath: { type: "string", description: "The AnimationPlayer node." },
34448
+ playerNodePath: {
34449
+ type: "string",
34450
+ ops: ["read", "create", "add_track"],
34451
+ description: "The AnimationPlayer node."
34452
+ },
34266
34453
  animationName: {
34267
34454
  type: "string",
34268
- description: "The animation in the player. add_state: the one that state plays."
34455
+ ops: ["read", "create", "add_track", "add_state"],
34456
+ description: "The animation in the player. read: the one to answer with whole; without it, read lists them. add_state: the one that state plays."
34269
34457
  },
34270
- length: { type: "number", description: "create: seconds. Default 1." },
34458
+ length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
34271
34459
  loopMode: {
34272
34460
  type: "string",
34461
+ ops: ["create"],
34273
34462
  enum: ["none", "linear", "pingpong"],
34274
34463
  description: "create: default none."
34275
34464
  },
34276
- step: { type: "number", description: "create: keyframe snap in seconds. Default 0.1." },
34277
- track: ANIMATION_TRACK,
34278
- animTreePath: { type: "string", description: "The AnimationTree node." },
34279
- stateName: { type: "string", description: "add_state: the state to add." },
34465
+ step: {
34466
+ type: "number",
34467
+ ops: ["create"],
34468
+ description: "create: keyframe snap in seconds. Default 0.1."
34469
+ },
34470
+ track: { ...ANIMATION_TRACK, ops: ["add_track"] },
34471
+ animTreePath: {
34472
+ type: "string",
34473
+ ops: ["read_states", "add_state", "connect_states"],
34474
+ description: "The AnimationTree node."
34475
+ },
34476
+ stateName: { type: "string", ops: ["add_state"], description: "add_state: the state to add." },
34280
34477
  stateMachinePath: {
34281
34478
  type: "string",
34282
- description: "add_state: a nested state machine. Default the root."
34479
+ ops: ["read_states", "add_state", "connect_states"],
34480
+ description: "A nested state machine, by the names of the states that lead to it. Default the root."
34481
+ },
34482
+ fromState: {
34483
+ type: "string",
34484
+ ops: ["connect_states"],
34485
+ description: "connect_states: the state the transition leaves."
34486
+ },
34487
+ toState: {
34488
+ type: "string",
34489
+ ops: ["connect_states"],
34490
+ description: "connect_states: the state it arrives at."
34283
34491
  },
34284
- fromState: { type: "string", description: "connect_states: the state the transition leaves." },
34285
- toState: { type: "string", description: "connect_states: the state it arrives at." },
34286
34492
  transitionType: {
34287
34493
  type: "string",
34494
+ ops: ["connect_states"],
34288
34495
  enum: ["immediate", "sync", "at_end"],
34289
34496
  description: "connect_states: default immediate."
34290
34497
  },
34291
34498
  advanceCondition: {
34292
34499
  type: "string",
34500
+ ops: ["connect_states"],
34293
34501
  description: "connect_states: the condition parameter that advances."
34294
34502
  }
34295
34503
  },
34296
34504
  requires: ["projectPath", "scenePath"],
34297
34505
  operations: {
34506
+ read: {
34507
+ effect: "read",
34508
+ summary: "the animations of an AnimationPlayer, or one with its tracks",
34509
+ requires: ["playerNodePath"]
34510
+ },
34511
+ read_states: {
34512
+ effect: "read",
34513
+ summary: "the states and transitions of an AnimationTree state machine",
34514
+ requires: ["animTreePath"]
34515
+ },
34298
34516
  create: {
34299
34517
  effect: "additive",
34300
34518
  summary: "a new animation in an AnimationPlayer",
@@ -34398,7 +34616,11 @@ var TOOL_SPECS = [
34398
34616
  summary: "document symbols from the language server, each with its kind, the line and column of its name, and the lastLine of its declaration",
34399
34617
  requires: []
34400
34618
  },
34401
- completion: { effect: "read", summary: "completions at a position", requires: ["line", "character"] },
34619
+ completion: {
34620
+ effect: "read",
34621
+ summary: "completions at a position, narrowed to the part of a name written before it, at most 100",
34622
+ requires: ["line", "character"]
34623
+ },
34402
34624
  hover: { effect: "read", summary: "hover text at a position", requires: ["line", "character"] }
34403
34625
  },
34404
34626
  defaultOperation: "structure"
@@ -34421,34 +34643,59 @@ var TOOL_SPECS = [
34421
34643
  {
34422
34644
  name: "resource_edit",
34423
34645
  title: "Edit resources",
34424
- description: "Resource files: 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. Needs the editor connected.",
34646
+ 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". Needs the editor connected.',
34425
34647
  parameters: {
34426
34648
  projectPath: PROJECT_PATH2,
34427
34649
  resourcePath: RESOURCE_PATH,
34428
34650
  resourceType: {
34429
34651
  type: "string",
34652
+ ops: ["create"],
34430
34653
  description: 'create: the resource class, such as "PhysicsMaterial" or "StandardMaterial3D".'
34431
34654
  },
34432
- properties: PROPERTIES,
34433
- script: { type: "string", description: "create: a script to attach, for custom resources." },
34655
+ properties: { ...PROPERTIES, ops: ["create", "modify"] },
34656
+ script: {
34657
+ type: "string",
34658
+ ops: ["create"],
34659
+ description: "create: a script to attach, for custom resources."
34660
+ },
34661
+ includeDefaults: {
34662
+ type: "boolean",
34663
+ ops: ["read"],
34664
+ description: "read: include properties still at their default. Default false."
34665
+ },
34434
34666
  shaderType: {
34435
34667
  type: "string",
34668
+ ops: ["create_shader"],
34436
34669
  enum: ["canvas_item", "spatial", "particles", "sky", "fog"],
34437
34670
  description: "create_shader."
34438
34671
  },
34439
34672
  code: {
34440
34673
  type: "string",
34674
+ ops: ["create_shader"],
34441
34675
  description: "create_shader: the shader source. Default: a minimal shader of that type."
34442
34676
  },
34443
- sources: TILESET_SOURCES,
34444
- controlType: { type: "string", description: 'Theme ops: the Control class, such as "Button".' },
34445
- colorName: { type: "string", description: 'set_theme_color: such as "font_color".' },
34446
- color: COLOUR2,
34447
- fontSizeName: { type: "string", description: 'set_theme_font_size: such as "font_size".' },
34448
- size: { type: "number", description: "set_theme_font_size: pixels." }
34677
+ sources: { ...TILESET_SOURCES, ops: ["create_tileset"] },
34678
+ controlType: {
34679
+ type: "string",
34680
+ ops: ["set_theme_color", "set_theme_font_size"],
34681
+ description: 'Theme ops: the Control class, such as "Button".'
34682
+ },
34683
+ colorName: {
34684
+ type: "string",
34685
+ ops: ["set_theme_color"],
34686
+ description: 'set_theme_color: such as "font_color".'
34687
+ },
34688
+ color: { ...COLOUR2, ops: ["set_theme_color"] },
34689
+ fontSizeName: {
34690
+ type: "string",
34691
+ ops: ["set_theme_font_size"],
34692
+ description: 'set_theme_font_size: such as "font_size".'
34693
+ },
34694
+ size: { type: "number", ops: ["set_theme_font_size"], description: "set_theme_font_size: pixels." }
34449
34695
  },
34450
34696
  requires: ["projectPath", "resourcePath"],
34451
34697
  operations: {
34698
+ read: { effect: "read", summary: "the properties a resource file sets", requires: [] },
34452
34699
  create: { effect: "destructive", summary: "a new resource of any class", requires: ["resourceType"] },
34453
34700
  modify: {
34454
34701
  effect: "destructive",
@@ -34475,8 +34722,8 @@ var TOOL_SPECS = [
34475
34722
  },
34476
34723
  {
34477
34724
  name: "editor_launch",
34478
- title: "Open or restart the editor",
34479
- description: "Opens the Godot editor on a project, or restarts the one already connected. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
34725
+ title: "Open, restart or close the editor",
34726
+ description: "Opens the Godot editor on a project, or restarts or closes the one already connected. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it; settingsDropped in its answer says what that save took out of project.godot. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose editor dials that project's own server; a project without the gdharness editor addon installed and enabled; and a project whose editor a restart is already bringing back.",
34480
34727
  parameters: {
34481
34728
  projectPath: { ...PROJECT_PATH2, ops: ["open"] },
34482
34729
  hidden: {
@@ -34492,6 +34739,11 @@ var TOOL_SPECS = [
34492
34739
  effect: "destructive",
34493
34740
  summary: "restart the connected editor and wait for it",
34494
34741
  requires: []
34742
+ },
34743
+ close: {
34744
+ effect: "destructive",
34745
+ summary: "save and quit the connected editor, and wait for it to go",
34746
+ requires: []
34495
34747
  }
34496
34748
  },
34497
34749
  defaultOperation: "open"
@@ -34617,7 +34869,7 @@ var TOOL_SPECS = [
34617
34869
  {
34618
34870
  name: "editor_output",
34619
34871
  title: "Game and editor output",
34620
- description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
34872
+ description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries: clean is true when no error was reported, and warnings, counted beside it, do not make it false. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing. startedAt is when the run began and elapsedMs how long it has been going; for a run that is over, endedAt is when it ended and elapsedMs how long it ran, and both are left out when nobody saw the end, as for a game closed in the editor or a run picked up from a note written before ends were recorded.",
34621
34873
  parameters: {
34622
34874
  severity: {
34623
34875
  type: "string",
@@ -34658,7 +34910,7 @@ var TOOL_SPECS = [
34658
34910
  },
34659
34911
  editor: {
34660
34912
  effect: "read",
34661
- summary: "What the connected editor itself has printed, from its first line, with repeated messages grouped and the ordinary lines above each group carried with it",
34913
+ summary: "What the connected editor itself has printed, from its first line, with repeated messages grouped and the ordinary lines above each group carried with it. The engine's progress bars are one line per task, saying how far an unfinished one got",
34662
34914
  requires: []
34663
34915
  }
34664
34916
  },
@@ -35311,7 +35563,6 @@ var RUNTIME_AUTOLOAD = {
35311
35563
  name: "GdharnessRuntime",
35312
35564
  path: "addons/gdharness_runtime/runtime_autoload.gd"
35313
35565
  };
35314
- var VERSION_MARKER = ".gdharness-version";
35315
35566
  var DIGEST_MARKER = ".gdharness-digest";
35316
35567
  var RUNTIME_ADDON = "gdharness_runtime";
35317
35568
  function editorAddonDigest(from = shippedAddonsDirectory()) {
@@ -36061,6 +36312,7 @@ var SCAN_SETTLED_MS = 500;
36061
36312
  var REIMPORT_TIMEOUT_MS = 300000;
36062
36313
  var AUDIO_BUS_OPS = new Set([
36063
36314
  "add_audio_bus",
36315
+ "remove_audio_bus",
36064
36316
  "set_audio_bus_effect",
36065
36317
  "set_audio_bus_volume"
36066
36318
  ]);
@@ -36082,6 +36334,7 @@ function settingsNamedBy(op, args) {
36082
36334
  case "set_main_scene":
36083
36335
  return ["application/run/main_scene"];
36084
36336
  case "add_input_action":
36337
+ case "remove_input_action":
36085
36338
  return text("actionName") === "" ? [] : [`input/${text("actionName")}`];
36086
36339
  case "enable_plugin":
36087
36340
  case "disable_plugin":
@@ -36680,6 +36933,7 @@ class GodotServer {
36680
36933
  launchedEditor = null;
36681
36934
  launchedFrom = null;
36682
36935
  launchedDropped = null;
36936
+ closedHere = null;
36683
36937
  ownProject;
36684
36938
  announcedAt = null;
36685
36939
  handingOverFrom = null;
@@ -36689,6 +36943,7 @@ class GodotServer {
36689
36943
  this.godotBridge = getDefaultBridge(this.ownProject);
36690
36944
  this.godotBridge.identifyEditorsWith(oneShotOptionOf);
36691
36945
  this.godotBridge.on("godot_connected", () => {
36946
+ this.closedHere = null;
36692
36947
  this.settleTheLaunchReading();
36693
36948
  });
36694
36949
  this.setupShutdownHandlers();
@@ -37282,6 +37537,10 @@ class GodotServer {
37282
37537
  }
37283
37538
  case "scene_animation":
37284
37539
  switch (op) {
37540
+ case "read":
37541
+ return await bridge("read_animations");
37542
+ case "read_states":
37543
+ return await bridge("read_animation_states");
37285
37544
  case "create":
37286
37545
  return await bridge("create_animation");
37287
37546
  case "add_track":
@@ -37306,6 +37565,8 @@ class GodotServer {
37306
37565
  return await this.handleScriptDiagnostics(args);
37307
37566
  case "resource_edit":
37308
37567
  switch (op) {
37568
+ case "read":
37569
+ return await bridge("read_resource");
37309
37570
  case "create":
37310
37571
  return await bridge("create_resource");
37311
37572
  case "modify":
@@ -37320,6 +37581,9 @@ class GodotServer {
37320
37581
  return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
37321
37582
  }
37322
37583
  case "editor_launch":
37584
+ if (op === "close") {
37585
+ return await this.handleCloseEditor();
37586
+ }
37323
37587
  return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
37324
37588
  case "editor_run":
37325
37589
  if (op === "stop") {
@@ -38217,21 +38481,31 @@ class GodotServer {
38217
38481
  }
38218
38482
  const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
38219
38483
  const asked = paths.value.filter((path) => !missing.includes(path));
38484
+ const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38485
+ const noRunner = !existsSync17(join22(project.value.path, runner));
38220
38486
  if (asked.length === 0) {
38221
38487
  const verdict = `nothing at ${missing.join(" and ")}`;
38488
+ const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
38222
38489
  return {
38223
38490
  content: [
38224
38491
  {
38225
38492
  type: "text",
38226
- text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
38493
+ text: `No tests ran: ${verdict}${andNoRunner}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
38227
38494
  },
38228
- { type: "text", text: answerJson({ passed: false, verdict, tests: 0 }) }
38495
+ {
38496
+ type: "text",
38497
+ text: answerJson({
38498
+ passed: false,
38499
+ verdict,
38500
+ tests: 0,
38501
+ ...noRunner ? { gdUnit4Installed: false } : {}
38502
+ })
38503
+ }
38229
38504
  ],
38230
38505
  isError: true
38231
38506
  };
38232
38507
  }
38233
- const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38234
- if (!existsSync17(join22(project.value.path, runner))) {
38508
+ if (noRunner) {
38235
38509
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
38236
38510
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
38237
38511
  ]);
@@ -38814,8 +39088,12 @@ class GodotServer {
38814
39088
  addonIsStale: greeted ? stale : undefined,
38815
39089
  greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
38816
39090
  bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
38817
- mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
39091
+ mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : this.closedHere !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
38818
39092
  editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
39093
+ closedHere: status.connected || arriving.length > 0 || this.closedHere === null ? undefined : {
39094
+ ...this.closedHere,
39095
+ note: "This server closed the editor on request, so nothing is coming until editor_launch open starts one."
39096
+ },
38819
39097
  restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
38820
39098
  quitEditorPid: unfinished.note.editorPid,
38821
39099
  quitAt: unfinished.note.quitAt,
@@ -39060,6 +39338,42 @@ class GodotServer {
39060
39338
  tookMs: Date.now() - began
39061
39339
  });
39062
39340
  }
39341
+ async handleCloseEditor() {
39342
+ const before = this.godotBridge.getStatus();
39343
+ if (!before.connected) {
39344
+ return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
39345
+ "editor_status says whether the bridge is up and what has reached it"
39346
+ ]);
39347
+ }
39348
+ if (!hasSaidWhoItIs(before) || before.editorPid === undefined) {
39349
+ return this.createErrorResponse(`The editor has connected and has not said who it is within ${greetingWaitMs() / 1000}s, so there is no process to wait on once it is asked to go.`, ["editor_status says when it has, under greeting while it has not", "Then ask for the close again"]);
39350
+ }
39351
+ const pid = before.editorPid;
39352
+ const settingsBefore = this.settingKeysOf(before.projectPath);
39353
+ const asked = await this.handleViaBridge("quit_editor", {});
39354
+ if (asked.isError === true) {
39355
+ return asked;
39356
+ }
39357
+ const began = Date.now();
39358
+ const gone = await this.waitForBridge(() => !alive(pid), began + editorRestartTimeoutMs());
39359
+ if (!gone) {
39360
+ return this.createErrorResponse(`The editor saved and was asked to go, and pid ${pid} is still running after ${editorRestartTimeoutMs() / 1000}s.`, [
39361
+ "It may be asking what to do about unsaved changes, or a script may be holding up its exit",
39362
+ "editor_status says whether it is still connected"
39363
+ ]);
39364
+ }
39365
+ this.closedHere = { editorPid: pid, at: new Date().toISOString() };
39366
+ if (this.launchedEditor?.pid === pid) {
39367
+ this.launchedEditor = null;
39368
+ }
39369
+ return this.jsonTextResponse({
39370
+ closed: true,
39371
+ editorPid: pid,
39372
+ projectPath: before.projectPath,
39373
+ ...this.whatTheEditorDropped(settingsBefore, this.settingKeysOf(before.projectPath)),
39374
+ tookMs: Date.now() - began
39375
+ });
39376
+ }
39063
39377
  settleTheLaunchReading() {
39064
39378
  const watched = this.launchedFrom;
39065
39379
  const status = this.godotBridge.getStatus();
@@ -39231,6 +39545,7 @@ class GodotServer {
39231
39545
  pid: launched.pid,
39232
39546
  startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
39233
39547
  };
39548
+ this.closedHere = null;
39234
39549
  return { pid: launched.pid, error: null };
39235
39550
  }
39236
39551
  async portsForAnEditor() {
@@ -39476,7 +39791,7 @@ class GodotServer {
39476
39791
  frameFunctions: profileFrameFunctions(projectPath)
39477
39792
  });
39478
39793
  if (armed.isError === true) {
39479
- return this.profilerRefusal(armed);
39794
+ return armed;
39480
39795
  }
39481
39796
  }
39482
39797
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
@@ -40326,7 +40641,7 @@ class GodotServer {
40326
40641
  async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
40327
40642
  const read = await this.handleViaBridge("profile_read", {});
40328
40643
  if (read.isError === true) {
40329
- return this.profilerRefusal(read);
40644
+ return read;
40330
40645
  }
40331
40646
  const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
40332
40647
  if (readBoolean(held, "profiled") !== true && readBoolean(held, "armed") !== true) {
@@ -40339,7 +40654,7 @@ class GodotServer {
40339
40654
  frameFunctions: profileFrameFunctions(projectPath)
40340
40655
  });
40341
40656
  if (started.isError === true) {
40342
- return this.profilerRefusal(started);
40657
+ return started;
40343
40658
  }
40344
40659
  if (readBoolean(asParams(JSON.parse(started.content[0]?.text ?? "{}")), "started") !== true) {
40345
40660
  return this.createErrorResponse("The editor says it is playing and has no debugger session for the game, so there is nothing to switch the profiler on in yet.", ["Ask again in a moment, once the game is up"]);
@@ -40395,13 +40710,6 @@ class GodotServer {
40395
40710
  }
40396
40711
  return false;
40397
40712
  }
40398
- profilerRefusal(refused) {
40399
- const said = refused.content[0]?.text ?? "";
40400
- return said.includes("Unknown tool: profile_") ? this.createErrorResponse("The editor's gdharness addon is older than the profiler, so it cannot profile the game it plays.", [
40401
- "editor_launch restart loads the addon this server installed",
40402
- "editor_status says which addon the editor holds"
40403
- ]) : refused;
40404
- }
40405
40713
  async handleWaitForRun(args) {
40406
40714
  await this.pickUpWhatTheEditorIsPlaying();
40407
40715
  const run = await this.currentRun();
@@ -41365,7 +41673,13 @@ class GodotServer {
41365
41673
  try {
41366
41674
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
41367
41675
  } catch (error) {
41368
- return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
41676
+ const message = errorMessage(error);
41677
+ if (message.includes(`Unknown tool: ${toolName}`)) {
41678
+ const status = this.godotBridge.getStatus();
41679
+ const remedy = addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
41680
+ return this.createErrorResponse(`The editor's gdharness addon is older than this ${SERVER_VERSION} server and has no ${toolName}, so it cannot answer this call. ${remedy ?? "Restart it with editor_launch restart to load the addon the project holds."}`, ["editor_status says which addon the editor holds"]);
41681
+ }
41682
+ return this.createErrorResponse(`The editor answered ${toolName} with an error: ${message}`);
41369
41683
  }
41370
41684
  }
41371
41685
  runtimeTimeoutMs() {