gdharness 0.13.36 → 0.13.38
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 +499 -225
- package/build/index.js +342 -124
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -9846,9 +9846,23 @@ function runLine(runner, version, rest = "") {
|
|
|
9846
9846
|
}
|
|
9847
9847
|
|
|
9848
9848
|
// src/update-check.ts
|
|
9849
|
-
var
|
|
9849
|
+
var REGISTRY_ROOT = "https://registry.npmjs.org";
|
|
9850
|
+
function packageUrl(root) {
|
|
9851
|
+
return `${root.replace(/\/+$/, "")}/gdharness/latest`;
|
|
9852
|
+
}
|
|
9853
|
+
function registryFor(environment) {
|
|
9854
|
+
const named = environment["GDHARNESS_REGISTRY"] ?? "";
|
|
9855
|
+
if (named === "") {
|
|
9856
|
+
return packageUrl(REGISTRY_ROOT);
|
|
9857
|
+
}
|
|
9858
|
+
try {
|
|
9859
|
+
return new URL(named).protocol === "https:" ? packageUrl(named) : packageUrl(REGISTRY_ROOT);
|
|
9860
|
+
} catch {
|
|
9861
|
+
return packageUrl(REGISTRY_ROOT);
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9850
9864
|
var RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag";
|
|
9851
|
-
var CACHE_MS =
|
|
9865
|
+
var CACHE_MS = 10 * 60 * 1000;
|
|
9852
9866
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
9853
9867
|
var MAX_BODY_BYTES = 1 << 20;
|
|
9854
9868
|
var FIRST_RETRY_MS = 30000;
|
|
@@ -9923,8 +9937,8 @@ function isNewer(candidate, current) {
|
|
|
9923
9937
|
}
|
|
9924
9938
|
return !left.prerelease && right.prerelease;
|
|
9925
9939
|
}
|
|
9926
|
-
async function fetchLatest() {
|
|
9927
|
-
const response = await fetch(
|
|
9940
|
+
async function fetchLatest(registry) {
|
|
9941
|
+
const response = await fetch(registry, {
|
|
9928
9942
|
headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
|
|
9929
9943
|
redirect: "error",
|
|
9930
9944
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
@@ -9956,16 +9970,19 @@ async function fetchLatest() {
|
|
|
9956
9970
|
class UpdateCheck {
|
|
9957
9971
|
latest = null;
|
|
9958
9972
|
checking = false;
|
|
9973
|
+
asked = false;
|
|
9959
9974
|
checkedAt = 0;
|
|
9960
9975
|
retryAt = 0;
|
|
9961
9976
|
backoffMs = FIRST_RETRY_MS;
|
|
9962
9977
|
enabled;
|
|
9963
9978
|
current;
|
|
9964
9979
|
cachePath;
|
|
9965
|
-
|
|
9980
|
+
ask;
|
|
9981
|
+
constructor(current, environment = process.env, ask = () => fetchLatest(registryFor(environment))) {
|
|
9966
9982
|
this.current = current;
|
|
9967
9983
|
this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
|
|
9968
9984
|
this.cachePath = cacheFile(environment);
|
|
9985
|
+
this.ask = ask;
|
|
9969
9986
|
const cached = this.enabled ? readCache(this.cachePath) : null;
|
|
9970
9987
|
if (cached !== null) {
|
|
9971
9988
|
this.latest = cached.latest;
|
|
@@ -9973,11 +9990,12 @@ class UpdateCheck {
|
|
|
9973
9990
|
}
|
|
9974
9991
|
}
|
|
9975
9992
|
refresh(now = Date.now()) {
|
|
9976
|
-
if (!this.enabled || this.checking || now < this.retryAt || !this.stale(now)) {
|
|
9993
|
+
if (!this.enabled || this.checking || now < this.retryAt || this.asked && !this.stale(now)) {
|
|
9977
9994
|
return;
|
|
9978
9995
|
}
|
|
9996
|
+
this.asked = true;
|
|
9979
9997
|
this.checking = true;
|
|
9980
|
-
|
|
9998
|
+
this.ask().then((version) => {
|
|
9981
9999
|
if (version === null) {
|
|
9982
10000
|
this.scheduleRetry(now);
|
|
9983
10001
|
return;
|
|
@@ -10107,15 +10125,26 @@ function filledTemplate(where, message, godotVersion) {
|
|
|
10107
10125
|
].join(`
|
|
10108
10126
|
`);
|
|
10109
10127
|
}
|
|
10110
|
-
function defectReport(where, error,
|
|
10128
|
+
function defectReport(where, error, context = {}) {
|
|
10129
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
10111
10130
|
const message = errorMessage(error);
|
|
10112
10131
|
const title = `Defect ${defectSignature(where, message)}: ${where}`;
|
|
10113
10132
|
const url = `${NEW_ISSUE}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate(where, message, godotVersion))}`;
|
|
10114
10133
|
const width = Math.max(...facts(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
10115
10134
|
return [
|
|
10116
|
-
"gdharness failed in a way it does not model.
|
|
10117
|
-
"
|
|
10118
|
-
"
|
|
10135
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
10136
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
10137
|
+
"in gdharness however the rest of it turns out.",
|
|
10138
|
+
"",
|
|
10139
|
+
...seenBefore === 0 ? [
|
|
10140
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
10141
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
10142
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
10143
|
+
"further only spends turns."
|
|
10144
|
+
] : [
|
|
10145
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
10146
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
10147
|
+
],
|
|
10119
10148
|
"",
|
|
10120
10149
|
...facts(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
10121
10150
|
"",
|
|
@@ -10146,15 +10175,15 @@ import {
|
|
|
10146
10175
|
closeSync,
|
|
10147
10176
|
existsSync as existsSync9,
|
|
10148
10177
|
fstatSync,
|
|
10149
|
-
mkdirSync as
|
|
10178
|
+
mkdirSync as mkdirSync6,
|
|
10150
10179
|
mkdtempSync as mkdtempSync2,
|
|
10151
10180
|
openSync as openSync2,
|
|
10152
10181
|
readdirSync as readdirSync6,
|
|
10153
|
-
readFileSync as
|
|
10182
|
+
readFileSync as readFileSync12,
|
|
10154
10183
|
readSync
|
|
10155
10184
|
} from "node:fs";
|
|
10156
10185
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10157
|
-
import { basename as basename2, dirname as
|
|
10186
|
+
import { basename as basename2, dirname as dirname5, join as join12, normalize as normalize2 } from "node:path";
|
|
10158
10187
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10159
10188
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10160
10189
|
import { promisify as promisify4 } from "node:util";
|
|
@@ -24886,7 +24915,7 @@ function staleAnalysisNote(contradicted, dependsOn) {
|
|
|
24886
24915
|
}
|
|
24887
24916
|
const types = new Set(contradicted.map((one) => one.type));
|
|
24888
24917
|
const declaring = [...new Set(contradicted.map((one) => one.declaredIn))].sort();
|
|
24889
|
-
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join("
|
|
24918
|
+
const lever = dependsOn.length === 0 ? ", and the named types depend on no other global class, so that lever is not available here. " + "That is the common case rather than the awkward one: an edit near a type that depends on " + "something refreshes it before the fault is noticed, which is why a type with no " + "dependencies is where this turns up" : `, so changing ${[...dependsOn].sort().join(" or ")} and then rescanning is worth a try`;
|
|
24890
24919
|
return `The editor is reporting against an older copy of ${types.size === 1 ? "a type" : "some types"} ` + "named under contradictedByTheFile. Each member listed is declared in the file the class cache " + "points at, so those diagnostics are wrong however the code is written. Run editor_rescan first: " + "it costs about half a second, and it has cleared this in every reproduction measured since the " + "scan timing was fixed: both in one project, the scan returning in 243ms and 275ms, and one in " + "another where the same call also rebuilt the copy. " + "The fault behind it is that a script the editor has loaded keeps the copy it built, and nothing " + `rebuilds that copy when the file changes; ${declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]} recompiles that copy` : `reloadScript takes one script, so a call each for ${declaring.join(" and ")} recompiles those copies`} from the file into the same ` + "object and answers with the members it has afterwards under reloadedMethods. Read that and the " + "next diagnostics as two readings, because they are of two things: measured in one window on " + "one editor, the analyser resolved a call to a newly added method while the built copy of that " + "same script did not have it. So the reload is worth doing and is not what clears these; the " + "scan is. The other lever measured is that a held type is refreshed when something it " + `depends on changes rather than when it changes itself${lever}. editor_launch restart has always ` + "worked and costs a window. project_import refresh_classes does not, and answers added: [] while " + "this is happening.";
|
|
24891
24920
|
}
|
|
24892
24921
|
function uncachedClassNote(types) {
|
|
@@ -25574,6 +25603,11 @@ function announced(line) {
|
|
|
25574
25603
|
return { severity: match[2] === "WARNING" ? "warning" : "error", text: match[3] ?? "" };
|
|
25575
25604
|
}
|
|
25576
25605
|
var STREAMS = ["stdout", "stderr", "transcript"];
|
|
25606
|
+
var ADMITTED = {
|
|
25607
|
+
error: new Set(["error"]),
|
|
25608
|
+
warning: new Set(["error", "warning"]),
|
|
25609
|
+
info: new Set(["error", "warning", "info"])
|
|
25610
|
+
};
|
|
25577
25611
|
function forAnswer(entries) {
|
|
25578
25612
|
return entries.map((entry) => entry.detail.length === 0 ? { index: entry.index, severity: entry.severity, source: entry.source, text: entry.text } : entry);
|
|
25579
25613
|
}
|
|
@@ -25587,7 +25621,7 @@ class GameLog {
|
|
|
25587
25621
|
};
|
|
25588
25622
|
partial = { stdout: "", stderr: "", transcript: "" };
|
|
25589
25623
|
lastHeadline = null;
|
|
25590
|
-
|
|
25624
|
+
seen = { error: 0, warning: 0, info: 0 };
|
|
25591
25625
|
append(source, chunk) {
|
|
25592
25626
|
const decoded = typeof chunk === "string" ? chunk : this.decoders[source].write(chunk);
|
|
25593
25627
|
const lines = (this.partial[source] + decoded).split(`
|
|
@@ -25644,10 +25678,14 @@ class GameLog {
|
|
|
25644
25678
|
return this.entries.filter((entry) => entry.severity === severity).length;
|
|
25645
25679
|
}
|
|
25646
25680
|
select(options) {
|
|
25647
|
-
const floor = options.sinceLastCall ? this.
|
|
25648
|
-
|
|
25649
|
-
const wanted = new Set(options.severity === "error" ? ["error"] : options.severity === "warning" ? ["error", "warning"] : ["error", "warning", "info"]);
|
|
25681
|
+
const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
|
|
25682
|
+
const wanted = ADMITTED[options.severity];
|
|
25650
25683
|
const needle = options.contains?.toLowerCase();
|
|
25684
|
+
if (needle === undefined) {
|
|
25685
|
+
for (const severity of wanted) {
|
|
25686
|
+
this.seen[severity] = this.entries.length;
|
|
25687
|
+
}
|
|
25688
|
+
}
|
|
25651
25689
|
const matching = this.entries.slice(floor).filter((entry) => {
|
|
25652
25690
|
if (!wanted.has(entry.severity)) {
|
|
25653
25691
|
return false;
|
|
@@ -26353,6 +26391,10 @@ function userDataIn(home, variables = process.env) {
|
|
|
26353
26391
|
}
|
|
26354
26392
|
return carried;
|
|
26355
26393
|
}
|
|
26394
|
+
function savesStayPut(platform = process.platform) {
|
|
26395
|
+
return platform === "darwin";
|
|
26396
|
+
}
|
|
26397
|
+
var SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
26356
26398
|
|
|
26357
26399
|
// src/godot-path.ts
|
|
26358
26400
|
var run = promisify(execFile);
|
|
@@ -26361,47 +26403,65 @@ class GodotLocator {
|
|
|
26361
26403
|
answered = new Map;
|
|
26362
26404
|
found = null;
|
|
26363
26405
|
async find() {
|
|
26364
|
-
if (this.found !== null && await this.
|
|
26365
|
-
return this.found;
|
|
26406
|
+
if (this.found !== null && await this.whyNot(this.found) === null) {
|
|
26407
|
+
return { ok: true, path: this.found };
|
|
26366
26408
|
}
|
|
26367
26409
|
this.found = null;
|
|
26368
26410
|
const named = envValue("GODOT_PATH");
|
|
26369
26411
|
if (named !== undefined) {
|
|
26370
26412
|
const path = normalize(named);
|
|
26371
|
-
|
|
26413
|
+
const reason = await this.whyNot(path);
|
|
26414
|
+
if (reason === null) {
|
|
26372
26415
|
this.found = path;
|
|
26416
|
+
return { ok: true, path };
|
|
26373
26417
|
}
|
|
26374
|
-
return
|
|
26418
|
+
return { ok: false, named: path, reason };
|
|
26375
26419
|
}
|
|
26376
26420
|
for (const candidate of godotCandidates(process.platform, resolveHomeDirectory())) {
|
|
26377
26421
|
const path = normalize(candidate);
|
|
26378
|
-
if (await this.
|
|
26422
|
+
if (await this.whyNot(path) === null) {
|
|
26379
26423
|
this.found = path;
|
|
26380
|
-
return path;
|
|
26424
|
+
return { ok: true, path };
|
|
26381
26425
|
}
|
|
26382
26426
|
}
|
|
26383
|
-
return null;
|
|
26427
|
+
return { ok: false, named: null };
|
|
26384
26428
|
}
|
|
26385
|
-
static
|
|
26386
|
-
|
|
26387
|
-
|
|
26388
|
-
|
|
26389
|
-
|
|
26429
|
+
static refusal(located) {
|
|
26430
|
+
if (located.named === null) {
|
|
26431
|
+
return {
|
|
26432
|
+
message: "No Godot executable found.",
|
|
26433
|
+
advice: [
|
|
26434
|
+
"Set GODOT_PATH to the Godot 4 executable",
|
|
26435
|
+
"Or install Godot where it is usually found, such as /usr/bin/godot or C:\\Program Files\\Godot"
|
|
26436
|
+
]
|
|
26437
|
+
};
|
|
26438
|
+
}
|
|
26439
|
+
return {
|
|
26440
|
+
message: `GODOT_PATH is set to ${located.named}, which ${located.reason ?? "does not answer"}.`,
|
|
26441
|
+
advice: [
|
|
26442
|
+
"Point GODOT_PATH at a Godot 4 executable that answers --version",
|
|
26443
|
+
"Or unset it, and the usual install locations are searched instead"
|
|
26444
|
+
]
|
|
26445
|
+
};
|
|
26446
|
+
}
|
|
26447
|
+
async whyNot(path) {
|
|
26390
26448
|
const known = this.answered.get(path);
|
|
26391
26449
|
if (known !== undefined) {
|
|
26392
26450
|
return known;
|
|
26393
26451
|
}
|
|
26394
|
-
let
|
|
26395
|
-
if (path
|
|
26452
|
+
let reason;
|
|
26453
|
+
if (path !== "godot" && !existsSync5(path)) {
|
|
26454
|
+
reason = "does not exist";
|
|
26455
|
+
} else {
|
|
26396
26456
|
try {
|
|
26397
26457
|
await run(path, ["--version"]);
|
|
26398
|
-
|
|
26399
|
-
} catch {
|
|
26400
|
-
|
|
26458
|
+
reason = null;
|
|
26459
|
+
} catch (error) {
|
|
26460
|
+
reason = `does not answer --version: ${errorMessage(error)}`;
|
|
26401
26461
|
}
|
|
26402
26462
|
}
|
|
26403
|
-
this.answered.set(path,
|
|
26404
|
-
return
|
|
26463
|
+
this.answered.set(path, reason);
|
|
26464
|
+
return reason;
|
|
26405
26465
|
}
|
|
26406
26466
|
}
|
|
26407
26467
|
|
|
@@ -26659,15 +26719,36 @@ function filledTemplate2(where, message, godotVersion) {
|
|
|
26659
26719
|
].join(`
|
|
26660
26720
|
`);
|
|
26661
26721
|
}
|
|
26662
|
-
|
|
26722
|
+
|
|
26723
|
+
class DefectsSeen {
|
|
26724
|
+
counts = new Map;
|
|
26725
|
+
before(where, message) {
|
|
26726
|
+
const signature = defectSignature2(where, message);
|
|
26727
|
+
const seen = this.counts.get(signature) ?? 0;
|
|
26728
|
+
this.counts.set(signature, seen + 1);
|
|
26729
|
+
return seen;
|
|
26730
|
+
}
|
|
26731
|
+
}
|
|
26732
|
+
function defectReport2(where, error, context = {}) {
|
|
26733
|
+
const { godotVersion, seenBefore = 0 } = context;
|
|
26663
26734
|
const message = errorMessage(error);
|
|
26664
26735
|
const title = `Defect ${defectSignature2(where, message)}: ${where}`;
|
|
26665
26736
|
const url = `${NEW_ISSUE2}?template=bug_report.md&title=${encodeURIComponent(title)}` + `&body=${encodeURIComponent(filledTemplate2(where, message, godotVersion))}`;
|
|
26666
26737
|
const width = Math.max(...facts2(where, message, godotVersion).map(([label]) => (label ?? "").length));
|
|
26667
26738
|
return [
|
|
26668
|
-
"gdharness failed in a way it does not model.
|
|
26669
|
-
"
|
|
26670
|
-
"
|
|
26739
|
+
"gdharness failed in a way it does not model. Whatever went wrong underneath, it reached you",
|
|
26740
|
+
"as a crash rather than as a refusal naming what would have worked, and that much is a defect",
|
|
26741
|
+
"in gdharness however the rest of it turns out.",
|
|
26742
|
+
"",
|
|
26743
|
+
...seenBefore === 0 ? [
|
|
26744
|
+
"Send the call once more before anything else. Some of what reaches this line is a file",
|
|
26745
|
+
"another process had open, or a connection that dropped, and those are gone by the next",
|
|
26746
|
+
"attempt. If the same Signature comes back, it is not one of those, and repeating it",
|
|
26747
|
+
"further only spends turns."
|
|
26748
|
+
] : [
|
|
26749
|
+
`You have had this exact failure ${seenBefore === 1 ? "once" : `${seenBefore} times`} in this session already, so it is a`,
|
|
26750
|
+
"state rather than a passing one, and sending the call again will not change it."
|
|
26751
|
+
],
|
|
26671
26752
|
"",
|
|
26672
26753
|
...facts2(where, message, godotVersion).map(([label, value]) => ` ${(label ?? "").padEnd(width)} ${value ?? ""}`),
|
|
26673
26754
|
"",
|
|
@@ -27735,6 +27816,11 @@ function settingKeys(content) {
|
|
|
27735
27816
|
return named;
|
|
27736
27817
|
}
|
|
27737
27818
|
function settingsDroppedReport(before, after) {
|
|
27819
|
+
if (before === null || after === null) {
|
|
27820
|
+
return {
|
|
27821
|
+
settingsNote: `project.godot could not be read ${before === null ? "before" : "after"} this, so nothing can` + " be said about what the editor saved away. Opening a project rewrites the file and drops any" + " key sitting at its own default, and this is the only thing that would have named them." + " Read the file yourself if a setting was pinned at a default on purpose."
|
|
27822
|
+
};
|
|
27823
|
+
}
|
|
27738
27824
|
const dropped = [...before].filter(([key]) => !after.has(key));
|
|
27739
27825
|
if (dropped.length === 0) {
|
|
27740
27826
|
return {};
|
|
@@ -27848,16 +27934,55 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
27848
27934
|
});
|
|
27849
27935
|
}
|
|
27850
27936
|
|
|
27937
|
+
// src/restart-note.ts
|
|
27938
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27939
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27940
|
+
function restartNotePath(projectPath) {
|
|
27941
|
+
return join8(projectPath, ".godot", "gdharness-restart.json");
|
|
27942
|
+
}
|
|
27943
|
+
function noteRestartBegun(note) {
|
|
27944
|
+
const path = restartNotePath(note.projectPath);
|
|
27945
|
+
try {
|
|
27946
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
27947
|
+
writeFileSync4(path, `${JSON.stringify(note, null, 2)}
|
|
27948
|
+
`, "utf8");
|
|
27949
|
+
} catch (error) {
|
|
27950
|
+
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
27951
|
+
}
|
|
27952
|
+
}
|
|
27953
|
+
function restartOwed(projectPath) {
|
|
27954
|
+
try {
|
|
27955
|
+
const parsed = JSON.parse(readFileSync8(restartNotePath(projectPath), "utf8"));
|
|
27956
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
27957
|
+
return null;
|
|
27958
|
+
}
|
|
27959
|
+
const said = parsed;
|
|
27960
|
+
if (typeof said.quitAt !== "string" || typeof said.byPid !== "number" || typeof said.ports !== "object") {
|
|
27961
|
+
return null;
|
|
27962
|
+
}
|
|
27963
|
+
return said;
|
|
27964
|
+
} catch {
|
|
27965
|
+
return null;
|
|
27966
|
+
}
|
|
27967
|
+
}
|
|
27968
|
+
function restartSettled(projectPath) {
|
|
27969
|
+
try {
|
|
27970
|
+
rmSync3(restartNotePath(projectPath), { force: true });
|
|
27971
|
+
} catch (error) {
|
|
27972
|
+
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
27973
|
+
}
|
|
27974
|
+
}
|
|
27975
|
+
|
|
27851
27976
|
// src/run-record.ts
|
|
27852
27977
|
import { execFileSync } from "node:child_process";
|
|
27853
|
-
import { mkdirSync as
|
|
27854
|
-
import { basename, join as
|
|
27978
|
+
import { mkdirSync as mkdirSync4, openSync, readdirSync as readdirSync5, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
27979
|
+
import { basename, join as join10 } from "node:path";
|
|
27855
27980
|
|
|
27856
27981
|
// src/runtime-client.ts
|
|
27857
|
-
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as
|
|
27982
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync9, unlinkSync } from "node:fs";
|
|
27858
27983
|
import { createConnection as createConnection3 } from "node:net";
|
|
27859
27984
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27860
|
-
import { join as
|
|
27985
|
+
import { join as join9, resolve as resolve4 } from "node:path";
|
|
27861
27986
|
import { setTimeout as delay2 } from "node:timers/promises";
|
|
27862
27987
|
|
|
27863
27988
|
// src/tool-args.ts
|
|
@@ -27925,13 +28050,13 @@ function runtimeDirectory(variables = process.env) {
|
|
|
27925
28050
|
return explicit;
|
|
27926
28051
|
}
|
|
27927
28052
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
27928
|
-
return
|
|
28053
|
+
return join9(perUser ?? tmpdir3(), "gdharness");
|
|
27929
28054
|
}
|
|
27930
28055
|
function runtimeDirectories(variables = process.env) {
|
|
27931
28056
|
const candidates = [runtimeDirectory(variables)];
|
|
27932
28057
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
27933
28058
|
for (const base of fallbacks) {
|
|
27934
|
-
candidates.push(
|
|
28059
|
+
candidates.push(join9(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
27935
28060
|
}
|
|
27936
28061
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
27937
28062
|
}
|
|
@@ -27946,7 +28071,7 @@ function processAlive(pid) {
|
|
|
27946
28071
|
function parseAnnouncement(file, pid) {
|
|
27947
28072
|
let fields;
|
|
27948
28073
|
try {
|
|
27949
|
-
fields = asParams(JSON.parse(
|
|
28074
|
+
fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
27950
28075
|
} catch {
|
|
27951
28076
|
return { kind: "rubbish" };
|
|
27952
28077
|
}
|
|
@@ -27984,6 +28109,31 @@ function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
|
27984
28109
|
function discoverRuntimes(directories = runtimeDirectories()) {
|
|
27985
28110
|
return runtimesAnnounced(directories).running;
|
|
27986
28111
|
}
|
|
28112
|
+
var GONE_WINDOW_MS = 120000;
|
|
28113
|
+
var GONE_KEPT = 32;
|
|
28114
|
+
var gone = [];
|
|
28115
|
+
function wentAway(file, pid) {
|
|
28116
|
+
let project = "";
|
|
28117
|
+
try {
|
|
28118
|
+
const fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
|
|
28119
|
+
project = readString(readParams(fields, "project") ?? {}, "path") ?? "";
|
|
28120
|
+
} catch {
|
|
28121
|
+
return;
|
|
28122
|
+
}
|
|
28123
|
+
if (project === "") {
|
|
28124
|
+
return;
|
|
28125
|
+
}
|
|
28126
|
+
gone.unshift({ pid, project, noticedAt: Date.now() });
|
|
28127
|
+
gone.length = Math.min(gone.length, GONE_KEPT);
|
|
28128
|
+
}
|
|
28129
|
+
function announcedAndGone(projectPath) {
|
|
28130
|
+
if (projectPath === undefined) {
|
|
28131
|
+
return [];
|
|
28132
|
+
}
|
|
28133
|
+
const since = Date.now() - GONE_WINDOW_MS;
|
|
28134
|
+
const wanted = resolve4(projectPath);
|
|
28135
|
+
return gone.filter((one) => one.noticedAt >= since && resolve4(one.project) === wanted);
|
|
28136
|
+
}
|
|
27987
28137
|
function announcedIn(directory) {
|
|
27988
28138
|
if (!existsSync7(directory)) {
|
|
27989
28139
|
return [];
|
|
@@ -27994,10 +28144,14 @@ function announcedIn(directory) {
|
|
|
27994
28144
|
if (!match) {
|
|
27995
28145
|
continue;
|
|
27996
28146
|
}
|
|
27997
|
-
const file =
|
|
28147
|
+
const file = join9(directory, entry);
|
|
27998
28148
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
27999
|
-
const
|
|
28149
|
+
const alive = processAlive(pid);
|
|
28150
|
+
const announced = alive ? parseAnnouncement(file, pid) : { kind: "rubbish" };
|
|
28000
28151
|
if (announced.kind === "rubbish") {
|
|
28152
|
+
if (!alive) {
|
|
28153
|
+
wentAway(file, pid);
|
|
28154
|
+
}
|
|
28001
28155
|
try {
|
|
28002
28156
|
unlinkSync(file);
|
|
28003
28157
|
} catch {}
|
|
@@ -28038,6 +28192,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = []) {
|
|
|
28038
28192
|
if (unspoken.length > 0) {
|
|
28039
28193
|
return { problem: tooNew(unspoken) };
|
|
28040
28194
|
}
|
|
28195
|
+
const went = announcedAndGone(projectPath);
|
|
28196
|
+
const first = went[0];
|
|
28197
|
+
if (first !== undefined) {
|
|
28198
|
+
const ago = Math.max(1, Math.round((Date.now() - first.noticedAt) / 1000));
|
|
28199
|
+
const others = went.length - 1;
|
|
28200
|
+
const earlier = others === 0 ? "" : ` ${others} earlier ${others === 1 ? "one" : "ones"} went the same way.`;
|
|
28201
|
+
return {
|
|
28202
|
+
problem: `A game with the runtime addon announced itself and its process is gone: pid ${first.pid}` + `${first.project === "" ? "" : ` for ${first.project}`}, noticed ${ago}s ago.${earlier}` + " It quit or was ended rather than never starting, so editor_output has what it printed" + " on the way, including whatever it broke on. Start another with editor_run once you have read it."
|
|
28203
|
+
};
|
|
28204
|
+
}
|
|
28041
28205
|
return {
|
|
28042
28206
|
problem: "No game with the runtime addon is running. Start one with editor_run, or play the project from the editor with the addon enabled."
|
|
28043
28207
|
};
|
|
@@ -28155,33 +28319,33 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
28155
28319
|
|
|
28156
28320
|
// src/run-record.ts
|
|
28157
28321
|
function runsDirectory() {
|
|
28158
|
-
return
|
|
28322
|
+
return join10(runtimeDirectory(), "runs");
|
|
28159
28323
|
}
|
|
28160
28324
|
function recordPath() {
|
|
28161
|
-
return
|
|
28325
|
+
return join10(runsDirectory(), "run.json");
|
|
28162
28326
|
}
|
|
28163
28327
|
function recordPaths() {
|
|
28164
|
-
return runtimeDirectories().map((directory) =>
|
|
28328
|
+
return runtimeDirectories().map((directory) => join10(directory, "runs", "run.json"));
|
|
28165
28329
|
}
|
|
28166
28330
|
function openTranscript(startedAt) {
|
|
28167
|
-
|
|
28168
|
-
const path =
|
|
28331
|
+
mkdirSync4(runsDirectory(), { recursive: true });
|
|
28332
|
+
const path = join10(runsDirectory(), `run-${startedAt}.log`);
|
|
28169
28333
|
return { path, fd: openSync(path, "a") };
|
|
28170
28334
|
}
|
|
28171
28335
|
function writeRunRecord(record) {
|
|
28172
|
-
|
|
28173
|
-
|
|
28336
|
+
mkdirSync4(runsDirectory(), { recursive: true });
|
|
28337
|
+
writeFileSync5(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
28174
28338
|
}
|
|
28175
28339
|
function editorNotePath() {
|
|
28176
|
-
return
|
|
28340
|
+
return join10(runsDirectory(), "editor-run.json");
|
|
28177
28341
|
}
|
|
28178
28342
|
function writeEditorRunNote(note) {
|
|
28179
|
-
|
|
28180
|
-
|
|
28343
|
+
mkdirSync4(runsDirectory(), { recursive: true });
|
|
28344
|
+
writeFileSync5(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
28181
28345
|
}
|
|
28182
28346
|
function readEditorRunNote() {
|
|
28183
28347
|
for (const directory of runtimeDirectories()) {
|
|
28184
|
-
const note = editorNoteAt(
|
|
28348
|
+
const note = editorNoteAt(join10(directory, "runs", "editor-run.json"));
|
|
28185
28349
|
if (note !== null) {
|
|
28186
28350
|
return note;
|
|
28187
28351
|
}
|
|
@@ -28191,7 +28355,7 @@ function readEditorRunNote() {
|
|
|
28191
28355
|
function editorNoteAt(path) {
|
|
28192
28356
|
let parsed;
|
|
28193
28357
|
try {
|
|
28194
|
-
parsed = JSON.parse(
|
|
28358
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
28195
28359
|
} catch {
|
|
28196
28360
|
return null;
|
|
28197
28361
|
}
|
|
@@ -28214,7 +28378,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
28214
28378
|
return;
|
|
28215
28379
|
}
|
|
28216
28380
|
try {
|
|
28217
|
-
|
|
28381
|
+
writeFileSync5(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
28218
28382
|
} catch {}
|
|
28219
28383
|
}
|
|
28220
28384
|
function readRunRecord() {
|
|
@@ -28229,7 +28393,7 @@ function readRunRecord() {
|
|
|
28229
28393
|
function recordAt(path) {
|
|
28230
28394
|
let parsed;
|
|
28231
28395
|
try {
|
|
28232
|
-
parsed = JSON.parse(
|
|
28396
|
+
parsed = JSON.parse(readFileSync10(path, "utf8"));
|
|
28233
28397
|
} catch {
|
|
28234
28398
|
return null;
|
|
28235
28399
|
}
|
|
@@ -28274,7 +28438,7 @@ function runningAs(pid) {
|
|
|
28274
28438
|
const began = startedAt(pid, null);
|
|
28275
28439
|
const when = began === null ? {} : { startedAt: began };
|
|
28276
28440
|
if (process.platform === "linux") {
|
|
28277
|
-
const raw =
|
|
28441
|
+
const raw = readFileSync10(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
28278
28442
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
28279
28443
|
}
|
|
28280
28444
|
const args = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -28396,7 +28560,7 @@ function clearRunRecord(ours = () => true) {
|
|
|
28396
28560
|
continue;
|
|
28397
28561
|
}
|
|
28398
28562
|
try {
|
|
28399
|
-
|
|
28563
|
+
rmSync4(path);
|
|
28400
28564
|
} catch {}
|
|
28401
28565
|
}
|
|
28402
28566
|
}
|
|
@@ -28420,19 +28584,19 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
28420
28584
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
28421
28585
|
continue;
|
|
28422
28586
|
}
|
|
28423
|
-
const path =
|
|
28587
|
+
const path = join10(directory, entry);
|
|
28424
28588
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
28425
28589
|
continue;
|
|
28426
28590
|
}
|
|
28427
28591
|
try {
|
|
28428
|
-
|
|
28592
|
+
rmSync4(path);
|
|
28429
28593
|
} catch {}
|
|
28430
28594
|
}
|
|
28431
28595
|
}
|
|
28432
28596
|
|
|
28433
28597
|
// src/setup.ts
|
|
28434
|
-
import { cpSync, existsSync as existsSync8, mkdirSync as
|
|
28435
|
-
import { dirname as
|
|
28598
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
28599
|
+
import { dirname as dirname4, join as join11 } from "node:path";
|
|
28436
28600
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
28437
28601
|
var RUNTIME_AUTOLOAD = {
|
|
28438
28602
|
name: "GdharnessRuntime",
|
|
@@ -28440,11 +28604,11 @@ var RUNTIME_AUTOLOAD = {
|
|
|
28440
28604
|
};
|
|
28441
28605
|
var VERSION_MARKER = ".gdharness-version";
|
|
28442
28606
|
function installedAddonVersion(projectPath) {
|
|
28443
|
-
const marker =
|
|
28607
|
+
const marker = join11(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
28444
28608
|
if (!existsSync8(marker)) {
|
|
28445
28609
|
return null;
|
|
28446
28610
|
}
|
|
28447
|
-
const said =
|
|
28611
|
+
const said = readFileSync11(marker, "utf8").trim();
|
|
28448
28612
|
return said === "" ? null : said;
|
|
28449
28613
|
}
|
|
28450
28614
|
|
|
@@ -28781,7 +28945,7 @@ var TOOL_SPECS = [
|
|
|
28781
28945
|
},
|
|
28782
28946
|
{
|
|
28783
28947
|
name: "project_test",
|
|
28784
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
28948
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
28785
28949
|
parameters: {
|
|
28786
28950
|
projectPath: PROJECT_PATH,
|
|
28787
28951
|
path: {
|
|
@@ -28998,7 +29162,7 @@ var TOOL_SPECS = [
|
|
|
28998
29162
|
},
|
|
28999
29163
|
{
|
|
29000
29164
|
name: "script_diagnostics",
|
|
29001
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says
|
|
29165
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
29002
29166
|
parameters: {
|
|
29003
29167
|
projectPath: PROJECT_PATH,
|
|
29004
29168
|
scriptPath: SCRIPT_PATH
|
|
@@ -29048,7 +29212,7 @@ var TOOL_SPECS = [
|
|
|
29048
29212
|
},
|
|
29049
29213
|
{
|
|
29050
29214
|
name: "editor_launch",
|
|
29051
|
-
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. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one.",
|
|
29215
|
+
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. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
|
|
29052
29216
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
29053
29217
|
requires: [],
|
|
29054
29218
|
operations: {
|
|
@@ -29119,7 +29283,7 @@ var TOOL_SPECS = [
|
|
|
29119
29283
|
},
|
|
29120
29284
|
sinceLastCall: {
|
|
29121
29285
|
type: "boolean",
|
|
29122
|
-
description: "Only entries
|
|
29286
|
+
description: "Only entries this has not already answered with at this severity. Default false. Measured per severity floor and not across them, so polling for errors leaves the rest of the output for a later read rather than consuming it, and a read with contains is a search over the whole run and consumes nothing."
|
|
29123
29287
|
},
|
|
29124
29288
|
cpu: {
|
|
29125
29289
|
type: "boolean",
|
|
@@ -29138,7 +29302,7 @@ var TOOL_SPECS = [
|
|
|
29138
29302
|
},
|
|
29139
29303
|
{
|
|
29140
29304
|
name: "editor_status",
|
|
29141
|
-
description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.',
|
|
29305
|
+
description: 'Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means "not reached yet" as often as it means "no editor". True is worth waiting out; false is an editor that is not there. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for; editor_launch open finishes it. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it.',
|
|
29142
29306
|
parameters: {},
|
|
29143
29307
|
requires: []
|
|
29144
29308
|
},
|
|
@@ -29555,7 +29719,7 @@ function buildToolDefinitions() {
|
|
|
29555
29719
|
var UPDATE_NOTICE_EVERY = 500;
|
|
29556
29720
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
29557
29721
|
var run4 = promisify4(execFile4);
|
|
29558
|
-
var __dirname2 =
|
|
29722
|
+
var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
29559
29723
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
29560
29724
|
var SLOWEST_FRAME_RATE = 20;
|
|
29561
29725
|
function patienceForFrames(frames, atLeast) {
|
|
@@ -29751,7 +29915,7 @@ function aboveTheRunner(entry) {
|
|
|
29751
29915
|
}
|
|
29752
29916
|
function elsewhereIn(projectPath, asked) {
|
|
29753
29917
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
29754
|
-
if (wanted === "" || existsSync9(
|
|
29918
|
+
if (wanted === "" || existsSync9(join12(projectPath, wanted))) {
|
|
29755
29919
|
return "";
|
|
29756
29920
|
}
|
|
29757
29921
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -29765,11 +29929,11 @@ function elsewhereIn(projectPath, asked) {
|
|
|
29765
29929
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
29766
29930
|
}
|
|
29767
29931
|
function hasMainScene(projectFile) {
|
|
29768
|
-
const scene = parseProjectGodot(
|
|
29932
|
+
const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
29769
29933
|
return typeof scene === "string" && scene !== "";
|
|
29770
29934
|
}
|
|
29771
29935
|
function editorPlaysHeadless(projectFile) {
|
|
29772
|
-
const runArgs = parseProjectGodot(
|
|
29936
|
+
const runArgs = parseProjectGodot(readFileSync12(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
29773
29937
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
29774
29938
|
}
|
|
29775
29939
|
function camelCased(params) {
|
|
@@ -29785,7 +29949,7 @@ function camelCased(params) {
|
|
|
29785
29949
|
class GodotServer {
|
|
29786
29950
|
mcp;
|
|
29787
29951
|
locator = new GodotLocator;
|
|
29788
|
-
operationsScript =
|
|
29952
|
+
operationsScript = join12(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
29789
29953
|
godotBridge;
|
|
29790
29954
|
tools = buildToolDefinitions();
|
|
29791
29955
|
activeProcess = null;
|
|
@@ -29793,6 +29957,8 @@ class GodotServer {
|
|
|
29793
29957
|
noticedUpdate = false;
|
|
29794
29958
|
callsSinceNotice = 0;
|
|
29795
29959
|
callsSinceFeedback = 0;
|
|
29960
|
+
defectsSeen = new DefectsSeen;
|
|
29961
|
+
lastGodotVersion = null;
|
|
29796
29962
|
lspClient = null;
|
|
29797
29963
|
dapClient = null;
|
|
29798
29964
|
bridgeStartupError = null;
|
|
@@ -29837,11 +30003,11 @@ class GodotServer {
|
|
|
29837
30003
|
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
29838
30004
|
}
|
|
29839
30005
|
async run() {
|
|
29840
|
-
const
|
|
29841
|
-
if (
|
|
29842
|
-
console.error(
|
|
30006
|
+
const located = await this.locator.find();
|
|
30007
|
+
if (!located.ok) {
|
|
30008
|
+
console.error(`[SERVER] ${GodotLocator.refusal(located).message} Until that is fixed every tool that runs the engine will say so.`);
|
|
29843
30009
|
} else {
|
|
29844
|
-
console.error(`[SERVER] Using Godot at: ${
|
|
30010
|
+
console.error(`[SERVER] Using Godot at: ${located.path}`);
|
|
29845
30011
|
}
|
|
29846
30012
|
const transport = new StdioServerTransport;
|
|
29847
30013
|
await this.mcp.server.connect(transport);
|
|
@@ -30041,7 +30207,19 @@ class GodotServer {
|
|
|
30041
30207
|
}
|
|
30042
30208
|
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
30043
30209
|
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
30044
|
-
|
|
30210
|
+
const seenBefore = this.defectsSeen.before(where, errorMessage(error));
|
|
30211
|
+
return {
|
|
30212
|
+
content: [
|
|
30213
|
+
{
|
|
30214
|
+
type: "text",
|
|
30215
|
+
text: defectReport2(where, error, {
|
|
30216
|
+
seenBefore,
|
|
30217
|
+
godotVersion: this.lastGodotVersion ?? undefined
|
|
30218
|
+
})
|
|
30219
|
+
}
|
|
30220
|
+
],
|
|
30221
|
+
isError: true
|
|
30222
|
+
};
|
|
30045
30223
|
}
|
|
30046
30224
|
}
|
|
30047
30225
|
withUpdateNotice(answer) {
|
|
@@ -30402,21 +30580,19 @@ class GodotServer {
|
|
|
30402
30580
|
}
|
|
30403
30581
|
}
|
|
30404
30582
|
async engine() {
|
|
30405
|
-
const
|
|
30406
|
-
if (
|
|
30407
|
-
|
|
30408
|
-
|
|
30409
|
-
response: this.createErrorResponse("No Godot executable found.", GodotLocator.ADVICE)
|
|
30410
|
-
};
|
|
30583
|
+
const located = await this.locator.find();
|
|
30584
|
+
if (!located.ok) {
|
|
30585
|
+
const refusal = GodotLocator.refusal(located);
|
|
30586
|
+
return { ok: false, response: this.createErrorResponse(refusal.message, [...refusal.advice]) };
|
|
30411
30587
|
}
|
|
30412
|
-
return { ok: true, value:
|
|
30588
|
+
return { ok: true, value: located.path };
|
|
30413
30589
|
}
|
|
30414
30590
|
project(args) {
|
|
30415
30591
|
const path = readNonEmptyString(args, "projectPath");
|
|
30416
30592
|
if (path === undefined) {
|
|
30417
30593
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
30418
30594
|
}
|
|
30419
|
-
const file =
|
|
30595
|
+
const file = join12(path, "project.godot");
|
|
30420
30596
|
if (!existsSync9(file)) {
|
|
30421
30597
|
return {
|
|
30422
30598
|
ok: false,
|
|
@@ -30558,7 +30734,7 @@ class GodotServer {
|
|
|
30558
30734
|
}
|
|
30559
30735
|
const engine = await this.engine();
|
|
30560
30736
|
if (!engine.ok) {
|
|
30561
|
-
return
|
|
30737
|
+
return engine.response;
|
|
30562
30738
|
}
|
|
30563
30739
|
const projectPath = project.value.path;
|
|
30564
30740
|
const before = scriptsWithoutUid(projectPath);
|
|
@@ -30581,7 +30757,11 @@ class GodotServer {
|
|
|
30581
30757
|
async operation(operation, params, projectPath) {
|
|
30582
30758
|
const engine = await this.engine();
|
|
30583
30759
|
if (!engine.ok) {
|
|
30584
|
-
return {
|
|
30760
|
+
return {
|
|
30761
|
+
ok: false,
|
|
30762
|
+
message: engine.response.content.map((block) => block.text).join(" "),
|
|
30763
|
+
messages: []
|
|
30764
|
+
};
|
|
30585
30765
|
}
|
|
30586
30766
|
this.logDebug(`Running ${operation} in ${projectPath}: ${JSON.stringify(params)}`);
|
|
30587
30767
|
return await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: GODOT_DEBUG_MODE_DEFAULT }, operation, params, projectPath);
|
|
@@ -30613,7 +30793,7 @@ class GodotServer {
|
|
|
30613
30793
|
if (!engine.ok) {
|
|
30614
30794
|
return engine.response;
|
|
30615
30795
|
}
|
|
30616
|
-
const application = parseProjectGodot(
|
|
30796
|
+
const application = parseProjectGodot(readFileSync12(project.value.file, "utf8"))["application"] ?? {};
|
|
30617
30797
|
const name = application["config/name"];
|
|
30618
30798
|
const mainScene = application["run/main_scene"];
|
|
30619
30799
|
const info = {
|
|
@@ -30674,18 +30854,18 @@ class GodotServer {
|
|
|
30674
30854
|
if (!engine.ok) {
|
|
30675
30855
|
return engine.response;
|
|
30676
30856
|
}
|
|
30677
|
-
const folder =
|
|
30857
|
+
const folder = dirname5(output.absolutePath);
|
|
30678
30858
|
try {
|
|
30679
|
-
|
|
30859
|
+
mkdirSync6(folder, { recursive: true });
|
|
30680
30860
|
} catch (error) {
|
|
30681
|
-
return this.createErrorResponse(`Cannot create ${
|
|
30861
|
+
return this.createErrorResponse(`Cannot create ${dirname5(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
30682
30862
|
}
|
|
30683
30863
|
const debug = readBoolean(args, "debug") ?? false;
|
|
30684
|
-
const exportLogs = mkdtempSync2(
|
|
30864
|
+
const exportLogs = mkdtempSync2(join12(tmpdir4(), "gdharness-export-"));
|
|
30685
30865
|
const exportArgs = [
|
|
30686
30866
|
"--headless",
|
|
30687
30867
|
"--log-file",
|
|
30688
|
-
|
|
30868
|
+
join12(exportLogs, "engine.log"),
|
|
30689
30869
|
"--path",
|
|
30690
30870
|
project.value.path,
|
|
30691
30871
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -30743,7 +30923,7 @@ class GodotServer {
|
|
|
30743
30923
|
return contained.response;
|
|
30744
30924
|
}
|
|
30745
30925
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
30746
|
-
if (!existsSync9(
|
|
30926
|
+
if (!existsSync9(join12(project.value.path, runner))) {
|
|
30747
30927
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
30748
30928
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
30749
30929
|
]);
|
|
@@ -30778,7 +30958,7 @@ class GodotServer {
|
|
|
30778
30958
|
];
|
|
30779
30959
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
30780
30960
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
30781
|
-
const userData = mkdtempSync2(
|
|
30961
|
+
const userData = mkdtempSync2(join12(tmpdir4(), "gdharness-tests-"));
|
|
30782
30962
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
30783
30963
|
const hung = await new Promise((resolve) => {
|
|
30784
30964
|
const timer = setTimeout(() => {
|
|
@@ -30794,14 +30974,14 @@ class GodotServer {
|
|
|
30794
30974
|
resolve(false);
|
|
30795
30975
|
});
|
|
30796
30976
|
});
|
|
30797
|
-
const reportsDir =
|
|
30977
|
+
const reportsDir = join12(project.value.path, ".godot", ...ours.split("/"));
|
|
30798
30978
|
let report = null;
|
|
30799
30979
|
let reportProblem = null;
|
|
30800
30980
|
try {
|
|
30801
30981
|
const written = existsSync9(reportsDir) ? readdirSync6(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
30802
30982
|
const newest = written.at(-1);
|
|
30803
30983
|
if (newest !== undefined) {
|
|
30804
|
-
report = parseJUnit(
|
|
30984
|
+
report = parseJUnit(readFileSync12(join12(reportsDir, newest, "results.xml"), "utf8"));
|
|
30805
30985
|
}
|
|
30806
30986
|
} catch (error) {
|
|
30807
30987
|
reportProblem = errorMessage(error);
|
|
@@ -30822,6 +31002,7 @@ class GodotServer {
|
|
|
30822
31002
|
const said = printed.map((entry) => entry.text);
|
|
30823
31003
|
const nothingRan = hung ? null : whyNoReport(said, asked);
|
|
30824
31004
|
const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? verdicts[exitCode ?? -1] ?? `exit ${exitCode ?? "unknown"}`;
|
|
31005
|
+
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
30825
31006
|
if (report === null) {
|
|
30826
31007
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
30827
31008
|
return {
|
|
@@ -30836,7 +31017,8 @@ class GodotServer {
|
|
|
30836
31017
|
exitCode,
|
|
30837
31018
|
hung,
|
|
30838
31019
|
arguments: cmdArgs,
|
|
30839
|
-
entries: forAnswer(printed.slice(0, 60))
|
|
31020
|
+
entries: forAnswer(printed.slice(0, 60)),
|
|
31021
|
+
savesNote
|
|
30840
31022
|
}, null, 2)
|
|
30841
31023
|
}
|
|
30842
31024
|
],
|
|
@@ -30869,6 +31051,7 @@ class GodotServer {
|
|
|
30869
31051
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
30870
31052
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
30871
31053
|
notRun: notRun > 0 ? notRun : undefined,
|
|
31054
|
+
savesNote,
|
|
30872
31055
|
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun > 0 ? `This run stopped at the first failure in each suite it failed in, so ${notRun} case${notRun === 1 ? "" : "s"} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : undefined,
|
|
30873
31056
|
suites: unclean.map((suite) => ({
|
|
30874
31057
|
name: suite.name,
|
|
@@ -30891,7 +31074,8 @@ class GodotServer {
|
|
|
30891
31074
|
async godotVersion(godotPath) {
|
|
30892
31075
|
try {
|
|
30893
31076
|
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
30894
|
-
|
|
31077
|
+
this.lastGodotVersion = stdout.trim();
|
|
31078
|
+
return this.lastGodotVersion;
|
|
30895
31079
|
} catch (error) {
|
|
30896
31080
|
this.logDebug(`Godot did not answer --version: ${errorMessage(error)}`);
|
|
30897
31081
|
return null;
|
|
@@ -30937,7 +31121,7 @@ class GodotServer {
|
|
|
30937
31121
|
return null;
|
|
30938
31122
|
}
|
|
30939
31123
|
try {
|
|
30940
|
-
return
|
|
31124
|
+
return readFileSync12(contained.absolutePath, "utf8");
|
|
30941
31125
|
} catch {
|
|
30942
31126
|
return null;
|
|
30943
31127
|
}
|
|
@@ -30979,7 +31163,7 @@ class GodotServer {
|
|
|
30979
31163
|
return null;
|
|
30980
31164
|
}
|
|
30981
31165
|
try {
|
|
30982
|
-
return
|
|
31166
|
+
return readFileSync12(contained.absolutePath, "utf8");
|
|
30983
31167
|
} catch {
|
|
30984
31168
|
return null;
|
|
30985
31169
|
}
|
|
@@ -31049,16 +31233,37 @@ class GodotServer {
|
|
|
31049
31233
|
const status = this.godotBridge.getStatus();
|
|
31050
31234
|
return (status.connected ? status[field] : undefined) ?? fallback;
|
|
31051
31235
|
}
|
|
31236
|
+
restartLeftUnfinished(connected) {
|
|
31237
|
+
if (this.ownProject === null) {
|
|
31238
|
+
return null;
|
|
31239
|
+
}
|
|
31240
|
+
const owed = restartOwed(this.ownProject);
|
|
31241
|
+
if (owed === null) {
|
|
31242
|
+
return null;
|
|
31243
|
+
}
|
|
31244
|
+
if (connected) {
|
|
31245
|
+
restartSettled(this.ownProject);
|
|
31246
|
+
return null;
|
|
31247
|
+
}
|
|
31248
|
+
return owed;
|
|
31249
|
+
}
|
|
31052
31250
|
getEditorStatusPayload() {
|
|
31053
31251
|
const status = this.godotBridge.getStatus();
|
|
31054
31252
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
31055
31253
|
const stale = status.connected && status.addonVersion !== SERVER_VERSION;
|
|
31254
|
+
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
31056
31255
|
return {
|
|
31057
31256
|
...status,
|
|
31058
31257
|
serverVersion: SERVER_VERSION,
|
|
31059
31258
|
addonIsStale: status.connected ? stale : undefined,
|
|
31060
31259
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
31061
|
-
mayYetConnect: status.connected ? undefined : this.anEditorIsStillComing(status.listeningSince),
|
|
31260
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? false : this.anEditorIsStillComing(status.listeningSince),
|
|
31261
|
+
restartInterrupted: unfinished === null ? undefined : {
|
|
31262
|
+
quitEditorPid: unfinished.editorPid,
|
|
31263
|
+
quitAt: unfinished.quitAt,
|
|
31264
|
+
byServerPid: unfinished.byPid,
|
|
31265
|
+
note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
|
|
31266
|
+
},
|
|
31062
31267
|
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
31063
31268
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
31064
31269
|
startupError: this.bridgeStartupError,
|
|
@@ -31094,7 +31299,7 @@ class GodotServer {
|
|
|
31094
31299
|
}
|
|
31095
31300
|
}
|
|
31096
31301
|
async handleEditorStatus() {
|
|
31097
|
-
const
|
|
31302
|
+
const located = await this.locator.find();
|
|
31098
31303
|
const announced = runtimesAnnounced();
|
|
31099
31304
|
const games = await Promise.all(announced.running.map(async (endpoint) => {
|
|
31100
31305
|
const reply = await runtimeRequest(endpoint, "ping", {}, this.runtimeTimeoutMs());
|
|
@@ -31114,8 +31319,9 @@ class GodotServer {
|
|
|
31114
31319
|
debugPort: playing?.debugPort ?? this.godotBridge.getStatus().debugPort
|
|
31115
31320
|
},
|
|
31116
31321
|
godot: {
|
|
31117
|
-
path:
|
|
31118
|
-
version:
|
|
31322
|
+
path: located.ok ? located.path : null,
|
|
31323
|
+
version: located.ok ? await this.godotVersion(located.path) : null,
|
|
31324
|
+
problem: located.ok ? undefined : GodotLocator.refusal(located).message
|
|
31119
31325
|
},
|
|
31120
31326
|
game: {
|
|
31121
31327
|
processActive: runIsUp(this.currentRun(), playing === null ? null : playing.playing),
|
|
@@ -31134,12 +31340,12 @@ class GodotServer {
|
|
|
31134
31340
|
}
|
|
31135
31341
|
settingKeysOf(projectPath) {
|
|
31136
31342
|
if (projectPath === undefined) {
|
|
31137
|
-
return
|
|
31343
|
+
return null;
|
|
31138
31344
|
}
|
|
31139
31345
|
try {
|
|
31140
|
-
return settingKeys(
|
|
31346
|
+
return settingKeys(readFileSync12(join12(projectPath, "project.godot"), "utf8"));
|
|
31141
31347
|
} catch {
|
|
31142
|
-
return
|
|
31348
|
+
return null;
|
|
31143
31349
|
}
|
|
31144
31350
|
}
|
|
31145
31351
|
async handleRestartEditor() {
|
|
@@ -31199,12 +31405,21 @@ class GodotServer {
|
|
|
31199
31405
|
lsp: this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT),
|
|
31200
31406
|
dap: this.editorServes("dapPort", "GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT)
|
|
31201
31407
|
};
|
|
31408
|
+
noteRestartBegun({
|
|
31409
|
+
projectPath,
|
|
31410
|
+
editorPid: editorPid ?? null,
|
|
31411
|
+
ports,
|
|
31412
|
+
quitAt: new Date().toISOString(),
|
|
31413
|
+
byPid: process.pid
|
|
31414
|
+
});
|
|
31202
31415
|
const asked = await this.handleViaBridge("quit_editor", {});
|
|
31203
31416
|
if (asked.isError === true) {
|
|
31417
|
+
restartSettled(projectPath);
|
|
31204
31418
|
return asked;
|
|
31205
31419
|
}
|
|
31206
31420
|
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
31207
31421
|
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
31422
|
+
restartSettled(projectPath);
|
|
31208
31423
|
if (opened.error !== null) {
|
|
31209
31424
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
31210
31425
|
}
|
|
@@ -31240,10 +31455,12 @@ class GodotServer {
|
|
|
31240
31455
|
this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
|
|
31241
31456
|
const ports = await this.portsForAnEditor();
|
|
31242
31457
|
const before = this.settingKeysOf(project.value.path);
|
|
31458
|
+
const unfinished = restartOwed(project.value.path);
|
|
31243
31459
|
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
31244
31460
|
if (opened.error !== null) {
|
|
31245
31461
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
31246
31462
|
}
|
|
31463
|
+
restartSettled(project.value.path);
|
|
31247
31464
|
this.launchedFrom = { projectPath: project.value.path, before };
|
|
31248
31465
|
return this.jsonTextResponse({
|
|
31249
31466
|
launched: true,
|
|
@@ -31251,7 +31468,8 @@ class GodotServer {
|
|
|
31251
31468
|
projectPath: project.value.path,
|
|
31252
31469
|
lspPort: ports.lsp,
|
|
31253
31470
|
dapPort: ports.dap,
|
|
31254
|
-
|
|
31471
|
+
finishesRestart: unfinished === null ? undefined : `This is the launch half of a restart begun at ${unfinished.quitAt} by server pid ${unfinished.byPid}, which quit editor pid ${unfinished.editorPid ?? "unknown"} and was ended before it could start one.`,
|
|
31472
|
+
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
31255
31473
|
});
|
|
31256
31474
|
}
|
|
31257
31475
|
async openAnEditor(engine, projectPath, ports) {
|
|
@@ -31368,7 +31586,7 @@ class GodotServer {
|
|
|
31368
31586
|
return runIsUp(run, editorSays);
|
|
31369
31587
|
}
|
|
31370
31588
|
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
31371
|
-
if (!existsSync9(
|
|
31589
|
+
if (!existsSync9(join12(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
31372
31590
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
31373
31591
|
}
|
|
31374
31592
|
const endpoint = await announcedSince(projectPath, before, {
|
|
@@ -32036,8 +32254,8 @@ class GodotServer {
|
|
|
32036
32254
|
return this.createErrorResponse(choice.problem);
|
|
32037
32255
|
}
|
|
32038
32256
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
32039
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
32040
|
-
const screenshotPath = screenshotDir ?
|
|
32257
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join12(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
32258
|
+
const screenshotPath = screenshotDir ? join12(screenshotDir, "capture.png") : null;
|
|
32041
32259
|
try {
|
|
32042
32260
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
32043
32261
|
if (!reply.ok) {
|
|
@@ -32058,7 +32276,7 @@ class GodotServer {
|
|
|
32058
32276
|
return {
|
|
32059
32277
|
content: [
|
|
32060
32278
|
{ type: "text", text: `Screenshot captured: ${dimensions}` },
|
|
32061
|
-
{ type: "image", data:
|
|
32279
|
+
{ type: "image", data: readFileSync12(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
32062
32280
|
]
|
|
32063
32281
|
};
|
|
32064
32282
|
} finally {
|