gdharness 1.1.14 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -605,8 +605,18 @@ function resolveHeadless(requested, host) {
|
|
|
605
605
|
}
|
|
606
606
|
return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
|
|
607
607
|
}
|
|
608
|
+
function resolveSilent(requested, variables) {
|
|
609
|
+
if (typeof requested === "boolean") {
|
|
610
|
+
return requested;
|
|
611
|
+
}
|
|
612
|
+
const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
|
|
613
|
+
return said !== undefined && said !== "0" && said !== "false";
|
|
614
|
+
}
|
|
608
615
|
function runArguments(options) {
|
|
609
616
|
const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
|
|
617
|
+
if (options.silent === true && !options.headless) {
|
|
618
|
+
args.push("--audio-driver", "Dummy");
|
|
619
|
+
}
|
|
610
620
|
if (options.quitAfter !== undefined && options.quitAfter !== null) {
|
|
611
621
|
args.push("--quit-after", String(options.quitAfter));
|
|
612
622
|
}
|
|
@@ -8561,20 +8571,57 @@ function withAddonClassesCounted(rebuilt, listed) {
|
|
|
8561
8571
|
return counted;
|
|
8562
8572
|
}
|
|
8563
8573
|
function missingMemberIn(message) {
|
|
8564
|
-
const
|
|
8565
|
-
if (
|
|
8574
|
+
const inferred = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
|
|
8575
|
+
if (inferred !== null) {
|
|
8576
|
+
return {
|
|
8577
|
+
kind: inferred[1] === "property" ? "property" : "method",
|
|
8578
|
+
member: inferred[2] ?? "",
|
|
8579
|
+
type: inferred[3] ?? ""
|
|
8580
|
+
};
|
|
8581
|
+
}
|
|
8582
|
+
const called = /Static function "([A-Za-z_]\w*)\(\)" not found in base "([A-Za-z_]\w*)"/.exec(message);
|
|
8583
|
+
if (called !== null) {
|
|
8584
|
+
return { kind: "static method", member: called[1] ?? "", type: called[2] ?? "" };
|
|
8585
|
+
}
|
|
8586
|
+
const inBase = /Cannot find member "([A-Za-z_]\w*)" in base "([A-Za-z_]\w*)((?:\.[A-Za-z_]\w*)*)"/.exec(message);
|
|
8587
|
+
if (inBase === null) {
|
|
8566
8588
|
return null;
|
|
8567
8589
|
}
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
};
|
|
8590
|
+
const member = inBase[1] ?? "";
|
|
8591
|
+
const type = inBase[2] ?? "";
|
|
8592
|
+
const within = (inBase[3] ?? "").split(".").filter((part) => part !== "");
|
|
8593
|
+
const enumName = within.at(-1);
|
|
8594
|
+
return enumName === undefined ? { kind: "member", member, type } : { kind: "enum member", member, type, enum: enumName };
|
|
8573
8595
|
}
|
|
8574
8596
|
function declaresMember(source, missing) {
|
|
8575
8597
|
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
8576
|
-
|
|
8577
|
-
|
|
8598
|
+
if (missing.kind === "method") {
|
|
8599
|
+
return new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m").test(source);
|
|
8600
|
+
}
|
|
8601
|
+
if (missing.kind === "static method") {
|
|
8602
|
+
return new RegExp(String.raw`^\s*static\s+func\s+${name}\s*\(`, "m").test(source);
|
|
8603
|
+
}
|
|
8604
|
+
if (missing.kind === "enum member") {
|
|
8605
|
+
return enumValues(source, missing.enum ?? "").includes(missing.member);
|
|
8606
|
+
}
|
|
8607
|
+
const kinds = missing.kind === "property" ? "var|const|enum|class|signal|func" : "var|const|enum|class|signal";
|
|
8608
|
+
return new RegExp(String.raw`^\s*(?:static\s+)?(?:@\w+(?:\([^)]*\))?\s+)*(?:${kinds})\s+${name}\b`, "m").test(source) || enumValues(source, null).includes(missing.member);
|
|
8609
|
+
}
|
|
8610
|
+
function enumValues(source, named) {
|
|
8611
|
+
const opening = named === null ? /^\s*enum\s*\{([^}]*)\}/gm : new RegExp(String.raw`^\s*enum\s+${named.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\s*\{([^}]*)\}`, "gm");
|
|
8612
|
+
const values = [];
|
|
8613
|
+
for (const [, body] of source.matchAll(opening)) {
|
|
8614
|
+
const uncommented = (body ?? "").split(`
|
|
8615
|
+
`).map((line) => line.replace(/#.*$/, "")).join(`
|
|
8616
|
+
`);
|
|
8617
|
+
for (const part of uncommented.split(",")) {
|
|
8618
|
+
const value = /^\s*([A-Za-z_]\w*)/.exec(part)?.[1];
|
|
8619
|
+
if (value !== undefined) {
|
|
8620
|
+
values.push(value);
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
}
|
|
8624
|
+
return values;
|
|
8578
8625
|
}
|
|
8579
8626
|
function unknownTypeIn(message) {
|
|
8580
8627
|
const found = /(?:Could not find (?:base class|type)|Cannot find class|Identifier) "?([A-Za-z_][A-Za-z0-9_]*)"?(?: not declared| in the current scope|\b)/.exec(message);
|
|
@@ -8605,7 +8652,7 @@ function classesNamedIn(source, known) {
|
|
|
8605
8652
|
}
|
|
8606
8653
|
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
8607
8654
|
const read = new Map;
|
|
8608
|
-
const found =
|
|
8655
|
+
const found = new Map;
|
|
8609
8656
|
for (const message of messages) {
|
|
8610
8657
|
const missing = missingMemberIn(message);
|
|
8611
8658
|
const declaredIn = missing === null ? undefined : classes.get(missing.type);
|
|
@@ -8616,20 +8663,48 @@ function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
|
8616
8663
|
read.set(declaredIn, sourceOf(declaredIn));
|
|
8617
8664
|
}
|
|
8618
8665
|
const source = read.get(declaredIn) ?? null;
|
|
8619
|
-
|
|
8620
|
-
|
|
8666
|
+
const key = [missing.type, missing.enum ?? "", missing.member].join(".");
|
|
8667
|
+
const already = found.get(key);
|
|
8668
|
+
if (source !== null && declaresMember(source, missing) && already?.kind !== "static method") {
|
|
8669
|
+
found.set(key, { ...missing, declaredIn });
|
|
8621
8670
|
}
|
|
8622
8671
|
}
|
|
8623
|
-
return found;
|
|
8672
|
+
return [...found.values()];
|
|
8624
8673
|
}
|
|
8625
8674
|
function staleAnalysisNote(contradicted, dependsOn) {
|
|
8626
8675
|
if (contradicted.length === 0) {
|
|
8627
8676
|
return "";
|
|
8628
8677
|
}
|
|
8629
8678
|
const types = new Set(contradicted.map((one) => one.type));
|
|
8630
|
-
const
|
|
8679
|
+
const onInstances = contradicted.filter((one) => one.kind === "method" || one.kind === "property");
|
|
8680
|
+
const onTheClass = contradicted.filter((one) => one.kind === "member" || one.kind === "static method" || one.kind === "enum member");
|
|
8681
|
+
return [
|
|
8682
|
+
`The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written.",
|
|
8683
|
+
instanceRemedy(onInstances, dependsOn, onTheClass.length > 0),
|
|
8684
|
+
classRemedy(onTheClass)
|
|
8685
|
+
].filter((part) => part !== "").join(" ");
|
|
8686
|
+
}
|
|
8687
|
+
function written(one) {
|
|
8688
|
+
return [one.type, ...one.enum === undefined ? [] : [one.enum], one.member].join(".");
|
|
8689
|
+
}
|
|
8690
|
+
function declaringScripts(entries) {
|
|
8691
|
+
return [...new Set(entries.map((one) => one.declaredIn))].sort();
|
|
8692
|
+
}
|
|
8693
|
+
function instanceRemedy(entries, dependsOn, besideOthers) {
|
|
8694
|
+
if (entries.length === 0) {
|
|
8695
|
+
return "";
|
|
8696
|
+
}
|
|
8697
|
+
const declaring = declaringScripts(entries);
|
|
8631
8698
|
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
8632
|
-
return
|
|
8699
|
+
return `${besideOthers ? `For ${entries.map(written).join(", ")}, run` : "Run"} editor_rescan first: ` + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
8700
|
+
}
|
|
8701
|
+
function classRemedy(entries) {
|
|
8702
|
+
if (entries.length === 0) {
|
|
8703
|
+
return "";
|
|
8704
|
+
}
|
|
8705
|
+
const declaring = declaringScripts(entries);
|
|
8706
|
+
const reload = declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]}` : `editor_rescan with reloadScript, a call each for ${declaring.join(" and ")} since it takes one script,`;
|
|
8707
|
+
return `For ${entries.map(written).join(", ")}, run ${reload} first. In the one project that has ` + "reproduced a stale enum member, twice, the reload cleared it at once both times, and the one " + "plain editor_rescan tried left the diagnostic standing.";
|
|
8633
8708
|
}
|
|
8634
8709
|
function uncachedClassNote(types) {
|
|
8635
8710
|
if (types.length === 0) {
|
|
@@ -17045,7 +17120,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17045
17120
|
},
|
|
17046
17121
|
{
|
|
17047
17122
|
name: "script_diagnostics",
|
|
17048
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured.
|
|
17123
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
17049
17124
|
parameters: {
|
|
17050
17125
|
projectPath: PROJECT_PATH,
|
|
17051
17126
|
scriptPath: SCRIPT_PATH
|
|
@@ -17137,6 +17212,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
17137
17212
|
ops: ["start"],
|
|
17138
17213
|
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
17139
17214
|
},
|
|
17215
|
+
silent: {
|
|
17216
|
+
type: "boolean",
|
|
17217
|
+
ops: ["start"],
|
|
17218
|
+
description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, and the debug_* tools do not answer for it. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
|
|
17219
|
+
},
|
|
17140
17220
|
savesIn: {
|
|
17141
17221
|
type: "string",
|
|
17142
17222
|
ops: ["start", "check"],
|
|
@@ -17261,7 +17341,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17261
17341
|
},
|
|
17262
17342
|
reloadScript: {
|
|
17263
17343
|
type: "string",
|
|
17264
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
17344
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods."
|
|
17265
17345
|
}
|
|
17266
17346
|
},
|
|
17267
17347
|
requires: ["projectPath"]
|
|
@@ -17522,7 +17602,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17522
17602
|
pressed: {
|
|
17523
17603
|
type: "boolean",
|
|
17524
17604
|
ops: ["action", "key", "mouse_click"],
|
|
17525
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held.
|
|
17605
|
+
description: "action, key, mouse_click: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
|
|
17526
17606
|
},
|
|
17527
17607
|
strength: {
|
|
17528
17608
|
type: "number",
|
|
@@ -17606,7 +17686,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17606
17686
|
requires: ["text"]
|
|
17607
17687
|
},
|
|
17608
17688
|
mouse_click: {
|
|
17609
|
-
summary: "
|
|
17689
|
+
summary: "click a mouse button at a position, pressed and released a frame apart, each event carrying the buttons held once it has happened; the answer says whole. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is always a whole step, press and release together as a mouse sends one: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
17610
17690
|
requires: ["x", "y"]
|
|
17611
17691
|
},
|
|
17612
17692
|
mouse_motion: {
|
|
@@ -41722,14 +41802,16 @@ class GodotServer {
|
|
|
41722
41802
|
});
|
|
41723
41803
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
41724
41804
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
41725
|
-
|
|
41805
|
+
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
41806
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
41726
41807
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
41727
41808
|
}
|
|
41728
41809
|
const cmdArgs = runArguments({
|
|
41729
41810
|
projectPath: project.value.path,
|
|
41730
41811
|
headless,
|
|
41731
41812
|
scene: sceneArgument,
|
|
41732
|
-
userArgs: given.value
|
|
41813
|
+
userArgs: given.value,
|
|
41814
|
+
silent
|
|
41733
41815
|
});
|
|
41734
41816
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
41735
41817
|
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
|
|
@@ -41740,7 +41822,7 @@ class GodotServer {
|
|
|
41740
41822
|
}
|
|
41741
41823
|
started.announcedBefore = alreadyPlaying;
|
|
41742
41824
|
this.activeProcess = started;
|
|
41743
|
-
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." : "";
|
|
41825
|
+
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.` : "";
|
|
41744
41826
|
const endedForThis = endedToStartThis(ended);
|
|
41745
41827
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
41746
41828
|
const alsoRunning = leftRunningNote(stranded);
|
|
@@ -42941,8 +43023,16 @@ class GodotServer {
|
|
|
42941
43023
|
try {
|
|
42942
43024
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
42943
43025
|
const methods = readArray(answer, "methods");
|
|
42944
|
-
const
|
|
42945
|
-
|
|
43026
|
+
const listed = (field) => {
|
|
43027
|
+
const found = readArray(answer, field);
|
|
43028
|
+
return found ? { [field]: found.map(String) } : {};
|
|
43029
|
+
};
|
|
43030
|
+
reloaded = methods ? {
|
|
43031
|
+
methods: methods.map(String),
|
|
43032
|
+
...listed("heldBefore"),
|
|
43033
|
+
...listed("constants"),
|
|
43034
|
+
...listed("heldConstantsBefore")
|
|
43035
|
+
} : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
42946
43036
|
} catch (error) {
|
|
42947
43037
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
42948
43038
|
}
|
|
@@ -42977,6 +43067,8 @@ class GodotServer {
|
|
|
42977
43067
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
42978
43068
|
heldBeforeReload: reloaded.heldBefore,
|
|
42979
43069
|
reloadedMethods: reloaded.methods,
|
|
43070
|
+
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
43071
|
+
reloadedConstants: reloaded.constants,
|
|
42980
43072
|
reloadProblem: reloaded.problem,
|
|
42981
43073
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
42982
43074
|
broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
|
|
@@ -64,6 +64,7 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
64
64
|
# than the one that mends it. Without it a caller cannot tell a copy that was already current
|
|
65
65
|
# from one this call repaired, and those are different facts about their editor.
|
|
66
66
|
var held: Array[String] = _method_names(script)
|
|
67
|
+
var held_constants: Array[String] = _constant_names(script)
|
|
67
68
|
|
|
68
69
|
# The source is read off disk and put back before reloading, which is the whole of the fix and
|
|
69
70
|
# is not obvious. `reload` recompiles from the object's own `source_code`, not from the file, so
|
|
@@ -80,7 +81,14 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
80
81
|
if failed != OK:
|
|
81
82
|
return {"ok": false, "error": "Reloading " + path + " answered error " + str(failed)}
|
|
82
83
|
|
|
83
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
"ok": true,
|
|
86
|
+
"script": path,
|
|
87
|
+
"heldBefore": held,
|
|
88
|
+
"methods": _method_names(script),
|
|
89
|
+
"heldConstantsBefore": held_constants,
|
|
90
|
+
"constants": _constant_names(script),
|
|
91
|
+
}
|
|
84
92
|
|
|
85
93
|
|
|
86
94
|
func _method_names(script: GDScript) -> Array[String]:
|
|
@@ -91,6 +99,24 @@ func _method_names(script: GDScript) -> Array[String]:
|
|
|
91
99
|
return names
|
|
92
100
|
|
|
93
101
|
|
|
102
|
+
## The script's own constants, and the keys of each one that is a Dictionary as `Kind.SHORT`, which
|
|
103
|
+
## is how the copy holds a named enum: a stale enum member is denied as a member of the enum rather
|
|
104
|
+
## than of the class. A constant Dictionary that is not an enum is listed the same way, since the
|
|
105
|
+
## map does not tell the two apart.
|
|
106
|
+
func _constant_names(script: GDScript) -> Array[String]:
|
|
107
|
+
var names: Array[String] = []
|
|
108
|
+
var constants: Dictionary = script.get_script_constant_map()
|
|
109
|
+
for named: Variant in constants:
|
|
110
|
+
var value: Variant = constants[named]
|
|
111
|
+
names.append(str(named))
|
|
112
|
+
if value is Dictionary:
|
|
113
|
+
var values: Dictionary = value
|
|
114
|
+
for member: Variant in values:
|
|
115
|
+
names.append("%s.%s" % [named, member])
|
|
116
|
+
names.sort()
|
|
117
|
+
return names
|
|
118
|
+
|
|
119
|
+
|
|
94
120
|
func global_classes(_args: Dictionary) -> Dictionary:
|
|
95
121
|
if not _editor_plugin:
|
|
96
122
|
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
@@ -39,6 +39,11 @@ var _values: Values
|
|
|
39
39
|
var _pointer: Vector2 = Vector2.ZERO
|
|
40
40
|
var _pointer_placed: bool = false
|
|
41
41
|
|
|
42
|
+
## When the pointer last arrived, and how long it took over the arrival before that, in seconds: what
|
|
43
|
+
## turns a motion's distance into the speed a real one carries.
|
|
44
|
+
var _pointer_at_usec: int = 0
|
|
45
|
+
var _arrival_seconds: float = 1.0 / 60.0
|
|
46
|
+
|
|
42
47
|
|
|
43
48
|
func _init(host: Node, values: Values) -> void:
|
|
44
49
|
_host = host
|
|
@@ -46,9 +51,15 @@ func _init(host: Node, values: Values) -> void:
|
|
|
46
51
|
|
|
47
52
|
|
|
48
53
|
## The movement a pointer arriving at [param position] carries, which is the distance from where
|
|
49
|
-
## the last injected event put it, and none at all for the first. The arrival is recorded
|
|
54
|
+
## the last injected event put it, and none at all for the first. The arrival is recorded, with how
|
|
55
|
+
## long it took: at least a frame, since two motions sent in one frame did not cross the screen in
|
|
56
|
+
## no time at all.
|
|
50
57
|
func _arrive(position: Vector2) -> Vector2:
|
|
58
|
+
var now: int = Time.get_ticks_usec()
|
|
51
59
|
var relative: Vector2 = position - _pointer if _pointer_placed else Vector2.ZERO
|
|
60
|
+
var took: float = float(now - _pointer_at_usec) / 1_000_000.0 if _pointer_placed else 0.0
|
|
61
|
+
_arrival_seconds = maxf(took, 1.0 / 60.0)
|
|
62
|
+
_pointer_at_usec = now
|
|
52
63
|
_pointer = position
|
|
53
64
|
_pointer_placed = true
|
|
54
65
|
return relative
|
|
@@ -342,6 +353,14 @@ func _read_point(params: Dictionary, x_key: String, y_key: String, pair_key: Str
|
|
|
342
353
|
return "%s must be Vector2 or [x, y]" % pair_key
|
|
343
354
|
|
|
344
355
|
|
|
356
|
+
## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
|
|
357
|
+
##
|
|
358
|
+
## The whole click by default, for the reason [method inject_action] is the whole press, and
|
|
359
|
+
## because much of a UI acts on the release: a rich label opens its link then, so a click on one
|
|
360
|
+
## answered input_injected and opened nothing, while the same op on the wheel was already whole.
|
|
361
|
+
##
|
|
362
|
+
## `pressed` is how a caller asks for one half, the way a drag is made: true holds the button down,
|
|
363
|
+
## false lets go of one being held.
|
|
345
364
|
func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
346
365
|
var point: Variant = _read_point(params, "x", "y", "position")
|
|
347
366
|
if point is String:
|
|
@@ -350,32 +369,37 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
350
369
|
var button: int = _resolve_mouse_button(params.get("button", MOUSE_BUTTON_LEFT))
|
|
351
370
|
if button < 0:
|
|
352
371
|
return _no_such_button(params.get("button"))
|
|
353
|
-
var
|
|
372
|
+
var held: bool = Read.as_bool(params.get("pressed", true), true)
|
|
354
373
|
var double: bool = Read.as_bool(params.get("doubleClick", false))
|
|
355
|
-
|
|
356
|
-
# A button pressed somewhere puts the pointer there: the motion after a held press measures
|
|
357
|
-
# its drag from the press, which is where a real pointer would be.
|
|
358
|
-
var _moved: Vector2 = _arrive(position)
|
|
359
|
-
Input.parse_input_event(_button(position, button, pressed, double))
|
|
360
374
|
# A wheel step is a press and a release together, as a mouse sends one: there is no holding a
|
|
361
375
|
# wheel. A lone wheel press left the viewport's mouse focus on the control that took it, with
|
|
362
376
|
# the wheel's bit in the focus mask, and every click after it landed on that control rather
|
|
363
377
|
# than under the pointer, landing true and doing nothing, until a release was sent by hand.
|
|
364
|
-
var notch: bool =
|
|
378
|
+
var notch: bool = held and _mask_of(button) == 0
|
|
379
|
+
var whole: bool = notch or not params.has("pressed")
|
|
380
|
+
|
|
381
|
+
# A button pressed somewhere puts the pointer there: the motion after a held press measures
|
|
382
|
+
# its drag from the press, which is where a real pointer would be.
|
|
383
|
+
var _moved: Vector2 = _arrive(position)
|
|
384
|
+
Input.parse_input_event(_button(position, button, whole or held, double))
|
|
365
385
|
if notch:
|
|
366
386
|
Input.parse_input_event(_button(position, button, false, false))
|
|
387
|
+
elif whole:
|
|
388
|
+
# A frame between the halves, as inject_action has, so a control that acts on the press and
|
|
389
|
+
# one that acts on the release each get a frame to do it in.
|
|
390
|
+
await _host.get_tree().process_frame
|
|
391
|
+
Input.parse_input_event(_button(position, button, false, false))
|
|
392
|
+
await _host.get_tree().process_frame
|
|
367
393
|
|
|
368
|
-
|
|
394
|
+
return {
|
|
369
395
|
"type": "input_injected",
|
|
370
396
|
"input_type": "mouse_click",
|
|
371
397
|
"position": [position.x, position.y],
|
|
372
398
|
"button": button,
|
|
373
|
-
"pressed":
|
|
399
|
+
"pressed": held and not whole,
|
|
400
|
+
"whole": whole,
|
|
374
401
|
"double": double
|
|
375
402
|
}
|
|
376
|
-
if notch:
|
|
377
|
-
answer["released"] = true
|
|
378
|
-
return answer
|
|
379
403
|
|
|
380
404
|
|
|
381
405
|
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
@@ -839,12 +863,19 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
839
863
|
## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
|
|
840
864
|
## from every button event it is given, injected ones included, so a motion between a held click
|
|
841
865
|
## and its release is a drag to a control that reads the mask rather than remembering the click.
|
|
866
|
+
##
|
|
867
|
+
## And the speed, the distance over the time the pointer took, which only the platform fills in. A
|
|
868
|
+
## menu ignores a motion with none, so that one opening under a resting pointer lights nothing: a
|
|
869
|
+
## motion over an open drop-down's item answered as injected and the menu highlighted nothing. A
|
|
870
|
+
## motion that goes nowhere still has none, as a resting pointer does.
|
|
842
871
|
func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
|
|
843
872
|
var event: InputEventMouseMotion = InputEventMouseMotion.new()
|
|
844
873
|
event.position = position
|
|
845
874
|
event.global_position = position
|
|
846
875
|
event.relative = relative
|
|
847
876
|
event.screen_relative = relative
|
|
877
|
+
event.velocity = relative / _arrival_seconds
|
|
878
|
+
event.screen_velocity = event.velocity
|
|
848
879
|
event.button_mask = Input.get_mouse_button_mask()
|
|
849
880
|
return event
|
|
850
881
|
|
package/build/index.js
CHANGED
|
@@ -24825,8 +24825,18 @@ function resolveHeadless(requested, host) {
|
|
|
24825
24825
|
}
|
|
24826
24826
|
return envValue("DISPLAY", host.variables) === undefined && envValue("WAYLAND_DISPLAY", host.variables) === undefined;
|
|
24827
24827
|
}
|
|
24828
|
+
function resolveSilent(requested, variables) {
|
|
24829
|
+
if (typeof requested === "boolean") {
|
|
24830
|
+
return requested;
|
|
24831
|
+
}
|
|
24832
|
+
const said = envValue("GDHARNESS_SILENT", variables)?.toLowerCase();
|
|
24833
|
+
return said !== undefined && said !== "0" && said !== "false";
|
|
24834
|
+
}
|
|
24828
24835
|
function runArguments(options) {
|
|
24829
24836
|
const args = options.headless ? ["--headless", "--path", options.projectPath] : ["--path", options.projectPath];
|
|
24837
|
+
if (options.silent === true && !options.headless) {
|
|
24838
|
+
args.push("--audio-driver", "Dummy");
|
|
24839
|
+
}
|
|
24830
24840
|
if (options.quitAfter !== undefined && options.quitAfter !== null) {
|
|
24831
24841
|
args.push("--quit-after", String(options.quitAfter));
|
|
24832
24842
|
}
|
|
@@ -26131,20 +26141,57 @@ function withAddonClassesCounted(rebuilt, listed) {
|
|
|
26131
26141
|
return counted;
|
|
26132
26142
|
}
|
|
26133
26143
|
function missingMemberIn(message) {
|
|
26134
|
-
const
|
|
26135
|
-
if (
|
|
26144
|
+
const inferred = /The (method|property) "([^"(]+)(?:\(\))?" is not present on the inferred type "([^"]+)"/.exec(message);
|
|
26145
|
+
if (inferred !== null) {
|
|
26146
|
+
return {
|
|
26147
|
+
kind: inferred[1] === "property" ? "property" : "method",
|
|
26148
|
+
member: inferred[2] ?? "",
|
|
26149
|
+
type: inferred[3] ?? ""
|
|
26150
|
+
};
|
|
26151
|
+
}
|
|
26152
|
+
const called = /Static function "([A-Za-z_]\w*)\(\)" not found in base "([A-Za-z_]\w*)"/.exec(message);
|
|
26153
|
+
if (called !== null) {
|
|
26154
|
+
return { kind: "static method", member: called[1] ?? "", type: called[2] ?? "" };
|
|
26155
|
+
}
|
|
26156
|
+
const inBase = /Cannot find member "([A-Za-z_]\w*)" in base "([A-Za-z_]\w*)((?:\.[A-Za-z_]\w*)*)"/.exec(message);
|
|
26157
|
+
if (inBase === null) {
|
|
26136
26158
|
return null;
|
|
26137
26159
|
}
|
|
26138
|
-
|
|
26139
|
-
|
|
26140
|
-
|
|
26141
|
-
|
|
26142
|
-
};
|
|
26160
|
+
const member = inBase[1] ?? "";
|
|
26161
|
+
const type = inBase[2] ?? "";
|
|
26162
|
+
const within = (inBase[3] ?? "").split(".").filter((part) => part !== "");
|
|
26163
|
+
const enumName = within.at(-1);
|
|
26164
|
+
return enumName === undefined ? { kind: "member", member, type } : { kind: "enum member", member, type, enum: enumName };
|
|
26143
26165
|
}
|
|
26144
26166
|
function declaresMember(source, missing) {
|
|
26145
26167
|
const name = missing.member.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26146
|
-
|
|
26147
|
-
|
|
26168
|
+
if (missing.kind === "method") {
|
|
26169
|
+
return new RegExp(String.raw`^\s*(?:static\s+)?func\s+${name}\s*\(`, "m").test(source);
|
|
26170
|
+
}
|
|
26171
|
+
if (missing.kind === "static method") {
|
|
26172
|
+
return new RegExp(String.raw`^\s*static\s+func\s+${name}\s*\(`, "m").test(source);
|
|
26173
|
+
}
|
|
26174
|
+
if (missing.kind === "enum member") {
|
|
26175
|
+
return enumValues(source, missing.enum ?? "").includes(missing.member);
|
|
26176
|
+
}
|
|
26177
|
+
const kinds = missing.kind === "property" ? "var|const|enum|class|signal|func" : "var|const|enum|class|signal";
|
|
26178
|
+
return new RegExp(String.raw`^\s*(?:static\s+)?(?:@\w+(?:\([^)]*\))?\s+)*(?:${kinds})\s+${name}\b`, "m").test(source) || enumValues(source, null).includes(missing.member);
|
|
26179
|
+
}
|
|
26180
|
+
function enumValues(source, named) {
|
|
26181
|
+
const opening = named === null ? /^\s*enum\s*\{([^}]*)\}/gm : new RegExp(String.raw`^\s*enum\s+${named.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\s*\{([^}]*)\}`, "gm");
|
|
26182
|
+
const values = [];
|
|
26183
|
+
for (const [, body] of source.matchAll(opening)) {
|
|
26184
|
+
const uncommented = (body ?? "").split(`
|
|
26185
|
+
`).map((line) => line.replace(/#.*$/, "")).join(`
|
|
26186
|
+
`);
|
|
26187
|
+
for (const part of uncommented.split(",")) {
|
|
26188
|
+
const value = /^\s*([A-Za-z_]\w*)/.exec(part)?.[1];
|
|
26189
|
+
if (value !== undefined) {
|
|
26190
|
+
values.push(value);
|
|
26191
|
+
}
|
|
26192
|
+
}
|
|
26193
|
+
}
|
|
26194
|
+
return values;
|
|
26148
26195
|
}
|
|
26149
26196
|
function unknownTypeIn(message) {
|
|
26150
26197
|
const found = /(?:Could not find (?:base class|type)|Cannot find class|Identifier) "?([A-Za-z_][A-Za-z0-9_]*)"?(?: not declared| in the current scope|\b)/.exec(message);
|
|
@@ -26175,7 +26222,7 @@ function classesNamedIn(source, known) {
|
|
|
26175
26222
|
}
|
|
26176
26223
|
function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
26177
26224
|
const read = new Map;
|
|
26178
|
-
const found =
|
|
26225
|
+
const found = new Map;
|
|
26179
26226
|
for (const message of messages) {
|
|
26180
26227
|
const missing = missingMemberIn(message);
|
|
26181
26228
|
const declaredIn = missing === null ? undefined : classes.get(missing.type);
|
|
@@ -26186,20 +26233,48 @@ function contradictedDiagnostics(messages, classes, sourceOf) {
|
|
|
26186
26233
|
read.set(declaredIn, sourceOf(declaredIn));
|
|
26187
26234
|
}
|
|
26188
26235
|
const source = read.get(declaredIn) ?? null;
|
|
26189
|
-
|
|
26190
|
-
|
|
26236
|
+
const key = [missing.type, missing.enum ?? "", missing.member].join(".");
|
|
26237
|
+
const already = found.get(key);
|
|
26238
|
+
if (source !== null && declaresMember(source, missing) && already?.kind !== "static method") {
|
|
26239
|
+
found.set(key, { ...missing, declaredIn });
|
|
26191
26240
|
}
|
|
26192
26241
|
}
|
|
26193
|
-
return found;
|
|
26242
|
+
return [...found.values()];
|
|
26194
26243
|
}
|
|
26195
26244
|
function staleAnalysisNote(contradicted, dependsOn) {
|
|
26196
26245
|
if (contradicted.length === 0) {
|
|
26197
26246
|
return "";
|
|
26198
26247
|
}
|
|
26199
26248
|
const types = new Set(contradicted.map((one) => one.type));
|
|
26200
|
-
const
|
|
26249
|
+
const onInstances = contradicted.filter((one) => one.kind === "method" || one.kind === "property");
|
|
26250
|
+
const onTheClass = contradicted.filter((one) => one.kind === "member" || one.kind === "static method" || one.kind === "enum member");
|
|
26251
|
+
return [
|
|
26252
|
+
`The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written.",
|
|
26253
|
+
instanceRemedy(onInstances, dependsOn, onTheClass.length > 0),
|
|
26254
|
+
classRemedy(onTheClass)
|
|
26255
|
+
].filter((part) => part !== "").join(" ");
|
|
26256
|
+
}
|
|
26257
|
+
function written(one) {
|
|
26258
|
+
return [one.type, ...one.enum === undefined ? [] : [one.enum], one.member].join(".");
|
|
26259
|
+
}
|
|
26260
|
+
function declaringScripts(entries) {
|
|
26261
|
+
return [...new Set(entries.map((one) => one.declaredIn))].sort();
|
|
26262
|
+
}
|
|
26263
|
+
function instanceRemedy(entries, dependsOn, besideOthers) {
|
|
26264
|
+
if (entries.length === 0) {
|
|
26265
|
+
return "";
|
|
26266
|
+
}
|
|
26267
|
+
const declaring = declaringScripts(entries);
|
|
26201
26268
|
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
26202
|
-
return
|
|
26269
|
+
return `${besideOthers ? `For ${entries.map(written).join(", ")}, run` : "Run"} editor_rescan first: ` + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
26270
|
+
}
|
|
26271
|
+
function classRemedy(entries) {
|
|
26272
|
+
if (entries.length === 0) {
|
|
26273
|
+
return "";
|
|
26274
|
+
}
|
|
26275
|
+
const declaring = declaringScripts(entries);
|
|
26276
|
+
const reload = declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]}` : `editor_rescan with reloadScript, a call each for ${declaring.join(" and ")} since it takes one script,`;
|
|
26277
|
+
return `For ${entries.map(written).join(", ")}, run ${reload} first. In the one project that has ` + "reproduced a stale enum member, twice, the reload cleared it at once both times, and the one " + "plain editor_rescan tried left the diagnostic standing.";
|
|
26203
26278
|
}
|
|
26204
26279
|
function uncachedClassNote(types) {
|
|
26205
26280
|
if (types.length === 0) {
|
|
@@ -30702,7 +30777,7 @@ var TOOL_SPECS = [
|
|
|
30702
30777
|
},
|
|
30703
30778
|
{
|
|
30704
30779
|
name: "script_diagnostics",
|
|
30705
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured.
|
|
30780
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares, a method, property, constant, static function or enum member, comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
30706
30781
|
parameters: {
|
|
30707
30782
|
projectPath: PROJECT_PATH,
|
|
30708
30783
|
scriptPath: SCRIPT_PATH
|
|
@@ -30794,6 +30869,11 @@ var TOOL_SPECS = [
|
|
|
30794
30869
|
ops: ["start"],
|
|
30795
30870
|
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
30796
30871
|
},
|
|
30872
|
+
silent: {
|
|
30873
|
+
type: "boolean",
|
|
30874
|
+
ops: ["start"],
|
|
30875
|
+
description: "start: run the game with no sound out of the machine, by starting the engine with --audio-driver Dummy: every stream still plays and every playback position still advances, so what is playing can still be asked. The editor takes no engine flags for a game it plays, so a silent windowed run is started by this server, and the debug_* tools do not answer for it. A headless run, check's included, is silent already. Default false, or true when GDHARNESS_SILENT is set in the server's environment to anything but 0 or false; silent: false then plays through the editor as before."
|
|
30876
|
+
},
|
|
30797
30877
|
savesIn: {
|
|
30798
30878
|
type: "string",
|
|
30799
30879
|
ops: ["start", "check"],
|
|
@@ -30918,7 +30998,7 @@ var TOOL_SPECS = [
|
|
|
30918
30998
|
},
|
|
30919
30999
|
reloadScript: {
|
|
30920
31000
|
type: "string",
|
|
30921
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
31001
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods."
|
|
30922
31002
|
}
|
|
30923
31003
|
},
|
|
30924
31004
|
requires: ["projectPath"]
|
|
@@ -31179,7 +31259,7 @@ var TOOL_SPECS = [
|
|
|
31179
31259
|
pressed: {
|
|
31180
31260
|
type: "boolean",
|
|
31181
31261
|
ops: ["action", "key", "mouse_click"],
|
|
31182
|
-
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held.
|
|
31262
|
+
description: "action, key, mouse_click: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. A wheel button held down is still a whole step, since a wheel is never held."
|
|
31183
31263
|
},
|
|
31184
31264
|
strength: {
|
|
31185
31265
|
type: "number",
|
|
@@ -31263,7 +31343,7 @@ var TOOL_SPECS = [
|
|
|
31263
31343
|
requires: ["text"]
|
|
31264
31344
|
},
|
|
31265
31345
|
mouse_click: {
|
|
31266
|
-
summary: "
|
|
31346
|
+
summary: "click a mouse button at a position, pressed and released a frame apart, each event carrying the buttons held once it has happened; the answer says whole. A drag is a press held with pressed true, motions, and a release with pressed false. A wheel button is always a whole step, press and release together as a mouse sends one: a lone wheel press holds the viewport on the control that took it, so every click after it lands there",
|
|
31267
31347
|
requires: ["x", "y"]
|
|
31268
31348
|
},
|
|
31269
31349
|
mouse_motion: {
|
|
@@ -34182,14 +34262,16 @@ class GodotServer {
|
|
|
34182
34262
|
});
|
|
34183
34263
|
const runtimeWaitMs = this.announceBudget(args, project.value.path);
|
|
34184
34264
|
const editorWouldPlay = !headless || editorPlaysHeadless(project.value.file);
|
|
34185
|
-
|
|
34265
|
+
const silent = resolveSilent(args["silent"], process.env) && !headless;
|
|
34266
|
+
if (this.godotBridge.isConnected() && editorWouldPlay && given.value.length === 0 && !needsItsOwnProcess(asked.value) && !silent) {
|
|
34186
34267
|
return await this.playThroughEditor(sceneArgument, refreshed.value, scanned, project.value.path, alreadyPlaying, runtimeWaitMs, ended);
|
|
34187
34268
|
}
|
|
34188
34269
|
const cmdArgs = runArguments({
|
|
34189
34270
|
projectPath: project.value.path,
|
|
34190
34271
|
headless,
|
|
34191
34272
|
scene: sceneArgument,
|
|
34192
|
-
userArgs: given.value
|
|
34273
|
+
userArgs: given.value,
|
|
34274
|
+
silent
|
|
34193
34275
|
});
|
|
34194
34276
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
34195
34277
|
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
|
|
@@ -34200,7 +34282,7 @@ class GodotServer {
|
|
|
34200
34282
|
}
|
|
34201
34283
|
started.announcedBefore = alreadyPlaying;
|
|
34202
34284
|
this.activeProcess = started;
|
|
34203
|
-
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." : "";
|
|
34285
|
+
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.` : "";
|
|
34204
34286
|
const endedForThis = endedToStartThis(ended);
|
|
34205
34287
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
34206
34288
|
const alsoRunning = leftRunningNote(stranded);
|
|
@@ -35401,8 +35483,16 @@ class GodotServer {
|
|
|
35401
35483
|
try {
|
|
35402
35484
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
35403
35485
|
const methods = readArray(answer, "methods");
|
|
35404
|
-
const
|
|
35405
|
-
|
|
35486
|
+
const listed = (field) => {
|
|
35487
|
+
const found = readArray(answer, field);
|
|
35488
|
+
return found ? { [field]: found.map(String) } : {};
|
|
35489
|
+
};
|
|
35490
|
+
reloaded = methods ? {
|
|
35491
|
+
methods: methods.map(String),
|
|
35492
|
+
...listed("heldBefore"),
|
|
35493
|
+
...listed("constants"),
|
|
35494
|
+
...listed("heldConstantsBefore")
|
|
35495
|
+
} : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
35406
35496
|
} catch (error) {
|
|
35407
35497
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
35408
35498
|
}
|
|
@@ -35437,6 +35527,8 @@ class GodotServer {
|
|
|
35437
35527
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
35438
35528
|
heldBeforeReload: reloaded.heldBefore,
|
|
35439
35529
|
reloadedMethods: reloaded.methods,
|
|
35530
|
+
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
35531
|
+
reloadedConstants: reloaded.constants,
|
|
35440
35532
|
reloadProblem: reloaded.problem,
|
|
35441
35533
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
35442
35534
|
broughtIn: broughtIn.length > 0 ? broughtIn : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.16",
|
|
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",
|