gdharness 1.2.17 → 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
 
@@ -10793,6 +10855,10 @@ function describeOperations(spec) {
10793
10855
  const fallback = spec.defaultOperation ? ` Default ${spec.defaultOperation}.` : "";
10794
10856
  return ` Operations: ${lines.join("; ")}.${fallback}`;
10795
10857
  }
10858
+ function servedDescription(spec) {
10859
+ const answer = spec.answer === undefined ? "" : " The tool reference in the gdharness skill, and the site, say what every field of the answer means.";
10860
+ return `${spec.description}${describeOperations(spec)}${answer}`;
10861
+ }
10796
10862
  function buildToolDefinitions() {
10797
10863
  return TOOL_SPECS.map((spec) => {
10798
10864
  const properties = {};
@@ -10814,7 +10880,7 @@ function buildToolDefinitions() {
10814
10880
  return {
10815
10881
  name: spec.name,
10816
10882
  title: spec.title,
10817
- description: `${spec.description}${describeOperations(spec)}`,
10883
+ description: servedDescription(spec),
10818
10884
  inputSchema: {
10819
10885
  type: "object",
10820
10886
  properties,
@@ -10836,6 +10902,7 @@ function annotationsOf(spec) {
10836
10902
  }
10837
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;
10838
10904
  var init_tool_definitions = __esm(() => {
10905
+ init_launch();
10839
10906
  PROJECT_INFO_SECTIONS = [
10840
10907
  "autoloads",
10841
10908
  "plugins",
@@ -11754,7 +11821,13 @@ var init_tool_definitions = __esm(() => {
11754
11821
  type: "array",
11755
11822
  items: { type: "string" },
11756
11823
  ops: ["start", "check"],
11757
- 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.`
11758
11831
  },
11759
11832
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
11760
11833
  visible: {
@@ -11832,12 +11905,12 @@ var init_tool_definitions = __esm(() => {
11832
11905
  },
11833
11906
  stop: {
11834
11907
  effect: "destructive",
11835
- summary: "end the run and answer with the errors and warnings it printed under entries (editor_output has everything else it printed), naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause",
11908
+ summary: "end the run, naming the process ended under endedPid, and answer with the errors and warnings it printed under entries (editor_output has the rest); a game that started processes of its own keeps them unless andChildren says to end those too",
11836
11909
  requires: []
11837
11910
  },
11838
11911
  check: {
11839
11912
  effect: "additive",
11840
- summary: "boot headless, quit after a few frames, and report the verdict. booted is that verdict rather than whether the game came up: true only for a run that quit on its own within timeoutMs, exited 0 and printed no error, so a game that reached _ready and then raised an error answers false. hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together",
11913
+ summary: "boot headless, quit after a few frames, and report the verdict: booted is true only for a run that quit on its own within timeoutMs, exited 0 and printed no error, so a game that reached _ready and then raised an error answers false",
11841
11914
  requires: ["projectPath"]
11842
11915
  },
11843
11916
  wait: {
@@ -11847,10 +11920,11 @@ var init_tool_definitions = __esm(() => {
11847
11920
  },
11848
11921
  profile: {
11849
11922
  effect: "read",
11850
- summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
11923
+ summary: "where the run spends its time by GDScript function, while it runs or after it ended, sorted by self time. A run started with profile: true is profiled from its start; a game the editor plays that was not is profiled from the moment this is asked; a run this server started without it is refused, since its debugger is fixed as it starts",
11851
11924
  requires: []
11852
11925
  }
11853
11926
  },
11927
+ answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
11854
11928
  defaultOperation: "start"
11855
11929
  },
11856
11930
  {
@@ -11908,7 +11982,8 @@ var init_tool_definitions = __esm(() => {
11908
11982
  name: "editor_status",
11909
11983
  title: "Editor status",
11910
11984
  effect: "read",
11911
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
11985
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on the editor and on each game, says it runs addon code other than what this server ships, and staleNote says which half moves: gdharness upgrade in the project, a restart, or a reconnect of this server. A false editor.lspListening or editor.dapListening comes with serversNote, and editor_launch restart binds the port again. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
11986
+ answer: "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
11912
11987
  parameters: {},
11913
11988
  requires: []
11914
11989
  },
@@ -12215,6 +12290,7 @@ var init_tool_definitions = __esm(() => {
12215
12290
  name: "runtime_input",
12216
12291
  title: "Send input to the game",
12217
12292
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
12293
+ answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens.',
12218
12294
  parameters: {
12219
12295
  projectPath: RUNNING_PROJECT_PATH,
12220
12296
  pid: RUNNING_PID,
@@ -12304,12 +12380,12 @@ var init_tool_definitions = __esm(() => {
12304
12380
  operations: {
12305
12381
  click: {
12306
12382
  effect: "additive",
12307
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. So is a control whose pointer a mouse_behavior_recursive set to disabled on it or above it has turned off, naming where it was set; by says, one the pointer can reach is picked over it. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
12383
+ summary: "press and release on a Control, a frame apart, named by nodePath or by says with the words on it (found under nodePath when both are given), or on a 3D node where it is drawn. A control out of sight inside a ScrollContainer is scrolled to first, and a link in a RichTextLabel is pressed where its words are drawn. A disabled button, a control that does not process and one whose pointer mouse_behavior_recursive turned off are refused, since the press would do nothing; by says, one the pointer can reach is picked over it",
12308
12384
  requires: []
12309
12385
  },
12310
12386
  choose: {
12311
12387
  effect: "additive",
12312
- summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the menu is opened, the item is looked for in it as it stands then, since a game can fill a menu as it opens, and the item takes the focus and ui_accept presses it, which is the engine's own path, needs no window, and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take, which is heard rather than assumed. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
12388
+ summary: "take an item out of a menu, by what it says or by where it is in the list: the menu is opened, the item found in it as it stands then, and pressed through the engine's own path, which needs no window and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take",
12313
12389
  requires: ["nodePath"]
12314
12390
  },
12315
12391
  action: { effect: "additive", summary: "press an action, or hold it", requires: ["action"] },
@@ -12585,6 +12661,9 @@ function renderToolsMarkdown() {
12585
12661
  ];
12586
12662
  for (const tool of TOOL_SPECS) {
12587
12663
  lines.push(`## ${tool.name}`, "", `${tool.title}. ${whatItCanChange(tool)}`, "", tool.description, "");
12664
+ if (tool.answer !== undefined) {
12665
+ lines.push(`The answer: ${tool.answer}`, "");
12666
+ }
12588
12667
  const operations = Object.entries(tool.operations ?? {});
12589
12668
  if (operations.length > 0) {
12590
12669
  for (const [op, spec] of operations) {
@@ -34724,6 +34803,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
34724
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;
34725
34804
  var init_runtime_client = __esm(() => {
34726
34805
  init_alive();
34806
+ init_launch();
34727
34807
  init_paths();
34728
34808
  init_process_children();
34729
34809
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
@@ -40736,6 +40816,7 @@ var init_lsp_client = __esm(() => {
40736
40816
  init_answer_json();
40737
40817
  init_errors();
40738
40818
  init_framing();
40819
+ init_launch();
40739
40820
  init_paths();
40740
40821
  init_ports();
40741
40822
  LSPTimeout = class LSPTimeout extends Error {
@@ -43608,6 +43689,19 @@ function scanWaitAnswer(scanned, started = "game") {
43608
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."
43609
43690
  };
43610
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
+ }
43700
+ function opElsewhere(tool, requested) {
43701
+ const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
43702
+ const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
43703
+ return where === undefined ? "" : ` A value is read with ${where}.`;
43704
+ }
43611
43705
  function changedSettings(before, after) {
43612
43706
  if (before === null || after === null) {
43613
43707
  return null;
@@ -43984,9 +44078,9 @@ function callersAnswer(totals, asked) {
43984
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
43985
44079
  };
43986
44080
  }
43987
- function foldedEntriesAnswer(log) {
44081
+ function foldedEntriesAnswer(log, floor = "warning") {
43988
44082
  const problems = log.selectFolded({
43989
- severity: "warning",
44083
+ severity: floor,
43990
44084
  sinceLastCall: false,
43991
44085
  limit: MOST_ENGINE_ENTRIES
43992
44086
  });
@@ -44662,7 +44756,7 @@ class GodotServer {
44662
44756
  } else {
44663
44757
  return {
44664
44758
  ok: false,
44665
- response: this.createErrorResponse(`${spec.name} has no op ${JSON.stringify(requested)}. Valid ops: ${valid.join(", ")}.`)
44759
+ response: this.createErrorResponse(`${spec.name} has no op ${JSON.stringify(requested)}. Valid ops: ${valid.join(", ")}.${opElsewhere(spec.name, requested)}`)
44666
44760
  };
44667
44761
  }
44668
44762
  }
@@ -45011,6 +45105,25 @@ class GodotServer {
45011
45105
  }
45012
45106
  return { ok: true, value: given };
45013
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
+ }
45014
45127
  runEnvironment(args) {
45015
45128
  const savesIn = readNonEmptyString(args, "savesIn");
45016
45129
  if (savesIn !== undefined && !isAbsolute3(savesIn)) {
@@ -46827,6 +46940,10 @@ class GodotServer {
46827
46940
  if (!given.ok) {
46828
46941
  return given.response;
46829
46942
  }
46943
+ const flags = this.engineArguments(args);
46944
+ if (!flags.ok) {
46945
+ return flags.response;
46946
+ }
46830
46947
  const asked = this.runEnvironment(args);
46831
46948
  if (!asked.ok) {
46832
46949
  return asked.response;
@@ -46848,7 +46965,7 @@ class GodotServer {
46848
46965
  }
46849
46966
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
46850
46967
  if (op === "check") {
46851
- 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);
46852
46969
  }
46853
46970
  await this.pickUpWhatTheEditorIsPlaying();
46854
46971
  let ended = null;
@@ -46875,7 +46992,7 @@ class GodotServer {
46875
46992
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
46876
46993
  const silent = resolveSilent(args["silent"], process.env) && !headless;
46877
46994
  const profiled = readBoolean(args, "profile") === true;
46878
- 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) {
46879
46996
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
46880
46997
  }
46881
46998
  const cmdArgs = runArguments({
@@ -46883,6 +47000,7 @@ class GodotServer {
46883
47000
  headless,
46884
47001
  scene: sceneArgument,
46885
47002
  userArgs: given.value,
47003
+ engineArgs: flags.value,
46886
47004
  silent
46887
47005
  });
46888
47006
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
@@ -46895,7 +47013,7 @@ class GodotServer {
46895
47013
  started.announcedBefore = alreadyPlaying;
46896
47014
  started.bootScene = runtimeWaitMs.scene;
46897
47015
  this.activeProcess = started;
46898
- 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.` : "";
46899
47017
  const endedForThis = endedToStartThis(ended);
46900
47018
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
46901
47019
  const alsoRunning = leftRunningNote(stranded);
@@ -46905,6 +47023,7 @@ class GodotServer {
46905
47023
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
46906
47024
  pid: started.pid,
46907
47025
  arguments: cmdArgs,
47026
+ argsNote: engineFlagsNote(given.value),
46908
47027
  transcript: started.transcript,
46909
47028
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
46910
47029
  refreshedClasses: refreshed.value,
@@ -47807,10 +47926,17 @@ class GodotServer {
47807
47926
  async currentRun() {
47808
47927
  return this.activeProcess ?? await this.adoptRecordedRun();
47809
47928
  }
47810
- async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
47929
+ async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}, engineArgs = []) {
47811
47930
  const frames = readPositiveNumber(args, "frames") ?? 3;
47812
47931
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
47813
- 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
+ });
47814
47940
  const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
47815
47941
  const hung = await new Promise((resolve) => {
47816
47942
  const timer = setTimeout(() => {
@@ -47838,9 +47964,10 @@ class GodotServer {
47838
47964
  frames,
47839
47965
  errors,
47840
47966
  warnings,
47967
+ argsNote: engineFlagsNote(userArgs),
47841
47968
  ...scanWaitAnswer(scanned),
47842
47969
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
47843
- ...foldedEntriesAnswer(boot.log)
47970
+ ...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
47844
47971
  });
47845
47972
  }
47846
47973
  async handleRunProfile(args) {
@@ -49218,7 +49345,7 @@ async function runGodotServer() {
49218
49345
  const server = new GodotServer;
49219
49346
  await server.run();
49220
49347
  }
49221
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, 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, 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;
49348
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run4, __dirname2, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, 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;
49222
49349
  var init_server = __esm(() => {
49223
49350
  init_dist();
49224
49351
  init_stdio();
@@ -49246,6 +49373,7 @@ var init_server = __esm(() => {
49246
49373
  init_issues();
49247
49374
  init_junit();
49248
49375
  init_keeper_channel();
49376
+ init_launch();
49249
49377
  init_lsp_client();
49250
49378
  init_outside();
49251
49379
  init_paths();
@@ -49281,6 +49409,9 @@ var init_server = __esm(() => {
49281
49409
  object: "an object"
49282
49410
  };
49283
49411
  HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
49412
+ OPS_ELSEWHERE = {
49413
+ runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
49414
+ };
49284
49415
  AUDIO_BUS_OPS = new Set([
49285
49416
  "add_audio_bus",
49286
49417
  "remove_audio_bus",
@@ -135,6 +135,24 @@ static func make_folder_for(file_path: String) -> String:
135
135
  return "" if made == OK else "Could not make the folder %s: %s" % [folder, error_string(made)]
136
136
 
137
137
 
138
+ ## Why [param script], loaded from [param script_path], cannot be attached because the editor could not
139
+ ## compile it, or "". A script that did not compile has no base type, and the refusal built from that
140
+ ## said "start.gd extends , so it cannot be on a Control root" about a script that was fine, whose
141
+ ## failure was a class it called being behind in the editor.
142
+ static func uncompiled(script: Script, script_path: String) -> String:
143
+ if not str(script.get_instance_base_type()).is_empty():
144
+ return ""
145
+ return (
146
+ (
147
+ "%s does not compile in the editor, so it cannot be attached. script_diagnostics on it gives "
148
+ + "the parser's errors. If they deny a member that another class's file declares, the "
149
+ + "editor's copy of that class is behind: editor_rescan with reloadScript on that class, then "
150
+ + "ask again."
151
+ )
152
+ % script_path
153
+ )
154
+
155
+
138
156
  ## Why no node can be called [param node_name], wherever it sits, or "".
139
157
  static func unfit_name(node_name: String) -> String:
140
158
  if node_name.is_empty():
@@ -174,6 +174,9 @@ func create_resource(args: Dictionary) -> Dictionary:
174
174
  var script: Script = load(script_file) as Script
175
175
  if script == null:
176
176
  return {"ok": false, "error": "No script loads from " + script_file}
177
+ var uncompiled: String = SceneFile.uncompiled(script, script_file)
178
+ if not uncompiled.is_empty():
179
+ return {"ok": false, "error": uncompiled}
177
180
  var base: String = str(script.get_instance_base_type())
178
181
  if not ClassDB.is_parent_class(resource_type, base):
179
182
  return {
@@ -313,6 +313,10 @@ func create_scene(args: Dictionary) -> Dictionary:
313
313
  if script == null:
314
314
  root.free()
315
315
  return {"ok": false, "error": "No script loads from " + full_script_path}
316
+ var uncompiled: String = SceneFile.uncompiled(script, full_script_path)
317
+ if not uncompiled.is_empty():
318
+ root.free()
319
+ return {"ok": false, "error": uncompiled}
316
320
  var base: String = str(script.get_instance_base_type())
317
321
  if not ClassDB.is_parent_class(root_node_type, base):
318
322
  root.free()
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: {
@@ -34845,12 +34910,12 @@ var TOOL_SPECS = [
34845
34910
  },
34846
34911
  stop: {
34847
34912
  effect: "destructive",
34848
- summary: "end the run and answer with the errors and warnings it printed under entries (editor_output has everything else it printed), naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause",
34913
+ summary: "end the run, naming the process ended under endedPid, and answer with the errors and warnings it printed under entries (editor_output has the rest); a game that started processes of its own keeps them unless andChildren says to end those too",
34849
34914
  requires: []
34850
34915
  },
34851
34916
  check: {
34852
34917
  effect: "additive",
34853
- summary: "boot headless, quit after a few frames, and report the verdict. booted is that verdict rather than whether the game came up: true only for a run that quit on its own within timeoutMs, exited 0 and printed no error, so a game that reached _ready and then raised an error answers false. hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together",
34918
+ summary: "boot headless, quit after a few frames, and report the verdict: booted is true only for a run that quit on its own within timeoutMs, exited 0 and printed no error, so a game that reached _ready and then raised an error answers false",
34854
34919
  requires: ["projectPath"]
34855
34920
  },
34856
34921
  wait: {
@@ -34860,10 +34925,11 @@ var TOOL_SPECS = [
34860
34925
  },
34861
34926
  profile: {
34862
34927
  effect: "read",
34863
- summary: "where the run spends its time by GDScript function, while it runs or after it ended: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, sorted by self time, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A run started with profile: true is profiled from its start. A game the editor plays that was not is profiled from the moment this is asked, and the answer says so; a run this server started without it is refused, since its debugger is fixed as it starts",
34928
+ summary: "where the run spends its time by GDScript function, while it runs or after it ended, sorted by self time. A run started with profile: true is profiled from its start; a game the editor plays that was not is profiled from the moment this is asked; a run this server started without it is refused, since its debugger is fixed as it starts",
34864
34929
  requires: []
34865
34930
  }
34866
34931
  },
34932
+ answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked.",
34867
34933
  defaultOperation: "start"
34868
34934
  },
34869
34935
  {
@@ -34921,7 +34987,8 @@ var TOOL_SPECS = [
34921
34987
  name: "editor_status",
34922
34988
  title: "Editor status",
34923
34989
  effect: "read",
34924
- description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34990
+ description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor; mayYetConnect true is worth waiting out, false is final, and restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on the editor and on each game, says it runs addon code other than what this server ships, and staleNote says which half moves: gdharness upgrade in the project, a restart, or a reconnect of this server. A false editor.lspListening or editor.dapListening comes with serversNote, and editor_launch restart binds the port again. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
34991
+ answer: "runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed; null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote, and editor_launch restart binds it again. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window.",
34925
34992
  parameters: {},
34926
34993
  requires: []
34927
34994
  },
@@ -35228,6 +35295,7 @@ var TOOL_SPECS = [
35228
35295
  name: "runtime_input",
35229
35296
  title: "Send input to the game",
35230
35297
  description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button or mouse motion. All of it works headless, where the window is 64 by 64 and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position.",
35298
+ answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens.',
35231
35299
  parameters: {
35232
35300
  projectPath: RUNNING_PROJECT_PATH,
35233
35301
  pid: RUNNING_PID,
@@ -35317,12 +35385,12 @@ var TOOL_SPECS = [
35317
35385
  operations: {
35318
35386
  click: {
35319
35387
  effect: "additive",
35320
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. So is a control whose pointer a mouse_behavior_recursive set to disabled on it or above it has turned off, naming where it was set; by says, one the pointer can reach is picked over it. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
35388
+ summary: "press and release on a Control, a frame apart, named by nodePath or by says with the words on it (found under nodePath when both are given), or on a 3D node where it is drawn. A control out of sight inside a ScrollContainer is scrolled to first, and a link in a RichTextLabel is pressed where its words are drawn. A disabled button, a control that does not process and one whose pointer mouse_behavior_recursive turned off are refused, since the press would do nothing; by says, one the pointer can reach is picked over it",
35321
35389
  requires: []
35322
35390
  },
35323
35391
  choose: {
35324
35392
  effect: "additive",
35325
- summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the menu is opened, the item is looked for in it as it stands then, since a game can fill a menu as it opens, and the item takes the focus and ui_accept presses it, which is the engine's own path, needs no window, and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take, which is heard rather than assumed. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
35393
+ summary: "take an item out of a menu, by what it says or by where it is in the list: the menu is opened, the item found in it as it stands then, and pressed through the engine's own path, which needs no window and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take",
35326
35394
  requires: ["nodePath"]
35327
35395
  },
35328
35396
  action: { effect: "additive", summary: "press an action, or hold it", requires: ["action"] },
@@ -35486,6 +35554,10 @@ function describeOperations(spec) {
35486
35554
  const fallback = spec.defaultOperation ? ` Default ${spec.defaultOperation}.` : "";
35487
35555
  return ` Operations: ${lines.join("; ")}.${fallback}`;
35488
35556
  }
35557
+ function servedDescription(spec) {
35558
+ const answer = spec.answer === undefined ? "" : " The tool reference in the gdharness skill, and the site, say what every field of the answer means.";
35559
+ return `${spec.description}${describeOperations(spec)}${answer}`;
35560
+ }
35489
35561
  function buildToolDefinitions() {
35490
35562
  return TOOL_SPECS.map((spec) => {
35491
35563
  const properties = {};
@@ -35507,7 +35579,7 @@ function buildToolDefinitions() {
35507
35579
  return {
35508
35580
  name: spec.name,
35509
35581
  title: spec.title,
35510
- description: `${spec.description}${describeOperations(spec)}`,
35582
+ description: servedDescription(spec),
35511
35583
  inputSchema: {
35512
35584
  type: "object",
35513
35585
  properties,
@@ -36310,6 +36382,22 @@ var PLAYING_STATUS_MS = 1000;
36310
36382
  var SCAN_START_MS = 2000;
36311
36383
  var SCAN_SETTLED_MS = 500;
36312
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
+ }
36393
+ var OPS_ELSEWHERE = {
36394
+ runtime_invoke: { get: "runtime_inspect property", property: "runtime_inspect property" }
36395
+ };
36396
+ function opElsewhere(tool, requested) {
36397
+ const ops = Object.hasOwn(OPS_ELSEWHERE, tool) ? OPS_ELSEWHERE[tool] : undefined;
36398
+ const where = ops !== undefined && typeof requested === "string" && Object.hasOwn(ops, requested) ? ops[requested] : undefined;
36399
+ return where === undefined ? "" : ` A value is read with ${where}.`;
36400
+ }
36313
36401
  var AUDIO_BUS_OPS = new Set([
36314
36402
  "add_audio_bus",
36315
36403
  "remove_audio_bus",
@@ -36732,9 +36820,9 @@ function callersAnswer(totals, asked) {
36732
36820
  };
36733
36821
  }
36734
36822
  var MOST_ENGINE_ENTRIES = 200;
36735
- function foldedEntriesAnswer(log) {
36823
+ function foldedEntriesAnswer(log, floor = "warning") {
36736
36824
  const problems = log.selectFolded({
36737
- severity: "warning",
36825
+ severity: floor,
36738
36826
  sinceLastCall: false,
36739
36827
  limit: MOST_ENGINE_ENTRIES
36740
36828
  });
@@ -37413,7 +37501,7 @@ class GodotServer {
37413
37501
  } else {
37414
37502
  return {
37415
37503
  ok: false,
37416
- response: this.createErrorResponse(`${spec.name} has no op ${JSON.stringify(requested)}. Valid ops: ${valid.join(", ")}.`)
37504
+ response: this.createErrorResponse(`${spec.name} has no op ${JSON.stringify(requested)}. Valid ops: ${valid.join(", ")}.${opElsewhere(spec.name, requested)}`)
37417
37505
  };
37418
37506
  }
37419
37507
  }
@@ -37762,6 +37850,25 @@ class GodotServer {
37762
37850
  }
37763
37851
  return { ok: true, value: given };
37764
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
+ }
37765
37872
  runEnvironment(args) {
37766
37873
  const savesIn = readNonEmptyString(args, "savesIn");
37767
37874
  if (savesIn !== undefined && !isAbsolute3(savesIn)) {
@@ -39578,6 +39685,10 @@ class GodotServer {
39578
39685
  if (!given.ok) {
39579
39686
  return given.response;
39580
39687
  }
39688
+ const flags = this.engineArguments(args);
39689
+ if (!flags.ok) {
39690
+ return flags.response;
39691
+ }
39581
39692
  const asked = this.runEnvironment(args);
39582
39693
  if (!asked.ok) {
39583
39694
  return asked.response;
@@ -39599,7 +39710,7 @@ class GodotServer {
39599
39710
  }
39600
39711
  const sceneArgument = sceneToRun?.ok ? sceneToRun.relativePath : null;
39601
39712
  if (op === "check") {
39602
- 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);
39603
39714
  }
39604
39715
  await this.pickUpWhatTheEditorIsPlaying();
39605
39716
  let ended = null;
@@ -39626,7 +39737,7 @@ class GodotServer {
39626
39737
  const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
39627
39738
  const silent = resolveSilent(args["silent"], process.env) && !headless;
39628
39739
  const profiled = readBoolean(args, "profile") === true;
39629
- 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) {
39630
39741
  return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended, profiled);
39631
39742
  }
39632
39743
  const cmdArgs = runArguments({
@@ -39634,6 +39745,7 @@ class GodotServer {
39634
39745
  headless,
39635
39746
  scene: sceneArgument,
39636
39747
  userArgs: given.value,
39748
+ engineArgs: flags.value,
39637
39749
  silent
39638
39750
  });
39639
39751
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
@@ -39646,7 +39758,7 @@ class GodotServer {
39646
39758
  started.announcedBefore = alreadyPlaying;
39647
39759
  started.bootScene = runtimeWaitMs.scene;
39648
39760
  this.activeProcess = started;
39649
- 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.` : "";
39650
39762
  const endedForThis = endedToStartThis(ended);
39651
39763
  const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
39652
39764
  const alsoRunning = leftRunningNote(stranded);
@@ -39656,6 +39768,7 @@ class GodotServer {
39656
39768
  alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
39657
39769
  pid: started.pid,
39658
39770
  arguments: cmdArgs,
39771
+ argsNote: engineFlagsNote(given.value),
39659
39772
  transcript: started.transcript,
39660
39773
  profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
39661
39774
  refreshedClasses: refreshed.value,
@@ -40558,10 +40671,17 @@ class GodotServer {
40558
40671
  async currentRun() {
40559
40672
  return this.activeProcess ?? await this.adoptRecordedRun();
40560
40673
  }
40561
- async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}) {
40674
+ async checkBoot(godotPath, projectPath, scene, args, userArgs, scanned, asked = {}, engineArgs = []) {
40562
40675
  const frames = readPositiveNumber(args, "frames") ?? 3;
40563
40676
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 60000;
40564
- 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
+ });
40565
40685
  const boot = this.spawnGame(godotPath, cmdArgs, needsItsOwnProcess(asked) ? environmentFor({ ...asked, runtimeDirectory: runtimeDirectory() }) : undefined);
40566
40686
  const hung = await new Promise((resolve) => {
40567
40687
  const timer = setTimeout(() => {
@@ -40589,9 +40709,10 @@ class GodotServer {
40589
40709
  frames,
40590
40710
  errors,
40591
40711
  warnings,
40712
+ argsNote: engineFlagsNote(userArgs),
40592
40713
  ...scanWaitAnswer(scanned),
40593
40714
  savesNote: asked.savesIn !== undefined && savesStayPut() ? SAVES_NOT_MOVED_NOTE : undefined,
40594
- ...foldedEntriesAnswer(boot.log)
40715
+ ...foldedEntriesAnswer(boot.log, engineArgs.includes("--verbose") ? "info" : "warning")
40595
40716
  });
40596
40717
  }
40597
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.17",
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",