gdharness 1.3.46 → 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
@@ -11027,9 +11027,10 @@ function rowOf(name) {
11027
11027
  function rowRuns(share, row) {
11028
11028
  return "only" in share ? row !== null && share.only.includes(row) : row === null || !share.besides.includes(row);
11029
11029
  }
11030
- function mismatchedRowsIn(printed) {
11030
+ function skippedAsDeclaredIn(printed) {
11031
11031
  const lines = printed.flatMap((text) => stripVTControlCharacters(text).split(/\r?\n/));
11032
11032
  const found = [];
11033
+ const unrunnable = [];
11033
11034
  let suite = null;
11034
11035
  let lastCase = null;
11035
11036
  for (let at = 0;at < lines.length; at += 1) {
@@ -11045,7 +11046,15 @@ function mismatchedRowsIn(printed) {
11045
11046
  lastCase = caseLine[3] === "STARTED" ? lastCase : caseLine[2] ?? null;
11046
11047
  continue;
11047
11048
  }
11048
- if (suite === null || lastCase === null || !MISMATCHED_ROW.test(line)) {
11049
+ if (suite === null || lastCase === null) {
11050
+ continue;
11051
+ }
11052
+ const declared = UNRUNNABLE_CASE.exec(line)?.[1];
11053
+ if (declared !== undefined) {
11054
+ unrunnable.push({ suite, name: lastCase, reason: declared });
11055
+ continue;
11056
+ }
11057
+ if (!MISMATCHED_ROW.test(line)) {
11049
11058
  continue;
11050
11059
  }
11051
11060
  const parts = [line.trim()];
@@ -11055,7 +11064,7 @@ function mismatchedRowsIn(printed) {
11055
11064
  }
11056
11065
  found.push({ suite, name: lastCase, reason: parts.join(" ").replaceAll(/\[\/?color[^\]]*\]/g, "") });
11057
11066
  }
11058
- return found;
11067
+ return { mismatched: found, unrunnable };
11059
11068
  }
11060
11069
  function readRun(printed) {
11061
11070
  const lines = stripVTControlCharacters(printed).replaceAll(`\r
@@ -11189,7 +11198,7 @@ function readRun(printed) {
11189
11198
  leftOut,
11190
11199
  engineVersion,
11191
11200
  orphansLeft,
11192
- mismatched: mismatchedRowsIn(lines)
11201
+ ...skippedAsDeclaredIn(lines)
11193
11202
  };
11194
11203
  }
11195
11204
  function withNestedErrors(printed, piped) {
@@ -11314,7 +11323,7 @@ function judgeEngine(printed, handed, end) {
11314
11323
  if (printed.executed === null || printed.executed === 0) {
11315
11324
  faults.push(printed.executed === null ? "the run never printed its summary" : "the run executed no test case at all, whatever it exited with");
11316
11325
  }
11317
- for (const row of printed.mismatched) {
11326
+ for (const row of [...printed.mismatched, ...printed.unrunnable]) {
11318
11327
  faults.push(`${row.suite} > ${row.name} never ran: gdUnit4 skipped it, saying "${row.reason}"`);
11319
11328
  failing.add(row.suite);
11320
11329
  }
@@ -11481,7 +11490,7 @@ ${String(error.warning)}`);
11481
11490
  const neverPrinted = [...owed.values()].flatMap((debt) => debt.texts.map((text) => ({ suite: debt.suite, during: debt.during, text, warning: debt.warning })));
11482
11491
  return { printed: unexpected, neverPrinted };
11483
11492
  }
11484
- var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, MISMATCHED_ROW, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11493
+ var SUITE_OPENED, TIME, CASE_LINE, STATISTICS, ERROR_LINE, WARNING_LINE, EXECUTED, MISMATCHED_ROW, UNRUNNABLE_CASE, BANNER, LEFT_OUT, DRIVER_FALLBACK, MACHINE_WARNED, ORPHANS_REPORTED, STARVED, NESTED_ERROR = "While attempting to print an error, another error was printed:", WINDOWS_EXITS, MEMORY_EXITS, GDUNIT_FAILED = 100, GDUNIT_ORPHANS = 101, GDUNIT_EXITS, GODOT_EXITS, TAIL = 60, MOST_NAMED = 8;
11485
11494
  var init_suite_judge = __esm(() => {
11486
11495
  init_engine_leaks();
11487
11496
  init_junit();
@@ -11493,6 +11502,7 @@ var init_suite_judge = __esm(() => {
11493
11502
  WARNING_LINE = /^\s*(?:SCRIPT |USER )?WARNING: (.*?)\s*$/;
11494
11503
  EXECUTED = /^Executed test cases\s*:\s*\((\d+)\//;
11495
11504
  MISMATCHED_ROW = /^\s*The test data set at index \(\d+\) does not match the expected test parameters/;
11505
+ UNRUNNABLE_CASE = /^\s*Reason: '((?:Unknown test case argument's \[.*\] found|Fuzzer argument '.+' must be declared before the test config argument\(s\) \[.*\])\.)'\s*$/;
11496
11506
  BANNER = /^\s*Godot Engine v(\S+?)(?:\s+-\s+\S+)?\s*$/;
11497
11507
  LEFT_OUT = /^gdharness left out (res:\/\/.+?\.gd) > ([^:\s]+):(-?\d+)\s*$/;
11498
11508
  DRIVER_FALLBACK = /^\s*WARNING: Your video card drivers .*, switching to /;
@@ -12101,7 +12111,8 @@ class EngineSlots {
12101
12111
  changes = new FolderChanges(this.directory);
12102
12112
  continue;
12103
12113
  }
12104
- 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);
12105
12116
  }
12106
12117
  } finally {
12107
12118
  changes?.close();
@@ -12159,11 +12170,17 @@ class EngineSlots {
12159
12170
  }
12160
12171
  release(lock) {
12161
12172
  this.held.delete(lock);
12162
- try {
12163
- if (holderOf(readFileSync8(lock, "utf8"))?.pid === this.holder.pid) {
12164
- removeWhole(lock);
12165
- }
12166
- } 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;
12167
12184
  }
12168
12185
  made(path) {
12169
12186
  let made;
@@ -12230,6 +12247,22 @@ class EngineSlots {
12230
12247
  this.starts.set(pid, { started, readAt: Date.now() });
12231
12248
  }
12232
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
+ }
12233
12266
  function sayWaitsTo(say) {
12234
12267
  saidTo = say;
12235
12268
  }
@@ -12298,8 +12331,14 @@ async function slotForCall() {
12298
12331
  waits.said ??= (waitedMs) => slotWaitedSaid(waitedMs, slots.count, slots.directory, heldBy);
12299
12332
  }
12300
12333
  return {
12334
+ lock,
12335
+ from: slots.holder.pid,
12301
12336
  release: () => {
12302
12337
  slots.release(lock);
12338
+ },
12339
+ handTo: (holder) => slots.hand(lock, holder),
12340
+ leave: () => {
12341
+ slots.forget(lock);
12303
12342
  }
12304
12343
  };
12305
12344
  }
@@ -12838,7 +12877,7 @@ class GameLog {
12838
12877
  return;
12839
12878
  }
12840
12879
  const owner = this.lastHeadline[source];
12841
- if (owner !== null && /^\s/.test(line)) {
12880
+ if (owner !== null && /^\s/.test(line) && !GDUNIT_CASE.test(line)) {
12842
12881
  owner.detail.push(line.trim());
12843
12882
  owner.continues = false;
12844
12883
  return;
@@ -12915,10 +12954,11 @@ function answersTo(entry, severity, contains) {
12915
12954
  const needle = contains.toLowerCase();
12916
12955
  return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
12917
12956
  }
12918
- 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;
12919
12958
  var init_game_log = __esm(() => {
12920
12959
  HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
12921
12960
  CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d+/;
12961
+ GDUNIT_CASE = /^\s+res:\/\/.+?\.(?:gd|cs) > .+? (?:STARTED|PASSED|FAILED|ERROR|SKIPPED|FLAKY)(?:\s|$)/;
12922
12962
  CRASH_FILLER = /^(?:=+|-- END OF .* BACKTRACE --|\[\d+\] .* - no debug info in .*)$/;
12923
12963
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
12924
12964
  STREAMS = ["stdout", "stderr", "transcript"];
@@ -14215,7 +14255,7 @@ async function operationOutcome(engine, operation, params, projectPath) {
14215
14255
  let ran;
14216
14256
  let answer;
14217
14257
  try {
14218
- ran = await runEngine(engine.godotPath, args);
14258
+ ran = await runEngine(engine.godotPath, args, { timeout: engine.ceiling ?? OPERATION_CEILING });
14219
14259
  answer = answerIn(answerFile);
14220
14260
  } finally {
14221
14261
  discard(paramsDir);
@@ -14305,7 +14345,7 @@ function answerIn(file) {
14305
14345
  };
14306
14346
  }
14307
14347
  }
14308
- 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.";
14309
14349
  var init_headless = __esm(() => {
14310
14350
  init_doc_cache();
14311
14351
  init_engine_leaks();
@@ -14316,6 +14356,7 @@ var init_headless = __esm(() => {
14316
14356
  init_reimport();
14317
14357
  init_scratch();
14318
14358
  init_script_origins();
14359
+ OPERATION_CEILING = { ms: 600000, said: "ten minutes" };
14319
14360
  });
14320
14361
 
14321
14362
  // src/update-check.ts
@@ -16583,13 +16624,25 @@ var init_tool_definitions = __esm(() => {
16583
16624
  ],
16584
16625
  description: "add_track makes property and method tracks; remove_track takes any type read answers."
16585
16626
  },
16586
- 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
+ },
16587
16631
  property: { type: "string", description: "For property tracks: the property to animate." },
16588
16632
  update: {
16589
16633
  type: "string",
16590
16634
  enum: ["continuous", "discrete", "capture"],
16591
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."
16592
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
+ },
16593
16646
  method: {
16594
16647
  type: "string",
16595
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."
@@ -16947,7 +17000,7 @@ var init_tool_definitions = __esm(() => {
16947
17000
  direction: {
16948
17001
  type: "string",
16949
17002
  enum: ["forward", "reverse"],
16950
- 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."
16951
17004
  },
16952
17005
  depth: {
16953
17006
  type: "integer",
@@ -16982,7 +17035,7 @@ var init_tool_definitions = __esm(() => {
16982
17035
  options: {
16983
17036
  type: "object",
16984
17037
  ops: ["set_options"],
16985
- 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}.`,
16986
17039
  additionalProperties: true
16987
17040
  },
16988
17041
  reimport: {
@@ -17022,7 +17075,7 @@ var init_tool_definitions = __esm(() => {
17022
17075
  },
17023
17076
  uid: {
17024
17077
  effect: "read",
17025
- 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",
17026
17079
  requires: ["resourcePath"]
17027
17080
  },
17028
17081
  refresh_uids: {
@@ -17069,7 +17122,7 @@ var init_tool_definitions = __esm(() => {
17069
17122
  type: "string",
17070
17123
  ops: ["set_preset"],
17071
17124
  enum: ["all_resources", "scenes", "resources", "exclude"],
17072
- 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."
17073
17126
  },
17074
17127
  files: {
17075
17128
  type: "array",
@@ -17130,7 +17183,7 @@ var init_tool_definitions = __esm(() => {
17130
17183
  path: {
17131
17184
  type: ["string", "array"],
17132
17185
  items: { type: "string" },
17133
- description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. A case declared there that gdUnit4 never ran, in a suite that passed, comes back under testsNotRun, and a row it skipped for data that does not fit the case under rowsMismatched with the reason gdUnit4 gave; neither run is called passed. Default tests and test, those the project has, which is where gdharness test looks.'
17186
+ description: 'A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry "suite.gd:test_name" runs that case alone, as gdharness test reads it and as tests runs it, so it cannot share the list with a whole suite or a directory. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. A script named that declares no test_ function at the top level is refused the same way, and so is a directory holding no suite. A script that declares test_ functions and does not extend GdUnitTestSuite, which gdUnit4 never runs and never mentions, is refused when named, and under a directory run whole it is named under notSuites and the run is not called passed. A case declared there that gdUnit4 never ran, in a suite that passed, comes back under testsNotRun, a row it skipped for data that does not fit the case under rowsMismatched, and a case it skipped because it cannot run it as declared (a parameter nothing fills, or a fuzzer after a setting) under casesUnrunnable, each with the reason gdUnit4 gave; none of those runs is called passed. Default tests and test, those the project has, which is where gdharness test looks.'
17134
17187
  },
17135
17188
  ignore: {
17136
17189
  type: "array",
@@ -17204,7 +17257,7 @@ var init_tool_definitions = __esm(() => {
17204
17257
  name: "scene_tree",
17205
17258
  title: "Scene tree",
17206
17259
  effect: "read",
17207
- 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.",
17208
17261
  parameters: {
17209
17262
  projectPath: PROJECT_PATH,
17210
17263
  scenePath: SCENE_PATH,
@@ -18215,7 +18268,11 @@ var init_tool_definitions = __esm(() => {
18215
18268
  },
18216
18269
  requires: [],
18217
18270
  operations: {
18218
- 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
+ },
18219
18276
  text: {
18220
18277
  effect: "read",
18221
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",
@@ -18620,12 +18677,12 @@ var init_tool_definitions = __esm(() => {
18620
18677
  says: {
18621
18678
  type: "string",
18622
18679
  ops: ["until"],
18623
- description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them.'
18680
+ description: 'until: wait for these words to appear anywhere under nodePath, or anywhere in the game when no nodePath is given, instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Matched the way runtime_inspect find matches says: case-insensitive, a plain word anywhere in the text, a pattern with * or ? a glob over the whole of it, and alternatives separated by | met by whichever appears first ("*won*|*lost*"); on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property. It looks every frame while a look is cheap and less often on a screen large enough for a look to cost a real share of a frame, so the game keeps its speed; the answer counts the frames the wait spanned and the looks it took, and says under looking_ms how long the looks took between them. A look on a large screen is read about 2ms to a frame, so no frame grows by more, except the last look as the time runs out, which is read whole; longest_hold_ms is the longest the looks held one frame, which tells a long frame the game logs apart from one the wait caused.'
18624
18681
  },
18625
18682
  includeHidden: {
18626
18683
  type: "boolean",
18627
18684
  ops: ["until"],
18628
- 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."
18629
18686
  },
18630
18687
  timeoutMs: {
18631
18688
  type: "integer",
@@ -18701,7 +18758,7 @@ var init_tool_definitions = __esm(() => {
18701
18758
  },
18702
18759
  step_out: {
18703
18760
  effect: "additive",
18704
- 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",
18705
18762
  requires: []
18706
18763
  }
18707
18764
  }
@@ -18719,7 +18776,7 @@ var init_tool_definitions = __esm(() => {
18719
18776
  expression: {
18720
18777
  type: "string",
18721
18778
  ops: ["evaluate"],
18722
- 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."
18723
18780
  }
18724
18781
  },
18725
18782
  requires: [],
@@ -42552,6 +42609,7 @@ async function handleDAPTool(client, toolName, args) {
42552
42609
  const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
42553
42610
  const expression = typeof safeArgs.expression === "string" ? safeArgs.expression : "";
42554
42611
  const evaluated = await client.evaluate(expression, frameId);
42612
+ const nil = evaluated.type === "Nil" || evaluated.value === "null" || evaluated.value === "<null>";
42555
42613
  return {
42556
42614
  content: [
42557
42615
  {
@@ -42560,7 +42618,10 @@ async function handleDAPTool(client, toolName, args) {
42560
42618
  frame: evaluated.frame,
42561
42619
  expression,
42562
42620
  value: evaluated.value,
42563
- ...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
+ } : {}
42564
42625
  })
42565
42626
  }
42566
42627
  ]
@@ -49508,6 +49569,13 @@ function restartsOf(run) {
49508
49569
  endedAt: new Date(one.endedAt).toISOString()
49509
49570
  }));
49510
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
+ }
49511
49579
  function followTheRestart(run) {
49512
49580
  if (run.throughEditor || run.pid === null || run.projectPath === null) {
49513
49581
  return false;
@@ -50034,6 +50102,13 @@ function presetsIn(projectPath) {
50034
50102
  function presetPlatform(projectPath, preset) {
50035
50103
  return presetsIn(projectPath)?.find((held) => held.name === preset)?.platform ?? null;
50036
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
+ }
50037
50112
  function exportPresetRefusal(projectPath, preset) {
50038
50113
  const presets = presetsIn(projectPath);
50039
50114
  if (presets === null) {
@@ -50068,7 +50143,24 @@ function exportAnswer(ending, asked, before, after, beside = [], docCacheNote =
50068
50143
  if (verdict.exported) {
50069
50144
  return { content: [{ type: "text", text: answerJson(verdict) }] };
50070
50145
  }
50071
- 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.`;
50072
50164
  const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
50073
50165
  return {
50074
50166
  content: [
@@ -50823,6 +50915,17 @@ function ownArguments(params) {
50823
50915
  function needsItsOwnProcess(asked) {
50824
50916
  return asked.savesIn !== undefined || asked.env !== undefined;
50825
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
+ }
50826
50929
 
50827
50930
  class GodotServer {
50828
50931
  stdio = null;
@@ -52454,7 +52557,7 @@ class GodotServer {
52454
52557
  ...written,
52455
52558
  payload: {
52456
52559
  ...written.payload,
52457
- 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"])}`
52458
52561
  }
52459
52562
  } : written);
52460
52563
  }
@@ -53509,10 +53612,14 @@ ${error.text}`;
53509
53612
  test: `${row.suite} > ${row.name}`,
53510
53613
  reason: row.reason
53511
53614
  }));
53615
+ const casesUnrunnable = casePrinted.unrunnable.map((one) => ({
53616
+ test: `${one.suite} > ${one.name}`,
53617
+ reason: one.reason
53618
+ }));
53512
53619
  return this.jsonTextResponse({
53513
- passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && casesNotRun.length === 0 && rowsMismatched.length === 0 && notSuites.length === 0,
53620
+ passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && uncaught.length === 0 && undeclaredPrinted.length === 0 && declaredNotPrinted.length === 0 && countedLeaks.length === 0 && missing.length === 0 && testsNotRun.length === 0 && casesNotRun.length === 0 && rowsMismatched.length === 0 && casesUnrunnable.length === 0 && notSuites.length === 0,
53514
53621
  verdict: [
53515
- verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + casesNotRun.length + rowsMismatched.length + notSuites.length > 0 ? undefined : verdict,
53622
+ verdict === "passed" && undeclaredPrinted.length + declaredNotPrinted.length + countedLeaks.length + testsNotRun.length + casesNotRun.length + rowsMismatched.length + casesUnrunnable.length + notSuites.length > 0 ? undefined : verdict,
53516
53623
  ...[false, true].map((warning) => {
53517
53624
  const count = undeclaredPrinted.filter((error) => error.warning === warning).length;
53518
53625
  const word = warning ? "warning" : "error";
@@ -53526,12 +53633,14 @@ ${error.text}`;
53526
53633
  countedLeaks.length > 0 ? "the engine exited holding what it should have freed" : undefined,
53527
53634
  testsNotRun.length + casesNotRun.length > 0 ? `not run: ${[...testsNotRun, ...casesNotRun].join(", ")}` : undefined,
53528
53635
  rowsMismatched.length > 0 ? `${rowsMismatched.length === 1 ? "one row" : `${String(rowsMismatched.length)} rows`} skipped for data that does not fit the case` : undefined,
53636
+ casesUnrunnable.length > 0 ? `${casesUnrunnable.length === 1 ? "one case" : `${String(casesUnrunnable.length)} cases`} skipped as gdUnit4 cannot run ${casesUnrunnable.length === 1 ? "it" : "them"} as declared` : undefined,
53529
53637
  notSuites.length > 0 ? `${notSuites.length === 1 ? "one script declares" : `${String(notSuites.length)} scripts declare`} cases gdUnit4 never runs` : undefined
53530
53638
  ].filter((part) => part !== undefined).join("; "),
53531
53639
  ...missing.length > 0 ? { missing } : {},
53532
53640
  ...notSuites.length > 0 ? { notSuites } : {},
53533
53641
  ...testsNotRun.length + casesNotRun.length > 0 ? { testsNotRun: [...testsNotRun, ...casesNotRun] } : {},
53534
53642
  ...rowsMismatched.length > 0 ? { rowsMismatched } : {},
53643
+ ...casesUnrunnable.length > 0 ? { casesUnrunnable } : {},
53535
53644
  ...timedOut ? { timedOut, hung, silentForMs } : {},
53536
53645
  exitCode,
53537
53646
  exitSignal: run.exitSignal ?? undefined,
@@ -53565,6 +53674,7 @@ ${error.text}`;
53565
53674
  testsNotRun.length > 0 ? `${testsNotRun.join(", ")} ${testsNotRun.length === 1 ? "was" : "were"} asked for and not run, so the run is not called passed: gdUnit4 runs a test_ function only in a script that extends GdUnitTestSuite, and leaves out a suite whose script does not load.` : undefined,
53566
53675
  casesNotRun.length > 0 ? `${casesNotRun.join(", ")} ${casesNotRun.length === 1 ? "is a case" : "are cases"} of a suite that ran and passed, and gdUnit4 never ran ${casesNotRun.length === 1 ? "it" : "them"} and said nothing of it, so the run is not called passed, as gdharness test judges it.` : undefined,
53567
53676
  rowsMismatched.length > 0 ? `gdUnit4 skipped what rowsMismatched names because the row's data does not fit the case's parameters, though it printed ${rowsMismatched.length === 1 ? "the row" : "each row"} passed first; each entry carries gdUnit4's reason. The run is not called passed, as gdharness test judges it.` : undefined,
53677
+ casesUnrunnable.length > 0 ? `gdUnit4 skipped what casesUnrunnable names because it cannot run ${casesUnrunnable.length === 1 ? "that case" : "those cases"} as declared, though it printed ${casesUnrunnable.length === 1 ? "it" : "each"} passed first; each entry carries gdUnit4's reason. A parameter is filled only by _test_parameters, a fuzzer or a setting gdUnit4 names (timeout, do_skip, skip_reason, fuzzer_iterations, fuzzer_seed), and fuzzers come before the settings. The run is not called passed, as gdharness test judges it.` : undefined,
53568
53678
  notSuites.length > 0 ? "What notSuites names declares cases gdUnit4 never runs, so the run is not called passed, as gdharness test judges it." : undefined,
53569
53679
  briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
53570
53680
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
@@ -55114,17 +55224,20 @@ ${error.text}`;
55114
55224
  const sendingFrom = Date.now();
55115
55225
  const breakpoints = await this.dapHoldingBreakpointsOf(projectPath).reapplyBreakpoints();
55116
55226
  this.logDebug(`Breakpoints were sent again after ${Date.now() - sendingFrom}ms`);
55227
+ const slot = await slotForCall();
55117
55228
  if (profiled) {
55118
55229
  const armed = await this.handleViaBridge("profile_start", {
55119
55230
  arm: true,
55120
55231
  frameFunctions: profileFrameFunctions(projectPath)
55121
55232
  });
55122
55233
  if (armed.isError === true) {
55234
+ slot.release();
55123
55235
  return armed;
55124
55236
  }
55125
55237
  }
55126
55238
  const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
55127
55239
  if (answer.isError === true) {
55240
+ slot.release();
55128
55241
  return answer;
55129
55242
  }
55130
55243
  const startedAt = Date.now();
@@ -55154,7 +55267,8 @@ ${error.text}`;
55154
55267
  announcedBefore: alreadyPlaying,
55155
55268
  seenPlaying: readBoolean(playAnswer, "playing") === true,
55156
55269
  ...which === null ? {} : { play: which },
55157
- bootScene: runtimeWaitMs.scene
55270
+ bootScene: runtimeWaitMs.scene,
55271
+ slot
55158
55272
  };
55159
55273
  this.activeProcess = played;
55160
55274
  this.noteTheLatestRun({ projectPath, pid: null, startedAt });
@@ -55170,6 +55284,11 @@ ${error.text}`;
55170
55284
  sweepTranscripts();
55171
55285
  step("sweep");
55172
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
+ }
55173
55292
  return this.jsonTextResponse({
55174
55293
  started: true,
55175
55294
  through: "editor",
@@ -55186,7 +55305,7 @@ ${error.text}`;
55186
55305
  scriptPath: projectSpelling(projectPath, one.scriptPath),
55187
55306
  reason: one.reason
55188
55307
  })),
55189
- runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
55308
+ runtime,
55190
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)
55191
55310
  });
55192
55311
  }
@@ -55567,20 +55686,33 @@ ${error.text}`;
55567
55686
  closeSync5(transcript.fd);
55568
55687
  const gameEnv = profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env;
55569
55688
  const profile = profiled ? profilePath(startedAt) : undefined;
55570
- const launched = await launchOutsideTheTree({
55571
- command: godotPath,
55572
- args: cmdArgs,
55573
- ...gameEnv === undefined ? {} : { env: gameEnv },
55574
- run: {
55575
- transcript: transcript.path,
55576
- startedAt,
55577
- projectPath,
55578
- ...this.servedBy(),
55579
- ...profile === undefined ? {} : { profile },
55580
- ...debugged ? { breakpoints: this.breakpointsForAGame(projectPath) } : { debugged: false }
55581
- },
55582
- ...desktop === undefined ? {} : { desktop }
55583
- });
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
+ }
55584
55716
  if ("error" in launched) {
55585
55717
  return launched;
55586
55718
  }
@@ -55802,6 +55934,11 @@ ${error.text}`;
55802
55934
  tie(run, endpoint) {
55803
55935
  run.announcedPid = endpoint.pid;
55804
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
+ }
55805
55942
  }
55806
55943
  playedByOurEditor(endpoint) {
55807
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"]