gdharness 1.3.47 → 1.3.48

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
@@ -12111,7 +12111,8 @@ class EngineSlots {
12111
12111
  changes = new FolderChanges(this.directory);
12112
12112
  continue;
12113
12113
  }
12114
- await changes.next(SLOT_LOOK_MS, SLOT_SETTLE_MS, signal);
12114
+ const sayAt = waiting !== undefined ? said + WAIT_SAID_MS : this.say !== undefined && this.held.size === 0 ? Math.max(began + COMMAND_WAIT_SAID_MS, this.saidAt + WAIT_SAID_MS) : Number.POSITIVE_INFINITY;
12115
+ await changes.next(Math.max(0, Math.min(SLOT_LOOK_MS, sayAt - Date.now())), SLOT_SETTLE_MS, signal);
12115
12116
  }
12116
12117
  } finally {
12117
12118
  changes?.close();
@@ -12169,11 +12170,17 @@ class EngineSlots {
12169
12170
  }
12170
12171
  release(lock) {
12171
12172
  this.held.delete(lock);
12172
- try {
12173
- if (holderOf(readFileSync8(lock, "utf8"))?.pid === this.holder.pid) {
12174
- removeWhole(lock);
12175
- }
12176
- } catch {}
12173
+ giveBackSlot(lock, this.holder.pid);
12174
+ }
12175
+ forget(lock) {
12176
+ this.held.delete(lock);
12177
+ }
12178
+ hand(lock, to) {
12179
+ const handed = handSlot(lock, this.holder.pid, to);
12180
+ if (handed) {
12181
+ this.held.delete(lock);
12182
+ }
12183
+ return handed;
12177
12184
  }
12178
12185
  made(path) {
12179
12186
  let made;
@@ -12240,6 +12247,22 @@ class EngineSlots {
12240
12247
  this.starts.set(pid, { started, readAt: Date.now() });
12241
12248
  }
12242
12249
  }
12250
+ function handSlot(lock, from, to) {
12251
+ let content;
12252
+ try {
12253
+ content = readWhole(lock);
12254
+ } catch {
12255
+ return false;
12256
+ }
12257
+ return content !== null && holderOf(content)?.pid === from && writeWhole(lock, JSON.stringify(to));
12258
+ }
12259
+ function giveBackSlot(lock, pid) {
12260
+ try {
12261
+ if (holderOf(readFileSync8(lock, "utf8"))?.pid === pid) {
12262
+ removeWhole(lock);
12263
+ }
12264
+ } catch {}
12265
+ }
12243
12266
  function sayWaitsTo(say) {
12244
12267
  saidTo = say;
12245
12268
  }
@@ -12308,8 +12331,14 @@ async function slotForCall() {
12308
12331
  waits.said ??= (waitedMs) => slotWaitedSaid(waitedMs, slots.count, slots.directory, heldBy);
12309
12332
  }
12310
12333
  return {
12334
+ lock,
12335
+ from: slots.holder.pid,
12311
12336
  release: () => {
12312
12337
  slots.release(lock);
12338
+ },
12339
+ handTo: (holder) => slots.hand(lock, holder),
12340
+ leave: () => {
12341
+ slots.forget(lock);
12313
12342
  }
12314
12343
  };
12315
12344
  }
@@ -12848,7 +12877,7 @@ class GameLog {
12848
12877
  return;
12849
12878
  }
12850
12879
  const owner = this.lastHeadline[source];
