gdharness 1.2.33 → 1.2.35

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
@@ -7847,6 +7847,11 @@ function isWithinRoot(rootPath, candidatePath) {
7847
7847
  const step = relative(resolve(rootPath), resolve(candidatePath));
7848
7848
  return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
7849
7849
  }
7850
+ function keptInside(given) {
7851
+ const at = given.indexOf("::");
7852
+ const inner = at === -1 ? "" : given.slice(at + 2);
7853
+ return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
7854
+ }
7850
7855
  function resolveWithinProject(projectPath, candidatePath, options = {}) {
7851
7856
  const trimmed = candidatePath.trim();
7852
7857
  if (trimmed === "") {
@@ -7875,6 +7880,13 @@ function resolveWithinProject(projectPath, candidatePath, options = {}) {
7875
7880
  reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
7876
7881
  };
7877
7882
  }
7883
+ const kept = keptInside(trimmed);
7884
+ if (kept.inner !== undefined) {
7885
+ return {
7886
+ ok: false,
7887
+ reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
7888
+ };
7889
+ }
7878
7890
  if (withoutScheme.includes(":")) {
7879
7891
  return {
7880
7892
  ok: false,
@@ -7917,9 +7929,10 @@ function exportedBuild(projectPath, candidatePath) {
7917
7929
  }
7918
7930
  return { ok: true, absolutePath: real, relativePath: found.relativePath };
7919
7931
  }
7920
- var RESOURCE_SCHEME = "res://", OTHER_SCHEME;
7932
+ var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
7921
7933
  var init_paths = __esm(() => {
7922
7934
  OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
7935
+ KEPT_ID = /^[A-Za-z0-9_]+$/;
7923
7936
  });
7924
7937
 
7925
7938
  // src/runner.ts
@@ -8128,9 +8141,14 @@ class GameLog {
8128
8141
  const owner = this.lastHeadline[source];
8129
8142
  if (owner !== null && /^\s/.test(line)) {
8130
8143
  owner.detail.push(line.trim());
8144
+ owner.continues = false;
8131
8145
  return;
8132
8146
  }
8133
8147
  const headline = announced(line);
8148
+ if (owner?.continues === true && headline === null) {
8149
+ owner.detail.push(line.trim());
8150
+ return;
8151
+ }
8134
8152
  const detail = [];
8135
8153
  const entry = {
8136
8154
  index: this.entries.length,
@@ -8143,7 +8161,7 @@ class GameLog {
8143
8161
  if (headline?.byScript === true) {
8144
8162
  this.byScript.add(entry.index);
8145
8163
  }
8146
- this.lastHeadline[source] = headline ? { detail } : null;
8164
+ this.lastHeadline[source] = headline ? { detail, continues: source === "stderr" && headline.text.endsWith(":") } : null;
8147
8165
  }
8148
8166
  raisedByScript(entry) {
8149
8167
  return this.byScript.has(entry.index);
@@ -11308,7 +11326,7 @@ var init_tool_definitions = __esm(() => {
11308
11326
  {
11309
11327
  name: "project_settings",
11310
11328
  title: "Project settings",
11311
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
11329
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
11312
11330
  parameters: {
11313
11331
  projectPath: PROJECT_PATH,
11314
11332
  setting: {
@@ -11406,8 +11424,8 @@ var init_tool_definitions = __esm(() => {
11406
11424
  requires: ["scenePath"]
11407
11425
  },
11408
11426
  add_input_action: {
11409
- effect: "additive",
11410
- summary: "register an input action and its events",
11427
+ effect: "destructive",
11428
+ summary: "register an input action and its events, or rebind one that exists",
11411
11429
  requires: ["actionName", "events"]
11412
11430
  },
11413
11431
  remove_input_action: {
@@ -11545,20 +11563,36 @@ var init_tool_definitions = __esm(() => {
11545
11563
  {
11546
11564
  name: "project_export",
11547
11565
  title: "Export",
11548
- description: "Export presets and exports.",
11566
+ description: "Export presets and exports. add_preset writes a preset to export_presets.cfg with the keys the editor's Project > Export writes for a new one, every platform option left at its default, and makes it the platform's runnable preset when the platform has none. It answers where this engine's export templates belong and whether the preset's platform has them, since an export needs them; list says the same for every preset. An editor open on the project does not see a preset added this way until it restarts. run names, as alsoWritten, what else the export wrote beside its output, such as the .pck a Windows or Linux build cannot start without.",
11549
11567
  parameters: {
11550
11568
  projectPath: PROJECT_PATH,
11551
- preset: { type: "string", description: "Preset name from export_presets.cfg." },
11569
+ preset: {
11570
+ type: "string",
11571
+ ops: ["run", "add_preset"],
11572
+ description: "Preset name from export_presets.cfg. add_preset: the name of the new one."
11573
+ },
11574
+ platform: {
11575
+ type: "string",
11576
+ ops: ["add_preset"],
11577
+ enum: ["Windows Desktop", "Linux", "macOS", "Android", "iOS", "Web"],
11578
+ description: "add_preset: the platform the preset exports for."
11579
+ },
11552
11580
  outputPath: {
11553
11581
  type: "string",
11554
- description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset.`
11582
+ ops: ["run", "add_preset"],
11583
+ description: `Where the export is written, inside the project. The directory is created if it is not there: Godot's command-line exporter refuses a missing one with "The given export path doesn't exist", which reads as a wrong path in the preset. add_preset: the path the preset exports to from the editor, optional.`
11555
11584
  },
11556
- debug: { type: "boolean", description: "run: a debug export. Default false." }
11585
+ debug: { type: "boolean", ops: ["run"], description: "run: a debug export. Default false." }
11557
11586
  },
11558
11587
  requires: ["projectPath"],
11559
11588
  operations: {
11560
11589
  list: { effect: "read", summary: "the presets in export_presets.cfg", requires: [] },
11561
- run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] }
11590
+ run: { effect: "destructive", summary: "export with a preset", requires: ["preset", "outputPath"] },
11591
+ add_preset: {
11592
+ effect: "additive",
11593
+ summary: "add an export preset to export_presets.cfg",
11594
+ requires: ["preset", "platform"]
11595
+ }
11562
11596
  }
11563
11597
  },
11564
11598
  {
@@ -11755,7 +11789,7 @@ var init_tool_definitions = __esm(() => {
11755
11789
  {
11756
11790
  name: "scene_animation",
11757
11791
  title: "Animations",
11758
- description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. Needs the editor connected.",
11792
+ description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need 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 running the scene's scripts. The ops that write need the editor connected.",
11759
11793
  parameters: {
11760
11794
  projectPath: PROJECT_PATH,
11761
11795
  scenePath: SCENE_PATH,
@@ -11787,7 +11821,11 @@ var init_tool_definitions = __esm(() => {
11787
11821
  ops: ["read_states", "add_state", "connect_states"],
11788
11822
  description: "The AnimationTree node."
11789
11823
  },
11790
- stateName: { type: "string", ops: ["add_state"], description: "add_state: the state to add." },
11824
+ stateName: {
11825
+ type: "string",
11826
+ ops: ["add_state"],
11827
+ description: "add_state: the state to add. It goes right of the rightmost state in the editor's graph, answered under position."
11828
+ },
11791
11829
  stateMachinePath: {
11792
11830
  type: "string",
11793
11831
  ops: ["read_states", "add_state", "connect_states"],
@@ -11962,7 +12000,7 @@ var init_tool_definitions = __esm(() => {
11962
12000
  projectPath: PROJECT_PATH,
11963
12001
  resourcePath: {
11964
12002
  ...RESOURCE_PATH,
11965
- description: `${RESOURCE_FILE} move: a directory too.`
12003
+ description: `${RESOURCE_FILE} move: a directory too. read: also a resource kept inside a scene or resource file, by the path a read answers a property holding it with, such as "res://hero.tscn::Animation_jump".`
11966
12004
  },
11967
12005
  resourceType: {
11968
12006
  type: "string",
@@ -12929,7 +12967,7 @@ var init_headless_operations = __esm(() => {
12929
12967
  uid: "get_uid",
12930
12968
  refresh_classes: "refresh_class_cache"
12931
12969
  }),
12932
- project_export: dictionary({ list: "list_export_presets" }),
12970
+ project_export: dictionary({ list: "list_export_presets", add_preset: "add_export_preset" }),
12933
12971
  script_edit: dictionary({ create: "create_script", modify: "modify_script" }),
12934
12972
  script_info: dictionary({ structure: "get_script_info" }),
12935
12973
  editor_classes: dictionary({
@@ -44875,7 +44913,7 @@ function exportPresetRefusal(projectPath, preset) {
44875
44913
  try {
44876
44914
  text = readFileSync26(join30(projectPath, "export_presets.cfg"), "utf8");
44877
44915
  } catch {
44878
- return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. The editor writes the file when a preset is added in Project > Export.`;
44916
+ return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
44879
44917
  }
44880
44918
  const names = [];
44881
44919
  let inPreset = false;
@@ -44893,9 +44931,9 @@ function exportPresetRefusal(projectPath, preset) {
44893
44931
  if (names.includes(preset)) {
44894
44932
  return null;
44895
44933
  }
44896
- return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. The editor adds one in Project > Export.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
44934
+ return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
44897
44935
  }
44898
- function exportAnswer(ending, asked, before, after) {
44936
+ function exportAnswer(ending, asked, before, after, beside = []) {
44899
44937
  const { log } = ending;
44900
44938
  const written = after !== null && after !== before;
44901
44939
  const notCopied = librariesNotCopied(log.all);
@@ -44907,6 +44945,7 @@ function exportAnswer(ending, asked, before, after) {
44907
44945
  failure: ending.failure ?? undefined,
44908
44946
  errors: log.count("error"),
44909
44947
  warnings: log.count("warning"),
44948
+ ...beside.length === 0 ? {} : { alsoWritten: beside },
44910
44949
  ...notCopied.length === 0 ? {} : {
44911
44950
  librariesNotLoaded: notCopied,
44912
44951
  extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
@@ -44929,6 +44968,20 @@ function exportAnswer(ending, asked, before, after) {
44929
44968
  isError: true
44930
44969
  };
44931
44970
  }
44971
+ function writtenBeside(before, after, output, within) {
44972
+ return [...after].filter(([name, at]) => name !== output && before.get(name) !== at).map(([name]) => within === "." ? name : `${within}/${name}`).sort();
44973
+ }
44974
+ function filesIn(folder) {
44975
+ const found = new Map;
44976
+ try {
44977
+ for (const entry of readdirSync13(folder, { withFileTypes: true })) {
44978
+ if (entry.isFile()) {
44979
+ found.set(entry.name, statSync13(join30(folder, entry.name)).mtimeMs);
44980
+ }
44981
+ }
44982
+ } catch {}
44983
+ return found;
44984
+ }
44932
44985
  function scanWaitAnswer(scanned, started = "game") {
44933
44986
  if (scanned.waitedMs === 0) {
44934
44987
  return {};
@@ -46163,6 +46216,9 @@ class GodotServer {
46163
46216
  if (tool === "project_import" && op === "set_options") {
46164
46217
  return await this.handleSetImportOptions(args);
46165
46218
  }
46219
+ if (tool === "project_export" && op === "add_preset") {
46220
+ return await this.handleAddExportPreset(args);
46221
+ }
46166
46222
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
46167
46223
  if (headless !== undefined) {
46168
46224
  const { from: asked, ...answerable } = args;
@@ -46220,7 +46276,11 @@ class GodotServer {
46220
46276
  case "scene_create":
46221
46277
  return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
46222
46278
  case "scene_tree":
46223
- return await this.readFromFile(args, "scenePath", "read_scene_tree", async () => await bridge("list_scene_nodes"));
46279
+ return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
46280
+ scenePath: file,
46281
+ depth: readNumber(args, "depth") ?? -1,
46282
+ includeProperties: readBoolean(args, "includeProperties") ?? false
46283
+ }), async () => await bridge("list_scene_nodes"));
46224
46284
  case "scene_node":
46225
46285
  switch (op) {
46226
46286
  case "add":
@@ -46250,9 +46310,17 @@ class GodotServer {
46250
46310
  case "scene_animation":
46251
46311
  switch (op) {
46252
46312
  case "read":
46253
- return await bridge("read_animations");
46313
+ return await this.readFromFile(args, "scenePath", "read_animations", (file) => ({
46314
+ scenePath: file,
46315
+ playerNodePath: readNonEmptyString(args, "playerNodePath") ?? ".",
46316
+ animationName: readString(args, "animationName") ?? ""
46317
+ }), async () => await bridge("read_animations"));
46254
46318
  case "read_states":
46255
- return await bridge("read_animation_states");
46319
+ return await this.readFromFile(args, "scenePath", "read_animation_states", (file) => ({
46320
+ scenePath: file,
46321
+ animTreePath: readString(args, "animTreePath") ?? "",
46322
+ stateMachinePath: readString(args, "stateMachinePath") ?? ""
46323
+ }), async () => await bridge("read_animation_states"));
46256
46324
  case "create":
46257
46325
  return await bridge("create_animation");
46258
46326
  case "add_track":
@@ -46279,8 +46347,14 @@ class GodotServer {
46279
46347
  switch (op) {
46280
46348
  case "move":
46281
46349
  return await this.handleMoveFile(args);
46282
- case "read":
46283
- return await this.readFromFile(args, "resourcePath", "read_resource", async () => await bridge("read_resource"));
46350
+ case "read": {
46351
+ const { file, inner } = keptInside(readString(args, "resourcePath") ?? "");
46352
+ const kept = inner === undefined ? {} : { resourcePath: file, subResource: inner };
46353
+ return await this.readFromFile({ ...args, ...kept }, "resourcePath", "read_resource", (found) => ({
46354
+ resourcePath: inner === undefined ? found : `${found}::${inner}`,
46355
+ includeDefaults: readBoolean(args, "includeDefaults") ?? false
46356
+ }), async () => await bridge("read_resource", kept));
46357
+ }
46284
46358
  case "create":
46285
46359
  return await bridge("create_resource");
46286
46360
  case "modify":
@@ -47208,6 +47282,30 @@ class GodotServer {
47208
47282
  return this.createErrorResponse(`Search failed: ${errorMessage(error)}`);
47209
47283
  }
47210
47284
  }
47285
+ async handleAddExportPreset(args) {
47286
+ const project = this.project(args);
47287
+ if (!project.ok) {
47288
+ return project.response;
47289
+ }
47290
+ const given = readNonEmptyString(args, "outputPath");
47291
+ const output = given === undefined ? null : resolveWithinProject(project.value.path, given);
47292
+ if (output !== null && !output.ok) {
47293
+ return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
47294
+ }
47295
+ const outcome = await this.operation("add_export_preset", {
47296
+ preset: readString(args, "preset") ?? "",
47297
+ platform: readString(args, "platform") ?? "",
47298
+ outputPath: output?.relativePath ?? ""
47299
+ }, project.value.path);
47300
+ if (!outcome.ok || !this.editorHasOpen(project.value.path)) {
47301
+ return this.answer(outcome);
47302
+ }
47303
+ return this.jsonTextResponse({
47304
+ ...outcome.payload,
47305
+ editorNote: "The editor open on this project read export_presets.cfg when it started and does not list this preset, and saving presets in its Export dialog writes its own list over the file. editor_launch restart loads the file; an export through project_export run reads the file and needs no restart.",
47306
+ ...engineExtras(outcome)
47307
+ });
47308
+ }
47211
47309
  async handleExportProject(args) {
47212
47310
  const project = this.project(args);
47213
47311
  if (!project.ok) {
@@ -47216,11 +47314,14 @@ class GodotServer {
47216
47314
  const preset = readNonEmptyString(args, "preset") ?? "";
47217
47315
  const output = resolveWithinProject(project.value.path, readString(args, "outputPath") ?? "");
47218
47316
  if (!output.ok) {
47219
- return this.createErrorResponse(output.reason, PATH_SOLUTIONS);
47317
+ return this.createErrorResponse(output.reason, OUTPUT_PATH_SOLUTIONS);
47220
47318
  }
47221
47319
  const noPreset = exportPresetRefusal(project.value.path, preset);
47222
47320
  if (noPreset !== null) {
47223
- return this.createErrorResponse(noPreset, ["project_export list reads the presets the project has"]);
47321
+ return this.createErrorResponse(noPreset, [
47322
+ "project_export list reads the presets the project has",
47323
+ "project_export add_preset adds one"
47324
+ ]);
47224
47325
  }
47225
47326
  const engine = await this.engine();
47226
47327
  if (!engine.ok) {
@@ -47247,6 +47348,7 @@ class GodotServer {
47247
47348
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
47248
47349
  const writtenAt = () => existsSync23(output.absolutePath) ? statSync13(output.absolutePath).mtimeMs : null;
47249
47350
  const before = writtenAt();
47351
+ const listedBefore = filesIn(folder);
47250
47352
  const freed = freeLibraryCopies(project.value.path);
47251
47353
  if (freed.setAside.length > 0 || freed.swept.length > 0) {
47252
47354
  this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
@@ -47260,7 +47362,8 @@ class GodotServer {
47260
47362
  if (ending.exitSignal === null && ending.exitCode === null) {
47261
47363
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
47262
47364
  }
47263
- return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
47365
+ const beside = writtenBeside(listedBefore, filesIn(folder), basename9(output.absolutePath), dirname22(output.relativePath).replaceAll("\\", "/"));
47366
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
47264
47367
  }
47265
47368
  reportTestProgress(log) {
47266
47369
  const progress = callProgress();
@@ -50670,7 +50773,7 @@ class GodotServer {
50670
50773
  ...leftChanged === undefined ? {} : { note: leftChanged }
50671
50774
  });
50672
50775
  }
50673
- async readFromFile(args, pathKey, operation, throughTheEditor) {
50776
+ async readFromFile(args, pathKey, operation, paramsFor, throughTheEditor) {
50674
50777
  const project = this.project(args);
50675
50778
  if (!project.ok) {
50676
50779
  return project.response;
@@ -50688,12 +50791,7 @@ class GodotServer {
50688
50791
  if (file === null) {
50689
50792
  return this.createErrorResponse(`${pathKey} must name a file inside the project.`);
50690
50793
  }
50691
- const params = operation === "read_scene_tree" ? {
50692
- scenePath: file,
50693
- depth: readNumber(args, "depth") ?? -1,
50694
- includeProperties: readBoolean(args, "includeProperties") ?? false
50695
- } : { resourcePath: file, includeDefaults: readBoolean(args, "includeDefaults") ?? false };
50696
- const outcome = await this.operation(operation, params, projectPath);
50794
+ const outcome = await this.operation(operation, paramsFor(file), projectPath);
50697
50795
  if (!outcome.ok) {
50698
50796
  return this.answer(outcome);
50699
50797
  }
@@ -51355,7 +51453,7 @@ async function runGodotServer() {
51355
51453
  const server = new GodotServer;
51356
51454
  await server.run();
51357
51455
  }
51358
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
51456
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
51359
51457
  var init_server = __esm(() => {
51360
51458
  init_dist();
51361
51459
  init_stdio();
@@ -51437,6 +51535,9 @@ var init_server = __esm(() => {
51437
51535
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
51438
51536
  "Point projectPath at the project the file belongs to"
51439
51537
  ];
51538
+ OUTPUT_PATH_SOLUTIONS = [
51539
+ 'Give outputPath inside the project, relative to it, such as "build/game.exe" or "res://build/game.exe"; copy the build elsewhere once it is exported'
51540
+ ];
51440
51541
  PROJECT_FILE_ARGUMENTS = [
51441
51542
  "scenePath",
51442
51543
  "instancePath",
@@ -0,0 +1,224 @@
1
+ extends RefCounted
2
+
3
+ ## Animations and state machines read off their nodes: by the editor's tools from the scene it holds,
4
+ ## and by a headless engine with none connected from a bare node of the class given what the file
5
+ ## stores, which runs no script of the scene's, so a read answers the same either way.
6
+
7
+ const FileReads = preload("file_reads.gd")
8
+ const Serialisation = preload("serialisation.gd")
9
+
10
+ const LOOP_MODE_NAMES: Dictionary[int, String] = {
11
+ Animation.LOOP_NONE: "none",
12
+ Animation.LOOP_LINEAR: "linear",
13
+ Animation.LOOP_PINGPONG: "pingpong",
14
+ }
15
+ const TRACK_TYPE_NAMES: Dictionary[int, String] = {
16
+ Animation.TYPE_VALUE: "property",
17
+ Animation.TYPE_POSITION_3D: "position_3d",
18
+ Animation.TYPE_ROTATION_3D: "rotation_3d",
19
+ Animation.TYPE_SCALE_3D: "scale_3d",
20
+ Animation.TYPE_BLEND_SHAPE: "blend_shape",
21
+ Animation.TYPE_METHOD: "method",
22
+ Animation.TYPE_BEZIER: "bezier",
23
+ Animation.TYPE_AUDIO: "audio",
24
+ Animation.TYPE_ANIMATION: "animation",
25
+ }
26
+ const SWITCH_MODE_NAMES: Dictionary[int, String] = {
27
+ AnimationNodeStateMachineTransition.SWITCH_MODE_IMMEDIATE: "immediate",
28
+ AnimationNodeStateMachineTransition.SWITCH_MODE_SYNC: "sync",
29
+ AnimationNodeStateMachineTransition.SWITCH_MODE_AT_END: "at_end",
30
+ }
31
+
32
+
33
+ ## [method animations] of the AnimationPlayer at [param player_path] in the file [param scene_path].
34
+ static func scene_animations(scene_path: String, player_path: String, animation_name: String) -> Dictionary:
35
+ var made: Array = _bare(scene_path, player_path, "AnimationPlayer")
36
+ if made[0] == null:
37
+ return made[1]
38
+ var player: AnimationPlayer = made[0]
39
+ var answer: Dictionary = animations(player, animation_name)
40
+ player.free()
41
+ return answer
42
+
43
+
44
+ ## [method states] of the AnimationTree at [param tree_path] in the file [param scene_path].
45
+ static func scene_states(scene_path: String, tree_path: String, state_machine_path: String) -> Dictionary:
46
+ var made: Array = _bare(scene_path, tree_path, "AnimationTree")
47
+ if made[0] == null:
48
+ return made[1]
49
+ var tree: AnimationTree = made[0]
50
+ var answer: Dictionary = states(tree, state_machine_path)
51
+ tree.free()
52
+ return answer
53
+
54
+
55
+ ## A node of the class the file gives the node at [param node_path], holding every property the
56
+ ## files set but its script, as [node, null] or [null, refusal]. The caller frees it.
57
+ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
58
+ var read: Dictionary = FileReads.scene_node(scene_path, node_path)
59
+ if not read.is_empty() and not read.get("ok", false):
60
+ return [null, read]
61
+ var type: String = str(read.get("type", ""))
62
+ if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
63
+ return [null, {"ok": false, "error": "%s not found at: %s" % [kind, node_path]}]
64
+ var bare: Node = ClassDB.instantiate(type)
65
+ bare.name = str(read["name"])
66
+ var given: Dictionary = read["properties"]
67
+ for property: Variant in given:
68
+ if str(property) != "script":
69
+ bare.set(str(property), given[property])
70
+ return [bare, null]
71
+
72
+
73
+ ## The animations of [param player] with their lengths, or [param animation_name] whole when it is
74
+ ## given. A track comes back in the shape add_track takes, so one read here can be written to
75
+ ## another animation as it stands.
76
+ static func animations(player: AnimationPlayer, animation_name: String) -> Dictionary:
77
+ if animation_name.is_empty():
78
+ var listed: Array[Dictionary] = []
79
+ for called: String in player.get_animation_list():
80
+ var listed_one: Animation = player.get_animation(StringName(called))
81
+ var summary: Dictionary = _summary(listed_one)
82
+ summary["animationName"] = called
83
+ summary["tracks"] = listed_one.get_track_count()
84
+ listed.append(summary)
85
+ return {"ok": true, "animations": listed}
86
+ if not player.has_animation(StringName(animation_name)):
87
+ var names: PackedStringArray = player.get_animation_list()
88
+ return {
89
+ "ok": false,
90
+ "error":
91
+ (
92
+ "%s has no animation %s. It has: %s."
93
+ % [player.name, animation_name, ", ".join(names) if not names.is_empty() else "none"]
94
+ )
95
+ }
96
+ var anim: Animation = player.get_animation(StringName(animation_name))
97
+ var answer: Dictionary = _summary(anim)
98
+ answer["ok"] = true
99
+ answer["animationName"] = animation_name
100
+ var tracks: Array[Dictionary] = []
101
+ var values: Serialisation = Serialisation.new()
102
+ for track: int in anim.get_track_count():
103
+ tracks.append(_track(anim, track, values))
104
+ answer["tracks"] = tracks
105
+ return answer
106
+
107
+
108
+ static func _summary(anim: Animation) -> Dictionary:
109
+ return {
110
+ "length": anim.length,
111
+ "loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
112
+ "step": anim.step,
113
+ }
114
+
115
+
116
+ static func _track(anim: Animation, track: int, values: Serialisation) -> Dictionary:
117
+ var kind: int = anim.track_get_type(track)
118
+ var path: NodePath = anim.track_get_path(track)
119
+ var shaped: Dictionary = {
120
+ "type": TRACK_TYPE_NAMES.get(kind, str(kind)), "nodePath": str(path.get_concatenated_names())
121
+ }
122
+ var property: String = str(path.get_concatenated_subnames())
123
+ if not property.is_empty():
124
+ shaped["property"] = property
125
+ var keyframes: Array[Dictionary] = []
126
+ var methods: Dictionary = {}
127
+ for key: int in anim.track_get_key_count(track):
128
+ var keyframe: Dictionary = {"time": anim.track_get_key_time(track, key)}
129
+ if kind == Animation.TYPE_METHOD:
130
+ var method: String = str(anim.method_track_get_name(track, key))
131
+ methods[method] = true
132
+ keyframe["method"] = method
133
+ keyframe["args"] = values.serialize_value(anim.method_track_get_params(track, key))
134
+ else:
135
+ keyframe["value"] = values.serialize_value(anim.track_get_key_value(track, key))
136
+ keyframes.append(keyframe)
137
+ # One method across every key is the track's, as add_track takes it. Keys calling different
138
+ # methods, which the editor can make and add_track cannot, keep their own.
139
+ if methods.size() == 1:
140
+ shaped["method"] = methods.keys()[0]
141
+ for keyframe: Dictionary in keyframes:
142
+ var _shared: bool = keyframe.erase("method")
143
+ shaped["keyframes"] = keyframes
144
+ return shaped
145
+
146
+
147
+ ## The state machine at a slash-separated path of nested state machine names in [param tree], or
148
+ ## null when any step of the path is not one. Every node in a tree of state machines is a state
149
+ ## machine itself, so the walk asks each step for the child by name and refuses anything else.
150
+ static func state_machine(tree: AnimationTree, state_machine_path: String) -> AnimationNodeStateMachine:
151
+ if tree == null:
152
+ return null
153
+ var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
154
+ if state_machine_path == "root":
155
+ return current
156
+ for segment: String in state_machine_path.split("/", false):
157
+ if current == null or not current.has_node(StringName(segment)):
158
+ return null
159
+ current = current.get_node(StringName(segment)) as AnimationNodeStateMachine
160
+ return current
161
+
162
+
163
+ ## Why [method state_machine] found none at [param state_machine_path] in [param tree]: the step of
164
+ ## the path that failed, and what is there instead.
165
+ static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
166
+ if tree == null or tree.tree_root == null:
167
+ return "The AnimationTree has no tree_root."
168
+ var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
169
+ if current == null:
170
+ return (
171
+ "The AnimationTree's tree_root is not a state machine: its type is %s."
172
+ % tree.tree_root.get_class()
173
+ )
174
+ var walked: PackedStringArray = []
175
+ for segment: String in state_machine_path.split("/", false):
176
+ var where: String = "the root state machine"
177
+ if not walked.is_empty():
178
+ where = "the state machine at " + "/".join(walked)
179
+ if not current.has_node(StringName(segment)):
180
+ var names: PackedStringArray = []
181
+ for state: StringName in current.get_node_list():
182
+ if state != &"Start" and state != &"End":
183
+ var _listed: bool = names.push_back(str(state))
184
+ var held: String = ", ".join(names) if not names.is_empty() else "none"
185
+ return (
186
+ "%s has no state %s. Its states are: %s."
187
+ % [where.to_upper()[0] + where.substr(1), segment, held]
188
+ )
189
+ var next: AnimationNode = current.get_node(StringName(segment))
190
+ if not next is AnimationNodeStateMachine:
191
+ return (
192
+ "The state %s in %s is not a state machine: its type is %s."
193
+ % [segment, where, next.get_class()]
194
+ )
195
+ current = next
196
+ var _walked: bool = walked.push_back(segment)
197
+ return "No state machine at " + state_machine_path
198
+
199
+
200
+ ## The states and transitions of the state machine at [param state_machine_path] in [param tree].
201
+ static func states(tree: AnimationTree, state_machine_path: String) -> Dictionary:
202
+ var sm: AnimationNodeStateMachine = state_machine(tree, state_machine_path)
203
+ if sm == null:
204
+ return {"ok": false, "error": no_machine(tree, state_machine_path)}
205
+ var listed: Array[Dictionary] = []
206
+ for state: StringName in sm.get_node_list():
207
+ var node: AnimationNode = sm.get_node(state)
208
+ var entry: Dictionary = {"stateName": str(state), "type": node.get_class()}
209
+ if node is AnimationNodeAnimation:
210
+ var plays: AnimationNodeAnimation = node
211
+ entry["animationName"] = str(plays.animation)
212
+ listed.append(entry)
213
+ var transitions: Array[Dictionary] = []
214
+ for index: int in sm.get_transition_count():
215
+ var transition: AnimationNodeStateMachineTransition = sm.get_transition(index)
216
+ var shaped: Dictionary = {
217
+ "from": str(sm.get_transition_from(index)),
218
+ "to": str(sm.get_transition_to(index)),
219
+ "transitionType": SWITCH_MODE_NAMES.get(transition.switch_mode, str(transition.switch_mode)),
220
+ }
221
+ if not str(transition.advance_condition).is_empty():
222
+ shaped["advanceCondition"] = str(transition.advance_condition)
223
+ transitions.append(shaped)
224
+ return {"ok": true, "states": listed, "transitions": transitions}
@@ -0,0 +1 @@
1
+ uid://c3calvdgcggh9