gdharness 0.13.8 → 0.13.10
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 +97 -22
- package/build/index.js +97 -22
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15740,7 +15740,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15740
15740
|
{
|
|
15741
15741
|
name: "editor_launch",
|
|
15742
15742
|
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
|
|
15743
|
-
parameters: { projectPath: PROJECT_PATH },
|
|
15743
|
+
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
15744
15744
|
requires: [],
|
|
15745
15745
|
operations: {
|
|
15746
15746
|
open: { summary: "open the editor on a project", requires: ["projectPath"] },
|
|
@@ -15752,7 +15752,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15752
15752
|
name: "editor_run",
|
|
15753
15753
|
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
15754
15754
|
parameters: {
|
|
15755
|
-
projectPath: PROJECT_PATH,
|
|
15755
|
+
projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
|
|
15756
15756
|
scene: {
|
|
15757
15757
|
type: "string",
|
|
15758
15758
|
ops: ["start", "check"],
|
|
@@ -15781,11 +15781,17 @@ var init_tool_definitions = __esm(() => {
|
|
|
15781
15781
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
15782
15782
|
}
|
|
15783
15783
|
},
|
|
15784
|
-
requires: [
|
|
15784
|
+
requires: [],
|
|
15785
15785
|
operations: {
|
|
15786
|
-
start: {
|
|
15786
|
+
start: {
|
|
15787
|
+
summary: "run the project until it quits or is stopped",
|
|
15788
|
+
requires: ["projectPath"]
|
|
15789
|
+
},
|
|
15787
15790
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
15788
|
-
check: {
|
|
15791
|
+
check: {
|
|
15792
|
+
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
15793
|
+
requires: ["projectPath"]
|
|
15794
|
+
},
|
|
15789
15795
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
15790
15796
|
},
|
|
15791
15797
|
defaultOperation: "start"
|
|
@@ -31168,6 +31174,7 @@ class GodotDAPClient {
|
|
|
31168
31174
|
pendingRequests;
|
|
31169
31175
|
reader = new FrameReader;
|
|
31170
31176
|
outputBuffer = [];
|
|
31177
|
+
onOutput = null;
|
|
31171
31178
|
maxOutputLines = 1000;
|
|
31172
31179
|
initialized = false;
|
|
31173
31180
|
attached = false;
|
|
@@ -31335,6 +31342,13 @@ class GodotDAPClient {
|
|
|
31335
31342
|
if (outputText.length > 0) {
|
|
31336
31343
|
const lines = outputText.split(/\r?\n/).filter((line) => line.length > 0);
|
|
31337
31344
|
this.outputBuffer.push(...lines);
|
|
31345
|
+
if (this.onOutput !== null) {
|
|
31346
|
+
for (const line of lines) {
|
|
31347
|
+
try {
|
|
31348
|
+
this.onOutput(line);
|
|
31349
|
+
} catch {}
|
|
31350
|
+
}
|
|
31351
|
+
}
|
|
31338
31352
|
}
|
|
31339
31353
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
31340
31354
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
@@ -31391,6 +31405,9 @@ class GodotDAPClient {
|
|
|
31391
31405
|
await this.sendRequest("configurationDone", {});
|
|
31392
31406
|
this.attached = true;
|
|
31393
31407
|
}
|
|
31408
|
+
setOutputSink(sink) {
|
|
31409
|
+
this.onOutput = sink;
|
|
31410
|
+
}
|
|
31394
31411
|
getOutput(clear = false) {
|
|
31395
31412
|
const lines = [...this.outputBuffer];
|
|
31396
31413
|
if (clear) {
|
|
@@ -36435,6 +36452,34 @@ function runningAs(pid) {
|
|
|
36435
36452
|
return null;
|
|
36436
36453
|
}
|
|
36437
36454
|
}
|
|
36455
|
+
function listeningPid(port) {
|
|
36456
|
+
try {
|
|
36457
|
+
if (process.platform === "win32") {
|
|
36458
|
+
const said = execFileSync("powershell", [
|
|
36459
|
+
"-NoProfile",
|
|
36460
|
+
"-NonInteractive",
|
|
36461
|
+
"-Command",
|
|
36462
|
+
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
36463
|
+
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
36464
|
+
return /^\d+$/.test(said) ? Number(said) : null;
|
|
36465
|
+
}
|
|
36466
|
+
for (const [command, args, pattern] of [
|
|
36467
|
+
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
36468
|
+
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
36469
|
+
]) {
|
|
36470
|
+
try {
|
|
36471
|
+
const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
36472
|
+
const found = pattern.exec(said);
|
|
36473
|
+
if (found?.[1] !== undefined) {
|
|
36474
|
+
return Number(found[1]);
|
|
36475
|
+
}
|
|
36476
|
+
} catch {}
|
|
36477
|
+
}
|
|
36478
|
+
return null;
|
|
36479
|
+
} catch {
|
|
36480
|
+
return null;
|
|
36481
|
+
}
|
|
36482
|
+
}
|
|
36438
36483
|
function windowsCommandLine(pid) {
|
|
36439
36484
|
try {
|
|
36440
36485
|
const answer = execFileSync("powershell", [
|
|
@@ -36567,8 +36612,7 @@ import {
|
|
|
36567
36612
|
readdirSync as readdirSync7,
|
|
36568
36613
|
readFileSync as readFileSync15,
|
|
36569
36614
|
readSync,
|
|
36570
|
-
rmSync as rmSync9
|
|
36571
|
-
statSync as statSync4
|
|
36615
|
+
rmSync as rmSync9
|
|
36572
36616
|
} from "node:fs";
|
|
36573
36617
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36574
36618
|
import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
|
|
@@ -37122,9 +37166,10 @@ class GodotServer {
|
|
|
37122
37166
|
if (op !== null) {
|
|
37123
37167
|
const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
|
|
37124
37168
|
if (elsewhere.length > 0) {
|
|
37169
|
+
const takes = argumentsOf(spec, op);
|
|
37125
37170
|
return {
|
|
37126
37171
|
ok: false,
|
|
37127
|
-
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${
|
|
37172
|
+
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
|
|
37128
37173
|
};
|
|
37129
37174
|
}
|
|
37130
37175
|
}
|
|
@@ -37869,14 +37914,45 @@ class GodotServer {
|
|
|
37869
37914
|
async handleDAP(toolName, args) {
|
|
37870
37915
|
return handleDAPTool(this.dap(), toolName, args);
|
|
37871
37916
|
}
|
|
37917
|
+
adapterBelongsElsewhere() {
|
|
37918
|
+
const status = this.godotBridge.getStatus();
|
|
37919
|
+
const ours = status.editorPid;
|
|
37920
|
+
if (ours === undefined) {
|
|
37921
|
+
return null;
|
|
37922
|
+
}
|
|
37923
|
+
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
37924
|
+
const holder = listeningPid(port);
|
|
37925
|
+
if (holder === null || holder === ours) {
|
|
37926
|
+
return null;
|
|
37927
|
+
}
|
|
37928
|
+
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
37929
|
+
}
|
|
37872
37930
|
dap() {
|
|
37873
37931
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
37874
37932
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
37875
37933
|
this.dapClient = null;
|
|
37876
37934
|
}
|
|
37877
|
-
this.dapClient
|
|
37935
|
+
if (this.dapClient === null) {
|
|
37936
|
+
this.dapClient = new GodotDAPClient(port);
|
|
37937
|
+
this.dapClient.setOutputSink((line) => {
|
|
37938
|
+
this.writeToTranscript(line);
|
|
37939
|
+
});
|
|
37940
|
+
}
|
|
37878
37941
|
return this.dapClient;
|
|
37879
37942
|
}
|
|
37943
|
+
writeToTranscript(line) {
|
|
37944
|
+
const run = this.activeProcess;
|
|
37945
|
+
if (run?.throughEditor !== true || run.transcript === null) {
|
|
37946
|
+
return;
|
|
37947
|
+
}
|
|
37948
|
+
try {
|
|
37949
|
+
appendFileSync(run.transcript, line.endsWith(`
|
|
37950
|
+
`) ? line : `${line}
|
|
37951
|
+
`);
|
|
37952
|
+
} catch (error) {
|
|
37953
|
+
this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
|
|
37954
|
+
}
|
|
37955
|
+
}
|
|
37880
37956
|
editorServes(field, variable, fallback) {
|
|
37881
37957
|
const named = portFromEnvOrNull(variable);
|
|
37882
37958
|
if (named !== null) {
|
|
@@ -38198,6 +38274,14 @@ class GodotServer {
|
|
|
38198
38274
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
38199
38275
|
]);
|
|
38200
38276
|
}
|
|
38277
|
+
const stranger = this.adapterBelongsElsewhere();
|
|
38278
|
+
if (stranger !== null) {
|
|
38279
|
+
return this.createErrorResponse(stranger, [
|
|
38280
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
38281
|
+
"GDHARNESS_DAP_PORT points this server at the right one",
|
|
38282
|
+
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
38283
|
+
]);
|
|
38284
|
+
}
|
|
38201
38285
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
38202
38286
|
if (answer.isError === true) {
|
|
38203
38287
|
return answer;
|
|
@@ -38235,20 +38319,11 @@ class GodotServer {
|
|
|
38235
38319
|
if (!game.throughEditor || !this.dapClient) {
|
|
38236
38320
|
return;
|
|
38237
38321
|
}
|
|
38238
|
-
|
|
38239
|
-
|
|
38240
|
-
|
|
38322
|
+
if (game.transcript === null) {
|
|
38323
|
+
for (const line of this.dapClient.getOutput(true)) {
|
|
38324
|
+
game.log.append("stdout", line.endsWith(`
|
|
38241
38325
|
`) ? line : `${line}
|
|
38242
|
-
|
|
38243
|
-
game.log.append("stdout", ended);
|
|
38244
|
-
heard.push(ended);
|
|
38245
|
-
}
|
|
38246
|
-
if (heard.length > 0 && game.transcript !== null) {
|
|
38247
|
-
try {
|
|
38248
|
-
appendFileSync(game.transcript, heard.join(""));
|
|
38249
|
-
game.readOffset = statSync4(game.transcript).size;
|
|
38250
|
-
} catch (error) {
|
|
38251
|
-
this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
|
|
38326
|
+
`);
|
|
38252
38327
|
}
|
|
38253
38328
|
}
|
|
38254
38329
|
if (!this.dapClient.isConnected()) {
|
package/build/index.js
CHANGED
|
@@ -10152,8 +10152,7 @@ import {
|
|
|
10152
10152
|
readdirSync as readdirSync6,
|
|
10153
10153
|
readFileSync as readFileSync11,
|
|
10154
10154
|
readSync,
|
|
10155
|
-
rmSync as rmSync5
|
|
10156
|
-
statSync as statSync4
|
|
10155
|
+
rmSync as rmSync5
|
|
10157
10156
|
} from "node:fs";
|
|
10158
10157
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10159
10158
|
import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
|
|
@@ -25011,6 +25010,7 @@ class GodotDAPClient {
|
|
|
25011
25010
|
pendingRequests;
|
|
25012
25011
|
reader = new FrameReader;
|
|
25013
25012
|
outputBuffer = [];
|
|
25013
|
+
onOutput = null;
|
|
25014
25014
|
maxOutputLines = 1000;
|
|
25015
25015
|
initialized = false;
|
|
25016
25016
|
attached = false;
|
|
@@ -25178,6 +25178,13 @@ class GodotDAPClient {
|
|
|
25178
25178
|
if (outputText.length > 0) {
|
|
25179
25179
|
const lines = outputText.split(/\r?\n/).filter((line) => line.length > 0);
|
|
25180
25180
|
this.outputBuffer.push(...lines);
|
|
25181
|
+
if (this.onOutput !== null) {
|
|
25182
|
+
for (const line of lines) {
|
|
25183
|
+
try {
|
|
25184
|
+
this.onOutput(line);
|
|
25185
|
+
} catch {}
|
|
25186
|
+
}
|
|
25187
|
+
}
|
|
25181
25188
|
}
|
|
25182
25189
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
25183
25190
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
@@ -25234,6 +25241,9 @@ class GodotDAPClient {
|
|
|
25234
25241
|
await this.sendRequest("configurationDone", {});
|
|
25235
25242
|
this.attached = true;
|
|
25236
25243
|
}
|
|
25244
|
+
setOutputSink(sink) {
|
|
25245
|
+
this.onOutput = sink;
|
|
25246
|
+
}
|
|
25237
25247
|
getOutput(clear = false) {
|
|
25238
25248
|
const lines = [...this.outputBuffer];
|
|
25239
25249
|
if (clear) {
|
|
@@ -28089,6 +28099,34 @@ function runningAs(pid) {
|
|
|
28089
28099
|
return null;
|
|
28090
28100
|
}
|
|
28091
28101
|
}
|
|
28102
|
+
function listeningPid(port) {
|
|
28103
|
+
try {
|
|
28104
|
+
if (process.platform === "win32") {
|
|
28105
|
+
const said = execFileSync("powershell", [
|
|
28106
|
+
"-NoProfile",
|
|
28107
|
+
"-NonInteractive",
|
|
28108
|
+
"-Command",
|
|
28109
|
+
`(Get-NetTCPConnection -State Listen -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -First 1).OwningProcess`
|
|
28110
|
+
], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28111
|
+
return /^\d+$/.test(said) ? Number(said) : null;
|
|
28112
|
+
}
|
|
28113
|
+
for (const [command, args, pattern] of [
|
|
28114
|
+
["lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], /^(\d+)/],
|
|
28115
|
+
["ss", ["-ltnpH", `sport = :${port}`], /pid=(\d+)/]
|
|
28116
|
+
]) {
|
|
28117
|
+
try {
|
|
28118
|
+
const said = execFileSync(command, [...args], { encoding: "utf8", timeout: 15000 }).trim();
|
|
28119
|
+
const found = pattern.exec(said);
|
|
28120
|
+
if (found?.[1] !== undefined) {
|
|
28121
|
+
return Number(found[1]);
|
|
28122
|
+
}
|
|
28123
|
+
} catch {}
|
|
28124
|
+
}
|
|
28125
|
+
return null;
|
|
28126
|
+
} catch {
|
|
28127
|
+
return null;
|
|
28128
|
+
}
|
|
28129
|
+
}
|
|
28092
28130
|
function windowsCommandLine(pid) {
|
|
28093
28131
|
try {
|
|
28094
28132
|
const answer = execFileSync("powershell", [
|
|
@@ -28792,7 +28830,7 @@ var TOOL_SPECS = [
|
|
|
28792
28830
|
{
|
|
28793
28831
|
name: "editor_launch",
|
|
28794
28832
|
description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. A restart saves project.godot on the way out, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped: settingsDropped names any that went, because nothing else will say so until something depends on one.",
|
|
28795
|
-
parameters: { projectPath: PROJECT_PATH },
|
|
28833
|
+
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
28796
28834
|
requires: [],
|
|
28797
28835
|
operations: {
|
|
28798
28836
|
open: { summary: "open the editor on a project", requires: ["projectPath"] },
|
|
@@ -28804,7 +28842,7 @@ var TOOL_SPECS = [
|
|
|
28804
28842
|
name: "editor_run",
|
|
28805
28843
|
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
28806
28844
|
parameters: {
|
|
28807
|
-
projectPath: PROJECT_PATH,
|
|
28845
|
+
projectPath: { ...PROJECT_PATH, ops: ["start", "check"] },
|
|
28808
28846
|
scene: {
|
|
28809
28847
|
type: "string",
|
|
28810
28848
|
ops: ["start", "check"],
|
|
@@ -28833,11 +28871,17 @@ var TOOL_SPECS = [
|
|
|
28833
28871
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
28834
28872
|
}
|
|
28835
28873
|
},
|
|
28836
|
-
requires: [
|
|
28874
|
+
requires: [],
|
|
28837
28875
|
operations: {
|
|
28838
|
-
start: {
|
|
28876
|
+
start: {
|
|
28877
|
+
summary: "run the project until it quits or is stopped",
|
|
28878
|
+
requires: ["projectPath"]
|
|
28879
|
+
},
|
|
28839
28880
|
stop: { summary: "end the run and answer with what it printed last", requires: [] },
|
|
28840
|
-
check: {
|
|
28881
|
+
check: {
|
|
28882
|
+
summary: "boot headless, quit after a few frames, and report the verdict",
|
|
28883
|
+
requires: ["projectPath"]
|
|
28884
|
+
},
|
|
28841
28885
|
wait: { summary: "wait for the run to end, then answer as editor_output does", requires: [] }
|
|
28842
28886
|
},
|
|
28843
28887
|
defaultOperation: "start"
|
|
@@ -29876,9 +29920,10 @@ class GodotServer {
|
|
|
29876
29920
|
if (op !== null) {
|
|
29877
29921
|
const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
|
|
29878
29922
|
if (elsewhere.length > 0) {
|
|
29923
|
+
const takes = argumentsOf(spec, op);
|
|
29879
29924
|
return {
|
|
29880
29925
|
ok: false,
|
|
29881
|
-
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${
|
|
29926
|
+
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${takes.length === 0 ? `${op} takes no arguments.` : `${op} takes: ${takes.join(", ")}.`}`)
|
|
29882
29927
|
};
|
|
29883
29928
|
}
|
|
29884
29929
|
}
|
|
@@ -30623,14 +30668,45 @@ class GodotServer {
|
|
|
30623
30668
|
async handleDAP(toolName, args) {
|
|
30624
30669
|
return handleDAPTool(this.dap(), toolName, args);
|
|
30625
30670
|
}
|
|
30671
|
+
adapterBelongsElsewhere() {
|
|
30672
|
+
const status = this.godotBridge.getStatus();
|
|
30673
|
+
const ours = status.editorPid;
|
|
30674
|
+
if (ours === undefined) {
|
|
30675
|
+
return null;
|
|
30676
|
+
}
|
|
30677
|
+
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
30678
|
+
const holder = listeningPid(port);
|
|
30679
|
+
if (holder === null || holder === ours) {
|
|
30680
|
+
return null;
|
|
30681
|
+
}
|
|
30682
|
+
return `The debug adapter on port ${port} belongs to process ${holder}, and the editor on this server's bridge is process ${ours}. Godot gives every editor the same debug adapter port by default, so the one this editor names in its settings is not necessarily the one it holds. Playing the game now would put another editor's console and another project's debugger under this project's name.`;
|
|
30683
|
+
}
|
|
30626
30684
|
dap() {
|
|
30627
30685
|
const port = this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT);
|
|
30628
30686
|
if (this.dapClient !== null && this.dapClient.port !== port) {
|
|
30629
30687
|
this.dapClient = null;
|
|
30630
30688
|
}
|
|
30631
|
-
this.dapClient
|
|
30689
|
+
if (this.dapClient === null) {
|
|
30690
|
+
this.dapClient = new GodotDAPClient(port);
|
|
30691
|
+
this.dapClient.setOutputSink((line) => {
|
|
30692
|
+
this.writeToTranscript(line);
|
|
30693
|
+
});
|
|
30694
|
+
}
|
|
30632
30695
|
return this.dapClient;
|
|
30633
30696
|
}
|
|
30697
|
+
writeToTranscript(line) {
|
|
30698
|
+
const run = this.activeProcess;
|
|
30699
|
+
if (run?.throughEditor !== true || run.transcript === null) {
|
|
30700
|
+
return;
|
|
30701
|
+
}
|
|
30702
|
+
try {
|
|
30703
|
+
appendFileSync(run.transcript, line.endsWith(`
|
|
30704
|
+
`) ? line : `${line}
|
|
30705
|
+
`);
|
|
30706
|
+
} catch (error) {
|
|
30707
|
+
this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
|
|
30708
|
+
}
|
|
30709
|
+
}
|
|
30634
30710
|
editorServes(field, variable, fallback) {
|
|
30635
30711
|
const named = portFromEnvOrNull(variable);
|
|
30636
30712
|
if (named !== null) {
|
|
@@ -30952,6 +31028,14 @@ class GodotServer {
|
|
|
30952
31028
|
"GDHARNESS_DAP_PORT points this server at another one"
|
|
30953
31029
|
]);
|
|
30954
31030
|
}
|
|
31031
|
+
const stranger = this.adapterBelongsElsewhere();
|
|
31032
|
+
if (stranger !== null) {
|
|
31033
|
+
return this.createErrorResponse(stranger, [
|
|
31034
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
31035
|
+
"GDHARNESS_DAP_PORT points this server at the right one",
|
|
31036
|
+
"Closing the other editor, or moving its debug adapter port in its editor settings, frees this one"
|
|
31037
|
+
]);
|
|
31038
|
+
}
|
|
30955
31039
|
const answer = await this.handleViaBridge("play_scene", scene === null ? {} : { scenePath: `res://${scene}` });
|
|
30956
31040
|
if (answer.isError === true) {
|
|
30957
31041
|
return answer;
|
|
@@ -30989,20 +31073,11 @@ class GodotServer {
|
|
|
30989
31073
|
if (!game.throughEditor || !this.dapClient) {
|
|
30990
31074
|
return;
|
|
30991
31075
|
}
|
|
30992
|
-
|
|
30993
|
-
|
|
30994
|
-
|
|
31076
|
+
if (game.transcript === null) {
|
|
31077
|
+
for (const line of this.dapClient.getOutput(true)) {
|
|
31078
|
+
game.log.append("stdout", line.endsWith(`
|
|
30995
31079
|
`) ? line : `${line}
|
|
30996
|
-
|
|
30997
|
-
game.log.append("stdout", ended);
|
|
30998
|
-
heard.push(ended);
|
|
30999
|
-
}
|
|
31000
|
-
if (heard.length > 0 && game.transcript !== null) {
|
|
31001
|
-
try {
|
|
31002
|
-
appendFileSync(game.transcript, heard.join(""));
|
|
31003
|
-
game.readOffset = statSync4(game.transcript).size;
|
|
31004
|
-
} catch (error) {
|
|
31005
|
-
this.logDebug(`Could not write an editor-played run's transcript: ${errorMessage(error)}`);
|
|
31080
|
+
`);
|
|
31006
31081
|
}
|
|
31007
31082
|
}
|
|
31008
31083
|
if (!this.dapClient.isConnected()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.10",
|
|
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",
|