gdharness 1.1.29 → 1.1.31
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
|
@@ -384,9 +384,18 @@ function readCommandLine(command) {
|
|
|
384
384
|
return {
|
|
385
385
|
executable,
|
|
386
386
|
projectPath,
|
|
387
|
-
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
387
|
+
editor: words.some((word) => word === "-e" || word === "--editor"),
|
|
388
|
+
oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
|
|
388
389
|
};
|
|
389
390
|
}
|
|
391
|
+
async function oneShotOptionOf(pid) {
|
|
392
|
+
const tree = await processTree();
|
|
393
|
+
const listed = tree?.get(pid);
|
|
394
|
+
if (listed === undefined || listed.command === "") {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
return readCommandLine(listed.command).oneShot;
|
|
398
|
+
}
|
|
390
399
|
function commandWords(command) {
|
|
391
400
|
const words = [];
|
|
392
401
|
let word = "";
|
|
@@ -422,10 +431,31 @@ async function askPosix() {
|
|
|
422
431
|
});
|
|
423
432
|
return stdout;
|
|
424
433
|
}
|
|
425
|
-
var run, ASK_TIMEOUT_MS = 15000, lastListingFailure;
|
|
434
|
+
var run, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
|
|
426
435
|
var init_process_children = __esm(() => {
|
|
427
436
|
init_windows_ask();
|
|
428
437
|
run = promisify(execFile);
|
|
438
|
+
ONE_SHOT_OPTIONS = [
|
|
439
|
+
"--import",
|
|
440
|
+
"--quit",
|
|
441
|
+
"--quit-after",
|
|
442
|
+
"--export-release",
|
|
443
|
+
"--export-debug",
|
|
444
|
+
"--export-pack",
|
|
445
|
+
"--export-patch",
|
|
446
|
+
"--doctool",
|
|
447
|
+
"--gdscript-docs",
|
|
448
|
+
"--script",
|
|
449
|
+
"-s",
|
|
450
|
+
"--check-only",
|
|
451
|
+
"--build-solutions",
|
|
452
|
+
"--dump-gdextension-interface",
|
|
453
|
+
"--dump-extension-api",
|
|
454
|
+
"--dump-extension-api-with-docs",
|
|
455
|
+
"--validate-extension-api",
|
|
456
|
+
"--convert-3to4",
|
|
457
|
+
"--validate-conversion-3to4"
|
|
458
|
+
];
|
|
429
459
|
});
|
|
430
460
|
|
|
431
461
|
// src/bridge-announce.ts
|
|
@@ -17617,7 +17647,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17617
17647
|
},
|
|
17618
17648
|
{
|
|
17619
17649
|
name: "editor_status",
|
|
17620
|
-
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: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. 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. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. 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. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. 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. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
17650
|
+
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: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. 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. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. 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. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. 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. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option.",
|
|
17621
17651
|
parameters: {},
|
|
17622
17652
|
requires: []
|
|
17623
17653
|
},
|
|
@@ -17841,7 +17871,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17841
17871
|
},
|
|
17842
17872
|
{
|
|
17843
17873
|
name: "runtime_capture",
|
|
17844
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17874
|
+
description: "A picture of the running game: the whole screen, one region or node of it at full resolution, or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
17845
17875
|
parameters: {
|
|
17846
17876
|
projectPath: RUNNING_PROJECT_PATH,
|
|
17847
17877
|
pid: RUNNING_PID,
|
|
@@ -17860,6 +17890,22 @@ var init_tool_definitions = __esm(() => {
|
|
|
17860
17890
|
minimum: 1,
|
|
17861
17891
|
description: "Scale the image to this height. Given alone, the width keeps the picture's proportions."
|
|
17862
17892
|
},
|
|
17893
|
+
region: {
|
|
17894
|
+
type: "object",
|
|
17895
|
+
ops: ["screenshot"],
|
|
17896
|
+
description: "screenshot: capture only this rectangle of the window, at the game's own pixels: x, y, width and height in window pixels, or the window rectangle runtime_inspect rect answers, passed as it came. A region partly outside the window is cut to it and the answer says clipped; one wholly outside is refused. The answer names the region captured in window pixels, and drawnAt when the project draws at its own size and stretches that to the window, since the picture then has the game's pixels rather than the window's."
|
|
17897
|
+
},
|
|
17898
|
+
nodePath: {
|
|
17899
|
+
type: "string",
|
|
17900
|
+
ops: ["screenshot"],
|
|
17901
|
+
description: "screenshot: capture only where this node is drawn, as runtime_inspect rect answers it: a Control's rectangle, what a 3D node's geometry covers, or what a Sprite2D draws. A node with a position and no extent is refused; give region around it instead. Not with region."
|
|
17902
|
+
},
|
|
17903
|
+
zoom: {
|
|
17904
|
+
type: "integer",
|
|
17905
|
+
minimum: 1,
|
|
17906
|
+
maximum: 16,
|
|
17907
|
+
description: "Enlarge the picture this many times, each pixel made a square of pixels rather than blended with its neighbours, so a thin line or a corner can be read. Not with width or height. Default 1."
|
|
17908
|
+
},
|
|
17863
17909
|
outputPath: {
|
|
17864
17910
|
type: "string",
|
|
17865
17911
|
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
@@ -37461,7 +37507,7 @@ function getDefaultBridge(ownProject = null) {
|
|
|
37461
37507
|
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
37462
37508
|
return defaultBridge;
|
|
37463
37509
|
}
|
|
37464
|
-
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, OTHER_PROJECT_CLOSE_CODE = 4001, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
37510
|
+
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, OTHER_PROJECT_CLOSE_CODE = 4001, ONE_SHOT_CLOSE_CODE = 4002, CONNECT_WINDOW_MS = 35000, GodotBridge, defaultBridge = null;
|
|
37465
37511
|
var init_godot_bridge = __esm(() => {
|
|
37466
37512
|
init_wrapper();
|
|
37467
37513
|
init_errors();
|
|
@@ -37474,6 +37520,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
37474
37520
|
socket = null;
|
|
37475
37521
|
pingInterval = null;
|
|
37476
37522
|
connectionInfo = null;
|
|
37523
|
+
oneShotReader = null;
|
|
37524
|
+
oneShots = new Map;
|
|
37525
|
+
turnedAway = null;
|
|
37477
37526
|
pendingRequests = new Map;
|
|
37478
37527
|
resourceQueues = new Map;
|
|
37479
37528
|
wantedPort;
|
|
@@ -37488,6 +37537,9 @@ var init_godot_bridge = __esm(() => {
|
|
|
37488
37537
|
this.timeoutMs = timeoutMs;
|
|
37489
37538
|
this.ownProject = ownProject;
|
|
37490
37539
|
}
|
|
37540
|
+
identifyEditorsWith(reader) {
|
|
37541
|
+
this.oneShotReader = reader;
|
|
37542
|
+
}
|
|
37491
37543
|
get mayMove() {
|
|
37492
37544
|
return this.ownProject !== null;
|
|
37493
37545
|
}
|
|
@@ -37594,25 +37646,29 @@ var init_godot_bridge = __esm(() => {
|
|
|
37594
37646
|
this.log("info", "WebSocket bridge stopped");
|
|
37595
37647
|
}
|
|
37596
37648
|
isConnected() {
|
|
37597
|
-
return this.socket?.readyState === import_websocket.default.OPEN;
|
|
37649
|
+
return this.socket?.readyState === import_websocket.default.OPEN && this.connectionInfo?.identifying !== true;
|
|
37598
37650
|
}
|
|
37599
37651
|
getStatus() {
|
|
37652
|
+
const identifying = this.connectionInfo?.identifying === true;
|
|
37653
|
+
const editor = identifying ? null : this.connectionInfo;
|
|
37600
37654
|
return {
|
|
37601
37655
|
host: this.host,
|
|
37602
37656
|
port: this.port,
|
|
37603
37657
|
connected: this.isConnected(),
|
|
37604
|
-
projectPath:
|
|
37605
|
-
connectedAt:
|
|
37606
|
-
lastPongAt:
|
|
37607
|
-
addonVersion:
|
|
37608
|
-
addonDigest:
|
|
37609
|
-
editorPid:
|
|
37610
|
-
lspPort:
|
|
37611
|
-
dapPort:
|
|
37612
|
-
debugPort:
|
|
37613
|
-
openedByAServer:
|
|
37614
|
-
startedByAnEditor:
|
|
37615
|
-
syncsBreakpoints:
|
|
37658
|
+
projectPath: editor?.projectPath,
|
|
37659
|
+
connectedAt: editor?.connectedAt,
|
|
37660
|
+
lastPongAt: editor?.lastPongAt,
|
|
37661
|
+
addonVersion: editor?.addonVersion,
|
|
37662
|
+
addonDigest: editor?.addonDigest,
|
|
37663
|
+
editorPid: editor?.editorPid,
|
|
37664
|
+
lspPort: editor?.lspPort,
|
|
37665
|
+
dapPort: editor?.dapPort,
|
|
37666
|
+
debugPort: editor?.debugPort,
|
|
37667
|
+
openedByAServer: editor?.openedByAServer,
|
|
37668
|
+
startedByAnEditor: editor?.startedByAnEditor,
|
|
37669
|
+
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
37670
|
+
turnedAway: this.turnedAway ?? undefined,
|
|
37671
|
+
identifying: identifying ? true : undefined,
|
|
37616
37672
|
pendingRequests: this.pendingRequests.size,
|
|
37617
37673
|
queuedResources: this.resourceQueues.size,
|
|
37618
37674
|
listeningSince: this.listeningSince ?? undefined,
|
|
@@ -37760,6 +37816,11 @@ var init_godot_bridge = __esm(() => {
|
|
|
37760
37816
|
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
37761
37817
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
37762
37818
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
37819
|
+
const pid = this.connectionInfo.editorPid;
|
|
37820
|
+
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
37821
|
+
this.identify(pid);
|
|
37822
|
+
return;
|
|
37823
|
+
}
|
|
37763
37824
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
37764
37825
|
}
|
|
37765
37826
|
return;
|
|
@@ -37850,6 +37911,45 @@ var init_godot_bridge = __esm(() => {
|
|
|
37850
37911
|
this.stopKeepalive();
|
|
37851
37912
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
37852
37913
|
}
|
|
37914
|
+
identify(pid) {
|
|
37915
|
+
const known = this.oneShots.get(pid);
|
|
37916
|
+
if (known !== undefined) {
|
|
37917
|
+
this.turnAway(pid, known);
|
|
37918
|
+
return;
|
|
37919
|
+
}
|
|
37920
|
+
const info = this.connectionInfo;
|
|
37921
|
+
const socket = this.socket;
|
|
37922
|
+
const reader = this.oneShotReader;
|
|
37923
|
+
if (info === null || reader === null) {
|
|
37924
|
+
return;
|
|
37925
|
+
}
|
|
37926
|
+
info.identifying = true;
|
|
37927
|
+
reader(pid).catch(() => {
|
|
37928
|
+
return;
|
|
37929
|
+
}).then((option) => {
|
|
37930
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
37931
|
+
return;
|
|
37932
|
+
}
|
|
37933
|
+
if (typeof option === "string") {
|
|
37934
|
+
this.oneShots.set(pid, option);
|
|
37935
|
+
this.turnAway(pid, option);
|
|
37936
|
+
return;
|
|
37937
|
+
}
|
|
37938
|
+
info.identifying = false;
|
|
37939
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
37940
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
37941
|
+
});
|
|
37942
|
+
}
|
|
37943
|
+
turnAway(pid, option) {
|
|
37944
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
37945
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
37946
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
37947
|
+
const run = this.socket;
|
|
37948
|
+
this.socket = null;
|
|
37949
|
+
this.connectionInfo = null;
|
|
37950
|
+
this.stopKeepalive();
|
|
37951
|
+
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
37952
|
+
}
|
|
37853
37953
|
handleDisconnect(disconnectedSocket, reason) {
|
|
37854
37954
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
37855
37955
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -39645,6 +39745,25 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
39645
39745
|
}
|
|
39646
39746
|
return null;
|
|
39647
39747
|
}
|
|
39748
|
+
function captureDetail(payload) {
|
|
39749
|
+
const parts = [];
|
|
39750
|
+
const region = payload["region"];
|
|
39751
|
+
if (region !== null && typeof region === "object") {
|
|
39752
|
+
const named = region;
|
|
39753
|
+
const at = (key) => String(Math.round(Number(named[key]) * 100) / 100);
|
|
39754
|
+
parts.push(`of the window region at ${at("x")},${at("y")}, ${at("width")}x${at("height")} window pixels${payload["clipped"] === true ? ", clipped to the window" : ""}`);
|
|
39755
|
+
}
|
|
39756
|
+
const drawnAt = payload["drawnAt"];
|
|
39757
|
+
if (drawnAt !== null && typeof drawnAt === "object") {
|
|
39758
|
+
const drawn = drawnAt;
|
|
39759
|
+
parts.push(`at the game's own pixels: it draws at ${String(drawn["width"])}x${String(drawn["height"])} and stretches that to its window`);
|
|
39760
|
+
}
|
|
39761
|
+
if (typeof payload["zoom"] === "number") {
|
|
39762
|
+
parts.push(`enlarged ${payload["zoom"]} times`);
|
|
39763
|
+
}
|
|
39764
|
+
const note = typeof payload["note"] === "string" ? `. ${payload["note"]}` : "";
|
|
39765
|
+
return parts.length === 0 ? note : `, ${parts.join(", ")}${note}`;
|
|
39766
|
+
}
|
|
39648
39767
|
function patienceForFrames(frames, atLeast) {
|
|
39649
39768
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
39650
39769
|
}
|
|
@@ -40224,6 +40343,7 @@ class GodotServer {
|
|
|
40224
40343
|
constructor() {
|
|
40225
40344
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
40226
40345
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
40346
|
+
this.godotBridge.identifyEditorsWith(oneShotOptionOf);
|
|
40227
40347
|
this.godotBridge.on("godot_connected", () => {
|
|
40228
40348
|
this.settleTheLaunchReading();
|
|
40229
40349
|
});
|
|
@@ -42483,6 +42603,10 @@ class GodotServer {
|
|
|
42483
42603
|
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
42484
42604
|
return { lsp, dap };
|
|
42485
42605
|
}
|
|
42606
|
+
editorOnTheBridge() {
|
|
42607
|
+
const pid = this.godotBridge.getStatus().editorPid;
|
|
42608
|
+
return pid === undefined ? "the editor on this bridge" : `the editor on this bridge (pid ${pid})`;
|
|
42609
|
+
}
|
|
42486
42610
|
async handleRunProject(args, op) {
|
|
42487
42611
|
const project = this.project(args);
|
|
42488
42612
|
if (!project.ok) {
|
|
@@ -42490,7 +42614,7 @@ class GodotServer {
|
|
|
42490
42614
|
}
|
|
42491
42615
|
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
42492
42616
|
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
42493
|
-
return this.createErrorResponse(`This start names ${project.value.path}, and
|
|
42617
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
42494
42618
|
"editor_status names the project this server's editor is showing",
|
|
42495
42619
|
"One server serves one editor: start a second server for the other project"
|
|
42496
42620
|
]);
|
|
@@ -44207,7 +44331,7 @@ class GodotServer {
|
|
|
44207
44331
|
const open = this.godotBridge.getStatus().projectPath;
|
|
44208
44332
|
const meant = readNonEmptyString(args, "projectPath");
|
|
44209
44333
|
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
44210
|
-
return this.createErrorResponse(`This call names ${meant}, and
|
|
44334
|
+
return this.createErrorResponse(`This call names ${meant}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
44211
44335
|
"editor_status names the project this server's editor is showing",
|
|
44212
44336
|
"One server serves one editor: start a second server for the other project"
|
|
44213
44337
|
]);
|
|
@@ -44398,7 +44522,7 @@ ${staleNote}`);
|
|
|
44398
44522
|
content: [
|
|
44399
44523
|
{
|
|
44400
44524
|
type: "text",
|
|
44401
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
44525
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
44402
44526
|
|
|
44403
44527
|
${staleNote}`}`
|
|
44404
44528
|
},
|
|
@@ -25,6 +25,10 @@ const MAX_RECONNECT_DELAY: float = 30.0
|
|
|
25
25
|
## What a server closes with when the editor that said hello belongs to another project. Kept in
|
|
26
26
|
## step with `OTHER_PROJECT_CLOSE_CODE` in src/godot-bridge.ts.
|
|
27
27
|
const ELSEWHERE_CLOSE_CODE: int = 4001
|
|
28
|
+
## What a server closes with when this process is one job that quits, such as a gate's `--import`,
|
|
29
|
+
## rather than an editor: dialling in again changes nothing for the rest of the run. Kept in step
|
|
30
|
+
## with `ONE_SHOT_CLOSE_CODE` in src/godot-bridge.ts.
|
|
31
|
+
const ONE_SHOT_CLOSE_CODE: int = 4002
|
|
28
32
|
|
|
29
33
|
## Where Godot keeps the three ports an editor serves. None of them is per editor: the settings
|
|
30
34
|
## file is one for every editor of this engine version on the machine, so two open at once want the
|
|
@@ -447,6 +451,8 @@ func _read_marker(path: String) -> String:
|
|
|
447
451
|
func _handle_disconnect() -> void:
|
|
448
452
|
_is_connected = false
|
|
449
453
|
_said_elsewhere()
|
|
454
|
+
if socket.get_close_code() == ONE_SHOT_CLOSE_CODE:
|
|
455
|
+
_should_reconnect = false
|
|
450
456
|
disconnected.emit()
|
|
451
457
|
|
|
452
458
|
if _should_reconnect:
|
|
@@ -58,7 +58,7 @@ var _queries: Queries = Queries.new(self, values)
|
|
|
58
58
|
var _changes: Changes = Changes.new(self, values)
|
|
59
59
|
var _input: InputCommands = InputCommands.new(self, values)
|
|
60
60
|
var _typing: Typing = Typing.new(self)
|
|
61
|
-
var _capture: Capture = Capture.new(self)
|
|
61
|
+
var _capture: Capture = Capture.new(self, values)
|
|
62
62
|
var _waits: Waits = Waits.new(self, values)
|
|
63
63
|
|
|
64
64
|
var _server: TCPServer
|
|
@@ -4,12 +4,20 @@ extends RefCounted
|
|
|
4
4
|
|
|
5
5
|
const Read = preload("reading.gd")
|
|
6
6
|
const Values = preload("runtime_values.gd")
|
|
7
|
+
const Queries = preload("runtime_queries.gd")
|
|
8
|
+
const Screen = preload("runtime_screen.gd")
|
|
9
|
+
|
|
10
|
+
## How far zoom enlarges at most. Sixteen makes a 3-pixel ring 48 pixels across, past any need to
|
|
11
|
+
## see it, and a 200-pixel region 3200, past the size anybody reads a picture at.
|
|
12
|
+
const ZOOM_MOST: int = 16
|
|
7
13
|
|
|
8
14
|
var _host: Node
|
|
15
|
+
var _values: Values
|
|
9
16
|
|
|
10
17
|
|
|
11
|
-
func _init(host: Node) -> void:
|
|
18
|
+
func _init(host: Node, values: Values) -> void:
|
|
12
19
|
_host = host
|
|
20
|
+
_values = values
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
func capture_screenshot(params: Dictionary) -> Dictionary:
|
|
@@ -88,12 +96,155 @@ static func scaled_to(drawn: Vector2i, width: int, height: int) -> Vector2i:
|
|
|
88
96
|
return drawn
|
|
89
97
|
|
|
90
98
|
|
|
99
|
+
## What part of the picture a capture is cut to and how far it is enlarged, from what was asked, or
|
|
100
|
+
## a refusal saying what is wrong with the asking. Read before a frame is waited for, so a call that
|
|
101
|
+
## cannot be answered costs no wait.
|
|
102
|
+
##
|
|
103
|
+
## A region and a node are asked for because width and height scale the whole picture, and a
|
|
104
|
+
## question about a 3-pixel ring in a 1600-pixel window was answered by capturing the window, saving
|
|
105
|
+
## it and cropping it with a script of the caller's own, three calls for every look.
|
|
106
|
+
func _cut_asked(params: Dictionary, in_window: bool) -> Dictionary:
|
|
107
|
+
var region_given: Variant = params.get("region")
|
|
108
|
+
var node_path: String = str(params.get("nodePath", ""))
|
|
109
|
+
var zoom: int = Read.as_int(params.get("zoom", 1), 1)
|
|
110
|
+
var sized: bool = Read.as_int(params.get("width", 0)) > 0 or Read.as_int(params.get("height", 0)) > 0
|
|
111
|
+
if region_given != null and not node_path.is_empty():
|
|
112
|
+
return _refused("region and nodePath each name the part to capture; give one of them")
|
|
113
|
+
if (region_given != null or not node_path.is_empty()) and not in_window:
|
|
114
|
+
return _refused(
|
|
115
|
+
(
|
|
116
|
+
"region and nodePath are in the window's pixels, and this viewport's texture is in its "
|
|
117
|
+
+ "own: capture the screen with them, or this viewport whole"
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
if zoom < 1 or zoom > ZOOM_MOST:
|
|
121
|
+
return _refused("zoom is a whole factor from 1 to %d, and %d was asked for" % [ZOOM_MOST, zoom])
|
|
122
|
+
if zoom > 1 and sized:
|
|
123
|
+
return _refused(
|
|
124
|
+
(
|
|
125
|
+
"zoom enlarges by a whole factor and keeps every pixel square, and width and height "
|
|
126
|
+
+ "scale to a size; give one or the other"
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
var asked: Dictionary = {"zoom": zoom}
|
|
130
|
+
if region_given != null:
|
|
131
|
+
var region: Variant = region_of(_values, region_given)
|
|
132
|
+
if region == null:
|
|
133
|
+
var shape: String = (
|
|
134
|
+
"region is a rectangle in window pixels with an area: x, y, width and height, or the "
|
|
135
|
+
+ "window rectangle runtime_inspect rect answers, passed back as it came. %s is neither"
|
|
136
|
+
)
|
|
137
|
+
return _refused(shape % JSON.stringify(region_given))
|
|
138
|
+
asked["region"] = region
|
|
139
|
+
if not node_path.is_empty():
|
|
140
|
+
asked["node_path"] = node_path
|
|
141
|
+
return asked
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
static func _refused(message: String) -> Dictionary:
|
|
145
|
+
return {"type": "error", "message": message}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## The rectangle [param given] names: the four numbers x, y, width and height, or a rectangle as the
|
|
149
|
+
## runtime answers one, its two corners tagged with their type, passed back as it came. Null when it
|
|
150
|
+
## is neither or has no area.
|
|
151
|
+
static func region_of(values: Values, given: Variant) -> Variant:
|
|
152
|
+
if not given is Dictionary:
|
|
153
|
+
return null
|
|
154
|
+
var fields: Dictionary = given
|
|
155
|
+
fields = fields.duplicate()
|
|
156
|
+
if not fields.has("_type"):
|
|
157
|
+
fields["_type"] = "Rect2"
|
|
158
|
+
var rebuilt: Variant = values.deserialize(fields)
|
|
159
|
+
var region: Rect2
|
|
160
|
+
if rebuilt is Rect2:
|
|
161
|
+
region = rebuilt
|
|
162
|
+
elif rebuilt is Rect2i:
|
|
163
|
+
var whole: Rect2i = rebuilt
|
|
164
|
+
region = Rect2(whole)
|
|
165
|
+
else:
|
|
166
|
+
return null
|
|
167
|
+
if not region.has_area():
|
|
168
|
+
return null
|
|
169
|
+
return region
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
## Where [param node] is drawn, in window pixels, as runtime_inspect rect answers it: a Control's
|
|
173
|
+
## rectangle, what a 3D node's geometry covers, or what a 2D node that knows its own rectangle draws.
|
|
174
|
+
## A refusal's message when it has no extent on screen.
|
|
175
|
+
static func drawn_rect_of(node: Node) -> Variant:
|
|
176
|
+
if node is Control:
|
|
177
|
+
var control: Control = node
|
|
178
|
+
return Screen.rect_in_window(
|
|
179
|
+
control.get_viewport(),
|
|
180
|
+
control.get_global_transform_with_canvas() * Rect2(Vector2.ZERO, control.size)
|
|
181
|
+
)
|
|
182
|
+
if node is Node3D:
|
|
183
|
+
var spatial: Node3D = node
|
|
184
|
+
var found: Dictionary = Queries.in_frame(spatial)
|
|
185
|
+
if not found.has("rect"):
|
|
186
|
+
return "%s is not drawn by any current camera, so it covers nothing on screen" % node.get_path()
|
|
187
|
+
var covered: Rect2 = found["rect"]
|
|
188
|
+
return Screen.rect_in_window(spatial.get_viewport(), covered)
|
|
189
|
+
if node is Node2D and node.has_method("get_rect"):
|
|
190
|
+
var item: Node2D = node
|
|
191
|
+
var own: Rect2 = item.call("get_rect")
|
|
192
|
+
return Screen.rect_in_window(item.get_viewport(), item.get_global_transform_with_canvas() * own)
|
|
193
|
+
var no_extent: String = (
|
|
194
|
+
"%s is a %s, which has a position on screen and no extent; give region around the point "
|
|
195
|
+
+ "runtime_inspect rect answers for it"
|
|
196
|
+
)
|
|
197
|
+
return no_extent % [node.get_path(), node.get_class()]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
## [param region], in window pixels, as pixels of a picture drawn at [param drawn] in a window
|
|
201
|
+
## [param window] pixels across: the same rectangle when the two match, scaled when the project draws
|
|
202
|
+
## at its own size and stretches that to the window. Widened to whole pixels, so an edge falling
|
|
203
|
+
## inside a pixel keeps that pixel, and cut to the picture. Without area when none of it is in it.
|
|
204
|
+
static func in_picture(region: Rect2, window: Vector2i, drawn: Vector2i) -> Rect2i:
|
|
205
|
+
return widened(region, window, drawn).intersection(Rect2i(Vector2i.ZERO, drawn))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## [param region] in the picture's pixels as [method in_picture] takes it, before it is cut to the
|
|
209
|
+
## picture, which is what tells a region that was clipped from one that fitted.
|
|
210
|
+
static func widened(region: Rect2, window: Vector2i, drawn: Vector2i) -> Rect2i:
|
|
211
|
+
var scale: Vector2 = Vector2.ONE
|
|
212
|
+
if window.x > 0 and window.y > 0:
|
|
213
|
+
scale = Vector2(drawn) / Vector2(window)
|
|
214
|
+
var start: Vector2 = (region.position * scale).floor()
|
|
215
|
+
var end: Vector2 = (region.end * scale).ceil()
|
|
216
|
+
return Rect2i(Vector2i(start), Vector2i(end - start))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## [param picture], pixels of a picture drawn at [param drawn], as the window pixels it covers in a
|
|
220
|
+
## window [param window] pixels across: what an answer names, since the region was asked for in them.
|
|
221
|
+
static func in_window_pixels(picture: Rect2i, window: Vector2i, drawn: Vector2i) -> Dictionary:
|
|
222
|
+
var scale: Vector2 = Vector2.ONE
|
|
223
|
+
if drawn.x > 0 and drawn.y > 0 and window.x > 0 and window.y > 0:
|
|
224
|
+
scale = Vector2(window) / Vector2(drawn)
|
|
225
|
+
var start: Vector2 = Vector2(picture.position) * scale
|
|
226
|
+
var size: Vector2 = Vector2(picture.size) * scale
|
|
227
|
+
return {"x": start.x, "y": start.y, "width": size.x, "height": size.y}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
## [param image] cut to [param picture] and enlarged [param zoom] times, each pixel made a square of
|
|
231
|
+
## them rather than blended with its neighbours, so a pixel in the answer is a pixel the game drew.
|
|
232
|
+
static func cut(image: Image, picture: Rect2i, zoom: int) -> Image:
|
|
233
|
+
var part: Image = image.get_region(picture)
|
|
234
|
+
if zoom > 1:
|
|
235
|
+
part.resize(part.get_width() * zoom, part.get_height() * zoom, Image.INTERPOLATE_NEAREST)
|
|
236
|
+
return part
|
|
237
|
+
|
|
238
|
+
|
|
91
239
|
## The server names the file, so a game cannot point it at a path of its own choosing; a call
|
|
92
240
|
## with no path is a call the server did not make.
|
|
93
241
|
func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false) -> Dictionary:
|
|
94
242
|
var requested_path: String = str(params.get("output_path", ""))
|
|
95
243
|
if requested_path.is_empty():
|
|
96
244
|
return {"type": "error", "message": "output_path required"}
|
|
245
|
+
var asked: Dictionary = _cut_asked(params, with_windows)
|
|
246
|
+
if asked.has("message"):
|
|
247
|
+
return asked
|
|
97
248
|
|
|
98
249
|
# Godot draws nothing to a minimised window and nothing at all without one, and the
|
|
99
250
|
# texture keeps whatever was drawn last. A capture then comes back byte for byte the same
|
|
@@ -127,12 +278,57 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
127
278
|
if with_windows:
|
|
128
279
|
_lay_native_windows(image)
|
|
129
280
|
|
|
130
|
-
var
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
281
|
+
var described: Dictionary = {}
|
|
282
|
+
var region: Variant = asked.get("region")
|
|
283
|
+
if asked.has("node_path"):
|
|
284
|
+
var node_path: String = asked["node_path"]
|
|
285
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
286
|
+
if standing.has("message"):
|
|
287
|
+
return standing
|
|
288
|
+
var node: Node = standing["node"]
|
|
289
|
+
var placed: Variant = drawn_rect_of(node)
|
|
290
|
+
if placed is String:
|
|
291
|
+
var why: String = placed
|
|
292
|
+
return _refused(why)
|
|
293
|
+
region = placed
|
|
294
|
+
# A hidden node's rectangle is still a place, and what is drawn there is whatever is behind it.
|
|
295
|
+
if not Queries.shown(node):
|
|
296
|
+
described["note"] = (
|
|
297
|
+
"%s is hidden, so this is what is drawn where it would be rather than the node" % node_path
|
|
298
|
+
)
|
|
299
|
+
if region != null:
|
|
300
|
+
var asked_region: Rect2 = region
|
|
301
|
+
var window: Vector2i = _host.get_tree().root.size
|
|
302
|
+
var picture_size: Vector2i = image.get_size()
|
|
303
|
+
var whole: Rect2i = widened(asked_region, window, picture_size)
|
|
304
|
+
var picture: Rect2i = whole.intersection(Rect2i(Vector2i.ZERO, picture_size))
|
|
305
|
+
if not picture.has_area():
|
|
306
|
+
return _refused(
|
|
307
|
+
(
|
|
308
|
+
"The region at %s, %s pixels across, is outside the window, which is %dx%d pixels"
|
|
309
|
+
% [asked_region.position, asked_region.size, window.x, window.y]
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
image = cut(image, picture, 1)
|
|
313
|
+
described["region"] = in_window_pixels(picture, window, picture_size)
|
|
314
|
+
if picture != whole:
|
|
315
|
+
described["clipped"] = true
|
|
316
|
+
# The project draws at its own size and stretches that to the window, so the picture's pixels
|
|
317
|
+
# are not the window's: said, because a caller sizing a region by window pixels gets fewer.
|
|
318
|
+
if picture_size != window:
|
|
319
|
+
described["drawnAt"] = {"width": picture_size.x, "height": picture_size.y}
|
|
320
|
+
|
|
321
|
+
var zoom: int = asked["zoom"]
|
|
322
|
+
if zoom > 1:
|
|
323
|
+
image = cut(image, Rect2i(Vector2i.ZERO, image.get_size()), zoom)
|
|
324
|
+
described["zoom"] = zoom
|
|
325
|
+
else:
|
|
326
|
+
var drawn: Vector2i = Vector2i(image.get_width(), image.get_height())
|
|
327
|
+
var target: Vector2i = scaled_to(
|
|
328
|
+
drawn, Read.as_int(params.get("width", 0)), Read.as_int(params.get("height", 0))
|
|
329
|
+
)
|
|
330
|
+
if target != drawn:
|
|
331
|
+
image.resize(target.x, target.y)
|
|
136
332
|
|
|
137
333
|
var screenshot_path: String = requested_path
|
|
138
334
|
if screenshot_path.begins_with("user://") or screenshot_path.begins_with("res://"):
|
|
@@ -141,10 +337,12 @@ func _capture(viewport: Viewport, params: Dictionary, with_windows: bool = false
|
|
|
141
337
|
if save_error != OK:
|
|
142
338
|
return {"type": "error", "message": "Failed to save screenshot as PNG: " + str(save_error)}
|
|
143
339
|
|
|
144
|
-
|
|
340
|
+
var answer: Dictionary = {
|
|
145
341
|
"type": "screenshot_file",
|
|
146
342
|
"format": "png",
|
|
147
343
|
"width": image.get_width(),
|
|
148
344
|
"height": image.get_height(),
|
|
149
345
|
"path": screenshot_path
|
|
150
346
|
}
|
|
347
|
+
answer.merge(described)
|
|
348
|
+
return answer
|
package/build/index.js
CHANGED
|
@@ -25284,6 +25284,27 @@ function ancestorsIn(tree, pid, root) {
|
|
|
25284
25284
|
}
|
|
25285
25285
|
return chain;
|
|
25286
25286
|
}
|
|
25287
|
+
var ONE_SHOT_OPTIONS = [
|
|
25288
|
+
"--import",
|
|
25289
|
+
"--quit",
|
|
25290
|
+
"--quit-after",
|
|
25291
|
+
"--export-release",
|
|
25292
|
+
"--export-debug",
|
|
25293
|
+
"--export-pack",
|
|
25294
|
+
"--export-patch",
|
|
25295
|
+
"--doctool",
|
|
25296
|
+
"--gdscript-docs",
|
|
25297
|
+
"--script",
|
|
25298
|
+
"-s",
|
|
25299
|
+
"--check-only",
|
|
25300
|
+
"--build-solutions",
|
|
25301
|
+
"--dump-gdextension-interface",
|
|
25302
|
+
"--dump-extension-api",
|
|
25303
|
+
"--dump-extension-api-with-docs",
|
|
25304
|
+
"--validate-extension-api",
|
|
25305
|
+
"--convert-3to4",
|
|
25306
|
+
"--validate-conversion-3to4"
|
|
25307
|
+
];
|
|
25287
25308
|
function readCommandLine(command) {
|
|
25288
25309
|
const all = commandWords(command);
|
|
25289
25310
|
const separator = all.findIndex((word) => word === "--" || word === "++");
|
|
@@ -25304,9 +25325,18 @@ function readCommandLine(command) {
|
|
|
25304
25325
|
return {
|
|
25305
25326
|
executable,
|
|
25306
25327
|
projectPath,
|
|
25307
|
-
editor: words.some((word) => word === "-e" || word === "--editor")
|
|
25328
|
+
editor: words.some((word) => word === "-e" || word === "--editor"),
|
|
25329
|
+
oneShot: words.find((word) => ONE_SHOT_OPTIONS.includes(word)) ?? null
|
|
25308
25330
|
};
|
|
25309
25331
|
}
|
|
25332
|
+
async function oneShotOptionOf(pid) {
|
|
25333
|
+
const tree = await processTree();
|
|
25334
|
+
const listed = tree?.get(pid);
|
|
25335
|
+
if (listed === undefined || listed.command === "") {
|
|
25336
|
+
return;
|
|
25337
|
+
}
|
|
25338
|
+
return readCommandLine(listed.command).oneShot;
|
|
25339
|
+
}
|
|
25310
25340
|
function commandWords(command) {
|
|
25311
25341
|
const words = [];
|
|
25312
25342
|
let word = "";
|
|
@@ -27664,6 +27694,7 @@ var DEFAULT_TIMEOUT_MS = 30000;
|
|
|
27664
27694
|
var KEEPALIVE_INTERVAL_MS = 1e4;
|
|
27665
27695
|
var SECOND_CONNECTION_CLOSE_CODE = 4000;
|
|
27666
27696
|
var OTHER_PROJECT_CLOSE_CODE = 4001;
|
|
27697
|
+
var ONE_SHOT_CLOSE_CODE = 4002;
|
|
27667
27698
|
function resolveDefaultBridgePort() {
|
|
27668
27699
|
return portFromEnv("GDHARNESS_BRIDGE_PORT", DEFAULT_PORT);
|
|
27669
27700
|
}
|
|
@@ -27695,6 +27726,9 @@ class GodotBridge extends EventEmitter {
|
|
|
27695
27726
|
socket = null;
|
|
27696
27727
|
pingInterval = null;
|
|
27697
27728
|
connectionInfo = null;
|
|
27729
|
+
oneShotReader = null;
|
|
27730
|
+
oneShots = new Map;
|
|
27731
|
+
turnedAway = null;
|
|
27698
27732
|
pendingRequests = new Map;
|
|
27699
27733
|
resourceQueues = new Map;
|
|
27700
27734
|
wantedPort;
|
|
@@ -27709,6 +27743,9 @@ class GodotBridge extends EventEmitter {
|
|
|
27709
27743
|
this.timeoutMs = timeoutMs;
|
|
27710
27744
|
this.ownProject = ownProject;
|
|
27711
27745
|
}
|
|
27746
|
+
identifyEditorsWith(reader) {
|
|
27747
|
+
this.oneShotReader = reader;
|
|
27748
|
+
}
|
|
27712
27749
|
get mayMove() {
|
|
27713
27750
|
return this.ownProject !== null;
|
|
27714
27751
|
}
|
|
@@ -27815,25 +27852,29 @@ class GodotBridge extends EventEmitter {
|
|
|
27815
27852
|
this.log("info", "WebSocket bridge stopped");
|
|
27816
27853
|
}
|
|
27817
27854
|
isConnected() {
|
|
27818
|
-
return this.socket?.readyState === import_websocket.default.OPEN;
|
|
27855
|
+
return this.socket?.readyState === import_websocket.default.OPEN && this.connectionInfo?.identifying !== true;
|
|
27819
27856
|
}
|
|
27820
27857
|
getStatus() {
|
|
27858
|
+
const identifying = this.connectionInfo?.identifying === true;
|
|
27859
|
+
const editor = identifying ? null : this.connectionInfo;
|
|
27821
27860
|
return {
|
|
27822
27861
|
host: this.host,
|
|
27823
27862
|
port: this.port,
|
|
27824
27863
|
connected: this.isConnected(),
|
|
27825
|
-
projectPath:
|
|
27826
|
-
connectedAt:
|
|
27827
|
-
lastPongAt:
|
|
27828
|
-
addonVersion:
|
|
27829
|
-
addonDigest:
|
|
27830
|
-
editorPid:
|
|
27831
|
-
lspPort:
|
|
27832
|
-
dapPort:
|
|
27833
|
-
debugPort:
|
|
27834
|
-
openedByAServer:
|
|
27835
|
-
startedByAnEditor:
|
|
27836
|
-
syncsBreakpoints:
|
|
27864
|
+
projectPath: editor?.projectPath,
|
|
27865
|
+
connectedAt: editor?.connectedAt,
|
|
27866
|
+
lastPongAt: editor?.lastPongAt,
|
|
27867
|
+
addonVersion: editor?.addonVersion,
|
|
27868
|
+
addonDigest: editor?.addonDigest,
|
|
27869
|
+
editorPid: editor?.editorPid,
|
|
27870
|
+
lspPort: editor?.lspPort,
|
|
27871
|
+
dapPort: editor?.dapPort,
|
|
27872
|
+
debugPort: editor?.debugPort,
|
|
27873
|
+
openedByAServer: editor?.openedByAServer,
|
|
27874
|
+
startedByAnEditor: editor?.startedByAnEditor,
|
|
27875
|
+
syncsBreakpoints: editor?.syncsBreakpoints,
|
|
27876
|
+
turnedAway: this.turnedAway ?? undefined,
|
|
27877
|
+
identifying: identifying ? true : undefined,
|
|
27837
27878
|
pendingRequests: this.pendingRequests.size,
|
|
27838
27879
|
queuedResources: this.resourceQueues.size,
|
|
27839
27880
|
listeningSince: this.listeningSince ?? undefined,
|
|
@@ -27981,6 +28022,11 @@ class GodotBridge extends EventEmitter {
|
|
|
27981
28022
|
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
27982
28023
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
27983
28024
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
28025
|
+
const pid = this.connectionInfo.editorPid;
|
|
28026
|
+
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
28027
|
+
this.identify(pid);
|
|
28028
|
+
return;
|
|
28029
|
+
}
|
|
27984
28030
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
27985
28031
|
}
|
|
27986
28032
|
return;
|
|
@@ -28071,6 +28117,45 @@ class GodotBridge extends EventEmitter {
|
|
|
28071
28117
|
this.stopKeepalive();
|
|
28072
28118
|
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
28073
28119
|
}
|
|
28120
|
+
identify(pid) {
|
|
28121
|
+
const known = this.oneShots.get(pid);
|
|
28122
|
+
if (known !== undefined) {
|
|
28123
|
+
this.turnAway(pid, known);
|
|
28124
|
+
return;
|
|
28125
|
+
}
|
|
28126
|
+
const info = this.connectionInfo;
|
|
28127
|
+
const socket = this.socket;
|
|
28128
|
+
const reader = this.oneShotReader;
|
|
28129
|
+
if (info === null || reader === null) {
|
|
28130
|
+
return;
|
|
28131
|
+
}
|
|
28132
|
+
info.identifying = true;
|
|
28133
|
+
reader(pid).catch(() => {
|
|
28134
|
+
return;
|
|
28135
|
+
}).then((option) => {
|
|
28136
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
28137
|
+
return;
|
|
28138
|
+
}
|
|
28139
|
+
if (typeof option === "string") {
|
|
28140
|
+
this.oneShots.set(pid, option);
|
|
28141
|
+
this.turnAway(pid, option);
|
|
28142
|
+
return;
|
|
28143
|
+
}
|
|
28144
|
+
info.identifying = false;
|
|
28145
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
28146
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
28147
|
+
});
|
|
28148
|
+
}
|
|
28149
|
+
turnAway(pid, option) {
|
|
28150
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
28151
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
28152
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
28153
|
+
const run = this.socket;
|
|
28154
|
+
this.socket = null;
|
|
28155
|
+
this.connectionInfo = null;
|
|
28156
|
+
this.stopKeepalive();
|
|
28157
|
+
run?.close(ONE_SHOT_CLOSE_CODE, said);
|
|
28158
|
+
}
|
|
28074
28159
|
handleDisconnect(disconnectedSocket, reason) {
|
|
28075
28160
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
28076
28161
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -31601,7 +31686,7 @@ var TOOL_SPECS = [
|
|
|
31601
31686
|
},
|
|
31602
31687
|
{
|
|
31603
31688
|
name: "editor_status",
|
|
31604
|
-
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: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. 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. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. 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. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. 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. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs.",
|
|
31689
|
+
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: runtimeConnected counts the games of the project this server serves, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath. 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. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it; null when there is none. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. 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. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say); addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs gdharness upgrade, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. Every runtime_* answer from such a game carries addonIsStale and staleNote too. 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. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. 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. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. port is the one the editor bridge is on; when that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds; bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it; startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and while that is read identifying is true and connected false; one started with such an option is turned away, and turnedAway names the last one, its pid and the option.",
|
|
31605
31690
|
parameters: {},
|
|
31606
31691
|
requires: []
|
|
31607
31692
|
},
|
|
@@ -31825,7 +31910,7 @@ var TOOL_SPECS = [
|
|
|
31825
31910
|
},
|
|
31826
31911
|
{
|
|
31827
31912
|
name: "runtime_capture",
|
|
31828
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
31913
|
+
description: "A picture of the running game: the whole screen, one region or node of it at full resolution, or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
31829
31914
|
parameters: {
|
|
31830
31915
|
projectPath: RUNNING_PROJECT_PATH,
|
|
31831
31916
|
pid: RUNNING_PID,
|
|
@@ -31844,6 +31929,22 @@ var TOOL_SPECS = [
|
|
|
31844
31929
|
minimum: 1,
|
|
31845
31930
|
description: "Scale the image to this height. Given alone, the width keeps the picture's proportions."
|
|
31846
31931
|
},
|
|
31932
|
+
region: {
|
|
31933
|
+
type: "object",
|
|
31934
|
+
ops: ["screenshot"],
|
|
31935
|
+
description: "screenshot: capture only this rectangle of the window, at the game's own pixels: x, y, width and height in window pixels, or the window rectangle runtime_inspect rect answers, passed as it came. A region partly outside the window is cut to it and the answer says clipped; one wholly outside is refused. The answer names the region captured in window pixels, and drawnAt when the project draws at its own size and stretches that to the window, since the picture then has the game's pixels rather than the window's."
|
|
31936
|
+
},
|
|
31937
|
+
nodePath: {
|
|
31938
|
+
type: "string",
|
|
31939
|
+
ops: ["screenshot"],
|
|
31940
|
+
description: "screenshot: capture only where this node is drawn, as runtime_inspect rect answers it: a Control's rectangle, what a 3D node's geometry covers, or what a Sprite2D draws. A node with a position and no extent is refused; give region around it instead. Not with region."
|
|
31941
|
+
},
|
|
31942
|
+
zoom: {
|
|
31943
|
+
type: "integer",
|
|
31944
|
+
minimum: 1,
|
|
31945
|
+
maximum: 16,
|
|
31946
|
+
description: "Enlarge the picture this many times, each pixel made a square of pixels rather than blended with its neighbours, so a thin line or a corner can be read. Not with width or height. Default 1."
|
|
31947
|
+
},
|
|
31847
31948
|
outputPath: {
|
|
31848
31949
|
type: "string",
|
|
31849
31950
|
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
@@ -32161,6 +32262,25 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
32161
32262
|
}
|
|
32162
32263
|
return null;
|
|
32163
32264
|
}
|
|
32265
|
+
function captureDetail(payload) {
|
|
32266
|
+
const parts = [];
|
|
32267
|
+
const region = payload["region"];
|
|
32268
|
+
if (region !== null && typeof region === "object") {
|
|
32269
|
+
const named = region;
|
|
32270
|
+
const at = (key) => String(Math.round(Number(named[key]) * 100) / 100);
|
|
32271
|
+
parts.push(`of the window region at ${at("x")},${at("y")}, ${at("width")}x${at("height")} window pixels${payload["clipped"] === true ? ", clipped to the window" : ""}`);
|
|
32272
|
+
}
|
|
32273
|
+
const drawnAt = payload["drawnAt"];
|
|
32274
|
+
if (drawnAt !== null && typeof drawnAt === "object") {
|
|
32275
|
+
const drawn = drawnAt;
|
|
32276
|
+
parts.push(`at the game's own pixels: it draws at ${String(drawn["width"])}x${String(drawn["height"])} and stretches that to its window`);
|
|
32277
|
+
}
|
|
32278
|
+
if (typeof payload["zoom"] === "number") {
|
|
32279
|
+
parts.push(`enlarged ${payload["zoom"]} times`);
|
|
32280
|
+
}
|
|
32281
|
+
const note = typeof payload["note"] === "string" ? `. ${payload["note"]}` : "";
|
|
32282
|
+
return parts.length === 0 ? note : `, ${parts.join(", ")}${note}`;
|
|
32283
|
+
}
|
|
32164
32284
|
function patienceForFrames(frames, atLeast) {
|
|
32165
32285
|
return Math.max(atLeast, frames / SLOWEST_FRAME_RATE * 1000 + atLeast);
|
|
32166
32286
|
}
|
|
@@ -32804,6 +32924,7 @@ class GodotServer {
|
|
|
32804
32924
|
constructor() {
|
|
32805
32925
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
32806
32926
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
32927
|
+
this.godotBridge.identifyEditorsWith(oneShotOptionOf);
|
|
32807
32928
|
this.godotBridge.on("godot_connected", () => {
|
|
32808
32929
|
this.settleTheLaunchReading();
|
|
32809
32930
|
});
|
|
@@ -35063,6 +35184,10 @@ class GodotServer {
|
|
|
35063
35184
|
const dap = portFromEnvOrNull("GDHARNESS_DAP_PORT") ?? await freePort(DEFAULT_DAP_PORT);
|
|
35064
35185
|
return { lsp, dap };
|
|
35065
35186
|
}
|
|
35187
|
+
editorOnTheBridge() {
|
|
35188
|
+
const pid = this.godotBridge.getStatus().editorPid;
|
|
35189
|
+
return pid === undefined ? "the editor on this bridge" : `the editor on this bridge (pid ${pid})`;
|
|
35190
|
+
}
|
|
35066
35191
|
async handleRunProject(args, op) {
|
|
35067
35192
|
const project = this.project(args);
|
|
35068
35193
|
if (!project.ok) {
|
|
@@ -35070,7 +35195,7 @@ class GodotServer {
|
|
|
35070
35195
|
}
|
|
35071
35196
|
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
35072
35197
|
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
35073
|
-
return this.createErrorResponse(`This start names ${project.value.path}, and
|
|
35198
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
35074
35199
|
"editor_status names the project this server's editor is showing",
|
|
35075
35200
|
"One server serves one editor: start a second server for the other project"
|
|
35076
35201
|
]);
|
|
@@ -36787,7 +36912,7 @@ class GodotServer {
|
|
|
36787
36912
|
const open = this.godotBridge.getStatus().projectPath;
|
|
36788
36913
|
const meant = readNonEmptyString(args, "projectPath");
|
|
36789
36914
|
if (open !== undefined && open !== "" && meant !== undefined && !isSameDirectory(open, meant)) {
|
|
36790
|
-
return this.createErrorResponse(`This call names ${meant}, and
|
|
36915
|
+
return this.createErrorResponse(`This call names ${meant}, and ${this.editorOnTheBridge()} has ${open} open.`, [
|
|
36791
36916
|
"editor_status names the project this server's editor is showing",
|
|
36792
36917
|
"One server serves one editor: start a second server for the other project"
|
|
36793
36918
|
]);
|
|
@@ -36978,7 +37103,7 @@ ${staleNote}`);
|
|
|
36978
37103
|
content: [
|
|
36979
37104
|
{
|
|
36980
37105
|
type: "text",
|
|
36981
|
-
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
37106
|
+
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${captureDetail(payload)}${saveTo === null ? "" : `. Saved to ${saveTo}`}${staleNote === undefined ? "" : `
|
|
36982
37107
|
|
|
36983
37108
|
${staleNote}`}`
|
|
36984
37109
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.31",
|
|
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",
|