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 +150 -19
- package/build/godot/addons/gdharness_editor/scene_file.gd +18 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +4 -0
- package/build/index.js +138 -17
- package/build/keeper.js +26 -0
- package/package.json +1 -1
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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({
|
|
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.
|
|
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",
|