gdharness 0.9.1 → 0.9.2
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 +114 -24
- package/build/godot/addons/gdharness_editor/bridge_client.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +6 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +8 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +52 -33
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +31 -0
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +8 -6
- package/build/index.js +167 -78
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -14711,6 +14711,9 @@ var init_types = __esm(() => {
|
|
|
14711
14711
|
|
|
14712
14712
|
// src/paths.ts
|
|
14713
14713
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
14714
|
+
function isSameDirectory(onePath, otherPath) {
|
|
14715
|
+
return relative(resolve(onePath), resolve(otherPath)) === "";
|
|
14716
|
+
}
|
|
14714
14717
|
function isWithinRoot(rootPath, candidatePath) {
|
|
14715
14718
|
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
14716
14719
|
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
@@ -15845,7 +15848,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15845
15848
|
blank: true,
|
|
15846
15849
|
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
15847
15850
|
},
|
|
15848
|
-
method: {
|
|
15851
|
+
method: {
|
|
15852
|
+
type: "string",
|
|
15853
|
+
ops: ["call"],
|
|
15854
|
+
description: 'call: which one to call. Colons call through the objects a node holds, "_game:run:advance", the same way a property is written through them.'
|
|
15855
|
+
},
|
|
15849
15856
|
args: {
|
|
15850
15857
|
type: "array",
|
|
15851
15858
|
ops: ["call"],
|
|
@@ -15858,7 +15865,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
15858
15865
|
summary: "set a property, on a node or on an object it holds",
|
|
15859
15866
|
requires: ["property", "value"]
|
|
15860
15867
|
},
|
|
15861
|
-
call: {
|
|
15868
|
+
call: {
|
|
15869
|
+
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
15870
|
+
requires: ["method"]
|
|
15871
|
+
}
|
|
15862
15872
|
}
|
|
15863
15873
|
},
|
|
15864
15874
|
{
|
|
@@ -30772,11 +30782,29 @@ var init_stdio2 = __esm(() => {
|
|
|
30772
30782
|
});
|
|
30773
30783
|
|
|
30774
30784
|
// src/bridge-announce.ts
|
|
30775
|
-
import { mkdirSync as mkdirSync5, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30785
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
30776
30786
|
import { dirname as dirname6, join as join11 } from "node:path";
|
|
30777
30787
|
function announcementPath(projectPath) {
|
|
30778
30788
|
return join11(projectPath, ".godot", "gdharness-bridge.json");
|
|
30779
30789
|
}
|
|
30790
|
+
function readAnnouncement(path) {
|
|
30791
|
+
try {
|
|
30792
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
30793
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
30794
|
+
return null;
|
|
30795
|
+
}
|
|
30796
|
+
const said = parsed;
|
|
30797
|
+
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
30798
|
+
return null;
|
|
30799
|
+
}
|
|
30800
|
+
return said;
|
|
30801
|
+
} catch {
|
|
30802
|
+
return null;
|
|
30803
|
+
}
|
|
30804
|
+
}
|
|
30805
|
+
function announcementIsOurs(path) {
|
|
30806
|
+
return readAnnouncement(path)?.pid === process.pid;
|
|
30807
|
+
}
|
|
30780
30808
|
function announceBridge(projectPath, bridge) {
|
|
30781
30809
|
const path = announcementPath(projectPath);
|
|
30782
30810
|
try {
|
|
@@ -30798,7 +30826,7 @@ function announceBridge(projectPath, bridge) {
|
|
|
30798
30826
|
}
|
|
30799
30827
|
}
|
|
30800
30828
|
function withdrawBridge(path) {
|
|
30801
|
-
if (path === null) {
|
|
30829
|
+
if (path === null || !announcementIsOurs(path)) {
|
|
30802
30830
|
return;
|
|
30803
30831
|
}
|
|
30804
30832
|
try {
|
|
@@ -34324,14 +34352,15 @@ function resolveDefaultBridgeHost() {
|
|
|
34324
34352
|
function servedPort(said) {
|
|
34325
34353
|
return typeof said === "number" && Number.isInteger(said) && said > 0 && said <= 65535 ? said : undefined;
|
|
34326
34354
|
}
|
|
34327
|
-
function getDefaultBridge(
|
|
34328
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS,
|
|
34355
|
+
function getDefaultBridge(ownProject = null) {
|
|
34356
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
34329
34357
|
return defaultBridge;
|
|
34330
34358
|
}
|
|
34331
|
-
var DEFAULT_PORT = 6505, DEFAULT_HOST = "127.0.0.1", DEFAULT_TIMEOUT_MS = 30000, KEEPALIVE_INTERVAL_MS = 1e4, SECOND_CONNECTION_CLOSE_CODE = 4000, GodotBridge, defaultBridge = null;
|
|
34359
|
+
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, GodotBridge, defaultBridge = null;
|
|
34332
34360
|
var init_godot_bridge = __esm(() => {
|
|
34333
34361
|
init_wrapper();
|
|
34334
34362
|
init_errors();
|
|
34363
|
+
init_paths();
|
|
34335
34364
|
init_ports();
|
|
34336
34365
|
GodotBridge = class GodotBridge extends EventEmitter {
|
|
34337
34366
|
httpServer = null;
|
|
@@ -34345,13 +34374,16 @@ var init_godot_bridge = __esm(() => {
|
|
|
34345
34374
|
boundPort = null;
|
|
34346
34375
|
host;
|
|
34347
34376
|
timeoutMs;
|
|
34348
|
-
|
|
34349
|
-
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
34377
|
+
ownProject;
|
|
34378
|
+
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, ownProject = null) {
|
|
34350
34379
|
super();
|
|
34351
34380
|
this.wantedPort = port;
|
|
34352
34381
|
this.host = host;
|
|
34353
34382
|
this.timeoutMs = timeoutMs;
|
|
34354
|
-
this.
|
|
34383
|
+
this.ownProject = ownProject;
|
|
34384
|
+
}
|
|
34385
|
+
get mayMove() {
|
|
34386
|
+
return this.ownProject !== null;
|
|
34355
34387
|
}
|
|
34356
34388
|
get port() {
|
|
34357
34389
|
return this.boundPort ?? this.wantedPort;
|
|
@@ -34597,6 +34629,10 @@ var init_godot_bridge = __esm(() => {
|
|
|
34597
34629
|
}
|
|
34598
34630
|
case "godot_ready":
|
|
34599
34631
|
if (this.connectionInfo) {
|
|
34632
|
+
if (this.belongsElsewhere(message.project_path)) {
|
|
34633
|
+
this.sendElsewhere(message.project_path);
|
|
34634
|
+
return;
|
|
34635
|
+
}
|
|
34600
34636
|
this.connectionInfo.projectPath = message.project_path;
|
|
34601
34637
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
34602
34638
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
@@ -34679,6 +34715,21 @@ var init_godot_bridge = __esm(() => {
|
|
|
34679
34715
|
clearInterval(this.pingInterval);
|
|
34680
34716
|
this.pingInterval = null;
|
|
34681
34717
|
}
|
|
34718
|
+
belongsElsewhere(theirProject) {
|
|
34719
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
34720
|
+
return false;
|
|
34721
|
+
}
|
|
34722
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
34723
|
+
}
|
|
34724
|
+
sendElsewhere(theirProject) {
|
|
34725
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
34726
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
34727
|
+
const stranger = this.socket;
|
|
34728
|
+
this.socket = null;
|
|
34729
|
+
this.connectionInfo = null;
|
|
34730
|
+
this.stopKeepalive();
|
|
34731
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
34732
|
+
}
|
|
34682
34733
|
handleDisconnect(disconnectedSocket, reason) {
|
|
34683
34734
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
34684
34735
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -35603,7 +35654,7 @@ var init_lsp_client = __esm(() => {
|
|
|
35603
35654
|
});
|
|
35604
35655
|
|
|
35605
35656
|
// src/project-scan.ts
|
|
35606
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
35657
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync10 } from "node:fs";
|
|
35607
35658
|
import { join as join12 } from "node:path";
|
|
35608
35659
|
function projectStructure(projectPath) {
|
|
35609
35660
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
@@ -35666,7 +35717,7 @@ function searchProject(projectPath, options) {
|
|
|
35666
35717
|
}
|
|
35667
35718
|
result.summary.files_searched += 1;
|
|
35668
35719
|
const matches = [];
|
|
35669
|
-
for (const [index, line] of
|
|
35720
|
+
for (const [index, line] of readFileSync10(entryPath, "utf8").split(`
|
|
35670
35721
|
`).entries()) {
|
|
35671
35722
|
if (full()) {
|
|
35672
35723
|
break;
|
|
@@ -35694,7 +35745,7 @@ var init_project_scan = __esm(() => {
|
|
|
35694
35745
|
});
|
|
35695
35746
|
|
|
35696
35747
|
// src/runtime-client.ts
|
|
35697
|
-
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as
|
|
35748
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync11, unlinkSync } from "node:fs";
|
|
35698
35749
|
import { createConnection as createConnection3 } from "node:net";
|
|
35699
35750
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
35700
35751
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
@@ -35725,7 +35776,7 @@ function processAlive(pid) {
|
|
|
35725
35776
|
function parseAnnouncement(file, pid) {
|
|
35726
35777
|
let fields;
|
|
35727
35778
|
try {
|
|
35728
|
-
fields = asParams(JSON.parse(
|
|
35779
|
+
fields = asParams(JSON.parse(readFileSync11(file, "utf8")));
|
|
35729
35780
|
} catch {
|
|
35730
35781
|
return { kind: "rubbish" };
|
|
35731
35782
|
}
|
|
@@ -35918,14 +35969,14 @@ var init_runtime_client = __esm(() => {
|
|
|
35918
35969
|
});
|
|
35919
35970
|
|
|
35920
35971
|
// src/setup.ts
|
|
35921
|
-
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as
|
|
35972
|
+
import { cpSync as cpSync2, existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35922
35973
|
import { dirname as dirname8, join as join14 } from "node:path";
|
|
35923
35974
|
function installedAddonVersion(projectPath) {
|
|
35924
35975
|
const marker = join14(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
|
|
35925
35976
|
if (!existsSync11(marker)) {
|
|
35926
35977
|
return null;
|
|
35927
35978
|
}
|
|
35928
|
-
const said =
|
|
35979
|
+
const said = readFileSync12(marker, "utf8").trim();
|
|
35929
35980
|
return said === "" ? null : said;
|
|
35930
35981
|
}
|
|
35931
35982
|
var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
|
|
@@ -35939,7 +35990,7 @@ var init_setup = __esm(() => {
|
|
|
35939
35990
|
|
|
35940
35991
|
// src/server.ts
|
|
35941
35992
|
import { execFile as execFile5, spawn } from "node:child_process";
|
|
35942
|
-
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as
|
|
35993
|
+
import { existsSync as existsSync12, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
|
|
35943
35994
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
35944
35995
|
import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
|
|
35945
35996
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -36020,11 +36071,11 @@ function aboveTheRunner(entry) {
|
|
|
36020
36071
|
};
|
|
36021
36072
|
}
|
|
36022
36073
|
function hasMainScene(projectFile) {
|
|
36023
|
-
const scene = parseProjectGodot(
|
|
36074
|
+
const scene = parseProjectGodot(readFileSync13(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
36024
36075
|
return typeof scene === "string" && scene !== "";
|
|
36025
36076
|
}
|
|
36026
36077
|
function editorPlaysHeadless(projectFile) {
|
|
36027
|
-
const runArgs = parseProjectGodot(
|
|
36078
|
+
const runArgs = parseProjectGodot(readFileSync13(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
36028
36079
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
36029
36080
|
}
|
|
36030
36081
|
function camelCased(params) {
|
|
@@ -36052,13 +36103,14 @@ class GodotServer {
|
|
|
36052
36103
|
dapClient = null;
|
|
36053
36104
|
bridgeStartupError = null;
|
|
36054
36105
|
bridgeRetry = null;
|
|
36106
|
+
successorWatch = null;
|
|
36055
36107
|
lastProjectPath = null;
|
|
36056
36108
|
shutdownInitiated = false;
|
|
36057
36109
|
ownProject;
|
|
36058
36110
|
announcedAt = null;
|
|
36059
36111
|
constructor() {
|
|
36060
36112
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
36061
|
-
this.godotBridge = getDefaultBridge(this.ownProject
|
|
36113
|
+
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
36062
36114
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
36063
36115
|
this.setupToolHandlers();
|
|
36064
36116
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -36151,6 +36203,7 @@ class GodotServer {
|
|
|
36151
36203
|
port: status.port,
|
|
36152
36204
|
version: SERVER_VERSION
|
|
36153
36205
|
});
|
|
36206
|
+
this.watchForASuccessor();
|
|
36154
36207
|
}
|
|
36155
36208
|
stopTryingTheBridge() {
|
|
36156
36209
|
if (this.bridgeRetry !== null) {
|
|
@@ -36158,9 +36211,46 @@ class GodotServer {
|
|
|
36158
36211
|
this.bridgeRetry = null;
|
|
36159
36212
|
}
|
|
36160
36213
|
}
|
|
36214
|
+
watchForASuccessor() {
|
|
36215
|
+
if (this.ownProject === null || this.successorWatch !== null) {
|
|
36216
|
+
return;
|
|
36217
|
+
}
|
|
36218
|
+
const watch = setInterval(() => {
|
|
36219
|
+
const successor = this.supersededBy();
|
|
36220
|
+
if (successor === null) {
|
|
36221
|
+
return;
|
|
36222
|
+
}
|
|
36223
|
+
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
36224
|
+
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
36225
|
+
}, SUCCESSOR_CHECK_MS);
|
|
36226
|
+
watch.unref();
|
|
36227
|
+
this.successorWatch = watch;
|
|
36228
|
+
}
|
|
36229
|
+
supersededBy() {
|
|
36230
|
+
if (this.ownProject === null) {
|
|
36231
|
+
return null;
|
|
36232
|
+
}
|
|
36233
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
36234
|
+
if (announced === null || announced.pid === process.pid) {
|
|
36235
|
+
return null;
|
|
36236
|
+
}
|
|
36237
|
+
try {
|
|
36238
|
+
process.kill(announced.pid, 0);
|
|
36239
|
+
return announced.pid;
|
|
36240
|
+
} catch {
|
|
36241
|
+
return null;
|
|
36242
|
+
}
|
|
36243
|
+
}
|
|
36244
|
+
stopWatchingForASuccessor() {
|
|
36245
|
+
if (this.successorWatch !== null) {
|
|
36246
|
+
clearInterval(this.successorWatch);
|
|
36247
|
+
this.successorWatch = null;
|
|
36248
|
+
}
|
|
36249
|
+
}
|
|
36161
36250
|
async cleanup() {
|
|
36162
36251
|
this.logDebug("Cleaning up resources");
|
|
36163
36252
|
this.stopTryingTheBridge();
|
|
36253
|
+
this.stopWatchingForASuccessor();
|
|
36164
36254
|
withdrawBridge(this.announcedAt);
|
|
36165
36255
|
this.announcedAt = null;
|
|
36166
36256
|
if (this.activeProcess) {
|
|
@@ -36742,7 +36832,7 @@ class GodotServer {
|
|
|
36742
36832
|
if (!engine.ok) {
|
|
36743
36833
|
return engine.response;
|
|
36744
36834
|
}
|
|
36745
|
-
const application = parseProjectGodot(
|
|
36835
|
+
const application = parseProjectGodot(readFileSync13(project.value.file, "utf8"))["application"] ?? {};
|
|
36746
36836
|
const name = application["config/name"];
|
|
36747
36837
|
const mainScene = application["run/main_scene"];
|
|
36748
36838
|
const info = {
|
|
@@ -36917,7 +37007,7 @@ class GodotServer {
|
|
|
36917
37007
|
const written = existsSync12(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
36918
37008
|
const newest = written.at(-1);
|
|
36919
37009
|
if (newest !== undefined) {
|
|
36920
|
-
report = parseJUnit(
|
|
37010
|
+
report = parseJUnit(readFileSync13(join15(reportsDir, newest, "results.xml"), "utf8"));
|
|
36921
37011
|
}
|
|
36922
37012
|
} catch (error) {
|
|
36923
37013
|
reportProblem = errorMessage(error);
|
|
@@ -37507,7 +37597,7 @@ class GodotServer {
|
|
|
37507
37597
|
return {
|
|
37508
37598
|
content: [
|
|
37509
37599
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
37510
|
-
{ type: "image", data:
|
|
37600
|
+
{ type: "image", data: readFileSync13(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
37511
37601
|
]
|
|
37512
37602
|
};
|
|
37513
37603
|
} finally {
|
|
@@ -37573,7 +37663,7 @@ async function runGodotServer() {
|
|
|
37573
37663
|
const server = new GodotServer;
|
|
37574
37664
|
await server.run();
|
|
37575
37665
|
}
|
|
37576
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
37666
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
37577
37667
|
var init_server3 = __esm(() => {
|
|
37578
37668
|
init_mcp();
|
|
37579
37669
|
init_stdio2();
|
|
@@ -18,6 +18,10 @@ const ANNOUNCE_PROTOCOL: int = 1
|
|
|
18
18
|
const RECONNECT_DELAY: float = 3.0
|
|
19
19
|
const MAX_RECONNECT_DELAY: float = 30.0
|
|
20
20
|
|
|
21
|
+
## What a server closes with when the editor that said hello belongs to another project. Kept in
|
|
22
|
+
## step with `OTHER_PROJECT_CLOSE_CODE` in src/godot-bridge.ts.
|
|
23
|
+
const ELSEWHERE_CLOSE_CODE: int = 4001
|
|
24
|
+
|
|
21
25
|
## Where Godot keeps the three ports an editor serves. None of them is per editor: the settings
|
|
22
26
|
## file is one for every editor on the machine, so two open at once want the same three and the
|
|
23
27
|
## second one binds nothing.
|
|
@@ -67,6 +71,10 @@ var _since_looked: float = 0.0
|
|
|
67
71
|
## anything connects or the project names a different one.
|
|
68
72
|
var _refused_url: String = ""
|
|
69
73
|
var _tried_announced: bool = false
|
|
74
|
+
|
|
75
|
+
## The address this editor has already been told is another project's, so a reconnect loop says it
|
|
76
|
+
## once rather than every few seconds for the rest of the session.
|
|
77
|
+
var _told_about_url: String = ""
|
|
70
78
|
var _connecting_for: float = 0.0
|
|
71
79
|
|
|
72
80
|
|
|
@@ -343,12 +351,35 @@ func _loaded_version() -> String:
|
|
|
343
351
|
|
|
344
352
|
func _handle_disconnect() -> void:
|
|
345
353
|
_is_connected = false
|
|
354
|
+
_said_elsewhere()
|
|
346
355
|
disconnected.emit()
|
|
347
356
|
|
|
348
357
|
if _should_reconnect:
|
|
349
358
|
_schedule_reconnect()
|
|
350
359
|
|
|
351
360
|
|
|
361
|
+
## Says so when the server turned this editor away as another project's.
|
|
362
|
+
##
|
|
363
|
+
## The fallback address is tried by every project whose own server has not run yet and so has
|
|
364
|
+
## announced nothing, which means any server holding it took the connection and served it. It is
|
|
365
|
+
## refused now, and the refusal is worth a person's attention rather than a quiet backoff: nothing
|
|
366
|
+
## about waiting reaches a server that is not this project's, and the answer is to start one.
|
|
367
|
+
##
|
|
368
|
+
## Said once per address. This is a reconnect loop, and a line every few seconds for the rest of
|
|
369
|
+
## the session is a line nobody reads.
|
|
370
|
+
func _said_elsewhere() -> void:
|
|
371
|
+
if socket.get_close_code() != ELSEWHERE_CLOSE_CODE:
|
|
372
|
+
return
|
|
373
|
+
if _told_about_url == server_url:
|
|
374
|
+
return
|
|
375
|
+
_told_about_url = server_url
|
|
376
|
+
var said: String = (
|
|
377
|
+
"[gdharness] %s is another project's server, so this editor is not connected: %s."
|
|
378
|
+
+ " Start this project's own server, or point it at one with GDHARNESS_BRIDGE_PORT."
|
|
379
|
+
)
|
|
380
|
+
push_warning(said % [server_url, socket.get_close_reason()])
|
|
381
|
+
|
|
382
|
+
|
|
352
383
|
func _schedule_reconnect() -> void:
|
|
353
384
|
if _reconnect_timer == null:
|
|
354
385
|
return
|
|
@@ -2,6 +2,8 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## A picture of the running game, as a PNG written where the server asked for it.
|
|
4
4
|
|
|
5
|
+
const Values = preload("runtime_values.gd")
|
|
6
|
+
|
|
5
7
|
var _host: Node
|
|
6
8
|
|
|
7
9
|
|
|
@@ -18,9 +20,10 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
18
20
|
if viewport_path.is_empty():
|
|
19
21
|
return capture_screenshot(params)
|
|
20
22
|
|
|
21
|
-
var
|
|
22
|
-
if
|
|
23
|
-
return
|
|
23
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, viewport_path)
|
|
24
|
+
if standing.has("message"):
|
|
25
|
+
return standing
|
|
26
|
+
var node: Node = standing["node"]
|
|
24
27
|
if not node is Viewport:
|
|
25
28
|
return {"type": "error", "message": "Node is not a Viewport: " + viewport_path}
|
|
26
29
|
return _capture(node, params)
|
|
@@ -440,9 +440,10 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
440
440
|
if node_path.is_empty():
|
|
441
441
|
return {"type": "error", "message": "Node path required"}
|
|
442
442
|
|
|
443
|
-
var
|
|
444
|
-
if
|
|
445
|
-
return
|
|
443
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
444
|
+
if standing.has("message"):
|
|
445
|
+
return standing
|
|
446
|
+
var node: Node = standing["node"]
|
|
446
447
|
if node is Node3D:
|
|
447
448
|
return await _click_in_the_world(node_path, node, params)
|
|
448
449
|
if not node is Control:
|
|
@@ -580,9 +581,10 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
580
581
|
var node_path: String = str(params.get("path", ""))
|
|
581
582
|
if node_path.is_empty():
|
|
582
583
|
return {"type": "error", "message": "Node path required"}
|
|
583
|
-
var
|
|
584
|
-
if
|
|
585
|
-
return
|
|
584
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
585
|
+
if standing.has("message"):
|
|
586
|
+
return standing
|
|
587
|
+
var node: Node = standing["node"]
|
|
586
588
|
|
|
587
589
|
var menu: PopupMenu = _menu_of(node)
|
|
588
590
|
if menu == null:
|
|
@@ -34,9 +34,10 @@ func get_tree(params: Dictionary) -> Dictionary:
|
|
|
34
34
|
var max_depth: int = int(params.get("depth", 3))
|
|
35
35
|
var include_properties: bool = bool(params.get("include_properties", false))
|
|
36
36
|
|
|
37
|
-
var
|
|
38
|
-
if
|
|
39
|
-
return
|
|
37
|
+
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
38
|
+
if reached.has("message"):
|
|
39
|
+
return reached
|
|
40
|
+
var root: Node = reached["node"]
|
|
40
41
|
|
|
41
42
|
return {"type": "tree", "root": _serialize_node_tree(root, 0, max_depth, include_properties)}
|
|
42
43
|
|
|
@@ -64,9 +65,10 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
64
65
|
if not wanted["script"].is_empty() and not wanted["script"].begins_with("res://"):
|
|
65
66
|
wanted["script"] = "res://" + wanted["script"]
|
|
66
67
|
|
|
67
|
-
var
|
|
68
|
-
if
|
|
69
|
-
return
|
|
68
|
+
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
69
|
+
if reached.has("message"):
|
|
70
|
+
return reached
|
|
71
|
+
var root: Node = reached["node"]
|
|
70
72
|
|
|
71
73
|
var found: Array[Dictionary] = []
|
|
72
74
|
var pending: Array[Node] = [root]
|
|
@@ -128,7 +130,7 @@ func _found(node: Node, wanted_property: String) -> Dictionary:
|
|
|
128
130
|
entry["has_property"] = false
|
|
129
131
|
return entry
|
|
130
132
|
var holder: Object = reached["holder"]
|
|
131
|
-
var named: String = reached["
|
|
133
|
+
var named: String = reached["name"]
|
|
132
134
|
var has: bool = _has_property(holder, named)
|
|
133
135
|
entry["has_property"] = has
|
|
134
136
|
if has:
|
|
@@ -208,9 +210,10 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
208
210
|
var include_hidden: bool = bool(params.get("include_hidden", false))
|
|
209
211
|
var limit: int = clampi(int(params.get("limit", READ_LIMIT)), 1, READ_LIMIT)
|
|
210
212
|
|
|
211
|
-
var
|
|
212
|
-
if
|
|
213
|
-
return
|
|
213
|
+
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
214
|
+
if reached.has("message"):
|
|
215
|
+
return reached
|
|
216
|
+
var root: Node = reached["node"]
|
|
214
217
|
|
|
215
218
|
# One line further than asked for, so that whether anything was left behind is read off the
|
|
216
219
|
# walk rather than guessed at from the count: a panel of exactly as many lines as the caller
|
|
@@ -275,9 +278,10 @@ func get_rect(params: Dictionary) -> Dictionary:
|
|
|
275
278
|
if node_path.is_empty():
|
|
276
279
|
return {"type": "error", "message": "Node path required"}
|
|
277
280
|
|
|
278
|
-
var
|
|
279
|
-
if
|
|
280
|
-
return
|
|
281
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
282
|
+
if standing.has("message"):
|
|
283
|
+
return standing
|
|
284
|
+
var node: Node = standing["node"]
|
|
281
285
|
|
|
282
286
|
if node is Control:
|
|
283
287
|
var control: Control = node
|
|
@@ -426,16 +430,17 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
426
430
|
if node_path.is_empty() or property.is_empty():
|
|
427
431
|
return {"type": "error", "message": "Node path and property required"}
|
|
428
432
|
|
|
429
|
-
var
|
|
430
|
-
if
|
|
431
|
-
return
|
|
433
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
434
|
+
if standing.has("message"):
|
|
435
|
+
return standing
|
|
436
|
+
var node: Node = standing["node"]
|
|
432
437
|
|
|
433
438
|
var reached: Dictionary = _reached(node, node_path, property)
|
|
434
439
|
if reached.has("message"):
|
|
435
440
|
return reached
|
|
436
441
|
|
|
437
442
|
var holder: Object = reached["holder"]
|
|
438
|
-
var named: String = reached["
|
|
443
|
+
var named: String = reached["name"]
|
|
439
444
|
# Asked of the property list rather than read and compared to null, because a property the
|
|
440
445
|
# holder does not have and a property that is null both read as null.
|
|
441
446
|
if not _has_property(holder, named):
|
|
@@ -449,11 +454,14 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
449
454
|
}
|
|
450
455
|
|
|
451
456
|
|
|
452
|
-
## What [param
|
|
453
|
-
## it, and the last name along that path.
|
|
457
|
+
## What [param reaching] names something on, which is the node itself until the name has a colon
|
|
458
|
+
## in it, and the last name along that path.
|
|
454
459
|
##
|
|
455
460
|
## A game's state does not sit on nodes, it hangs off them: the speed of the clock is a property of
|
|
456
461
|
## a [RefCounted] held by a [RefCounted] held by the root, and that is what an agent asks about.
|
|
462
|
+
## What a game does hangs off them the same way, so the last name is a property to read, a property
|
|
463
|
+
## to write or a method to call, and the walk to it is one walk.
|
|
464
|
+
##
|
|
457
465
|
## Colons, because that is the separator [method Object.get_indexed] already takes. Walked a step
|
|
458
466
|
## at a time rather than handed to that method, which answers null for a path that goes wrong
|
|
459
467
|
## halfway along and for one that ends on null.
|
|
@@ -461,8 +469,8 @@ func get_property(params: Dictionary) -> Dictionary:
|
|
|
461
469
|
## Answers with a `message` instead when a step along the way is not there or holds something that
|
|
462
470
|
## is not an object, naming the step rather than the whole path: "/root/Main:_game has no property
|
|
463
471
|
## clocks" is a typo found, and "no property _game:clocks:speed" is a puzzle.
|
|
464
|
-
func _reached(node: Node, node_path: String,
|
|
465
|
-
var parts: PackedStringArray =
|
|
472
|
+
func _reached(node: Node, node_path: String, reaching: String) -> Dictionary:
|
|
473
|
+
var parts: PackedStringArray = reaching.split(":")
|
|
466
474
|
var holder: Object = node
|
|
467
475
|
var called: String = node_path
|
|
468
476
|
for step: int in parts.size() - 1:
|
|
@@ -475,7 +483,7 @@ func _reached(node: Node, node_path: String, property: String) -> Dictionary:
|
|
|
475
483
|
var wanted: String = parts[step + 1]
|
|
476
484
|
return {"type": "error", "message": "%s holds no object to read %s off" % [called, wanted]}
|
|
477
485
|
holder = next
|
|
478
|
-
return {"holder": holder, "
|
|
486
|
+
return {"holder": holder, "name": parts[parts.size() - 1], "called": called}
|
|
479
487
|
|
|
480
488
|
|
|
481
489
|
## Whether [param holder] declares [param named]. Its own function because three ops ask it and a
|
|
@@ -495,9 +503,10 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
495
503
|
if node_path.is_empty() or property.is_empty():
|
|
496
504
|
return {"type": "error", "message": "Node path and property required"}
|
|
497
505
|
|
|
498
|
-
var
|
|
499
|
-
if
|
|
500
|
-
return
|
|
506
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
507
|
+
if standing.has("message"):
|
|
508
|
+
return standing
|
|
509
|
+
var node: Node = standing["node"]
|
|
501
510
|
|
|
502
511
|
# Through a path as well, for the reason [method _reached] gives, and read back off the same
|
|
503
512
|
# holder afterwards: a set that does not take says so by answering with the old value, which is
|
|
@@ -507,7 +516,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
507
516
|
return reached
|
|
508
517
|
|
|
509
518
|
var holder: Object = reached["holder"]
|
|
510
|
-
var named: String = reached["
|
|
519
|
+
var named: String = reached["name"]
|
|
511
520
|
var old_value: Variant = holder.get(named)
|
|
512
521
|
holder.set(named, _values.fitted(value, typeof(old_value)))
|
|
513
522
|
|
|
@@ -528,18 +537,28 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
528
537
|
if node_path.is_empty() or method.is_empty():
|
|
529
538
|
return {"type": "error", "message": "Node path and method required"}
|
|
530
539
|
|
|
531
|
-
var
|
|
532
|
-
if
|
|
533
|
-
return
|
|
540
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
541
|
+
if standing.has("message"):
|
|
542
|
+
return standing
|
|
543
|
+
var node: Node = standing["node"]
|
|
534
544
|
|
|
535
|
-
|
|
536
|
-
|
|
545
|
+
# Through a path as well, for the reason [method _reached] gives. What a game does hangs off its
|
|
546
|
+
# nodes as much as its state does, so reading `_game:run:day` while being unable to call
|
|
547
|
+
# `_game:run:advance` answers half of what a node holds and refuses the other half.
|
|
548
|
+
var reached: Dictionary = _reached(node, node_path, method)
|
|
549
|
+
if reached.has("message"):
|
|
550
|
+
return reached
|
|
551
|
+
|
|
552
|
+
var holder: Object = reached["holder"]
|
|
553
|
+
var named: String = reached["name"]
|
|
554
|
+
if not holder.has_method(named):
|
|
555
|
+
return {"type": "error", "message": "%s has no method %s" % [reached["called"], named]}
|
|
537
556
|
|
|
538
557
|
var deserialized_args: Array = []
|
|
539
558
|
for index: int in args.size():
|
|
540
|
-
deserialized_args.append(_values.fitted(args[index], _values.parameter_type(
|
|
559
|
+
deserialized_args.append(_values.fitted(args[index], _values.parameter_type(holder, named, index)))
|
|
541
560
|
|
|
542
|
-
var result: Variant =
|
|
561
|
+
var result: Variant = holder.callv(named, deserialized_args)
|
|
543
562
|
|
|
544
563
|
return {"type": "method_result", "path": node_path, "method": method, "result": _values.serialize(result)}
|
|
545
564
|
|
|
@@ -42,6 +42,37 @@ func serialize(value: Variant) -> Variant:
|
|
|
42
42
|
return call(serialiser, value) if not serialiser.is_empty() else value
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
## The node [param path] names under [param root], or a refusal saying why there is not one.
|
|
46
|
+
##
|
|
47
|
+
## The other direction of what a node serialises as, and here for that reason: a node crosses the
|
|
48
|
+
## wire as its path, so a path crossing back is a node or it is nothing. One answer for every op,
|
|
49
|
+
## because each of them read a path for itself and they could disagree about what one means.
|
|
50
|
+
##
|
|
51
|
+
## A colon is the disagreement worth its own sentence. Godot reads everything after the first one
|
|
52
|
+
## as subnames and [method Node.get_node_or_null] drops them, so "/root/Main:_game:run" was quietly
|
|
53
|
+
## answered about "/root/Main", and an op that then said the path had no method `advance` was right
|
|
54
|
+
## about an object nobody had asked for. Colons reach through what a node holds, and the arguments
|
|
55
|
+
## that take them are the property and the method.
|
|
56
|
+
static func node_at(root: Node, path: String) -> Dictionary:
|
|
57
|
+
if path.is_empty():
|
|
58
|
+
return {"type": "error", "message": "Node path required"}
|
|
59
|
+
|
|
60
|
+
if path.contains(":"):
|
|
61
|
+
var shape: String = (
|
|
62
|
+
'"%s" names a node and colons reach past one: "%s" is the node,'
|
|
63
|
+
+ ' and "%s" goes in the property or the method'
|
|
64
|
+
)
|
|
65
|
+
return {
|
|
66
|
+
"type": "error",
|
|
67
|
+
"message": shape % [path, path.get_slice(":", 0), path.substr(path.find(":") + 1)],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var node: Node = root.get_node_or_null(path)
|
|
71
|
+
if node == null:
|
|
72
|
+
return {"type": "error", "message": "Node not found: " + path}
|
|
73
|
+
return {"node": node}
|
|
74
|
+
|
|
75
|
+
|
|
45
76
|
func _serialize_nil(_value: Variant) -> Variant:
|
|
46
77
|
return null
|
|
47
78
|
|
|
@@ -53,9 +53,10 @@ func wait_signal(params: Dictionary) -> Dictionary:
|
|
|
53
53
|
if node_path.is_empty() or signal_name.is_empty():
|
|
54
54
|
return {"type": "error", "message": "Node path and signal name required"}
|
|
55
55
|
|
|
56
|
-
var
|
|
57
|
-
if
|
|
58
|
-
return
|
|
56
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
57
|
+
if standing.has("message"):
|
|
58
|
+
return standing
|
|
59
|
+
var node: Node = standing["node"]
|
|
59
60
|
if not node.has_signal(signal_name):
|
|
60
61
|
return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
|
|
61
62
|
|
|
@@ -90,8 +91,9 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
90
91
|
return _out_of_range("timeout_ms", timeout_ms, 1, CEILING_MSEC)
|
|
91
92
|
if node_path.is_empty():
|
|
92
93
|
return {"type": "error", "message": "Node path required"}
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
var standing: Dictionary = Values.node_at(_host.get_tree().root, node_path)
|
|
95
|
+
if standing.has("message"):
|
|
96
|
+
return standing
|
|
95
97
|
if not says.is_empty():
|
|
96
98
|
return await _wait_until_said(node_path, says, timeout_ms)
|
|
97
99
|
if property.is_empty():
|
|
@@ -99,7 +101,7 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
99
101
|
if not params.has("value"):
|
|
100
102
|
return {"type": "error", "message": "A value to wait for is required"}
|
|
101
103
|
|
|
102
|
-
var node: Node =
|
|
104
|
+
var node: Node = standing["node"]
|
|
103
105
|
|
|
104
106
|
var current: Variant = node.get(property)
|
|
105
107
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
package/build/index.js
CHANGED
|
@@ -10126,7 +10126,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10126
10126
|
|
|
10127
10127
|
// src/server.ts
|
|
10128
10128
|
import { execFile as execFile3, spawn } from "node:child_process";
|
|
10129
|
-
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as
|
|
10129
|
+
import { existsSync as existsSync7, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
|
|
10130
10130
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10131
10131
|
import { basename, dirname as dirname4, join as join9, normalize as normalize2 } from "node:path";
|
|
10132
10132
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
@@ -24697,12 +24697,30 @@ class StdioServerTransport {
|
|
|
24697
24697
|
}
|
|
24698
24698
|
|
|
24699
24699
|
// src/bridge-announce.ts
|
|
24700
|
-
import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24700
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
24701
24701
|
import { dirname, join as join2 } from "node:path";
|
|
24702
24702
|
function announcementPath(projectPath) {
|
|
24703
24703
|
return join2(projectPath, ".godot", "gdharness-bridge.json");
|
|
24704
24704
|
}
|
|
24705
24705
|
var BRIDGE_ANNOUNCE_PROTOCOL = 1;
|
|
24706
|
+
function readAnnouncement(path) {
|
|
24707
|
+
try {
|
|
24708
|
+
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
24709
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
24710
|
+
return null;
|
|
24711
|
+
}
|
|
24712
|
+
const said = parsed;
|
|
24713
|
+
if (typeof said.port !== "number" || typeof said.pid !== "number") {
|
|
24714
|
+
return null;
|
|
24715
|
+
}
|
|
24716
|
+
return said;
|
|
24717
|
+
} catch {
|
|
24718
|
+
return null;
|
|
24719
|
+
}
|
|
24720
|
+
}
|
|
24721
|
+
function announcementIsOurs(path) {
|
|
24722
|
+
return readAnnouncement(path)?.pid === process.pid;
|
|
24723
|
+
}
|
|
24706
24724
|
function announceBridge(projectPath, bridge) {
|
|
24707
24725
|
const path = announcementPath(projectPath);
|
|
24708
24726
|
try {
|
|
@@ -24724,7 +24742,7 @@ function announceBridge(projectPath, bridge) {
|
|
|
24724
24742
|
}
|
|
24725
24743
|
}
|
|
24726
24744
|
function withdrawBridge(path) {
|
|
24727
|
-
if (path === null) {
|
|
24745
|
+
if (path === null || !announcementIsOurs(path)) {
|
|
24728
24746
|
return;
|
|
24729
24747
|
}
|
|
24730
24748
|
try {
|
|
@@ -24735,7 +24753,7 @@ function withdrawBridge(path) {
|
|
|
24735
24753
|
}
|
|
24736
24754
|
|
|
24737
24755
|
// src/class-cache.ts
|
|
24738
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
24756
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
24739
24757
|
import { join as join3 } from "node:path";
|
|
24740
24758
|
function declaredClasses(projectPath) {
|
|
24741
24759
|
const declared = new Map;
|
|
@@ -24748,7 +24766,7 @@ function declaredClasses(projectPath) {
|
|
|
24748
24766
|
if (entry.isDirectory()) {
|
|
24749
24767
|
visit(path, `${prefix}${entry.name}/`);
|
|
24750
24768
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24751
|
-
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(
|
|
24769
|
+
const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync4(path, "utf8"));
|
|
24752
24770
|
if (found?.[1]) {
|
|
24753
24771
|
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24754
24772
|
}
|
|
@@ -24764,7 +24782,7 @@ function cachedClasses(projectPath) {
|
|
|
24764
24782
|
return null;
|
|
24765
24783
|
}
|
|
24766
24784
|
const listed = new Map;
|
|
24767
|
-
const text =
|
|
24785
|
+
const text = readFileSync4(cache, "utf8");
|
|
24768
24786
|
for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
|
|
24769
24787
|
listed.set(entry[1] ?? "", entry[2] ?? "");
|
|
24770
24788
|
}
|
|
@@ -25494,12 +25512,64 @@ var import_subprotocol = __toESM(require_subprotocol(), 1);
|
|
|
25494
25512
|
var import_websocket = __toESM(require_websocket(), 1);
|
|
25495
25513
|
var import_websocket_server = __toESM(require_websocket_server(), 1);
|
|
25496
25514
|
|
|
25515
|
+
// src/paths.ts
|
|
25516
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
25517
|
+
var RESOURCE_SCHEME = "res://";
|
|
25518
|
+
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
25519
|
+
function isSameDirectory(onePath, otherPath) {
|
|
25520
|
+
return relative(resolve(onePath), resolve(otherPath)) === "";
|
|
25521
|
+
}
|
|
25522
|
+
function isWithinRoot(rootPath, candidatePath) {
|
|
25523
|
+
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
25524
|
+
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
25525
|
+
}
|
|
25526
|
+
function resolveWithinProject(projectPath, candidatePath) {
|
|
25527
|
+
const trimmed = candidatePath.trim();
|
|
25528
|
+
if (trimmed === "") {
|
|
25529
|
+
return { ok: false, reason: "Path is empty." };
|
|
25530
|
+
}
|
|
25531
|
+
if (trimmed.includes("\x00")) {
|
|
25532
|
+
return { ok: false, reason: "Path contains a null byte." };
|
|
25533
|
+
}
|
|
25534
|
+
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
25535
|
+
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
25536
|
+
if (scheme) {
|
|
25537
|
+
return {
|
|
25538
|
+
ok: false,
|
|
25539
|
+
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
25540
|
+
};
|
|
25541
|
+
}
|
|
25542
|
+
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
25543
|
+
return {
|
|
25544
|
+
ok: false,
|
|
25545
|
+
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
25546
|
+
};
|
|
25547
|
+
}
|
|
25548
|
+
if (withoutScheme.includes(":")) {
|
|
25549
|
+
return {
|
|
25550
|
+
ok: false,
|
|
25551
|
+
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
25552
|
+
};
|
|
25553
|
+
}
|
|
25554
|
+
const root = resolve(projectPath);
|
|
25555
|
+
const absolutePath = resolve(root, withoutScheme);
|
|
25556
|
+
if (!isWithinRoot(root, absolutePath)) {
|
|
25557
|
+
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
25558
|
+
}
|
|
25559
|
+
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
25560
|
+
if (relativePath === "") {
|
|
25561
|
+
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
25562
|
+
}
|
|
25563
|
+
return { ok: true, absolutePath, relativePath };
|
|
25564
|
+
}
|
|
25565
|
+
|
|
25497
25566
|
// src/godot-bridge.ts
|
|
25498
25567
|
var DEFAULT_PORT = 6505;
|
|
25499
25568
|
var DEFAULT_HOST = "127.0.0.1";
|
|
25500
25569
|
var DEFAULT_TIMEOUT_MS = 30000;
|
|
25501
25570
|
var KEEPALIVE_INTERVAL_MS = 1e4;
|
|
25502
25571
|
var SECOND_CONNECTION_CLOSE_CODE = 4000;
|
|
25572
|
+
var OTHER_PROJECT_CLOSE_CODE = 4001;
|
|
25503
25573
|
function resolveDefaultBridgePort() {
|
|
25504
25574
|
return portFromEnv("GDHARNESS_BRIDGE_PORT", DEFAULT_PORT);
|
|
25505
25575
|
}
|
|
@@ -25523,13 +25593,16 @@ class GodotBridge extends EventEmitter {
|
|
|
25523
25593
|
boundPort = null;
|
|
25524
25594
|
host;
|
|
25525
25595
|
timeoutMs;
|
|
25526
|
-
|
|
25527
|
-
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
25596
|
+
ownProject;
|
|
25597
|
+
constructor(port = DEFAULT_PORT, host = DEFAULT_HOST, timeoutMs = DEFAULT_TIMEOUT_MS, ownProject = null) {
|
|
25528
25598
|
super();
|
|
25529
25599
|
this.wantedPort = port;
|
|
25530
25600
|
this.host = host;
|
|
25531
25601
|
this.timeoutMs = timeoutMs;
|
|
25532
|
-
this.
|
|
25602
|
+
this.ownProject = ownProject;
|
|
25603
|
+
}
|
|
25604
|
+
get mayMove() {
|
|
25605
|
+
return this.ownProject !== null;
|
|
25533
25606
|
}
|
|
25534
25607
|
get port() {
|
|
25535
25608
|
return this.boundPort ?? this.wantedPort;
|
|
@@ -25775,6 +25848,10 @@ class GodotBridge extends EventEmitter {
|
|
|
25775
25848
|
}
|
|
25776
25849
|
case "godot_ready":
|
|
25777
25850
|
if (this.connectionInfo) {
|
|
25851
|
+
if (this.belongsElsewhere(message.project_path)) {
|
|
25852
|
+
this.sendElsewhere(message.project_path);
|
|
25853
|
+
return;
|
|
25854
|
+
}
|
|
25778
25855
|
this.connectionInfo.projectPath = message.project_path;
|
|
25779
25856
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
25780
25857
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
@@ -25857,6 +25934,21 @@ class GodotBridge extends EventEmitter {
|
|
|
25857
25934
|
clearInterval(this.pingInterval);
|
|
25858
25935
|
this.pingInterval = null;
|
|
25859
25936
|
}
|
|
25937
|
+
belongsElsewhere(theirProject) {
|
|
25938
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
25939
|
+
return false;
|
|
25940
|
+
}
|
|
25941
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
25942
|
+
}
|
|
25943
|
+
sendElsewhere(theirProject) {
|
|
25944
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
25945
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
25946
|
+
const stranger = this.socket;
|
|
25947
|
+
this.socket = null;
|
|
25948
|
+
this.connectionInfo = null;
|
|
25949
|
+
this.stopKeepalive();
|
|
25950
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, said);
|
|
25951
|
+
}
|
|
25860
25952
|
handleDisconnect(disconnectedSocket, reason) {
|
|
25861
25953
|
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
25862
25954
|
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
@@ -25933,8 +26025,8 @@ class GodotBridge extends EventEmitter {
|
|
|
25933
26025
|
}
|
|
25934
26026
|
}
|
|
25935
26027
|
var defaultBridge = null;
|
|
25936
|
-
function getDefaultBridge(
|
|
25937
|
-
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS,
|
|
26028
|
+
function getDefaultBridge(ownProject = null) {
|
|
26029
|
+
defaultBridge ??= new GodotBridge(resolveDefaultBridgePort(), resolveDefaultBridgeHost(), DEFAULT_TIMEOUT_MS, ownProject);
|
|
25938
26030
|
return defaultBridge;
|
|
25939
26031
|
}
|
|
25940
26032
|
|
|
@@ -26495,56 +26587,6 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
26495
26587
|
import { createConnection as createConnection2 } from "node:net";
|
|
26496
26588
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
26497
26589
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
26498
|
-
|
|
26499
|
-
// src/paths.ts
|
|
26500
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
26501
|
-
var RESOURCE_SCHEME = "res://";
|
|
26502
|
-
var OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
|
|
26503
|
-
function isWithinRoot(rootPath, candidatePath) {
|
|
26504
|
-
const step = relative(resolve(rootPath), resolve(candidatePath));
|
|
26505
|
-
return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
|
|
26506
|
-
}
|
|
26507
|
-
function resolveWithinProject(projectPath, candidatePath) {
|
|
26508
|
-
const trimmed = candidatePath.trim();
|
|
26509
|
-
if (trimmed === "") {
|
|
26510
|
-
return { ok: false, reason: "Path is empty." };
|
|
26511
|
-
}
|
|
26512
|
-
if (trimmed.includes("\x00")) {
|
|
26513
|
-
return { ok: false, reason: "Path contains a null byte." };
|
|
26514
|
-
}
|
|
26515
|
-
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
26516
|
-
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
26517
|
-
if (scheme) {
|
|
26518
|
-
return {
|
|
26519
|
-
ok: false,
|
|
26520
|
-
reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.`
|
|
26521
|
-
};
|
|
26522
|
-
}
|
|
26523
|
-
if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
|
|
26524
|
-
return {
|
|
26525
|
-
ok: false,
|
|
26526
|
-
reason: `Path '${trimmed}' is absolute; give a path inside the project, such as 'scenes/main.tscn'.`
|
|
26527
|
-
};
|
|
26528
|
-
}
|
|
26529
|
-
if (withoutScheme.includes(":")) {
|
|
26530
|
-
return {
|
|
26531
|
-
ok: false,
|
|
26532
|
-
reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
|
|
26533
|
-
};
|
|
26534
|
-
}
|
|
26535
|
-
const root = resolve(projectPath);
|
|
26536
|
-
const absolutePath = resolve(root, withoutScheme);
|
|
26537
|
-
if (!isWithinRoot(root, absolutePath)) {
|
|
26538
|
-
return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
|
|
26539
|
-
}
|
|
26540
|
-
const relativePath = relative(root, absolutePath).split(sep).join("/");
|
|
26541
|
-
if (relativePath === "") {
|
|
26542
|
-
return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
|
|
26543
|
-
}
|
|
26544
|
-
return { ok: true, absolutePath, relativePath };
|
|
26545
|
-
}
|
|
26546
|
-
|
|
26547
|
-
// src/lsp_client.ts
|
|
26548
26590
|
var DEFAULT_LSP_PORT = 6005;
|
|
26549
26591
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
26550
26592
|
function diagnosticsKey(uri) {
|
|
@@ -27038,7 +27080,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
27038
27080
|
}
|
|
27039
27081
|
|
|
27040
27082
|
// src/project-scan.ts
|
|
27041
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
27083
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
27042
27084
|
import { join as join6 } from "node:path";
|
|
27043
27085
|
var SKIPPED = new Set([".git", ".godot", ".import", "node_modules"]);
|
|
27044
27086
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
@@ -27103,7 +27145,7 @@ function searchProject(projectPath, options) {
|
|
|
27103
27145
|
}
|
|
27104
27146
|
result.summary.files_searched += 1;
|
|
27105
27147
|
const matches = [];
|
|
27106
|
-
for (const [index, line] of
|
|
27148
|
+
for (const [index, line] of readFileSync5(entryPath, "utf8").split(`
|
|
27107
27149
|
`).entries()) {
|
|
27108
27150
|
if (full()) {
|
|
27109
27151
|
break;
|
|
@@ -27126,7 +27168,7 @@ function searchProject(projectPath, options) {
|
|
|
27126
27168
|
}
|
|
27127
27169
|
|
|
27128
27170
|
// src/resources.ts
|
|
27129
|
-
import { readFileSync as
|
|
27171
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
27130
27172
|
import { extname, resolve as resolve3 } from "node:path";
|
|
27131
27173
|
var STATIC_RESOURCES = [
|
|
27132
27174
|
{
|
|
@@ -27297,7 +27339,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27297
27339
|
const parsedUri = parseGodotUri(uri);
|
|
27298
27340
|
if (parsedUri.kind === "project-info") {
|
|
27299
27341
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
27300
|
-
const rawProject =
|
|
27342
|
+
const rawProject = readFileSync6(projectFilePath, "utf-8");
|
|
27301
27343
|
const parsedProject = parseProjectGodot(rawProject);
|
|
27302
27344
|
return {
|
|
27303
27345
|
mimeType: "application/json",
|
|
@@ -27306,7 +27348,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
27306
27348
|
}
|
|
27307
27349
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
27308
27350
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
27309
|
-
const text =
|
|
27351
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
27310
27352
|
return {
|
|
27311
27353
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
27312
27354
|
text
|
|
@@ -27343,7 +27385,7 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27343
27385
|
}
|
|
27344
27386
|
|
|
27345
27387
|
// src/runtime-client.ts
|
|
27346
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
|
|
27388
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, unlinkSync } from "node:fs";
|
|
27347
27389
|
import { createConnection as createConnection3 } from "node:net";
|
|
27348
27390
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27349
27391
|
import { join as join7, resolve as resolve4 } from "node:path";
|
|
@@ -27429,7 +27471,7 @@ function processAlive(pid) {
|
|
|
27429
27471
|
function parseAnnouncement(file, pid) {
|
|
27430
27472
|
let fields;
|
|
27431
27473
|
try {
|
|
27432
|
-
fields = asParams(JSON.parse(
|
|
27474
|
+
fields = asParams(JSON.parse(readFileSync7(file, "utf8")));
|
|
27433
27475
|
} catch {
|
|
27434
27476
|
return { kind: "rubbish" };
|
|
27435
27477
|
}
|
|
@@ -27619,7 +27661,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27619
27661
|
}
|
|
27620
27662
|
|
|
27621
27663
|
// src/setup.ts
|
|
27622
|
-
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
27664
|
+
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27623
27665
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27624
27666
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27625
27667
|
var VERSION_MARKER = ".gdharness-version";
|
|
@@ -27628,7 +27670,7 @@ function installedAddonVersion(projectPath) {
|
|
|
27628
27670
|
if (!existsSync6(marker)) {
|
|
27629
27671
|
return null;
|
|
27630
27672
|
}
|
|
27631
|
-
const said =
|
|
27673
|
+
const said = readFileSync8(marker, "utf8").trim();
|
|
27632
27674
|
return said === "" ? null : said;
|
|
27633
27675
|
}
|
|
27634
27676
|
|
|
@@ -28398,7 +28440,11 @@ var TOOL_SPECS = [
|
|
|
28398
28440
|
blank: true,
|
|
28399
28441
|
description: `set: the value, fitted to the property's type. "" writes an empty string.`
|
|
28400
28442
|
},
|
|
28401
|
-
method: {
|
|
28443
|
+
method: {
|
|
28444
|
+
type: "string",
|
|
28445
|
+
ops: ["call"],
|
|
28446
|
+
description: 'call: which one to call. Colons call through the objects a node holds, "_game:run:advance", the same way a property is written through them.'
|
|
28447
|
+
},
|
|
28402
28448
|
args: {
|
|
28403
28449
|
type: "array",
|
|
28404
28450
|
ops: ["call"],
|
|
@@ -28411,7 +28457,10 @@ var TOOL_SPECS = [
|
|
|
28411
28457
|
summary: "set a property, on a node or on an object it holds",
|
|
28412
28458
|
requires: ["property", "value"]
|
|
28413
28459
|
},
|
|
28414
|
-
call: {
|
|
28460
|
+
call: {
|
|
28461
|
+
summary: "call a method, on a node or on an object it holds, and return its result",
|
|
28462
|
+
requires: ["method"]
|
|
28463
|
+
}
|
|
28415
28464
|
}
|
|
28416
28465
|
},
|
|
28417
28466
|
{
|
|
@@ -28714,6 +28763,7 @@ function wrongTypes(spec, args) {
|
|
|
28714
28763
|
return complaints;
|
|
28715
28764
|
}
|
|
28716
28765
|
var BRIDGE_RETRY_MS = 2000;
|
|
28766
|
+
var SUCCESSOR_CHECK_MS = 1e4;
|
|
28717
28767
|
var PATH_SOLUTIONS = [
|
|
28718
28768
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
28719
28769
|
"Point projectPath at the project the file belongs to"
|
|
@@ -28801,11 +28851,11 @@ function aboveTheRunner(entry) {
|
|
|
28801
28851
|
};
|
|
28802
28852
|
}
|
|
28803
28853
|
function hasMainScene(projectFile) {
|
|
28804
|
-
const scene = parseProjectGodot(
|
|
28854
|
+
const scene = parseProjectGodot(readFileSync9(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28805
28855
|
return typeof scene === "string" && scene !== "";
|
|
28806
28856
|
}
|
|
28807
28857
|
function editorPlaysHeadless(projectFile) {
|
|
28808
|
-
const runArgs = parseProjectGodot(
|
|
28858
|
+
const runArgs = parseProjectGodot(readFileSync9(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
28809
28859
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
28810
28860
|
}
|
|
28811
28861
|
function camelCased(params) {
|
|
@@ -28833,13 +28883,14 @@ class GodotServer {
|
|
|
28833
28883
|
dapClient = null;
|
|
28834
28884
|
bridgeStartupError = null;
|
|
28835
28885
|
bridgeRetry = null;
|
|
28886
|
+
successorWatch = null;
|
|
28836
28887
|
lastProjectPath = null;
|
|
28837
28888
|
shutdownInitiated = false;
|
|
28838
28889
|
ownProject;
|
|
28839
28890
|
announcedAt = null;
|
|
28840
28891
|
constructor() {
|
|
28841
28892
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
28842
|
-
this.godotBridge = getDefaultBridge(this.ownProject
|
|
28893
|
+
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
28843
28894
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
28844
28895
|
this.setupToolHandlers();
|
|
28845
28896
|
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
@@ -28932,6 +28983,7 @@ class GodotServer {
|
|
|
28932
28983
|
port: status.port,
|
|
28933
28984
|
version: SERVER_VERSION
|
|
28934
28985
|
});
|
|
28986
|
+
this.watchForASuccessor();
|
|
28935
28987
|
}
|
|
28936
28988
|
stopTryingTheBridge() {
|
|
28937
28989
|
if (this.bridgeRetry !== null) {
|
|
@@ -28939,9 +28991,46 @@ class GodotServer {
|
|
|
28939
28991
|
this.bridgeRetry = null;
|
|
28940
28992
|
}
|
|
28941
28993
|
}
|
|
28994
|
+
watchForASuccessor() {
|
|
28995
|
+
if (this.ownProject === null || this.successorWatch !== null) {
|
|
28996
|
+
return;
|
|
28997
|
+
}
|
|
28998
|
+
const watch = setInterval(() => {
|
|
28999
|
+
const successor = this.supersededBy();
|
|
29000
|
+
if (successor === null) {
|
|
29001
|
+
return;
|
|
29002
|
+
}
|
|
29003
|
+
console.error(`[SERVER] pid ${successor} now serves ${this.ownProject}; this server is finished and is stopping.`);
|
|
29004
|
+
this.handleShutdown(`superseded by ${successor}`, 0);
|
|
29005
|
+
}, SUCCESSOR_CHECK_MS);
|
|
29006
|
+
watch.unref();
|
|
29007
|
+
this.successorWatch = watch;
|
|
29008
|
+
}
|
|
29009
|
+
supersededBy() {
|
|
29010
|
+
if (this.ownProject === null) {
|
|
29011
|
+
return null;
|
|
29012
|
+
}
|
|
29013
|
+
const announced = readAnnouncement(announcementPath(this.ownProject));
|
|
29014
|
+
if (announced === null || announced.pid === process.pid) {
|
|
29015
|
+
return null;
|
|
29016
|
+
}
|
|
29017
|
+
try {
|
|
29018
|
+
process.kill(announced.pid, 0);
|
|
29019
|
+
return announced.pid;
|
|
29020
|
+
} catch {
|
|
29021
|
+
return null;
|
|
29022
|
+
}
|
|
29023
|
+
}
|
|
29024
|
+
stopWatchingForASuccessor() {
|
|
29025
|
+
if (this.successorWatch !== null) {
|
|
29026
|
+
clearInterval(this.successorWatch);
|
|
29027
|
+
this.successorWatch = null;
|
|
29028
|
+
}
|
|
29029
|
+
}
|
|
28942
29030
|
async cleanup() {
|
|
28943
29031
|
this.logDebug("Cleaning up resources");
|
|
28944
29032
|
this.stopTryingTheBridge();
|
|
29033
|
+
this.stopWatchingForASuccessor();
|
|
28945
29034
|
withdrawBridge(this.announcedAt);
|
|
28946
29035
|
this.announcedAt = null;
|
|
28947
29036
|
if (this.activeProcess) {
|
|
@@ -29523,7 +29612,7 @@ class GodotServer {
|
|
|
29523
29612
|
if (!engine.ok) {
|
|
29524
29613
|
return engine.response;
|
|
29525
29614
|
}
|
|
29526
|
-
const application = parseProjectGodot(
|
|
29615
|
+
const application = parseProjectGodot(readFileSync9(project.value.file, "utf8"))["application"] ?? {};
|
|
29527
29616
|
const name = application["config/name"];
|
|
29528
29617
|
const mainScene = application["run/main_scene"];
|
|
29529
29618
|
const info = {
|
|
@@ -29698,7 +29787,7 @@ class GodotServer {
|
|
|
29698
29787
|
const written = existsSync7(reportsDir) ? readdirSync5(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
29699
29788
|
const newest = written.at(-1);
|
|
29700
29789
|
if (newest !== undefined) {
|
|
29701
|
-
report = parseJUnit(
|
|
29790
|
+
report = parseJUnit(readFileSync9(join9(reportsDir, newest, "results.xml"), "utf8"));
|
|
29702
29791
|
}
|
|
29703
29792
|
} catch (error) {
|
|
29704
29793
|
reportProblem = errorMessage(error);
|
|
@@ -30288,7 +30377,7 @@ class GodotServer {
|
|
|
30288
30377
|
return {
|
|
30289
30378
|
content: [
|
|
30290
30379
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
30291
|
-
{ type: "image", data:
|
|
30380
|
+
{ type: "image", data: readFileSync9(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
30292
30381
|
]
|
|
30293
30382
|
};
|
|
30294
30383
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
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",
|