12851
- if (owner !== null && /^\s/.test(line)) {
12880
+ if (owner !== null && /^\s/.test(line) && !GDUNIT_CASE.test(line)) {
12852
12881
  owner.detail.push(line.trim());
12853
12882
  owner.continues = false;
12854
12883
  return;
@@ -12925,10 +12954,11 @@ function answersTo(entry, severity, contains) {
12925
12954
  const needle = contains.toLowerCase();
12926
12955
  return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
12927
12956
  }
12928
- var HEADLINE, CRASH, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, LOCATION2, FRAME, LONGEST_CYCLE = 64;
12957
+ var HEADLINE, CRASH, GDUNIT_CASE, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, LOCATION2, FRAME, LONGEST_CYCLE = 64;
12929
12958
  var init_game_log = __esm(() => {
12930
12959
  HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
12931
12960
  CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d+/;
12961
+ GDUNIT_CASE = /^\s+res:\/\/.+?\.(?:gd|cs) > .+? (?:STARTED|PASSED|FAILED|ERROR|SKIPPED|FLAKY)(?:\s|$)/;
12932
12962
  CRASH_FILLER = /^(?:=+|-- END OF .* BACKTRACE --|\[\d+\] .* - no debug info in .*)$/;
12933
12963
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
12934
12964
  STREAMS = ["stdout", "stderr", "transcript"];
@@ -14225,7 +14255,7 @@ async function operationOutcome(engine, operation, params, projectPath) {
14225
14255
  let ran;
14226
14256
  let answer;
14227
14257
  try {
14228
- ran = await runEngine(engine.godotPath, args);
14258
+ ran = await runEngine(engine.godotPath, args, { timeout: engine.ceiling ?? OPERATION_CEILING });
14229
14259
  answer = answerIn(answerFile);
14230
14260
  } finally {
14231
14261
  discard(paramsDir);
@@ -14315,7 +14345,7 @@ function answerIn(file) {
14315
14345
  };
14316
14346
  }
14317
14347
  }
14318
- var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", ASIDE = "[gdharness] aside", RESUMED = "[gdharness] resumed", MESSAGES_KEPT = 200, ERRORS_NAMED = 3, NO_BLENDER = "Blender path is invalid or not set", BLENDER_STOPPED = "the import pass stopped at a .blend file and imported nothing: the engine imports .blend files through Blender, which the editor setting filesystem/import/blender/blender_path names, and here it names none. Set it in an editor, or set the project setting filesystem/import/blender/enabled to false with project_settings set, which has the engine leave .blend files unimported.";
14348
+ var AFTER_ANSWER_NOTE = "Printed after this operation had written its answer, while the engine ran the project's autoloads and shut down: Godot starts a project's autoloads in every script run and cannot be told not to. An error raised in a project script here is that autoload's own, and none of these changes the answer.", BEFORE_OPERATION_NOTE = "Printed while the engine started the project's autoloads, before this operation ran: an operation that compiles the project's scripts waits for them, because the engine knows an autoload's name only once it is in the tree. An error raised in a project script here is that autoload's own.", OWN_ERROR = "[ERROR] ", ANSWERED = "[gdharness] answered", WAITING = "[gdharness] waiting for the autoloads", BEGUN = "[gdharness] begun", ASIDE = "[gdharness] aside", RESUMED = "[gdharness] resumed", MESSAGES_KEPT = 200, OPERATION_CEILING, ERRORS_NAMED = 3, NO_BLENDER = "Blender path is invalid or not set", BLENDER_STOPPED = "the import pass stopped at a .blend file and imported nothing: the engine imports .blend files through Blender, which the editor setting filesystem/import/blender/blender_path names, and here it names none. Set it in an editor, or set the project setting filesystem/import/blender/enabled to false with project_settings set, which has the engine leave .blend files unimported.";
14319
14349
  var init_headless = __esm(() => {
14320
14350
  init_doc_cache();
14321
14351
  init_engine_leaks();
@@ -14326,6 +14356,7 @@ var init_headless = __esm(() => {
14326
14356
  init_reimport();
14327
14357
  init_scratch();
14328
14358
  init_script_origins();
14359
+ OPERATION_CEILING = { ms: 600000, said: "ten minutes" };
14329
14360
  });
14330
14361
 
14331
14362
  // src/update-check.ts
@@ -16593,13 +16624,25 @@ var init_tool_definitions = __esm(() => {
16593
16624
  ],
16594
16625
  description: "add_track makes property and method tracks; remove_track takes any type read answers."
16595
16626
  },
16596
- nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
16627
+ nodePath: {
16628
+ type: "string",
16629
+ description: "Target node, relative to the AnimationPlayer's root_node, which is the player's parent unless the scene sets it."
16630
+ },
16597
16631
  property: { type: "string", description: "For property tracks: the property to animate." },
16598
16632
  update: {
16599
16633
  type: "string",
16600
16634
  enum: ["continuous", "discrete", "capture"],
16601
16635
  description: "For property tracks: how the value moves between keys. continuous blends toward the next key, so a bool or an int changes halfway to it; discrete holds each key's value until the next; capture blends from the value the property holds when playing starts. Without it a new track is made as the editor makes one: as the RESET animation's track for the same property, otherwise continuous for a float, vector, colour or transform and discrete for anything else. The answer gives the track's update."
16602
16636
  },
16637
+ interpolation: {
16638
+ type: "string",
16639
+ enum: ["nearest", "linear", "cubic", "linear_angle", "cubic_angle"],
16640
+ description: "For property tracks: how a continuous track blends between keys. Without it a new track is made linear, or as the RESET track for the same property is, and an existing track keeps its own."
16641
+ },
16642
+ loopWrap: {
16643
+ type: "boolean",
16644
+ description: "For property tracks: whether a looping animation blends its last key into its first. Without it a new track wraps, or as the RESET track for the same property does, and an existing track keeps its own."
16645
+ },
16603
16646
  method: {
16604
16647
  type: "string",
16605
16648
  description: "For method tracks: the method to call. A method track holds every method key for its node, so a key calling another method joins the node's track, with existingTrack true. The answer's methodFound says whether the target node has it; a method it lacks is still added, while a nodePath that reaches no node is refused, since it may be written next, and until it exists each call the track makes fails. A key whose args the method cannot take (too few or too many, or an object where it takes a value or the reverse) is written too, and answers argumentsFit false with misfit saying why. remove_track: only the keys calling this method."
@@ -16957,7 +17000,7 @@ var init_tool_definitions = __esm(() => {
16957
17000
  direction: {
16958
17001
  type: "string",
16959
17002
  enum: ["forward", "reverse"],
16960
- description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
17003
+ description: "forward: what this resource loads or names. Each file named by its path carries kind, as reverse words it: ext_resource, extends, preload, load, include, or path for a path only written in a string, which the file itself may never load. Each reference closing a cycle is marked circular, with the cycle under circular_references. A script's dependencies include every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. A file's dependencies are listed at its first entry, and again further down only where the depth cut that list short and more is left; every other entry for a file naming any carries dependencies_listed_above. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
16961
17004
  },
16962
17005
  depth: {
16963
17006
  type: "integer",
@@ -16992,7 +17035,7 @@ var init_tool_definitions = __esm(() => {
16992
17035
  options: {
16993
17036
  type: "object",
16994
17037
  ops: ["set_options"],
16995
- description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported. changed gives each option written as {was, now}, was null for one the import file did not hold.`,
17038
+ description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads. One already holding the value given is answered under unchanged_options rather than updated_options, and when none changed nothing is written or reimported. changed gives each option written as {was, now}.`,
16996
17039
  additionalProperties: true
16997
17040
  },
16998
17041
  reimport: {
@@ -17032,7 +17075,7 @@ var init_tool_definitions = __esm(() => {
17032
17075
  },
17033
17076
  uid: {
17034
17077
  effect: "read",
17035
- summary: "the UID of one file and where it was read: a script's or shader's .uid file, an imported file's .import, or a scene's or resource's own header",
17078
+ summary: "the UID of one file and where it was read: a script's or shader's .uid file, an imported file's .import, or a scene's or resource's own header. A file with none is answered with uid null and what would give it one",
17036
17079
  requires: ["resourcePath"]
17037
17080
  },
17038
17081
  refresh_uids: {
@@ -17079,7 +17122,7 @@ var init_tool_definitions = __esm(() => {
17079
17122
  type: "string",
17080
17123
  ops: ["set_preset"],
17081
17124
  enum: ["all_resources", "scenes", "resources", "exclude"],
17082
- description: "set_preset: what the preset exports: every resource, the scenes listed in files and what they use, the resources listed, or every resource but those listed."
17125
+ description: "set_preset: what the preset exports: every resource, the scenes listed in files and what they use, the resources listed, or every resource but those listed. What a scene or a resource uses is what the engine counts as its dependencies, so a file named only in a string, such as a scene path held in an exported property, or a script reached only by its class_name, is left out of the export, though project_dependencies lists both."
17083
17126
  },
17084
17127
  files: {
17085
17128
  type: "array",
@@ -17214,7 +17257,7 @@ var init_tool_definitions = __esm(() => {
17214
17257
  name: "scene_tree",
17215
17258
  title: "Scene tree",
17216
17259
  effect: "read",
17217
- description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one; its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). The root of a scene inheriting another names the scene it inherits under inherits, and every node that scene or one further down makes names its file under inheritedFrom; this file holds only the values it changes on them. A node in any group answers groups, sorted, from this file and every scene it comes from. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
17260
+ description: "The nodes of a scene file: names, classes and hierarchy, with properties when asked. type is the engine class, and a node whose script declares a class_name names it under scriptClass, as scene_node get does. A node that instances another scene names that file under instanceOf, and the nodes listed under it come from that file rather than this one; its editableChildren says whether this scene, or a scene it inherits, keeps values of its own for them (scene_node set editableChildren). The root of a scene inheriting another names the scene it inherits under inherits, and every node that scene or one further down makes names its file under inheritedFrom; this file holds only the values it changes on them. A node in any group answers groups, sorted, from this file and every scene it comes from. Needs no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without making its nodes, so no script of theirs runs (autoloads still start once it has answered, as in every headless call). For the nodes of a game that is running, runtime_inspect tree.",
17218
17261
  parameters: {
17219
17262
  projectPath: PROJECT_PATH,
17220
17263
  scenePath: SCENE_PATH,
@@ -18225,7 +18268,11 @@ var init_tool_definitions = __esm(() => {
18225
18268
  },
18226
18269
  requires: [],
18227
18270
  operations: {
18228
- tree: { effect: "read", summary: "the live scene tree", requires: [] },
18271
+ tree: {
18272
+ effect: "read",
18273
+ summary: "the live scene tree, with current_scene giving the path of the scene the game is showing and the file it was loaded from, or null when it shows none",
18274
+ requires: []
18275
+ },
18229
18276
  text: {
18230
18277
  effect: "read",
18231
18278
  summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it, and a control drawn wholly outside the viewport or scrolled out of a ScrollContainer, which the player cannot read. Every line reads as drawn: rich text without tags, text only as far as it is typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each. A line drawn as a copy under a node saying the same words, as an outline or a glow is, reads once, counted under copies",
@@ -18635,7 +18682,7 @@ var init_tool_definitions = __esm(() => {
18635
18682
  includeHidden: {
18636
18683
  type: "boolean",
18637
18684
  ops: ["until"],
18638
- description: "until, with says: count words on a hidden node too, and on one drawn outside the viewport or scrolled out of a ScrollContainer. Default false, since a wait for words is a wait for them to be shown, and a wait that runs out while such a node holds them says so under unseen_note: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
18685
+ description: "until, with says: count words on a hidden node too, and on one drawn outside the viewport or scrolled out of a ScrollContainer. Default false, since a wait for words is a wait for them to be shown, and a wait that runs out while such a node holds them says so under unseen_note: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames. What is drawn over a node is not looked at, so words under a dialog or a panel count either way."
18639
18686
  },
18640
18687
  timeoutMs: {
18641
18688
  type: "integer",
@@ -18711,7 +18758,7 @@ var init_tool_definitions = __esm(() => {
18711
18758
  },
18712
18759
  step_out: {
18713
18760
  effect: "additive",
18714
- summary: "run to the end of the current function and stop at the next line run at its caller's depth: in its caller, or, when the call was the last thing its caller did, wherever script runs next, such as the caller's next call; a game this server started only",
18761
+ summary: "run to the end of the current function and stop at the next line run at its caller's depth: in its caller, or, when the call was the last thing its caller did, wherever script runs next, such as the caller's next call; only for a game this server started",
18715
18762
  requires: []
18716
18763
  }
18717
18764
  }
@@ -18729,7 +18776,7 @@ var init_tool_definitions = __esm(() => {
18729
18776
  expression: {
18730
18777
  type: "string",
18731
18778
  ops: ["evaluate"],
18732
- description: "evaluate: a GDScript expression, as Godot's Expression class parses it: members of self by name, locals, autoloads and global classes, operators, indexing and calls, such as wave.spawned[0] or Ledger.entries.size(). An expression that does not parse, or fails as it runs, comes to null, and the game prints why as an error, which editor_output reads for a game this server started. The game reports what an expression raises while it is held inside a runtime_invoke call as raised by that call, and the call's answer says so under errorsNote."
18779
+ description: "evaluate: a GDScript expression, as Godot's Expression class parses it: members of self by name, locals, autoloads and global classes, operators, indexing and calls, such as wave.spawned[0] or Ledger.entries.size(). An expression that does not parse, or fails as it runs, comes to null with failed saying why, as the engine words it, and a null with no failed is the value; for a game the editor plays, the reason reaches only the editor's Debugger panel, so a null there carries a note saying it may be either. The game reports what an expression raises while it is held inside a runtime_invoke call as raised by that call, and the call's answer says so under errorsNote."
18733
18780
  }
18734
18781
  },
18735
18782
  requires: [],
@@ -42562,6 +42609,7 @@ async function handleDAPTool(client, toolName, args) {
42562
42609
  const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
42563
42610
  const expression = typeof safeArgs.expression === "string" ? safeArgs.expression : "";
42564
42611
  const evaluated = await client.evaluate(expression, frameId);
42612
+ const nil = evaluated.type === "Nil" || evaluated.value === "null" || evaluated.value === "<null>";
42565
42613
  return {
42566
42614
  content: [
42567
42615
  {
@@ -42570,7 +42618,10 @@ async function handleDAPTool(client, toolName, args) {
42570
42618
  frame: evaluated.frame,
42571
42619
  expression,
42572
42620
  value: evaluated.value,
42573
- ...evaluated.type === undefined ? {} : { type: evaluated.type }
42621
+ ...evaluated.type === undefined ? {} : { type: evaluated.type },
42622
+ ...nil ? {
42623
+ note: "null is also what an expression comes to when it does not parse or fails as it runs. For a game the editor plays, why reaches only the editor's Debugger panel, so this cannot say which it was."
42624
+ } : {}
42574
42625
  })
42575
42626
  }
42576
42627
  ]
@@ -49518,6 +49569,13 @@ function restartsOf(run) {
49518
49569
  endedAt: new Date(one.endedAt).toISOString()
49519
49570
  }));
49520
49571
  }
49572
+ async function handToTheGame(slot, pid, projectPath) {
49573
+ const started = (await startTimesOf([pid])).get(pid);
49574
+ const by = `a game${projectPath === null ? "" : ` of ${projectPath}`} the editor played for editor_run`;
49575
+ if (started === undefined || !slot.handTo({ pid, started, by })) {
49576
+ slot.release();
49577
+ }
49578
+ }
49521
49579
  function followTheRestart(run) {
49522
49580
  if (run.throughEditor || run.pid === null || run.projectPath === null) {
49523
49581
  return false;
@@ -50044,6 +50102,13 @@ function presetsIn(projectPath) {
50044
50102
  function presetPlatform(projectPath, preset) {
50045
50103
  return presetsIn(projectPath)?.find((held) => held.name === preset)?.platform ?? null;
50046
50104
  }
50105
+ function engineEndSaid(ending) {
50106
+ if (ending.failure === null && ending.exitSignal === null) {
50107
+ return `the engine ended with ${exitCodeText(ending.exitCode)}`;
50108
+ }
50109
+ const how = howItEnded(ending);
50110
+ return how.startsWith("ended by") ? `the engine was ${how}` : how;
50111
+ }
50047
50112
  function exportPresetRefusal(projectPath, preset) {
50048
50113
  const presets = presetsIn(projectPath);
50049
50114
  if (presets === null) {
@@ -50078,7 +50143,24 @@ function exportAnswer(ending, asked, before, after, beside = [], docCacheNote =
50078
50143
  if (verdict.exported) {
50079
50144
  return { content: [{ type: "text", text: answerJson(verdict) }] };
50080
50145
  }
50081
- const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
50146
+ if (written) {
50147
+ const errors = log.count("error");
50148
+ const because = [
50149
+ ...ending.exitCode === 0 && ending.failure === null && ending.exitSignal === null ? [] : [engineEndSaid(ending)],
50150
+ ...errors === 0 ? [] : [`it printed ${String(errors)} ${errors === 1 ? "error" : "errors"}, under entries`]
50151
+ ];
50152
+ return {
50153
+ content: [
50154
+ {
50155
+ type: "text",
50156
+ text: `Export with preset '${asked.preset}' wrote ${asked.outputPath}, but it is not counted as exported, as ${because.join(" and ")}.`
50157
+ },
50158
+ { type: "text", text: answerJson(verdict) }
50159
+ ],
50160
+ isError: true
50161
+ };
50162
+ }
50163
+ const stale = after === null ? "" : ` The ${asked.outputPath} there is from before this export, which did not write it.`;
50082
50164
  const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
50083
50165
  return {
50084
50166
  content: [
@@ -50833,6 +50915,17 @@ function ownArguments(params) {
50833
50915
  function needsItsOwnProcess(asked) {
50834
50916
  return asked.savesIn !== undefined || asked.env !== undefined;
50835
50917
  }
50918
+ function unheldNote(changed) {
50919
+ if (!isPlainObject3(changed)) {
50920
+ return "";
50921
+ }
50922
+ const unheld = Object.entries(changed).filter(([, change]) => isPlainObject3(change) && change["was"] === null).map(([name]) => name);
50923
+ if (unheld.length === 0) {
50924
+ return "";
50925
+ }
50926
+ const one = unheld.length === 1;
50927
+ return ` ${unheld.join(", ")} ${one ? "was" : "were"} not in the import file before, so only that reimport can tell whether the importer has ${one ? "it" : "them"}: one it lacks is dropped from the file then, and the reimport through set_options says which.`;
50928
+ }
50836
50929
 
50837
50930
  class GodotServer {
50838
50931
  stdio = null;
@@ -52464,7 +52557,7 @@ class GodotServer {
52464
52557
  ...written,
52465
52558
  payload: {
52466
52559
  ...written.payload,
52467
- note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
52560
+ note: `Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now.${unheldNote(written.payload["changed"])}`
52468
52561
  }
52469
52562
  } : written);
52470
52563
  }
@@ -55131,17 +55224,20 @@ ${error.text}`;
55131
55224
  const sendingFrom = Date.now();
55132
55225
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
55133
55226
  this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
55227
+ const slot = await slotForCall();
55134
55228
  if (profiled) {
55135
55229
  const armed = await this.handleViaBridge("profile_start", {
55136
55230
  arm: true,
55137
55231
  frameFunctions: profileFrameFunctions(projectPath)
55138
55232
  });
55139
55233
  if (armed.isError === true) {
55234
+ slot.release();
55140
55235
  return armed;
55141
55236
  }
55142
55237
  }
55143
55238
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
55144
55239
  if (answer.isError === true) {
55240
+ slot.release();
55145
55241
  return answer;
55146
55242
  }
55147
55243
  const startedAt = Date.now();
@@ -55171,7 +55267,8 @@ ${error.text}`;
55171
55267
  announcedBefore: alreadyPlaying,
55172
55268
  seenPlaying: readBoolean(playAnswer, "playing") === true,
55173
55269
  ...which === null ? {} : { play: which },
55174
- bootScene: runtimeWaitMs.scene
55270
+ bootScene: runtimeWaitMs.scene,
55271
+ slot
55175
55272
  };
55176
55273
  this.activeProcess = played;
55177
55274
  this.noteTheLatestRun({ projectPath, pid: null, startedAt });
@@ -55187,6 +55284,11 @@ ${error.text}`;
55187
55284
  sweepTranscripts();
55188
55285
  step("sweep");
55189
55286
  this.logDebug(`After the play: ${steps.join(", ")}`);
55287
+ const runtime = await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false);
55288
+ if (played.slot !== undefined) {
55289
+ played.slot.release();
55290
+ delete played.slot;
55291
+ }
55190
55292
  return this.jsonTextResponse({
55191
55293
  started: true,
55192
55294
  through: "editor",
@@ -55203,7 +55305,7 @@ ${error.text}`;
55203
55305
  scriptPath: projectSpelling(projectPath, one.scriptPath),
55204
55306
  reason: one.reason
55205
55307
  })),
55206
- runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
55308
+ runtime,
55207
55309
  message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter and the runtime's report of it, and editor_run stop ends it." + endedToStartThis(ended)
55208
55310
  });
55209
55311
  }
@@ -55584,20 +55686,33 @@ ${error.text}`;
55584
55686
  closeSync5(transcript.fd);
55585
55687
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
55586
55688
  const profile = profiled ? profilePath(startedAt) : undefined;
55587
- const launched = await launchOutsideTheTree({
55588
- command: godotPath,
55589
- args: cmdArgs,
55590
- ...gameEnv === undefined ? {} : { env: gameEnv },
55591
- run: {
55592
- transcript: transcript.path,
55593
- startedAt,
55594
- projectPath,
55595
- ...this.servedBy(),
55596
- ...profile === undefined ? {} : { profile },
55597
- ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false }
55598
- },
55599
- ...desktop === undefined ? {} : { desktop }
55600
- });
55689
+ const slot = await slotForCall();
55690
+ let launched;
55691
+ try {
55692
+ launched = await launchOutsideTheTree({
55693
+ command: godotPath,
55694
+ args: cmdArgs,
55695
+ ...gameEnv === undefined ? {} : { env: gameEnv },
55696
+ run: {
55697
+ transcript: transcript.path,
55698
+ startedAt,
55699
+ projectPath,
55700
+ ...this.servedBy(),
55701
+ ...profile === undefined ? {} : { profile },
55702
+ ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false },
55703
+ slot: { lock: slot.lock, from: slot.from, by: `a game of ${projectPath} editor_run started` }
55704
+ },
55705
+ ...desktop === undefined ? {} : { desktop }
55706
+ });
55707
+ } catch (error) {
55708
+ slot.release();
55709
+ throw error;
55710
+ }
55711
+ if ("error" in launched) {
55712
+ slot.release();
55713
+ } else {
55714
+ slot.leave();
55715
+ }
55601
55716
  if ("error" in launched) {
55602
55717
  return launched;
55603
55718
  }
@@ -55819,6 +55934,11 @@ ${error.text}`;
55819
55934
  tie(run, endpoint) {
55820
55935
  run.announcedPid = endpoint.pid;
55821
55936
  this.noteBootOf(run, endpoint);
55937
+ const slot = run.slot;
55938
+ if (slot !== undefined) {
55939
+ delete run.slot;
55940
+ handToTheGame(slot, endpoint.pid, run.projectPath);
55941
+ }
55822
55942
  }
55823
55943
  playedByOurEditor(endpoint) {
55824
55944
  const editor = this.godotBridge.getStatus().editorPid;
@@ -16,6 +16,22 @@ const UPDATE_MODE_NAMES: Dictionary[int, String] = {
16
16
  Animation.UPDATE_DISCRETE: "discrete",
17
17
  Animation.UPDATE_CAPTURE: "capture",
18
18
  }
19
+ const INTERPOLATION_NAMES: Dictionary[int, String] = {
20
+ Animation.INTERPOLATION_NEAREST: "nearest",
21
+ Animation.INTERPOLATION_LINEAR: "linear",
22
+ Animation.INTERPOLATION_CUBIC: "cubic",
23
+ Animation.INTERPOLATION_LINEAR_ANGLE: "linear_angle",
24
+ Animation.INTERPOLATION_CUBIC_ANGLE: "cubic_angle",
25
+ }
26
+ ## The track types whose keys are blended between, so how they are and whether a looping animation
27
+ ## blends its last key into its first say how the track moves.
28
+ const INTERPOLATED_TYPES: Array[int] = [
29
+ Animation.TYPE_VALUE,
30
+ Animation.TYPE_POSITION_3D,
31
+ Animation.TYPE_ROTATION_3D,
32
+ Animation.TYPE_SCALE_3D,
33
+ Animation.TYPE_BLEND_SHAPE,
34
+ ]
19
35
  const TRACK_TYPE_NAMES: Dictionary[int, String] = {
20
36
  Animation.TYPE_VALUE: "property",
21
37
  Animation.TYPE_POSITION_3D: "position_3d",
@@ -174,6 +190,10 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
174
190
  if kind == Animation.TYPE_VALUE:
175
191
  var mode: int = anim.value_track_get_update_mode(track)
176
192
  shaped["update"] = UPDATE_MODE_NAMES.get(mode, str(mode))
193
+ if kind in INTERPOLATED_TYPES:
194
+ var interpolation: int = anim.track_get_interpolation_type(track)
195
+ shaped["interpolation"] = INTERPOLATION_NAMES.get(interpolation, str(interpolation))
196
+ shaped["loopWrap"] = anim.track_get_interpolation_loop_wrap(track)
177
197
  var keyframes: Array[Dictionary] = []
178
198
  var methods: Dictionary = {}
179
199
  for key: int in anim.track_get_key_count(track):
@@ -356,6 +356,12 @@ static func _entry(
356
356
  var data: Dictionary = {
357
357
  "name": node["name"], "type": node["type"], "path": path, "children": children,
358
358
  }
359
+ var given_values: Dictionary = node["properties"]
360
+ var attached: Variant = given_values.get("script")
361
+ if attached is Script:
362
+ var script: Script = attached
363
+ if not str(script.get_global_name()).is_empty():
364
+ data["scriptClass"] = str(script.get_global_name())
359
365
  # The root's file is the scene being read, so only another node's names a second scene.
360
366
  if path != "." and node.has("instanceOf"):
361
367
  data["instanceOf"] = node["instanceOf"]
@@ -109,6 +109,9 @@ func add_animation_track(args: Dictionary) -> Dictionary:
109
109
  if anim.track_get_type(track_idx) == Animation.TYPE_VALUE:
110
110
  var mode: int = anim.value_track_get_update_mode(track_idx)
111
111
  answer["update"] = GdhAnimationReads.UPDATE_MODE_NAMES.get(mode, str(mode))
112
+ var blended: int = anim.track_get_interpolation_type(track_idx)
113
+ answer["interpolation"] = GdhAnimationReads.INTERPOLATION_NAMES.get(blended, str(blended))
114
+ answer["loopWrap"] = anim.track_get_interpolation_loop_wrap(track_idx)
112
115
  if built[3]:
113
116
  answer["existingTrack"] = true
114
117
  var replaced: Array = built[5]
@@ -165,6 +168,7 @@ func _add_track(
165
168
  var kind: Animation.TrackType = Animation.TYPE_VALUE
166
169
  var path: NodePath = NodePath(target_path)
167
170
  var update: int = -1
171
+ var interpolation: int = -1
168
172
  if track_type == "property":
169
173
  var prop_name: String = str(track.get("property", ""))
170
174
  if prop_name.is_empty():
@@ -175,6 +179,11 @@ func _add_track(
175
179
  )
176
180
  if named != null:
177
181
  update = GdhRead.as_int(named)
182
+ var blended: Variant = GdhAnimationReads.INTERPOLATION_NAMES.find_key(
183
+ str(track.get("interpolation", ""))
184
+ )
185
+ if blended != null:
186
+ interpolation = GdhRead.as_int(blended)
178
187
  var values: Array = _key_values(target, prop_name, keyframes)
179
188
  if not values[0]:
180
189
  return _not_added(str(values[1]))
@@ -196,6 +205,12 @@ func _add_track(
196
205
  misfit = _key_misfit(target, method_name, arguments, index)
197
206
  method_found = target.has_method(method_name)
198
207
  kind = Animation.TYPE_METHOD
208
+ # A method key is called at its time and never blended, so neither has anything to set.
209
+ for blending: String in ["interpolation", "loopWrap"]:
210
+ if track.has(blending):
211
+ return _not_added(
212
+ "track.%s is for property tracks; a method key is not blended." % blending
213
+ )
199
214
  else:
200
215
  return _not_added(
201
216
  (
@@ -250,6 +265,10 @@ func _add_track(
250
265
  _as_the_editor_makes(anim, track_idx, player, target)
251
266
  if update >= 0:
252
267
  anim.value_track_set_update_mode(track_idx, update as Animation.UpdateMode)
268
+ if interpolation >= 0:
269
+ anim.track_set_interpolation_type(track_idx, interpolation as Animation.InterpolationType)
270
+ if track.has("loopWrap"):
271
+ anim.track_set_interpolation_loop_wrap(track_idx, GdhRead.as_bool(track["loopWrap"]))
253
272
  for index: int in keyframes.size():
254
273
  var keyframe: Dictionary = keyframes[index]
255
274
  if anim.track_insert_key(track_idx, GdhRead.as_float(keyframe["time"]), keys[index]) < 0:
@@ -160,7 +160,7 @@ func _hover(viewport: Viewport, point: Vector2) -> void:
160
160
  var inside: Vector2 = container.get_global_transform_with_canvas().affine_inverse() * point
161
161
  if container.stretch:
162
162
  inside /= container.stretch_shrink
163
- for child: Node in container.get_children():
163
+ for child: Node in container.get_children(true):
164
164
  var shown: SubViewport = child as SubViewport
165
165
  if shown != null and not shown.gui_disable_input:
166
166
  _hover(shown, shown.get_final_transform().affine_inverse() * inside)
@@ -179,7 +179,7 @@ func _drop(viewport: Viewport, until: int) -> void:
179
179
  var over: Control = instance_from_id(top) as Control
180
180
  var container: SubViewportContainer = over as SubViewportContainer
181
181
  if container != null:
182
- for child: Node in container.get_children():
182
+ for child: Node in container.get_children(true):
183
183
  var shown: SubViewport = child as SubViewport
184
184
  if shown != null:
185
185
  _drop(shown, 0)
@@ -264,7 +264,7 @@ func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
264
264
  var inside: Vector2 = container.get_global_transform_with_canvas().affine_inverse() * point
265
265
  if container.stretch:
266
266
  inside /= container.stretch_shrink
267
- for child: Node in container.get_children():
267
+ for child: Node in container.get_children(true):
268
268
  var shown: SubViewport = child as SubViewport
269
269
  if shown != null and not shown.gui_disable_input:
270
270
  var within: Node = _pressed_at(
@@ -277,9 +277,11 @@ func _pressed_at(viewport: Viewport, point: Vector2) -> Node:
277
277
 
278
278
  ## The controls [param node] holds that the engine keeps as roots of [param node]'s viewport: drawn
279
279
  ## on their own, or with no control above them. Not into another viewport, a window or a
280
- ## SubViewport, whose controls are that one's.
280
+ ## SubViewport, whose controls are that one's. Internal children count, as they do for the engine,
281
+ ## though get_children leaves them out unless asked: a layer added with INTERNAL_MODE_BACK was
282
+ ## passed over, and the press predicted for the control under it.
281
283
  static func _gather_roots(node: Node, roots: Array[Control]) -> void:
282
- for child: Node in node.get_children():
284
+ for child: Node in node.get_children(true):
283
285
  if child is Viewport:
284
286
  continue
285
287
  var control: Control = child as Control
@@ -331,8 +333,8 @@ func _at(item: CanvasItem, point: Vector2, holder: Transform2D) -> Control:
331
333
  var control: Control = item as Control
332
334
  var local: Vector2 = matrix.affine_inverse() * point
333
335
  if control == null or not control.is_clipping_contents() or _has_point(control, local):
334
- for index: int in range(item.get_child_count() - 1, -1, -1):
335
- var child: CanvasItem = item.get_child(index) as CanvasItem
336
+ for index: int in range(item.get_child_count(true) - 1, -1, -1):
337
+ var child: CanvasItem = item.get_child(index, true) as CanvasItem
336
338
  if child == null or child.top_level:
337
339
  continue
338
340
  var found: Control = _at(child, point, matrix)
@@ -61,9 +61,16 @@ func get_tree(params: Dictionary) -> Dictionary:
61
61
  return reached
62
62
  var root: Node = reached["node"]
63
63
 
64
+ # A game whose first scene hands over to another shows a tree that names neither, so which
65
+ # scene is showing was guessed from the project's documentation.
66
+ var scene: Node = _host.get_tree().current_scene
67
+ var showing: Variant = null
68
+ if scene != null:
69
+ showing = { "path": str(scene.get_path()), "file": scene.scene_file_path }
64
70
  return {
65
71
  "type": "tree",
66
72
  "root": _serialize_node_tree(root, 0, max_depth, include_properties, named),
73
+ "current_scene": showing,
67
74
  }
68
75
 
69
76
 
@@ -163,7 +163,7 @@ static func pressable_words(from: Node, open: Callable, most: int) -> String:
163
163
  if offered.size() == most:
164
164
  return ", ".join(offered) + ", and more"
165
165
  offered.append(quoted)
166
- var children: Array[Node] = node.get_children()
166
+ var children: Array[Node] = node.get_children(true)
167
167
  for index: int in range(children.size() - 1, -1, -1):
168
168
  pending.push_front(children[index])
169
169
  return ", ".join(offered)