gdharness 0.13.39 → 0.13.41
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
|
@@ -15373,7 +15373,7 @@ function buildToolDefinitions() {
|
|
|
15373
15373
|
};
|
|
15374
15374
|
});
|
|
15375
15375
|
}
|
|
15376
|
-
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
15376
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, SCRIPT_PATH, RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR, TOOL_SPECS, SPECS_BY_NAME;
|
|
15377
15377
|
var init_tool_definitions = __esm(() => {
|
|
15378
15378
|
PROJECT_INFO_SECTIONS = [
|
|
15379
15379
|
"autoloads",
|
|
@@ -15391,6 +15391,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15391
15391
|
type: "string",
|
|
15392
15392
|
description: "Which game, when more than one is running: the project directory it was started from. Not needed with one game."
|
|
15393
15393
|
};
|
|
15394
|
+
RUNNING_PID = {
|
|
15395
|
+
type: "number",
|
|
15396
|
+
description: "Which game, when several are running from one project, such as a bench and its workers: its process id, as editor_status lists under runtimes. Not needed with one game per project."
|
|
15397
|
+
};
|
|
15394
15398
|
SCENE_PATH = {
|
|
15395
15399
|
type: "string",
|
|
15396
15400
|
description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
|
|
@@ -16057,7 +16061,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16057
16061
|
},
|
|
16058
16062
|
{
|
|
16059
16063
|
name: "editor_status",
|
|
16060
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16064
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout; for the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself; a game some other server started announces none, which is how the two are told apart when both are running from one project. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.",
|
|
16061
16065
|
parameters: {},
|
|
16062
16066
|
requires: []
|
|
16063
16067
|
},
|
|
@@ -16114,6 +16118,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16114
16118
|
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, or the performance metrics. Needs the game running with the runtime addon.",
|
|
16115
16119
|
parameters: {
|
|
16116
16120
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16121
|
+
pid: RUNNING_PID,
|
|
16117
16122
|
nodePath: {
|
|
16118
16123
|
type: "string",
|
|
16119
16124
|
ops: ["tree", "text", "find", "rect", "property"],
|
|
@@ -16192,6 +16197,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16192
16197
|
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon.",
|
|
16193
16198
|
parameters: {
|
|
16194
16199
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16200
|
+
pid: RUNNING_PID,
|
|
16195
16201
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
16196
16202
|
property: {
|
|
16197
16203
|
type: "string",
|
|
@@ -16231,6 +16237,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16231
16237
|
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
|
|
16232
16238
|
parameters: {
|
|
16233
16239
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16240
|
+
pid: RUNNING_PID,
|
|
16234
16241
|
viewportPath: {
|
|
16235
16242
|
type: "string",
|
|
16236
16243
|
ops: ["viewport"],
|
|
@@ -16251,6 +16258,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16251
16258
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path, 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.",
|
|
16252
16259
|
parameters: {
|
|
16253
16260
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16261
|
+
pid: RUNNING_PID,
|
|
16254
16262
|
nodePath: {
|
|
16255
16263
|
type: "string",
|
|
16256
16264
|
ops: ["click", "choose"],
|
|
@@ -16347,6 +16355,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16347
16355
|
description: "Lets the running game get on with it and answers when something has happened: a number of frames, a signal, a property reaching a value, or words appearing on a screen. Needs the game running with the runtime addon.",
|
|
16348
16356
|
parameters: {
|
|
16349
16357
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16358
|
+
pid: RUNNING_PID,
|
|
16350
16359
|
frames: {
|
|
16351
16360
|
type: "number",
|
|
16352
16361
|
ops: ["frames"],
|
|
@@ -16382,7 +16391,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16382
16391
|
},
|
|
16383
16392
|
{
|
|
16384
16393
|
name: "debug_breakpoint",
|
|
16385
|
-
description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked. 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.",
|
|
16394
|
+
description: "Sets or removes a breakpoint through the editor's debug adapter. Needs the editor, not a running game: set them first, then editor_run, and the game stops where you asked. 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.",
|
|
16386
16395
|
parameters: {
|
|
16387
16396
|
projectPath: PROJECT_PATH,
|
|
16388
16397
|
scriptPath: SCRIPT_PATH,
|
|
@@ -31481,6 +31490,8 @@ class GodotDAPClient {
|
|
|
31481
31490
|
halt = null;
|
|
31482
31491
|
holdKnown = false;
|
|
31483
31492
|
breakpoints = new Map;
|
|
31493
|
+
inEditor = new Map;
|
|
31494
|
+
onBreakpointsChanged = null;
|
|
31484
31495
|
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1") {
|
|
31485
31496
|
this.port = port;
|
|
31486
31497
|
this.host = host;
|
|
@@ -31570,6 +31581,7 @@ class GodotDAPClient {
|
|
|
31570
31581
|
this.attached = false;
|
|
31571
31582
|
this.halt = null;
|
|
31572
31583
|
this.holdKnown = false;
|
|
31584
|
+
this.inEditor.clear();
|
|
31573
31585
|
}
|
|
31574
31586
|
async ensureConnected() {
|
|
31575
31587
|
if (!this.connected) {
|
|
@@ -31666,6 +31678,31 @@ class GodotDAPClient {
|
|
|
31666
31678
|
this.holdKnown = true;
|
|
31667
31679
|
return;
|
|
31668
31680
|
}
|
|
31681
|
+
if (eventName === "breakpoint") {
|
|
31682
|
+
const body = event.body;
|
|
31683
|
+
const breakpoint = body?.["breakpoint"];
|
|
31684
|
+
const source = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["source"] : undefined;
|
|
31685
|
+
const path = typeof source === "object" && source !== null ? source["path"] : undefined;
|
|
31686
|
+
const line = typeof breakpoint === "object" && breakpoint !== null ? breakpoint["line"] : undefined;
|
|
31687
|
+
if (typeof path !== "string" || typeof line !== "number") {
|
|
31688
|
+
return;
|
|
31689
|
+
}
|
|
31690
|
+
if (said(body, "reason") === "removed") {
|
|
31691
|
+
this.inEditor.get(path)?.delete(line);
|
|
31692
|
+
const held = this.breakpoints.get(path);
|
|
31693
|
+
if (held?.delete(line) === true) {
|
|
31694
|
+
if (held.size === 0) {
|
|
31695
|
+
this.breakpoints.delete(path);
|
|
31696
|
+
}
|
|
31697
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
31698
|
+
}
|
|
31699
|
+
return;
|
|
31700
|
+
}
|
|
31701
|
+
const known = this.inEditor.get(path) ?? new Set;
|
|
31702
|
+
known.add(line);
|
|
31703
|
+
this.inEditor.set(path, known);
|
|
31704
|
+
return;
|
|
31705
|
+
}
|
|
31669
31706
|
if (eventName === "terminated" || eventName === "exited") {
|
|
31670
31707
|
this.attached = false;
|
|
31671
31708
|
this.halt = null;
|
|
@@ -31758,23 +31795,46 @@ class GodotDAPClient {
|
|
|
31758
31795
|
async removeBreakpoint(filePath, line) {
|
|
31759
31796
|
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
31760
31797
|
wanted.delete(line);
|
|
31761
|
-
return await this.sendBreakpoints(filePath, wanted);
|
|
31798
|
+
return await this.sendBreakpoints(filePath, wanted, line);
|
|
31762
31799
|
}
|
|
31763
|
-
async sendBreakpoints(filePath,
|
|
31800
|
+
async sendBreakpoints(filePath, mine, clearing) {
|
|
31764
31801
|
await this.ensureConnected();
|
|
31765
31802
|
await this.initialize();
|
|
31803
|
+
const lines = new Set([...mine, ...this.inEditor.get(filePath) ?? []]);
|
|
31804
|
+
if (clearing !== undefined) {
|
|
31805
|
+
lines.delete(clearing);
|
|
31806
|
+
}
|
|
31766
31807
|
const sorted = Array.from(lines).sort((a, b) => a - b);
|
|
31767
31808
|
const answer = await this.sendRequest("setBreakpoints", {
|
|
31768
31809
|
source: { path: filePath },
|
|
31769
31810
|
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
31770
31811
|
});
|
|
31771
|
-
if (
|
|
31812
|
+
if (mine.size === 0) {
|
|
31772
31813
|
this.breakpoints.delete(filePath);
|
|
31773
31814
|
} else {
|
|
31774
|
-
this.breakpoints.set(filePath, new Set(
|
|
31815
|
+
this.breakpoints.set(filePath, new Set(mine));
|
|
31816
|
+
}
|
|
31817
|
+
if (lines.size === 0) {
|
|
31818
|
+
this.inEditor.delete(filePath);
|
|
31819
|
+
} else {
|
|
31820
|
+
this.inEditor.set(filePath, lines);
|
|
31775
31821
|
}
|
|
31776
31822
|
return answer;
|
|
31777
31823
|
}
|
|
31824
|
+
setBreakpointsSink(sink) {
|
|
31825
|
+
this.onBreakpointsChanged = sink;
|
|
31826
|
+
}
|
|
31827
|
+
breakpointsInEditor() {
|
|
31828
|
+
const theirs = [];
|
|
31829
|
+
for (const [scriptPath, lines] of this.inEditor) {
|
|
31830
|
+
const mine = this.breakpoints.get(scriptPath);
|
|
31831
|
+
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
31832
|
+
if (rest.length > 0) {
|
|
31833
|
+
theirs.push({ scriptPath, lines: rest });
|
|
31834
|
+
}
|
|
31835
|
+
}
|
|
31836
|
+
return theirs;
|
|
31837
|
+
}
|
|
31778
31838
|
async reapplyBreakpoints() {
|
|
31779
31839
|
const applied = [];
|
|
31780
31840
|
const refused = [];
|
|
@@ -31790,6 +31850,9 @@ class GodotDAPClient {
|
|
|
31790
31850
|
});
|
|
31791
31851
|
}
|
|
31792
31852
|
}
|
|
31853
|
+
if (refused.length > 0) {
|
|
31854
|
+
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
31855
|
+
}
|
|
31793
31856
|
return { applied, refused };
|
|
31794
31857
|
}
|
|
31795
31858
|
breakpointsHeld() {
|
|
@@ -31992,14 +32055,10 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
31992
32055
|
}
|
|
31993
32056
|
} catch (error) {
|
|
31994
32057
|
const message = error instanceof Error ? error.message : String(error);
|
|
32058
|
+
const cure = toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
|
|
31995
32059
|
return {
|
|
31996
32060
|
isError: true,
|
|
31997
|
-
content: [
|
|
31998
|
-
{
|
|
31999
|
-
type: "text",
|
|
32000
|
-
text: `DAP tool '${toolName}' failed: ${message}. ` + "The debug tools answer for a game the editor is playing: start one with editor_run."
|
|
32001
|
-
}
|
|
32002
|
-
]
|
|
32061
|
+
content: [{ type: "text", text: `DAP tool '${toolName}' failed: ${message}. ${cure}` }]
|
|
32003
32062
|
};
|
|
32004
32063
|
}
|
|
32005
32064
|
}
|
|
@@ -35076,6 +35135,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
35076
35135
|
dapPort: this.connectionInfo?.dapPort,
|
|
35077
35136
|
debugPort: this.connectionInfo?.debugPort,
|
|
35078
35137
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
35138
|
+
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
35079
35139
|
pendingRequests: this.pendingRequests.size,
|
|
35080
35140
|
queuedResources: this.resourceQueues.size,
|
|
35081
35141
|
listeningSince: this.listeningSince ?? undefined
|
|
@@ -35218,6 +35278,7 @@ var init_godot_bridge = __esm(() => {
|
|
|
35218
35278
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
35219
35279
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
35220
35280
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
35281
|
+
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
35221
35282
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
35222
35283
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
35223
35284
|
}
|
|
@@ -36386,7 +36447,7 @@ function secondsFromClock(printed) {
|
|
|
36386
36447
|
const extra = days === null ? 0 : Number(days) * 86400;
|
|
36387
36448
|
return Number.isFinite(extra) ? seconds + extra : undefined;
|
|
36388
36449
|
}
|
|
36389
|
-
var run5, ASK_TIMEOUT_MS =
|
|
36450
|
+
var run5, ASK_TIMEOUT_MS = 6000;
|
|
36390
36451
|
var init_process_time = __esm(() => {
|
|
36391
36452
|
run5 = promisify5(execFile5);
|
|
36392
36453
|
});
|
|
@@ -36615,7 +36676,18 @@ function parseAnnouncement(file, pid) {
|
|
|
36615
36676
|
if (protocol !== RUNTIME_PROTOCOL) {
|
|
36616
36677
|
return { kind: "unspoken", unspoken: { pid, protocol: protocol ?? 0, project: named } };
|
|
36617
36678
|
}
|
|
36618
|
-
|
|
36679
|
+
const editorPid = readNumber(fields, "editor_pid");
|
|
36680
|
+
return {
|
|
36681
|
+
kind: "runtime",
|
|
36682
|
+
endpoint: {
|
|
36683
|
+
pid,
|
|
36684
|
+
port,
|
|
36685
|
+
address,
|
|
36686
|
+
project: named,
|
|
36687
|
+
file,
|
|
36688
|
+
...editorPid !== undefined && Number.isInteger(editorPid) && editorPid > 0 ? { editorPid } : {}
|
|
36689
|
+
}
|
|
36690
|
+
};
|
|
36619
36691
|
}
|
|
36620
36692
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
36621
36693
|
const running = new Map;
|
|
@@ -36689,7 +36761,7 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
36689
36761
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
36690
36762
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
36691
36763
|
for (;; ) {
|
|
36692
|
-
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
36764
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted && (waiting.accept?.(endpoint) ?? true));
|
|
36693
36765
|
if (fresh !== undefined) {
|
|
36694
36766
|
return fresh;
|
|
36695
36767
|
}
|
|
@@ -36708,7 +36780,25 @@ function tooNew(unspoken) {
|
|
|
36708
36780
|
const half = unspoken.some((game) => game.protocol > RUNTIME_PROTOCOL) ? "this server is the older half: reconnect it so it spawns the installed version" : "the addon is the older half: reinstall it and restart the game";
|
|
36709
36781
|
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
36710
36782
|
}
|
|
36711
|
-
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
36783
|
+
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
36784
|
+
if (pid !== undefined) {
|
|
36785
|
+
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
36786
|
+
if (named !== undefined) {
|
|
36787
|
+
if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
|
|
36788
|
+
return {
|
|
36789
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
36790
|
+
};
|
|
36791
|
+
}
|
|
36792
|
+
return { endpoint: named };
|
|
36793
|
+
}
|
|
36794
|
+
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
36795
|
+
if (tooNewToo !== undefined) {
|
|
36796
|
+
return { problem: tooNew([tooNewToo]) };
|
|
36797
|
+
}
|
|
36798
|
+
return {
|
|
36799
|
+
problem: endpoints.length === 0 ? `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.` : `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
|
|
36800
|
+
};
|
|
36801
|
+
}
|
|
36712
36802
|
if (endpoints.length === 0) {
|
|
36713
36803
|
if (unspoken.length > 0) {
|
|
36714
36804
|
return { problem: tooNew(unspoken) };
|
|
@@ -36739,14 +36829,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
36739
36829
|
return { endpoint: matching[0] };
|
|
36740
36830
|
}
|
|
36741
36831
|
return {
|
|
36742
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}.
|
|
36832
|
+
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
36743
36833
|
};
|
|
36744
36834
|
}
|
|
36745
36835
|
if (endpoints.length === 1 && endpoints[0]) {
|
|
36746
36836
|
return { endpoint: endpoints[0] };
|
|
36747
36837
|
}
|
|
36748
36838
|
return {
|
|
36749
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
|
|
36839
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
36750
36840
|
};
|
|
36751
36841
|
}
|
|
36752
36842
|
function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
@@ -37151,6 +37241,9 @@ function patienceForFrames(frames, atLeast) {
|
|
|
37151
37241
|
function projectSpelling(projectPath, absolutePath) {
|
|
37152
37242
|
return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
37153
37243
|
}
|
|
37244
|
+
function whichGame(args) {
|
|
37245
|
+
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
37246
|
+
}
|
|
37154
37247
|
function describeHalt(halt) {
|
|
37155
37248
|
switch (halt.reason) {
|
|
37156
37249
|
case "breakpoint":
|
|
@@ -37186,6 +37279,19 @@ function runIsUp(run, editorSays) {
|
|
|
37186
37279
|
}
|
|
37187
37280
|
return editorSays ?? stillRunning(run);
|
|
37188
37281
|
}
|
|
37282
|
+
function endedPreviousRun(ended) {
|
|
37283
|
+
if (ended === null) {
|
|
37284
|
+
return;
|
|
37285
|
+
}
|
|
37286
|
+
return ended.pid ?? true;
|
|
37287
|
+
}
|
|
37288
|
+
function endedToStartThis(ended) {
|
|
37289
|
+
if (ended === null) {
|
|
37290
|
+
return "";
|
|
37291
|
+
}
|
|
37292
|
+
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
37293
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
37294
|
+
}
|
|
37189
37295
|
function runtimeVerdict(endpoint, after) {
|
|
37190
37296
|
if (endpoint !== null) {
|
|
37191
37297
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -37330,6 +37436,16 @@ function hasMainScene(projectFile) {
|
|
|
37330
37436
|
const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
37331
37437
|
return typeof scene === "string" && scene !== "";
|
|
37332
37438
|
}
|
|
37439
|
+
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
37440
|
+
if (projectPath === null) {
|
|
37441
|
+
return false;
|
|
37442
|
+
}
|
|
37443
|
+
try {
|
|
37444
|
+
return readFileSync17(join19(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
|
|
37445
|
+
} catch {
|
|
37446
|
+
return false;
|
|
37447
|
+
}
|
|
37448
|
+
}
|
|
37333
37449
|
function editorPlaysHeadless(projectFile) {
|
|
37334
37450
|
const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
37335
37451
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
@@ -37884,7 +38000,7 @@ class GodotServer {
|
|
|
37884
38000
|
switch (op) {
|
|
37885
38001
|
case "tree":
|
|
37886
38002
|
return await this.handleRuntimeCommand("get_tree", {
|
|
37887
|
-
|
|
38003
|
+
...whichGame(args),
|
|
37888
38004
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37889
38005
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
37890
38006
|
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
@@ -37893,36 +38009,36 @@ class GodotServer {
|
|
|
37893
38009
|
return await this.handleFindRuntimeNodes(args);
|
|
37894
38010
|
case "text":
|
|
37895
38011
|
return await this.handleRuntimeCommand("read_text", {
|
|
37896
|
-
|
|
38012
|
+
...whichGame(args),
|
|
37897
38013
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37898
38014
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
37899
38015
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
37900
38016
|
});
|
|
37901
38017
|
case "rect":
|
|
37902
38018
|
return await this.handleRuntimeCommand("get_rect", {
|
|
37903
|
-
|
|
38019
|
+
...whichGame(args),
|
|
37904
38020
|
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
37905
38021
|
});
|
|
37906
38022
|
case "property":
|
|
37907
38023
|
return await this.handleRuntimeCommand("get_property", {
|
|
37908
|
-
|
|
38024
|
+
...whichGame(args),
|
|
37909
38025
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37910
38026
|
property: readNonEmptyString(args, "property") ?? ""
|
|
37911
38027
|
});
|
|
37912
38028
|
default:
|
|
37913
38029
|
return await this.handleRuntimeCommand("get_metrics", {
|
|
37914
|
-
|
|
38030
|
+
...whichGame(args),
|
|
37915
38031
|
metrics: readArray(args, "metrics") ?? []
|
|
37916
38032
|
});
|
|
37917
38033
|
}
|
|
37918
38034
|
case "runtime_invoke":
|
|
37919
38035
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
37920
|
-
|
|
38036
|
+
...whichGame(args),
|
|
37921
38037
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37922
38038
|
property: readString(args, "property") ?? "",
|
|
37923
38039
|
value: args["value"]
|
|
37924
38040
|
}) : await this.handleRuntimeCommand("call_method", {
|
|
37925
|
-
|
|
38041
|
+
...whichGame(args),
|
|
37926
38042
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37927
38043
|
method: readString(args, "method") ?? "",
|
|
37928
38044
|
args: readArray(args, "args") ?? []
|
|
@@ -37932,7 +38048,7 @@ class GodotServer {
|
|
|
37932
38048
|
case "runtime_input":
|
|
37933
38049
|
if (op === "click") {
|
|
37934
38050
|
return await this.handleRuntimeCommand("click", {
|
|
37935
|
-
|
|
38051
|
+
...whichGame(args),
|
|
37936
38052
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37937
38053
|
button: readString(args, "button") ?? "left",
|
|
37938
38054
|
double: readBoolean(args, "doubleClick") ?? false
|
|
@@ -37940,7 +38056,7 @@ class GodotServer {
|
|
|
37940
38056
|
}
|
|
37941
38057
|
if (op === "choose") {
|
|
37942
38058
|
return await this.handleRuntimeCommand("choose", {
|
|
37943
|
-
|
|
38059
|
+
...whichGame(args),
|
|
37944
38060
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37945
38061
|
text: readString(args, "text") ?? "",
|
|
37946
38062
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -37971,7 +38087,9 @@ class GodotServer {
|
|
|
37971
38087
|
return this.jsonTextResponse({
|
|
37972
38088
|
...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
|
|
37973
38089
|
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
37974
|
-
|
|
38090
|
+
setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
|
|
38091
|
+
note: "The editor keeps a breakpoint set this way for one play. Every breakpoint held here is sent again before each play editor_run starts, and kept in the project for the server after a reconnect.",
|
|
38092
|
+
...this.breakpointsAtRisk()
|
|
37975
38093
|
});
|
|
37976
38094
|
}
|
|
37977
38095
|
case "debug_control":
|
|
@@ -38045,6 +38163,16 @@ class GodotServer {
|
|
|
38045
38163
|
response: this.createErrorResponse("No game is running, so there is no debug session to answer for.", ["Start one with editor_run start, which has the editor play it"])
|
|
38046
38164
|
};
|
|
38047
38165
|
}
|
|
38166
|
+
const going = await this.runStillGoing(game);
|
|
38167
|
+
if (!going) {
|
|
38168
|
+
return {
|
|
38169
|
+
ok: false,
|
|
38170
|
+
response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
|
|
38171
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
38172
|
+
"editor_run start plays another, which the debugger can hold"
|
|
38173
|
+
])
|
|
38174
|
+
};
|
|
38175
|
+
}
|
|
38048
38176
|
if (!game.throughEditor) {
|
|
38049
38177
|
return {
|
|
38050
38178
|
ok: false,
|
|
@@ -38066,7 +38194,7 @@ class GodotServer {
|
|
|
38066
38194
|
};
|
|
38067
38195
|
}
|
|
38068
38196
|
if (!this.dap().isStopped()) {
|
|
38069
|
-
const hold = await this.holdOf(game);
|
|
38197
|
+
const hold = await this.holdOf(game, going);
|
|
38070
38198
|
if (hold.heldAt !== null) {
|
|
38071
38199
|
if (intent === "move") {
|
|
38072
38200
|
return { ok: true, value: game };
|
|
@@ -38632,6 +38760,12 @@ class GodotServer {
|
|
|
38632
38760
|
this.dapClient.setOutputSink((line) => {
|
|
38633
38761
|
this.writeToTranscript(line);
|
|
38634
38762
|
});
|
|
38763
|
+
this.dapClient.setBreakpointsSink((held) => {
|
|
38764
|
+
const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
|
|
38765
|
+
if (project !== null) {
|
|
38766
|
+
writeBreakpointNote(project, held);
|
|
38767
|
+
}
|
|
38768
|
+
});
|
|
38635
38769
|
}
|
|
38636
38770
|
return this.dapClient;
|
|
38637
38771
|
}
|
|
@@ -38643,6 +38777,15 @@ class GodotServer {
|
|
|
38643
38777
|
}
|
|
38644
38778
|
return client;
|
|
38645
38779
|
}
|
|
38780
|
+
breakpointsAtRisk() {
|
|
38781
|
+
const status = this.godotBridge.getStatus();
|
|
38782
|
+
if (!status.connected || status.syncsBreakpoints === true) {
|
|
38783
|
+
return {};
|
|
38784
|
+
}
|
|
38785
|
+
return {
|
|
38786
|
+
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."
|
|
38787
|
+
};
|
|
38788
|
+
}
|
|
38646
38789
|
breakpointsAsProjectSpells(projectPath, held) {
|
|
38647
38790
|
return held.map((one) => ({
|
|
38648
38791
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -38705,6 +38848,7 @@ class GodotServer {
|
|
|
38705
38848
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38706
38849
|
startupError: this.bridgeStartupError,
|
|
38707
38850
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
38851
|
+
...this.breakpointsAtRisk(),
|
|
38708
38852
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
38709
38853
|
announcedAt: this.announcedAt ?? undefined,
|
|
38710
38854
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
@@ -38739,7 +38883,7 @@ class GodotServer {
|
|
|
38739
38883
|
const located = await this.locator.find();
|
|
38740
38884
|
const announced = runtimesAnnounced();
|
|
38741
38885
|
const pinged = Promise.all(announced.running.map(async (endpoint) => {
|
|
38742
|
-
const held = this.knownHoldOn(endpoint);
|
|
38886
|
+
const held = this.knownHoldOn(endpoint, announced.running);
|
|
38743
38887
|
const reply = held === null ? await runtimeRequest(endpoint, "ping", {}, HOLD_PING_MS) : {
|
|
38744
38888
|
ok: false,
|
|
38745
38889
|
message: `The game is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is. debug_control continue lets it go.`
|
|
@@ -38748,6 +38892,7 @@ class GodotServer {
|
|
|
38748
38892
|
pid: endpoint.pid,
|
|
38749
38893
|
port: endpoint.port,
|
|
38750
38894
|
project: endpoint.project,
|
|
38895
|
+
editorPid: endpoint.editorPid,
|
|
38751
38896
|
reachable: reply.ok,
|
|
38752
38897
|
problem: reply.ok ? null : reply.message
|
|
38753
38898
|
};
|
|
@@ -38756,9 +38901,14 @@ class GodotServer {
|
|
|
38756
38901
|
const playing = await this.editorPlayingState();
|
|
38757
38902
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
38758
38903
|
const current = this.currentRun();
|
|
38759
|
-
|
|
38904
|
+
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
38905
|
+
return {
|
|
38906
|
+
playing,
|
|
38907
|
+
active,
|
|
38908
|
+
hold: current === null ? {} : await this.holdOf(current, active)
|
|
38909
|
+
};
|
|
38760
38910
|
})();
|
|
38761
|
-
const [games, { playing, hold }] = await Promise.all([pinged, asked]);
|
|
38911
|
+
const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
|
|
38762
38912
|
return this.jsonTextResponse({
|
|
38763
38913
|
editor: {
|
|
38764
38914
|
...this.getEditorStatusPayload(),
|
|
@@ -38770,7 +38920,7 @@ class GodotServer {
|
|
|
38770
38920
|
problem: located.ok ? undefined : GodotLocator2.refusal(located).message
|
|
38771
38921
|
},
|
|
38772
38922
|
game: {
|
|
38773
|
-
processActive:
|
|
38923
|
+
processActive: active,
|
|
38774
38924
|
playingInEditor: playing,
|
|
38775
38925
|
...hold,
|
|
38776
38926
|
runtimeConnected: games.some((game) => game.reachable),
|
|
@@ -38983,10 +39133,12 @@ class GodotServer {
|
|
|
38983
39133
|
if (op === "check") {
|
|
38984
39134
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
38985
39135
|
}
|
|
39136
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38986
39137
|
let ended = null;
|
|
38987
|
-
|
|
39138
|
+
const before = this.currentRun();
|
|
39139
|
+
if (before !== null && await this.runStillGoing(before)) {
|
|
38988
39140
|
this.logDebug("Ending the running game before starting another");
|
|
38989
|
-
ended = this.
|
|
39141
|
+
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
38990
39142
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
38991
39143
|
}
|
|
38992
39144
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
@@ -39010,8 +39162,8 @@ class GodotServer {
|
|
|
39010
39162
|
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
39011
39163
|
this.activeProcess = started;
|
|
39012
39164
|
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." : "";
|
|
39013
|
-
const endedForThis = ended
|
|
39014
|
-
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
39165
|
+
const endedForThis = endedToStartThis(ended);
|
|
39166
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
39015
39167
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
39016
39168
|
const alsoRunning = stranded.length === 0 ? "" : ` A game of this project was already running and still is: ${named}. Starting this one did not end it and nothing here can, because this server did not start it. End it yourself, or bring the halves to one version where a protocol is named.`;
|
|
39017
39169
|
return this.jsonTextResponse({
|
|
@@ -39022,23 +39174,39 @@ class GodotServer {
|
|
|
39022
39174
|
arguments: cmdArgs,
|
|
39023
39175
|
transcript: started.transcript,
|
|
39024
39176
|
refreshedClasses: refreshed.value,
|
|
39025
|
-
endedPreviousRun: ended
|
|
39177
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
39026
39178
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
39027
39179
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
39028
39180
|
});
|
|
39029
39181
|
}
|
|
39030
|
-
async gameIsUp() {
|
|
39031
|
-
const run = this.currentRun();
|
|
39032
|
-
const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
|
|
39033
|
-
return runIsUp(run, editorSays);
|
|
39034
|
-
}
|
|
39035
39182
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
39036
39183
|
if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
39037
39184
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
39038
39185
|
}
|
|
39186
|
+
let asking = null;
|
|
39187
|
+
let editorAskedAt = 0;
|
|
39188
|
+
let editorSaysGoing = true;
|
|
39039
39189
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39040
39190
|
budgetMs,
|
|
39041
|
-
|
|
39191
|
+
accept: (one) => this.isTheGameOf(this.currentRun(), one, projectPath),
|
|
39192
|
+
giveUp: () => {
|
|
39193
|
+
if (this.dapClient?.isStopped() === true) {
|
|
39194
|
+
return true;
|
|
39195
|
+
}
|
|
39196
|
+
const run = this.currentRun();
|
|
39197
|
+
if (run === null || !run.throughEditor) {
|
|
39198
|
+
return !stillRunning(run);
|
|
39199
|
+
}
|
|
39200
|
+
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
39201
|
+
editorAskedAt = Date.now();
|
|
39202
|
+
asking = this.runStillGoing(run).then((going) => {
|
|
39203
|
+
editorSaysGoing = going;
|
|
39204
|
+
}).finally(() => {
|
|
39205
|
+
asking = null;
|
|
39206
|
+
});
|
|
39207
|
+
}
|
|
39208
|
+
return !editorSaysGoing;
|
|
39209
|
+
}
|
|
39042
39210
|
});
|
|
39043
39211
|
const going = this.currentRun();
|
|
39044
39212
|
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
@@ -39048,7 +39216,7 @@ class GodotServer {
|
|
|
39048
39216
|
addon: true,
|
|
39049
39217
|
budgetMs,
|
|
39050
39218
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
39051
|
-
running: await this.
|
|
39219
|
+
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
39052
39220
|
withArgs
|
|
39053
39221
|
});
|
|
39054
39222
|
}
|
|
@@ -39089,7 +39257,8 @@ class GodotServer {
|
|
|
39089
39257
|
startedAt,
|
|
39090
39258
|
exitCode: null,
|
|
39091
39259
|
throughEditor: true,
|
|
39092
|
-
brokeOn: null
|
|
39260
|
+
brokeOn: null,
|
|
39261
|
+
announcedBefore: alreadyPlaying
|
|
39093
39262
|
};
|
|
39094
39263
|
this.activeProcess = played;
|
|
39095
39264
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -39100,14 +39269,14 @@ class GodotServer {
|
|
|
39100
39269
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
39101
39270
|
refreshedClasses,
|
|
39102
39271
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
39103
|
-
endedPreviousRun: ended
|
|
39272
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
39104
39273
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
39105
39274
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
39106
39275
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
39107
39276
|
reason: one.reason
|
|
39108
39277
|
})),
|
|
39109
39278
|
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
39110
|
-
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + (ended
|
|
39279
|
+
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + endedToStartThis(ended)
|
|
39111
39280
|
});
|
|
39112
39281
|
}
|
|
39113
39282
|
drainEditorOutput(game) {
|
|
@@ -39323,15 +39492,40 @@ class GodotServer {
|
|
|
39323
39492
|
brokeOn: null,
|
|
39324
39493
|
pickedUpPlaying: true
|
|
39325
39494
|
};
|
|
39326
|
-
const announced = this.theOneAnnouncedForOurProject();
|
|
39327
|
-
if (announced !== undefined) {
|
|
39328
|
-
picked.announcedPid = announced.pid;
|
|
39329
|
-
}
|
|
39330
39495
|
this.activeProcess = picked;
|
|
39331
39496
|
}
|
|
39332
|
-
|
|
39333
|
-
|
|
39334
|
-
|
|
39497
|
+
announcedPidOf(run, announced) {
|
|
39498
|
+
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
39499
|
+
return run.announcedPid;
|
|
39500
|
+
}
|
|
39501
|
+
const before = run.announcedBefore ?? new Set;
|
|
39502
|
+
const fresh = this.allAnnouncedForOurProject(announced ?? runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39503
|
+
const candidates = fresh.filter((one) => this.playedByOurEditor(one, run.projectPath));
|
|
39504
|
+
const theOne = candidates.length === 1 ? candidates[0] : undefined;
|
|
39505
|
+
if (theOne !== undefined) {
|
|
39506
|
+
run.announcedPid = theOne.pid;
|
|
39507
|
+
}
|
|
39508
|
+
return run.announcedPid;
|
|
39509
|
+
}
|
|
39510
|
+
playedByOurEditor(endpoint, projectPath) {
|
|
39511
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
39512
|
+
if (editor === undefined) {
|
|
39513
|
+
return true;
|
|
39514
|
+
}
|
|
39515
|
+
if (endpoint.editorPid !== undefined) {
|
|
39516
|
+
return endpoint.editorPid === editor;
|
|
39517
|
+
}
|
|
39518
|
+
return !runtimeAddonAnnouncesEditors(projectPath ?? this.ownProject);
|
|
39519
|
+
}
|
|
39520
|
+
isTheGameOf(run, endpoint, projectPath) {
|
|
39521
|
+
if (run === null) {
|
|
39522
|
+
return false;
|
|
39523
|
+
}
|
|
39524
|
+
if (run.throughEditor) {
|
|
39525
|
+
return this.playedByOurEditor(endpoint, projectPath);
|
|
39526
|
+
}
|
|
39527
|
+
const editor = this.godotBridge.getStatus().editorPid;
|
|
39528
|
+
return endpoint.editorPid === undefined || endpoint.editorPid !== editor;
|
|
39335
39529
|
}
|
|
39336
39530
|
adoptRecordedRun() {
|
|
39337
39531
|
const record = readRunRecord();
|
|
@@ -39461,12 +39655,25 @@ class GodotServer {
|
|
|
39461
39655
|
}
|
|
39462
39656
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
39463
39657
|
const until = Date.now() + budgetMs;
|
|
39464
|
-
while (
|
|
39658
|
+
while (await this.runStillGoing(run) && Date.now() < until) {
|
|
39465
39659
|
this.drainEditorOutput(run);
|
|
39466
39660
|
this.drainTranscript(run);
|
|
39467
39661
|
await delay3(RUN_POLL_MS);
|
|
39468
39662
|
}
|
|
39469
|
-
return await this.handleGetDebugOutput(args,
|
|
39663
|
+
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
39664
|
+
}
|
|
39665
|
+
async runStillGoing(run, editorSays) {
|
|
39666
|
+
if (!run.throughEditor) {
|
|
39667
|
+
return stillRunning(run);
|
|
39668
|
+
}
|
|
39669
|
+
const announced = this.announcedPidOf(run);
|
|
39670
|
+
if (announced !== undefined && !alive(announced)) {
|
|
39671
|
+
return false;
|
|
39672
|
+
}
|
|
39673
|
+
if (announced !== undefined && editorSays === undefined) {
|
|
39674
|
+
return stillRunning(run);
|
|
39675
|
+
}
|
|
39676
|
+
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
39470
39677
|
}
|
|
39471
39678
|
async handleGetDebugOutput(args, waitedMs) {
|
|
39472
39679
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -39476,7 +39683,8 @@ class GodotServer {
|
|
|
39476
39683
|
}
|
|
39477
39684
|
this.drainEditorOutput(run);
|
|
39478
39685
|
this.drainTranscript(run);
|
|
39479
|
-
|
|
39686
|
+
const going = await this.runStillGoing(run);
|
|
39687
|
+
if (run.endedUnwatched !== true && !going) {
|
|
39480
39688
|
run.log.finish();
|
|
39481
39689
|
run.endedUnwatched = run.exitCode === null;
|
|
39482
39690
|
}
|
|
@@ -39487,12 +39695,13 @@ class GodotServer {
|
|
|
39487
39695
|
contains: readNonEmptyString(args, "contains"),
|
|
39488
39696
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
39489
39697
|
});
|
|
39490
|
-
const hold = await this.holdOf(run);
|
|
39698
|
+
const hold = await this.holdOf(run, going);
|
|
39491
39699
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
39492
|
-
const
|
|
39700
|
+
const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
|
|
39701
|
+
const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
|
|
39493
39702
|
const notes = [];
|
|
39494
39703
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
39495
|
-
notes.push(
|
|
39704
|
+
notes.push(processToAsk === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, and the game announced no runtime, so nothing here has its process id. A game with the runtime addon announces one; editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds either way." : "cpu was asked for and this platform would not say what the run has used.");
|
|
39496
39705
|
}
|
|
39497
39706
|
if (waitedMs !== undefined) {
|
|
39498
39707
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
@@ -39516,10 +39725,10 @@ class GodotServer {
|
|
|
39516
39725
|
notes.push(`Everything this run has printed is in ${run.transcript}, uncapped and still being written. Read it for output, not for whether the run is alive: a transcript that stops growing is a run between prints. running above is asked of the operating system while there is a process to ask about, and for an editor-played run that announced no runtime it is the editor's answer instead, which can lag after the game has gone.`);
|
|
39517
39726
|
}
|
|
39518
39727
|
return this.jsonTextResponse({
|
|
39519
|
-
running:
|
|
39728
|
+
running: going,
|
|
39520
39729
|
exitCode: run.exitCode,
|
|
39521
39730
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
39522
|
-
pid: run.pid,
|
|
39731
|
+
pid: run.pid ?? this.announcedPidOf(run) ?? null,
|
|
39523
39732
|
errors: run.log.count("error"),
|
|
39524
39733
|
warnings: run.log.count("warning"),
|
|
39525
39734
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
@@ -39545,18 +39754,19 @@ class GodotServer {
|
|
|
39545
39754
|
}
|
|
39546
39755
|
this.drainEditorOutput(stopped);
|
|
39547
39756
|
this.drainTranscript(stopped);
|
|
39757
|
+
const wasRunning = await this.runStillGoing(stopped);
|
|
39548
39758
|
this.logDebug("Stopping the running game");
|
|
39549
39759
|
await this.endActiveGame("editor_run stop");
|
|
39550
39760
|
return this.jsonTextResponse({
|
|
39551
39761
|
stopped: true,
|
|
39552
39762
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
39553
|
-
endedPid: stopped.pid,
|
|
39554
|
-
exitedBeforeStop: stopped.exitCode !== null,
|
|
39763
|
+
endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
|
|
39764
|
+
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
39555
39765
|
exitCode: stopped.exitCode,
|
|
39556
39766
|
errors: stopped.log.count("error"),
|
|
39557
39767
|
warnings: stopped.log.count("warning"),
|
|
39558
39768
|
clean: stopped.log.count("error") === 0,
|
|
39559
|
-
note: stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
|
|
39769
|
+
note: !wasRunning ? "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running." : stopped.throughEditor ? "The editor was asked to stop the scene it is playing. Anything that game started for itself, with OS.create_process or otherwise, is not the editor's to stop and is still running." : "The process named under endedPid was ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and is still running.",
|
|
39560
39770
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
39561
39771
|
});
|
|
39562
39772
|
}
|
|
@@ -39707,27 +39917,27 @@ class GodotServer {
|
|
|
39707
39917
|
const override = Number.parseInt(envValue("GDHARNESS_RUNTIME_TIMEOUT_MS") ?? "", 10);
|
|
39708
39918
|
return Number.isInteger(override) && override > 0 ? override : 1e4;
|
|
39709
39919
|
}
|
|
39710
|
-
knownHoldOn(endpoint) {
|
|
39920
|
+
knownHoldOn(endpoint, announced) {
|
|
39711
39921
|
const run = this.activeProcess;
|
|
39712
39922
|
const session = this.dapClient;
|
|
39713
39923
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
39714
39924
|
return null;
|
|
39715
39925
|
}
|
|
39716
|
-
|
|
39717
|
-
if (its !== endpoint.pid || !session.holdIsKnown()) {
|
|
39926
|
+
if (this.announcedPidOf(run, announced) !== endpoint.pid || !session.holdIsKnown()) {
|
|
39718
39927
|
return null;
|
|
39719
39928
|
}
|
|
39720
39929
|
return session.whereItStopped();
|
|
39721
39930
|
}
|
|
39722
|
-
async holdOf(run) {
|
|
39723
|
-
if (!run.throughEditor || !
|
|
39931
|
+
async holdOf(run, going) {
|
|
39932
|
+
if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
|
|
39724
39933
|
return { heldAt: null };
|
|
39725
39934
|
}
|
|
39726
39935
|
const session = this.dapClient;
|
|
39727
39936
|
if (session?.holdIsKnown()) {
|
|
39728
39937
|
return { heldAt: session.whereItStopped() };
|
|
39729
39938
|
}
|
|
39730
|
-
const
|
|
39939
|
+
const its = this.announcedPidOf(run);
|
|
39940
|
+
const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
|
|
39731
39941
|
if (endpoint === undefined) {
|
|
39732
39942
|
return {
|
|
39733
39943
|
heldAt: undefined,
|
|
@@ -39752,13 +39962,13 @@ class GodotServer {
|
|
|
39752
39962
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
39753
39963
|
}
|
|
39754
39964
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
39755
|
-
const { op: _op, projectPath, ...params } = asParams(args);
|
|
39965
|
+
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
39756
39966
|
const announced = runtimesAnnounced();
|
|
39757
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
39967
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
|
|
39758
39968
|
if ("problem" in choice) {
|
|
39759
39969
|
return this.createErrorResponse(choice.problem);
|
|
39760
39970
|
}
|
|
39761
|
-
const held = this.knownHoldOn(choice.endpoint);
|
|
39971
|
+
const held = this.knownHoldOn(choice.endpoint, announced.running);
|
|
39762
39972
|
if (held !== null) {
|
|
39763
39973
|
return this.createErrorResponse(`The game (pid ${choice.endpoint.pid}) is held by the editor's debugger, ${describeHalt(held)}, and answers no runtime call while it is.`, [
|
|
39764
39974
|
"debug_control continue lets it go, after which this call answers",
|
|
@@ -39820,7 +40030,7 @@ class GodotServer {
|
|
|
39820
40030
|
const includeHidden = readBoolean(args, "includeHidden");
|
|
39821
40031
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
39822
40032
|
...filters,
|
|
39823
|
-
|
|
40033
|
+
...whichGame(args),
|
|
39824
40034
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
39825
40035
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
39826
40036
|
...property === undefined ? {} : { property },
|
|
@@ -39831,18 +40041,18 @@ class GodotServer {
|
|
|
39831
40041
|
if (op === "frames") {
|
|
39832
40042
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
39833
40043
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
39834
|
-
return await this.handleRuntimeCommand("wait_frames", {
|
|
40044
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
39835
40045
|
}
|
|
39836
40046
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
39837
40047
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
39838
40048
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
39839
40049
|
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
39840
|
-
|
|
40050
|
+
...whichGame(args),
|
|
39841
40051
|
path: nodePath,
|
|
39842
40052
|
signal: readString(args, "signal") ?? "",
|
|
39843
40053
|
timeout_ms: timeoutMs
|
|
39844
40054
|
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
39845
|
-
|
|
40055
|
+
...whichGame(args),
|
|
39846
40056
|
path: nodePath,
|
|
39847
40057
|
property: readString(args, "property") ?? "",
|
|
39848
40058
|
value: args["value"],
|
|
@@ -41397,7 +41607,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41397
41607
|
## Start here
|
|
41398
41608
|
|
|
41399
41609
|
- ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
|
|
41400
|
-
games instead
|
|
41610
|
+
games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
|
|
41611
|
+
and its workers do; the others answer about the editor this server is connected to. There is no
|
|
41401
41612
|
ambient project, and an argument a tool does not declare is refused rather than ignored.
|
|
41402
41613
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
41403
41614
|
\`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
|
|
@@ -41469,6 +41680,12 @@ The autoload reaches an export. \`gdharness runtime off\` in the project before
|
|
|
41469
41680
|
move. There is no pause and no step out: Godot's debug adapter implements neither, so use a
|
|
41470
41681
|
breakpoint on the line you want instead.
|
|
41471
41682
|
|
|
41683
|
+
A held game draws nothing and answers no \`runtime_*\` call. The server says so at once when it
|
|
41684
|
+
knows, naming why the game is held and \`debug_control continue\` as what lets it go, and
|
|
41685
|
+
\`editor_output\` and \`editor_status\` carry the same under \`heldAt\`. A breakpoint set with
|
|
41686
|
+
\`debug_breakpoint\` holds for every start until \`debug_breakpoint remove\`, across a reconnect,
|
|
41687
|
+
so take it off when the question it was for is answered.
|
|
41688
|
+
|
|
41472
41689
|
## Editing
|
|
41473
41690
|
|
|
41474
41691
|
\`scene_tree\` for what a scene holds, rather than what the \`.tscn\` text implies. \`scene_node\`,
|