gdharness 1.2.25 → 1.2.27
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 +277 -50
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +294 -0
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +7 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +150 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +5 -0
- package/build/godot/operations/import_pipeline.gd +92 -0
- package/build/godot/operations/input_actions.gd +3 -2
- package/build/godot/operations/plugins.gd +3 -2
- package/build/godot/operations/project_config.gd +17 -9
- package/build/godot/operations/project_file.gd +202 -0
- package/build/index.js +272 -48
- package/build/keeper.js +1207 -750
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -24933,12 +24933,27 @@ var ENGINE_FLAGS = Object.freeze({
|
|
|
24933
24933
|
"--fixed-fps": "number",
|
|
24934
24934
|
"--max-fps": "number",
|
|
24935
24935
|
"--resolution": "size",
|
|
24936
|
-
"--write-movie": "file"
|
|
24936
|
+
"--write-movie": "file",
|
|
24937
|
+
"--rendering-driver": "driver",
|
|
24938
|
+
"--rendering-method": "method",
|
|
24939
|
+
"--accessibility": "accessibility"
|
|
24937
24940
|
});
|
|
24938
24941
|
var VALUE_KIND = {
|
|
24939
24942
|
number: { pattern: /^\d+(?:\.\d+)?$/, named: "a number" },
|
|
24940
24943
|
size: { pattern: /^\d+x\d+$/, named: "a size such as 1280x720" },
|
|
24941
|
-
file: { pattern: /^[^-]/, named: "a file path" }
|
|
24944
|
+
file: { pattern: /^[^-]/, named: "a file path" },
|
|
24945
|
+
driver: {
|
|
24946
|
+
pattern: /^(?:vulkan|d3d12|metal|opengl3|opengl3_es|opengl3_angle|dummy)$/,
|
|
24947
|
+
named: "a rendering driver: vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy"
|
|
24948
|
+
},
|
|
24949
|
+
method: {
|
|
24950
|
+
pattern: /^(?:forward_plus|mobile|gl_compatibility)$/,
|
|
24951
|
+
named: "a rendering method: forward_plus, mobile or gl_compatibility"
|
|
24952
|
+
},
|
|
24953
|
+
accessibility: {
|
|
24954
|
+
pattern: /^(?:auto|always|disabled)$/,
|
|
24955
|
+
named: "an accessibility mode: auto, always or disabled"
|
|
24956
|
+
}
|
|
24942
24957
|
};
|
|
24943
24958
|
var SET_BY_ARGUMENT = Object.freeze({
|
|
24944
24959
|
"--headless": "headless",
|
|
@@ -24954,7 +24969,7 @@ function engineFlags(given) {
|
|
|
24954
24969
|
const setBy = Object.hasOwn(SET_BY_ARGUMENT, flag) ? SET_BY_ARGUMENT[flag] : undefined;
|
|
24955
24970
|
return {
|
|
24956
24971
|
ok: false,
|
|
24957
|
-
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
|
|
24972
|
+
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, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${offered}. A value goes as the next entry, as in ["--fixed-fps", "60"].`
|
|
24958
24973
|
};
|
|
24959
24974
|
}
|
|
24960
24975
|
if (takes === "none") {
|
|
@@ -27914,6 +27929,18 @@ class GodotDAPClient {
|
|
|
27914
27929
|
lines: Array.from(lines).sort((a, b) => a - b)
|
|
27915
27930
|
}));
|
|
27916
27931
|
}
|
|
27932
|
+
forgetBreakpoint(scriptPath, line) {
|
|
27933
|
+
const key = fileKey(scriptPath);
|
|
27934
|
+
const lines = this.breakpoints.get(key);
|
|
27935
|
+
if (lines === undefined) {
|
|
27936
|
+
return;
|
|
27937
|
+
}
|
|
27938
|
+
lines.delete(line);
|
|
27939
|
+
if (lines.size === 0) {
|
|
27940
|
+
this.breakpoints.delete(key);
|
|
27941
|
+
this.spelled.delete(key);
|
|
27942
|
+
}
|
|
27943
|
+
}
|
|
27917
27944
|
holdBreakpoints(held) {
|
|
27918
27945
|
for (const { scriptPath, lines } of held) {
|
|
27919
27946
|
const key = fileKey(scriptPath);
|
|
@@ -28692,6 +28719,22 @@ function howItEnded(run) {
|
|
|
28692
28719
|
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
|
|
28693
28720
|
}
|
|
28694
28721
|
|
|
28722
|
+
// src/godot-variant.ts
|
|
28723
|
+
var FLAG_64 = 1 << 16;
|
|
28724
|
+
var MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
28725
|
+
|
|
28726
|
+
// src/game-debugger.ts
|
|
28727
|
+
var PAUSE_MS = 30000;
|
|
28728
|
+
var STEP_MS = 1e4;
|
|
28729
|
+
var READ_MS = 5000;
|
|
28730
|
+
var REPLIES = new Set([
|
|
28731
|
+
"debug_enter",
|
|
28732
|
+
"debug_exit",
|
|
28733
|
+
"stack_dump",
|
|
28734
|
+
"stack_frame_vars",
|
|
28735
|
+
"stack_frame_var"
|
|
28736
|
+
]);
|
|
28737
|
+
|
|
28695
28738
|
// src/godot-bridge.ts
|
|
28696
28739
|
import { randomUUID } from "node:crypto";
|
|
28697
28740
|
import { EventEmitter } from "node:events";
|
|
@@ -29643,7 +29686,7 @@ function reported2(problems) {
|
|
|
29643
29686
|
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
29644
29687
|
}
|
|
29645
29688
|
function reason(log, problems) {
|
|
29646
|
-
const own = log
|
|
29689
|
+
const own = refusalsIn(log);
|
|
29647
29690
|
if (own.length > 0) {
|
|
29648
29691
|
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
29649
29692
|
}
|
|
@@ -29655,6 +29698,9 @@ function reason(log, problems) {
|
|
|
29655
29698
|
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
29656
29699
|
return printed.at(-1)?.text ?? "no output at all";
|
|
29657
29700
|
}
|
|
29701
|
+
function refusalsIn(log) {
|
|
29702
|
+
return log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
29703
|
+
}
|
|
29658
29704
|
function scriptFault(problems, script) {
|
|
29659
29705
|
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
29660
29706
|
const directories = [...new Set([spelled(dirname8(script)), spelled(resolvedOr(dirname8(script)))])];
|
|
@@ -29774,6 +29820,15 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
29774
29820
|
};
|
|
29775
29821
|
}
|
|
29776
29822
|
if (answer.kind === "none") {
|
|
29823
|
+
const refusals = refusalsIn(ran.log);
|
|
29824
|
+
if (refusals.length > 0) {
|
|
29825
|
+
const said = new Set(refusals.map((entry) => entry.index));
|
|
29826
|
+
return {
|
|
29827
|
+
ok: false,
|
|
29828
|
+
message: refusals.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; "),
|
|
29829
|
+
...reported2(problems.filter((entry) => !said.has(entry.index)))
|
|
29830
|
+
};
|
|
29831
|
+
}
|
|
29777
29832
|
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
29778
29833
|
return {
|
|
29779
29834
|
ok: false,
|
|
@@ -30313,7 +30368,31 @@ function testProgressMessage(printed, elapsedMs) {
|
|
|
30313
30368
|
|
|
30314
30369
|
// src/keeper-channel.ts
|
|
30315
30370
|
import { connect, createServer as createServer2 } from "node:net";
|
|
30316
|
-
|
|
30371
|
+
var DEBUG_PREFIX = "debug ";
|
|
30372
|
+
async function askTheKeeperToStop(address, withinMs) {
|
|
30373
|
+
const answered = await askTheKeeper(address, "stop", withinMs);
|
|
30374
|
+
if (typeof answered !== "string") {
|
|
30375
|
+
return answered;
|
|
30376
|
+
}
|
|
30377
|
+
return answered === "signalled" || answered === "gone" ? answered : { unreached: `it answered ${JSON.stringify(answered)}` };
|
|
30378
|
+
}
|
|
30379
|
+
async function askTheKeepersDebugger(address, request, withinMs) {
|
|
30380
|
+
const answered = await askTheKeeper(address, `${DEBUG_PREFIX}${JSON.stringify(request)}`, withinMs);
|
|
30381
|
+
if (typeof answered !== "string") {
|
|
30382
|
+
return answered;
|
|
30383
|
+
}
|
|
30384
|
+
if (answered === "unknown") {
|
|
30385
|
+
return { unreached: "its keeper was started by a gdharness from before game debuggers, and has none" };
|
|
30386
|
+
}
|
|
30387
|
+
try {
|
|
30388
|
+
const parsed = JSON.parse(answered);
|
|
30389
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
30390
|
+
return parsed;
|
|
30391
|
+
}
|
|
30392
|
+
} catch {}
|
|
30393
|
+
return { unreached: `it answered ${JSON.stringify(answered.slice(0, 200))}` };
|
|
30394
|
+
}
|
|
30395
|
+
function askTheKeeper(address, line, withinMs) {
|
|
30317
30396
|
return new Promise((resolve) => {
|
|
30318
30397
|
let said = "";
|
|
30319
30398
|
let settled = false;
|
|
@@ -30331,16 +30410,15 @@ function askTheKeeperToStop(address, withinMs) {
|
|
|
30331
30410
|
}, withinMs);
|
|
30332
30411
|
socket.setEncoding("utf8");
|
|
30333
30412
|
socket.on("connect", () => {
|
|
30334
|
-
socket.write(
|
|
30413
|
+
socket.write(`${line}
|
|
30335
30414
|
`);
|
|
30336
30415
|
});
|
|
30337
30416
|
socket.on("data", (chunk) => {
|
|
30338
30417
|
said += chunk;
|
|
30339
|
-
const line = said.split(`
|
|
30340
|
-
`)[0] ?? "";
|
|
30341
30418
|
if (said.includes(`
|
|
30342
30419
|
`)) {
|
|
30343
|
-
settle(
|
|
30420
|
+
settle(said.split(`
|
|
30421
|
+
`)[0] ?? "");
|
|
30344
30422
|
}
|
|
30345
30423
|
});
|
|
30346
30424
|
socket.on("error", (error) => {
|
|
@@ -33841,12 +33919,6 @@ function withoutListed(entries, errors) {
|
|
|
33841
33919
|
|
|
33842
33920
|
// src/script-profile.ts
|
|
33843
33921
|
import { readFileSync as readFileSync19 } from "node:fs";
|
|
33844
|
-
|
|
33845
|
-
// src/godot-variant.ts
|
|
33846
|
-
var FLAG_64 = 1 << 16;
|
|
33847
|
-
var MOST_MESSAGE_BYTES = (8 << 20) + 4;
|
|
33848
|
-
|
|
33849
|
-
// src/script-profile.ts
|
|
33850
33922
|
function readProfileFile(path) {
|
|
33851
33923
|
try {
|
|
33852
33924
|
const parsed = JSON.parse(readFileSync19(path, "utf8"));
|
|
@@ -34221,7 +34293,7 @@ var TOOL_SPECS = [
|
|
|
34221
34293
|
{
|
|
34222
34294
|
name: "project_settings",
|
|
34223
34295
|
title: "Project settings",
|
|
34224
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
34296
|
+
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
|
|
34225
34297
|
parameters: {
|
|
34226
34298
|
projectPath: PROJECT_PATH2,
|
|
34227
34299
|
setting: {
|
|
@@ -34406,7 +34478,7 @@ var TOOL_SPECS = [
|
|
|
34406
34478
|
},
|
|
34407
34479
|
options: {
|
|
34408
34480
|
type: "object",
|
|
34409
|
-
description:
|
|
34481
|
+
description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
|
|
34410
34482
|
additionalProperties: true
|
|
34411
34483
|
},
|
|
34412
34484
|
reimport: {
|
|
@@ -34494,7 +34566,7 @@ var TOOL_SPECS = [
|
|
|
34494
34566
|
engineArgs: {
|
|
34495
34567
|
type: "array",
|
|
34496
34568
|
items: { type: "string" },
|
|
34497
|
-
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU
|
|
34569
|
+
description: `The engine's own flags for the run, such as ["--verbose"], a value going as the next entry. Only those that change what the engine logs, draws for debugging, checks on the GPU, which renderer draws, how it times frames or whether it builds the tree a screen reader reads: ${Object.keys(ENGINE_FLAGS).join(", ")}. Any other is refused. With --verbose, engineEntries holds the ordinary lines the run printed as well, which is where the engine names the instances leaked and the resources still in use at exit.`
|
|
34498
34570
|
},
|
|
34499
34571
|
failFast: {
|
|
34500
34572
|
type: "boolean",
|
|
@@ -34965,13 +35037,13 @@ var TOOL_SPECS = [
|
|
|
34965
35037
|
type: "array",
|
|
34966
35038
|
items: { type: "string" },
|
|
34967
35039
|
ops: ["start", "check"],
|
|
34968
|
-
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
|
|
35040
|
+
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 its debugger is the keeper holding it rather than the editor; the debug_* tools answer for it all the same.`
|
|
34969
35041
|
},
|
|
34970
35042
|
engineArgs: {
|
|
34971
35043
|
type: "array",
|
|
34972
35044
|
items: { type: "string" },
|
|
34973
35045
|
ops: ["start", "check"],
|
|
34974
|
-
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
|
|
35046
|
+
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, which renderer draws, how it times frames or whether it builds the tree a screen reader reads, and Movie Maker's --write-movie: ${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. --rendering-driver takes vulkan, d3d12, metal, opengl3, opengl3_es, opengl3_angle or dummy, and --rendering-method forward_plus, mobile or gl_compatibility, so the same scene can be measured on another driver and a project's OpenGL fallback exercised on a machine that has Vulkan; they choose what draws a run with a window. --accessibility always has the game build the tree a screen reader reads, as the project's accessibility/general/accessibility_support at Always Active does, so runtime_inspect accessibility can read it; otherwise the engine builds it only when a screen reader is running. --write-movie takes an absolute path outside the project, in a directory that exists, ending in .avi (one file, video and sound) or .png (one numbered PNG per frame and a .wav beside them), where nothing it would write is there already; it needs a start with headless: false, since the engine crashes writing a movie headless, and it records at the project's editor/movie_writer/fps unless --fixed-fps says otherwise. 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.`
|
|
34975
35047
|
},
|
|
34976
35048
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
34977
35049
|
visible: {
|
|
@@ -34982,7 +35054,7 @@ var TOOL_SPECS = [
|
|
|
34982
35054
|
silent: {
|
|
34983
35055
|
type: "boolean",
|
|
34984
35056
|
ops: ["start"],
|
|
34985
|
-
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,
|
|
35057
|
+
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, whose keeper is its debugger rather than the editor. 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."
|
|
34986
35058
|
},
|
|
34987
35059
|
savesIn: {
|
|
34988
35060
|
type: "string",
|
|
@@ -35211,7 +35283,7 @@ var TOOL_SPECS = [
|
|
|
35211
35283
|
{
|
|
35212
35284
|
name: "runtime_inspect",
|
|
35213
35285
|
title: "Inspect the running game",
|
|
35214
|
-
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads,
|
|
35286
|
+
description: "Questions about the running game: what is written on the screen, the scene tree, the nodes matching a query, where one node is on screen, what one property reads, the performance metrics, or what it tells a screen reader. Needs the game running with the runtime addon.",
|
|
35215
35287
|
parameters: {
|
|
35216
35288
|
projectPath: RUNNING_PROJECT_PATH,
|
|
35217
35289
|
pid: RUNNING_PID,
|
|
@@ -35280,7 +35352,7 @@ var TOOL_SPECS = [
|
|
|
35280
35352
|
timeoutMs: {
|
|
35281
35353
|
type: "integer",
|
|
35282
35354
|
minimum: 1,
|
|
35283
|
-
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
35355
|
+
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS; accessibility defaults to 30000 or that, whichever is longer."
|
|
35284
35356
|
}
|
|
35285
35357
|
},
|
|
35286
35358
|
requires: [],
|
|
@@ -35308,7 +35380,12 @@ var TOOL_SPECS = [
|
|
|
35308
35380
|
},
|
|
35309
35381
|
metrics: {
|
|
35310
35382
|
effect: "read",
|
|
35311
|
-
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked",
|
|
35383
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game itself, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. The memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
|
|
35384
|
+
requires: []
|
|
35385
|
+
},
|
|
35386
|
+
accessibility: {
|
|
35387
|
+
effect: "read",
|
|
35388
|
+
summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader reads it, under windows: one tree per window, each element with its role as UI Automation names it (text for static text, button, check_box, group for a control holding others), its name, its value where it has one, its bounds in the window's pixels, the actions a screen reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the one holding the focus. The name is what is read aloud, so an empty one is read as nothing; static text takes it from DisplayServer.accessibility_update_set_value, not from a name set on it, and the answer says under _note how many text elements on screen have none. With no control focused, the engine reports the first node under the root as focused, which in a run with the runtime is GdharnessRuntime. Needs a windowed run on Windows started with engineArgs ["--accessibility", "always"] or with the project's accessibility/general/accessibility_support at Always Active, since the engine builds the tree only when asked; reading it takes a second or two, starting PowerShell inside the game so it reaches the game's own desktop`,
|
|
35312
35389
|
requires: []
|
|
35313
35390
|
}
|
|
35314
35391
|
},
|
|
@@ -35438,7 +35515,7 @@ var TOOL_SPECS = [
|
|
|
35438
35515
|
name: "runtime_input",
|
|
35439
35516
|
title: "Send input to the game",
|
|
35440
35517
|
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.",
|
|
35441
|
-
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. action, key and
|
|
35518
|
+
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. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
35442
35519
|
parameters: {
|
|
35443
35520
|
projectPath: RUNNING_PROJECT_PATH,
|
|
35444
35521
|
pid: RUNNING_PID,
|
|
@@ -35459,8 +35536,8 @@ var TOOL_SPECS = [
|
|
|
35459
35536
|
},
|
|
35460
35537
|
pressed: {
|
|
35461
35538
|
type: "boolean",
|
|
35462
|
-
ops: ["action", "key", "mouse_click"],
|
|
35463
|
-
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."
|
|
35539
|
+
ops: ["action", "key", "mouse_click", "joypad_button"],
|
|
35540
|
+
description: "action, key, mouse_click, joypad_button: 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."
|
|
35464
35541
|
},
|
|
35465
35542
|
strength: {
|
|
35466
35543
|
type: "number",
|
|
@@ -35505,8 +35582,26 @@ var TOOL_SPECS = [
|
|
|
35505
35582
|
},
|
|
35506
35583
|
button: {
|
|
35507
35584
|
type: ["string", "number"],
|
|
35508
|
-
ops: ["click", "mouse_click"],
|
|
35509
|
-
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
35585
|
+
ops: ["click", "mouse_click", "joypad_button"],
|
|
35586
|
+
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left. joypad_button: the controller button, by name (a, b, x, y, back, guide, start, left_stick, right_stick, left_shoulder, right_shoulder, dpad_up, dpad_down, dpad_left, dpad_right, misc1, paddle1 to paddle4, touchpad) or by its JoyButton number; a, b, x and y are the bottom, right, left and top face buttons, as Godot names them, whatever the controller prints on them."
|
|
35587
|
+
},
|
|
35588
|
+
axis: {
|
|
35589
|
+
type: ["string", "number"],
|
|
35590
|
+
ops: ["joypad_motion"],
|
|
35591
|
+
description: "joypad_motion: the stick or trigger axis, by name (left_x, left_y, right_x, right_y, trigger_left, trigger_right) or by its JoyAxis number."
|
|
35592
|
+
},
|
|
35593
|
+
value: {
|
|
35594
|
+
type: "number",
|
|
35595
|
+
minimum: -1,
|
|
35596
|
+
maximum: 1,
|
|
35597
|
+
ops: ["joypad_motion"],
|
|
35598
|
+
description: "joypad_motion: where the axis is, -1 to 1: down and right are positive on a stick, and a trigger runs 0 to 1. The axis stays there until another motion moves it, as a stick held over does; send 0 to let it go back."
|
|
35599
|
+
},
|
|
35600
|
+
device: {
|
|
35601
|
+
type: "number",
|
|
35602
|
+
minimum: 0,
|
|
35603
|
+
ops: ["joypad_button", "joypad_motion"],
|
|
35604
|
+
description: "joypad_button, joypad_motion: which controller, counting from 0. Default 0."
|
|
35510
35605
|
},
|
|
35511
35606
|
doubleClick: {
|
|
35512
35607
|
type: "boolean",
|
|
@@ -35556,6 +35651,16 @@ var TOOL_SPECS = [
|
|
|
35556
35651
|
effect: "additive",
|
|
35557
35652
|
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
35558
35653
|
requires: ["x", "y"]
|
|
35654
|
+
},
|
|
35655
|
+
joypad_button: {
|
|
35656
|
+
effect: "additive",
|
|
35657
|
+
summary: "press a controller button, or hold it, as an InputEventJoypadButton, so a game reading the pad in _input or _unhandled_input gets what a controller sends; an action bound to the button fires with it, as it would for a real one. Pressed and released a frame apart unless pressed says otherwise",
|
|
35658
|
+
requires: ["button"]
|
|
35659
|
+
},
|
|
35660
|
+
joypad_motion: {
|
|
35661
|
+
effect: "additive",
|
|
35662
|
+
summary: "move a stick or trigger axis to a value, as an InputEventJoypadMotion, which is what a game reading the sticks gets; the axis stays there until another motion moves it",
|
|
35663
|
+
requires: ["axis", "value"]
|
|
35559
35664
|
}
|
|
35560
35665
|
}
|
|
35561
35666
|
},
|
|
@@ -35630,7 +35735,7 @@ var TOOL_SPECS = [
|
|
|
35630
35735
|
{
|
|
35631
35736
|
name: "debug_breakpoint",
|
|
35632
35737
|
title: "Breakpoints",
|
|
35633
|
-
description: "Sets or removes a breakpoint through the editor's debug adapter.
|
|
35738
|
+
description: "Sets or removes a breakpoint. Set them first, then editor_run, and the game stops where you asked. With the editor connected it goes through the editor's debug adapter; with none, it is held here. A game this server starts itself, which editor_run start does for a run given args, savesIn, env or silent, or with no editor, is handed every breakpoint held here as it starts, and one set while that game runs reaches it at once (runningGame says so). The editor keeps a breakpoint set this way for one play, so every breakpoint this server holds is sent again before each play editor_run starts, and the start answer lists them under breakpoints. The set is also kept in the project, for the server that comes after a reconnect: a breakpoint stays until it is removed here, whichever server set it. Every answer lists the whole set under held, and the breakpoints the user set in the editor's own gutter under setInEditor; setting one here never takes those away, and removing a line here clears it whoever set it. Godot clears every breakpoint in the editor when a debug session opens unless network/debug_adapter/sync_breakpoints is on, which the addon turns on as it loads; an editor running an older addon is said so under breakpointsAtRisk, here and in editor_status.",
|
|
35634
35739
|
parameters: {
|
|
35635
35740
|
projectPath: PROJECT_PATH2,
|
|
35636
35741
|
scriptPath: SCRIPT_PATH,
|
|
@@ -35645,23 +35750,33 @@ var TOOL_SPECS = [
|
|
|
35645
35750
|
{
|
|
35646
35751
|
name: "debug_control",
|
|
35647
35752
|
title: "Step the debugger",
|
|
35648
|
-
description: "
|
|
35753
|
+
description: "Pauses, continues or steps the debugged game. A step waits for the game to stop again and answers with the stack where it ended up; one that has not stopped the game again within ten seconds answers heldAgain false and no stack. A game the editor plays is moved through the editor's debug adapter, which cannot pause or step out: it answers a pause by reporting the game stopped and leaving it running, and implements no stepOut, so hold such a game with a breakpoint and step over or into from there. A game this server started itself has its keeper as its debugger, which does both: pause breaks into the game wherever it is, a script spinning in a loop included, which is how a game hung on its first frame is asked where it is.",
|
|
35649
35754
|
parameters: {},
|
|
35650
35755
|
requires: [],
|
|
35651
35756
|
operations: {
|
|
35757
|
+
pause: {
|
|
35758
|
+
effect: "additive",
|
|
35759
|
+
summary: "stop a game this server started where it is, answering with the stack. The game takes it between frames, and in a script every 2048 lines it runs, so a loop that sleeps between few lines can take seconds; the call waits thirty, and past that answers paused false with the request still standing. Stopped between frames, where no script runs, the game is stepped on to the next line of script it runs",
|
|
35760
|
+
requires: []
|
|
35761
|
+
},
|
|
35652
35762
|
continue: { effect: "additive", summary: "resume after a breakpoint", requires: [] },
|
|
35653
35763
|
step_over: { effect: "additive", summary: "run the current line", requires: [] },
|
|
35654
35764
|
step_into: {
|
|
35655
35765
|
effect: "additive",
|
|
35656
35766
|
summary: "run the current line, stopping inside whatever it calls",
|
|
35657
35767
|
requires: []
|
|
35768
|
+
},
|
|
35769
|
+
step_out: {
|
|
35770
|
+
effect: "additive",
|
|
35771
|
+
summary: "run to the end of the current function and stop in its caller; a game this server started only",
|
|
35772
|
+
requires: []
|
|
35658
35773
|
}
|
|
35659
35774
|
}
|
|
35660
35775
|
},
|
|
35661
35776
|
{
|
|
35662
35777
|
name: "debug_state",
|
|
35663
35778
|
title: "Debugger state",
|
|
35664
|
-
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the
|
|
35779
|
+
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debugger's console output so far, its last thousand lines, with any earlier ones counted under notShown. For a game this server started, its keeper's debugger answers: each frame names its script as the project spells it under script and on this machine under path, and held says which thread stopped and why. For a game the editor plays, the editor's debug adapter answers. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
|
|
35665
35780
|
parameters: {
|
|
35666
35781
|
frameId: {
|
|
35667
35782
|
type: "number",
|
|
@@ -35751,7 +35866,7 @@ function serverInstructions() {
|
|
|
35751
35866
|
"Call editor_status first: it says whether an editor is connected and whether its addon matches this server.",
|
|
35752
35867
|
"Start the game with editor_run start, never by spawning an engine, and read editor_output after every run:",
|
|
35753
35868
|
"its clean verdict and its error entries are how a run is judged, and editor_run wait is how to wait for one.",
|
|
35754
|
-
"The runtime_* tools work on the running game and the debug_* tools on
|
|
35869
|
+
"The runtime_* tools work on the running game and the debug_* tools on its debugger.",
|
|
35755
35870
|
"A refusal names what would have worked, so read it before calling again."
|
|
35756
35871
|
].join(" ");
|
|
35757
35872
|
}
|
|
@@ -36092,6 +36207,31 @@ function longerWaitNote(tool, op, waitedMs) {
|
|
|
36092
36207
|
const how = tool === "runtime_wait" && op === "frames" ? `the wait is counted from the number of frames, at ${SLOWEST_FRAME_RATE} a second on top of GDHARNESS_RUNTIME_TIMEOUT_MS: a game drawing frames more slowly than that is waited on in fewer frames at a time, and a game inside one long frame, such as a long _ready, answers no call however few frames are asked for` : "the wait is GDHARNESS_RUNTIME_TIMEOUT_MS, which is set in the environment the server is started with";
|
|
36093
36208
|
return `${tool} ${op} takes no timeoutMs: it was given ${waitedMs}ms, and ${how}.`;
|
|
36094
36209
|
}
|
|
36210
|
+
function keptGameRequest(tool, op, args) {
|
|
36211
|
+
if (tool === "debug_state") {
|
|
36212
|
+
if (op === "output") {
|
|
36213
|
+
return { op: "output" };
|
|
36214
|
+
}
|
|
36215
|
+
if (op === "variables") {
|
|
36216
|
+
const frame = args["frameId"];
|
|
36217
|
+
return { op: "variables", frame: typeof frame === "number" ? frame : 0 };
|
|
36218
|
+
}
|
|
36219
|
+
return { op: "stack" };
|
|
36220
|
+
}
|
|
36221
|
+
switch (op) {
|
|
36222
|
+
case "pause":
|
|
36223
|
+
return { op: "pause" };
|
|
36224
|
+
case "step_over":
|
|
36225
|
+
return { op: "resume", how: "next" };
|
|
36226
|
+
case "step_into":
|
|
36227
|
+
return { op: "resume", how: "step" };
|
|
36228
|
+
case "step_out":
|
|
36229
|
+
return { op: "resume", how: "out" };
|
|
36230
|
+
default:
|
|
36231
|
+
return { op: "resume", how: "continue" };
|
|
36232
|
+
}
|
|
36233
|
+
}
|
|
36234
|
+
var OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.";
|
|
36095
36235
|
function projectSpelling(projectPath, absolutePath) {
|
|
36096
36236
|
return `res://${relative4(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
36097
36237
|
}
|
|
@@ -36179,6 +36319,7 @@ function stopWaitMs() {
|
|
|
36179
36319
|
return waitFromEnv("GDHARNESS_STOP_WAIT_MS", 1e4);
|
|
36180
36320
|
}
|
|
36181
36321
|
var KEEPER_ANSWER_MS = 15000;
|
|
36322
|
+
var KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
|
|
36182
36323
|
var EXIT_REPORTED_WITHIN_MS = 2000;
|
|
36183
36324
|
async function untilGone(pids, withinMs = stopWaitMs()) {
|
|
36184
36325
|
const gone = () => pids.every((pid) => !alive(pid));
|
|
@@ -38014,6 +38155,12 @@ class GodotServer {
|
|
|
38014
38155
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
38015
38156
|
property: readNonEmptyString(args, "property") ?? ""
|
|
38016
38157
|
}, { tool: "runtime_inspect", op, patience });
|
|
38158
|
+
case "accessibility":
|
|
38159
|
+
return await this.handleRuntimeCommand("get_accessibility", whichGame(args), {
|
|
38160
|
+
tool: "runtime_inspect",
|
|
38161
|
+
op,
|
|
38162
|
+
patience: readPositiveNumber(args, "timeoutMs") ?? Math.max(30000, this.runtimeTimeoutMs())
|
|
38163
|
+
});
|
|
38017
38164
|
default:
|
|
38018
38165
|
return await this.handleRuntimeCommand("get_metrics", { ...whichGame(args), metrics: readArray(args, "metrics") ?? [] }, { tool: "runtime_inspect", op, patience });
|
|
38019
38166
|
}
|
|
@@ -38073,28 +38220,70 @@ class GodotServer {
|
|
|
38073
38220
|
return this.createErrorResponse(located.reason, PATH_SOLUTIONS);
|
|
38074
38221
|
}
|
|
38075
38222
|
const session = this.dapHoldingBreakpointsOf(project.value.path);
|
|
38076
|
-
const
|
|
38077
|
-
|
|
38078
|
-
|
|
38079
|
-
|
|
38080
|
-
|
|
38081
|
-
|
|
38223
|
+
const scriptPath = realPathOr(located.absolutePath);
|
|
38224
|
+
const line = readPositiveNumber(args, "line");
|
|
38225
|
+
if (line === undefined) {
|
|
38226
|
+
return this.createErrorResponse("line is required: the one-based line to stop on.");
|
|
38227
|
+
}
|
|
38228
|
+
const editor = this.godotBridge.isConnected();
|
|
38229
|
+
let answer;
|
|
38230
|
+
if (editor) {
|
|
38231
|
+
const answered = await this.handleDAP(op === "set" ? "dap_set_breakpoint" : "dap_remove_breakpoint", {
|
|
38232
|
+
...args,
|
|
38233
|
+
scriptPath
|
|
38234
|
+
});
|
|
38235
|
+
if (answered.isError === true) {
|
|
38236
|
+
return answered;
|
|
38237
|
+
}
|
|
38238
|
+
answer = asParams(JSON.parse(answered.content[0]?.text ?? "{}"));
|
|
38239
|
+
} else {
|
|
38240
|
+
if (op === "set") {
|
|
38241
|
+
session.holdBreakpoints([{ scriptPath, lines: [line] }]);
|
|
38242
|
+
} else {
|
|
38243
|
+
session.forgetBreakpoint(scriptPath, line);
|
|
38244
|
+
}
|
|
38245
|
+
answer = { [op === "set" ? "set" : "removed"]: true };
|
|
38082
38246
|
}
|
|
38083
38247
|
const held = session.breakpointsHeld();
|
|
38084
38248
|
writeBreakpointNote(project.value.path, held);
|
|
38249
|
+
const kept = this.keptRun();
|
|
38250
|
+
const told = kept === null ? null : await askTheKeepersDebugger(kept.keeper, {
|
|
38251
|
+
op: "breakpoint",
|
|
38252
|
+
script: projectSpelling(project.value.path, scriptPath),
|
|
38253
|
+
line,
|
|
38254
|
+
set: op === "set"
|
|
38255
|
+
}, KEEPER_DEBUG_MS);
|
|
38085
38256
|
return this.jsonTextResponse({
|
|
38086
|
-
...
|
|
38257
|
+
...answer,
|
|
38087
38258
|
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
38088
|
-
|
|
38089
|
-
|
|
38090
|
-
|
|
38259
|
+
...editor ? {
|
|
38260
|
+
setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor())
|
|
38261
|
+
} : {},
|
|
38262
|
+
...told === null ? {} : ("unreached" in told) || typeof told["refused"] === "string" ? {
|
|
38263
|
+
runningGame: `The game this server started is running, and its debugger was not told: ${String("unreached" in told ? told.unreached : told["refused"])}. It takes the line when it is started again.`
|
|
38264
|
+
} : {
|
|
38265
|
+
runningGame: `Told to the running game (pid ${kept?.pid ?? "?"}), which this server started.`
|
|
38266
|
+
},
|
|
38267
|
+
note: editor ? "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, handed to every game this server starts itself, and kept in the project for the server after a reconnect." : "No editor is connected, so the set under held is kept here: every game this server starts is handed it as it starts, an editor that connects is sent it before its first play, and it is kept in the project for the server after a reconnect.",
|
|
38268
|
+
...editor ? this.breakpointsAtRisk() : {}
|
|
38091
38269
|
});
|
|
38092
38270
|
}
|
|
38093
38271
|
case "debug_control":
|
|
38094
38272
|
case "debug_state": {
|
|
38273
|
+
const kept = this.keptRun();
|
|
38274
|
+
if (kept !== null) {
|
|
38275
|
+
return await this.askTheKeptGame(kept.keeper, keptGameRequest(tool, op, args), kept.projectPath ?? "");
|
|
38276
|
+
}
|
|
38095
38277
|
if (tool === "debug_state" && op === "output") {
|
|
38096
38278
|
return await this.handleDAP("dap_get_output", args);
|
|
38097
38279
|
}
|
|
38280
|
+
const played = this.activeProcess;
|
|
38281
|
+
if ((op === "pause" || op === "step_out") && played?.throughEditor === true && !exited(played)) {
|
|
38282
|
+
return this.createErrorResponse(op === "pause" ? "A game the editor plays cannot be paused from here: Godot's debug adapter answers a pause by reporting the game stopped and leaving it running." : "A game the editor plays cannot be stepped out of a function from here: Godot's debug adapter implements no stepOut.", [
|
|
38283
|
+
"Set a breakpoint where it should stop, then step over or into from there",
|
|
38284
|
+
"A game this server starts, as editor_run start does with args, savesIn or env, has a debugger here that pauses and steps out"
|
|
38285
|
+
]);
|
|
38286
|
+
}
|
|
38098
38287
|
const held = await this.debuggedGame(tool === "debug_control" ? "move" : "read");
|
|
38099
38288
|
if (!held.ok) {
|
|
38100
38289
|
return held.response;
|
|
@@ -38238,8 +38427,8 @@ class GodotServer {
|
|
|
38238
38427
|
if (!game.throughEditor) {
|
|
38239
38428
|
return {
|
|
38240
38429
|
ok: false,
|
|
38241
|
-
response: this.createErrorResponse("The running game
|
|
38242
|
-
"editor_run start
|
|
38430
|
+
response: this.createErrorResponse("The running game has no debugger: it was started with none, or by a keeper that could not take questions, so breakpoints never hit and there is no stack to read.", [
|
|
38431
|
+
"editor_run start starts it again, and the keeper holding a game this server starts is its debugger",
|
|
38243
38432
|
"editor_status says whether an editor has reached this server"
|
|
38244
38433
|
])
|
|
38245
38434
|
};
|
|
@@ -39450,6 +39639,38 @@ class GodotServer {
|
|
|
39450
39639
|
breakpointsAtRisk: status.syncsBreakpoints === false ? "This editor clears every breakpoint in its script editor whenever a debug session opens on its adapter, which this server does for its first breakpoint or stack read: network/debug_adapter/sync_breakpoints is off. The installed addon turns it on as it loads, so the editor is running an older one; editor_launch restart puts the installed addon in, or turn the setting on in Editor Settings > Network > Debug Adapter." : "Whether this editor keeps its breakpoints when a debug session opens cannot be told: the addon it is running predates the report. Godot clears them unless network/debug_adapter/sync_breakpoints is on, which the installed addon turns on as it loads; editor_launch restart puts the installed addon in."
|
|
39451
39640
|
};
|
|
39452
39641
|
}
|
|
39642
|
+
breakpointsForAGame(projectPath) {
|
|
39643
|
+
return this.dapHoldingBreakpointsOf(projectPath).breakpointsHeld().flatMap((one) => one.lines.map((line) => `${projectSpelling(projectPath, one.scriptPath)}:${line}`));
|
|
39644
|
+
}
|
|
39645
|
+
keptRun() {
|
|
39646
|
+
const game = this.activeProcess;
|
|
39647
|
+
if (game === null || game.throughEditor || exited(game) || game.keeper === undefined) {
|
|
39648
|
+
return null;
|
|
39649
|
+
}
|
|
39650
|
+
return { ...game, keeper: game.keeper };
|
|
39651
|
+
}
|
|
39652
|
+
async askTheKeptGame(keeper, request, projectPath) {
|
|
39653
|
+
const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
|
|
39654
|
+
if ("unreached" in answered) {
|
|
39655
|
+
return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
|
|
39656
|
+
"editor_run start starts the game again, with a debugger",
|
|
39657
|
+
"editor_status says whether it is still running"
|
|
39658
|
+
]);
|
|
39659
|
+
}
|
|
39660
|
+
if (typeof answered["refused"] === "string") {
|
|
39661
|
+
return this.createErrorResponse(answered["refused"]);
|
|
39662
|
+
}
|
|
39663
|
+
const stack = answered["stack"];
|
|
39664
|
+
return this.jsonTextResponse({
|
|
39665
|
+
...answered,
|
|
39666
|
+
...Array.isArray(stack) ? {
|
|
39667
|
+
stack: stack.map((frame) => ({
|
|
39668
|
+
...frame,
|
|
39669
|
+
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join23(projectPath, frame["script"].slice("res://".length)) } : {}
|
|
39670
|
+
}))
|
|
39671
|
+
} : {}
|
|
39672
|
+
});
|
|
39673
|
+
}
|
|
39453
39674
|
breakpointsAsProjectSpells(projectPath, held) {
|
|
39454
39675
|
return held.map((one) => ({
|
|
39455
39676
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -40111,9 +40332,10 @@ class GodotServer {
|
|
|
40111
40332
|
started.announcedBefore = alreadyPlaying;
|
|
40112
40333
|
started.bootScene = runtimeWaitMs.scene;
|
|
40113
40334
|
this.activeProcess = started;
|
|
40114
|
-
const spawnedInstead = this.godotBridge.isConnected() && editorWouldPlay && (given.value.length > 0 || flags.value.length > 0) ?
|
|
40335
|
+
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.${OWN_DEBUGGER}` : this.godotBridge.isConnected() && editorWouldPlay && needsItsOwnProcess(asked.value) ? ` The editor plays a game with its own environment, so a run given one was started here.${OWN_DEBUGGER}` : 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"}, and silent: false plays it through the editor.${OWN_DEBUGGER}` : "";
|
|
40115
40336
|
const endedForThis = endedToStartThis(ended);
|
|
40116
40337
|
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
40338
|
+
const handed = this.dapHoldingBreakpointsOf(project.value.path).breakpointsHeld();
|
|
40117
40339
|
const alsoRunning = leftRunningNote(stranded);
|
|
40118
40340
|
return this.jsonTextResponse({
|
|
40119
40341
|
started: true,
|
|
@@ -40123,6 +40345,7 @@ class GodotServer {
|
|
|
40123
40345
|
arguments: cmdArgs,
|
|
40124
40346
|
argsNote: engineFlagsNote(given.value),
|
|
40125
40347
|
transcript: started.transcript,
|
|
40348
|
+
breakpoints: started.keeper === undefined || handed.length === 0 ? undefined : this.breakpointsAsProjectSpells(project.value.path, handed),
|
|
40126
40349
|
profile: profileOnStart(profiled, "gdharness", runtimeAddonRegistered(project.value.path)),
|
|
40127
40350
|
refreshedClasses: refreshed.value,
|
|
40128
40351
|
...scanWaitAnswer(scanned),
|
|
@@ -40131,7 +40354,7 @@ class GodotServer {
|
|
|
40131
40354
|
previousRunLeft: previousRunLeft(ended),
|
|
40132
40355
|
...previousRunHow(ended),
|
|
40133
40356
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
40134
|
-
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
40357
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${started.keeper !== undefined && handed.length > 0 ? " It stops at the lines under breakpoints, and debug_state stack reads where it is held." : ""}${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
40135
40358
|
});
|
|
40136
40359
|
}
|
|
40137
40360
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
@@ -40658,7 +40881,8 @@ class GodotServer {
|
|
|
40658
40881
|
startedAt,
|
|
40659
40882
|
projectPath,
|
|
40660
40883
|
...this.servedBy(),
|
|
40661
|
-
...profile === undefined ? {} : { profile }
|
|
40884
|
+
...profile === undefined ? {} : { profile },
|
|
40885
|
+
breakpoints: this.breakpointsForAGame(projectPath)
|
|
40662
40886
|
},
|
|
40663
40887
|
...desktop === undefined ? {} : { desktop }
|
|
40664
40888
|
});
|