gdharness 0.13.7 → 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 +119 -13
- package/build/index.js +119 -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: {
|
|
@@ -36321,6 +36330,41 @@ function writeRunRecord(record) {
|
|
|
36321
36330
|
mkdirSync6(runsDirectory(), { recursive: true });
|
|
36322
36331
|
writeFileSync8(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
36323
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
|
+
}
|
|
36324
36368
|
function recordRunEnded(pid, exitCode) {
|
|
36325
36369
|
const path = recordPath();
|
|
36326
36370
|
const record = recordAt(path);
|
|
@@ -36513,6 +36557,7 @@ var init_setup = __esm(() => {
|
|
|
36513
36557
|
import { execFile as execFile6, spawn } from "node:child_process";
|
|
36514
36558
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
36515
36559
|
import {
|
|
36560
|
+
appendFileSync,
|
|
36516
36561
|
closeSync,
|
|
36517
36562
|
existsSync as existsSync14,
|
|
36518
36563
|
fstatSync,
|
|
@@ -36522,7 +36567,8 @@ import {
|
|
|
36522
36567
|
readdirSync as readdirSync7,
|
|
36523
36568
|
readFileSync as readFileSync15,
|
|
36524
36569
|
readSync,
|
|
36525
|
-
rmSync as rmSync9
|
|
36570
|
+
rmSync as rmSync9,
|
|
36571
|
+
statSync as statSync4
|
|
36526
36572
|
} from "node:fs";
|
|
36527
36573
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36528
36574
|
import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
|
|
@@ -37906,6 +37952,16 @@ class GodotServer {
|
|
|
37906
37952
|
}
|
|
37907
37953
|
});
|
|
37908
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
|
+
}
|
|
37909
37965
|
async handleRestartEditor() {
|
|
37910
37966
|
const before = this.godotBridge.getStatus();
|
|
37911
37967
|
if (!before.connected) {
|
|
@@ -37914,6 +37970,7 @@ class GodotServer {
|
|
|
37914
37970
|
"editor_status says whether the bridge is up and what has reached it"
|
|
37915
37971
|
]);
|
|
37916
37972
|
}
|
|
37973
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
37917
37974
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
37918
37975
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
37919
37976
|
if (asked.isError === true) {
|
|
@@ -37933,6 +37990,8 @@ class GodotServer {
|
|
|
37933
37990
|
]);
|
|
37934
37991
|
}
|
|
37935
37992
|
const now = this.godotBridge.getStatus();
|
|
37993
|
+
const after = this.settingKeysOf(before.projectPath);
|
|
37994
|
+
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
37936
37995
|
return this.jsonTextResponse({
|
|
37937
37996
|
restarted: true,
|
|
37938
37997
|
editorPid: now.editorPid,
|
|
@@ -37940,6 +37999,8 @@ class GodotServer {
|
|
|
37940
37999
|
serverVersion: SERVER_VERSION,
|
|
37941
38000
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
37942
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,
|
|
37943
38004
|
tookMs: Date.now() - began
|
|
37944
38005
|
});
|
|
37945
38006
|
}
|
|
@@ -38130,6 +38191,7 @@ class GodotServer {
|
|
|
38130
38191
|
const log = new GameLog;
|
|
38131
38192
|
try {
|
|
38132
38193
|
await this.dap().connect();
|
|
38194
|
+
this.dapClient?.getOutput(true);
|
|
38133
38195
|
} catch (error) {
|
|
38134
38196
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
38135
38197
|
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
@@ -38140,19 +38202,24 @@ class GodotServer {
|
|
|
38140
38202
|
if (answer.isError === true) {
|
|
38141
38203
|
return answer;
|
|
38142
38204
|
}
|
|
38205
|
+
const startedAt = Date.now();
|
|
38206
|
+
const transcript = openTranscript(startedAt);
|
|
38207
|
+
closeSync(transcript.fd);
|
|
38143
38208
|
const played = {
|
|
38144
38209
|
process: null,
|
|
38145
38210
|
pid: null,
|
|
38146
38211
|
log,
|
|
38147
|
-
transcript:
|
|
38212
|
+
transcript: transcript.path,
|
|
38148
38213
|
readOffset: 0,
|
|
38149
38214
|
projectPath,
|
|
38150
|
-
startedAt
|
|
38215
|
+
startedAt,
|
|
38151
38216
|
exitCode: null,
|
|
38152
38217
|
throughEditor: true,
|
|
38153
38218
|
brokeOn: null
|
|
38154
38219
|
};
|
|
38155
38220
|
this.activeProcess = played;
|
|
38221
|
+
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
38222
|
+
sweepTranscripts();
|
|
38156
38223
|
return this.jsonTextResponse({
|
|
38157
38224
|
started: true,
|
|
38158
38225
|
through: "editor",
|
|
@@ -38168,10 +38235,24 @@ class GodotServer {
|
|
|
38168
38235
|
if (!game.throughEditor || !this.dapClient) {
|
|
38169
38236
|
return;
|
|
38170
38237
|
}
|
|
38238
|
+
const heard = [];
|
|
38171
38239
|
for (const line of this.dapClient.getOutput(true)) {
|
|
38172
|
-
|
|
38240
|
+
const ended = line.endsWith(`
|
|
38173
38241
|
`) ? line : `${line}
|
|
38174
|
-
|
|
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;
|
|
38175
38256
|
}
|
|
38176
38257
|
this.recordWhatItBrokeOn(game);
|
|
38177
38258
|
}
|
|
@@ -38325,7 +38406,20 @@ class GodotServer {
|
|
|
38325
38406
|
}
|
|
38326
38407
|
}
|
|
38327
38408
|
async pickUpWhatTheEditorIsPlaying() {
|
|
38328
|
-
if (
|
|
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
|
+
}
|
|
38329
38423
|
return;
|
|
38330
38424
|
}
|
|
38331
38425
|
const playing = await this.editorPlayingState();
|
|
@@ -38337,19 +38431,23 @@ class GodotServer {
|
|
|
38337
38431
|
} catch (error) {
|
|
38338
38432
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
38339
38433
|
}
|
|
38340
|
-
this.
|
|
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 = {
|
|
38341
38438
|
process: null,
|
|
38342
38439
|
pid: null,
|
|
38343
38440
|
log: new GameLog,
|
|
38344
|
-
transcript: null,
|
|
38441
|
+
transcript: note?.transcript ?? null,
|
|
38345
38442
|
readOffset: 0,
|
|
38346
|
-
projectPath:
|
|
38347
|
-
startedAt: Date.now(),
|
|
38443
|
+
projectPath: project,
|
|
38444
|
+
startedAt: note?.startedAt ?? Date.now(),
|
|
38348
38445
|
exitCode: null,
|
|
38349
38446
|
throughEditor: true,
|
|
38350
38447
|
brokeOn: null,
|
|
38351
38448
|
pickedUpPlaying: true
|
|
38352
38449
|
};
|
|
38450
|
+
this.activeProcess = picked;
|
|
38353
38451
|
}
|
|
38354
38452
|
adoptRecordedRun() {
|
|
38355
38453
|
const record = readRunRecord();
|
|
@@ -38469,6 +38567,9 @@ class GodotServer {
|
|
|
38469
38567
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
38470
38568
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
38471
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
|
+
}
|
|
38472
38573
|
if (waitedMs !== undefined) {
|
|
38473
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.`);
|
|
38474
38575
|
}
|
|
@@ -38476,7 +38577,11 @@ class GodotServer {
|
|
|
38476
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.");
|
|
38477
38578
|
}
|
|
38478
38579
|
if (run.pickedUpPlaying === true) {
|
|
38479
|
-
notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that
|
|
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.`);
|
|
38480
38585
|
}
|
|
38481
38586
|
if (typeof run.endedHere === "string") {
|
|
38482
38587
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
@@ -38493,7 +38598,8 @@ class GodotServer {
|
|
|
38493
38598
|
pid: run.pid,
|
|
38494
38599
|
errors: run.log.count("error"),
|
|
38495
38600
|
warnings: run.log.count("warning"),
|
|
38496
|
-
clean: run.log.count("error") === 0,
|
|
38601
|
+
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
38602
|
+
consoleLost: run.consoleLost === true ? true : undefined,
|
|
38497
38603
|
heldAt: halt,
|
|
38498
38604
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
38499
38605
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
package/build/index.js
CHANGED
|
@@ -10142,6 +10142,7 @@ function defectReport(where, error, godotVersion) {
|
|
|
10142
10142
|
import { execFile as execFile4, spawn } from "node:child_process";
|
|
10143
10143
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10144
10144
|
import {
|
|
10145
|
+
appendFileSync,
|
|
10145
10146
|
closeSync,
|
|
10146
10147
|
existsSync as existsSync9,
|
|
10147
10148
|
fstatSync,
|
|
@@ -10151,7 +10152,8 @@ import {
|
|
|
10151
10152
|
readdirSync as readdirSync6,
|
|
10152
10153
|
readFileSync as readFileSync11,
|
|
10153
10154
|
readSync,
|
|
10154
|
-
rmSync as rmSync5
|
|
10155
|
+
rmSync as rmSync5,
|
|
10156
|
+
statSync as statSync4
|
|
10155
10157
|
} from "node:fs";
|
|
10156
10158
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10157
10159
|
import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
|
|
@@ -27545,6 +27547,15 @@ function isValueComplete(value) {
|
|
|
27545
27547
|
}
|
|
27546
27548
|
return depth <= 0 && !inString;
|
|
27547
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
|
+
}
|
|
27548
27559
|
function parseProjectGodot(content) {
|
|
27549
27560
|
const result = emptyRecord();
|
|
27550
27561
|
let section = emptyRecord();
|
|
@@ -27973,6 +27984,41 @@ function writeRunRecord(record) {
|
|
|
27973
27984
|
mkdirSync3(runsDirectory(), { recursive: true });
|
|
27974
27985
|
writeFileSync4(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
27975
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
|
+
}
|
|
27976
28022
|
function recordRunEnded(pid, exitCode) {
|
|
27977
28023
|
const path = recordPath();
|
|
27978
28024
|
const record = recordAt(path);
|
|
@@ -28745,7 +28791,7 @@ var TOOL_SPECS = [
|
|
|
28745
28791
|
},
|
|
28746
28792
|
{
|
|
28747
28793
|
name: "editor_launch",
|
|
28748
|
-
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.",
|
|
28749
28795
|
parameters: { projectPath: PROJECT_PATH },
|
|
28750
28796
|
requires: [],
|
|
28751
28797
|
operations: {
|
|
@@ -30660,6 +30706,16 @@ class GodotServer {
|
|
|
30660
30706
|
}
|
|
30661
30707
|
});
|
|
30662
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
|
+
}
|
|
30663
30719
|
async handleRestartEditor() {
|
|
30664
30720
|
const before = this.godotBridge.getStatus();
|
|
30665
30721
|
if (!before.connected) {
|
|
@@ -30668,6 +30724,7 @@ class GodotServer {
|
|
|
30668
30724
|
"editor_status says whether the bridge is up and what has reached it"
|
|
30669
30725
|
]);
|
|
30670
30726
|
}
|
|
30727
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
30671
30728
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
30672
30729
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
30673
30730
|
if (asked.isError === true) {
|
|
@@ -30687,6 +30744,8 @@ class GodotServer {
|
|
|
30687
30744
|
]);
|
|
30688
30745
|
}
|
|
30689
30746
|
const now = this.godotBridge.getStatus();
|
|
30747
|
+
const after = this.settingKeysOf(before.projectPath);
|
|
30748
|
+
const dropped = [...settingsBefore].filter((key) => !after.has(key));
|
|
30690
30749
|
return this.jsonTextResponse({
|
|
30691
30750
|
restarted: true,
|
|
30692
30751
|
editorPid: now.editorPid,
|
|
@@ -30694,6 +30753,8 @@ class GodotServer {
|
|
|
30694
30753
|
serverVersion: SERVER_VERSION,
|
|
30695
30754
|
addonIsStale: now.addonVersion !== SERVER_VERSION,
|
|
30696
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,
|
|
30697
30758
|
tookMs: Date.now() - began
|
|
30698
30759
|
});
|
|
30699
30760
|
}
|
|
@@ -30884,6 +30945,7 @@ class GodotServer {
|
|
|
30884
30945
|
const log = new GameLog;
|
|
30885
30946
|
try {
|
|
30886
30947
|
await this.dap().connect();
|
|
30948
|
+
this.dapClient?.getOutput(true);
|
|
30887
30949
|
} catch (error) {
|
|
30888
30950
|
return this.createErrorResponse(`The editor is connected but its debug adapter is not: ${errorMessage(error)}`, [
|
|
30889
30951
|
`This asked on port ${this.dap().port}, which is where the connected editor says it serves`,
|
|
@@ -30894,19 +30956,24 @@ class GodotServer {
|
|
|
30894
30956
|
if (answer.isError === true) {
|
|
30895
30957
|
return answer;
|
|
30896
30958
|
}
|
|
30959
|
+
const startedAt = Date.now();
|
|
30960
|
+
const transcript = openTranscript(startedAt);
|
|
30961
|
+
closeSync(transcript.fd);
|
|
30897
30962
|
const played = {
|
|
30898
30963
|
process: null,
|
|
30899
30964
|
pid: null,
|
|
30900
30965
|
log,
|
|
30901
|
-
transcript:
|
|
30966
|
+
transcript: transcript.path,
|
|
30902
30967
|
readOffset: 0,
|
|
30903
30968
|
projectPath,
|
|
30904
|
-
startedAt
|
|
30969
|
+
startedAt,
|
|
30905
30970
|
exitCode: null,
|
|
30906
30971
|
throughEditor: true,
|
|
30907
30972
|
brokeOn: null
|
|
30908
30973
|
};
|
|
30909
30974
|
this.activeProcess = played;
|
|
30975
|
+
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
30976
|
+
sweepTranscripts();
|
|
30910
30977
|
return this.jsonTextResponse({
|
|
30911
30978
|
started: true,
|
|
30912
30979
|
through: "editor",
|
|
@@ -30922,10 +30989,24 @@ class GodotServer {
|
|
|
30922
30989
|
if (!game.throughEditor || !this.dapClient) {
|
|
30923
30990
|
return;
|
|
30924
30991
|
}
|
|
30992
|
+
const heard = [];
|
|
30925
30993
|
for (const line of this.dapClient.getOutput(true)) {
|
|
30926
|
-
|
|
30994
|
+
const ended = line.endsWith(`
|
|
30927
30995
|
`) ? line : `${line}
|
|
30928
|
-
|
|
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;
|
|
30929
31010
|
}
|
|
30930
31011
|
this.recordWhatItBrokeOn(game);
|
|
30931
31012
|
}
|
|
@@ -31079,7 +31160,20 @@ class GodotServer {
|
|
|
31079
31160
|
}
|
|
31080
31161
|
}
|
|
31081
31162
|
async pickUpWhatTheEditorIsPlaying() {
|
|
31082
|
-
if (
|
|
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
|
+
}
|
|
31083
31177
|
return;
|
|
31084
31178
|
}
|
|
31085
31179
|
const playing = await this.editorPlayingState();
|
|
@@ -31091,19 +31185,23 @@ class GodotServer {
|
|
|
31091
31185
|
} catch (error) {
|
|
31092
31186
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
31093
31187
|
}
|
|
31094
|
-
this.
|
|
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 = {
|
|
31095
31192
|
process: null,
|
|
31096
31193
|
pid: null,
|
|
31097
31194
|
log: new GameLog,
|
|
31098
|
-
transcript: null,
|
|
31195
|
+
transcript: note?.transcript ?? null,
|
|
31099
31196
|
readOffset: 0,
|
|
31100
|
-
projectPath:
|
|
31101
|
-
startedAt: Date.now(),
|
|
31197
|
+
projectPath: project,
|
|
31198
|
+
startedAt: note?.startedAt ?? Date.now(),
|
|
31102
31199
|
exitCode: null,
|
|
31103
31200
|
throughEditor: true,
|
|
31104
31201
|
brokeOn: null,
|
|
31105
31202
|
pickedUpPlaying: true
|
|
31106
31203
|
};
|
|
31204
|
+
this.activeProcess = picked;
|
|
31107
31205
|
}
|
|
31108
31206
|
adoptRecordedRun() {
|
|
31109
31207
|
const record = readRunRecord();
|
|
@@ -31223,6 +31321,9 @@ class GodotServer {
|
|
|
31223
31321
|
const wanted = readBoolean(args, "cpu") ?? false;
|
|
31224
31322
|
const cpuSeconds = wanted && run.pid !== null && stillRunning(run) ? await cpuSecondsOf(run.pid) : undefined;
|
|
31225
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
|
+
}
|
|
31226
31327
|
if (waitedMs !== undefined) {
|
|
31227
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.`);
|
|
31228
31329
|
}
|
|
@@ -31230,7 +31331,11 @@ class GodotServer {
|
|
|
31230
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.");
|
|
31231
31332
|
}
|
|
31232
31333
|
if (run.pickedUpPlaying === true) {
|
|
31233
|
-
notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that
|
|
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.`);
|
|
31234
31339
|
}
|
|
31235
31340
|
if (typeof run.endedHere === "string") {
|
|
31236
31341
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
@@ -31247,7 +31352,8 @@ class GodotServer {
|
|
|
31247
31352
|
pid: run.pid,
|
|
31248
31353
|
errors: run.log.count("error"),
|
|
31249
31354
|
warnings: run.log.count("warning"),
|
|
31250
|
-
clean: run.log.count("error") === 0,
|
|
31355
|
+
clean: run.consoleLost === true ? undefined : run.log.count("error") === 0,
|
|
31356
|
+
consoleLost: run.consoleLost === true ? true : undefined,
|
|
31251
31357
|
heldAt: halt,
|
|
31252
31358
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
31253
31359
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
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",
|