gdharness 1.1.5 → 1.1.7
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 +447 -115
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +44 -0
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +50 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +10 -3
- package/build/godot/operations/godot_operations.gd +0 -2
- package/build/godot/operations/import_pipeline.gd +226 -97
- package/build/index.js +454 -124
- package/build/keeper.js +272 -23
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10190,19 +10190,19 @@ import {
|
|
|
10190
10190
|
closeSync as closeSync2,
|
|
10191
10191
|
constants,
|
|
10192
10192
|
copyFileSync,
|
|
10193
|
-
existsSync as
|
|
10193
|
+
existsSync as existsSync11,
|
|
10194
10194
|
fstatSync,
|
|
10195
10195
|
mkdirSync as mkdirSync10,
|
|
10196
10196
|
mkdtempSync as mkdtempSync2,
|
|
10197
10197
|
openSync as openSync3,
|
|
10198
10198
|
readdirSync as readdirSync7,
|
|
10199
|
-
readFileSync as
|
|
10199
|
+
readFileSync as readFileSync17,
|
|
10200
10200
|
readSync as readSync2,
|
|
10201
10201
|
rmdirSync,
|
|
10202
|
-
statSync as
|
|
10202
|
+
statSync as statSync8
|
|
10203
10203
|
} from "node:fs";
|
|
10204
10204
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10205
|
-
import { basename as
|
|
10205
|
+
import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join17, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10206
10206
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10207
10207
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10208
10208
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -24808,9 +24808,10 @@ function runArguments(options) {
|
|
|
24808
24808
|
}
|
|
24809
24809
|
return args;
|
|
24810
24810
|
}
|
|
24811
|
-
function editorArguments(projectPath, ports, logFile) {
|
|
24811
|
+
function editorArguments(projectPath, ports, logFile, hidden) {
|
|
24812
24812
|
return [
|
|
24813
24813
|
"-e",
|
|
24814
|
+
...hidden ? ["--headless"] : [],
|
|
24814
24815
|
"--path",
|
|
24815
24816
|
projectPath,
|
|
24816
24817
|
"--lsp-port",
|
|
@@ -26748,6 +26749,9 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
26748
26749
|
}
|
|
26749
26750
|
}
|
|
26750
26751
|
|
|
26752
|
+
// src/desktop.ts
|
|
26753
|
+
var HIDDEN_DESKTOP = "gdharness";
|
|
26754
|
+
|
|
26751
26755
|
// src/dictionary.ts
|
|
26752
26756
|
function emptyRecord() {
|
|
26753
26757
|
return Object.create(null);
|
|
@@ -27921,7 +27925,6 @@ var HEADLESS_OPERATIONS = dictionary({
|
|
|
27921
27925
|
status: "get_import_status",
|
|
27922
27926
|
options: "get_import_options",
|
|
27923
27927
|
set_options: "set_import_options",
|
|
27924
|
-
reimport: "reimport_resource",
|
|
27925
27928
|
uid: "get_uid",
|
|
27926
27929
|
refresh_classes: "refresh_class_cache"
|
|
27927
27930
|
}),
|
|
@@ -27935,7 +27938,7 @@ var HEADLESS_OPERATIONS = dictionary({
|
|
|
27935
27938
|
})
|
|
27936
27939
|
});
|
|
27937
27940
|
var ENGINE_PASSES = dictionary({
|
|
27938
|
-
project_import: dictionary({ refresh_uids: "import" })
|
|
27941
|
+
project_import: dictionary({ refresh_uids: "import", reimport: "import" })
|
|
27939
27942
|
});
|
|
27940
27943
|
var EDITOR_READS = dictionary({
|
|
27941
27944
|
project_settings: dictionary({ get: "get_project_setting" })
|
|
@@ -28253,7 +28256,8 @@ function whyNoReport(printed, asked) {
|
|
|
28253
28256
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
28254
28257
|
}
|
|
28255
28258
|
var ESCAPE = String.fromCharCode(27);
|
|
28256
|
-
var
|
|
28259
|
+
var LOCATION = `'[^'\\n]*' in \\S+:\\d+`;
|
|
28260
|
+
var PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
28257
28261
|
var COLOUR = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
28258
28262
|
var SPELLED = {
|
|
28259
28263
|
"<LF>": `
|
|
@@ -28297,6 +28301,7 @@ function readDiff(printed) {
|
|
|
28297
28301
|
return { merged, actual };
|
|
28298
28302
|
}
|
|
28299
28303
|
var BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]";
|
|
28304
|
+
var VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]';
|
|
28300
28305
|
function withActualsPrinted(failed, printed) {
|
|
28301
28306
|
const diffs = [...printed.replaceAll(`\r
|
|
28302
28307
|
`, `
|
|
@@ -28309,6 +28314,7 @@ function withActualsPrinted(failed, printed) {
|
|
|
28309
28314
|
return entry;
|
|
28310
28315
|
}
|
|
28311
28316
|
let detail = entry.detail;
|
|
28317
|
+
let recovered = false;
|
|
28312
28318
|
for (const diff of diffs) {
|
|
28313
28319
|
const wrong = ` but was
|
|
28314
28320
|
'${diff.merged.replaceAll("\r", "")}'
|
|
@@ -28317,12 +28323,13 @@ function withActualsPrinted(failed, printed) {
|
|
|
28317
28323
|
detail = detail.replace(wrong, () => ` but was
|
|
28318
28324
|
'${diff.actual}'
|
|
28319
28325
|
at ${diff.at}`);
|
|
28326
|
+
recovered = true;
|
|
28320
28327
|
}
|
|
28321
28328
|
}
|
|
28322
|
-
|
|
28323
|
-
|
|
28329
|
+
if (!recovered && /Expecting:\n '[\s\S]*'\n but was\n '[\s\S]*'\n\tat /.test(detail)) {
|
|
28330
|
+
const alike = /Expecting:\n '([\s\S]*)'\n but was\n '\1'\n\tat /.test(detail);
|
|
28324
28331
|
detail = `${detail}
|
|
28325
|
-
${BOTH_SIDES_ALIKE_NOTE}`;
|
|
28332
|
+
${alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE}`;
|
|
28326
28333
|
}
|
|
28327
28334
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
28328
28335
|
});
|
|
@@ -28936,7 +28943,9 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
28936
28943
|
command: spec.command,
|
|
28937
28944
|
args: spec.args,
|
|
28938
28945
|
envChanges: changesFrom(spec.env, process.env),
|
|
28939
|
-
...spec.run === undefined ? {} : { run: spec.run }
|
|
28946
|
+
...spec.run === undefined ? {} : { run: spec.run },
|
|
28947
|
+
...spec.desktop === undefined ? {} : { desktop: spec.desktop },
|
|
28948
|
+
...spec.noActivate === true ? { noActivate: true } : {}
|
|
28940
28949
|
});
|
|
28941
28950
|
let printed = "";
|
|
28942
28951
|
let complained = "";
|
|
@@ -29121,8 +29130,28 @@ function searchProject(projectPath, options) {
|
|
|
29121
29130
|
return result;
|
|
29122
29131
|
}
|
|
29123
29132
|
|
|
29133
|
+
// src/reimport.ts
|
|
29134
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29135
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
29136
|
+
import { basename as basename2, join as join13 } from "node:path";
|
|
29137
|
+
function forceNextImport(projectPath, resourcePath) {
|
|
29138
|
+
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
29139
|
+
const sidecar = join13(projectPath, `${relative}.import`);
|
|
29140
|
+
if (!existsSync9(sidecar)) {
|
|
29141
|
+
return;
|
|
29142
|
+
}
|
|
29143
|
+
const { atime, mtimeMs } = statSync5(sidecar);
|
|
29144
|
+
writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
29145
|
+
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
29146
|
+
rmSync5(importRecord(projectPath, resourcePath), { force: true });
|
|
29147
|
+
}
|
|
29148
|
+
function importRecord(projectPath, resourcePath) {
|
|
29149
|
+
const digest = createHash3("md5").update(resourcePath).digest("hex");
|
|
29150
|
+
return join13(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
29151
|
+
}
|
|
29152
|
+
|
|
29124
29153
|
// src/resources.ts
|
|
29125
|
-
import { readFileSync as
|
|
29154
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
29126
29155
|
import { extname, resolve as resolve4 } from "node:path";
|
|
29127
29156
|
var STATIC_RESOURCES = [
|
|
29128
29157
|
{
|
|
@@ -29317,7 +29346,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29317
29346
|
const parsedUri = parseGodotUri(uri);
|
|
29318
29347
|
if (parsedUri.kind === "project-info") {
|
|
29319
29348
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
29320
|
-
const rawProject =
|
|
29349
|
+
const rawProject = readFileSync13(projectFilePath, "utf-8");
|
|
29321
29350
|
const parsedProject = parseProjectGodot(rawProject);
|
|
29322
29351
|
return {
|
|
29323
29352
|
mimeType: "application/json",
|
|
@@ -29326,7 +29355,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29326
29355
|
}
|
|
29327
29356
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
29328
29357
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
29329
|
-
const text =
|
|
29358
|
+
const text = readFileSync13(filePath, "utf-8");
|
|
29330
29359
|
return {
|
|
29331
29360
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
29332
29361
|
text
|
|
@@ -29363,16 +29392,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
29363
29392
|
}
|
|
29364
29393
|
|
|
29365
29394
|
// src/restart-note.ts
|
|
29366
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
29367
|
-
import { dirname as dirname8, join as
|
|
29395
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
29396
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
29368
29397
|
function restartNotePath(projectPath) {
|
|
29369
|
-
return
|
|
29398
|
+
return join14(projectPath, ".godot", "gdharness-restart.json");
|
|
29370
29399
|
}
|
|
29371
29400
|
function noteRestartBegun(note) {
|
|
29372
29401
|
const path = restartNotePath(note.projectPath);
|
|
29373
29402
|
try {
|
|
29374
29403
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
29375
|
-
|
|
29404
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
29376
29405
|
`, "utf8");
|
|
29377
29406
|
} catch (error) {
|
|
29378
29407
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -29380,7 +29409,7 @@ function noteRestartBegun(note) {
|
|
|
29380
29409
|
}
|
|
29381
29410
|
function restartOwed(projectPath) {
|
|
29382
29411
|
try {
|
|
29383
|
-
const parsed = JSON.parse(
|
|
29412
|
+
const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
|
|
29384
29413
|
if (typeof parsed !== "object" || parsed === null) {
|
|
29385
29414
|
return null;
|
|
29386
29415
|
}
|
|
@@ -29395,7 +29424,7 @@ function restartOwed(projectPath) {
|
|
|
29395
29424
|
}
|
|
29396
29425
|
function restartSettled(projectPath) {
|
|
29397
29426
|
try {
|
|
29398
|
-
|
|
29427
|
+
rmSync6(restartNotePath(projectPath), { force: true });
|
|
29399
29428
|
} catch (error) {
|
|
29400
29429
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
29401
29430
|
}
|
|
@@ -29403,33 +29432,33 @@ function restartSettled(projectPath) {
|
|
|
29403
29432
|
|
|
29404
29433
|
// src/run-record.ts
|
|
29405
29434
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29406
|
-
import { createHash as
|
|
29435
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
29407
29436
|
import {
|
|
29408
29437
|
mkdirSync as mkdirSync8,
|
|
29409
29438
|
openSync as openSync2,
|
|
29410
29439
|
readdirSync as readdirSync5,
|
|
29411
|
-
readFileSync as
|
|
29440
|
+
readFileSync as readFileSync15,
|
|
29412
29441
|
realpathSync as realpathSync3,
|
|
29413
|
-
rmSync as
|
|
29414
|
-
statSync as
|
|
29415
|
-
writeFileSync as
|
|
29442
|
+
rmSync as rmSync7,
|
|
29443
|
+
statSync as statSync6,
|
|
29444
|
+
writeFileSync as writeFileSync10
|
|
29416
29445
|
} from "node:fs";
|
|
29417
|
-
import { basename as
|
|
29446
|
+
import { basename as basename3, dirname as dirname9, join as join15, resolve as resolve5 } from "node:path";
|
|
29418
29447
|
function runsDirectory() {
|
|
29419
|
-
return
|
|
29448
|
+
return join15(runtimeDirectory(), "runs");
|
|
29420
29449
|
}
|
|
29421
29450
|
function realAsFarAsItExists(path) {
|
|
29422
29451
|
const missing = [];
|
|
29423
29452
|
let at = path;
|
|
29424
29453
|
for (;; ) {
|
|
29425
29454
|
try {
|
|
29426
|
-
return
|
|
29455
|
+
return join15(realpathSync3.native(at), ...missing);
|
|
29427
29456
|
} catch {
|
|
29428
29457
|
const parent = dirname9(at);
|
|
29429
29458
|
if (parent === at) {
|
|
29430
29459
|
return path;
|
|
29431
29460
|
}
|
|
29432
|
-
missing.unshift(
|
|
29461
|
+
missing.unshift(basename3(at));
|
|
29433
29462
|
at = parent;
|
|
29434
29463
|
}
|
|
29435
29464
|
}
|
|
@@ -29437,27 +29466,27 @@ function realAsFarAsItExists(path) {
|
|
|
29437
29466
|
function projectKey(projectPath) {
|
|
29438
29467
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
29439
29468
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
29440
|
-
return
|
|
29469
|
+
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29441
29470
|
}
|
|
29442
29471
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29443
|
-
return
|
|
29472
|
+
return join15(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29444
29473
|
}
|
|
29445
29474
|
var SHARED_NOTE = "run.json";
|
|
29446
29475
|
function openTranscript(startedAt) {
|
|
29447
29476
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29448
|
-
const path =
|
|
29477
|
+
const path = join15(runsDirectory(), `run-${startedAt}.log`);
|
|
29449
29478
|
return { path, fd: openSync2(path, "a") };
|
|
29450
29479
|
}
|
|
29451
29480
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29452
|
-
return
|
|
29481
|
+
return join15(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29453
29482
|
}
|
|
29454
29483
|
function writeEditorRunNote(note) {
|
|
29455
29484
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29456
|
-
|
|
29485
|
+
writeFileSync10(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
29457
29486
|
}
|
|
29458
29487
|
function readEditorRunNote(projectPath) {
|
|
29459
29488
|
for (const directory of runtimeDirectories()) {
|
|
29460
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
29489
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join15(directory, "runs", "editor-run.json"));
|
|
29461
29490
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29462
29491
|
return note;
|
|
29463
29492
|
}
|
|
@@ -29467,7 +29496,7 @@ function readEditorRunNote(projectPath) {
|
|
|
29467
29496
|
function editorNoteAt(path) {
|
|
29468
29497
|
let parsed;
|
|
29469
29498
|
try {
|
|
29470
|
-
parsed = JSON.parse(
|
|
29499
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
29471
29500
|
} catch {
|
|
29472
29501
|
return null;
|
|
29473
29502
|
}
|
|
@@ -29489,7 +29518,7 @@ function readRunRecord(projectPath) {
|
|
|
29489
29518
|
if (own !== null) {
|
|
29490
29519
|
return own;
|
|
29491
29520
|
}
|
|
29492
|
-
const shared = recordAt(
|
|
29521
|
+
const shared = recordAt(join15(directory, "runs", SHARED_NOTE));
|
|
29493
29522
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29494
29523
|
return shared;
|
|
29495
29524
|
}
|
|
@@ -29501,13 +29530,13 @@ function everyRunRecord() {
|
|
|
29501
29530
|
for (const directory of runtimeDirectories()) {
|
|
29502
29531
|
let entries;
|
|
29503
29532
|
try {
|
|
29504
|
-
entries = readdirSync5(
|
|
29533
|
+
entries = readdirSync5(join15(directory, "runs"));
|
|
29505
29534
|
} catch {
|
|
29506
29535
|
continue;
|
|
29507
29536
|
}
|
|
29508
29537
|
for (const entry of entries) {
|
|
29509
29538
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29510
|
-
const record = recordAt(
|
|
29539
|
+
const record = recordAt(join15(directory, "runs", entry));
|
|
29511
29540
|
if (record !== null) {
|
|
29512
29541
|
found.push(record);
|
|
29513
29542
|
}
|
|
@@ -29519,7 +29548,7 @@ function everyRunRecord() {
|
|
|
29519
29548
|
function recordAt(path) {
|
|
29520
29549
|
let parsed;
|
|
29521
29550
|
try {
|
|
29522
|
-
parsed = JSON.parse(
|
|
29551
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
29523
29552
|
} catch {
|
|
29524
29553
|
return null;
|
|
29525
29554
|
}
|
|
@@ -29565,7 +29594,7 @@ function runningAs(pid) {
|
|
|
29565
29594
|
const began = startedAt(pid, null);
|
|
29566
29595
|
const when = began === null ? {} : { startedAt: began };
|
|
29567
29596
|
if (process.platform === "linux") {
|
|
29568
|
-
const raw =
|
|
29597
|
+
const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
29569
29598
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
29570
29599
|
}
|
|
29571
29600
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -29668,7 +29697,7 @@ function judgeRun(record, running, needed) {
|
|
|
29668
29697
|
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
29669
29698
|
return false;
|
|
29670
29699
|
}
|
|
29671
|
-
const engine = record.command === undefined ? null :
|
|
29700
|
+
const engine = record.command === undefined ? null : basename3(record.command);
|
|
29672
29701
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
29673
29702
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
29674
29703
|
if (wanted !== null && !said.includes(wanted)) {
|
|
@@ -29704,19 +29733,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
29704
29733
|
}
|
|
29705
29734
|
function clearRunRecord(projectPath) {
|
|
29706
29735
|
for (const directory of runtimeDirectories()) {
|
|
29707
|
-
const shared =
|
|
29736
|
+
const shared = join15(directory, "runs", SHARED_NOTE);
|
|
29708
29737
|
const sharedRecord = recordAt(shared);
|
|
29709
29738
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
29710
29739
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
29711
29740
|
try {
|
|
29712
|
-
|
|
29741
|
+
rmSync7(path, { force: true });
|
|
29713
29742
|
} catch {}
|
|
29714
29743
|
}
|
|
29715
29744
|
}
|
|
29716
29745
|
}
|
|
29717
29746
|
function ageMs(path, now) {
|
|
29718
29747
|
try {
|
|
29719
|
-
return now -
|
|
29748
|
+
return now - statSync6(path).mtimeMs;
|
|
29720
29749
|
} catch {
|
|
29721
29750
|
return 0;
|
|
29722
29751
|
}
|
|
@@ -29734,29 +29763,29 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29734
29763
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
29735
29764
|
continue;
|
|
29736
29765
|
}
|
|
29737
|
-
const path =
|
|
29766
|
+
const path = join15(directory, entry);
|
|
29738
29767
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
29739
29768
|
continue;
|
|
29740
29769
|
}
|
|
29741
29770
|
try {
|
|
29742
|
-
|
|
29771
|
+
rmSync7(path);
|
|
29743
29772
|
} catch {}
|
|
29744
29773
|
}
|
|
29745
29774
|
}
|
|
29746
29775
|
|
|
29747
29776
|
// src/setup.ts
|
|
29748
|
-
import { createHash as
|
|
29777
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
29749
29778
|
import {
|
|
29750
29779
|
cpSync,
|
|
29751
|
-
existsSync as
|
|
29780
|
+
existsSync as existsSync10,
|
|
29752
29781
|
mkdirSync as mkdirSync9,
|
|
29753
29782
|
readdirSync as readdirSync6,
|
|
29754
|
-
readFileSync as
|
|
29755
|
-
rmSync as
|
|
29756
|
-
statSync as
|
|
29757
|
-
writeFileSync as
|
|
29783
|
+
readFileSync as readFileSync16,
|
|
29784
|
+
rmSync as rmSync8,
|
|
29785
|
+
statSync as statSync7,
|
|
29786
|
+
writeFileSync as writeFileSync11
|
|
29758
29787
|
} from "node:fs";
|
|
29759
|
-
import { dirname as dirname10, join as
|
|
29788
|
+
import { dirname as dirname10, join as join16 } from "node:path";
|
|
29760
29789
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29761
29790
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29762
29791
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -29767,27 +29796,27 @@ var RUNTIME_AUTOLOAD = {
|
|
|
29767
29796
|
var VERSION_MARKER = ".gdharness-version";
|
|
29768
29797
|
var DIGEST_MARKER = ".gdharness-digest";
|
|
29769
29798
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29770
|
-
const hash =
|
|
29799
|
+
const hash = createHash5("sha256");
|
|
29771
29800
|
const walk = (directory, relative) => {
|
|
29772
29801
|
for (const name of readdirSync6(directory).sort()) {
|
|
29773
29802
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
29774
29803
|
continue;
|
|
29775
29804
|
}
|
|
29776
|
-
const path =
|
|
29805
|
+
const path = join16(directory, name);
|
|
29777
29806
|
const inside = `${relative}/${name}`;
|
|
29778
|
-
if (
|
|
29807
|
+
if (statSync7(path).isDirectory()) {
|
|
29779
29808
|
walk(path, inside);
|
|
29780
29809
|
} else {
|
|
29781
29810
|
hash.update(`${inside}
|
|
29782
29811
|
`);
|
|
29783
|
-
hash.update(
|
|
29812
|
+
hash.update(readFileSync16(path));
|
|
29784
29813
|
hash.update(`
|
|
29785
29814
|
`);
|
|
29786
29815
|
}
|
|
29787
29816
|
}
|
|
29788
29817
|
};
|
|
29789
29818
|
for (const name of EDITOR_PLUGINS) {
|
|
29790
|
-
walk(
|
|
29819
|
+
walk(join16(from, name), name);
|
|
29791
29820
|
}
|
|
29792
29821
|
return hash.digest("hex");
|
|
29793
29822
|
}
|
|
@@ -29803,14 +29832,14 @@ function shippedEditorDigest() {
|
|
|
29803
29832
|
return shippedDigest ?? undefined;
|
|
29804
29833
|
}
|
|
29805
29834
|
function shippedAddonsDirectory() {
|
|
29806
|
-
return
|
|
29835
|
+
return join16(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29807
29836
|
}
|
|
29808
29837
|
function installedAddonVersion(projectPath) {
|
|
29809
|
-
const marker =
|
|
29810
|
-
if (!
|
|
29838
|
+
const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
29839
|
+
if (!existsSync10(marker)) {
|
|
29811
29840
|
return null;
|
|
29812
29841
|
}
|
|
29813
|
-
const said =
|
|
29842
|
+
const said = readFileSync16(marker, "utf8").trim();
|
|
29814
29843
|
return said === "" ? null : said;
|
|
29815
29844
|
}
|
|
29816
29845
|
|
|
@@ -30001,7 +30030,7 @@ var TOOL_SPECS = [
|
|
|
30001
30030
|
},
|
|
30002
30031
|
{
|
|
30003
30032
|
name: "project_settings",
|
|
30004
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses.",
|
|
30033
|
+
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed from the file (named under editorAdopted), or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start.",
|
|
30005
30034
|
parameters: {
|
|
30006
30035
|
projectPath: PROJECT_PATH,
|
|
30007
30036
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
@@ -30113,19 +30142,30 @@ var TOOL_SPECS = [
|
|
|
30113
30142
|
description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
|
|
30114
30143
|
additionalProperties: true
|
|
30115
30144
|
},
|
|
30116
|
-
reimport: {
|
|
30117
|
-
|
|
30145
|
+
reimport: {
|
|
30146
|
+
type: "boolean",
|
|
30147
|
+
description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
|
|
30148
|
+
},
|
|
30149
|
+
force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
|
|
30150
|
+
timeoutMs: {
|
|
30151
|
+
type: "integer",
|
|
30152
|
+
minimum: 1,
|
|
30153
|
+
description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
|
|
30154
|
+
}
|
|
30118
30155
|
},
|
|
30119
30156
|
requires: ["projectPath"],
|
|
30120
30157
|
operations: {
|
|
30121
30158
|
status: {
|
|
30122
|
-
summary: "which resources are outdated or failed, or one resource with resourcePath,
|
|
30159
|
+
summary: "which resources are outdated or failed, or one resource with resourcePath, each with its reason. Judged by content with the checks the editor makes before it reimports: a sidecar changed since its import, as setting an option without a reimport leaves it; a sidecar with no uid or written for another file; an output under .godot/imported that is gone (missing_outputs) or changed; a source whose hash differs from the one its import recorded, or with no record. Beyond the editor, a glTF scene whose import does not depend on an image it names, because it was built before that image was imported (imported_without; a scene whose import runs an import script is not judged on images). failed is an import that failed, which the editor never tries again on its own. Without resourcePath, a sidecar whose source is gone is listed as missing_source",
|
|
30123
30160
|
requires: []
|
|
30124
30161
|
},
|
|
30125
30162
|
options: { summary: "the import options of one resource", requires: ["resourcePath"] },
|
|
30126
|
-
set_options: {
|
|
30163
|
+
set_options: {
|
|
30164
|
+
summary: "change import options in the sidecar, then apply them with a reimport of that resource",
|
|
30165
|
+
requires: ["resourcePath", "options"]
|
|
30166
|
+
},
|
|
30127
30167
|
reimport: {
|
|
30128
|
-
summary: "reimport one resource, or
|
|
30168
|
+
summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. Answered from a second status read: reimported lists what is current now, notReimported what is not, with its status and reason, and via says which of the two did the work. stillImporting is an editor that had not finished within timeoutMs",
|
|
30129
30169
|
requires: []
|
|
30130
30170
|
},
|
|
30131
30171
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
@@ -30427,8 +30467,15 @@ var TOOL_SPECS = [
|
|
|
30427
30467
|
},
|
|
30428
30468
|
{
|
|
30429
30469
|
name: "editor_launch",
|
|
30430
|
-
description: "Opens the Godot editor on a project, in
|
|
30431
|
-
parameters: {
|
|
30470
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again. One opened by hand restarts itself, which only one with a window can do, 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.",
|
|
30471
|
+
parameters: {
|
|
30472
|
+
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
30473
|
+
hidden: {
|
|
30474
|
+
type: "boolean",
|
|
30475
|
+
ops: ["open", "restart"],
|
|
30476
|
+
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus."
|
|
30477
|
+
}
|
|
30478
|
+
},
|
|
30432
30479
|
requires: [],
|
|
30433
30480
|
operations: {
|
|
30434
30481
|
open: { summary: "open the editor on a project", requires: ["projectPath"] },
|
|
@@ -30453,6 +30500,11 @@ var TOOL_SPECS = [
|
|
|
30453
30500
|
description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
|
|
30454
30501
|
},
|
|
30455
30502
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
30503
|
+
visible: {
|
|
30504
|
+
type: "boolean",
|
|
30505
|
+
ops: ["start"],
|
|
30506
|
+
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
30507
|
+
},
|
|
30456
30508
|
savesIn: {
|
|
30457
30509
|
type: "string",
|
|
30458
30510
|
ops: ["start", "check"],
|
|
@@ -30783,7 +30835,7 @@ var TOOL_SPECS = [
|
|
|
30783
30835
|
},
|
|
30784
30836
|
{
|
|
30785
30837
|
name: "runtime_capture",
|
|
30786
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
|
|
30838
|
+
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
30787
30839
|
parameters: {
|
|
30788
30840
|
projectPath: RUNNING_PROJECT_PATH,
|
|
30789
30841
|
pid: RUNNING_PID,
|
|
@@ -31091,15 +31143,15 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
31091
31143
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
31092
31144
|
}
|
|
31093
31145
|
const directory = realPathOr(dirname11(outputPath));
|
|
31094
|
-
if (!
|
|
31146
|
+
if (!existsSync11(directory)) {
|
|
31095
31147
|
return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
|
|
31096
31148
|
}
|
|
31097
|
-
const where =
|
|
31149
|
+
const where = join17(directory, basename4(outputPath));
|
|
31098
31150
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
31099
31151
|
if (inside !== undefined) {
|
|
31100
31152
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
31101
31153
|
}
|
|
31102
|
-
if (
|
|
31154
|
+
if (existsSync11(where)) {
|
|
31103
31155
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
31104
31156
|
}
|
|
31105
31157
|
return null;
|
|
@@ -31477,6 +31529,45 @@ function scanWaitAnswer(scanned) {
|
|
|
31477
31529
|
var PLAYING_STATUS_MS = 1000;
|
|
31478
31530
|
var SCAN_START_MS = 2000;
|
|
31479
31531
|
var SCAN_SETTLED_MS = 500;
|
|
31532
|
+
var REIMPORT_TIMEOUT_MS = 300000;
|
|
31533
|
+
var AUDIO_BUS_OPS = new Set([
|
|
31534
|
+
"add_audio_bus",
|
|
31535
|
+
"set_audio_bus_effect",
|
|
31536
|
+
"set_audio_bus_volume"
|
|
31537
|
+
]);
|
|
31538
|
+
function changedSettings(before, after) {
|
|
31539
|
+
if (before === null || after === null) {
|
|
31540
|
+
return null;
|
|
31541
|
+
}
|
|
31542
|
+
const names = new Set([...before.keys(), ...after.keys()]);
|
|
31543
|
+
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
31544
|
+
}
|
|
31545
|
+
var STALLED_AFTER_MS = 30000;
|
|
31546
|
+
function timedOutVerdict(timeoutMs, silentForMs) {
|
|
31547
|
+
const quiet = Math.round(silentForMs);
|
|
31548
|
+
return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
|
|
31549
|
+
verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
|
|
31550
|
+
hung: true
|
|
31551
|
+
} : {
|
|
31552
|
+
verdict: `timed out after ${timeoutMs} ms while still running: it last printed ${quiet} ms before it was killed, so pass a longer timeoutMs or a narrower path`,
|
|
31553
|
+
hung: false
|
|
31554
|
+
};
|
|
31555
|
+
}
|
|
31556
|
+
function importStatuses(payload) {
|
|
31557
|
+
const listed = payload["resources"];
|
|
31558
|
+
if (!Array.isArray(listed)) {
|
|
31559
|
+
return [];
|
|
31560
|
+
}
|
|
31561
|
+
return listed.map((entry) => {
|
|
31562
|
+
const one = asParams(entry);
|
|
31563
|
+
const reason = readString(one, "reason");
|
|
31564
|
+
return {
|
|
31565
|
+
path: readString(one, "path") ?? "",
|
|
31566
|
+
status: readString(one, "status") ?? "unknown",
|
|
31567
|
+
...reason === undefined ? {} : { reason }
|
|
31568
|
+
};
|
|
31569
|
+
});
|
|
31570
|
+
}
|
|
31480
31571
|
var WAIT_FOR_RUN_MS = 600000;
|
|
31481
31572
|
var RUN_POLL_MS = 250;
|
|
31482
31573
|
var PATH_SOLUTIONS = [
|
|
@@ -31538,7 +31629,7 @@ function aboveTheRunner(entry) {
|
|
|
31538
31629
|
}
|
|
31539
31630
|
function elsewhereIn(projectPath, asked) {
|
|
31540
31631
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
31541
|
-
if (wanted === "" ||
|
|
31632
|
+
if (wanted === "" || existsSync11(join17(projectPath, wanted))) {
|
|
31542
31633
|
return "";
|
|
31543
31634
|
}
|
|
31544
31635
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -31552,7 +31643,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
31552
31643
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
31553
31644
|
}
|
|
31554
31645
|
function hasMainScene(projectFile) {
|
|
31555
|
-
const scene = parseProjectGodot(
|
|
31646
|
+
const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
31556
31647
|
return typeof scene === "string" && scene !== "";
|
|
31557
31648
|
}
|
|
31558
31649
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -31560,13 +31651,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
31560
31651
|
return false;
|
|
31561
31652
|
}
|
|
31562
31653
|
try {
|
|
31563
|
-
return
|
|
31654
|
+
return readFileSync17(join17(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
31564
31655
|
} catch {
|
|
31565
31656
|
return false;
|
|
31566
31657
|
}
|
|
31567
31658
|
}
|
|
31568
31659
|
function editorPlaysHeadless(projectFile) {
|
|
31569
|
-
const runArgs = parseProjectGodot(
|
|
31660
|
+
const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
31570
31661
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
31571
31662
|
}
|
|
31572
31663
|
function camelCased(params) {
|
|
@@ -31585,7 +31676,7 @@ function needsItsOwnProcess(asked) {
|
|
|
31585
31676
|
class GodotServer {
|
|
31586
31677
|
mcp;
|
|
31587
31678
|
locator = new GodotLocator;
|
|
31588
|
-
operationsScript =
|
|
31679
|
+
operationsScript = join17(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
31589
31680
|
godotBridge;
|
|
31590
31681
|
tools = buildToolDefinitions();
|
|
31591
31682
|
activeProcess = null;
|
|
@@ -31871,9 +31962,12 @@ class GodotServer {
|
|
|
31871
31962
|
try {
|
|
31872
31963
|
return await this.dispatch(tool, op, args);
|
|
31873
31964
|
} catch (error) {
|
|
31874
|
-
if (error instanceof McpError
|
|
31965
|
+
if (error instanceof McpError) {
|
|
31875
31966
|
throw error;
|
|
31876
31967
|
}
|
|
31968
|
+
if (error instanceof Refusal) {
|
|
31969
|
+
return this.createErrorResponse(error.message);
|
|
31970
|
+
}
|
|
31877
31971
|
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
31878
31972
|
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
31879
31973
|
const seenBefore = this.defectsSeen.before(where, errorMessage(error));
|
|
@@ -32030,7 +32124,14 @@ class GodotServer {
|
|
|
32030
32124
|
}
|
|
32031
32125
|
async dispatch(tool, op, args) {
|
|
32032
32126
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
32033
|
-
return await this.handleRefreshUids(args);
|
|
32127
|
+
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
32128
|
+
}
|
|
32129
|
+
const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
|
|
32130
|
+
if (settingsWrite !== undefined) {
|
|
32131
|
+
return await this.handleProjectSettingsWrite(op, settingsWrite, args);
|
|
32132
|
+
}
|
|
32133
|
+
if (tool === "project_import" && op === "set_options") {
|
|
32134
|
+
return await this.handleSetImportOptions(args);
|
|
32034
32135
|
}
|
|
32035
32136
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
32036
32137
|
if (headless !== undefined) {
|
|
@@ -32146,7 +32247,7 @@ class GodotServer {
|
|
|
32146
32247
|
return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
|
|
32147
32248
|
}
|
|
32148
32249
|
case "editor_launch":
|
|
32149
|
-
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
32250
|
+
return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
|
|
32150
32251
|
case "editor_run":
|
|
32151
32252
|
if (op === "stop") {
|
|
32152
32253
|
return await this.handleStopProject(args);
|
|
@@ -32289,8 +32390,8 @@ class GodotServer {
|
|
|
32289
32390
|
if (path === undefined) {
|
|
32290
32391
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
32291
32392
|
}
|
|
32292
|
-
const file =
|
|
32293
|
-
if (!
|
|
32393
|
+
const file = join17(path, "project.godot");
|
|
32394
|
+
if (!existsSync11(file)) {
|
|
32294
32395
|
return {
|
|
32295
32396
|
ok: false,
|
|
32296
32397
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -32561,6 +32662,222 @@ class GodotServer {
|
|
|
32561
32662
|
}
|
|
32562
32663
|
}, scanned));
|
|
32563
32664
|
}
|
|
32665
|
+
async handleReimport(args) {
|
|
32666
|
+
const project = this.project(args);
|
|
32667
|
+
if (!project.ok) {
|
|
32668
|
+
return project.response;
|
|
32669
|
+
}
|
|
32670
|
+
const contained = this.containProjectFiles(args);
|
|
32671
|
+
if (!contained.ok) {
|
|
32672
|
+
return contained.response;
|
|
32673
|
+
}
|
|
32674
|
+
const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
32675
|
+
return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
|
|
32676
|
+
}
|
|
32677
|
+
editorHasOpen(projectPath) {
|
|
32678
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
32679
|
+
return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
|
|
32680
|
+
}
|
|
32681
|
+
async handleProjectSettingsWrite(op, operation, args) {
|
|
32682
|
+
const project = this.project(args);
|
|
32683
|
+
if (!project.ok) {
|
|
32684
|
+
return project.response;
|
|
32685
|
+
}
|
|
32686
|
+
const projectPath = project.value.path;
|
|
32687
|
+
const served = this.editorHasOpen(projectPath);
|
|
32688
|
+
const before = served ? this.settingKeysOf(projectPath) : null;
|
|
32689
|
+
const written = await this.headless(operation, args);
|
|
32690
|
+
if (!served || written.isError === true) {
|
|
32691
|
+
return written;
|
|
32692
|
+
}
|
|
32693
|
+
const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
|
|
32694
|
+
if (AUDIO_BUS_OPS.has(op)) {
|
|
32695
|
+
return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
|
|
32696
|
+
}
|
|
32697
|
+
const changed = changedSettings(before, this.settingKeysOf(projectPath));
|
|
32698
|
+
if (changed === null) {
|
|
32699
|
+
return this.jsonTextResponse({
|
|
32700
|
+
...answer,
|
|
32701
|
+
editorNote: "project.godot could not be read on both sides of the write, so what it changed is unknown and the open editor was not told: it may still hold the old values and write them back the next time it saves. editor_launch restart loads the file."
|
|
32702
|
+
});
|
|
32703
|
+
}
|
|
32704
|
+
if (changed.length === 0) {
|
|
32705
|
+
return written;
|
|
32706
|
+
}
|
|
32707
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
|
|
32708
|
+
return this.jsonTextResponse({
|
|
32709
|
+
...answer,
|
|
32710
|
+
...taken,
|
|
32711
|
+
...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
|
|
32712
|
+
});
|
|
32713
|
+
}
|
|
32714
|
+
async editorTakes(command, args) {
|
|
32715
|
+
try {
|
|
32716
|
+
await this.godotBridge.invokeTool(command, args);
|
|
32717
|
+
return {};
|
|
32718
|
+
} catch (error) {
|
|
32719
|
+
const message = errorMessage(error);
|
|
32720
|
+
const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
|
|
32721
|
+
return {
|
|
32722
|
+
editorNote: `The open editor ${why}, so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. editor_launch restart loads the file and the addon this server ships.`
|
|
32723
|
+
};
|
|
32724
|
+
}
|
|
32725
|
+
}
|
|
32726
|
+
async handleSetImportOptions(args) {
|
|
32727
|
+
const project = this.project(args);
|
|
32728
|
+
if (!project.ok) {
|
|
32729
|
+
return project.response;
|
|
32730
|
+
}
|
|
32731
|
+
const contained = this.containProjectFiles(args);
|
|
32732
|
+
if (!contained.ok) {
|
|
32733
|
+
return contained.response;
|
|
32734
|
+
}
|
|
32735
|
+
const {
|
|
32736
|
+
op: _op,
|
|
32737
|
+
projectPath: _projectPath,
|
|
32738
|
+
reimport: _reimport,
|
|
32739
|
+
timeoutMs: _timeoutMs,
|
|
32740
|
+
...params
|
|
32741
|
+
} = contained.value;
|
|
32742
|
+
const written = await this.operation("set_import_options", params, project.value.path);
|
|
32743
|
+
if (!written.ok || args["reimport"] === false) {
|
|
32744
|
+
return this.answer(written.ok ? {
|
|
32745
|
+
...written,
|
|
32746
|
+
payload: {
|
|
32747
|
+
...written.payload,
|
|
32748
|
+
note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
|
|
32749
|
+
}
|
|
32750
|
+
} : written);
|
|
32751
|
+
}
|
|
32752
|
+
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
32753
|
+
if (!applied.ok) {
|
|
32754
|
+
return applied.response;
|
|
32755
|
+
}
|
|
32756
|
+
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
32757
|
+
}
|
|
32758
|
+
async reimport(projectPath, resourcePath, force, timeoutMs) {
|
|
32759
|
+
const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
|
|
32760
|
+
const before = await this.operation("get_import_status", ask, projectPath);
|
|
32761
|
+
if (!before.ok) {
|
|
32762
|
+
return { ok: false, response: this.answer(before) };
|
|
32763
|
+
}
|
|
32764
|
+
const listed = importStatuses(before.payload);
|
|
32765
|
+
const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
|
|
32766
|
+
if (resourcePath !== undefined && missingSource.length > 0) {
|
|
32767
|
+
return {
|
|
32768
|
+
ok: false,
|
|
32769
|
+
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
32770
|
+
};
|
|
32771
|
+
}
|
|
32772
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
32773
|
+
if (targets.length === 0) {
|
|
32774
|
+
return {
|
|
32775
|
+
ok: true,
|
|
32776
|
+
payload: {
|
|
32777
|
+
reimported: [],
|
|
32778
|
+
notReimported: [],
|
|
32779
|
+
note: resourcePath === undefined ? "Nothing needed reimporting. Pass force to reimport what is current." : `${listed[0]?.path ?? resourcePath} is current. Pass force to reimport it anyway.`
|
|
32780
|
+
}
|
|
32781
|
+
};
|
|
32782
|
+
}
|
|
32783
|
+
const viaEditor = this.editorHasOpen(projectPath);
|
|
32784
|
+
const extra = {};
|
|
32785
|
+
if (viaEditor) {
|
|
32786
|
+
const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
|
|
32787
|
+
if (!inEditor.ok) {
|
|
32788
|
+
return inEditor;
|
|
32789
|
+
}
|
|
32790
|
+
if (!inEditor.finished) {
|
|
32791
|
+
return {
|
|
32792
|
+
ok: true,
|
|
32793
|
+
payload: {
|
|
32794
|
+
via: "editor",
|
|
32795
|
+
stillImporting: true,
|
|
32796
|
+
asked: targets,
|
|
32797
|
+
note: `The editor had not finished after ${timeoutMs} ms. project_import status says what it has done so far; pass a longer timeoutMs to wait it out.`
|
|
32798
|
+
}
|
|
32799
|
+
};
|
|
32800
|
+
}
|
|
32801
|
+
} else {
|
|
32802
|
+
const engine = await this.engine();
|
|
32803
|
+
if (!engine.ok) {
|
|
32804
|
+
return { ok: false, response: engine.response };
|
|
32805
|
+
}
|
|
32806
|
+
for (const target of targets) {
|
|
32807
|
+
forceNextImport(projectPath, target);
|
|
32808
|
+
}
|
|
32809
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
32810
|
+
const imported = await runImport(engine.value, projectPath);
|
|
32811
|
+
if (!imported.ok) {
|
|
32812
|
+
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
32813
|
+
}
|
|
32814
|
+
if (imported.messages.length > 0) {
|
|
32815
|
+
extra["engine_messages"] = imported.messages;
|
|
32816
|
+
}
|
|
32817
|
+
}
|
|
32818
|
+
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
32819
|
+
if (!after.ok) {
|
|
32820
|
+
return { ok: false, response: this.answer(after) };
|
|
32821
|
+
}
|
|
32822
|
+
const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
|
|
32823
|
+
const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
|
|
32824
|
+
const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
|
|
32825
|
+
path,
|
|
32826
|
+
status: now.get(path)?.status ?? "unknown",
|
|
32827
|
+
...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
|
|
32828
|
+
}));
|
|
32829
|
+
return {
|
|
32830
|
+
ok: true,
|
|
32831
|
+
payload: {
|
|
32832
|
+
via: viaEditor ? "editor" : "engine",
|
|
32833
|
+
reimported,
|
|
32834
|
+
notReimported,
|
|
32835
|
+
...missingSource.length > 0 ? { missingSource } : {},
|
|
32836
|
+
...extra
|
|
32837
|
+
}
|
|
32838
|
+
};
|
|
32839
|
+
}
|
|
32840
|
+
async reimportInTheEditor(paths, timeoutMs) {
|
|
32841
|
+
const started = Date.now();
|
|
32842
|
+
let completedBefore;
|
|
32843
|
+
for (;; ) {
|
|
32844
|
+
while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
|
|
32845
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
32846
|
+
}
|
|
32847
|
+
let answer;
|
|
32848
|
+
try {
|
|
32849
|
+
answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
|
|
32850
|
+
} catch (error) {
|
|
32851
|
+
const message = errorMessage(error);
|
|
32852
|
+
return {
|
|
32853
|
+
ok: false,
|
|
32854
|
+
response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
|
|
32855
|
+
"editor_launch restart loads the addon this server ships",
|
|
32856
|
+
"Or close the editor, and project_import reimport runs the engine import instead"
|
|
32857
|
+
]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
|
|
32858
|
+
};
|
|
32859
|
+
}
|
|
32860
|
+
if (answer["started"] !== false) {
|
|
32861
|
+
completedBefore = readNumber(answer, "reimportsCompletedBefore");
|
|
32862
|
+
break;
|
|
32863
|
+
}
|
|
32864
|
+
if (Date.now() - started >= timeoutMs) {
|
|
32865
|
+
return { ok: true, finished: false };
|
|
32866
|
+
}
|
|
32867
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
32868
|
+
}
|
|
32869
|
+
while (Date.now() - started < timeoutMs) {
|
|
32870
|
+
await new Promise((settle) => setTimeout(settle, 250));
|
|
32871
|
+
try {
|
|
32872
|
+
const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
32873
|
+
const completed = readNumber(status, "reimportsCompleted");
|
|
32874
|
+
if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
|
|
32875
|
+
return { ok: true, finished: true };
|
|
32876
|
+
}
|
|
32877
|
+
} catch {}
|
|
32878
|
+
}
|
|
32879
|
+
return { ok: true, finished: false };
|
|
32880
|
+
}
|
|
32564
32881
|
async operation(operation, params, projectPath) {
|
|
32565
32882
|
const engine = await this.engine();
|
|
32566
32883
|
if (!engine.ok) {
|
|
@@ -32602,11 +32919,11 @@ class GodotServer {
|
|
|
32602
32919
|
if (!engine.ok) {
|
|
32603
32920
|
return engine.response;
|
|
32604
32921
|
}
|
|
32605
|
-
const application = parseProjectGodot(
|
|
32922
|
+
const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
|
|
32606
32923
|
const name = application["config/name"];
|
|
32607
32924
|
const mainScene = application["run/main_scene"];
|
|
32608
32925
|
const info = {
|
|
32609
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
32926
|
+
name: typeof name === "string" && name !== "" ? name : basename4(project.value.path),
|
|
32610
32927
|
path: project.value.path,
|
|
32611
32928
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
32612
32929
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -32670,11 +32987,11 @@ class GodotServer {
|
|
|
32670
32987
|
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
32671
32988
|
}
|
|
32672
32989
|
const debug = readBoolean(args, "debug") ?? false;
|
|
32673
|
-
const exportLogs = mkdtempSync2(
|
|
32990
|
+
const exportLogs = mkdtempSync2(join17(tmpdir4(), "gdharness-export-"));
|
|
32674
32991
|
const exportArgs = [
|
|
32675
32992
|
"--headless",
|
|
32676
32993
|
"--log-file",
|
|
32677
|
-
|
|
32994
|
+
join17(exportLogs, "engine.log"),
|
|
32678
32995
|
"--path",
|
|
32679
32996
|
project.value.path,
|
|
32680
32997
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -32706,7 +33023,7 @@ class GodotServer {
|
|
|
32706
33023
|
log.finish();
|
|
32707
33024
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
32708
33025
|
const verdict = {
|
|
32709
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
33026
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync11(output.absolutePath),
|
|
32710
33027
|
preset,
|
|
32711
33028
|
outputPath: output.relativePath,
|
|
32712
33029
|
debug,
|
|
@@ -32742,7 +33059,7 @@ class GodotServer {
|
|
|
32742
33059
|
return contained.response;
|
|
32743
33060
|
}
|
|
32744
33061
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
32745
|
-
if (!
|
|
33062
|
+
if (!existsSync11(join17(project.value.path, runner))) {
|
|
32746
33063
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
32747
33064
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
32748
33065
|
]);
|
|
@@ -32778,14 +33095,21 @@ class GodotServer {
|
|
|
32778
33095
|
];
|
|
32779
33096
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
32780
33097
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
32781
|
-
const userData = mkdtempSync2(
|
|
33098
|
+
const userData = mkdtempSync2(join17(tmpdir4(), "gdharness-tests-"));
|
|
32782
33099
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
32783
33100
|
const console2 = [];
|
|
33101
|
+
let lastSaid = Date.now();
|
|
32784
33102
|
run.process.stdout?.on("data", (data) => {
|
|
32785
33103
|
console2.push(data);
|
|
33104
|
+
lastSaid = Date.now();
|
|
32786
33105
|
});
|
|
32787
|
-
|
|
33106
|
+
run.process.stderr?.on("data", () => {
|
|
33107
|
+
lastSaid = Date.now();
|
|
33108
|
+
});
|
|
33109
|
+
let silentForMs = 0;
|
|
33110
|
+
const timedOut = await new Promise((resolve) => {
|
|
32788
33111
|
const timer = setTimeout(() => {
|
|
33112
|
+
silentForMs = Date.now() - lastSaid;
|
|
32789
33113
|
run.process.kill();
|
|
32790
33114
|
resolve(true);
|
|
32791
33115
|
}, timeoutMs);
|
|
@@ -32798,14 +33122,14 @@ class GodotServer {
|
|
|
32798
33122
|
resolve(false);
|
|
32799
33123
|
});
|
|
32800
33124
|
});
|
|
32801
|
-
const reportsDir =
|
|
33125
|
+
const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
|
|
32802
33126
|
let report = null;
|
|
32803
33127
|
let reportProblem = null;
|
|
32804
33128
|
try {
|
|
32805
|
-
const written =
|
|
33129
|
+
const written = existsSync11(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
32806
33130
|
const newest = written.at(-1);
|
|
32807
33131
|
if (newest !== undefined) {
|
|
32808
|
-
report = parseJUnit(
|
|
33132
|
+
report = parseJUnit(readFileSync17(join17(reportsDir, newest, "results.xml"), "utf8"));
|
|
32809
33133
|
}
|
|
32810
33134
|
} catch (error) {
|
|
32811
33135
|
reportProblem = errorMessage(error);
|
|
@@ -32827,8 +33151,10 @@ class GodotServer {
|
|
|
32827
33151
|
const exitCode = run.exitCode;
|
|
32828
33152
|
const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
|
|
32829
33153
|
const said = printed.map((entry) => entry.text);
|
|
32830
|
-
const nothingRan =
|
|
32831
|
-
const
|
|
33154
|
+
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
33155
|
+
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
33156
|
+
const hung = cutShort?.hung ?? false;
|
|
33157
|
+
const verdict = cutShort !== null ? cutShort.verdict : nothingRan ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
32832
33158
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
32833
33159
|
if (report === null) {
|
|
32834
33160
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
@@ -32844,6 +33170,7 @@ class GodotServer {
|
|
|
32844
33170
|
exitCode,
|
|
32845
33171
|
exitSignal: run.exitSignal ?? undefined,
|
|
32846
33172
|
hung,
|
|
33173
|
+
...timedOut ? { timedOut, silentForMs } : {},
|
|
32847
33174
|
arguments: cmdArgs,
|
|
32848
33175
|
entries: forAnswer(printed.slice(0, 60)),
|
|
32849
33176
|
savesNote,
|
|
@@ -32868,8 +33195,9 @@ class GodotServer {
|
|
|
32868
33195
|
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
32869
33196
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
32870
33197
|
return this.jsonTextResponse({
|
|
32871
|
-
passed: !
|
|
33198
|
+
passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
32872
33199
|
verdict,
|
|
33200
|
+
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
32873
33201
|
exitCode,
|
|
32874
33202
|
exitSignal: run.exitSignal ?? undefined,
|
|
32875
33203
|
tests: report.tests,
|
|
@@ -32948,11 +33276,11 @@ class GodotServer {
|
|
|
32948
33276
|
const cached = cachedClasses(projectPath);
|
|
32949
33277
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
32950
33278
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
32951
|
-
if (!contained.ok || !
|
|
33279
|
+
if (!contained.ok || !existsSync11(contained.absolutePath)) {
|
|
32952
33280
|
return null;
|
|
32953
33281
|
}
|
|
32954
33282
|
try {
|
|
32955
|
-
return
|
|
33283
|
+
return readFileSync17(contained.absolutePath, "utf8");
|
|
32956
33284
|
} catch {
|
|
32957
33285
|
return null;
|
|
32958
33286
|
}
|
|
@@ -33002,11 +33330,11 @@ class GodotServer {
|
|
|
33002
33330
|
}
|
|
33003
33331
|
sourceOfClass(projectPath, resourcePath) {
|
|
33004
33332
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33005
|
-
if (!contained.ok || !
|
|
33333
|
+
if (!contained.ok || !existsSync11(contained.absolutePath)) {
|
|
33006
33334
|
return null;
|
|
33007
33335
|
}
|
|
33008
33336
|
try {
|
|
33009
|
-
return
|
|
33337
|
+
return readFileSync17(contained.absolutePath, "utf8");
|
|
33010
33338
|
} catch {
|
|
33011
33339
|
return null;
|
|
33012
33340
|
}
|
|
@@ -33275,12 +33603,12 @@ class GodotServer {
|
|
|
33275
33603
|
return null;
|
|
33276
33604
|
}
|
|
33277
33605
|
try {
|
|
33278
|
-
return settingKeys(
|
|
33606
|
+
return settingKeys(readFileSync17(join17(projectPath, "project.godot"), "utf8"));
|
|
33279
33607
|
} catch {
|
|
33280
33608
|
return null;
|
|
33281
33609
|
}
|
|
33282
33610
|
}
|
|
33283
|
-
async handleRestartEditor() {
|
|
33611
|
+
async handleRestartEditor(args) {
|
|
33284
33612
|
const before = this.godotBridge.getStatus();
|
|
33285
33613
|
if (!before.connected) {
|
|
33286
33614
|
return this.createErrorResponse("No editor is connected, so there is nothing to restart.", [
|
|
@@ -33290,7 +33618,7 @@ class GodotServer {
|
|
|
33290
33618
|
}
|
|
33291
33619
|
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
33292
33620
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
33293
|
-
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
33621
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
33294
33622
|
if (asked.isError === true) {
|
|
33295
33623
|
return asked;
|
|
33296
33624
|
}
|
|
@@ -33328,7 +33656,7 @@ class GodotServer {
|
|
|
33328
33656
|
whatTheEditorDropped(before, after) {
|
|
33329
33657
|
return settingsDroppedReport(before, after);
|
|
33330
33658
|
}
|
|
33331
|
-
async startItAgain(projectPath, editorPid) {
|
|
33659
|
+
async startItAgain(projectPath, editorPid, hidden) {
|
|
33332
33660
|
const engine = await this.engine();
|
|
33333
33661
|
if (!engine.ok) {
|
|
33334
33662
|
return engine.response;
|
|
@@ -33350,7 +33678,7 @@ class GodotServer {
|
|
|
33350
33678
|
return asked;
|
|
33351
33679
|
}
|
|
33352
33680
|
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
33353
|
-
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
33681
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
33354
33682
|
restartSettled(projectPath);
|
|
33355
33683
|
if (opened.error !== null) {
|
|
33356
33684
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
@@ -33388,7 +33716,7 @@ class GodotServer {
|
|
|
33388
33716
|
const ports = await this.portsForAnEditor();
|
|
33389
33717
|
const before = this.settingKeysOf(project.value.path);
|
|
33390
33718
|
const unfinished = restartOwed(project.value.path);
|
|
33391
|
-
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
33719
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true);
|
|
33392
33720
|
if (opened.error !== null) {
|
|
33393
33721
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
33394
33722
|
}
|
|
@@ -33404,18 +33732,19 @@ class GodotServer {
|
|
|
33404
33732
|
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."
|
|
33405
33733
|
});
|
|
33406
33734
|
}
|
|
33407
|
-
async openAnEditor(engine, projectPath, ports) {
|
|
33735
|
+
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
33408
33736
|
clearEditorLog(projectPath);
|
|
33409
33737
|
const launched = await launchOutsideTheTree({
|
|
33410
33738
|
command: engine,
|
|
33411
|
-
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
33739
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
|
|
33412
33740
|
env: {
|
|
33413
33741
|
...process.env,
|
|
33414
33742
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
33415
33743
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
33416
33744
|
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
33417
33745
|
[OPENED_BY_A_SERVER]: "1"
|
|
33418
|
-
}
|
|
33746
|
+
},
|
|
33747
|
+
...hidden ? { desktop: HIDDEN_DESKTOP } : { noActivate: true }
|
|
33419
33748
|
});
|
|
33420
33749
|
if ("error" in launched) {
|
|
33421
33750
|
return { pid: null, error: launched.error };
|
|
@@ -33497,7 +33826,7 @@ class GodotServer {
|
|
|
33497
33826
|
userArgs: given.value
|
|
33498
33827
|
});
|
|
33499
33828
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
33500
|
-
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
33829
|
+
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
|
|
33501
33830
|
if ("error" in started) {
|
|
33502
33831
|
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
33503
33832
|
"editor_status says which Godot this server runs and whether it answers"
|
|
@@ -33527,7 +33856,7 @@ class GodotServer {
|
|
|
33527
33856
|
}
|
|
33528
33857
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
33529
33858
|
const budgetMs = budget.ms;
|
|
33530
|
-
if (!
|
|
33859
|
+
if (!existsSync11(join17(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
33531
33860
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
33532
33861
|
}
|
|
33533
33862
|
let asking = null;
|
|
@@ -33590,7 +33919,7 @@ class GodotServer {
|
|
|
33590
33919
|
}
|
|
33591
33920
|
let announcedAt;
|
|
33592
33921
|
try {
|
|
33593
|
-
announcedAt =
|
|
33922
|
+
announcedAt = statSync8(endpoint.file).mtimeMs;
|
|
33594
33923
|
} catch {
|
|
33595
33924
|
return;
|
|
33596
33925
|
}
|
|
@@ -33837,7 +34166,7 @@ class GodotServer {
|
|
|
33837
34166
|
});
|
|
33838
34167
|
return started;
|
|
33839
34168
|
}
|
|
33840
|
-
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
34169
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
33841
34170
|
const startedAt = Date.now();
|
|
33842
34171
|
const transcript = openTranscript(startedAt);
|
|
33843
34172
|
closeSync2(transcript.fd);
|
|
@@ -33845,7 +34174,8 @@ class GodotServer {
|
|
|
33845
34174
|
command: godotPath,
|
|
33846
34175
|
args: cmdArgs,
|
|
33847
34176
|
...env === undefined ? {} : { env },
|
|
33848
|
-
run: { transcript: transcript.path, startedAt, projectPath }
|
|
34177
|
+
run: { transcript: transcript.path, startedAt, projectPath },
|
|
34178
|
+
...desktop === undefined ? {} : { desktop }
|
|
33849
34179
|
});
|
|
33850
34180
|
if ("error" in launched) {
|
|
33851
34181
|
return launched;
|
|
@@ -34340,7 +34670,7 @@ class GodotServer {
|
|
|
34340
34670
|
notes.push(endedWithoutACode({
|
|
34341
34671
|
throughEditor: run.throughEditor,
|
|
34342
34672
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34343
|
-
couldAnnounce: run.projectPath !== null &&
|
|
34673
|
+
couldAnnounce: run.projectPath !== null && existsSync11(join17(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34344
34674
|
endedHere: typeof run.endedHere === "string",
|
|
34345
34675
|
pickedUp: run.pickedUp === true
|
|
34346
34676
|
}));
|
|
@@ -34853,7 +35183,7 @@ class GodotServer {
|
|
|
34853
35183
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
34854
35184
|
const run = this.currentRun();
|
|
34855
35185
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
34856
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
35186
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync11(join17(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
34857
35187
|
return null;
|
|
34858
35188
|
}
|
|
34859
35189
|
const already = runtimesAnnounced().running;
|
|
@@ -34862,7 +35192,7 @@ class GodotServer {
|
|
|
34862
35192
|
return !run.announcedBefore.has(one.pid);
|
|
34863
35193
|
}
|
|
34864
35194
|
try {
|
|
34865
|
-
return
|
|
35195
|
+
return statSync8(one.file).mtimeMs >= run.startedAt;
|
|
34866
35196
|
} catch {
|
|
34867
35197
|
return false;
|
|
34868
35198
|
}
|
|
@@ -34932,8 +35262,8 @@ class GodotServer {
|
|
|
34932
35262
|
return this.createErrorResponse(refusal);
|
|
34933
35263
|
}
|
|
34934
35264
|
}
|
|
34935
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
34936
|
-
const screenshotPath = screenshotDir ?
|
|
35265
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join17(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
35266
|
+
const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
|
|
34937
35267
|
try {
|
|
34938
35268
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
34939
35269
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -34974,7 +35304,7 @@ class GodotServer {
|
|
|
34974
35304
|
type: "text",
|
|
34975
35305
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
34976
35306
|
},
|
|
34977
|
-
{ type: "image", data:
|
|
35307
|
+
{ type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
34978
35308
|
]
|
|
34979
35309
|
};
|
|
34980
35310
|
} finally {
|