gdharness 1.2.18 → 1.2.19

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
@@ -642,6 +642,36 @@ function resolveSilent(requested, variables) {
642
642
  const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
643
643
  return said !== undefined && said !== "0" && said !== "false";
644
644
  }
645
+ function engineFlags(given) {
646
+ const offered = Object.keys(ENGINE_FLAGS).join(", ");
647
+ for (let at = 0;at < given.length; at += 1) {
648
+ const flag = given[at] ?? "";
649
+ const takes = Object.hasOwn(ENGINE_FLAGS, flag) ? ENGINE_FLAGS[flag] : undefined;
650
+ if (takes === undefined) {
651
+ const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
652
+ return {
653
+ ok: false,
654
+ reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
655
+ };
656
+ }
657
+ if (takes === "none") {
658
+ continue;
659
+ }
660
+ const value = given[at + 1];
661
+ const fits = value !== undefined && (takes === "number" ? /^\d+(?:\.\d+)?$/.test(value) : /^\d+x\d+$/.test(value));
662
+ if (!fits) {
663
+ return {
664
+ ok: false,
665
+ reason: `${flag} takes ${takes === "number" ? "a number" : "a size such as 1280x720"} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
666
+ };
667
+ }
668
+ at += 1;
669
+ }
670
+ return { ok: true };
671
+ }
672
+ function engineFlagsAmongGameArgs(args) {
673
+ return args.filter((one) => Object.hasOwn(ENGINE_FLAGS, one) || Object.hasOwn(SET_BY_ARGUMENT, one));
674
+ }
645
675
  function runArguments(options) {
646
676
  const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
647
677
  if (options.silent === true && !options.headless) {
@@ -650,6 +680,9 @@ function runArguments(options) {
650
680
  if (options.quitAfter !== undefined && options.quitAfter !== null) {
651
681
  args.push("--quit-after", String(options.quitAfter));
652
682
  }
683
+ if (options.engineArgs !== undefined) {
684
+ args.push(...options.engineArgs);
685
+ }
653
686
  if (options.scene !== null) {
654
687
  args.push(`res://${options.scene}`);
655
688
  }
@@ -692,7 +725,35 @@ function environmentFor(options, variables = process.env) {
692
725
  const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
693
726
  return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
694
727
  }
695
- var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
728
+ var ENGINE_FLAGS, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
729
+ var init_launch = __esm(() => {
730
+ ENGINE_FLAGS = Object.freeze({
731
+ "--verbose": "none",
732
+ "--quiet": "none",
733
+ "--debug-collisions": "none",
734
+ "--debug-paths": "none",
735
+ "--debug-navigation": "none",
736
+ "--debug-avoidance": "none",
737
+ "--debug-stringnames": "none",
738
+ "--debug-canvas-item-redraw": "none",
739
+ "--debug-mute-audio": "none",
740
+ "--print-fps": "none",
741
+ "--gpu-profile": "none",
742
+ "--gpu-validation": "none",
743
+ "--gpu-abort": "none",
744
+ "--disable-vsync": "none",
745
+ "--frame-delay": "number",
746
+ "--time-scale": "number",
747
+ "--fixed-fps": "number",
748
+ "--max-fps": "number",
749
+ "--resolution": "size"
750
+ });
751
+ SET_BY_ARGUMENT = Object.freeze({
752
+ "--headless": "headless",
753
+ "--audio-driver": "silent",
754
+ "--quit-after": "frames, on editor_run check"
755
+ });
756
+ });
696
757
 
697
758
  // src/godot-path.ts
698
759
  import { execFile as execFile2 } from "node:child_process";
@@ -769,6 +830,7 @@ var run2;
769
830
  var init_godot_path = __esm(() => {
770
831
  init_detection();
771
832
  init_errors();
833
+ init_launch();
772
834
  run2 = promisify2(execFile2);
773
835
  });
774
836
 
@@ -10840,6 +10902,7 @@ function annotationsOf(spec) {
10840
10902
  }
10841
10903
  var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
10842
10904
  var init_tool_definitions = __esm(() => {
10905
+ init_launch();
10843
10906
  PROJECT_INFO_SECTIONS = [
10844
10907
  "autoloads",
10845
10908
  "plugins",
@@ -11758,7 +11821,13 @@ var init_tool_definitions = __esm(() => {
11758
11821
  type: "array",
11759
11822
  items: { type: "string" },
11760
11823
  ops: ["start", "check"],
11761
- description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
11824
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
11825
+ },
11826
+ engineArgs: {
11827
+ type: "array",
11828
+ items: { type: "string" },
11829
+ ops: ["start", "check"],
11830
+ description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
11762
11831
  },
11763
11832
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
11764
11833
  visible: {
@@ -34734,6 +34803,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
34734
34803
  var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, ERROR_REPORT_PATTERN, CONSOLE_BOUNDARY = "[gdharness] runtime listening on ", ERROR_REPORT_KEEP_MS, GONE_WINDOW_MS = 120000, GONE_KEPT = 32, gone, STARTED_AFTER_ANNOUNCING_MS = 2000, CONFIRMED_FOR_MS = 60000, JUDGED_AFTER_MS = 60000, verdicts, judging = null, ENDED_WITHIN_MS = 3000, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1, LATE_ANSWER_MS, LATE_KEPT = 50, lateAnswers;
34735
34804
  var init_runtime_client = __esm(() => {
34736
34805
  init_alive();
34806
+ init_launch();
34737
34807
  init_paths();
34738
34808
  init_process_children();
34739
34809
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
@@ -40746,6 +40816,7 @@ var init_lsp_client = __esm(() => {
40746
40816
  init_answer_json();
40747
40817
  init_errors();
40748
40818
  init_framing();
40819
+ init_launch();
40749
40820
  init_paths();
40750
40821
  init_ports();
40751
40822
  LSPTimeout = class LSPTimeout extends Error {
@@ -43618,6 +43689,14 @@ function scanWaitAnswer(scanned, started = "game") {
43618
43689
  scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
43619
43690
  };
43620
43691
  }
43692
+ function engineFlagsNote(args) {
43693
+ const among = engineFlagsAmongGameArgs(args);
43694
+ if (among.length === 0) {
43695
+ return;
43696
+ }
43697
+ const named = among.join(", ");
43698
+ return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
43699
+ }
43621
43700
  function opElsewhere(tool, requested) {
43622
43701
  const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
43623
43702
  const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
@@ -43999,9 +44078,9 @@ function callersAnswer(totals, asked) {
43999
44078
  note: found.sites.length === 0 ? `No script in the project names ${asked.name} in code, outside its declaration: a call made through a string, such as call("name"), is not found by name.` : found.property ? `${byName} ${PROPERTY_PLACES}` : byName
44000
44079
  };
44001
44080
  }
44002
- function foldedEntriesAnswer(log) {
44081
+ function foldedEntriesAnswer(log, floor = "warning") {
44003
44082
  const problems = log.selectFolded({
44004
- severity: "warning",
44083
+ severity: floor,
44005
44084
  sinceLastCall: false,
44006
44085
  limit: MOST_ENGINE_ENTRIES
44007
44086
  });
@@ -45026,6 +45105,25 @@ class GodotServer {
45026
45105
  }
45027
45106
  return { ok: true, value: given };
45028
45107
  }
45108
+ engineArguments(args) {
45109
+ if (args["engineArgs"] === undefined) {
45110
+ return { ok: true, value: [] };
45111
+ }
45112
+ const given = readStringArray(args, "engineArgs");
45113
+ if (given === undefined) {
45114
+ return {
45115
+ ok: false,
45116
+ response: this.createErrorResponse("engineArgs must be a list of strings.", [
45117
+ 'Pass engine flags, such as ["--verbose"] or ["--fixed-fps", "60"]'
45118
+ ])
45119
+ };
45120
+ }
45121
+ const checked = engineFlags(given);
45122
+ if (!checked.ok) {
45123
+ return { ok: false, response: this.createErrorResponse(checked.reason) };
45124
+ }
45125
+ return { ok: true, value: given };
45126
+ }
45029
45127
  runEnvironment(args) {
45030
45128
  const savesIn = readNonEmptyString(args, "savesIn");
45031
45129
  if (savesIn !== undefined && !isAbsolute3(savesIn)) {
@@ -46842,6 +46940,10 @@ class GodotServer {
46842
46940
  if (!given.ok) {
46843
46941
  return given.response;
46844
46942
  }
46943
+ const flags = this.engineArguments(args);
46944
+ if (!flags.ok) {
46945
+ return flags.response;
46946
+ }
46845
46947
  const asked = this.runEnvironment(args);
46846
46948
  if (!asked.ok) {
46847
46949
  return asked.response;
@@ -46863,7 +46965,7 @@ class GodotServer {
46863
46965
  }
46864
46966
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
46865
46967
  if (op === "check") {
46866
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
46968
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
46867
46969
  }
46868
46970
  await this.pickUpWhatTheEditorIsPlaying();
46869
46971
  let ended = null;
@@ -46890,7 +46992,7 @@ class GodotServer {
46890
46992
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
46891
46993
  const silent = resolveSilent(args["silent"], process.env) && !headless;
46892
46994
  const profiled = readBoolean(args, "profile") === true;
46893
- if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
46995
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
46894
46996
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
46895
46997
  }
46896
46998
  const cmdArgs = runArguments({
@@ -46898,6 +47000,7 @@ class GodotServer {
46898
47000
  headless,
46899
47001
  scene: sceneArgument,
46900
47002
  userArgs: given.value,
47003
+ engineArgs: flags.value,
46901
47004
  silent
46902
47005
  });
46903
47006
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
@@ -46910,7 +47013,7 @@ class GodotServer {
46910
47013
  started.announcedBefore = alreadyPlaying;
46911
47014
  started.bootScene = runtimeWaitMs.scene;
46912
47015
  this.activeProcess = started;
46913
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
47016
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
46914
47017
  const endedForThis = endedToStartThis(ended);
46915
47018
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
46916
47019
  const alsoRunning = leftRunningNote(stranded);
@@ -46920,6 +47023,7 @@ class GodotServer {
46920
47023
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
46921
47024
  pid: started.pid,
46922
47025
  arguments: cmdArgs,
47026
+ argsNote: engineFlagsNote(given.value),
46923
47027
  transcript: started.transcript,
46924
47028
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
46925
47029
  refreshedClasses: refreshed.value,
@@ -47822,10 +47926,17 @@ class GodotServer {
47822
47926
  async currentRun() {
47823
47927
  return this.activeProcess ?? await this.adoptRecordedRun();
47824
47928
  }
47825
- async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
47929
+ async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}, engineArgs = []) {
47826
47930
  const frames = readPositiveNumber(args, "frames") ?? 3;
47827
47931
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
47828
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
47932
+ const cmdArgs = runArguments({
47933
+ projectPath,
47934
+ headless: true,
47935
+ scene,
47936
+ quitAfter: frames,
47937
+ userArgs,
47938
+ engineArgs
47939
+ });
47829
47940
  const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
47830
47941
  const hung = await new Promise((resolve) => {
47831
47942
  const timer = setTimeout(() => {
@@ -47853,9 +47964,10 @@ class GodotServer {
47853
47964
  frames,
47854
47965
  errors,
47855
47966
  warnings,
47967
+ argsNote: engineFlagsNote(userArgs),
47856
47968
  ...scanWaitAnswer(scanned),
47857
47969
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
47858
- ...foldedEntriesAnswer(boot.log)
47970
+ ...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
47859
47971
  });
47860
47972
  }
47861
47973
  async handleRunProfile(args) {
@@ -49261,6 +49373,7 @@ var init_server = __esm(() => {
49261
49373
  init_issues();
49262
49374
  init_junit();
49263
49375
  init_keeper_channel();
49376
+ init_launch();
49264
49377
  init_lsp_client();
49265
49378
  init_outside();
49266
49379
  init_paths();
package/build/index.js CHANGED
@@ -24913,6 +24913,62 @@ function resolveSilent(requested, variables) {
24913
24913
  const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
24914
24914
  return said !== undefined && said !== "0" && said !== "false";
24915
24915
  }
24916
+ var ENGINE_FLAGS = Object.freeze({
24917
+ "--verbose": "none",
24918
+ "--quiet": "none",
24919
+ "--debug-collisions": "none",
24920
+ "--debug-paths": "none",
24921
+ "--debug-navigation": "none",
24922
+ "--debug-avoidance": "none",
24923
+ "--debug-stringnames": "none",
24924
+ "--debug-canvas-item-redraw": "none",
24925
+ "--debug-mute-audio": "none",
24926
+ "--print-fps": "none",
24927
+ "--gpu-profile": "none",
24928
+ "--gpu-validation": "none",
24929
+ "--gpu-abort": "none",
24930
+ "--disable-vsync": "none",
24931
+ "--frame-delay": "number",
24932
+ "--time-scale": "number",
24933
+ "--fixed-fps": "number",
24934
+ "--max-fps": "number",
24935
+ "--resolution": "size"
24936
+ });
24937
+ var SET_BY_ARGUMENT = Object.freeze({
24938
+ "--headless": "headless",
24939
+ "--audio-driver": "silent",
24940
+ "--quit-after": "frames, on editor_run check"
24941
+ });
24942
+ function engineFlags(given) {
24943
+ const offered = Object.keys(ENGINE_FLAGS).join(", ");
24944
+ for (let at = 0;at < given.length; at += 1) {
24945
+ const flag = given[at] ?? "";
24946
+ const takes = Object.hasOwn(ENGINE_FLAGS, flag) ? ENGINE_FLAGS[flag] : undefined;
24947
+ if (takes === undefined) {
24948
+ const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
24949
+ return {
24950
+ ok: false,
24951
+ reason: setBy !== undefined ? `${flag} is set by this tool's own argument, ${setBy}, rather than through engineArgs.` : `${flag} is not one of the engine flags engineArgs takes, which are those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
24952
+ };
24953
+ }
24954
+ if (takes === "none") {
24955
+ continue;
24956
+ }
24957
+ const value = given[at + 1];
24958
+ const fits = value !== undefined && (takes === "number" ? /^\d+(?:\.\d+)?$/.test(value) : /^\d+x\d+$/.test(value));
24959
+ if (!fits) {
24960
+ return {
24961
+ ok: false,
24962
+ reason: `${flag} takes ${takes === "number" ? "a number" : "a size such as 1280x720"} as the next entry, and ${value === undefined ? "nothing followed it" : `${JSON.stringify(value)} is not one`}.`
24963
+ };
24964
+ }
24965
+ at += 1;
24966
+ }
24967
+ return { ok: true };
24968
+ }
24969
+ function engineFlagsAmongGameArgs(args) {
24970
+ return args.filter((one) => Object.hasOwn(ENGINE_FLAGS, one) || Object.hasOwn(SET_BY_ARGUMENT, one));
24971
+ }
24916
24972
  function runArguments(options) {
24917
24973
  const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
24918
24974
  if (options.silent === true && !options.headless) {
@@ -24921,6 +24977,9 @@ function runArguments(options) {
24921
24977
  if (options.quitAfter !== undefined && options.quitAfter !== null) {
24922
24978
  args.push("--quit-after", String(options.quitAfter));
24923
24979
  }
24980
+ if (options.engineArgs !== undefined) {
24981
+ args.push(...options.engineArgs);
24982
+ }
24924
24983
  if (options.scene !== null) {
24925
24984
  args.push(`res://${options.scene}`);
24926
24985
  }
@@ -34767,7 +34826,13 @@ var TOOL_SPECS = [
34767
34826
  type: "array",
34768
34827
  items: { type: "string" },
34769
34828
  ops: ["start", "check"],
34770
- description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
34829
+ description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here, so a flag the engine reads goes to the game instead, and the answer says so under argsNote. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
34830
+ },
34831
+ engineArgs: {
34832
+ type: "array",
34833
+ items: { type: "string" },
34834
+ ops: ["start", "check"],
34835
+ description: `The engine's own flags, before the separator, such as ["--verbose"] or ["--fixed-fps", "60"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU or how it times frames: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused, since the engine's command line can also run scripts, open other projects and write files. A run with any is started by this server, as one with args is. A check given --verbose answers with everything printed rather than only errors and warnings, since what verbose adds, the instances leaked at exit among it, is printed as ordinary lines.`
34771
34836
  },
34772
34837
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
34773
34838
  visible: {
@@ -36317,6 +36382,14 @@ var PLAYING_STATUS_MS = 1000;
36317
36382
  var SCAN_START_MS = 2000;
36318
36383
  var SCAN_SETTLED_MS = 500;
36319
36384
  var REIMPORT_TIMEOUT_MS = 300000;
36385
+ function engineFlagsNote(args) {
36386
+ const among = engineFlagsAmongGameArgs(args);
36387
+ if (among.length === 0) {
36388
+ return;
36389
+ }
36390
+ const named = among.join(", ");
36391
+ return `${named} ${among.length === 1 ? "is a flag" : "are flags"} the engine reads, and args goes to the game after --, where the engine does not read ${among.length === 1 ? "it" : "them"}. engineArgs hands the engine its own flags; ${among.length === 1 ? "leave it" : "leave them"} in args only for a game that reads ${among.length === 1 ? "that flag" : "those flags"} itself.`;
36392
+ }
36320
36393
  var OPS_ELSEWHERE = {
36321
36394
  runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
36322
36395
  };
@@ -36747,9 +36820,9 @@ function callersAnswer(totals, asked) {
36747
36820
  };
36748
36821
  }
36749
36822
  var MOST_ENGINE_ENTRIES = 200;
36750
- function foldedEntriesAnswer(log) {
36823
+ function foldedEntriesAnswer(log, floor = "warning") {
36751
36824
  const problems = log.selectFolded({
36752
- severity: "warning",
36825
+ severity: floor,
36753
36826
  sinceLastCall: false,
36754
36827
  limit: MOST_ENGINE_ENTRIES
36755
36828
  });
@@ -37777,6 +37850,25 @@ class GodotServer {
37777
37850
  }
37778
37851
  return { ok: true, value: given };
37779
37852
  }
37853
+ engineArguments(args) {
37854
+ if (args["engineArgs"] === undefined) {
37855
+ return { ok: true, value: [] };
37856
+ }
37857
+ const given = readStringArray(args, "engineArgs");
37858
+ if (given === undefined) {
37859
+ return {
37860
+ ok: false,
37861
+ response: this.createErrorResponse("engineArgs must be a list of strings.", [
37862
+ 'Pass engine flags, such as ["--verbose"] or ["--fixed-fps", "60"]'
37863
+ ])
37864
+ };
37865
+ }
37866
+ const checked = engineFlags(given);
37867
+ if (!checked.ok) {
37868
+ return { ok: false, response: this.createErrorResponse(checked.reason) };
37869
+ }
37870
+ return { ok: true, value: given };
37871
+ }
37780
37872
  runEnvironment(args) {
37781
37873
  const savesIn = readNonEmptyString(args, "savesIn");
37782
37874
  if (savesIn !== undefined && !isAbsolute3(savesIn)) {
@@ -39593,6 +39685,10 @@ class GodotServer {
39593
39685
  if (!given.ok) {
39594
39686
  return given.response;
39595
39687
  }
39688
+ const flags = this.engineArguments(args);
39689
+ if (!flags.ok) {
39690
+ return flags.response;
39691
+ }
39596
39692
  const asked = this.runEnvironment(args);
39597
39693
  if (!asked.ok) {
39598
39694
  return asked.response;
@@ -39614,7 +39710,7 @@ class GodotServer {
39614
39710
  }
39615
39711
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
39616
39712
  if (op === "check") {
39617
- return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value);
39713
+ return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value, scanned, asked.value, flags.value);
39618
39714
  }
39619
39715
  await this.pickUpWhatTheEditorIsPlaying();
39620
39716
  let ended = null;
@@ -39641,7 +39737,7 @@ class GodotServer {
39641
39737
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
39642
39738
  const silent = resolveSilent(args["silent"], process.env) && !headless;
39643
39739
  const profiled = readBoolean(args, "profile") === true;
39644
- if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
39740
+ if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && flags.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
39645
39741
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
39646
39742
  }
39647
39743
  const cmdArgs = runArguments({
@@ -39649,6 +39745,7 @@ class GodotServer {
39649
39745
  headless,
39650
39746
  scene: sceneArgument,
39651
39747
  userArgs: given.value,
39748
+ engineArgs: flags.value,
39652
39749
  silent
39653
39750
  });
39654
39751
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
@@ -39661,7 +39758,7 @@ class GodotServer {
39661
39758
  started.announcedBefore = alreadyPlaying;
39662
39759
  started.bootScene = runtimeWaitMs.scene;
39663
39760
  this.activeProcess = started;
39664
- const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && given.value.length > 0 ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
39761
+ const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ? " The editor cannot be handed arguments for a game it plays, so this one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? " The editor plays a game with its own environment, so a run given one was started " + "here: the debug_* tools answer only for a game the editor is playing." : this.godotBridge.isConnected() && editorWouldPlay && silent ? " The editor cannot play a game silent, which takes an engine flag, so this one was " + `started here${args["silent"] === true ? "" : ", silent because GDHARNESS_SILENT says so"}: the debug_* tools answer only for a game the editor is playing, and silent: false plays it through the editor.` : "";
39665
39762
  const endedForThis = endedToStartThis(ended);
39666
39763
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
39667
39764
  const alsoRunning = leftRunningNote(stranded);
@@ -39671,6 +39768,7 @@ class GodotServer {
39671
39768
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
39672
39769
  pid: started.pid,
39673
39770
  arguments: cmdArgs,
39771
+ argsNote: engineFlagsNote(given.value),
39674
39772
  transcript: started.transcript,
39675
39773
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
39676
39774
  refreshedClasses: refreshed.value,
@@ -40573,10 +40671,17 @@ class GodotServer {
40573
40671
  async currentRun() {
40574
40672
  return this.activeProcess ?? await this.adoptRecordedRun();
40575
40673
  }
40576
- async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
40674
+ async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}, engineArgs = []) {
40577
40675
  const frames = readPositiveNumber(args, "frames") ?? 3;
40578
40676
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
40579
- const cmdArgs = runArguments({ projectPath, headless: true, scene, quitAfter: frames, userArgs });
40677
+ const cmdArgs = runArguments({
40678
+ projectPath,
40679
+ headless: true,
40680
+ scene,
40681
+ quitAfter: frames,
40682
+ userArgs,
40683
+ engineArgs
40684
+ });
40580
40685
  const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
40581
40686
  const hung = await new Promise((resolve) => {
40582
40687
  const timer = setTimeout(() => {
@@ -40604,9 +40709,10 @@ class GodotServer {
40604
40709
  frames,
40605
40710
  errors,
40606
40711
  warnings,
40712
+ argsNote: engineFlagsNote(userArgs),
40607
40713
  ...scanWaitAnswer(scanned),
40608
40714
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
40609
- ...foldedEntriesAnswer(boot.log)
40715
+ ...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
40610
40716
  });
40611
40717
  }
40612
40718
  async handleRunProfile(args) {
package/build/keeper.js CHANGED
@@ -364,6 +364,32 @@ function envValue(name, variables = process.env) {
364
364
  const value = variables[name];
365
365
  return value === undefined || value === "" ? undefined : value;
366
366
  }
367
+ var ENGINE_FLAGS = Object.freeze({
368
+ "--verbose": "none",
369
+ "--quiet": "none",
370
+ "--debug-collisions": "none",
371
+ "--debug-paths": "none",
372
+ "--debug-navigation": "none",
373
+ "--debug-avoidance": "none",
374
+ "--debug-stringnames": "none",
375
+ "--debug-canvas-item-redraw": "none",
376
+ "--debug-mute-audio": "none",
377
+ "--print-fps": "none",
378
+ "--gpu-profile": "none",
379
+ "--gpu-validation": "none",
380
+ "--gpu-abort": "none",
381
+ "--disable-vsync": "none",
382
+ "--frame-delay": "number",
383
+ "--time-scale": "number",
384
+ "--fixed-fps": "number",
385
+ "--max-fps": "number",
386
+ "--resolution": "size"
387
+ });
388
+ var SET_BY_ARGUMENT = Object.freeze({
389
+ "--headless": "headless",
390
+ "--audio-driver": "silent",
391
+ "--quit-after": "frames, on editor_run check"
392
+ });
367
393
 
368
394
  // src/runtime-client.ts
369
395
  function runtimeDirectory(variables = process.env) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.18",
3
+ "version": "1.2.19",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",