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/cli.js CHANGED
@@ -9852,16 +9852,6 @@ var init_class_cache = __esm(() => {
9852
9852
  TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
9853
9853
  });
9854
9854
 
9855
- // src/answer-json.ts
9856
- function answerJson(value) {
9857
- return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
9858
- }
9859
- var SCALAR, SCALAR_ARRAY;
9860
- var init_answer_json = __esm(() => {
9861
- SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
9862
- SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
9863
- });
9864
-
9865
9855
  // src/project-scan.ts
9866
9856
  import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync8 } from "node:fs";
9867
9857
  import { extname, join as join10 } from "node:path";
@@ -9869,8 +9859,14 @@ function skipped(name) {
9869
9859
  return name.startsWith(".") || SKIPPED.has(name);
9870
9860
  }
9871
9861
  function steppedOver(directory) {
9862
+ return gdignored(directory) || installedByUs(directory);
9863
+ }
9864
+ function gdignored(directory) {
9872
9865
  return existsSync10(join10(directory, ".gdignore"));
9873
9866
  }
9867
+ function installedByUs(directory) {
9868
+ return existsSync10(join10(directory, VERSION_MARKER));
9869
+ }
9874
9870
  function projectStructure(projectPath) {
9875
9871
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
9876
9872
  const visit = (directory) => {
@@ -9983,7 +9979,10 @@ function projectTexts(projectPath) {
9983
9979
  function ignoredTexts(projectPath) {
9984
9980
  const found = [];
9985
9981
  const visit = (directory, prefix, ignored) => {
9986
- const under = ignored || steppedOver(directory);
9982
+ if (installedByUs(directory)) {
9983
+ return;
9984
+ }
9985
+ const under = ignored || gdignored(directory);
9987
9986
  for (const entry of readdirSync5(directory, { withFileTypes: true })) {
9988
9987
  if (skipped(entry.name)) {
9989
9988
  continue;
@@ -10065,7 +10064,7 @@ function searchProject(projectPath, options) {
10065
10064
  visit(projectPath);
10066
10065
  return result;
10067
10066
  }
10068
- var SKIPPED, SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
10067
+ var SKIPPED, VERSION_MARKER = ".gdharness-version", SCENE_EXTENSIONS, ASSET_EXTENSIONS, CARRIES_A_UID, BINARY_EXTENSIONS;
10069
10068
  var init_project_scan = __esm(() => {
10070
10069
  SKIPPED = new Set(["node_modules"]);
10071
10070
  SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
@@ -10113,6 +10112,16 @@ var init_project_scan = __esm(() => {
10113
10112
  ]);
10114
10113
  });
10115
10114
 
10115
+ // src/answer-json.ts
10116
+ function answerJson(value) {
10117
+ return JSON.stringify(value, null, 2).replace(SCALAR_ARRAY, (_whole, items) => `[${items.split(/,\n\s+/).join(", ")}]`);
10118
+ }
10119
+ var SCALAR, SCALAR_ARRAY;
10120
+ var init_answer_json = __esm(() => {
10121
+ SCALAR = String.raw`(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)`;
10122
+ SCALAR_ARRAY = new RegExp(String.raw`\[\n\s+(${SCALAR}(?:,\n\s+${SCALAR})*)\n\s*\]`, "g");
10123
+ });
10124
+
10116
10125
  // src/resources.ts
10117
10126
  import { readFileSync as readFileSync9, realpathSync as realpathSync3 } from "node:fs";
10118
10127
  import { extname as extname2, resolve as resolve3 } from "node:path";
@@ -10719,10 +10728,11 @@ function inspectProject(projectPath) {
10719
10728
  problems
10720
10729
  };
10721
10730
  }
10722
- var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", VERSION_MARKER = ".gdharness-version", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime;
10731
+ var ADDONS, EDITOR_PLUGINS, RUNTIME_AUTOLOAD, SCRIPT_RUNS_SETTING = "gdharness/runtime/serve_script_runs", BIND_ADDRESS_SETTING = "gdharness/runtime/bind_address", PORT_SETTING = "gdharness/runtime/port", DIGEST_MARKER = ".gdharness-digest", RUNTIME_ADDON = "gdharness_runtime", shippedDigest, shippedRuntime;
10723
10732
  var init_setup = __esm(() => {
10724
10733
  init_class_cache();
10725
10734
  init_headless();
10735
+ init_project_scan();
10726
10736
  init_resources();
10727
10737
  init_server_version();
10728
10738
  ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
@@ -11005,10 +11015,14 @@ var init_tool_definitions = __esm(() => {
11005
11015
  {
11006
11016
  name: "project_settings",
11007
11017
  title: "Project settings",
11008
- 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.",
11018
+ 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.",
11009
11019
  parameters: {
11010
11020
  projectPath: PROJECT_PATH,
11011
- setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
11021
+ setting: {
11022
+ type: "string",
11023
+ ops: ["get", "set"],
11024
+ description: 'Setting path, such as "display/window/size/viewport_width".'
11025
+ },
11012
11026
  from: {
11013
11027
  type: "string",
11014
11028
  enum: ["disk", "editor"],
@@ -11022,40 +11036,66 @@ var init_tool_definitions = __esm(() => {
11022
11036
  },
11023
11037
  value: {
11024
11038
  blank: true,
11039
+ ops: ["set"],
11025
11040
  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.'
11026
11041
  },
11027
- name: { type: "string", description: "Autoload name." },
11028
- path: { type: "string", description: "Autoload script or scene inside the project." },
11042
+ name: { type: "string", ops: ["add_autoload", "remove_autoload"], description: "Autoload name." },
11043
+ path: {
11044
+ type: "string",
11045
+ ops: ["add_autoload"],
11046
+ description: "Autoload script or scene inside the project."
11047
+ },
11029
11048
  enabled: {
11030
11049
  type: "boolean",
11031
- 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."
11050
+ ops: ["add_autoload", "set_audio_bus_effect"],
11051
+ 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."
11032
11052
  },
11033
11053
  global: {
11034
11054
  type: "boolean",
11055
+ ops: ["add_autoload"],
11035
11056
  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.`
11036
11057
  },
11037
- scenePath: SCENE_PATH,
11038
- actionName: { type: "string", description: 'Input action name, such as "jump".' },
11039
- events: INPUT_EVENTS,
11058
+ scenePath: { ...SCENE_PATH, ops: ["set_main_scene"] },
11059
+ actionName: {
11060
+ type: "string",
11061
+ ops: ["add_input_action", "remove_input_action"],
11062
+ description: 'Input action name, such as "jump".'
11063
+ },
11064
+ events: { ...INPUT_EVENTS, ops: ["add_input_action"] },
11040
11065
  deadzone: {
11041
11066
  type: "number",
11067
+ ops: ["add_input_action"],
11042
11068
  minimum: 0,
11043
11069
  maximum: 1,
11044
11070
  description: "Input actions: analogue deadzone, 0 to 1. Default 0.5."
11045
11071
  },
11046
- pluginName: { type: "string", description: "Folder name under addons/." },
11047
- busName: { type: "string", description: "Audio bus name." },
11072
+ pluginName: {
11073
+ type: "string",
11074
+ ops: ["enable_plugin", "disable_plugin"],
11075
+ description: "Folder name under addons/."
11076
+ },
11077
+ busName: { type: "string", ops: ["add_audio_bus", "remove_audio_bus"], description: "Audio bus name." },
11048
11078
  parentBusIndex: {
11049
11079
  type: "number",
11080
+ ops: ["add_audio_bus"],
11050
11081
  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."
11051
11082
  },
11052
- busIndex: { type: "number", description: "Audio bus index." },
11083
+ busIndex: {
11084
+ type: "number",
11085
+ ops: ["set_audio_bus_effect", "set_audio_bus_volume"],
11086
+ description: "Audio bus index."
11087
+ },
11053
11088
  effectIndex: {
11054
11089
  type: "number",
11090
+ ops: ["set_audio_bus_effect"],
11055
11091
  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."
11056
11092
  },
11057
- effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
11058
- volumeDb: { type: "number", description: "Bus volume in decibels." }
11093
+ effectType: {
11094
+ type: "string",
11095
+ ops: ["set_audio_bus_effect"],
11096
+ description: 'Effect class, such as "AudioEffectReverb".'
11097
+ },
11098
+ volumeDb: { type: "number", ops: ["set_audio_bus_volume"], description: "Bus volume in decibels." }
11059
11099
  },
11060
11100
  requires: ["projectPath"],
11061
11101
  operations: {
@@ -11077,9 +11117,19 @@ var init_tool_definitions = __esm(() => {
11077
11117
  summary: "register an input action and its events",
11078
11118
  requires: ["actionName", "events"]
11079
11119
  },
11120
+ remove_input_action: {
11121
+ effect: "destructive",
11122
+ summary: "remove an input action the project defines",
11123
+ requires: ["actionName"]
11124
+ },
11080
11125
  enable_plugin: { effect: "destructive", summary: "enable an addon", requires: ["pluginName"] },
11081
11126
  disable_plugin: { effect: "destructive", summary: "disable an addon", requires: ["pluginName"] },
11082
11127
  add_audio_bus: { effect: "additive", summary: "add an audio bus", requires: ["busName"] },
11128
+ remove_audio_bus: {
11129
+ effect: "destructive",
11130
+ summary: "remove an audio bus, sending what sent to it on to where it sent",
11131
+ requires: ["busName"]
11132
+ },
11083
11133
  set_audio_bus_effect: {
11084
11134
  effect: "destructive",
11085
11135
  summary: "put an effect in a slot on a bus",
@@ -11096,7 +11146,7 @@ var init_tool_definitions = __esm(() => {
11096
11146
  name: "project_search",
11097
11147
  title: "Search the project",
11098
11148
  effect: "read",
11099
- 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.",
11149
+ 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.",
11100
11150
  parameters: {
11101
11151
  projectPath: PROJECT_PATH,
11102
11152
  query: { type: "string", description: "The text or pattern to find." },
@@ -11258,8 +11308,18 @@ var init_tool_definitions = __esm(() => {
11258
11308
  parameters: {
11259
11309
  projectPath: PROJECT_PATH,
11260
11310
  scenePath: SCENE_PATH,
11261
- rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
11262
- newPath: { type: "string", description: "save_as: where the copy goes." }
11311
+ rootNodeType: {
11312
+ type: "string",
11313
+ ops: ["create"],
11314
+ description: "create: the root node class. Default Node."
11315
+ },
11316
+ rootNodeName: {
11317
+ type: "string",
11318
+ ops: ["create"],
11319
+ description: "create: the root node name. Default the class name, as the editor names a new root."
11320
+ },
11321
+ script: { type: "string", ops: ["create"], description: "create: a script to attach to the root." },
11322
+ newPath: { type: "string", ops: ["save_as"], description: "save_as: where the copy goes." }
11263
11323
  },
11264
11324
  requires: ["projectPath", "scenePath"],
11265
11325
  operations: {
@@ -11339,7 +11399,7 @@ var init_tool_definitions = __esm(() => {
11339
11399
  {
11340
11400
  name: "scene_signal",
11341
11401
  title: "Scene signals",
11342
- description: "Signal connections in a scene file. Needs the editor connected.",
11402
+ 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.",
11343
11403
  parameters: {
11344
11404
  projectPath: PROJECT_PATH,
11345
11405
  scenePath: SCENE_PATH,
@@ -11368,43 +11428,78 @@ var init_tool_definitions = __esm(() => {
11368
11428
  {
11369
11429
  name: "scene_animation",
11370
11430
  title: "Animations",
11371
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. Needs the editor connected.",
11431
+ 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.",
11372
11432
  parameters: {
11373
11433
  projectPath: PROJECT_PATH,
11374
11434
  scenePath: SCENE_PATH,
11375
- playerNodePath: { type: "string", description: "The AnimationPlayer node." },
11435
+ playerNodePath: {
11436
+ type: "string",
11437
+ ops: ["read", "create", "add_track"],
11438
+ description: "The AnimationPlayer node."
11439
+ },
11376
11440
  animationName: {
11377
11441
  type: "string",
11378
- description: "The animation in the player. add_state: the one that state plays."
11442
+ ops: ["read", "create", "add_track", "add_state"],
11443
+ 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."
11379
11444
  },
11380
- length: { type: "number", description: "create: seconds. Default 1." },
11445
+ length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
11381
11446
  loopMode: {
11382
11447
  type: "string",
11448
+ ops: ["create"],
11383
11449
  enum: ["none", "linear", "pingpong"],
11384
11450
  description: "create: default none."
11385
11451
  },
11386
- step: { type: "number", description: "create: keyframe snap in seconds. Default 0.1." },
11387
- track: ANIMATION_TRACK,
11388
- animTreePath: { type: "string", description: "The AnimationTree node." },
11389
- stateName: { type: "string", description: "add_state: the state to add." },
11452
+ step: {
11453
+ type: "number",
11454
+ ops: ["create"],
11455
+ description: "create: keyframe snap in seconds. Default 0.1."
11456
+ },
11457
+ track: { ...ANIMATION_TRACK, ops: ["add_track"] },
11458
+ animTreePath: {
11459
+ type: "string",
11460
+ ops: ["read_states", "add_state", "connect_states"],
11461
+ description: "The AnimationTree node."
11462
+ },
11463
+ stateName: { type: "string", ops: ["add_state"], description: "add_state: the state to add." },
11390
11464
  stateMachinePath: {
11391
11465
  type: "string",
11392
- description: "add_state: a nested state machine. Default the root."
11466
+ ops: ["read_states", "add_state", "connect_states"],
11467
+ description: "A nested state machine, by the names of the states that lead to it. Default the root."
11468
+ },
11469
+ fromState: {
11470
+ type: "string",
11471
+ ops: ["connect_states"],
11472
+ description: "connect_states: the state the transition leaves."
11473
+ },
11474
+ toState: {
11475
+ type: "string",
11476
+ ops: ["connect_states"],
11477
+ description: "connect_states: the state it arrives at."
11393
11478
  },
11394
- fromState: { type: "string", description: "connect_states: the state the transition leaves." },
11395
- toState: { type: "string", description: "connect_states: the state it arrives at." },
11396
11479
  transitionType: {
11397
11480
  type: "string",
11481
+ ops: ["connect_states"],
11398
11482
  enum: ["immediate", "sync", "at_end"],
11399
11483
  description: "connect_states: default immediate."
11400
11484
  },
11401
11485
  advanceCondition: {
11402
11486
  type: "string",
11487
+ ops: ["connect_states"],
11403
11488
  description: "connect_states: the condition parameter that advances."
11404
11489
  }
11405
11490
  },
11406
11491
  requires: ["projectPath", "scenePath"],
11407
11492
  operations: {
11493
+ read: {
11494
+ effect: "read",
11495
+ summary: "the animations of an AnimationPlayer, or one with its tracks",
11496
+ requires: ["playerNodePath"]
11497
+ },
11498
+ read_states: {
11499
+ effect: "read",
11500
+ summary: "the states and transitions of an AnimationTree state machine",
11501
+ requires: ["animTreePath"]
11502
+ },
11408
11503
  create: {
11409
11504
  effect: "additive",
11410
11505
  summary: "a new animation in an AnimationPlayer",
@@ -11508,7 +11603,11 @@ var init_tool_definitions = __esm(() => {
11508
11603
  summary: "document symbols from the language server, each with its kind, the line and column of its name, and the lastLine of its declaration",
11509
11604
  requires: []
11510
11605
  },
11511
- completion: { effect: "read", summary: "completions at a position", requires: ["line", "character"] },
11606
+ completion: {
11607
+ effect: "read",
11608
+ summary: "completions at a position, narrowed to the part of a name written before it, at most 100",
11609
+ requires: ["line", "character"]
11610
+ },
11512
11611
  hover: { effect: "read", summary: "hover text at a position", requires: ["line", "character"] }
11513
11612
  },
11514
11613
  defaultOperation: "structure"
@@ -11531,34 +11630,59 @@ var init_tool_definitions = __esm(() => {
11531
11630
  {
11532
11631
  name: "resource_edit",
11533
11632
  title: "Edit resources",
11534
- 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.",
11633
+ 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.',
11535
11634
  parameters: {
11536
11635
  projectPath: PROJECT_PATH,
11537
11636
  resourcePath: RESOURCE_PATH,
11538
11637
  resourceType: {
11539
11638
  type: "string",
11639
+ ops: ["create"],
11540
11640
  description: 'create: the resource class, such as "PhysicsMaterial" or "StandardMaterial3D".'
11541
11641
  },
11542
- properties: PROPERTIES,
11543
- script: { type: "string", description: "create: a script to attach, for custom resources." },
11642
+ properties: { ...PROPERTIES, ops: ["create", "modify"] },
11643
+ script: {
11644
+ type: "string",
11645
+ ops: ["create"],
11646
+ description: "create: a script to attach, for custom resources."
11647
+ },
11648
+ includeDefaults: {
11649
+ type: "boolean",
11650
+ ops: ["read"],
11651
+ description: "read: include properties still at their default. Default false."
11652
+ },
11544
11653
  shaderType: {
11545
11654
  type: "string",
11655
+ ops: ["create_shader"],
11546
11656
  enum: ["canvas_item", "spatial", "particles", "sky", "fog"],
11547
11657
  description: "create_shader."
11548
11658
  },
11549
11659
  code: {
11550
11660
  type: "string",
11661
+ ops: ["create_shader"],
11551
11662
  description: "create_shader: the shader source. Default: a minimal shader of that type."
11552
11663
  },
11553
- sources: TILESET_SOURCES,
11554
- controlType: { type: "string", description: 'Theme ops: the Control class, such as "Button".' },
11555
- colorName: { type: "string", description: 'set_theme_color: such as "font_color".' },
11556
- color: COLOUR,
11557
- fontSizeName: { type: "string", description: 'set_theme_font_size: such as "font_size".' },
11558
- size: { type: "number", description: "set_theme_font_size: pixels." }
11664
+ sources: { ...TILESET_SOURCES, ops: ["create_tileset"] },
11665
+ controlType: {
11666
+ type: "string",
11667
+ ops: ["set_theme_color", "set_theme_font_size"],
11668
+ description: 'Theme ops: the Control class, such as "Button".'
11669
+ },
11670
+ colorName: {
11671
+ type: "string",
11672
+ ops: ["set_theme_color"],
11673
+ description: 'set_theme_color: such as "font_color".'
11674
+ },
11675
+ color: { ...COLOUR, ops: ["set_theme_color"] },
11676
+ fontSizeName: {
11677
+ type: "string",
11678
+ ops: ["set_theme_font_size"],
11679
+ description: 'set_theme_font_size: such as "font_size".'
11680
+ },
11681
+ size: { type: "number", ops: ["set_theme_font_size"], description: "set_theme_font_size: pixels." }
11559
11682
  },
11560
11683
  requires: ["projectPath", "resourcePath"],
11561
11684
  operations: {
11685
+ read: { effect: "read", summary: "the properties a resource file sets", requires: [] },
11562
11686
  create: { effect: "destructive", summary: "a new resource of any class", requires: ["resourceType"] },
11563
11687
  modify: {
11564
11688
  effect: "destructive",
@@ -11585,8 +11709,8 @@ var init_tool_definitions = __esm(() => {
11585
11709
  },
11586
11710
  {
11587
11711
  name: "editor_launch",
11588
- title: "Open or restart the editor",
11589
- 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.",
11712
+ title: "Open, restart or close the editor",
11713
+ 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.",
11590
11714
  parameters: {
11591
11715
  projectPath: { ...PROJECT_PATH, ops: ["open"] },
11592
11716
  hidden: {
@@ -11602,6 +11726,11 @@ var init_tool_definitions = __esm(() => {
11602
11726
  effect: "destructive",
11603
11727
  summary: "restart the connected editor and wait for it",
11604
11728
  requires: []
11729
+ },
11730
+ close: {
11731
+ effect: "destructive",
11732
+ summary: "save and quit the connected editor, and wait for it to go",
11733
+ requires: []
11605
11734
  }
11606
11735
  },
11607
11736
  defaultOperation: "open"
@@ -11727,7 +11856,7 @@ var init_tool_definitions = __esm(() => {
11727
11856
  {
11728
11857
  name: "editor_output",
11729
11858
  title: "Game and editor output",
11730
- 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.",
11859
+ 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.",
11731
11860
  parameters: {
11732
11861
  severity: {
11733
11862
  type: "string",
@@ -11768,7 +11897,7 @@ var init_tool_definitions = __esm(() => {
11768
11897
  },
11769
11898
  editor: {
11770
11899
  effect: "read",
11771
- 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",
11900
+ 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",
11772
11901
  requires: []
11773
11902
  }
11774
11903
  },
@@ -12343,9 +12472,11 @@ var init_headless_operations = __esm(() => {
12343
12472
  remove_autoload: "remove_autoload",
12344
12473
  set_main_scene: "set_main_scene",
12345
12474
  add_input_action: "add_input_action",
12475
+ remove_input_action: "remove_input_action",
12346
12476
  enable_plugin: "enable_plugin",
12347
12477
  disable_plugin: "disable_plugin",
12348
12478
  add_audio_bus: "create_audio_bus",
12479
+ remove_audio_bus: "remove_audio_bus",
12349
12480
  set_audio_bus_effect: "set_audio_bus_effect",
12350
12481
  set_audio_bus_volume: "set_audio_bus_volume"
12351
12482
  }),
@@ -35665,10 +35796,56 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
35665
35796
  }
35666
35797
  const log = new GameLog;
35667
35798
  const { text, of } = theFirstOf(path);
35668
- log.append("transcript", text);
35799
+ log.append("transcript", foldProgress(text));
35669
35800
  log.finish();
35670
35801
  return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
35671
35802
  }
35803
+ function foldProgress(text) {
35804
+ const out = [];
35805
+ const open = new Map;
35806
+ for (const raw of text.split(`
35807
+ `)) {
35808
+ const line = raw.replace(/\r$/, "").replace(COLOUR_CODE, "");
35809
+ const step = PROGRESS_STEP.exec(line);
35810
+ if (step !== null) {
35811
+ const [, percent = "", name = "", said = ""] = step;
35812
+ const started = /^Started (.*)$/.exec(said);
35813
+ if (started !== null) {
35814
+ const task = { name, label: started[1] ?? "", percent, done: false };
35815
+ open.set(name, task);
35816
+ out.push(task);
35817
+ continue;
35818
+ }
35819
+ const task = open.get(name);
35820
+ if (task !== undefined) {
35821
+ task.percent = percent;
35822
+ if (said.trim() !== "") {
35823
+ task.last = said.trim();
35824
+ }
35825
+ continue;
35826
+ }
35827
+ }
35828
+ const finished = PROGRESS_DONE.exec(line);
35829
+ const task = finished === null ? undefined : open.get(finished[1] ?? "");
35830
+ if (task !== undefined) {
35831
+ task.done = true;
35832
+ open.delete(task.name);
35833
+ continue;
35834
+ }
35835
+ out.push(raw);
35836
+ }
35837
+ return out.map((entry) => {
35838
+ if (typeof entry === "string") {
35839
+ return entry;
35840
+ }
35841
+ if (entry.done) {
35842
+ return `[ DONE ] ${entry.name} | ${entry.label}`;
35843
+ }
35844
+ const last = entry.last === undefined ? "" : `, and the last step it named was "${entry.last}"`;
35845
+ return `[ ${entry.percent}% ] ${entry.name} | ${entry.label}, which has not finished${last}`;
35846
+ }).join(`
35847
+ `);
35848
+ }
35672
35849
  function theFirstOf(path) {
35673
35850
  const whole = statSync7(path).size;
35674
35851
  if (whole <= MOST_CONSOLE_BYTES) {
@@ -35823,9 +36000,11 @@ function linesBefore(entries, at, collapsed, wanted) {
35823
36000
  }
35824
36001
  return found.reverse();
35825
36002
  }
35826
- var MOST_CONSOLE_BYTES, NAMED_VALUES = 6, WALK_BACK_LIMIT = 500, SLOTS;
36003
+ var PROGRESS_STEP, PROGRESS_DONE, MOST_CONSOLE_BYTES, NAMED_VALUES = 6, WALK_BACK_LIMIT = 500, SLOTS;
35827
36004
  var init_editor_log = __esm(() => {
35828
36005
  init_game_log();
36006
+ PROGRESS_STEP = /^\[\s*(\d+)% \] (\S+) \| (.*)$/;
36007
+ PROGRESS_DONE = /^\[ DONE \] (\S+)\s*$/;
35829
36008
  MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
35830
36009
  SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
35831
36010
  });
@@ -38774,6 +38953,12 @@ function theEditorHasComeBack(status, startedAt) {
38774
38953
  function hasSaidWhoItIs(status) {
38775
38954
  return status.connected && status.addonVersion !== undefined;
38776
38955
  }
38956
+ function withoutTrailingSeparator(path) {
38957
+ if (path.length <= 1 || !/[\\/]$/.test(path) || /^[A-Za-z]:[\\/]$/.test(path)) {
38958
+ return path;
38959
+ }
38960
+ return path.slice(0, -1);
38961
+ }
38777
38962
  function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
38778
38963
  return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
38779
38964
  }
@@ -39083,7 +39268,7 @@ var init_godot_bridge = __esm(() => {
39083
39268
  this.elsewhereUnlessOneShot(message.project_path, message.editor_pid, message.opened_by_a_server === true);
39084
39269
  return;
39085
39270
  }
39086
- this.connectionInfo.projectPath = message.project_path;
39271
+ this.connectionInfo.projectPath = withoutTrailingSeparator(message.project_path);
39087
39272
  this.connectionInfo.addonVersion = message.addon_version ?? "";
39088
39273
  this.connectionInfo.addonDigest = message.addon_digest;
39089
39274
  this.connectionInfo.editorPid = message.editor_pid;
@@ -40429,6 +40614,28 @@ function shownSymbol(entry) {
40429
40614
  ...children.length > 0 ? { children: children.map(shownSymbol) } : {}
40430
40615
  };
40431
40616
  }
40617
+ function typedBefore(content, line, character) {
40618
+ const text = content.split(/\r?\n/)[line] ?? "";
40619
+ return /[\p{L}\p{N}_]*$/u.exec(text.slice(0, character))?.[0] ?? "";
40620
+ }
40621
+ function narrowedCompletions(completions, typed) {
40622
+ const wanted = typed.toLowerCase();
40623
+ const matching = wanted === "" ? completions : completions.filter((entry) => {
40624
+ const record = asRecord(entry);
40625
+ const name = typeof record["filterText"] === "string" ? record["filterText"] : record["label"];
40626
+ return typeof name === "string" && name.toLowerCase().startsWith(wanted);
40627
+ });
40628
+ const shown = matching.slice(0, MOST_COMPLETIONS);
40629
+ return {
40630
+ ...typed === "" ? {} : { typed },
40631
+ completions: shown.map(shownCompletion),
40632
+ total: matching.length,
40633
+ ...matching.length > shown.length ? {
40634
+ omitted: matching.length - shown.length,
40635
+ 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.`
40636
+ } : {}
40637
+ };
40638
+ }
40432
40639
  function shownCompletion(entry) {
40433
40640
  const { data: _request, kind, ...rest } = asRecord(entry);
40434
40641
  return { ...rest, kind: named(kind, COMPLETION_KINDS) };
@@ -40499,7 +40706,7 @@ async function handleLSPTool(client, toolName, args) {
40499
40706
  case "lsp_get_completions": {
40500
40707
  const { line, character } = protocolPosition(parsedArgs);
40501
40708
  const completions = await client.getCompletions(scriptPath, content, line, character);
40502
- return asToolResponse({ completions: completions.map(shownCompletion) });
40709
+ return asToolResponse(narrowedCompletions(completions, typedBefore(content, line, character)));
40503
40710
  }
40504
40711
  case "lsp_get_hover": {
40505
40712
  const { line, character } = protocolPosition(parsedArgs);
@@ -40524,7 +40731,7 @@ async function handleLSPTool(client, toolName, args) {
40524
40731
  });
40525
40732
  }
40526
40733
  }
40527
- var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, LANGUAGE_SERVER_START_MS = 30000, LANGUAGE_SERVER_RETRY_MS = 250, LanguageServerNotListening, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer, SEVERITIES, SYMBOL_KINDS, COMPLETION_KINDS;
40734
+ var DEFAULT_LSP_PORT = 6005, DIAGNOSTICS_TIMEOUT_MS = 5000, REQUEST_TIMEOUT_MS2 = 1e4, REFERENCES_TIMEOUT_MS = 180000, LSPTimeout, LANGUAGE_SERVER_START_MS = 30000, LANGUAGE_SERVER_RETRY_MS = 250, LanguageServerNotListening, ArgumentRefusal, NoDiagnosticsPublished, AnotherProjectsServer, SEVERITIES, SYMBOL_KINDS, COMPLETION_KINDS, MOST_COMPLETIONS = 100;
40528
40735
  var init_lsp_client = __esm(() => {
40529
40736
  init_answer_json();
40530
40737
  init_errors();
@@ -43419,6 +43626,7 @@ function settingsNamedBy(op, args) {
43419
43626
  case "set_main_scene":
43420
43627
  return ["application/run/main_scene"];
43421
43628
  case "add_input_action":
43629
+ case "remove_input_action":
43422
43630
  return text("actionName") === "" ? [] : [`input/${text("actionName")}`];
43423
43631
  case "enable_plugin":
43424
43632
  case "disable_plugin":
@@ -43974,6 +44182,7 @@ class GodotServer {
43974
44182
  launchedEditor = null;
43975
44183
  launchedFrom = null;
43976
44184
  launchedDropped = null;
44185
+ closedHere = null;
43977
44186
  ownProject;
43978
44187
  announcedAt = null;
43979
44188
  handingOverFrom = null;
@@ -43983,6 +44192,7 @@ class GodotServer {
43983
44192
  this.godotBridge = getDefaultBridge(this.ownProject);
43984
44193
  this.godotBridge.identifyEditorsWith(oneShotOptionOf);
43985
44194
  this.godotBridge.on("godot_connected", () => {
44195
+ this.closedHere = null;
43986
44196
  this.settleTheLaunchReading();
43987
44197
  });
43988
44198
  this.setupShutdownHandlers();
@@ -44576,6 +44786,10 @@ class GodotServer {
44576
44786
  }
44577
44787
  case "scene_animation":
44578
44788
  switch (op) {
44789
+ case "read":
44790
+ return await bridge("read_animations");
44791
+ case "read_states":
44792
+ return await bridge("read_animation_states");
44579
44793
  case "create":
44580
44794
  return await bridge("create_animation");
44581
44795
  case "add_track":
@@ -44600,6 +44814,8 @@ class GodotServer {
44600
44814
  return await this.handleScriptDiagnostics(args);
44601
44815
  case "resource_edit":
44602
44816
  switch (op) {
44817
+ case "read":
44818
+ return await bridge("read_resource");
44603
44819
  case "create":
44604
44820
  return await bridge("create_resource");
44605
44821
  case "modify":
@@ -44614,6 +44830,9 @@ class GodotServer {
44614
44830
  return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
44615
44831
  }
44616
44832
  case "editor_launch":
44833
+ if (op === "close") {
44834
+ return await this.handleCloseEditor();
44835
+ }
44617
44836
  return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
44618
44837
  case "editor_run":
44619
44838
  if (op === "stop") {
@@ -45511,21 +45730,31 @@ class GodotServer {
45511
45730
  }
45512
45731
  const missing = paths.value.filter((path) => !existsSync20(join26(project.value.path, path.slice("res://".length))));
45513
45732
  const asked = paths.value.filter((path) => !missing.includes(path));
45733
+ const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
45734
+ const noRunner = !existsSync20(join26(project.value.path, runner));
45514
45735
  if (asked.length === 0) {
45515
45736
  const verdict = `nothing at ${missing.join(" and ")}`;
45737
+ const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
45516
45738
  return {
45517
45739
  content: [
45518
45740
  {
45519
45741
  type: "text",
45520
- text: `No tests ran: ${verdict}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
45742
+ text: `No tests ran: ${verdict}${andNoRunner}.${elsewhereIn(project.value.path, missing[0] ?? "")}`
45521
45743
  },
45522
- { type: "text", text: answerJson({ passed: false, verdict, tests: 0 }) }
45744
+ {
45745
+ type: "text",
45746
+ text: answerJson({
45747
+ passed: false,
45748
+ verdict,
45749
+ tests: 0,
45750
+ ...noRunner ? { gdUnit4Installed: false } : {}
45751
+ })
45752
+ }
45523
45753
  ],
45524
45754
  isError: true
45525
45755
  };
45526
45756
  }
45527
- const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
45528
- if (!existsSync20(join26(project.value.path, runner))) {
45757
+ if (noRunner) {
45529
45758
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
45530
45759
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
45531
45760
  ]);
@@ -46108,8 +46337,12 @@ class GodotServer {
46108
46337
  addonIsStale: greeted ? stale : undefined,
46109
46338
  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,
46110
46339
  bridgeAvailable: this.bridgeStartupError === null && status.port !== null,
46111
- mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
46340
+ mayYetConnect: status.connected ? undefined : arriving.length > 0 ? true : unfinished !== null ? unfinished.underway : this.closedHere !== null ? false : anEditorIsStillComing(status.listeningSince, launchedIsUp),
46112
46341
  editorsNotYetConnected: arriving.length > 0 ? arriving : undefined,
46342
+ closedHere: status.connected || arriving.length > 0 || this.closedHere === null ? undefined : {
46343
+ ...this.closedHere,
46344
+ note: "This server closed the editor on request, so nothing is coming until editor_launch open starts one."
46345
+ },
46113
46346
  restartInterrupted: unfinished === null || unfinished.underway || arriving.length > 0 ? undefined : {
46114
46347
  quitEditorPid: unfinished.note.editorPid,
46115
46348
  quitAt: unfinished.note.quitAt,
@@ -46354,6 +46587,42 @@ class GodotServer {
46354
46587
  tookMs: Date.now() - began
46355
46588
  });
46356
46589
  }
46590
+ async handleCloseEditor() {
46591
+ const before = this.godotBridge.getStatus();
46592
+ if (!before.connected) {
46593
+ return this.createErrorResponse("No editor is connected, so there is nothing to close.", [
46594
+ "editor_status says whether the bridge is up and what has reached it"
46595
+ ]);
46596
+ }
46597
+ if (!hasSaidWhoItIs(before) || before.editorPid === undefined) {
46598
+ 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"]);
46599
+ }
46600
+ const pid = before.editorPid;
46601
+ const settingsBefore = this.settingKeysOf(before.projectPath);
46602
+ const asked = await this.handleViaBridge("quit_editor", {});
46603
+ if (asked.isError === true) {
46604
+ return asked;
46605
+ }
46606
+ const began = Date.now();
46607
+ const gone = await this.waitForBridge(() => !alive(pid), began + editorRestartTimeoutMs());
46608
+ if (!gone) {
46609
+ return this.createErrorResponse(`The editor saved and was asked to go, and pid ${pid} is still running after ${editorRestartTimeoutMs() / 1000}s.`, [
46610
+ "It may be asking what to do about unsaved changes, or a script may be holding up its exit",
46611
+ "editor_status says whether it is still connected"
46612
+ ]);
46613
+ }
46614
+ this.closedHere = { editorPid: pid, at: new Date().toISOString() };
46615
+ if (this.launchedEditor?.pid === pid) {
46616
+ this.launchedEditor = null;
46617
+ }
46618
+ return this.jsonTextResponse({
46619
+ closed: true,
46620
+ editorPid: pid,
46621
+ projectPath: before.projectPath,
46622
+ ...this.whatTheEditorDropped(settingsBefore, this.settingKeysOf(before.projectPath)),
46623
+ tookMs: Date.now() - began
46624
+ });
46625
+ }
46357
46626
  settleTheLaunchReading() {
46358
46627
  const watched = this.launchedFrom;
46359
46628
  const status = this.godotBridge.getStatus();
@@ -46525,6 +46794,7 @@ class GodotServer {
46525
46794
  pid: launched.pid,
46526
46795
  startedAt: (await startTimesOf([launched.pid])).get(launched.pid) ?? null
46527
46796
  };
46797
+ this.closedHere = null;
46528
46798
  return { pid: launched.pid, error: null };
46529
46799
  }
46530
46800
  async portsForAnEditor() {
@@ -46770,7 +47040,7 @@ class GodotServer {
46770
47040
  frameFunctions: profileFrameFunctions(projectPath)
46771
47041
  });
46772
47042
  if (armed.isError === true) {
46773
- return this.profilerRefusal(armed);
47043
+ return armed;
46774
47044
  }
46775
47045
  }
46776
47046
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
@@ -47620,7 +47890,7 @@ class GodotServer {
47620
47890
  async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
47621
47891
  const read = await this.handleViaBridge("profile_read", {});
47622
47892
  if (read.isError === true) {
47623
- return this.profilerRefusal(read);
47893
+ return read;
47624
47894
  }
47625
47895
  const held = asParams(JSON.parse(read.content[0]?.text ?? "{}"));
47626
47896
  if (readBoolean(held, "profiled") !== true && readBoolean(held, "armed") !== true) {
@@ -47633,7 +47903,7 @@ class GodotServer {
47633
47903
  frameFunctions: profileFrameFunctions(projectPath)
47634
47904
  });
47635
47905
  if (started.isError === true) {
47636
- return this.profilerRefusal(started);
47906
+ return started;
47637
47907
  }
47638
47908
  if (readBoolean(asParams(JSON.parse(started.content[0]?.text ?? "{}")), "started") !== true) {
47639
47909
  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"]);
@@ -47689,13 +47959,6 @@ class GodotServer {
47689
47959
  }
47690
47960
  return false;
47691
47961
  }
47692
- profilerRefusal(refused) {
47693
- const said = refused.content[0]?.text ?? "";
47694
- 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.", [
47695
- "editor_launch restart loads the addon this server installed",
47696
- "editor_status says which addon the editor holds"
47697
- ]) : refused;
47698
- }
47699
47962
  async handleWaitForRun(args) {
47700
47963
  await this.pickUpWhatTheEditorIsPlaying();
47701
47964
  const run = await this.currentRun();
@@ -48659,7 +48922,13 @@ class GodotServer {
48659
48922
  try {
48660
48923
  return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
48661
48924
  } catch (error) {
48662
- return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
48925
+ const message = errorMessage(error);
48926
+ if (message.includes(`Unknown tool: ${toolName}`)) {
48927
+ const status = this.godotBridge.getStatus();
48928
+ const remedy = addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
48929
+ 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"]);
48930
+ }
48931
+ return this.createErrorResponse(`The editor answered ${toolName} with an error: ${message}`);
48663
48932
  }
48664
48933
  }
48665
48934
  runtimeTimeoutMs() {
@@ -49014,6 +49283,7 @@ var init_server = __esm(() => {
49014
49283
  HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
49015
49284
  AUDIO_BUS_OPS = new Set([
49016
49285
  "add_audio_bus",
49286
+ "remove_audio_bus",
49017
49287
  "set_audio_bus_effect",
49018
49288
  "set_audio_bus_volume"
49019
49289
  ]);