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