gdharness 1.2.15 → 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
@@ -28352,10 +28352,58 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
28352
28352
  }
28353
28353
  const log = new GameLog;
28354
28354
  const { text, of } = theFirstOf(path);
28355
- log.append("transcript", text);
28355
+ log.append("transcript", foldProgress(text));
28356
28356
  log.finish();
28357
28357
  return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
28358
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
+ }
28359
28407
  var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
28360
28408
  function theFirstOf(path) {
28361
28409
  const whole = statSync3(path).size;
@@ -28615,6 +28663,12 @@ function theEditorHasComeBack(status, startedAt) {
28615
28663
  function hasSaidWhoItIs(status) {
28616
28664
  return status.connected && status.addonVersion !== undefined;
28617
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
+ }
28618
28672
  function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
28619
28673
  return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
28620
28674
  }
@@ -28915,7 +28969,7 @@ class GodotBridge extends EventEmitter {
28915
28969
  this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
28916
28970
  return;
28917
28971
  }
28918
- this.connectionInfo.projectPath = message.project_path;
28972
+ this.connectionInfo.projectPath = withoutTrailingSeparator(message.project_path);
28919
28973
  this.connectionInfo.addonVersion = message.addon_version ?? "";
28920
28974
  this.connectionInfo.addonDigest = message.addon_digest;
28921
28975
  this.connectionInfo.editorPid = message.editor_pid;
@@ -29664,9 +29718,11 @@ var HEADLESS_OPERATIONS = dictionary({
29664
29718
  remove_autoload: "remove_autoload",
29665
29719
  set_main_scene: "set_main_scene",
29666
29720
  add_input_action: "add_input_action",
29721
+ remove_input_action: "remove_input_action",
29667
29722
  enable_plugin: "enable_plugin",
29668
29723
  disable_plugin: "disable_plugin",
29669
29724
  add_audio_bus: "create_audio_bus",
29725
+ remove_audio_bus: "remove_audio_bus",
29670
29726
  set_audio_bus_effect: "set_audio_bus_effect",
29671
29727
  set_audio_bus_volume: "set_audio_bus_volume"
29672
29728
  }),
@@ -30883,6 +30939,29 @@ function shownSymbol(entry) {
30883
30939
  ...children.length > 0 ? { children: children.map(shownSymbol) } : {}
30884
30940
  };
30885
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
+ }
30886
30965
  function shownCompletion(entry) {
30887
30966
  const { data: _request, kind, ...rest } = asRecord(entry);
30888
30967
  return { ...rest, kind: named(kind, COMPLETION_KINDS) };
@@ -30953,7 +31032,7 @@ async function handleLSPTool(client, toolName, args) {
30953
31032
  case "lsp_get_completions": {
30954
31033
  const { line, character } = protocolPosition(parsedArgs);
30955
31034
  const completions = await client.getCompletions(scriptPath, content, line, character);
30956
- return asToolResponse({ completions: completions.map(shownCompletion) });
31035
+ return asToolResponse(narrowedCompletions(completions, typedBefore(content, line, character)));
30957
31036
  }
30958
31037
  case "lsp_get_hover": {
30959
31038
  const { line, character } = protocolPosition(parsedArgs);
@@ -31123,9 +31202,16 @@ var SKIPPED = new Set(["node_modules"]);
31123
31202
  function skipped(name) {
31124
31203
  return name.startsWith(".") || SKIPPED.has(name);
31125
31204
  }
31205
+ var VERSION_MARKER = ".gdharness-version";
31126
31206
  function steppedOver(directory) {
31207
+ return gdignored(directory) || installedByUs(directory);
31208
+ }
31209
+ function gdignored(directory) {
31127
31210
  return existsSync13(join16(directory, ".gdignore"));
31128
31211
  }
31212
+ function installedByUs(directory) {
31213
+ return existsSync13(join16(directory, VERSION_MARKER));
31214
+ }
31129
31215
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
31130
31216
  var ASSET_EXTENSIONS = new Set([
31131
31217
  "png",
@@ -31281,7 +31367,10 @@ function projectTexts(projectPath) {
31281
31367
  function ignoredTexts(projectPath) {
31282
31368
  const found = [];
31283
31369
  const visit = (directory, prefix, ignored) => {
31284
- const under = ignored || steppedOver(directory);
31370
+ if (installedByUs(directory)) {
31371
+ return;
31372
+ }
31373
+ const under = ignored || gdignored(directory);
31285
31374
  for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31286
31375
  if (skipped(entry.name)) {
31287
31376
  continue;
@@ -33939,10 +34028,14 @@ var TOOL_SPECS = [
33939
34028
  {
33940
34029
  name: "project_settings",
33941
34030
  title: "Project settings",
33942
- 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.",
33943
34032
  parameters: {
33944
34033
  projectPath: PROJECT_PATH2,
33945
- 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
+ },
33946
34039
  from: {
33947
34040
  type: "string",
33948
34041
  enum: ["disk", "editor"],
@@ -33956,40 +34049,66 @@ var TOOL_SPECS = [
33956
34049
  },
33957
34050
  value: {
33958
34051
  blank: true,
34052
+ ops: ["set"],
33959
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.'
33960
34054
  },
33961
- name: { type: "string", description: "Autoload name." },
33962
- 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
+ },
33963
34061
  enabled: {
33964
34062
  type: "boolean",
33965
- 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."
33966
34065
  },
33967
34066
  global: {
33968
34067
  type: "boolean",
34068
+ ops: ["add_autoload"],
33969
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.`
33970
34070
  },
33971
- scenePath: SCENE_PATH,
33972
- actionName: { type: "string", description: 'Input action name, such as "jump".' },
33973
- 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"] },
33974
34078
  deadzone: {
33975
34079
  type: "number",
34080
+ ops: ["add_input_action"],
33976
34081
  minimum: 0,
33977
34082
  maximum: 1,
33978
34083
  description: "Input actions: analogue deadzone, 0 to 1. Default 0.5."
33979
34084
  },
33980
- pluginName: { type: "string", description: "Folder name under addons/." },
33981
- 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." },
33982
34091
  parentBusIndex: {
33983
34092
  type: "number",
34093
+ ops: ["add_audio_bus"],
33984
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."
33985
34095
  },
33986
- 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
+ },
33987
34101
  effectIndex: {
33988
34102
  type: "number",
34103
+ ops: ["set_audio_bus_effect"],
33989
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."
33990
34105
  },
33991
- effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
33992
- 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." }
33993
34112
  },
33994
34113
  requires: ["projectPath"],
33995
34114
  operations: {
@@ -34011,9 +34130,19 @@ var TOOL_SPECS = [
34011
34130
  summary: "register an input action and its events",
34012
34131
  requires: ["actionName", "events"]
34013
34132
  },
34133
+ remove_input_action: {
34134
+ effect: "destructive",
34135
+ summary: "remove an input action the project defines",
34136
+ requires: ["actionName"]
34137
+ },
34014
34138
  enable_plugin: { effect: "destructive", summary: "enable an addon", requires: ["pluginName"] },
34015
34139
  disable_plugin: { effect: "destructive", summary: "disable an addon", requires: ["pluginName"] },
34016
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
+ },
34017
34146
  set_audio_bus_effect: {
34018
34147
  effect: "destructive",
34019
34148
  summary: "put an effect in a slot on a bus",
@@ -34030,7 +34159,7 @@ var TOOL_SPECS = [
34030
34159
  name: "project_search",
34031
34160
  title: "Search the project",
34032
34161
  effect: "read",
34033
- 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.",
34034
34163
  parameters: {
34035
34164
  projectPath: PROJECT_PATH2,
34036
34165
  query: { type: "string", description: "The text or pattern to find." },
@@ -34192,8 +34321,18 @@ var TOOL_SPECS = [
34192
34321
  parameters: {
34193
34322
  projectPath: PROJECT_PATH2,
34194
34323
  scenePath: SCENE_PATH,
34195
- rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
34196
- 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." }
34197
34336
  },
34198
34337
  requires: ["projectPath", "scenePath"],
34199
34338
  operations: {
@@ -34273,7 +34412,7 @@ var TOOL_SPECS = [
34273
34412
  {
34274
34413
  name: "scene_signal",
34275
34414
  title: "Scene signals",
34276
- 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.",
34277
34416
  parameters: {
34278
34417
  projectPath: PROJECT_PATH2,
34279
34418
  scenePath: SCENE_PATH,
@@ -34302,43 +34441,78 @@ var TOOL_SPECS = [
34302
34441
  {
34303
34442
  name: "scene_animation",
34304
34443
  title: "Animations",
34305
- 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.",
34306
34445
  parameters: {
34307
34446
  projectPath: PROJECT_PATH2,
34308
34447
  scenePath: SCENE_PATH,
34309
- playerNodePath: { type: "string", description: "The AnimationPlayer node." },
34448
+ playerNodePath: {
34449
+ type: "string",
34450
+ ops: ["read", "create", "add_track"],
34451
+ description: "The AnimationPlayer node."
34452
+ },
34310
34453
  animationName: {
34311
34454
  type: "string",
34312
- 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."
34313
34457
  },
34314
- length: { type: "number", description: "create: seconds. Default 1." },
34458
+ length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
34315
34459
  loopMode: {
34316
34460
  type: "string",
34461
+ ops: ["create"],
34317
34462
  enum: ["none", "linear", "pingpong"],
34318
34463
  description: "create: default none."
34319
34464
  },
34320
- step: { type: "number", description: "create: keyframe snap in seconds. Default 0.1." },
34321
- track: ANIMATION_TRACK,
34322
- animTreePath: { type: "string", description: "The AnimationTree node." },
34323
- 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." },
34324
34477
  stateMachinePath: {
34325
34478
  type: "string",
34326
- 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."
34327
34491
  },
34328
- fromState: { type: "string", description: "connect_states: the state the transition leaves." },
34329
- toState: { type: "string", description: "connect_states: the state it arrives at." },
34330
34492
  transitionType: {
34331
34493
  type: "string",
34494
+ ops: ["connect_states"],
34332
34495
  enum: ["immediate", "sync", "at_end"],
34333
34496
  description: "connect_states: default immediate."
34334
34497
  },
34335
34498
  advanceCondition: {
34336
34499
  type: "string",
34500
+ ops: ["connect_states"],
34337
34501
  description: "connect_states: the condition parameter that advances."
34338
34502
  }
34339
34503
  },
34340
34504
  requires: ["projectPath", "scenePath"],
34341
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
+ },
34342
34516
  create: {
34343
34517
  effect: "additive",
34344
34518
  summary: "a new animation in an AnimationPlayer",
@@ -34442,7 +34616,11 @@ var TOOL_SPECS = [
34442
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",
34443
34617
  requires: []
34444
34618
  },
34445
- 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
+ },
34446
34624
  hover: { effect: "read", summary: "hover text at a position", requires: ["line", "character"] }
34447
34625
  },
34448
34626
  defaultOperation: "structure"
@@ -34465,34 +34643,59 @@ var TOOL_SPECS = [
34465
34643
  {
34466
34644
  name: "resource_edit",
34467
34645
  title: "Edit resources",
34468
- 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.',
34469
34647
  parameters: {
34470
34648
  projectPath: PROJECT_PATH2,
34471
34649
  resourcePath: RESOURCE_PATH,
34472
34650
  resourceType: {
34473
34651
  type: "string",
34652
+ ops: ["create"],
34474
34653
  description: 'create: the resource class, such as "PhysicsMaterial" or "StandardMaterial3D".'
34475
34654
  },
34476
- properties: PROPERTIES,
34477
- 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
+ },
34478
34666
  shaderType: {
34479
34667
  type: "string",
34668
+ ops: ["create_shader"],
34480
34669
  enum: ["canvas_item", "spatial", "particles", "sky", "fog"],
34481
34670
  description: "create_shader."
34482
34671
  },
34483
34672
  code: {
34484
34673
  type: "string",
34674
+ ops: ["create_shader"],
34485
34675
  description: "create_shader: the shader source. Default: a minimal shader of that type."
34486
34676
  },
34487
- sources: TILESET_SOURCES,
34488
- controlType: { type: "string", description: 'Theme ops: the Control class, such as "Button".' },
34489
- colorName: { type: "string", description: 'set_theme_color: such as "font_color".' },
34490
- color: COLOUR2,
34491
- fontSizeName: { type: "string", description: 'set_theme_font_size: such as "font_size".' },
34492
- 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." }
34493
34695
  },
34494
34696
  requires: ["projectPath", "resourcePath"],
34495
34697
  operations: {
34698
+ read: { effect: "read", summary: "the properties a resource file sets", requires: [] },
34496
34699
  create: { effect: "destructive", summary: "a new resource of any class", requires: ["resourceType"] },
34497
34700
  modify: {
34498
34701
  effect: "destructive",
@@ -34519,8 +34722,8 @@ var TOOL_SPECS = [
34519
34722
  },
34520
34723
  {
34521
34724
  name: "editor_launch",
34522
- title: "Open or restart the editor",
34523
- 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.",
34524
34727
  parameters: {
34525
34728
  projectPath: { ...PROJECT_PATH2, ops: ["open"] },
34526
34729
  hidden: {
@@ -34536,6 +34739,11 @@ var TOOL_SPECS = [
34536
34739
  effect: "destructive",
34537
34740
  summary: "restart the connected editor and wait for it",
34538
34741
  requires: []
34742
+ },
34743
+ close: {
34744
+ effect: "destructive",
34745
+ summary: "save and quit the connected editor, and wait for it to go",
34746
+ requires: []
34539
34747
  }
34540
34748
  },
34541
34749
  defaultOperation: "open"
@@ -34661,7 +34869,7 @@ var TOOL_SPECS = [
34661
34869
  {
34662
34870
  name: "editor_output",
34663
34871
  title: "Game and editor output",
34664
- 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.",
34665
34873
  parameters: {
34666
34874
  severity: {
34667
34875
  type: "string",
@@ -34702,7 +34910,7 @@ var TOOL_SPECS = [
34702
34910
  },
34703
34911
  editor: {
34704
34912
  effect: "read",
34705
- 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",
34706
34914
  requires: []
34707
34915
  }
34708
34916
  },
@@ -35355,7 +35563,6 @@ var RUNTIME_AUTOLOAD = {
35355
35563
  name: "GdharnessRuntime",
35356
35564
  path: "addons/gdharness_runtime/runtime_autoload.gd"
35357
35565
  };
35358
- var VERSION_MARKER = ".gdharness-version";
35359
35566
  var DIGEST_MARKER = ".gdharness-digest";
35360
35567
  var RUNTIME_ADDON = "gdharness_runtime";
35361
35568
  function editorAddonDigest(from = shippedAddonsDirectory()) {
@@ -36105,6 +36312,7 @@ var SCAN_SETTLED_MS = 500;
36105
36312
  var REIMPORT_TIMEOUT_MS = 300000;
36106
36313
  var AUDIO_BUS_OPS = new Set([
36107
36314
  "add_audio_bus",
36315
+ "remove_audio_bus",
36108
36316
  "set_audio_bus_effect",
36109
36317
  "set_audio_bus_volume"
36110
36318
  ]);
@@ -36126,6 +36334,7 @@ function settingsNamedBy(op, args) {
36126
36334
  case "set_main_scene":
36127
36335
  return ["application/run/main_scene"];
36128
36336
  case "add_input_action":
36337
+ case "remove_input_action":
36129
36338
  return text("actionName") === "" ? [] : [`input/${text("actionName")}`];
36130
36339
  case "enable_plugin":
36131
36340
  case "disable_plugin":
@@ -36724,6 +36933,7 @@ class GodotServer {
36724
36933
  launchedEditor = null;
36725
36934
  launchedFrom = null;
36726
36935
  launchedDropped = null;
36936
+ closedHere = null;
36727
36937
  ownProject;
36728
36938
  announcedAt = null;
36729
36939
  handingOverFrom = null;
@@ -36733,6 +36943,7 @@ class GodotServer {
36733
36943
  this.godotBridge = getDefaultBridge(this.ownProject);
36734
36944
  this.godotBridge.identifyEditorsWith(oneShotOptionOf);
36735
36945
  this.godotBridge.on("godot_connected", () => {
36946
+ this.closedHere = null;
36736
36947
  this.settleTheLaunchReading();
36737
36948
  });
36738
36949
  this.setupShutdownHandlers();
@@ -37326,6 +37537,10 @@ class GodotServer {
37326
37537
  }
37327
37538
  case "scene_animation":
37328
37539
  switch (op) {
37540
+ case "read":
37541
+ return await bridge("read_animations");
37542
+ case "read_states":
37543
+ return await bridge("read_animation_states");
37329
37544
  case "create":
37330
37545
  return await bridge("create_animation");
37331
37546
  case "add_track":
@@ -37350,6 +37565,8 @@ class GodotServer {
37350
37565
  return await this.handleScriptDiagnostics(args);
37351
37566
  case "resource_edit":
37352
37567
  switch (op) {
37568
+ case "read":
37569
+ return await bridge("read_resource");
37353
37570
  case "create":
37354
37571
  return await bridge("create_resource");
37355
37572
  case "modify":
@@ -37364,6 +37581,9 @@ class GodotServer {
37364
37581
  return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
37365
37582
  }
37366
37583
  case "editor_launch":
37584
+ if (op === "close") {
37585
+ return await this.handleCloseEditor();
37586
+ }
37367
37587
  return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
37368
37588
  case "editor_run":
37369
37589
  if (op === "stop") {
@@ -38261,21 +38481,31 @@ class GodotServer {
38261
38481
  }
38262
38482
  const missing = paths.value.filter((path) => !existsSync17(join22(project.value.path, path.slice("res://".length))));
38263
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));
38264
38486
  if (asked.length === 0) {
38265
38487
  const verdict = `nothing at ${missing.join(" and ")}`;
38488
+ const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
38266
38489
  return {
38267
38490
  content: [
38268
38491
  {
38269
38492
  type: "text",
38270
- text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
38493
+ text: `No tests ran: ${verdict}${andNoRunner}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
38271
38494
  },
38272
- { 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
+ }
38273
38504
  ],
38274
38505
  isError: true
38275
38506
  };
38276
38507
  }
38277
- const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
38278
- if (!existsSync17(join22(project.value.path, runner))) {
38508
+ if (noRunner) {
38279
38509
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
38280
38510
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
38281
38511
  ]);
@@ -38858,8 +39088,12 @@ class GodotServer {
38858
39088
  addonIsStale: greeted ? stale : undefined,
38859
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,
38860
39090
  bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
38861
- 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),
38862
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
+ },
38863
39097
  restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
38864
39098
  quitEditorPid: unfinished.note.editorPid,
38865
39099
  quitAt: unfinished.note.quitAt,
@@ -39104,6 +39338,42 @@ class GodotServer {
39104
39338
  tookMs: Date.now() - began
39105
39339
  });
39106
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
+ }
39107
39377
  settleTheLaunchReading() {
39108
39378
  const watched = this.launchedFrom;
39109
39379
  const status = this.godotBridge.getStatus();
@@ -39275,6 +39545,7 @@ class GodotServer {
39275
39545
  pid: launched.pid,
39276
39546
  startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
39277
39547
  };
39548
+ this.closedHere = null;
39278
39549
  return { pid: launched.pid, error: null };
39279
39550
  }
39280
39551
  async portsForAnEditor() {
@@ -39520,7 +39791,7 @@ class GodotServer {
39520
39791
  frameFunctions: profileFrameFunctions(projectPath)
39521
39792
  });
39522
39793
  if (armed.isError === true) {
39523
- return this.profilerRefusal(armed);
39794
+ return armed;
39524
39795
  }
39525
39796
  }
39526
39797
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
@@ -40370,7 +40641,7 @@ class GodotServer {
40370
40641
  async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
40371
40642
  const read = await this.handleViaBridge("profile_read", {});
40372
40643
  if (read.isError === true) {
40373
- return this.profilerRefusal(read);
40644
+ return read;
40374
40645
  }
40375
40646
  const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
40376
40647
  if (readBoolean(held, "profiled") !== true && readBoolean(held, "armed") !== true) {
@@ -40383,7 +40654,7 @@ class GodotServer {
40383
40654
  frameFunctions: profileFrameFunctions(projectPath)
40384
40655
  });
40385
40656
  if (started.isError === true) {
40386
- return this.profilerRefusal(started);
40657
+ return started;
40387
40658
  }
40388
40659
  if (readBoolean(asParams(JSON.parse(started.content[0]?.text ?? "{}")), "started") !== true) {
40389
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"]);
@@ -40439,13 +40710,6 @@ class GodotServer {
40439
40710
  }
40440
40711
  return false;
40441
40712
  }
40442
- profilerRefusal(refused) {
40443
- const said = refused.content[0]?.text ?? "";
40444
- 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.", [
40445
- "editor_launch restart loads the addon this server installed",
40446
- "editor_status says which addon the editor holds"
40447
- ]) : refused;
40448
- }
40449
40713
  async handleWaitForRun(args) {
40450
40714
  await this.pickUpWhatTheEditorIsPlaying();
40451
40715
  const run = await this.currentRun();
@@ -41409,7 +41673,13 @@ class GodotServer {
41409
41673
  try {
41410
41674
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
41411
41675
  } catch (error) {
41412
- 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}`);
41413
41683
  }
41414
41684
  }
41415
41685
  runtimeTimeoutMs() {