gdharness 0.13.39 → 0.13.40
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 +244 -74
- package/build/godot/addons/gdharness_editor/bridge_client.gd +53 -0
- package/build/index.js +235 -72
- package/package.json +1 -1
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".'
|
|
@@ -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
|
}
|
|
@@ -36708,7 +36769,25 @@ function tooNew(unspoken) {
|
|
|
36708
36769
|
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
36770
|
return `A game is running, in a protocol this server does not speak ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${half}.`;
|
|
36710
36771
|
}
|
|
36711
|
-
function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
36772
|
+
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
36773
|
+
if (pid !== undefined) {
|
|
36774
|
+
const named = endpoints.find((endpoint) => endpoint.pid === pid);
|
|
36775
|
+
if (named !== undefined) {
|
|
36776
|
+
if (projectPath !== undefined && resolve4(named.project.path) !== resolve4(projectPath)) {
|
|
36777
|
+
return {
|
|
36778
|
+
problem: `The game with pid ${pid} is running ${named.project.path}, not ${resolve4(projectPath)}. Running: ${endpoints.map(describe2).join("; ")}.`
|
|
36779
|
+
};
|
|
36780
|
+
}
|
|
36781
|
+
return { endpoint: named };
|
|
36782
|
+
}
|
|
36783
|
+
const tooNewToo = unspoken.find((game) => game.pid === pid);
|
|
36784
|
+
if (tooNewToo !== undefined) {
|
|
36785
|
+
return { problem: tooNew([tooNewToo]) };
|
|
36786
|
+
}
|
|
36787
|
+
return {
|
|
36788
|
+
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.`
|
|
36789
|
+
};
|
|
36790
|
+
}
|
|
36712
36791
|
if (endpoints.length === 0) {
|
|
36713
36792
|
if (unspoken.length > 0) {
|
|
36714
36793
|
return { problem: tooNew(unspoken) };
|
|
@@ -36739,14 +36818,14 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
36739
36818
|
return { endpoint: matching[0] };
|
|
36740
36819
|
}
|
|
36741
36820
|
return {
|
|
36742
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}.
|
|
36821
|
+
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
36743
36822
|
};
|
|
36744
36823
|
}
|
|
36745
36824
|
if (endpoints.length === 1 && endpoints[0]) {
|
|
36746
36825
|
return { endpoint: endpoints[0] };
|
|
36747
36826
|
}
|
|
36748
36827
|
return {
|
|
36749
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one.`
|
|
36828
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass projectPath to choose one, or pid when they are from one project.`
|
|
36750
36829
|
};
|
|
36751
36830
|
}
|
|
36752
36831
|
function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
@@ -37151,6 +37230,9 @@ function patienceForFrames(frames, atLeast) {
|
|
|
37151
37230
|
function projectSpelling(projectPath, absolutePath) {
|
|
37152
37231
|
return `res://${relative3(realPathOr(projectPath), realPathOr(absolutePath)).replaceAll("\\", "/")}`;
|
|
37153
37232
|
}
|
|
37233
|
+
function whichGame(args) {
|
|
37234
|
+
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
37235
|
+
}
|
|
37154
37236
|
function describeHalt(halt) {
|
|
37155
37237
|
switch (halt.reason) {
|
|
37156
37238
|
case "breakpoint":
|
|
@@ -37186,6 +37268,19 @@ function runIsUp(run, editorSays) {
|
|
|
37186
37268
|
}
|
|
37187
37269
|
return editorSays ?? stillRunning(run);
|
|
37188
37270
|
}
|
|
37271
|
+
function endedPreviousRun(ended) {
|
|
37272
|
+
if (ended === null) {
|
|
37273
|
+
return;
|
|
37274
|
+
}
|
|
37275
|
+
return ended.pid ?? true;
|
|
37276
|
+
}
|
|
37277
|
+
function endedToStartThis(ended) {
|
|
37278
|
+
if (ended === null) {
|
|
37279
|
+
return "";
|
|
37280
|
+
}
|
|
37281
|
+
const which = ended.pid === null ? "The game the editor was playing" : `The run that was going, pid ${ended.pid},`;
|
|
37282
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
37283
|
+
}
|
|
37189
37284
|
function runtimeVerdict(endpoint, after) {
|
|
37190
37285
|
if (endpoint !== null) {
|
|
37191
37286
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -37884,7 +37979,7 @@ class GodotServer {
|
|
|
37884
37979
|
switch (op) {
|
|
37885
37980
|
case "tree":
|
|
37886
37981
|
return await this.handleRuntimeCommand("get_tree", {
|
|
37887
|
-
|
|
37982
|
+
...whichGame(args),
|
|
37888
37983
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37889
37984
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
37890
37985
|
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
@@ -37893,36 +37988,36 @@ class GodotServer {
|
|
|
37893
37988
|
return await this.handleFindRuntimeNodes(args);
|
|
37894
37989
|
case "text":
|
|
37895
37990
|
return await this.handleRuntimeCommand("read_text", {
|
|
37896
|
-
|
|
37991
|
+
...whichGame(args),
|
|
37897
37992
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
37898
37993
|
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
37899
37994
|
limit: readPositiveNumber(args, "limit") ?? 500
|
|
37900
37995
|
});
|
|
37901
37996
|
case "rect":
|
|
37902
37997
|
return await this.handleRuntimeCommand("get_rect", {
|
|
37903
|
-
|
|
37998
|
+
...whichGame(args),
|
|
37904
37999
|
path: readNonEmptyString(args, "nodePath") ?? ""
|
|
37905
38000
|
});
|
|
37906
38001
|
case "property":
|
|
37907
38002
|
return await this.handleRuntimeCommand("get_property", {
|
|
37908
|
-
|
|
38003
|
+
...whichGame(args),
|
|
37909
38004
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37910
38005
|
property: readNonEmptyString(args, "property") ?? ""
|
|
37911
38006
|
});
|
|
37912
38007
|
default:
|
|
37913
38008
|
return await this.handleRuntimeCommand("get_metrics", {
|
|
37914
|
-
|
|
38009
|
+
...whichGame(args),
|
|
37915
38010
|
metrics: readArray(args, "metrics") ?? []
|
|
37916
38011
|
});
|
|
37917
38012
|
}
|
|
37918
38013
|
case "runtime_invoke":
|
|
37919
38014
|
return op === "set" ? await this.handleRuntimeCommand("set_property", {
|
|
37920
|
-
|
|
38015
|
+
...whichGame(args),
|
|
37921
38016
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37922
38017
|
property: readString(args, "property") ?? "",
|
|
37923
38018
|
value: args["value"]
|
|
37924
38019
|
}) : await this.handleRuntimeCommand("call_method", {
|
|
37925
|
-
|
|
38020
|
+
...whichGame(args),
|
|
37926
38021
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37927
38022
|
method: readString(args, "method") ?? "",
|
|
37928
38023
|
args: readArray(args, "args") ?? []
|
|
@@ -37932,7 +38027,7 @@ class GodotServer {
|
|
|
37932
38027
|
case "runtime_input":
|
|
37933
38028
|
if (op === "click") {
|
|
37934
38029
|
return await this.handleRuntimeCommand("click", {
|
|
37935
|
-
|
|
38030
|
+
...whichGame(args),
|
|
37936
38031
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37937
38032
|
button: readString(args, "button") ?? "left",
|
|
37938
38033
|
double: readBoolean(args, "doubleClick") ?? false
|
|
@@ -37940,7 +38035,7 @@ class GodotServer {
|
|
|
37940
38035
|
}
|
|
37941
38036
|
if (op === "choose") {
|
|
37942
38037
|
return await this.handleRuntimeCommand("choose", {
|
|
37943
|
-
|
|
38038
|
+
...whichGame(args),
|
|
37944
38039
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
37945
38040
|
text: readString(args, "text") ?? "",
|
|
37946
38041
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -37971,7 +38066,9 @@ class GodotServer {
|
|
|
37971
38066
|
return this.jsonTextResponse({
|
|
37972
38067
|
...asParams(JSON.parse(answered.content[0]?.text ?? "{}")),
|
|
37973
38068
|
held: this.breakpointsAsProjectSpells(project.value.path, held),
|
|
37974
|
-
|
|
38069
|
+
setInEditor: this.breakpointsAsProjectSpells(project.value.path, session.breakpointsInEditor()),
|
|
38070
|
+
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.",
|
|
38071
|
+
...this.breakpointsAtRisk()
|
|
37975
38072
|
});
|
|
37976
38073
|
}
|
|
37977
38074
|
case "debug_control":
|
|
@@ -38045,6 +38142,16 @@ class GodotServer {
|
|
|
38045
38142
|
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
38143
|
};
|
|
38047
38144
|
}
|
|
38145
|
+
const going = await this.runStillGoing(game);
|
|
38146
|
+
if (!going) {
|
|
38147
|
+
return {
|
|
38148
|
+
ok: false,
|
|
38149
|
+
response: this.createErrorResponse("The last run has already ended, so there is no debug session to answer for.", [
|
|
38150
|
+
"editor_output still reads what it printed, until the next run starts",
|
|
38151
|
+
"editor_run start plays another, which the debugger can hold"
|
|
38152
|
+
])
|
|
38153
|
+
};
|
|
38154
|
+
}
|
|
38048
38155
|
if (!game.throughEditor) {
|
|
38049
38156
|
return {
|
|
38050
38157
|
ok: false,
|
|
@@ -38066,7 +38173,7 @@ class GodotServer {
|
|
|
38066
38173
|
};
|
|
38067
38174
|
}
|
|
38068
38175
|
if (!this.dap().isStopped()) {
|
|
38069
|
-
const hold = await this.holdOf(game);
|
|
38176
|
+
const hold = await this.holdOf(game, going);
|
|
38070
38177
|
if (hold.heldAt !== null) {
|
|
38071
38178
|
if (intent === "move") {
|
|
38072
38179
|
return { ok: true, value: game };
|
|
@@ -38632,6 +38739,12 @@ class GodotServer {
|
|
|
38632
38739
|
this.dapClient.setOutputSink((line) => {
|
|
38633
38740
|
this.writeToTranscript(line);
|
|
38634
38741
|
});
|
|
38742
|
+
this.dapClient.setBreakpointsSink((held) => {
|
|
38743
|
+
const project = this.godotBridge.getStatus().projectPath ?? this.ownProject;
|
|
38744
|
+
if (project !== null) {
|
|
38745
|
+
writeBreakpointNote(project, held);
|
|
38746
|
+
}
|
|
38747
|
+
});
|
|
38635
38748
|
}
|
|
38636
38749
|
return this.dapClient;
|
|
38637
38750
|
}
|
|
@@ -38643,6 +38756,15 @@ class GodotServer {
|
|
|
38643
38756
|
}
|
|
38644
38757
|
return client;
|
|
38645
38758
|
}
|
|
38759
|
+
breakpointsAtRisk() {
|
|
38760
|
+
const status = this.godotBridge.getStatus();
|
|
38761
|
+
if (!status.connected || status.syncsBreakpoints === true) {
|
|
38762
|
+
return {};
|
|
38763
|
+
}
|
|
38764
|
+
return {
|
|
38765
|
+
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."
|
|
38766
|
+
};
|
|
38767
|
+
}
|
|
38646
38768
|
breakpointsAsProjectSpells(projectPath, held) {
|
|
38647
38769
|
return held.map((one) => ({
|
|
38648
38770
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
@@ -38705,6 +38827,7 @@ class GodotServer {
|
|
|
38705
38827
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38706
38828
|
startupError: this.bridgeStartupError,
|
|
38707
38829
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
38830
|
+
...this.breakpointsAtRisk(),
|
|
38708
38831
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
38709
38832
|
announcedAt: this.announcedAt ?? undefined,
|
|
38710
38833
|
projectIs: this.projectHasMovedOn() ?? undefined,
|
|
@@ -38756,9 +38879,14 @@ class GodotServer {
|
|
|
38756
38879
|
const playing = await this.editorPlayingState();
|
|
38757
38880
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
38758
38881
|
const current = this.currentRun();
|
|
38759
|
-
|
|
38882
|
+
const active = current === null ? false : await this.runStillGoing(current, playing?.playing ?? null);
|
|
38883
|
+
return {
|
|
38884
|
+
playing,
|
|
38885
|
+
active,
|
|
38886
|
+
hold: current === null ? {} : await this.holdOf(current, active)
|
|
38887
|
+
};
|
|
38760
38888
|
})();
|
|
38761
|
-
const [games, { playing, hold }] = await Promise.all([pinged, asked]);
|
|
38889
|
+
const [games, { playing, active, hold }] = await Promise.all([pinged, asked]);
|
|
38762
38890
|
return this.jsonTextResponse({
|
|
38763
38891
|
editor: {
|
|
38764
38892
|
...this.getEditorStatusPayload(),
|
|
@@ -38770,7 +38898,7 @@ class GodotServer {
|
|
|
38770
38898
|
problem: located.ok ? undefined : GodotLocator2.refusal(located).message
|
|
38771
38899
|
},
|
|
38772
38900
|
game: {
|
|
38773
|
-
processActive:
|
|
38901
|
+
processActive: active,
|
|
38774
38902
|
playingInEditor: playing,
|
|
38775
38903
|
...hold,
|
|
38776
38904
|
runtimeConnected: games.some((game) => game.reachable),
|
|
@@ -38983,10 +39111,12 @@ class GodotServer {
|
|
|
38983
39111
|
if (op === "check") {
|
|
38984
39112
|
return await this.checkBoot(engine.value, project.value.path, sceneArgument, args, given.value);
|
|
38985
39113
|
}
|
|
39114
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38986
39115
|
let ended = null;
|
|
38987
|
-
|
|
39116
|
+
const before = this.currentRun();
|
|
39117
|
+
if (before !== null && await this.runStillGoing(before)) {
|
|
38988
39118
|
this.logDebug("Ending the running game before starting another");
|
|
38989
|
-
ended = this.
|
|
39119
|
+
ended = { pid: before.pid ?? this.announcedPidOf(before) ?? null };
|
|
38990
39120
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
38991
39121
|
}
|
|
38992
39122
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
@@ -39010,8 +39140,8 @@ class GodotServer {
|
|
|
39010
39140
|
const started = this.spawnKeptGame(engine.value, cmdArgs, project.value.path);
|
|
39011
39141
|
this.activeProcess = started;
|
|
39012
39142
|
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));
|
|
39143
|
+
const endedForThis = endedToStartThis(ended);
|
|
39144
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended?.pid && alive(one.pid));
|
|
39015
39145
|
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
39016
39146
|
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
39147
|
return this.jsonTextResponse({
|
|
@@ -39022,23 +39152,38 @@ class GodotServer {
|
|
|
39022
39152
|
arguments: cmdArgs,
|
|
39023
39153
|
transcript: started.transcript,
|
|
39024
39154
|
refreshedClasses: refreshed.value,
|
|
39025
|
-
endedPreviousRun: ended
|
|
39155
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
39026
39156
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
39027
39157
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
39028
39158
|
});
|
|
39029
39159
|
}
|
|
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
39160
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
39036
39161
|
if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
39037
39162
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
39038
39163
|
}
|
|
39164
|
+
let asking = null;
|
|
39165
|
+
let editorAskedAt = 0;
|
|
39166
|
+
let editorSaysGoing = true;
|
|
39039
39167
|
const endpoint = await announcedSince(projectPath, before, {
|
|
39040
39168
|
budgetMs,
|
|
39041
|
-
giveUp: () =>
|
|
39169
|
+
giveUp: () => {
|
|
39170
|
+
if (this.dapClient?.isStopped() === true) {
|
|
39171
|
+
return true;
|
|
39172
|
+
}
|
|
39173
|
+
const run = this.currentRun();
|
|
39174
|
+
if (run === null || !run.throughEditor) {
|
|
39175
|
+
return !stillRunning(run);
|
|
39176
|
+
}
|
|
39177
|
+
if (asking === null && Date.now() - editorAskedAt >= RUN_POLL_MS) {
|
|
39178
|
+
editorAskedAt = Date.now();
|
|
39179
|
+
asking = this.runStillGoing(run).then((going) => {
|
|
39180
|
+
editorSaysGoing = going;
|
|
39181
|
+
}).finally(() => {
|
|
39182
|
+
asking = null;
|
|
39183
|
+
});
|
|
39184
|
+
}
|
|
39185
|
+
return !editorSaysGoing;
|
|
39186
|
+
}
|
|
39042
39187
|
});
|
|
39043
39188
|
const going = this.currentRun();
|
|
39044
39189
|
if (endpoint !== null && going?.throughEditor === true && going.announcedPid === undefined) {
|
|
@@ -39048,7 +39193,7 @@ class GodotServer {
|
|
|
39048
39193
|
addon: true,
|
|
39049
39194
|
budgetMs,
|
|
39050
39195
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
39051
|
-
running: await this.
|
|
39196
|
+
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
39052
39197
|
withArgs
|
|
39053
39198
|
});
|
|
39054
39199
|
}
|
|
@@ -39089,7 +39234,8 @@ class GodotServer {
|
|
|
39089
39234
|
startedAt,
|
|
39090
39235
|
exitCode: null,
|
|
39091
39236
|
throughEditor: true,
|
|
39092
|
-
brokeOn: null
|
|
39237
|
+
brokeOn: null,
|
|
39238
|
+
announcedBefore: alreadyPlaying
|
|
39093
39239
|
};
|
|
39094
39240
|
this.activeProcess = played;
|
|
39095
39241
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -39100,14 +39246,14 @@ class GodotServer {
|
|
|
39100
39246
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
39101
39247
|
refreshedClasses,
|
|
39102
39248
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
39103
|
-
endedPreviousRun: ended
|
|
39249
|
+
endedPreviousRun: endedPreviousRun(ended),
|
|
39104
39250
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
39105
39251
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
39106
39252
|
scriptPath: projectSpelling(projectPath, one.scriptPath),
|
|
39107
39253
|
reason: one.reason
|
|
39108
39254
|
})),
|
|
39109
39255
|
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
|
|
39256
|
+
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
39257
|
});
|
|
39112
39258
|
}
|
|
39113
39259
|
drainEditorOutput(game) {
|
|
@@ -39323,15 +39469,19 @@ class GodotServer {
|
|
|
39323
39469
|
brokeOn: null,
|
|
39324
39470
|
pickedUpPlaying: true
|
|
39325
39471
|
};
|
|
39326
|
-
const announced = this.theOneAnnouncedForOurProject();
|
|
39327
|
-
if (announced !== undefined) {
|
|
39328
|
-
picked.announcedPid = announced.pid;
|
|
39329
|
-
}
|
|
39330
39472
|
this.activeProcess = picked;
|
|
39331
39473
|
}
|
|
39332
|
-
|
|
39333
|
-
|
|
39334
|
-
|
|
39474
|
+
announcedPidOf(run) {
|
|
39475
|
+
if (!run.throughEditor || run.announcedPid !== undefined) {
|
|
39476
|
+
return run.announcedPid;
|
|
39477
|
+
}
|
|
39478
|
+
const before = run.announcedBefore ?? new Set;
|
|
39479
|
+
const fresh = this.allAnnouncedForOurProject(runtimesAnnounced().running).filter((one) => !before.has(one.pid));
|
|
39480
|
+
const theOne = fresh.length === 1 ? fresh[0] : undefined;
|
|
39481
|
+
if (theOne !== undefined) {
|
|
39482
|
+
run.announcedPid = theOne.pid;
|
|
39483
|
+
}
|
|
39484
|
+
return run.announcedPid;
|
|
39335
39485
|
}
|
|
39336
39486
|
adoptRecordedRun() {
|
|
39337
39487
|
const record = readRunRecord();
|
|
@@ -39461,12 +39611,22 @@ class GodotServer {
|
|
|
39461
39611
|
}
|
|
39462
39612
|
const budgetMs = readPositiveNumber(args, "timeoutMs") ?? WAIT_FOR_RUN_MS;
|
|
39463
39613
|
const until = Date.now() + budgetMs;
|
|
39464
|
-
while (
|
|
39614
|
+
while (await this.runStillGoing(run) && Date.now() < until) {
|
|
39465
39615
|
this.drainEditorOutput(run);
|
|
39466
39616
|
this.drainTranscript(run);
|
|
39467
39617
|
await delay3(RUN_POLL_MS);
|
|
39468
39618
|
}
|
|
39469
|
-
return await this.handleGetDebugOutput(args,
|
|
39619
|
+
return await this.handleGetDebugOutput(args, await this.runStillGoing(run) ? budgetMs : undefined);
|
|
39620
|
+
}
|
|
39621
|
+
async runStillGoing(run, editorSays) {
|
|
39622
|
+
if (!run.throughEditor) {
|
|
39623
|
+
return stillRunning(run);
|
|
39624
|
+
}
|
|
39625
|
+
const announced = this.announcedPidOf(run);
|
|
39626
|
+
if (announced !== undefined && !alive(announced)) {
|
|
39627
|
+
return false;
|
|
39628
|
+
}
|
|
39629
|
+
return runIsUp(run, editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays);
|
|
39470
39630
|
}
|
|
39471
39631
|
async handleGetDebugOutput(args, waitedMs) {
|
|
39472
39632
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -39476,7 +39636,8 @@ class GodotServer {
|
|
|
39476
39636
|
}
|
|
39477
39637
|
this.drainEditorOutput(run);
|
|
39478
39638
|
this.drainTranscript(run);
|
|
39479
|
-
|
|
39639
|
+
const going = await this.runStillGoing(run);
|
|
39640
|
+
if (run.endedUnwatched !== true && !going) {
|
|
39480
39641
|
run.log.finish();
|
|
39481
39642
|
run.endedUnwatched = run.exitCode === null;
|
|
39482
39643
|
}
|
|
@@ -39487,12 +39648,13 @@ class GodotServer {
|
|
|
39487
39648
|
contains: readNonEmptyString(args, "contains"),
|
|
39488
39649
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
39489
39650
|
});
|
|
39490
|
-
const hold = await this.holdOf(run);
|
|
39651
|
+
const hold = await this.holdOf(run, going);
|
|
39491
39652
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
39492
|
-
const
|
|
39653
|
+
const processToAsk = run.pid ?? this.announcedPidOf(run) ?? null;
|
|
39654
|
+
const cpuSeconds = wanted && processToAsk !== null && stillRunning(run) ? await cpuSecondsOf(processToAsk) : undefined;
|
|
39493
39655
|
const notes = [];
|
|
39494
39656
|
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
39495
|
-
notes.push(
|
|
39657
|
+
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
39658
|
}
|
|
39497
39659
|
if (waitedMs !== undefined) {
|
|
39498
39660
|
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 +39678,10 @@ class GodotServer {
|
|
|
39516
39678
|
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
39679
|
}
|
|
39518
39680
|
return this.jsonTextResponse({
|
|
39519
|
-
running:
|
|
39681
|
+
running: going,
|
|
39520
39682
|
exitCode: run.exitCode,
|
|
39521
39683
|
through: run.throughEditor ? "editor" : "gdharness",
|
|
39522
|
-
pid: run.pid,
|
|
39684
|
+
pid: run.pid ?? this.announcedPidOf(run) ?? null,
|
|
39523
39685
|
errors: run.log.count("error"),
|
|
39524
39686
|
warnings: run.log.count("warning"),
|
|
39525
39687
|
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
@@ -39545,18 +39707,19 @@ class GodotServer {
|
|
|
39545
39707
|
}
|
|
39546
39708
|
this.drainEditorOutput(stopped);
|
|
39547
39709
|
this.drainTranscript(stopped);
|
|
39710
|
+
const wasRunning = await this.runStillGoing(stopped);
|
|
39548
39711
|
this.logDebug("Stopping the running game");
|
|
39549
39712
|
await this.endActiveGame("editor_run stop");
|
|
39550
39713
|
return this.jsonTextResponse({
|
|
39551
39714
|
stopped: true,
|
|
39552
39715
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
39553
|
-
endedPid: stopped.pid,
|
|
39554
|
-
exitedBeforeStop: stopped.exitCode !== null,
|
|
39716
|
+
endedPid: stopped.pid ?? this.announcedPidOf(stopped) ?? null,
|
|
39717
|
+
exitedBeforeStop: stopped.exitCode !== null || !wasRunning,
|
|
39555
39718
|
exitCode: stopped.exitCode,
|
|
39556
39719
|
errors: stopped.log.count("error"),
|
|
39557
39720
|
warnings: stopped.log.count("warning"),
|
|
39558
39721
|
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.",
|
|
39722
|
+
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
39723
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
39561
39724
|
});
|
|
39562
39725
|
}
|
|
@@ -39713,21 +39876,21 @@ class GodotServer {
|
|
|
39713
39876
|
if (run === null || !run.throughEditor || !stillRunning(run) || session === null) {
|
|
39714
39877
|
return null;
|
|
39715
39878
|
}
|
|
39716
|
-
|
|
39717
|
-
if (its !== endpoint.pid || !session.holdIsKnown()) {
|
|
39879
|
+
if (this.announcedPidOf(run) !== endpoint.pid || !session.holdIsKnown()) {
|
|
39718
39880
|
return null;
|
|
39719
39881
|
}
|
|
39720
39882
|
return session.whereItStopped();
|
|
39721
39883
|
}
|
|
39722
|
-
async holdOf(run) {
|
|
39723
|
-
if (!run.throughEditor || !
|
|
39884
|
+
async holdOf(run, going) {
|
|
39885
|
+
if (!run.throughEditor || !(going ?? await this.runStillGoing(run))) {
|
|
39724
39886
|
return { heldAt: null };
|
|
39725
39887
|
}
|
|
39726
39888
|
const session = this.dapClient;
|
|
39727
39889
|
if (session?.holdIsKnown()) {
|
|
39728
39890
|
return { heldAt: session.whereItStopped() };
|
|
39729
39891
|
}
|
|
39730
|
-
const
|
|
39892
|
+
const its = this.announcedPidOf(run);
|
|
39893
|
+
const endpoint = its === undefined ? undefined : runtimesAnnounced().running.find((one) => one.pid === its);
|
|
39731
39894
|
if (endpoint === undefined) {
|
|
39732
39895
|
return {
|
|
39733
39896
|
heldAt: undefined,
|
|
@@ -39752,9 +39915,9 @@ class GodotServer {
|
|
|
39752
39915
|
return { heldAt: undefined, heldUnknown: true, heldNote: reply.message };
|
|
39753
39916
|
}
|
|
39754
39917
|
async handleRuntimeCommand(command, args, timeoutMs = this.runtimeTimeoutMs()) {
|
|
39755
|
-
const { op: _op, projectPath, ...params } = asParams(args);
|
|
39918
|
+
const { op: _op, projectPath, pid, ...params } = asParams(args);
|
|
39756
39919
|
const announced = runtimesAnnounced();
|
|
39757
|
-
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken);
|
|
39920
|
+
const choice = chooseRuntime(announced.running, typeof projectPath === "string" ? projectPath : this.ownProject ?? undefined, announced.unspoken, typeof pid === "number" ? pid : undefined);
|
|
39758
39921
|
if ("problem" in choice) {
|
|
39759
39922
|
return this.createErrorResponse(choice.problem);
|
|
39760
39923
|
}
|
|
@@ -39820,7 +39983,7 @@ class GodotServer {
|
|
|
39820
39983
|
const includeHidden = readBoolean(args, "includeHidden");
|
|
39821
39984
|
return await this.handleRuntimeCommand("find_nodes", {
|
|
39822
39985
|
...filters,
|
|
39823
|
-
|
|
39986
|
+
...whichGame(args),
|
|
39824
39987
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
39825
39988
|
limit: readPositiveNumber(args, "limit") ?? 100,
|
|
39826
39989
|
...property === undefined ? {} : { property },
|
|
@@ -39831,18 +39994,18 @@ class GodotServer {
|
|
|
39831
39994
|
if (op === "frames") {
|
|
39832
39995
|
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
39833
39996
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
39834
|
-
return await this.handleRuntimeCommand("wait_frames", {
|
|
39997
|
+
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, waited);
|
|
39835
39998
|
}
|
|
39836
39999
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
39837
40000
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
39838
40001
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
39839
40002
|
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
39840
|
-
|
|
40003
|
+
...whichGame(args),
|
|
39841
40004
|
path: nodePath,
|
|
39842
40005
|
signal: readString(args, "signal") ?? "",
|
|
39843
40006
|
timeout_ms: timeoutMs
|
|
39844
40007
|
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
39845
|
-
|
|
40008
|
+
...whichGame(args),
|
|
39846
40009
|
path: nodePath,
|
|
39847
40010
|
property: readString(args, "property") ?? "",
|
|
39848
40011
|
value: args["value"],
|
|
@@ -41397,7 +41560,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
41397
41560
|
## Start here
|
|
41398
41561
|
|
|
41399
41562
|
- ${projectPathSentence()}. The \`runtime_*\` and \`debug_*\` ones pick between running
|
|
41400
|
-
games instead
|
|
41563
|
+
games instead, by \`projectPath\`, or by \`pid\` when several run from one project, as a bench
|
|
41564
|
+
and its workers do; the others answer about the editor this server is connected to. There is no
|
|
41401
41565
|
ambient project, and an argument a tool does not declare is refused rather than ignored.
|
|
41402
41566
|
- \`editor_status\` says whether an editor is connected and whether its addon matches the server.
|
|
41403
41567
|
\`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
|
|
@@ -41469,6 +41633,12 @@ The autoload reaches an export. \`gdharness runtime off\` in the project before
|
|
|
41469
41633
|
move. There is no pause and no step out: Godot's debug adapter implements neither, so use a
|
|
41470
41634
|
breakpoint on the line you want instead.
|
|
41471
41635
|
|
|
41636
|
+
A held game draws nothing and answers no \`runtime_*\` call. The server says so at once when it
|
|
41637
|
+
knows, naming why the game is held and \`debug_control continue\` as what lets it go, and
|
|
41638
|
+
\`editor_output\` and \`editor_status\` carry the same under \`heldAt\`. A breakpoint set with
|
|
41639
|
+
\`debug_breakpoint\` holds for every start until \`debug_breakpoint remove\`, across a reconnect,
|
|
41640
|
+
so take it off when the question it was for is answered.
|
|
41641
|
+
|
|
41472
41642
|
## Editing
|
|
41473
41643
|
|
|
41474
41644
|
\`scene_tree\` for what a scene holds, rather than what the \`.tscn\` text implies. \`scene_node\`,
|