gdharness 1.0.11 → 1.0.13
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 +650 -147
- package/build/godot/addons/gdharness_editor/bridge_client.gd +18 -2
- package/build/index.js +589 -142
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10047,11 +10047,20 @@ var SERVER_VERSION = (() => {
|
|
|
10047
10047
|
}
|
|
10048
10048
|
})();
|
|
10049
10049
|
var UNVERSIONED = "an addon from before versions were reported";
|
|
10050
|
-
function
|
|
10051
|
-
if (
|
|
10050
|
+
function editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10051
|
+
if (addonDigest !== undefined && addonDigest !== "" && shippedDigest !== undefined) {
|
|
10052
|
+
return addonDigest !== shippedDigest;
|
|
10053
|
+
}
|
|
10054
|
+
return addonVersion !== serverVersion;
|
|
10055
|
+
}
|
|
10056
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10057
|
+
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
10052
10058
|
return;
|
|
10053
10059
|
}
|
|
10054
10060
|
const reported = addonVersion ?? "";
|
|
10061
|
+
if (reported === serverVersion) {
|
|
10062
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. Restart it with editor_launch restart to pick this one up.`;
|
|
10063
|
+
}
|
|
10055
10064
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
10056
10065
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
10057
10066
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
@@ -10059,8 +10068,8 @@ function addonMismatch(addonVersion, serverVersion) {
|
|
|
10059
10068
|
}
|
|
10060
10069
|
return `${both} Restart it with editor_launch restart to pick the new one up.`;
|
|
10061
10070
|
}
|
|
10062
|
-
function markIfStale(answer, addonVersion, serverVersion) {
|
|
10063
|
-
const staleNote = addonMismatch(addonVersion, serverVersion);
|
|
10071
|
+
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10072
|
+
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
|
|
10064
10073
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
10065
10074
|
return answer;
|
|
10066
10075
|
}
|
|
@@ -10172,22 +10181,22 @@ import { execFile as execFile5, spawn } from "node:child_process";
|
|
|
10172
10181
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10173
10182
|
import {
|
|
10174
10183
|
appendFileSync,
|
|
10175
|
-
closeSync,
|
|
10176
|
-
existsSync as
|
|
10184
|
+
closeSync as closeSync2,
|
|
10185
|
+
existsSync as existsSync10,
|
|
10177
10186
|
fstatSync,
|
|
10178
|
-
mkdirSync as
|
|
10187
|
+
mkdirSync as mkdirSync10,
|
|
10179
10188
|
mkdtempSync as mkdtempSync2,
|
|
10180
|
-
openSync as
|
|
10181
|
-
readdirSync as
|
|
10182
|
-
readFileSync as
|
|
10183
|
-
readSync,
|
|
10189
|
+
openSync as openSync3,
|
|
10190
|
+
readdirSync as readdirSync7,
|
|
10191
|
+
readFileSync as readFileSync16,
|
|
10192
|
+
readSync as readSync2,
|
|
10184
10193
|
rmdirSync,
|
|
10185
|
-
statSync as
|
|
10194
|
+
statSync as statSync7
|
|
10186
10195
|
} from "node:fs";
|
|
10187
10196
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10188
|
-
import { basename as basename3, dirname as
|
|
10197
|
+
import { basename as basename3, dirname as dirname10, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10189
10198
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10190
|
-
import { fileURLToPath as
|
|
10199
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
10191
10200
|
import { promisify as promisify5 } from "node:util";
|
|
10192
10201
|
|
|
10193
10202
|
// node_modules/zod/v3/helpers/util.js
|
|
@@ -24791,8 +24800,18 @@ function runArguments(options) {
|
|
|
24791
24800
|
}
|
|
24792
24801
|
return args;
|
|
24793
24802
|
}
|
|
24794
|
-
function editorArguments(projectPath, ports) {
|
|
24795
|
-
return [
|
|
24803
|
+
function editorArguments(projectPath, ports, logFile) {
|
|
24804
|
+
return [
|
|
24805
|
+
"-e",
|
|
24806
|
+
"--path",
|
|
24807
|
+
projectPath,
|
|
24808
|
+
"--lsp-port",
|
|
24809
|
+
String(ports.lsp),
|
|
24810
|
+
"--dap-port",
|
|
24811
|
+
String(ports.dap),
|
|
24812
|
+
"--log-file",
|
|
24813
|
+
logFile
|
|
24814
|
+
];
|
|
24796
24815
|
}
|
|
24797
24816
|
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
|
|
24798
24817
|
function userDataIn(home, variables = process.env) {
|
|
@@ -25318,10 +25337,37 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
25318
25337
|
function describe2(endpoint) {
|
|
25319
25338
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
25320
25339
|
}
|
|
25340
|
+
function describeWithin(endpoint) {
|
|
25341
|
+
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port}`;
|
|
25342
|
+
}
|
|
25343
|
+
function theOneProject(endpoints) {
|
|
25344
|
+
if (endpoints.some((endpoint) => endpoint.project.path === "")) {
|
|
25345
|
+
return;
|
|
25346
|
+
}
|
|
25347
|
+
const paths = new Set(endpoints.map((endpoint) => resolve(endpoint.project.path)));
|
|
25348
|
+
const only = [...paths][0];
|
|
25349
|
+
return paths.size === 1 ? only : undefined;
|
|
25350
|
+
}
|
|
25351
|
+
function severalFrom(project, games) {
|
|
25352
|
+
return `${games.length} games are running from ${project}: ${games.map(describeWithin).join("; ")}. ` + "Pass pid to choose one; projectPath cannot, because every one of them has this one. " + "editor_status lists them under runtimes.";
|
|
25353
|
+
}
|
|
25354
|
+
function whichHalfIsBehind(unspoken) {
|
|
25355
|
+
const pids = (games) => games.map((game) => `pid ${game.pid}`).join(", ");
|
|
25356
|
+
const ahead = unspoken.filter((game) => game.protocol > RUNTIME_PROTOCOL);
|
|
25357
|
+
const behind = unspoken.filter((game) => game.protocol < RUNTIME_PROTOCOL);
|
|
25358
|
+
if (ahead.length > 0 && behind.length > 0) {
|
|
25359
|
+
return `they are on both sides of it: reconnect this server for ${pids(ahead)}, and reinstall the ` + `addon and restart ${pids(behind)}`;
|
|
25360
|
+
}
|
|
25361
|
+
if (ahead.length > 0) {
|
|
25362
|
+
return "this server is the older half: reconnect it so it spawns the installed version";
|
|
25363
|
+
}
|
|
25364
|
+
return `the addon is the older half: reinstall it and restart ${behind.length === 1 ? "the game" : "those games"}`;
|
|
25365
|
+
}
|
|
25321
25366
|
function tooNew(unspoken) {
|
|
25322
25367
|
const named = unspoken.map((game) => `pid ${game.pid} speaks ${game.protocol} (${game.project.name || "unnamed"} at ${game.project.path})`).join("; ");
|
|
25323
|
-
const
|
|
25324
|
-
|
|
25368
|
+
const count = unspoken.length === 1 ? "A game is running" : `${unspoken.length} games are running`;
|
|
25369
|
+
const spoken = new Set(unspoken.map((game) => game.protocol)).size === 1 ? "a protocol" : "protocols";
|
|
25370
|
+
return `${count}, in ${spoken} this server does not speak: ${named}. This server speaks ${RUNTIME_PROTOCOL}, so ${whichHalfIsBehind(unspoken)}.`;
|
|
25325
25371
|
}
|
|
25326
25372
|
function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
25327
25373
|
if (pid !== undefined) {
|
|
@@ -25338,8 +25384,16 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
25338
25384
|
if (tooNewToo !== undefined) {
|
|
25339
25385
|
return { problem: tooNew([tooNewToo]) };
|
|
25340
25386
|
}
|
|
25387
|
+
if (endpoints.length > 0) {
|
|
25388
|
+
return {
|
|
25389
|
+
problem: `No running game has pid ${pid}. Running: ${endpoints.map(describe2).join("; ")}. editor_status lists them under runtimes as they come and go.`
|
|
25390
|
+
};
|
|
25391
|
+
}
|
|
25392
|
+
if (unspoken.length > 0) {
|
|
25393
|
+
return { problem: `No game reachable from here has pid ${pid}. ${tooNew(unspoken)}` };
|
|
25394
|
+
}
|
|
25341
25395
|
return {
|
|
25342
|
-
problem:
|
|
25396
|
+
problem: `No game with the runtime addon is running, so there is none with pid ${pid}. Start one with editor_run, or play the project from the editor with the addon enabled.`
|
|
25343
25397
|
};
|
|
25344
25398
|
}
|
|
25345
25399
|
if (endpoints.length === 0) {
|
|
@@ -25371,15 +25425,17 @@ function chooseRuntime(endpoints, projectPath, unspoken = [], pid) {
|
|
|
25371
25425
|
if (matching.length === 1 && matching[0]) {
|
|
25372
25426
|
return { endpoint: matching[0] };
|
|
25373
25427
|
}
|
|
25374
|
-
return {
|
|
25375
|
-
problem: `Several games are running from ${wanted}: ${matching.map(describe2).join("; ")}. Pass pid to choose one.`
|
|
25376
|
-
};
|
|
25428
|
+
return { problem: severalFrom(wanted, matching) };
|
|
25377
25429
|
}
|
|
25378
25430
|
if (endpoints.length === 1 && endpoints[0]) {
|
|
25379
25431
|
return { endpoint: endpoints[0] };
|
|
25380
25432
|
}
|
|
25433
|
+
const shared = theOneProject(endpoints);
|
|
25434
|
+
if (shared !== undefined) {
|
|
25435
|
+
return { problem: severalFrom(shared, endpoints) };
|
|
25436
|
+
}
|
|
25381
25437
|
return {
|
|
25382
|
-
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass
|
|
25438
|
+
problem: `Several games are running: ${endpoints.map(describe2).join("; ")}. Pass pid to choose one, or projectPath when the project you mean is running only one.`
|
|
25383
25439
|
};
|
|
25384
25440
|
}
|
|
25385
25441
|
var nextRequestId = 1;
|
|
@@ -25802,6 +25858,19 @@ function cacheWrittenAt(projectPath) {
|
|
|
25802
25858
|
return null;
|
|
25803
25859
|
}
|
|
25804
25860
|
}
|
|
25861
|
+
function declaredSince(projectPath, paths, names, since) {
|
|
25862
|
+
return names.filter((name) => {
|
|
25863
|
+
const path = paths.get(name);
|
|
25864
|
+
if (path === undefined || !path.startsWith("res://")) {
|
|
25865
|
+
return false;
|
|
25866
|
+
}
|
|
25867
|
+
try {
|
|
25868
|
+
return statSync2(join6(projectPath, path.slice("res://".length))).mtimeMs > since;
|
|
25869
|
+
} catch {
|
|
25870
|
+
return false;
|
|
25871
|
+
}
|
|
25872
|
+
});
|
|
25873
|
+
}
|
|
25805
25874
|
function cachedAtMissingPaths(cached, projectPath) {
|
|
25806
25875
|
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync3(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
25807
25876
|
}
|
|
@@ -26613,6 +26682,20 @@ function dictionary(entries) {
|
|
|
26613
26682
|
return Object.assign(emptyRecord(), entries);
|
|
26614
26683
|
}
|
|
26615
26684
|
|
|
26685
|
+
// src/editor-log.ts
|
|
26686
|
+
import {
|
|
26687
|
+
closeSync,
|
|
26688
|
+
existsSync as existsSync5,
|
|
26689
|
+
mkdirSync as mkdirSync6,
|
|
26690
|
+
openSync,
|
|
26691
|
+
readFileSync as readFileSync10,
|
|
26692
|
+
readSync,
|
|
26693
|
+
rmSync as rmSync3,
|
|
26694
|
+
statSync as statSync3,
|
|
26695
|
+
writeFileSync as writeFileSync6
|
|
26696
|
+
} from "node:fs";
|
|
26697
|
+
import { dirname as dirname6, join as join9 } from "node:path";
|
|
26698
|
+
|
|
26616
26699
|
// src/game-log.ts
|
|
26617
26700
|
import { StringDecoder } from "node:string_decoder";
|
|
26618
26701
|
var HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
@@ -26699,6 +26782,9 @@ class GameLog {
|
|
|
26699
26782
|
count(severity) {
|
|
26700
26783
|
return this.entries.filter((entry) => entry.severity === severity).length;
|
|
26701
26784
|
}
|
|
26785
|
+
everything() {
|
|
26786
|
+
return this.entries;
|
|
26787
|
+
}
|
|
26702
26788
|
select(options) {
|
|
26703
26789
|
const floor = options.sinceLastCall ? this.seen[options.severity] : 0;
|
|
26704
26790
|
const wanted = ADMITTED[options.severity];
|
|
@@ -26722,6 +26808,221 @@ class GameLog {
|
|
|
26722
26808
|
}
|
|
26723
26809
|
}
|
|
26724
26810
|
|
|
26811
|
+
// src/editor-log.ts
|
|
26812
|
+
function editorLogPath(projectPath) {
|
|
26813
|
+
return join9(projectPath, ".godot", "gdharness-editor.log");
|
|
26814
|
+
}
|
|
26815
|
+
function editorLogNotePath(projectPath) {
|
|
26816
|
+
return join9(projectPath, ".godot", "gdharness-editor.json");
|
|
26817
|
+
}
|
|
26818
|
+
function readEditorLogNote(projectPath) {
|
|
26819
|
+
try {
|
|
26820
|
+
const fields = asParams(JSON.parse(readFileSync10(editorLogNotePath(projectPath), "utf8")));
|
|
26821
|
+
const pid = readNumber(fields, "pid");
|
|
26822
|
+
const startedAt = readString(fields, "startedAt");
|
|
26823
|
+
return pid !== undefined && Number.isInteger(pid) && pid > 0 && startedAt !== undefined ? { pid, startedAt } : null;
|
|
26824
|
+
} catch {
|
|
26825
|
+
return null;
|
|
26826
|
+
}
|
|
26827
|
+
}
|
|
26828
|
+
function clearEditorLog(projectPath) {
|
|
26829
|
+
try {
|
|
26830
|
+
const path = editorLogPath(projectPath);
|
|
26831
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
26832
|
+
rmSync3(path, { force: true });
|
|
26833
|
+
rmSync3(editorLogNotePath(projectPath), { force: true });
|
|
26834
|
+
} catch {}
|
|
26835
|
+
}
|
|
26836
|
+
function writeEditorLogNote(projectPath, pid) {
|
|
26837
|
+
try {
|
|
26838
|
+
const path = editorLogNotePath(projectPath);
|
|
26839
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
26840
|
+
writeFileSync6(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
|
|
26841
|
+
} catch {}
|
|
26842
|
+
}
|
|
26843
|
+
function theConsoleWasNotCaptured(why, projectPath) {
|
|
26844
|
+
if (why.kind === "not ours") {
|
|
26845
|
+
return "This editor was not opened by gdharness, so its console was never captured and nothing " + "here has it: an editor prints to a console no plugin can read, and the addon cannot get " + "at it from inside. This is not an editor that printed nothing.";
|
|
26846
|
+
}
|
|
26847
|
+
if (why.kind === "older server") {
|
|
26848
|
+
return `This editor was opened by a gdharness server, but the console beside ${projectPath} belongs ` + `to editor ${why.notedPid ?? "none recorded"} rather than to this one, so it holds an ` + "earlier session and is not offered as this one. A server from before consoles were " + "captured leaves exactly this.";
|
|
26849
|
+
}
|
|
26850
|
+
return "This editor was opened by gdharness and has written nothing to its console file yet, not " + "even the engine version it prints first, so the file is missing rather than empty. An " + "editor that could not open the file for writing leaves this.";
|
|
26851
|
+
}
|
|
26852
|
+
function editorConsole(projectPath, editorPid, openedByAServer) {
|
|
26853
|
+
if (!openedByAServer) {
|
|
26854
|
+
return { kind: "not ours" };
|
|
26855
|
+
}
|
|
26856
|
+
const noted = readEditorLogNote(projectPath);
|
|
26857
|
+
if (noted === null || editorPid === null || noted.pid !== editorPid) {
|
|
26858
|
+
return { kind: "older server", notedPid: noted?.pid ?? null };
|
|
26859
|
+
}
|
|
26860
|
+
const path = editorLogPath(projectPath);
|
|
26861
|
+
if (!existsSync5(path)) {
|
|
26862
|
+
return { kind: "not written" };
|
|
26863
|
+
}
|
|
26864
|
+
const log = new GameLog;
|
|
26865
|
+
const { text, of } = theFirstOf(path);
|
|
26866
|
+
log.append("transcript", text);
|
|
26867
|
+
log.finish();
|
|
26868
|
+
return { log, path, ...of === undefined ? {} : { readBytes: text.length, of } };
|
|
26869
|
+
}
|
|
26870
|
+
var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
|
|
26871
|
+
function theFirstOf(path) {
|
|
26872
|
+
const whole = statSync3(path).size;
|
|
26873
|
+
if (whole <= MOST_CONSOLE_BYTES) {
|
|
26874
|
+
return { text: readFileSync10(path, "utf8") };
|
|
26875
|
+
}
|
|
26876
|
+
const handle = openSync(path, "r");
|
|
26877
|
+
try {
|
|
26878
|
+
const buffer = Buffer.alloc(MOST_CONSOLE_BYTES);
|
|
26879
|
+
const read = readSync(handle, buffer, 0, MOST_CONSOLE_BYTES, 0);
|
|
26880
|
+
const decoded = buffer.subarray(0, read).toString("utf8");
|
|
26881
|
+
const lastLine = decoded.lastIndexOf(`
|
|
26882
|
+
`);
|
|
26883
|
+
return { text: lastLine === -1 ? decoded : decoded.slice(0, lastLine + 1), of: whole };
|
|
26884
|
+
} finally {
|
|
26885
|
+
closeSync(handle);
|
|
26886
|
+
}
|
|
26887
|
+
}
|
|
26888
|
+
var NAMED_VALUES = 6;
|
|
26889
|
+
var WALK_BACK_LIMIT = 500;
|
|
26890
|
+
var SLOTS = [/"[^"]*"/g, /'[^']*'/g, /\b(?:res|user):\/\/\S+/g, /(?<=\.gd|\.tscn|\.cs):\d+(?::\d+)?/g];
|
|
26891
|
+
function shapeOf(text) {
|
|
26892
|
+
const values = [];
|
|
26893
|
+
let shape = "";
|
|
26894
|
+
let at = 0;
|
|
26895
|
+
while (at < text.length) {
|
|
26896
|
+
let earliest = null;
|
|
26897
|
+
for (const pattern of SLOTS) {
|
|
26898
|
+
pattern.lastIndex = at;
|
|
26899
|
+
const found = pattern.exec(text);
|
|
26900
|
+
if (found !== null && (earliest === null || found.index < earliest.index || found.index === earliest.index && found[0].length > earliest.value.length)) {
|
|
26901
|
+
earliest = { index: found.index, value: found[0] };
|
|
26902
|
+
}
|
|
26903
|
+
}
|
|
26904
|
+
if (earliest === null) {
|
|
26905
|
+
shape += text.slice(at);
|
|
26906
|
+
break;
|
|
26907
|
+
}
|
|
26908
|
+
if (earliest.value.length === 0) {
|
|
26909
|
+
shape += text.slice(at, at + 1);
|
|
26910
|
+
at += 1;
|
|
26911
|
+
continue;
|
|
26912
|
+
}
|
|
26913
|
+
shape += `${text.slice(at, earliest.index)}…`;
|
|
26914
|
+
values.push(earliest.value);
|
|
26915
|
+
at = earliest.index + earliest.value.length;
|
|
26916
|
+
}
|
|
26917
|
+
return { shape, values };
|
|
26918
|
+
}
|
|
26919
|
+
function bursts(entries, before) {
|
|
26920
|
+
const shapes = new Map;
|
|
26921
|
+
const shaped = entries.map((entry) => shapeOf(entry.text));
|
|
26922
|
+
for (const [at, entry] of entries.entries()) {
|
|
26923
|
+
const key = `${entry.severity}\x00${shaped[at]?.shape ?? entry.text}`;
|
|
26924
|
+
const group = shapes.get(key) ?? { indices: [], values: [] };
|
|
26925
|
+
group.indices.push(at);
|
|
26926
|
+
group.values.push(shaped[at]?.values ?? []);
|
|
26927
|
+
shapes.set(key, group);
|
|
26928
|
+
}
|
|
26929
|
+
const collapsed = new Set;
|
|
26930
|
+
for (const group of shapes.values()) {
|
|
26931
|
+
if (group.indices.length > 1) {
|
|
26932
|
+
for (const at of group.indices) {
|
|
26933
|
+
collapsed.add(at);
|
|
26934
|
+
}
|
|
26935
|
+
}
|
|
26936
|
+
}
|
|
26937
|
+
const repeated = [];
|
|
26938
|
+
for (const [key, group] of shapes) {
|
|
26939
|
+
const first = group.indices[0];
|
|
26940
|
+
if (group.indices.length < 2 || first === undefined) {
|
|
26941
|
+
continue;
|
|
26942
|
+
}
|
|
26943
|
+
const entry = entries[first];
|
|
26944
|
+
if (entry === undefined) {
|
|
26945
|
+
continue;
|
|
26946
|
+
}
|
|
26947
|
+
repeated.push({
|
|
26948
|
+
shape: key.slice(key.indexOf("\x00") + 1),
|
|
26949
|
+
count: group.indices.length,
|
|
26950
|
+
severity: entry.severity,
|
|
26951
|
+
first: { index: entry.index, text: entry.text },
|
|
26952
|
+
slots: slotsOf(group.values, group.indices.length),
|
|
26953
|
+
before: linesBefore(entries, first, collapsed, before)
|
|
26954
|
+
});
|
|
26955
|
+
}
|
|
26956
|
+
return repeated.sort((one, other) => other.count - one.count);
|
|
26957
|
+
}
|
|
26958
|
+
function slotsOf(values, count) {
|
|
26959
|
+
const width = Math.max(0, ...values.map((row) => row.length));
|
|
26960
|
+
const distinct = [];
|
|
26961
|
+
for (let at = 0;at < width; at += 1) {
|
|
26962
|
+
distinct.push([...new Set(values.map((row) => row[at]).filter((one) => one !== undefined))]);
|
|
26963
|
+
}
|
|
26964
|
+
const slots = [];
|
|
26965
|
+
for (let at = 0;at < width; at += 1) {
|
|
26966
|
+
const seen = distinct[at] ?? [];
|
|
26967
|
+
const partner = theSlotItMovesWith(values, distinct, at);
|
|
26968
|
+
const paired = partner === undefined ? {} : { movesWith: partner };
|
|
26969
|
+
if (seen.length === count && count > 1) {
|
|
26970
|
+
slots.push({ distinct: seen.length, allDifferent: true, ...paired });
|
|
26971
|
+
continue;
|
|
26972
|
+
}
|
|
26973
|
+
const some = seen.slice(0, NAMED_VALUES);
|
|
26974
|
+
slots.push({
|
|
26975
|
+
distinct: seen.length,
|
|
26976
|
+
some,
|
|
26977
|
+
...seen.length > some.length ? { more: seen.length - some.length } : {},
|
|
26978
|
+
...paired
|
|
26979
|
+
});
|
|
26980
|
+
}
|
|
26981
|
+
return slots;
|
|
26982
|
+
}
|
|
26983
|
+
function theSlotItMovesWith(values, distinct, at) {
|
|
26984
|
+
const mine = distinct[at] ?? [];
|
|
26985
|
+
if (mine.length < 2) {
|
|
26986
|
+
return;
|
|
26987
|
+
}
|
|
26988
|
+
for (let other = 0;other < at; other += 1) {
|
|
26989
|
+
if ((distinct[other] ?? []).length !== mine.length) {
|
|
26990
|
+
continue;
|
|
26991
|
+
}
|
|
26992
|
+
const goesWith = new Map;
|
|
26993
|
+
let oneForOne = true;
|
|
26994
|
+
for (const row of values) {
|
|
26995
|
+
const theirs = row[other];
|
|
26996
|
+
const ours = row[at];
|
|
26997
|
+
if (theirs === undefined || ours === undefined) {
|
|
26998
|
+
oneForOne = false;
|
|
26999
|
+
break;
|
|
27000
|
+
}
|
|
27001
|
+
const already = goesWith.get(theirs);
|
|
27002
|
+
if (already === undefined) {
|
|
27003
|
+
goesWith.set(theirs, ours);
|
|
27004
|
+
} else if (already !== ours) {
|
|
27005
|
+
oneForOne = false;
|
|
27006
|
+
break;
|
|
27007
|
+
}
|
|
27008
|
+
}
|
|
27009
|
+
if (oneForOne) {
|
|
27010
|
+
return other;
|
|
27011
|
+
}
|
|
27012
|
+
}
|
|
27013
|
+
return;
|
|
27014
|
+
}
|
|
27015
|
+
function linesBefore(entries, at, collapsed, wanted) {
|
|
27016
|
+
const found = [];
|
|
27017
|
+
for (let back = at - 1;back >= 0 && at - back <= WALK_BACK_LIMIT && found.length < wanted; back -= 1) {
|
|
27018
|
+
const entry = entries[back];
|
|
27019
|
+
if (entry !== undefined && !collapsed.has(back)) {
|
|
27020
|
+
found.push(entry.text);
|
|
27021
|
+
}
|
|
27022
|
+
}
|
|
27023
|
+
return found.reverse();
|
|
27024
|
+
}
|
|
27025
|
+
|
|
26725
27026
|
// src/godot-bridge.ts
|
|
26726
27027
|
import { randomUUID } from "node:crypto";
|
|
26727
27028
|
import { EventEmitter } from "node:events";
|
|
@@ -26900,6 +27201,7 @@ class GodotBridge extends EventEmitter {
|
|
|
26900
27201
|
connectedAt: this.connectionInfo?.connectedAt,
|
|
26901
27202
|
lastPongAt: this.connectionInfo?.lastPongAt,
|
|
26902
27203
|
addonVersion: this.connectionInfo?.addonVersion,
|
|
27204
|
+
addonDigest: this.connectionInfo?.addonDigest,
|
|
26903
27205
|
editorPid: this.connectionInfo?.editorPid,
|
|
26904
27206
|
lspPort: this.connectionInfo?.lspPort,
|
|
26905
27207
|
dapPort: this.connectionInfo?.dapPort,
|
|
@@ -27043,6 +27345,7 @@ class GodotBridge extends EventEmitter {
|
|
|
27043
27345
|
}
|
|
27044
27346
|
this.connectionInfo.projectPath = message.project_path;
|
|
27045
27347
|
this.connectionInfo.addonVersion = message.addon_version ?? "";
|
|
27348
|
+
this.connectionInfo.addonDigest = message.addon_digest;
|
|
27046
27349
|
this.connectionInfo.editorPid = message.editor_pid;
|
|
27047
27350
|
this.connectionInfo.lspPort = servedPort(message.lsp_port);
|
|
27048
27351
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
@@ -27223,14 +27526,14 @@ function getDefaultBridge(ownProject = null) {
|
|
|
27223
27526
|
|
|
27224
27527
|
// src/godot-path.ts
|
|
27225
27528
|
import { execFile as execFile2 } from "node:child_process";
|
|
27226
|
-
import { existsSync as
|
|
27529
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
27227
27530
|
import { normalize } from "node:path";
|
|
27228
27531
|
import { promisify as promisify2 } from "node:util";
|
|
27229
27532
|
|
|
27230
27533
|
// src/detection.ts
|
|
27231
|
-
import { existsSync as
|
|
27534
|
+
import { existsSync as existsSync6, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
|
|
27232
27535
|
import { homedir as homedir2 } from "node:os";
|
|
27233
|
-
import { join as
|
|
27536
|
+
import { join as join10 } from "node:path";
|
|
27234
27537
|
function resolveHomeDirectory() {
|
|
27235
27538
|
try {
|
|
27236
27539
|
return homedir2();
|
|
@@ -27239,7 +27542,7 @@ function resolveHomeDirectory() {
|
|
|
27239
27542
|
}
|
|
27240
27543
|
}
|
|
27241
27544
|
function scanDirectoryForGodotBinaries(directory, platform) {
|
|
27242
|
-
if (!directory || !
|
|
27545
|
+
if (!directory || !existsSync6(directory)) {
|
|
27243
27546
|
return [];
|
|
27244
27547
|
}
|
|
27245
27548
|
let entries;
|
|
@@ -27254,16 +27557,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
|
|
|
27254
27557
|
if (!pattern.test(name)) {
|
|
27255
27558
|
continue;
|
|
27256
27559
|
}
|
|
27257
|
-
const fullPath =
|
|
27560
|
+
const fullPath = join10(directory, name);
|
|
27258
27561
|
try {
|
|
27259
|
-
const stat =
|
|
27562
|
+
const stat = statSync4(fullPath);
|
|
27260
27563
|
if (stat.isFile()) {
|
|
27261
27564
|
matches.push({ name, mtime: stat.mtimeMs });
|
|
27262
27565
|
}
|
|
27263
27566
|
} catch {}
|
|
27264
27567
|
}
|
|
27265
27568
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
27266
|
-
return matches.map((m) =>
|
|
27569
|
+
return matches.map((m) => join10(directory, m.name));
|
|
27267
27570
|
}
|
|
27268
27571
|
function conventionalPaths(platform, home) {
|
|
27269
27572
|
const paths = ["godot"];
|
|
@@ -27366,7 +27669,7 @@ class GodotLocator {
|
|
|
27366
27669
|
return known;
|
|
27367
27670
|
}
|
|
27368
27671
|
let reason;
|
|
27369
|
-
if (path !== "godot" && !
|
|
27672
|
+
if (path !== "godot" && !existsSync7(path)) {
|
|
27370
27673
|
reason = "does not exist";
|
|
27371
27674
|
} else {
|
|
27372
27675
|
try {
|
|
@@ -27383,13 +27686,13 @@ class GodotLocator {
|
|
|
27383
27686
|
|
|
27384
27687
|
// src/headless.ts
|
|
27385
27688
|
import { execFile as execFile3 } from "node:child_process";
|
|
27386
|
-
import { mkdtempSync, writeFileSync as
|
|
27689
|
+
import { mkdtempSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
27387
27690
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27388
|
-
import { join as
|
|
27691
|
+
import { join as join11 } from "node:path";
|
|
27389
27692
|
import { promisify as promisify3 } from "node:util";
|
|
27390
27693
|
|
|
27391
27694
|
// src/scratch.ts
|
|
27392
|
-
import { rmSync as
|
|
27695
|
+
import { rmSync as rmSync4 } from "node:fs";
|
|
27393
27696
|
var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
|
|
27394
27697
|
function discardWith(remove, paths) {
|
|
27395
27698
|
const left = [];
|
|
@@ -27407,7 +27710,7 @@ function discardWith(remove, paths) {
|
|
|
27407
27710
|
}
|
|
27408
27711
|
function discard(...paths) {
|
|
27409
27712
|
return discardWith((path) => {
|
|
27410
|
-
|
|
27713
|
+
rmSync4(path, SWEPT);
|
|
27411
27714
|
}, paths);
|
|
27412
27715
|
}
|
|
27413
27716
|
|
|
@@ -27451,12 +27754,12 @@ function reason(stdout, stderr) {
|
|
|
27451
27754
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
27452
27755
|
}
|
|
27453
27756
|
async function runImport(godotPath, projectPath) {
|
|
27454
|
-
const logDir = mkdtempSync(
|
|
27757
|
+
const logDir = mkdtempSync(join11(tmpdir3(), "gdharness-import-"));
|
|
27455
27758
|
try {
|
|
27456
27759
|
const { stderr } = await run3(godotPath, [
|
|
27457
27760
|
"--headless",
|
|
27458
27761
|
"--log-file",
|
|
27459
|
-
|
|
27762
|
+
join11(logDir, "engine.log"),
|
|
27460
27763
|
"--path",
|
|
27461
27764
|
projectPath,
|
|
27462
27765
|
"--import"
|
|
@@ -27481,13 +27784,13 @@ async function runImport(godotPath, projectPath) {
|
|
|
27481
27784
|
}
|
|
27482
27785
|
}
|
|
27483
27786
|
async function runOperation(engine, operation, params, projectPath) {
|
|
27484
|
-
const paramsDir = mkdtempSync(
|
|
27485
|
-
const paramsFile =
|
|
27486
|
-
|
|
27787
|
+
const paramsDir = mkdtempSync(join11(tmpdir3(), "gdharness-params-"));
|
|
27788
|
+
const paramsFile = join11(paramsDir, `${operation}.json`);
|
|
27789
|
+
writeFileSync7(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
27487
27790
|
const args = [
|
|
27488
27791
|
"--headless",
|
|
27489
27792
|
"--log-file",
|
|
27490
|
-
|
|
27793
|
+
join11(paramsDir, "engine.log"),
|
|
27491
27794
|
"--path",
|
|
27492
27795
|
projectPath,
|
|
27493
27796
|
"--script",
|
|
@@ -27916,7 +28219,7 @@ function orphansPrinted(printed) {
|
|
|
27916
28219
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
27917
28220
|
import { readFile, realpath } from "node:fs/promises";
|
|
27918
28221
|
import { createConnection as createConnection3 } from "node:net";
|
|
27919
|
-
import { dirname as
|
|
28222
|
+
import { dirname as dirname7, resolve as resolve3 } from "node:path";
|
|
27920
28223
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
27921
28224
|
var DEFAULT_LSP_PORT = 6005;
|
|
27922
28225
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
@@ -28046,7 +28349,7 @@ class GodotLSPClient {
|
|
|
28046
28349
|
if (this.initialized) {
|
|
28047
28350
|
return;
|
|
28048
28351
|
}
|
|
28049
|
-
const rootPath = this.rootPath ??
|
|
28352
|
+
const rootPath = this.rootPath ?? dirname7(resolve3(filePath));
|
|
28050
28353
|
await this.initialize(rootPath);
|
|
28051
28354
|
}
|
|
28052
28355
|
async sendRequest(method, params) {
|
|
@@ -28482,14 +28785,14 @@ function secondsFromClock(printed) {
|
|
|
28482
28785
|
}
|
|
28483
28786
|
|
|
28484
28787
|
// src/project-scan.ts
|
|
28485
|
-
import { existsSync as
|
|
28486
|
-
import { join as
|
|
28788
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
|
|
28789
|
+
import { join as join12 } from "node:path";
|
|
28487
28790
|
var SKIPPED = new Set(["node_modules"]);
|
|
28488
28791
|
function skipped(name) {
|
|
28489
28792
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
28490
28793
|
}
|
|
28491
28794
|
function steppedOver(directory) {
|
|
28492
|
-
return
|
|
28795
|
+
return existsSync8(join12(directory, ".gdignore"));
|
|
28493
28796
|
}
|
|
28494
28797
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
28495
28798
|
function projectStructure(projectPath) {
|
|
@@ -28503,7 +28806,7 @@ function projectStructure(projectPath) {
|
|
|
28503
28806
|
continue;
|
|
28504
28807
|
}
|
|
28505
28808
|
if (entry.isDirectory()) {
|
|
28506
|
-
visit(
|
|
28809
|
+
visit(join12(directory, entry.name));
|
|
28507
28810
|
} else if (entry.isFile()) {
|
|
28508
28811
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
28509
28812
|
if (extension === "tscn") {
|
|
@@ -28534,14 +28837,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
28534
28837
|
}
|
|
28535
28838
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
28536
28839
|
if (entry.isDirectory()) {
|
|
28537
|
-
visit(
|
|
28840
|
+
visit(join12(directory, entry.name), spelled);
|
|
28538
28841
|
continue;
|
|
28539
28842
|
}
|
|
28540
28843
|
if (!entry.isFile()) {
|
|
28541
28844
|
continue;
|
|
28542
28845
|
}
|
|
28543
28846
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
28544
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
28847
|
+
if (CARRIES_A_UID.has(extension) && !existsSync8(join12(directory, `${entry.name}.uid`))) {
|
|
28545
28848
|
found.push(spelled);
|
|
28546
28849
|
}
|
|
28547
28850
|
}
|
|
@@ -28576,7 +28879,7 @@ function searchProject(projectPath, options) {
|
|
|
28576
28879
|
if (skipped(entry.name)) {
|
|
28577
28880
|
continue;
|
|
28578
28881
|
}
|
|
28579
|
-
const entryPath =
|
|
28882
|
+
const entryPath = join12(directory, entry.name);
|
|
28580
28883
|
if (entry.isDirectory()) {
|
|
28581
28884
|
visit(entryPath);
|
|
28582
28885
|
continue;
|
|
@@ -28587,7 +28890,7 @@ function searchProject(projectPath, options) {
|
|
|
28587
28890
|
}
|
|
28588
28891
|
result.summary.files_searched += 1;
|
|
28589
28892
|
const matches = [];
|
|
28590
|
-
for (const [index, line] of
|
|
28893
|
+
for (const [index, line] of readFileSync11(entryPath, "utf8").split(`
|
|
28591
28894
|
`).entries()) {
|
|
28592
28895
|
if (full()) {
|
|
28593
28896
|
break;
|
|
@@ -28610,7 +28913,7 @@ function searchProject(projectPath, options) {
|
|
|
28610
28913
|
}
|
|
28611
28914
|
|
|
28612
28915
|
// src/resources.ts
|
|
28613
|
-
import { readFileSync as
|
|
28916
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
28614
28917
|
import { extname, resolve as resolve4 } from "node:path";
|
|
28615
28918
|
var STATIC_RESOURCES = [
|
|
28616
28919
|
{
|
|
@@ -28805,7 +29108,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
28805
29108
|
const parsedUri = parseGodotUri(uri);
|
|
28806
29109
|
if (parsedUri.kind === "project-info") {
|
|
28807
29110
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
28808
|
-
const rawProject =
|
|
29111
|
+
const rawProject = readFileSync12(projectFilePath, "utf-8");
|
|
28809
29112
|
const parsedProject = parseProjectGodot(rawProject);
|
|
28810
29113
|
return {
|
|
28811
29114
|
mimeType: "application/json",
|
|
@@ -28814,7 +29117,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
28814
29117
|
}
|
|
28815
29118
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
28816
29119
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
28817
|
-
const text =
|
|
29120
|
+
const text = readFileSync12(filePath, "utf-8");
|
|
28818
29121
|
return {
|
|
28819
29122
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
28820
29123
|
text
|
|
@@ -28851,16 +29154,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
28851
29154
|
}
|
|
28852
29155
|
|
|
28853
29156
|
// src/restart-note.ts
|
|
28854
|
-
import { mkdirSync as
|
|
28855
|
-
import { dirname as
|
|
29157
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
29158
|
+
import { dirname as dirname8, join as join13 } from "node:path";
|
|
28856
29159
|
function restartNotePath(projectPath) {
|
|
28857
|
-
return
|
|
29160
|
+
return join13(projectPath, ".godot", "gdharness-restart.json");
|
|
28858
29161
|
}
|
|
28859
29162
|
function noteRestartBegun(note) {
|
|
28860
29163
|
const path = restartNotePath(note.projectPath);
|
|
28861
29164
|
try {
|
|
28862
|
-
|
|
28863
|
-
|
|
29165
|
+
mkdirSync7(dirname8(path), { recursive: true });
|
|
29166
|
+
writeFileSync8(path, `${JSON.stringify(note, null, 2)}
|
|
28864
29167
|
`, "utf8");
|
|
28865
29168
|
} catch (error) {
|
|
28866
29169
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -28868,7 +29171,7 @@ function noteRestartBegun(note) {
|
|
|
28868
29171
|
}
|
|
28869
29172
|
function restartOwed(projectPath) {
|
|
28870
29173
|
try {
|
|
28871
|
-
const parsed = JSON.parse(
|
|
29174
|
+
const parsed = JSON.parse(readFileSync13(restartNotePath(projectPath), "utf8"));
|
|
28872
29175
|
if (typeof parsed !== "object" || parsed === null) {
|
|
28873
29176
|
return null;
|
|
28874
29177
|
}
|
|
@@ -28883,7 +29186,7 @@ function restartOwed(projectPath) {
|
|
|
28883
29186
|
}
|
|
28884
29187
|
function restartSettled(projectPath) {
|
|
28885
29188
|
try {
|
|
28886
|
-
|
|
29189
|
+
rmSync5(restartNotePath(projectPath), { force: true });
|
|
28887
29190
|
} catch (error) {
|
|
28888
29191
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
28889
29192
|
}
|
|
@@ -28891,36 +29194,36 @@ function restartSettled(projectPath) {
|
|
|
28891
29194
|
|
|
28892
29195
|
// src/run-record.ts
|
|
28893
29196
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
28894
|
-
import { mkdirSync as
|
|
28895
|
-
import { basename as basename2, join as
|
|
29197
|
+
import { mkdirSync as mkdirSync8, openSync as openSync2, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync as rmSync6, statSync as statSync5, writeFileSync as writeFileSync9 } from "node:fs";
|
|
29198
|
+
import { basename as basename2, join as join14 } from "node:path";
|
|
28896
29199
|
function runsDirectory() {
|
|
28897
|
-
return
|
|
29200
|
+
return join14(runtimeDirectory(), "runs");
|
|
28898
29201
|
}
|
|
28899
29202
|
function recordPath() {
|
|
28900
|
-
return
|
|
29203
|
+
return join14(runsDirectory(), "run.json");
|
|
28901
29204
|
}
|
|
28902
29205
|
function recordPaths() {
|
|
28903
|
-
return runtimeDirectories().map((directory) =>
|
|
29206
|
+
return runtimeDirectories().map((directory) => join14(directory, "runs", "run.json"));
|
|
28904
29207
|
}
|
|
28905
29208
|
function openTranscript(startedAt) {
|
|
28906
|
-
|
|
28907
|
-
const path =
|
|
28908
|
-
return { path, fd:
|
|
29209
|
+
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29210
|
+
const path = join14(runsDirectory(), `run-${startedAt}.log`);
|
|
29211
|
+
return { path, fd: openSync2(path, "a") };
|
|
28909
29212
|
}
|
|
28910
29213
|
function writeRunRecord(record) {
|
|
28911
|
-
|
|
28912
|
-
|
|
29214
|
+
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29215
|
+
writeFileSync9(recordPath(), JSON.stringify(record, null, 2), "utf8");
|
|
28913
29216
|
}
|
|
28914
29217
|
function editorNotePath() {
|
|
28915
|
-
return
|
|
29218
|
+
return join14(runsDirectory(), "editor-run.json");
|
|
28916
29219
|
}
|
|
28917
29220
|
function writeEditorRunNote(note) {
|
|
28918
|
-
|
|
28919
|
-
|
|
29221
|
+
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29222
|
+
writeFileSync9(editorNotePath(), JSON.stringify(note, null, 2), "utf8");
|
|
28920
29223
|
}
|
|
28921
29224
|
function readEditorRunNote() {
|
|
28922
29225
|
for (const directory of runtimeDirectories()) {
|
|
28923
|
-
const note = editorNoteAt(
|
|
29226
|
+
const note = editorNoteAt(join14(directory, "runs", "editor-run.json"));
|
|
28924
29227
|
if (note !== null) {
|
|
28925
29228
|
return note;
|
|
28926
29229
|
}
|
|
@@ -28930,7 +29233,7 @@ function readEditorRunNote() {
|
|
|
28930
29233
|
function editorNoteAt(path) {
|
|
28931
29234
|
let parsed;
|
|
28932
29235
|
try {
|
|
28933
|
-
parsed = JSON.parse(
|
|
29236
|
+
parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
28934
29237
|
} catch {
|
|
28935
29238
|
return null;
|
|
28936
29239
|
}
|
|
@@ -28953,7 +29256,7 @@ function recordRunEnded(pid, exitCode) {
|
|
|
28953
29256
|
return;
|
|
28954
29257
|
}
|
|
28955
29258
|
try {
|
|
28956
|
-
|
|
29259
|
+
writeFileSync9(path, JSON.stringify({ ...record, exitCode }, null, 2), "utf8");
|
|
28957
29260
|
} catch {}
|
|
28958
29261
|
}
|
|
28959
29262
|
function readRunRecord() {
|
|
@@ -28968,7 +29271,7 @@ function readRunRecord() {
|
|
|
28968
29271
|
function recordAt(path) {
|
|
28969
29272
|
let parsed;
|
|
28970
29273
|
try {
|
|
28971
|
-
parsed = JSON.parse(
|
|
29274
|
+
parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
28972
29275
|
} catch {
|
|
28973
29276
|
return null;
|
|
28974
29277
|
}
|
|
@@ -29013,7 +29316,7 @@ function runningAs(pid) {
|
|
|
29013
29316
|
const began = startedAt(pid, null);
|
|
29014
29317
|
const when = began === null ? {} : { startedAt: began };
|
|
29015
29318
|
if (process.platform === "linux") {
|
|
29016
|
-
const raw =
|
|
29319
|
+
const raw = readFileSync14(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
29017
29320
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
29018
29321
|
}
|
|
29019
29322
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -29142,13 +29445,13 @@ function clearRunRecord(ours = () => true) {
|
|
|
29142
29445
|
continue;
|
|
29143
29446
|
}
|
|
29144
29447
|
try {
|
|
29145
|
-
|
|
29448
|
+
rmSync6(path);
|
|
29146
29449
|
} catch {}
|
|
29147
29450
|
}
|
|
29148
29451
|
}
|
|
29149
29452
|
function ageMs(path, now) {
|
|
29150
29453
|
try {
|
|
29151
|
-
return now -
|
|
29454
|
+
return now - statSync5(path).mtimeMs;
|
|
29152
29455
|
} catch {
|
|
29153
29456
|
return 0;
|
|
29154
29457
|
}
|
|
@@ -29166,31 +29469,83 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29166
29469
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
29167
29470
|
continue;
|
|
29168
29471
|
}
|
|
29169
|
-
const path =
|
|
29472
|
+
const path = join14(directory, entry);
|
|
29170
29473
|
if (path === keep || ageMs(path, now) < keepMs) {
|
|
29171
29474
|
continue;
|
|
29172
29475
|
}
|
|
29173
29476
|
try {
|
|
29174
|
-
|
|
29477
|
+
rmSync6(path);
|
|
29175
29478
|
} catch {}
|
|
29176
29479
|
}
|
|
29177
29480
|
}
|
|
29178
29481
|
|
|
29179
29482
|
// src/setup.ts
|
|
29180
|
-
import {
|
|
29181
|
-
import {
|
|
29483
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29484
|
+
import {
|
|
29485
|
+
cpSync,
|
|
29486
|
+
existsSync as existsSync9,
|
|
29487
|
+
mkdirSync as mkdirSync9,
|
|
29488
|
+
readdirSync as readdirSync6,
|
|
29489
|
+
readFileSync as readFileSync15,
|
|
29490
|
+
rmSync as rmSync7,
|
|
29491
|
+
statSync as statSync6,
|
|
29492
|
+
writeFileSync as writeFileSync10
|
|
29493
|
+
} from "node:fs";
|
|
29494
|
+
import { dirname as dirname9, join as join15 } from "node:path";
|
|
29495
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29182
29496
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29497
|
+
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
29183
29498
|
var RUNTIME_AUTOLOAD = {
|
|
29184
29499
|
name: "GdharnessRuntime",
|
|
29185
29500
|
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
29186
29501
|
};
|
|
29187
29502
|
var VERSION_MARKER = ".gdharness-version";
|
|
29503
|
+
var DIGEST_MARKER = ".gdharness-digest";
|
|
29504
|
+
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29505
|
+
const hash = createHash3("sha256");
|
|
29506
|
+
const walk = (directory, relative) => {
|
|
29507
|
+
for (const name of readdirSync6(directory).sort()) {
|
|
29508
|
+
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
29509
|
+
continue;
|
|
29510
|
+
}
|
|
29511
|
+
const path = join15(directory, name);
|
|
29512
|
+
const inside = `${relative}/${name}`;
|
|
29513
|
+
if (statSync6(path).isDirectory()) {
|
|
29514
|
+
walk(path, inside);
|
|
29515
|
+
} else {
|
|
29516
|
+
hash.update(`${inside}
|
|
29517
|
+
`);
|
|
29518
|
+
hash.update(readFileSync15(path));
|
|
29519
|
+
hash.update(`
|
|
29520
|
+
`);
|
|
29521
|
+
}
|
|
29522
|
+
}
|
|
29523
|
+
};
|
|
29524
|
+
for (const name of EDITOR_PLUGINS) {
|
|
29525
|
+
walk(join15(from, name), name);
|
|
29526
|
+
}
|
|
29527
|
+
return hash.digest("hex");
|
|
29528
|
+
}
|
|
29529
|
+
var shippedDigest;
|
|
29530
|
+
function shippedEditorDigest() {
|
|
29531
|
+
if (shippedDigest === undefined) {
|
|
29532
|
+
try {
|
|
29533
|
+
shippedDigest = editorAddonDigest();
|
|
29534
|
+
} catch {
|
|
29535
|
+
shippedDigest = null;
|
|
29536
|
+
}
|
|
29537
|
+
}
|
|
29538
|
+
return shippedDigest ?? undefined;
|
|
29539
|
+
}
|
|
29540
|
+
function shippedAddonsDirectory() {
|
|
29541
|
+
return join15(dirname9(fileURLToPath2(import.meta.url)), "godot", "addons");
|
|
29542
|
+
}
|
|
29188
29543
|
function installedAddonVersion(projectPath) {
|
|
29189
|
-
const marker =
|
|
29190
|
-
if (!
|
|
29544
|
+
const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
29545
|
+
if (!existsSync9(marker)) {
|
|
29191
29546
|
return null;
|
|
29192
29547
|
}
|
|
29193
|
-
const said =
|
|
29548
|
+
const said = readFileSync15(marker, "utf8").trim();
|
|
29194
29549
|
return said === "" ? null : said;
|
|
29195
29550
|
}
|
|
29196
29551
|
|
|
@@ -29211,7 +29566,7 @@ function argumentsOf(spec, op) {
|
|
|
29211
29566
|
return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
|
|
29212
29567
|
}
|
|
29213
29568
|
function theGamePicked() {
|
|
29214
|
-
return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game
|
|
29569
|
+
return "the game this server started or is playing when it is among those running, named under " + "`answeredBy` where there was a choice; else the only game there is; else a refusal naming " + "every game and which argument tells them apart, `pid` when they are all one project and " + "`projectPath` when they are not";
|
|
29215
29570
|
}
|
|
29216
29571
|
var PROJECT_PATH = {
|
|
29217
29572
|
type: "string",
|
|
@@ -29878,7 +30233,7 @@ var TOOL_SPECS = [
|
|
|
29878
30233
|
},
|
|
29879
30234
|
{
|
|
29880
30235
|
name: "editor_output",
|
|
29881
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering.",
|
|
30236
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
29882
30237
|
parameters: {
|
|
29883
30238
|
severity: {
|
|
29884
30239
|
type: "string",
|
|
@@ -29886,10 +30241,12 @@ var TOOL_SPECS = [
|
|
|
29886
30241
|
description: "The least severe entry to include. Default info, which is everything."
|
|
29887
30242
|
},
|
|
29888
30243
|
sinceLastCall: {
|
|
30244
|
+
ops: ["run"],
|
|
29889
30245
|
type: "boolean",
|
|
29890
30246
|
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."
|
|
29891
30247
|
},
|
|
29892
30248
|
cpu: {
|
|
30249
|
+
ops: ["run"],
|
|
29893
30250
|
type: "boolean",
|
|
29894
30251
|
description: "Also answer with cpuSeconds, the processor time the run has used. Default false, because asking costs a subprocess. Worth it when elapsedMs is climbing and nothing is being printed: processor time standing still is a run that has stopped doing anything, rather than one that is slow."
|
|
29895
30252
|
},
|
|
@@ -29900,8 +30257,24 @@ var TOOL_SPECS = [
|
|
|
29900
30257
|
limit: {
|
|
29901
30258
|
type: "number",
|
|
29902
30259
|
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
30260
|
+
},
|
|
30261
|
+
before: {
|
|
30262
|
+
ops: ["editor"],
|
|
30263
|
+
type: "number",
|
|
30264
|
+
description: "How many lines above a repeated group to carry with it, counting only lines that are not themselves part of a group. Default 3. The line that explains a wall of identical errors is the last ordinary line above it, so this walks past the wall rather than a fixed distance back."
|
|
29903
30265
|
}
|
|
29904
30266
|
},
|
|
30267
|
+
operations: {
|
|
30268
|
+
run: {
|
|
30269
|
+
summary: "What the game a run is playing has printed, which is everything described above",
|
|
30270
|
+
requires: []
|
|
30271
|
+
},
|
|
30272
|
+
editor: {
|
|
30273
|
+
summary: "What the connected editor itself has printed, from its first line, with repeated messages grouped and the ordinary lines above each group carried with it",
|
|
30274
|
+
requires: []
|
|
30275
|
+
}
|
|
30276
|
+
},
|
|
30277
|
+
defaultOperation: "run",
|
|
29905
30278
|
requires: []
|
|
29906
30279
|
},
|
|
29907
30280
|
{
|
|
@@ -30360,7 +30733,7 @@ function buildToolDefinitions() {
|
|
|
30360
30733
|
var UPDATE_NOTICE_EVERY = 500;
|
|
30361
30734
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
30362
30735
|
var run5 = promisify5(execFile5);
|
|
30363
|
-
var __dirname2 =
|
|
30736
|
+
var __dirname2 = dirname10(fileURLToPath3(import.meta.url));
|
|
30364
30737
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
30365
30738
|
var HOLD_PING_MS = 2000;
|
|
30366
30739
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -30473,6 +30846,7 @@ function endedToStartThis(ended) {
|
|
|
30473
30846
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
30474
30847
|
}
|
|
30475
30848
|
var LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
30849
|
+
var MOST_GROUPS = 20;
|
|
30476
30850
|
function runtimeVerdict(endpoint, after) {
|
|
30477
30851
|
if (endpoint !== null) {
|
|
30478
30852
|
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
@@ -30647,13 +31021,13 @@ function aboveTheRunner(entry) {
|
|
|
30647
31021
|
}
|
|
30648
31022
|
function elsewhereIn(projectPath, asked) {
|
|
30649
31023
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
30650
|
-
if (wanted === "" ||
|
|
31024
|
+
if (wanted === "" || existsSync10(join16(projectPath, wanted))) {
|
|
30651
31025
|
return "";
|
|
30652
31026
|
}
|
|
30653
31027
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
30654
31028
|
let near = [];
|
|
30655
31029
|
try {
|
|
30656
|
-
near =
|
|
31030
|
+
near = readdirSync7(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
|
|
30657
31031
|
} catch {
|
|
30658
31032
|
return "";
|
|
30659
31033
|
}
|
|
@@ -30661,7 +31035,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
30661
31035
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
30662
31036
|
}
|
|
30663
31037
|
function hasMainScene(projectFile) {
|
|
30664
|
-
const scene = parseProjectGodot(
|
|
31038
|
+
const scene = parseProjectGodot(readFileSync16(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
30665
31039
|
return typeof scene === "string" && scene !== "";
|
|
30666
31040
|
}
|
|
30667
31041
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -30669,13 +31043,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
30669
31043
|
return false;
|
|
30670
31044
|
}
|
|
30671
31045
|
try {
|
|
30672
|
-
return
|
|
31046
|
+
return readFileSync16(join16(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
30673
31047
|
} catch {
|
|
30674
31048
|
return false;
|
|
30675
31049
|
}
|
|
30676
31050
|
}
|
|
30677
31051
|
function editorPlaysHeadless(projectFile) {
|
|
30678
|
-
const runArgs = parseProjectGodot(
|
|
31052
|
+
const runArgs = parseProjectGodot(readFileSync16(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
30679
31053
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
30680
31054
|
}
|
|
30681
31055
|
function camelCased(params) {
|
|
@@ -30694,7 +31068,7 @@ function needsItsOwnProcess(asked) {
|
|
|
30694
31068
|
class GodotServer {
|
|
30695
31069
|
mcp;
|
|
30696
31070
|
locator = new GodotLocator;
|
|
30697
|
-
operationsScript =
|
|
31071
|
+
operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
30698
31072
|
godotBridge;
|
|
30699
31073
|
tools = buildToolDefinitions();
|
|
30700
31074
|
activeProcess = null;
|
|
@@ -31359,8 +31733,8 @@ class GodotServer {
|
|
|
31359
31733
|
if (path === undefined) {
|
|
31360
31734
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
31361
31735
|
}
|
|
31362
|
-
const file =
|
|
31363
|
-
if (!
|
|
31736
|
+
const file = join16(path, "project.godot");
|
|
31737
|
+
if (!existsSync10(file)) {
|
|
31364
31738
|
return {
|
|
31365
31739
|
ok: false,
|
|
31366
31740
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -31628,7 +32002,7 @@ class GodotServer {
|
|
|
31628
32002
|
if (!engine.ok) {
|
|
31629
32003
|
return engine.response;
|
|
31630
32004
|
}
|
|
31631
|
-
const application = parseProjectGodot(
|
|
32005
|
+
const application = parseProjectGodot(readFileSync16(project.value.file, "utf8"))["application"] ?? {};
|
|
31632
32006
|
const name = application["config/name"];
|
|
31633
32007
|
const mainScene = application["run/main_scene"];
|
|
31634
32008
|
const info = {
|
|
@@ -31689,18 +32063,18 @@ class GodotServer {
|
|
|
31689
32063
|
if (!engine.ok) {
|
|
31690
32064
|
return engine.response;
|
|
31691
32065
|
}
|
|
31692
|
-
const folder =
|
|
32066
|
+
const folder = dirname10(output.absolutePath);
|
|
31693
32067
|
try {
|
|
31694
|
-
|
|
32068
|
+
mkdirSync10(folder, { recursive: true });
|
|
31695
32069
|
} catch (error) {
|
|
31696
|
-
return this.createErrorResponse(`Cannot create ${
|
|
32070
|
+
return this.createErrorResponse(`Cannot create ${dirname10(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
31697
32071
|
}
|
|
31698
32072
|
const debug = readBoolean(args, "debug") ?? false;
|
|
31699
|
-
const exportLogs = mkdtempSync2(
|
|
32073
|
+
const exportLogs = mkdtempSync2(join16(tmpdir4(), "gdharness-export-"));
|
|
31700
32074
|
const exportArgs = [
|
|
31701
32075
|
"--headless",
|
|
31702
32076
|
"--log-file",
|
|
31703
|
-
|
|
32077
|
+
join16(exportLogs, "engine.log"),
|
|
31704
32078
|
"--path",
|
|
31705
32079
|
project.value.path,
|
|
31706
32080
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -31728,7 +32102,7 @@ class GodotServer {
|
|
|
31728
32102
|
log.finish();
|
|
31729
32103
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
31730
32104
|
const verdict = {
|
|
31731
|
-
exported: exitCode === 0 && log.count("error") === 0 &&
|
|
32105
|
+
exported: exitCode === 0 && log.count("error") === 0 && existsSync10(output.absolutePath),
|
|
31732
32106
|
preset,
|
|
31733
32107
|
outputPath: output.relativePath,
|
|
31734
32108
|
debug,
|
|
@@ -31758,7 +32132,7 @@ class GodotServer {
|
|
|
31758
32132
|
return contained.response;
|
|
31759
32133
|
}
|
|
31760
32134
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
31761
|
-
if (!
|
|
32135
|
+
if (!existsSync10(join16(project.value.path, runner))) {
|
|
31762
32136
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
31763
32137
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
31764
32138
|
]);
|
|
@@ -31793,7 +32167,7 @@ class GodotServer {
|
|
|
31793
32167
|
];
|
|
31794
32168
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
31795
32169
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
31796
|
-
const userData = mkdtempSync2(
|
|
32170
|
+
const userData = mkdtempSync2(join16(tmpdir4(), "gdharness-tests-"));
|
|
31797
32171
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
31798
32172
|
const hung = await new Promise((resolve) => {
|
|
31799
32173
|
const timer = setTimeout(() => {
|
|
@@ -31809,21 +32183,21 @@ class GodotServer {
|
|
|
31809
32183
|
resolve(false);
|
|
31810
32184
|
});
|
|
31811
32185
|
});
|
|
31812
|
-
const reportsDir =
|
|
32186
|
+
const reportsDir = join16(project.value.path, ".godot", ...ours.split("/"));
|
|
31813
32187
|
let report = null;
|
|
31814
32188
|
let reportProblem = null;
|
|
31815
32189
|
try {
|
|
31816
|
-
const written =
|
|
32190
|
+
const written = existsSync10(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
31817
32191
|
const newest = written.at(-1);
|
|
31818
32192
|
if (newest !== undefined) {
|
|
31819
|
-
report = parseJUnit(
|
|
32193
|
+
report = parseJUnit(readFileSync16(join16(reportsDir, newest, "results.xml"), "utf8"));
|
|
31820
32194
|
}
|
|
31821
32195
|
} catch (error) {
|
|
31822
32196
|
reportProblem = errorMessage(error);
|
|
31823
32197
|
} finally {
|
|
31824
32198
|
discard(reportsDir, userData);
|
|
31825
32199
|
try {
|
|
31826
|
-
rmdirSync(
|
|
32200
|
+
rmdirSync(dirname10(reportsDir));
|
|
31827
32201
|
} catch {}
|
|
31828
32202
|
}
|
|
31829
32203
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -31942,7 +32316,7 @@ class GodotServer {
|
|
|
31942
32316
|
warnings: diagnostics.length - errors,
|
|
31943
32317
|
diagnostics,
|
|
31944
32318
|
...disagrees
|
|
31945
|
-
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
32319
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
31946
32320
|
}
|
|
31947
32321
|
whereTheFileDisagrees(diagnostics, args) {
|
|
31948
32322
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -31955,11 +32329,11 @@ class GodotServer {
|
|
|
31955
32329
|
const cached = cachedClasses(projectPath);
|
|
31956
32330
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
31957
32331
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
31958
|
-
if (!contained.ok || !
|
|
32332
|
+
if (!contained.ok || !existsSync10(contained.absolutePath)) {
|
|
31959
32333
|
return null;
|
|
31960
32334
|
}
|
|
31961
32335
|
try {
|
|
31962
|
-
return
|
|
32336
|
+
return readFileSync16(contained.absolutePath, "utf8");
|
|
31963
32337
|
} catch {
|
|
31964
32338
|
return null;
|
|
31965
32339
|
}
|
|
@@ -32009,11 +32383,11 @@ class GodotServer {
|
|
|
32009
32383
|
}
|
|
32010
32384
|
sourceOfClass(projectPath, resourcePath) {
|
|
32011
32385
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
32012
|
-
if (!contained.ok || !
|
|
32386
|
+
if (!contained.ok || !existsSync10(contained.absolutePath)) {
|
|
32013
32387
|
return null;
|
|
32014
32388
|
}
|
|
32015
32389
|
try {
|
|
32016
|
-
return
|
|
32390
|
+
return readFileSync16(contained.absolutePath, "utf8");
|
|
32017
32391
|
} catch {
|
|
32018
32392
|
return null;
|
|
32019
32393
|
}
|
|
@@ -32129,7 +32503,7 @@ class GodotServer {
|
|
|
32129
32503
|
getEditorStatusPayload() {
|
|
32130
32504
|
const status = this.godotBridge.getStatus();
|
|
32131
32505
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
32132
|
-
const stale = status.connected && status.addonVersion
|
|
32506
|
+
const stale = status.connected && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
32133
32507
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
32134
32508
|
return {
|
|
32135
32509
|
...status,
|
|
@@ -32146,7 +32520,7 @@ class GodotServer {
|
|
|
32146
32520
|
awaitingLaunchedEditor: status.connected || !this.launchedEditorIsUp() ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
32147
32521
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
32148
32522
|
startupError: this.bridgeStartupError,
|
|
32149
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
|
|
32523
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
32150
32524
|
...this.breakpointsAtRisk(),
|
|
32151
32525
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
32152
32526
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -32242,7 +32616,7 @@ class GodotServer {
|
|
|
32242
32616
|
return null;
|
|
32243
32617
|
}
|
|
32244
32618
|
try {
|
|
32245
|
-
return settingKeys(
|
|
32619
|
+
return settingKeys(readFileSync16(join16(projectPath, "project.godot"), "utf8"));
|
|
32246
32620
|
} catch {
|
|
32247
32621
|
return null;
|
|
32248
32622
|
}
|
|
@@ -32278,8 +32652,8 @@ class GodotServer {
|
|
|
32278
32652
|
editorPid: now.editorPid,
|
|
32279
32653
|
addonVersion: now.addonVersion,
|
|
32280
32654
|
serverVersion: SERVER_VERSION,
|
|
32281
|
-
addonIsStale: now.addonVersion
|
|
32282
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
|
|
32655
|
+
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
32656
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
32283
32657
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
32284
32658
|
tookMs: Date.now() - began
|
|
32285
32659
|
});
|
|
@@ -32372,7 +32746,8 @@ class GodotServer {
|
|
|
32372
32746
|
});
|
|
32373
32747
|
}
|
|
32374
32748
|
async openAnEditor(engine, projectPath, ports) {
|
|
32375
|
-
|
|
32749
|
+
clearEditorLog(projectPath);
|
|
32750
|
+
const editor = spawn(engine, editorArguments(projectPath, ports, editorLogPath(projectPath)), {
|
|
32376
32751
|
stdio: "ignore",
|
|
32377
32752
|
detached: true,
|
|
32378
32753
|
env: {
|
|
@@ -32383,6 +32758,9 @@ class GodotServer {
|
|
|
32383
32758
|
[OPENED_BY_A_SERVER]: "1"
|
|
32384
32759
|
}
|
|
32385
32760
|
});
|
|
32761
|
+
if (editor.pid !== undefined) {
|
|
32762
|
+
writeEditorLogNote(projectPath, editor.pid);
|
|
32763
|
+
}
|
|
32386
32764
|
const started = await new Promise((resolve) => {
|
|
32387
32765
|
editor.once("spawn", () => {
|
|
32388
32766
|
resolve(null);
|
|
@@ -32489,7 +32867,7 @@ class GodotServer {
|
|
|
32489
32867
|
}
|
|
32490
32868
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
32491
32869
|
const budgetMs = budget.ms;
|
|
32492
|
-
if (!
|
|
32870
|
+
if (!existsSync10(join16(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
32493
32871
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
32494
32872
|
}
|
|
32495
32873
|
let asking = null;
|
|
@@ -32552,7 +32930,7 @@ class GodotServer {
|
|
|
32552
32930
|
}
|
|
32553
32931
|
let announcedAt;
|
|
32554
32932
|
try {
|
|
32555
|
-
announcedAt =
|
|
32933
|
+
announcedAt = statSync7(endpoint.file).mtimeMs;
|
|
32556
32934
|
} catch {
|
|
32557
32935
|
return;
|
|
32558
32936
|
}
|
|
@@ -32588,7 +32966,7 @@ class GodotServer {
|
|
|
32588
32966
|
}
|
|
32589
32967
|
const startedAt = Date.now();
|
|
32590
32968
|
const transcript = openTranscript(startedAt);
|
|
32591
|
-
|
|
32969
|
+
closeSync2(transcript.fd);
|
|
32592
32970
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
32593
32971
|
const played = {
|
|
32594
32972
|
process: null,
|
|
@@ -32655,7 +33033,7 @@ class GodotServer {
|
|
|
32655
33033
|
}
|
|
32656
33034
|
let handle;
|
|
32657
33035
|
try {
|
|
32658
|
-
handle =
|
|
33036
|
+
handle = openSync3(game.errorReport, "r");
|
|
32659
33037
|
} catch {
|
|
32660
33038
|
return;
|
|
32661
33039
|
}
|
|
@@ -32666,7 +33044,7 @@ class GodotServer {
|
|
|
32666
33044
|
return;
|
|
32667
33045
|
}
|
|
32668
33046
|
const buffer = Buffer.alloc(size - from);
|
|
32669
|
-
const read =
|
|
33047
|
+
const read = readSync2(handle, buffer, 0, buffer.length, from);
|
|
32670
33048
|
game.errorReportOffset = from + read;
|
|
32671
33049
|
const reported = buffer.subarray(0, read);
|
|
32672
33050
|
if (game.transcript !== null) {
|
|
@@ -32675,7 +33053,7 @@ class GodotServer {
|
|
|
32675
33053
|
game.log.append("stderr", reported);
|
|
32676
33054
|
}
|
|
32677
33055
|
} finally {
|
|
32678
|
-
|
|
33056
|
+
closeSync2(handle);
|
|
32679
33057
|
}
|
|
32680
33058
|
}
|
|
32681
33059
|
recordWhatItBrokeOn(game) {
|
|
@@ -32769,7 +33147,7 @@ class GodotServer {
|
|
|
32769
33147
|
...env ? { env } : {}
|
|
32770
33148
|
});
|
|
32771
33149
|
} finally {
|
|
32772
|
-
|
|
33150
|
+
closeSync2(transcript.fd);
|
|
32773
33151
|
}
|
|
32774
33152
|
child.unref();
|
|
32775
33153
|
const log = new GameLog;
|
|
@@ -32816,7 +33194,7 @@ class GodotServer {
|
|
|
32816
33194
|
}
|
|
32817
33195
|
let handle;
|
|
32818
33196
|
try {
|
|
32819
|
-
handle =
|
|
33197
|
+
handle = openSync3(game.transcript, "r");
|
|
32820
33198
|
} catch {
|
|
32821
33199
|
return;
|
|
32822
33200
|
}
|
|
@@ -32826,11 +33204,11 @@ class GodotServer {
|
|
|
32826
33204
|
return;
|
|
32827
33205
|
}
|
|
32828
33206
|
const buffer = Buffer.alloc(size - game.readOffset);
|
|
32829
|
-
const read =
|
|
33207
|
+
const read = readSync2(handle, buffer, 0, buffer.length, game.readOffset);
|
|
32830
33208
|
game.readOffset += read;
|
|
32831
33209
|
game.log.append("transcript", buffer.subarray(0, read));
|
|
32832
33210
|
} finally {
|
|
32833
|
-
|
|
33211
|
+
closeSync2(handle);
|
|
32834
33212
|
}
|
|
32835
33213
|
}
|
|
32836
33214
|
async pickUpWhatTheEditorIsPlaying() {
|
|
@@ -33028,7 +33406,7 @@ class GodotServer {
|
|
|
33028
33406
|
}
|
|
33029
33407
|
const unreadable = this.aRuntimeOfOursIsTooFarOff();
|
|
33030
33408
|
if (unreadable !== null) {
|
|
33031
|
-
const behind = unreadable
|
|
33409
|
+
const behind = whichHalfIsBehind([unreadable]);
|
|
33032
33410
|
return `A game is running for ${unreadable.project.path}, pid ${unreadable.pid}, announced in protocol` + ` ${unreadable.protocol}, which this server does not speak: it speaks ${RUNTIME_PROTOCOL}, so ${behind}.` + " Until then neither editor_run nor the runtime_* tools can reach it." + " A run started here is a separate process rather than a replacement, so starting one leaves" + " this game running as well.";
|
|
33033
33411
|
}
|
|
33034
33412
|
const record = readRunRecord();
|
|
@@ -33153,7 +33531,66 @@ class GodotServer {
|
|
|
33153
33531
|
}
|
|
33154
33532
|
return runIsUp(run, said);
|
|
33155
33533
|
}
|
|
33534
|
+
handleEditorConsole(args) {
|
|
33535
|
+
const status = this.godotBridge.getStatus();
|
|
33536
|
+
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
33537
|
+
if (projectPath === undefined) {
|
|
33538
|
+
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
33539
|
+
"editor_status says whether one is connected and whether it may yet be",
|
|
33540
|
+
"editor_launch opens one, and an editor opened here has its console captured"
|
|
33541
|
+
]);
|
|
33542
|
+
}
|
|
33543
|
+
const launched = status.connected ? null : this.launchedEditor?.pid ?? null;
|
|
33544
|
+
if (!status.connected && launched === null) {
|
|
33545
|
+
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
33546
|
+
"editor_status says whether one is connected and whether it may yet be",
|
|
33547
|
+
"editor_launch opens one, and an editor opened here has its console captured"
|
|
33548
|
+
]);
|
|
33549
|
+
}
|
|
33550
|
+
const editorPid = status.connected ? status.editorPid ?? null : launched;
|
|
33551
|
+
const console2 = editorConsole(projectPath, editorPid, status.connected ? status.openedByAServer === true : true);
|
|
33552
|
+
if ("kind" in console2) {
|
|
33553
|
+
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
33554
|
+
"editor_launch restart replaces this editor with one whose console is captured",
|
|
33555
|
+
"editor_output with no op answers about the game a run is playing, which is a different log"
|
|
33556
|
+
]);
|
|
33557
|
+
}
|
|
33558
|
+
const severity = readString(args, "severity");
|
|
33559
|
+
const selected = console2.log.select({
|
|
33560
|
+
severity: severity === "error" || severity === "warning" ? severity : "info",
|
|
33561
|
+
sinceLastCall: false,
|
|
33562
|
+
contains: readNonEmptyString(args, "contains"),
|
|
33563
|
+
limit: readPositiveNumber(args, "limit") ?? 200
|
|
33564
|
+
});
|
|
33565
|
+
const everything = console2.log.everything();
|
|
33566
|
+
const grouped = bursts(everything, readPositiveNumber(args, "before") ?? 3);
|
|
33567
|
+
const shown = grouped.slice(0, MOST_GROUPS);
|
|
33568
|
+
return this.jsonTextResponse({
|
|
33569
|
+
editorPid: editorPid ?? undefined,
|
|
33570
|
+
projectPath,
|
|
33571
|
+
...status.connected ? {} : this.launchedEditorIsUp() ? { stillConnecting: true } : { exitedBeforeConnecting: true },
|
|
33572
|
+
capturedIn: console2.path,
|
|
33573
|
+
...console2.of === undefined ? {} : {
|
|
33574
|
+
readFromTheStart: console2.readBytes,
|
|
33575
|
+
consoleBytes: console2.of,
|
|
33576
|
+
truncatedNote: `This editor's console is ${console2.of} bytes and the first ${console2.readBytes} of them were read, which is the start of the session rather than the end of it. Restart the editor to begin a fresh console.`
|
|
33577
|
+
},
|
|
33578
|
+
lines: everything.length,
|
|
33579
|
+
counts: {
|
|
33580
|
+
error: console2.log.count("error"),
|
|
33581
|
+
warning: console2.log.count("warning"),
|
|
33582
|
+
info: console2.log.count("info")
|
|
33583
|
+
},
|
|
33584
|
+
repeated: shown,
|
|
33585
|
+
moreShapes: grouped.length > shown.length ? grouped.length - shown.length : undefined,
|
|
33586
|
+
entries: forAnswer(selected.entries),
|
|
33587
|
+
omitted: selected.omitted === 0 ? undefined : selected.omitted
|
|
33588
|
+
});
|
|
33589
|
+
}
|
|
33156
33590
|
async handleGetDebugOutput(args, waitedMs) {
|
|
33591
|
+
if (readString(args, "op") === "editor") {
|
|
33592
|
+
return this.handleEditorConsole(args);
|
|
33593
|
+
}
|
|
33157
33594
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
33158
33595
|
const run = this.currentRun();
|
|
33159
33596
|
if (!run) {
|
|
@@ -33430,6 +33867,7 @@ class GodotServer {
|
|
|
33430
33867
|
}
|
|
33431
33868
|
async rebuildClassCache(projectPath) {
|
|
33432
33869
|
const before = cachedClasses(projectPath);
|
|
33870
|
+
const writtenBefore = cacheWrittenAt(projectPath);
|
|
33433
33871
|
const noted = readClassNote(projectPath);
|
|
33434
33872
|
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
33435
33873
|
if (!rebuilt.ok) {
|
|
@@ -33440,21 +33878,30 @@ class GodotServer {
|
|
|
33440
33878
|
return rebuilt;
|
|
33441
33879
|
}
|
|
33442
33880
|
const lost = noted === null || before === null ? [] : noted.rebuiltWith.filter((name) => !before.has(name) && after.has(name)).sort();
|
|
33443
|
-
const
|
|
33881
|
+
const cameBack = writtenBefore === null ? [] : declaredSince(projectPath, after, lost, writtenBefore);
|
|
33882
|
+
const droppedByTheEditor = lost.filter((name) => !cameBack.includes(name));
|
|
33883
|
+
const seenLosing = droppedByTheEditor.length > 0 && this.godotBridge.isConnected();
|
|
33444
33884
|
writeClassNote(projectPath, {
|
|
33445
33885
|
rebuiltWith: [...after.keys()].sort(),
|
|
33446
33886
|
...seenLosing ? { shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null } : noted?.shortListEditorPid === undefined ? {} : { shortListEditorPid: noted.shortListEditorPid }
|
|
33447
33887
|
});
|
|
33448
|
-
|
|
33888
|
+
const notes = [];
|
|
33889
|
+
if (seenLosing) {
|
|
33890
|
+
const them = droppedByTheEditor.length === 1 ? "it" : "them";
|
|
33891
|
+
notes.push(`The cache held ${droppedByTheEditor.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`);
|
|
33892
|
+
}
|
|
33893
|
+
if (cameBack.length > 0) {
|
|
33894
|
+
notes.push(`${cameBack.join(", ")} left the cache while ${cameBack.length === 1 ? "its script was" : "their scripts were"} gone: the script${cameBack.length === 1 ? " is" : "s are"} newer than the cache the editor last wrote, so it listed what was on disk then. editor_rescan picks ${cameBack.length === 1 ? "it" : "them"} up.`);
|
|
33895
|
+
}
|
|
33896
|
+
if (notes.length === 0) {
|
|
33449
33897
|
return rebuilt;
|
|
33450
33898
|
}
|
|
33451
|
-
const them = lost.length === 1 ? "it" : "them";
|
|
33452
33899
|
return {
|
|
33453
33900
|
...rebuilt,
|
|
33454
33901
|
payload: {
|
|
33455
33902
|
...rebuilt.payload,
|
|
33456
33903
|
lostSinceLastRebuild: lost,
|
|
33457
|
-
note:
|
|
33904
|
+
note: notes.join(" ")
|
|
33458
33905
|
}
|
|
33459
33906
|
};
|
|
33460
33907
|
}
|
|
@@ -33530,7 +33977,7 @@ class GodotServer {
|
|
|
33530
33977
|
]);
|
|
33531
33978
|
}
|
|
33532
33979
|
try {
|
|
33533
|
-
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION));
|
|
33980
|
+
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
33534
33981
|
} catch (error) {
|
|
33535
33982
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
33536
33983
|
}
|
|
@@ -33604,8 +34051,8 @@ class GodotServer {
|
|
|
33604
34051
|
]);
|
|
33605
34052
|
}
|
|
33606
34053
|
const expectsScreenshot = command === "capture_screenshot" || command === "capture_viewport";
|
|
33607
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
33608
|
-
const screenshotPath = screenshotDir ?
|
|
34054
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join16(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
34055
|
+
const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
|
|
33609
34056
|
try {
|
|
33610
34057
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs);
|
|
33611
34058
|
if (!reply.ok) {
|
|
@@ -33629,7 +34076,7 @@ class GodotServer {
|
|
|
33629
34076
|
type: "text",
|
|
33630
34077
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}`
|
|
33631
34078
|
},
|
|
33632
|
-
{ type: "image", data:
|
|
34079
|
+
{ type: "image", data: readFileSync16(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
33633
34080
|
]
|
|
33634
34081
|
};
|
|
33635
34082
|
} finally {
|