gdharness 0.13.33 → 0.13.35
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 +68 -14
- package/build/index.js +67 -13
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -16022,7 +16022,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16022
16022
|
},
|
|
16023
16023
|
{
|
|
16024
16024
|
name: "editor_status",
|
|
16025
|
-
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. 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. 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.',
|
|
16025
|
+
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. 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. 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: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
|
|
16026
16026
|
parameters: {},
|
|
16027
16027
|
requires: []
|
|
16028
16028
|
},
|
|
@@ -36752,6 +36752,9 @@ function judgeRun(record, running, needed) {
|
|
|
36752
36752
|
if (wanted !== null && !said.includes(wanted)) {
|
|
36753
36753
|
return false;
|
|
36754
36754
|
}
|
|
36755
|
+
if (running.kind === "commandLine" && AN_EDITOR.test(said)) {
|
|
36756
|
+
return false;
|
|
36757
|
+
}
|
|
36755
36758
|
if (record.projectPath === "") {
|
|
36756
36759
|
return wanted !== null;
|
|
36757
36760
|
}
|
|
@@ -36801,8 +36804,10 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
36801
36804
|
} catch {}
|
|
36802
36805
|
}
|
|
36803
36806
|
}
|
|
36807
|
+
var AN_EDITOR;
|
|
36804
36808
|
var init_run_record = __esm(() => {
|
|
36805
36809
|
init_runtime_client();
|
|
36810
|
+
AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
36806
36811
|
});
|
|
36807
36812
|
|
|
36808
36813
|
// src/server.ts
|
|
@@ -38229,7 +38234,10 @@ class GodotServer {
|
|
|
38229
38234
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
38230
38235
|
}
|
|
38231
38236
|
anEditorIsStillComing(listeningSince) {
|
|
38232
|
-
return anEditorIsStillComing(listeningSince, this.
|
|
38237
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
38238
|
+
}
|
|
38239
|
+
launchedEditorIsUp() {
|
|
38240
|
+
return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
|
|
38233
38241
|
}
|
|
38234
38242
|
dap() {
|
|
38235
38243
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -38275,7 +38283,7 @@ class GodotServer {
|
|
|
38275
38283
|
addonIsStale: status.connected ? stale : undefined,
|
|
38276
38284
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
38277
38285
|
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
38278
|
-
awaitingLaunchedEditor: status.connected || this.
|
|
38286
|
+
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
38279
38287
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
38280
38288
|
startupError: this.bridgeStartupError,
|
|
38281
38289
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
@@ -38289,11 +38297,17 @@ class GodotServer {
|
|
|
38289
38297
|
}
|
|
38290
38298
|
async editorPlayingState() {
|
|
38291
38299
|
const status = this.godotBridge.getStatus();
|
|
38292
|
-
if (!status.connected
|
|
38300
|
+
if (!status.connected) {
|
|
38293
38301
|
return null;
|
|
38294
38302
|
}
|
|
38295
38303
|
try {
|
|
38296
|
-
const answer = await
|
|
38304
|
+
const answer = await Promise.race([
|
|
38305
|
+
this.godotBridge.invokeTool("playing_status", {}),
|
|
38306
|
+
delay3(PLAYING_STATUS_MS).then(() => null)
|
|
38307
|
+
]);
|
|
38308
|
+
if (answer === null) {
|
|
38309
|
+
return null;
|
|
38310
|
+
}
|
|
38297
38311
|
return {
|
|
38298
38312
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
38299
38313
|
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
@@ -38305,7 +38319,8 @@ class GodotServer {
|
|
|
38305
38319
|
}
|
|
38306
38320
|
async handleEditorStatus() {
|
|
38307
38321
|
const godotPath = await this.locator.find();
|
|
38308
|
-
const
|
|
38322
|
+
const announced = runtimesAnnounced();
|
|
38323
|
+
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
38309
38324
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
38310
38325
|
return {
|
|
38311
38326
|
pid: endpoint.pid,
|
|
@@ -38327,10 +38342,17 @@ class GodotServer {
|
|
|
38327
38342
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
38328
38343
|
},
|
|
38329
38344
|
game: {
|
|
38330
|
-
processActive:
|
|
38345
|
+
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
38331
38346
|
playingInEditor: playing,
|
|
38332
38347
|
runtimeConnected: games.some((game) => game.reachable),
|
|
38333
|
-
runtimes: games
|
|
38348
|
+
runtimes: games,
|
|
38349
|
+
unreadable: announced.unspoken.map((game) => ({
|
|
38350
|
+
pid: game.pid,
|
|
38351
|
+
project: game.project,
|
|
38352
|
+
protocol: game.protocol,
|
|
38353
|
+
serverSpeaks: RUNTIME_PROTOCOL,
|
|
38354
|
+
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
38355
|
+
}))
|
|
38334
38356
|
}
|
|
38335
38357
|
});
|
|
38336
38358
|
}
|
|
@@ -38480,7 +38502,7 @@ class GodotServer {
|
|
|
38480
38502
|
return { pid: null, error: started };
|
|
38481
38503
|
}
|
|
38482
38504
|
editor.unref();
|
|
38483
|
-
this.launchedEditor = editor.pid
|
|
38505
|
+
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
38484
38506
|
return { pid: editor.pid ?? null, error: null };
|
|
38485
38507
|
}
|
|
38486
38508
|
async portsForAnEditor() {
|
|
@@ -38527,6 +38549,7 @@ class GodotServer {
|
|
|
38527
38549
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
38528
38550
|
}
|
|
38529
38551
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
38552
|
+
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
38530
38553
|
const headless = resolveHeadless(args["headless"], {
|
|
38531
38554
|
platform: process.platform,
|
|
38532
38555
|
variables: process.env
|
|
@@ -38547,16 +38570,20 @@ class GodotServer {
|
|
|
38547
38570
|
this.activeProcess = started;
|
|
38548
38571
|
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." : "";
|
|
38549
38572
|
const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
38573
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
38574
|
+
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
38575
|
+
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.`;
|
|
38550
38576
|
return this.jsonTextResponse({
|
|
38551
38577
|
started: true,
|
|
38552
38578
|
through: "gdharness",
|
|
38579
|
+
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
38553
38580
|
pid: started.process.pid ?? null,
|
|
38554
38581
|
arguments: cmdArgs,
|
|
38555
38582
|
transcript: started.transcript,
|
|
38556
38583
|
refreshedClasses: refreshed.value,
|
|
38557
38584
|
endedPreviousRun: ended ?? undefined,
|
|
38558
38585
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
38559
|
-
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
|
|
38586
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
38560
38587
|
});
|
|
38561
38588
|
}
|
|
38562
38589
|
async gameIsUp() {
|
|
@@ -38872,7 +38899,12 @@ class GodotServer {
|
|
|
38872
38899
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
38873
38900
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
38874
38901
|
if (live !== null) {
|
|
38875
|
-
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + "
|
|
38902
|
+
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
|
|
38903
|
+
}
|
|
38904
|
+
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
38905
|
+
if (unreadable !== null) {
|
|
38906
|
+
const behind = unreadable.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";
|
|
38907
|
+
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
38876
38908
|
}
|
|
38877
38909
|
const record = readRunRecord();
|
|
38878
38910
|
if (record === null) {
|
|
@@ -38884,12 +38916,34 @@ class GodotServer {
|
|
|
38884
38916
|
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
38885
38917
|
}
|
|
38886
38918
|
aRuntimeOfOursIsAnswering() {
|
|
38919
|
+
return this.announcedForOurProject(discoverRuntimes());
|
|
38920
|
+
}
|
|
38921
|
+
aRuntimeOfOursIsTooFarOff() {
|
|
38922
|
+
return this.announcedForOurProject(runtimesAnnounced().unspoken);
|
|
38923
|
+
}
|
|
38924
|
+
announcedForOurProject(announced) {
|
|
38925
|
+
return this.allAnnouncedForOurProject(announced)[0] ?? null;
|
|
38926
|
+
}
|
|
38927
|
+
allAnnouncedForOurProject(announced) {
|
|
38887
38928
|
const status = this.godotBridge.getStatus();
|
|
38888
38929
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
38889
38930
|
if (mine === null) {
|
|
38890
|
-
return
|
|
38931
|
+
return [];
|
|
38891
38932
|
}
|
|
38892
|
-
return
|
|
38933
|
+
return announced.filter((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path));
|
|
38934
|
+
}
|
|
38935
|
+
everyRuntimeOfOurs() {
|
|
38936
|
+
const announced = runtimesAnnounced();
|
|
38937
|
+
return [
|
|
38938
|
+
...this.allAnnouncedForOurProject(announced.running).map((one) => ({
|
|
38939
|
+
pid: one.pid,
|
|
38940
|
+
protocol: null
|
|
38941
|
+
})),
|
|
38942
|
+
...this.allAnnouncedForOurProject(announced.unspoken).map((one) => ({
|
|
38943
|
+
pid: one.pid,
|
|
38944
|
+
protocol: one.protocol
|
|
38945
|
+
}))
|
|
38946
|
+
];
|
|
38893
38947
|
}
|
|
38894
38948
|
couldBeOurs(project) {
|
|
38895
38949
|
const status = this.godotBridge.getStatus();
|
|
@@ -39284,7 +39338,7 @@ async function runGodotServer() {
|
|
|
39284
39338
|
const server = new GodotServer;
|
|
39285
39339
|
await server.run();
|
|
39286
39340
|
}
|
|
39287
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
39341
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run6, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
39288
39342
|
var init_server3 = __esm(() => {
|
|
39289
39343
|
init_mcp();
|
|
39290
39344
|
init_stdio2();
|
package/build/index.js
CHANGED
|
@@ -28336,6 +28336,7 @@ function stillTheRecordedRun(record) {
|
|
|
28336
28336
|
function couldStillBeTheRecordedRun(record) {
|
|
28337
28337
|
return judgeRun(record, runningAs(record.pid), "possible");
|
|
28338
28338
|
}
|
|
28339
|
+
var AN_EDITOR = /(?:^|\s)(?:-e|--editor)(?:\s|$)/;
|
|
28339
28340
|
function judgeRun(record, running, needed) {
|
|
28340
28341
|
if (running === null) {
|
|
28341
28342
|
return false;
|
|
@@ -28346,6 +28347,9 @@ function judgeRun(record, running, needed) {
|
|
|
28346
28347
|
if (wanted !== null && !said.includes(wanted)) {
|
|
28347
28348
|
return false;
|
|
28348
28349
|
}
|
|
28350
|
+
if (running.kind === "commandLine" && AN_EDITOR.test(said)) {
|
|
28351
|
+
return false;
|
|
28352
|
+
}
|
|
28349
28353
|
if (record.projectPath === "") {
|
|
28350
28354
|
return wanted !== null;
|
|
28351
28355
|
}
|
|
@@ -29104,7 +29108,7 @@ var TOOL_SPECS = [
|
|
|
29104
29108
|
},
|
|
29105
29109
|
{
|
|
29106
29110
|
name: "editor_status",
|
|
29107
|
-
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. 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. 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.',
|
|
29111
|
+
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. 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. 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: installing moves the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with. Those games are real and running, but no tool here can reach them until the two halves match, so runtimes stays empty and runtimeConnected stays false.',
|
|
29108
29112
|
parameters: {},
|
|
29109
29113
|
requires: []
|
|
29110
29114
|
},
|
|
@@ -29651,6 +29655,7 @@ function outsideTheirLists(spec, args) {
|
|
|
29651
29655
|
var BRIDGE_RETRY_MS = 2000;
|
|
29652
29656
|
var SUCCESSOR_CHECK_MS = 1e4;
|
|
29653
29657
|
var CACHE_WRITE_MS = 2000;
|
|
29658
|
+
var PLAYING_STATUS_MS = 1000;
|
|
29654
29659
|
var SCAN_START_MS = 2000;
|
|
29655
29660
|
var WAIT_FOR_RUN_MS = 600000;
|
|
29656
29661
|
var RUN_POLL_MS = 250;
|
|
@@ -30969,7 +30974,10 @@ class GodotServer {
|
|
|
30969
30974
|
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
30970
30975
|
}
|
|
30971
30976
|
anEditorIsStillComing(listeningSince) {
|
|
30972
|
-
return anEditorIsStillComing(listeningSince, this.
|
|
30977
|
+
return anEditorIsStillComing(listeningSince, this.launchedEditorIsUp());
|
|
30978
|
+
}
|
|
30979
|
+
launchedEditorIsUp() {
|
|
30980
|
+
return this.launchedEditor?.exitCode === null && this.launchedEditor.signalCode === null;
|
|
30973
30981
|
}
|
|
30974
30982
|
dap() {
|
|
30975
30983
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
@@ -31015,7 +31023,7 @@ class GodotServer {
|
|
|
31015
31023
|
addonIsStale: status.connected ? stale : undefined,
|
|
31016
31024
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
31017
31025
|
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
31018
|
-
awaitingLaunchedEditor: status.connected || this.
|
|
31026
|
+
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
31019
31027
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
31020
31028
|
startupError: this.bridgeStartupError,
|
|
31021
31029
|
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
@@ -31029,11 +31037,17 @@ class GodotServer {
|
|
|
31029
31037
|
}
|
|
31030
31038
|
async editorPlayingState() {
|
|
31031
31039
|
const status = this.godotBridge.getStatus();
|
|
31032
|
-
if (!status.connected
|
|
31040
|
+
if (!status.connected) {
|
|
31033
31041
|
return null;
|
|
31034
31042
|
}
|
|
31035
31043
|
try {
|
|
31036
|
-
const answer = await
|
|
31044
|
+
const answer = await Promise.race([
|
|
31045
|
+
this.godotBridge.invokeTool("playing_status", {}),
|
|
31046
|
+
delay3(PLAYING_STATUS_MS).then(() => null)
|
|
31047
|
+
]);
|
|
31048
|
+
if (answer === null) {
|
|
31049
|
+
return null;
|
|
31050
|
+
}
|
|
31037
31051
|
return {
|
|
31038
31052
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
31039
31053
|
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
@@ -31045,7 +31059,8 @@ class GodotServer {
|
|
|
31045
31059
|
}
|
|
31046
31060
|
async handleEditorStatus() {
|
|
31047
31061
|
const godotPath = await this.locator.find();
|
|
31048
|
-
const
|
|
31062
|
+
const announced = runtimesAnnounced();
|
|
31063
|
+
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
31049
31064
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
31050
31065
|
return {
|
|
31051
31066
|
pid: endpoint.pid,
|
|
@@ -31067,10 +31082,17 @@ class GodotServer {
|
|
|
31067
31082
|
version: godotPath === null ? null : await this.godotVersion(godotPath)
|
|
31068
31083
|
},
|
|
31069
31084
|
game: {
|
|
31070
|
-
processActive:
|
|
31085
|
+
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
31071
31086
|
playingInEditor: playing,
|
|
31072
31087
|
runtimeConnected: games.some((game) => game.reachable),
|
|
31073
|
-
runtimes: games
|
|
31088
|
+
runtimes: games,
|
|
31089
|
+
unreadable: announced.unspoken.map((game) => ({
|
|
31090
|
+
pid: game.pid,
|
|
31091
|
+
project: game.project,
|
|
31092
|
+
protocol: game.protocol,
|
|
31093
|
+
serverSpeaks: RUNTIME_PROTOCOL,
|
|
31094
|
+
behind: game.protocol > RUNTIME_PROTOCOL ? "server" : "addon"
|
|
31095
|
+
}))
|
|
31074
31096
|
}
|
|
31075
31097
|
});
|
|
31076
31098
|
}
|
|
@@ -31220,7 +31242,7 @@ class GodotServer {
|
|
|
31220
31242
|
return { pid: null, error: started };
|
|
31221
31243
|
}
|
|
31222
31244
|
editor.unref();
|
|
31223
|
-
this.launchedEditor = editor.pid
|
|
31245
|
+
this.launchedEditor = editor.pid === undefined ? null : editor;
|
|
31224
31246
|
return { pid: editor.pid ?? null, error: null };
|
|
31225
31247
|
}
|
|
31226
31248
|
async portsForAnEditor() {
|
|
@@ -31267,6 +31289,7 @@ class GodotServer {
|
|
|
31267
31289
|
await this.endActiveGame("editor_run start, which ends the run that was going before it starts another");
|
|
31268
31290
|
}
|
|
31269
31291
|
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
31292
|
+
const wereHereFirst = this.everyRuntimeOfOurs();
|
|
31270
31293
|
const headless = resolveHeadless(args["headless"], {
|
|
31271
31294
|
platform: process.platform,
|
|
31272
31295
|
variables: process.env
|
|
@@ -31287,16 +31310,20 @@ class GodotServer {
|
|
|
31287
31310
|
this.activeProcess = started;
|
|
31288
31311
|
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." : "";
|
|
31289
31312
|
const endedForThis = ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
31313
|
+
const stranded = wereHereFirst.filter((one) => one.pid !== ended && alive(one.pid));
|
|
31314
|
+
const named = stranded.map((one) => one.protocol === null ? `pid ${one.pid}` : `pid ${one.pid} on protocol ${one.protocol}`).join(", ");
|
|
31315
|
+
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.`;
|
|
31290
31316
|
return this.jsonTextResponse({
|
|
31291
31317
|
started: true,
|
|
31292
31318
|
through: "gdharness",
|
|
31319
|
+
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
31293
31320
|
pid: started.process.pid ?? null,
|
|
31294
31321
|
arguments: cmdArgs,
|
|
31295
31322
|
transcript: started.transcript,
|
|
31296
31323
|
refreshedClasses: refreshed.value,
|
|
31297
31324
|
endedPreviousRun: ended ?? undefined,
|
|
31298
31325
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
31299
|
-
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
|
|
31326
|
+
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}${alsoRunning}`
|
|
31300
31327
|
});
|
|
31301
31328
|
}
|
|
31302
31329
|
async gameIsUp() {
|
|
@@ -31612,7 +31639,12 @@ class GodotServer {
|
|
|
31612
31639
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
31613
31640
|
const live = this.aRuntimeOfOursIsAnswering();
|
|
31614
31641
|
if (live !== null) {
|
|
31615
|
-
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + "
|
|
31642
|
+
return `A game is running for ${live.project.path} and has announced its runtime on port ${live.port}, pid ${live.pid},` + " but this server did not start it and cannot reach it through the editor, so editor_run and" + " editor_output have nothing to answer about. The runtime_* tools reach it now." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well." + (this.godotBridge.getStatus().connected ? "" : " editor_status says whether an editor is on its way, and editor_run can end it once one is.");
|
|
31643
|
+
}
|
|
31644
|
+
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
31645
|
+
if (unreadable !== null) {
|
|
31646
|
+
const behind = unreadable.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";
|
|
31647
|
+
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
31616
31648
|
}
|
|
31617
31649
|
const record = readRunRecord();
|
|
31618
31650
|
if (record === null) {
|
|
@@ -31624,12 +31656,34 @@ class GodotServer {
|
|
|
31624
31656
|
return `No game of this server's is running.${noEditor} A run started from ${whose} is recorded on this machine, and ${ours}, so it is not this server's to answer for or to end. Start one with editor_run.`;
|
|
31625
31657
|
}
|
|
31626
31658
|
aRuntimeOfOursIsAnswering() {
|
|
31659
|
+
return this.announcedForOurProject(discoverRuntimes());
|
|
31660
|
+
}
|
|
31661
|
+
aRuntimeOfOursIsTooFarOff() {
|
|
31662
|
+
return this.announcedForOurProject(runtimesAnnounced().unspoken);
|
|
31663
|
+
}
|
|
31664
|
+
announcedForOurProject(announced) {
|
|
31665
|
+
return this.allAnnouncedForOurProject(announced)[0] ?? null;
|
|
31666
|
+
}
|
|
31667
|
+
allAnnouncedForOurProject(announced) {
|
|
31627
31668
|
const status = this.godotBridge.getStatus();
|
|
31628
31669
|
const mine = this.ownProject ?? (status.connected ? status.projectPath ?? null : null);
|
|
31629
31670
|
if (mine === null) {
|
|
31630
|
-
return
|
|
31671
|
+
return [];
|
|
31631
31672
|
}
|
|
31632
|
-
return
|
|
31673
|
+
return announced.filter((one) => one.project.path !== "" && isSameDirectory(mine, one.project.path));
|
|
31674
|
+
}
|
|
31675
|
+
everyRuntimeOfOurs() {
|
|
31676
|
+
const announced = runtimesAnnounced();
|
|
31677
|
+
return [
|
|
31678
|
+
...this.allAnnouncedForOurProject(announced.running).map((one) => ({
|
|
31679
|
+
pid: one.pid,
|
|
31680
|
+
protocol: null
|
|
31681
|
+
})),
|
|
31682
|
+
...this.allAnnouncedForOurProject(announced.unspoken).map((one) => ({
|
|
31683
|
+
pid: one.pid,
|
|
31684
|
+
protocol: one.protocol
|
|
31685
|
+
}))
|
|
31686
|
+
];
|
|
31633
31687
|
}
|
|
31634
31688
|
couldBeOurs(project) {
|
|
31635
31689
|
const status = this.godotBridge.getStatus();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.35",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|