gdharness 0.13.6 → 0.13.8
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 +154 -13
- package/build/index.js +154 -13
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -14896,6 +14896,15 @@ function isValueComplete(value) {
|
|
|
14896
14896
|
}
|
|
14897
14897
|
return depth <= 0 && !inString;
|
|
14898
14898
|
}
|
|
14899
|
+
function settingKeys(content) {
|
|
14900
|
+
const named = new Set;
|
|
14901
|
+
for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
|
|
14902
|
+
for (const key of Object.keys(entries)) {
|
|
14903
|
+
named.add(`${section}/${key}`);
|
|
14904
|
+
}
|
|
14905
|
+
}
|
|
14906
|
+
return named;
|
|
14907
|
+
}
|
|
14899
14908
|
function parseProjectGodot(content) {
|
|
14900
14909
|
const result = emptyRecord();
|
|
14901
14910
|
let section = emptyRecord();
|
|
@@ -15730,7 +15739,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15730
15739
|
},
|
|
15731
15740
|
{
|
|
15732
15741
|
name: "editor_launch",
|
|
15733
|
-
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.",
|
|
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.",
|
|
15734
15743
|
parameters: { projectPath: PROJECT_PATH },
|
|
15735
15744
|
requires: [],
|
|
15736
15745
|
operations: {
|
|
@@ -26630,13 +26639,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
|
|
|
26630
26639
|
if (!malformedIPLiteral) {
|
|
26631
26640
|
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
|
|
26632
26641
|
}
|
|
26633
|
-
if (
|
|
26634
|
-
|
|
26635
|
-
|
|
26636
|
-
|
|
26637
|
-
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
26638
|
-
}
|
|
26642
|
+
if (uri.indexOf("%") !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
|
|
26643
|
+
let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
|
|
26644
|
+
if (!isIP) {
|
|
26645
|
+
host = normalizePercentEncoding(host.toLowerCase());
|
|
26639
26646
|
}
|
|
26647
|
+
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
26648
|
+
}
|
|
26649
|
+
if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
|
|
26640
26650
|
if (parsed.path) {
|
|
26641
26651
|
parsed.path = normalizePathEncoding(parsed.path);
|
|
26642
26652
|
}
|
|
@@ -36320,6 +36330,41 @@ function writeRunRecord(record) {
|
|
|
36320
36330
|
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36321
36331
|
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36322
36332
|
}
|
|
36333
|
+
function editorNotePath() {
|
|
36334
|
+
return join15(runsDirectory(), "editor-run.json");
|
|
36335
|
+
}
|
|
36336
|
+
function writeEditorRunNote(note) {
|
|
36337
|
+
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36338
|
+
writeFileSync8(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
36339
|
+
}
|
|
36340
|
+
function readEditorRunNote() {
|
|
36341
|
+
for (const directory of runtimeDirectories()) {
|
|
36342
|
+
const note = editorNoteAt(join15(directory, "runs", "editor-run.json"));
|
|
36343
|
+
if (note !== null) {
|
|
36344
|
+
return note;
|
|
36345
|
+
}
|
|
36346
|
+
}
|
|
36347
|
+
return null;
|
|
36348
|
+
}
|
|
36349
|
+
function editorNoteAt(path) {
|
|
36350
|
+
let parsed;
|
|
36351
|
+
try {
|
|
36352
|
+
parsed = JSON.parse(readFileSync13(path, "utf8"));
|
|
36353
|
+
} catch {
|
|
36354
|
+
return null;
|
|
36355
|
+
}
|
|
36356
|
+
if (!parsed || typeof parsed !== "object") {
|
|
36357
|
+
return null;
|
|
36358
|
+
}
|
|
36359
|
+
const fields = parsed;
|
|
36360
|
+
const projectPath = fields["projectPath"];
|
|
36361
|
+
const transcript = fields["transcript"];
|
|
36362
|
+
const startedAt = fields["startedAt"];
|
|
36363
|
+
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
36364
|
+
return null;
|
|
36365
|
+
}
|
|
36366
|
+
return { projectPath, transcript, startedAt };
|
|
36367
|
+
}
|
|
36323
36368
|
function recordRunEnded(pid, exitCode) {
|
|
36324
36369
|
const path = recordPath();
|
|
36325
36370
|
const record = recordAt(path);
|
|
@@ -36512,6 +36557,7 @@ var init_setup = __esm(() => {
|
|
|
36512
36557
|
import { execFile as execFile6, spawn } from "node:child_process";
|
|
36513
36558
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
36514
36559
|
import {
|
|
36560
|
+
appendFileSync,
|
|
36515
36561
|
closeSync,
|
|
36516
36562
|
existsSync as existsSync14,
|
|
36517
36563
|
fstatSync,
|
|
@@ -36521,7 +36567,8 @@ import {
|
|
|
36521
36567
|
readdirSync as readdirSync7,
|
|
36522
36568
|
readFileSync as readFileSync15,
|
|
36523
36569
|
readSync,
|
|
36524
|
-
rmSync as rmSync9
|
|
36570
|
+
rmSync as rmSync9,
|
|
36571
|
+
statSync as statSync4
|
|
36525
36572
|
} from "node:fs";
|
|
36526
36573
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36527
36574
|
import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
|
|
@@ -37887,6 +37934,7 @@ class GodotServer {
|
|
|
37887
37934
|
};
|
|
37888
37935
|
}));
|
|
37889
37936
|
const playing = await this.editorPlayingState();
|
|
37937
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
37890
37938
|
return this.jsonTextResponse({
|
|
37891
37939
|
editor: {
|
|
37892
37940
|
...this.getEditorStatusPayload(),
|
|
@@ -37904,6 +37952,16 @@ class GodotServer {
|
|
|
37904
37952
|
}
|
|
37905
37953
|
});
|
|
37906
37954
|
}
|
|
37955
|
+
settingKeysOf(projectPath) {
|
|
37956
|
+
if (projectPath === undefined) {
|
|
37957
|
+
return new Set;
|
|
37958
|
+
}
|
|
37959
|
+
try {
|
|
37960
|
+
return settingKeys(readFileSync15(join17(projectPath, "project.godot"), "utf8"));
|
|
37961
|
+
} catch {
|
|
37962
|
+
return new Set;
|
|
37963
|
+
}
|
|
37964
|
+
}
|
|
37907
37965
|
async handleRestartEditor() {
|
|
37908
37966
|
const before = this.godotBridge.getStatus();
|
|
37909
37967
|
if (!before.connected) {
|
|
@@ -37912,6 +37970,7 @@ class GodotServer {
|
|
|
37912
37970
|
"editor_status says whether the bridge is up and what has reached it"
|
|
37913
37971
|
]);
|
|
37914
37972
|
}
|
|
37973
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
37915
37974
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
37916
37975
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
37917
37976
|
if (asked.isError === true) {
|
|
@@ -37931,6 +37990,8 @@ class GodotServer {
|
|
|
37931
37990
|
]);
|
|
37932
37991
|
}
|
|
37933
37992
|
const now = this.godotBridge.getStatus();
|
|
37993
|
+
const after = this.settingKeysOf(before.projectPath);
|
|
37994
|
+
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
37934
37995
|
return this.jsonTextResponse({
|
|
37935
37996
|
restarted: true,
|
|
37936
37997
|
editorPid: now.editorPid,
|
|
@@ -37938,6 +37999,8 @@ class GodotServer {
|
|
|
37938
37999
|
serverVersion: SERVER_VERSION,
|
|
37939
38000
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
37940
38001
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
38002
|
+
settingsDropped: dropped.length > 0 ? dropped : undefined,
|
|
38003
|
+
settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
|
|
37941
38004
|
tookMs: Date.now() - began
|
|
37942
38005
|
});
|
|
37943
38006
|
}
|
|
@@ -38128,6 +38191,7 @@ class GodotServer {
|
|
|
38128
38191
|
const log = new GameLog;
|
|
38129
38192
|
try {
|
|
38130
38193
|
await this.dap().connect();
|
|
38194
|
+
this.dapClient?.getOutput(true);
|
|
38131
38195
|
} catch (error) {
|
|
38132
38196
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
38133
38197
|
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
@@ -38138,19 +38202,24 @@ class GodotServer {
|
|
|
38138
38202
|
if (answer.isError === true) {
|
|
38139
38203
|
return answer;
|
|
38140
38204
|
}
|
|
38205
|
+
const startedAt = Date.now();
|
|
38206
|
+
const transcript = openTranscript(startedAt);
|
|
38207
|
+
closeSync(transcript.fd);
|
|
38141
38208
|
const played = {
|
|
38142
38209
|
process: null,
|
|
38143
38210
|
pid: null,
|
|
38144
38211
|
log,
|
|
38145
|
-
transcript:
|
|
38212
|
+
transcript: transcript.path,
|
|
38146
38213
|
readOffset: 0,
|
|
38147
38214
|
projectPath,
|
|
38148
|
-
startedAt
|
|
38215
|
+
startedAt,
|
|
38149
38216
|
exitCode: null,
|
|
38150
38217
|
throughEditor: true,
|
|
38151
38218
|
brokeOn: null
|
|
38152
38219
|
};
|
|
38153
38220
|
this.activeProcess = played;
|
|
38221
|
+
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
38222
|
+
sweepTranscripts();
|
|
38154
38223
|
return this.jsonTextResponse({
|
|
38155
38224
|
started: true,
|
|
38156
38225
|
through: "editor",
|
|
@@ -38166,10 +38235,24 @@ class GodotServer {
|
|
|
38166
38235
|
if (!game.throughEditor || !this.dapClient) {
|
|
38167
38236
|
return;
|
|
38168
38237
|
}
|
|
38238
|
+
const heard = [];
|
|
38169
38239
|
for (const line of this.dapClient.getOutput(true)) {
|
|
38170
|
-
|
|
38240
|
+
const ended = line.endsWith(`
|
|
38171
38241
|
`) ? line : `${line}
|
|
38172
|
-
|
|
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)}`);
|
|
38252
|
+
}
|
|
38253
|
+
}
|
|
38254
|
+
if (!this.dapClient.isConnected()) {
|
|
38255
|
+
game.consoleLost = true;
|
|
38173
38256
|
}
|
|
38174
38257
|
this.recordWhatItBrokeOn(game);
|
|
38175
38258
|
}
|
|
@@ -38322,6 +38405,50 @@ class GodotServer {
|
|
|
38322
38405
|
closeSync(handle);
|
|
38323
38406
|
}
|
|
38324
38407
|
}
|
|
38408
|
+
async pickUpWhatTheEditorIsPlaying() {
|
|
38409
|
+
if (!this.godotBridge.isConnected()) {
|
|
38410
|
+
return;
|
|
38411
|
+
}
|
|
38412
|
+
const going = this.activeProcess;
|
|
38413
|
+
if (going !== null) {
|
|
38414
|
+
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
38415
|
+
try {
|
|
38416
|
+
await this.dap().connect();
|
|
38417
|
+
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
38418
|
+
`);
|
|
38419
|
+
} catch (error) {
|
|
38420
|
+
this.logDebug(`Could not reconnect the adapter for a playing run: ${errorMessage(error)}`);
|
|
38421
|
+
}
|
|
38422
|
+
}
|
|
38423
|
+
return;
|
|
38424
|
+
}
|
|
38425
|
+
const playing = await this.editorPlayingState();
|
|
38426
|
+
if (playing?.playing !== true) {
|
|
38427
|
+
return;
|
|
38428
|
+
}
|
|
38429
|
+
try {
|
|
38430
|
+
await this.dap().connect();
|
|
38431
|
+
} catch (error) {
|
|
38432
|
+
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
38433
|
+
}
|
|
38434
|
+
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
38435
|
+
const left = readEditorRunNote();
|
|
38436
|
+
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
38437
|
+
const picked = {
|
|
38438
|
+
process: null,
|
|
38439
|
+
pid: null,
|
|
38440
|
+
log: new GameLog,
|
|
38441
|
+
transcript: note?.transcript ?? null,
|
|
38442
|
+
readOffset: 0,
|
|
38443
|
+
projectPath: project,
|
|
38444
|
+
startedAt: note?.startedAt ?? Date.now(),
|
|
38445
|
+
exitCode: null,
|
|
38446
|
+
throughEditor: true,
|
|
38447
|
+
brokeOn: null,
|
|
38448
|
+
pickedUpPlaying: true
|
|
38449
|
+
};
|
|
38450
|
+
this.activeProcess = picked;
|
|
38451
|
+
}
|
|
38325
38452
|
adoptRecordedRun() {
|
|
38326
38453
|
const record = readRunRecord();
|
|
38327
38454
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -38403,6 +38530,7 @@ class GodotServer {
|
|
|
38403
38530
|
});
|
|
38404
38531
|
}
|
|
38405
38532
|
async handleWaitForRun(args) {
|
|
38533
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38406
38534
|
const run = this.currentRun();
|
|
38407
38535
|
if (!run) {
|
|
38408
38536
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -38417,6 +38545,7 @@ class GodotServer {
|
|
|
38417
38545
|
return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
|
|
38418
38546
|
}
|
|
38419
38547
|
async handleGetDebugOutput(args, waitedMs) {
|
|
38548
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38420
38549
|
const run = this.currentRun();
|
|
38421
38550
|
if (!run) {
|
|
38422
38551
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -38438,12 +38567,22 @@ class GodotServer {
|
|
|
38438
38567
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
38439
38568
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
38440
38569
|
const notes = [];
|
|
38570
|
+
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
38571
|
+
notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
|
|
38572
|
+
}
|
|
38441
38573
|
if (waitedMs !== undefined) {
|
|
38442
38574
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
38443
38575
|
}
|
|
38444
38576
|
if (run.endedUnwatched === true) {
|
|
38445
38577
|
notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
|
|
38446
38578
|
}
|
|
38579
|
+
if (run.pickedUpPlaying === true) {
|
|
38580
|
+
notes.push(run.transcript === null ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
38581
|
+
}
|
|
38582
|
+
if (run.consoleLost === true) {
|
|
38583
|
+
const back = this.dapClient?.isConnected() === true;
|
|
38584
|
+
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
38585
|
+
}
|
|
38447
38586
|
if (typeof run.endedHere === "string") {
|
|
38448
38587
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
38449
38588
|
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
@@ -38459,7 +38598,8 @@ class GodotServer {
|
|
|
38459
38598
|
pid: run.pid,
|
|
38460
38599
|
errors: run.log.count("error"),
|
|
38461
38600
|
warnings: run.log.count("warning"),
|
|
38462
|
-
clean: run.log.count("error") === 0,
|
|
38601
|
+
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
38602
|
+
consoleLost: run.consoleLost === true ? true : undefined,
|
|
38463
38603
|
heldAt: halt,
|
|
38464
38604
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
38465
38605
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
@@ -38474,6 +38614,7 @@ class GodotServer {
|
|
|
38474
38614
|
});
|
|
38475
38615
|
}
|
|
38476
38616
|
async handleStopProject() {
|
|
38617
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
38477
38618
|
const stopped = this.currentRun();
|
|
38478
38619
|
if (!stopped) {
|
|
38479
38620
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
package/build/index.js
CHANGED
|
@@ -4060,13 +4060,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
|
|
|
4060
4060
|
if (!malformedIPLiteral) {
|
|
4061
4061
|
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
|
|
4062
4062
|
}
|
|
4063
|
-
if (
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
4068
|
-
}
|
|
4063
|
+
if (uri.indexOf("%") !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
|
|
4064
|
+
let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
|
|
4065
|
+
if (!isIP) {
|
|
4066
|
+
host = normalizePercentEncoding(host.toLowerCase());
|
|
4069
4067
|
}
|
|
4068
|
+
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
4069
|
+
}
|
|
4070
|
+
if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
|
|
4070
4071
|
if (parsed.path) {
|
|
4071
4072
|
parsed.path = normalizePathEncoding(parsed.path);
|
|
4072
4073
|
}
|
|
@@ -10141,6 +10142,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10141
10142
|
import { execFile as execFile4, spawn } from "node:child_process";
|
|
10142
10143
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10143
10144
|
import {
|
|
10145
|
+
appendFileSync,
|
|
10144
10146
|
closeSync,
|
|
10145
10147
|
existsSync as existsSync9,
|
|
10146
10148
|
fstatSync,
|
|
@@ -10150,7 +10152,8 @@ import {
|
|
|
10150
10152
|
readdirSync as readdirSync6,
|
|
10151
10153
|
readFileSync as readFileSync11,
|
|
10152
10154
|
readSync,
|
|
10153
|
-
rmSync as rmSync5
|
|
10155
|
+
rmSync as rmSync5,
|
|
10156
|
+
statSync as statSync4
|
|
10154
10157
|
} from "node:fs";
|
|
10155
10158
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10156
10159
|
import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
|
|
@@ -27544,6 +27547,15 @@ function isValueComplete(value) {
|
|
|
27544
27547
|
}
|
|
27545
27548
|
return depth <= 0 && !inString;
|
|
27546
27549
|
}
|
|
27550
|
+
function settingKeys(content) {
|
|
27551
|
+
const named = new Set;
|
|
27552
|
+
for (const [section, entries] of Object.entries(parseProjectGodot(content))) {
|
|
27553
|
+
for (const key of Object.keys(entries)) {
|
|
27554
|
+
named.add(`${section}/${key}`);
|
|
27555
|
+
}
|
|
27556
|
+
}
|
|
27557
|
+
return named;
|
|
27558
|
+
}
|
|
27547
27559
|
function parseProjectGodot(content) {
|
|
27548
27560
|
const result = emptyRecord();
|
|
27549
27561
|
let section = emptyRecord();
|
|
@@ -27972,6 +27984,41 @@ function writeRunRecord(record) {
|
|
|
27972
27984
|
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27973
27985
|
writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
27974
27986
|
}
|
|
27987
|
+
function editorNotePath() {
|
|
27988
|
+
return join9(runsDirectory(), "editor-run.json");
|
|
27989
|
+
}
|
|
27990
|
+
function writeEditorRunNote(note) {
|
|
27991
|
+
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27992
|
+
writeFileSync4(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
27993
|
+
}
|
|
27994
|
+
function readEditorRunNote() {
|
|
27995
|
+
for (const directory of runtimeDirectories()) {
|
|
27996
|
+
const note = editorNoteAt(join9(directory, "runs", "editor-run.json"));
|
|
27997
|
+
if (note !== null) {
|
|
27998
|
+
return note;
|
|
27999
|
+
}
|
|
28000
|
+
}
|
|
28001
|
+
return null;
|
|
28002
|
+
}
|
|
28003
|
+
function editorNoteAt(path) {
|
|
28004
|
+
let parsed;
|
|
28005
|
+
try {
|
|
28006
|
+
parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
28007
|
+
} catch {
|
|
28008
|
+
return null;
|
|
28009
|
+
}
|
|
28010
|
+
if (!parsed || typeof parsed !== "object") {
|
|
28011
|
+
return null;
|
|
28012
|
+
}
|
|
28013
|
+
const fields = parsed;
|
|
28014
|
+
const projectPath = fields["projectPath"];
|
|
28015
|
+
const transcript = fields["transcript"];
|
|
28016
|
+
const startedAt = fields["startedAt"];
|
|
28017
|
+
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
28018
|
+
return null;
|
|
28019
|
+
}
|
|
28020
|
+
return { projectPath, transcript, startedAt };
|
|
28021
|
+
}
|
|
27975
28022
|
function recordRunEnded(pid, exitCode) {
|
|
27976
28023
|
const path = recordPath();
|
|
27977
28024
|
const record = recordAt(path);
|
|
@@ -28744,7 +28791,7 @@ var TOOL_SPECS = [
|
|
|
28744
28791
|
},
|
|
28745
28792
|
{
|
|
28746
28793
|
name: "editor_launch",
|
|
28747
|
-
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.",
|
|
28794
|
+
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.",
|
|
28748
28795
|
parameters: { projectPath: PROJECT_PATH },
|
|
28749
28796
|
requires: [],
|
|
28750
28797
|
operations: {
|
|
@@ -30641,6 +30688,7 @@ class GodotServer {
|
|
|
30641
30688
|
};
|
|
30642
30689
|
}));
|
|
30643
30690
|
const playing = await this.editorPlayingState();
|
|
30691
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
30644
30692
|
return this.jsonTextResponse({
|
|
30645
30693
|
editor: {
|
|
30646
30694
|
...this.getEditorStatusPayload(),
|
|
@@ -30658,6 +30706,16 @@ class GodotServer {
|
|
|
30658
30706
|
}
|
|
30659
30707
|
});
|
|
30660
30708
|
}
|
|
30709
|
+
settingKeysOf(projectPath) {
|
|
30710
|
+
if (projectPath === undefined) {
|
|
30711
|
+
return new Set;
|
|
30712
|
+
}
|
|
30713
|
+
try {
|
|
30714
|
+
return settingKeys(readFileSync11(join11(projectPath, "project.godot"), "utf8"));
|
|
30715
|
+
} catch {
|
|
30716
|
+
return new Set;
|
|
30717
|
+
}
|
|
30718
|
+
}
|
|
30661
30719
|
async handleRestartEditor() {
|
|
30662
30720
|
const before = this.godotBridge.getStatus();
|
|
30663
30721
|
if (!before.connected) {
|
|
@@ -30666,6 +30724,7 @@ class GodotServer {
|
|
|
30666
30724
|
"editor_status says whether the bridge is up and what has reached it"
|
|
30667
30725
|
]);
|
|
30668
30726
|
}
|
|
30727
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
30669
30728
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
30670
30729
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
30671
30730
|
if (asked.isError === true) {
|
|
@@ -30685,6 +30744,8 @@ class GodotServer {
|
|
|
30685
30744
|
]);
|
|
30686
30745
|
}
|
|
30687
30746
|
const now = this.godotBridge.getStatus();
|
|
30747
|
+
const after = this.settingKeysOf(before.projectPath);
|
|
30748
|
+
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
30688
30749
|
return this.jsonTextResponse({
|
|
30689
30750
|
restarted: true,
|
|
30690
30751
|
editorPid: now.editorPid,
|
|
@@ -30692,6 +30753,8 @@ class GodotServer {
|
|
|
30692
30753
|
serverVersion: SERVER_VERSION,
|
|
30693
30754
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
30694
30755
|
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
30756
|
+
settingsDropped: dropped.length > 0 ? dropped : undefined,
|
|
30757
|
+
settingsNote: dropped.length > 0 ? 'The editor saved project.godot on its way out and these keys are no longer in it. Godot writes only what differs from its own defaults, so a key named deliberately at its default value is redundant to the editor and is dropped on save. If any of them were pinned on purpose, to keep "set to this on purpose" and "not set" apart, put them back: nothing else will say they have gone until something depends on one.' : undefined,
|
|
30695
30758
|
tookMs: Date.now() - began
|
|
30696
30759
|
});
|
|
30697
30760
|
}
|
|
@@ -30882,6 +30945,7 @@ class GodotServer {
|
|
|
30882
30945
|
const log = new GameLog;
|
|
30883
30946
|
try {
|
|
30884
30947
|
await this.dap().connect();
|
|
30948
|
+
this.dapClient?.getOutput(true);
|
|
30885
30949
|
} catch (error) {
|
|
30886
30950
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
30887
30951
|
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
@@ -30892,19 +30956,24 @@ class GodotServer {
|
|
|
30892
30956
|
if (answer.isError === true) {
|
|
30893
30957
|
return answer;
|
|
30894
30958
|
}
|
|
30959
|
+
const startedAt = Date.now();
|
|
30960
|
+
const transcript = openTranscript(startedAt);
|
|
30961
|
+
closeSync(transcript.fd);
|
|
30895
30962
|
const played = {
|
|
30896
30963
|
process: null,
|
|
30897
30964
|
pid: null,
|
|
30898
30965
|
log,
|
|
30899
|
-
transcript:
|
|
30966
|
+
transcript: transcript.path,
|
|
30900
30967
|
readOffset: 0,
|
|
30901
30968
|
projectPath,
|
|
30902
|
-
startedAt
|
|
30969
|
+
startedAt,
|
|
30903
30970
|
exitCode: null,
|
|
30904
30971
|
throughEditor: true,
|
|
30905
30972
|
brokeOn: null
|
|
30906
30973
|
};
|
|
30907
30974
|
this.activeProcess = played;
|
|
30975
|
+
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
30976
|
+
sweepTranscripts();
|
|
30908
30977
|
return this.jsonTextResponse({
|
|
30909
30978
|
started: true,
|
|
30910
30979
|
through: "editor",
|
|
@@ -30920,10 +30989,24 @@ class GodotServer {
|
|
|
30920
30989
|
if (!game.throughEditor || !this.dapClient) {
|
|
30921
30990
|
return;
|
|
30922
30991
|
}
|
|
30992
|
+
const heard = [];
|
|
30923
30993
|
for (const line of this.dapClient.getOutput(true)) {
|
|
30924
|
-
|
|
30994
|
+
const ended = line.endsWith(`
|
|
30925
30995
|
`) ? line : `${line}
|
|
30926
|
-
|
|
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)}`);
|
|
31006
|
+
}
|
|
31007
|
+
}
|
|
31008
|
+
if (!this.dapClient.isConnected()) {
|
|
31009
|
+
game.consoleLost = true;
|
|
30927
31010
|
}
|
|
30928
31011
|
this.recordWhatItBrokeOn(game);
|
|
30929
31012
|
}
|
|
@@ -31076,6 +31159,50 @@ class GodotServer {
|
|
|
31076
31159
|
closeSync(handle);
|
|
31077
31160
|
}
|
|
31078
31161
|
}
|
|
31162
|
+
async pickUpWhatTheEditorIsPlaying() {
|
|
31163
|
+
if (!this.godotBridge.isConnected()) {
|
|
31164
|
+
return;
|
|
31165
|
+
}
|
|
31166
|
+
const going = this.activeProcess;
|
|
31167
|
+
if (going !== null) {
|
|
31168
|
+
if (going.throughEditor && going.consoleLost === true && (await this.editorPlayingState())?.playing) {
|
|
31169
|
+
try {
|
|
31170
|
+
await this.dap().connect();
|
|
31171
|
+
going.log.append("stdout", `[gdharness] the debug adapter was reconnected here
|
|
31172
|
+
`);
|
|
31173
|
+
} catch (error) {
|
|
31174
|
+
this.logDebug(`Could not reconnect the adapter for a playing run: ${errorMessage(error)}`);
|
|
31175
|
+
}
|
|
31176
|
+
}
|
|
31177
|
+
return;
|
|
31178
|
+
}
|
|
31179
|
+
const playing = await this.editorPlayingState();
|
|
31180
|
+
if (playing?.playing !== true) {
|
|
31181
|
+
return;
|
|
31182
|
+
}
|
|
31183
|
+
try {
|
|
31184
|
+
await this.dap().connect();
|
|
31185
|
+
} catch (error) {
|
|
31186
|
+
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
31187
|
+
}
|
|
31188
|
+
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
31189
|
+
const left = readEditorRunNote();
|
|
31190
|
+
const note = left !== null && project !== null && isSameDirectory(left.projectPath, project) ? left : null;
|
|
31191
|
+
const picked = {
|
|
31192
|
+
process: null,
|
|
31193
|
+
pid: null,
|
|
31194
|
+
log: new GameLog,
|
|
31195
|
+
transcript: note?.transcript ?? null,
|
|
31196
|
+
readOffset: 0,
|
|
31197
|
+
projectPath: project,
|
|
31198
|
+
startedAt: note?.startedAt ?? Date.now(),
|
|
31199
|
+
exitCode: null,
|
|
31200
|
+
throughEditor: true,
|
|
31201
|
+
brokeOn: null,
|
|
31202
|
+
pickedUpPlaying: true
|
|
31203
|
+
};
|
|
31204
|
+
this.activeProcess = picked;
|
|
31205
|
+
}
|
|
31079
31206
|
adoptRecordedRun() {
|
|
31080
31207
|
const record = readRunRecord();
|
|
31081
31208
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -31157,6 +31284,7 @@ class GodotServer {
|
|
|
31157
31284
|
});
|
|
31158
31285
|
}
|
|
31159
31286
|
async handleWaitForRun(args) {
|
|
31287
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31160
31288
|
const run = this.currentRun();
|
|
31161
31289
|
if (!run) {
|
|
31162
31290
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -31171,6 +31299,7 @@ class GodotServer {
|
|
|
31171
31299
|
return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
|
|
31172
31300
|
}
|
|
31173
31301
|
async handleGetDebugOutput(args, waitedMs) {
|
|
31302
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31174
31303
|
const run = this.currentRun();
|
|
31175
31304
|
if (!run) {
|
|
31176
31305
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -31192,12 +31321,22 @@ class GodotServer {
|
|
|
31192
31321
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
31193
31322
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
31194
31323
|
const notes = [];
|
|
31324
|
+
if (wanted && cpuSeconds === undefined && stillRunning(run)) {
|
|
31325
|
+
notes.push(run.pid === null ? "cpu was asked for and there is no process to ask: the editor is playing this run and holds it, so nothing here has its process id. editor_run start without a connected editor, or with args, starts the game here instead and that run answers cpuSeconds." : "cpu was asked for and this platform would not say what the run has used.");
|
|
31326
|
+
}
|
|
31195
31327
|
if (waitedMs !== undefined) {
|
|
31196
31328
|
notes.push(`The wait of ${waitedMs}ms ran out and the run is still going, so this is what it had printed by then rather than everything it will print. editor_run wait again to keep waiting.`);
|
|
31197
31329
|
}
|
|
31198
31330
|
if (run.endedUnwatched === true) {
|
|
31199
31331
|
notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
|
|
31200
31332
|
}
|
|
31333
|
+
if (run.pickedUpPlaying === true) {
|
|
31334
|
+
notes.push(run.transcript === null ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
31335
|
+
}
|
|
31336
|
+
if (run.consoleLost === true) {
|
|
31337
|
+
const back = this.dapClient?.isConnected() === true;
|
|
31338
|
+
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
31339
|
+
}
|
|
31201
31340
|
if (typeof run.endedHere === "string") {
|
|
31202
31341
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
31203
31342
|
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
@@ -31213,7 +31352,8 @@ class GodotServer {
|
|
|
31213
31352
|
pid: run.pid,
|
|
31214
31353
|
errors: run.log.count("error"),
|
|
31215
31354
|
warnings: run.log.count("warning"),
|
|
31216
|
-
clean: run.log.count("error") === 0,
|
|
31355
|
+
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
31356
|
+
consoleLost: run.consoleLost === true ? true : undefined,
|
|
31217
31357
|
heldAt: halt,
|
|
31218
31358
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
31219
31359
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
@@ -31228,6 +31368,7 @@ class GodotServer {
|
|
|
31228
31368
|
});
|
|
31229
31369
|
}
|
|
31230
31370
|
async handleStopProject() {
|
|
31371
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31231
31372
|
const stopped = this.currentRun();
|
|
31232
31373
|
if (!stopped) {
|
|
31233
31374
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.8",
|
|
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",
|