gdharness 1.1.4 → 1.1.6
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 +396 -96
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +64 -15
- 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 +250 -85
- package/build/index.js +405 -108
- package/build/keeper.js +205 -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";
|
|
@@ -24811,6 +24811,7 @@ function runArguments(options) {
|
|
|
24811
24811
|
function editorArguments(projectPath, ports, logFile) {
|
|
24812
24812
|
return [
|
|
24813
24813
|
"-e",
|
|
24814
|
+
"--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" })
|
|
@@ -28936,7 +28939,8 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
28936
28939
|
command: spec.command,
|
|
28937
28940
|
args: spec.args,
|
|
28938
28941
|
envChanges: changesFrom(spec.env, process.env),
|
|
28939
|
-
...spec.run === undefined ? {} : { run: spec.run }
|
|
28942
|
+
...spec.run === undefined ? {} : { run: spec.run },
|
|
28943
|
+
...spec.desktop === undefined ? {} : { desktop: spec.desktop }
|
|
28940
28944
|
});
|
|
28941
28945
|
let printed = "";
|
|
28942
28946
|
let complained = "";
|
|
@@ -29121,8 +29125,28 @@ function searchProject(projectPath, options) {
|
|
|
29121
29125
|
return result;
|
|
29122
29126
|
}
|
|
29123
29127
|
|
|
29128
|
+
// src/reimport.ts
|
|
29129
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
29130
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
|
|
29131
|
+
import { basename as basename2, join as join13 } from "node:path";
|
|
29132
|
+
function forceNextImport(projectPath, resourcePath) {
|
|
29133
|
+
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
29134
|
+
const sidecar = join13(projectPath, `${relative}.import`);
|
|
29135
|
+
if (!existsSync9(sidecar)) {
|
|
29136
|
+
return;
|
|
29137
|
+
}
|
|
29138
|
+
const { atime, mtimeMs } = statSync5(sidecar);
|
|
29139
|
+
writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
29140
|
+
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
29141
|
+
rmSync5(importRecord(projectPath, resourcePath), { force: true });
|
|
29142
|
+
}
|
|
29143
|
+
function importRecord(projectPath, resourcePath) {
|
|
29144
|
+
const digest = createHash3("md5").update(resourcePath).digest("hex");
|
|
29145
|
+
return join13(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
|
|
29146
|
+
}
|
|
29147
|
+
|
|
29124
29148
|
// src/resources.ts
|
|
29125
|
-
import { readFileSync as
|
|
29149
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
29126
29150
|
import { extname, resolve as resolve4 } from "node:path";
|
|
29127
29151
|
var STATIC_RESOURCES = [
|
|
29128
29152
|
{
|
|
@@ -29317,7 +29341,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29317
29341
|
const parsedUri = parseGodotUri(uri);
|
|
29318
29342
|
if (parsedUri.kind === "project-info") {
|
|
29319
29343
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
29320
|
-
const rawProject =
|
|
29344
|
+
const rawProject = readFileSync13(projectFilePath, "utf-8");
|
|
29321
29345
|
const parsedProject = parseProjectGodot(rawProject);
|
|
29322
29346
|
return {
|
|
29323
29347
|
mimeType: "application/json",
|
|
@@ -29326,7 +29350,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29326
29350
|
}
|
|
29327
29351
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
29328
29352
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
29329
|
-
const text =
|
|
29353
|
+
const text = readFileSync13(filePath, "utf-8");
|
|
29330
29354
|
return {
|
|
29331
29355
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
29332
29356
|
text
|
|
@@ -29363,16 +29387,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
29363
29387
|
}
|
|
29364
29388
|
|
|
29365
29389
|
// src/restart-note.ts
|
|
29366
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
29367
|
-
import { dirname as dirname8, join as
|
|
29390
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
29391
|
+
import { dirname as dirname8, join as join14 } from "node:path";
|
|
29368
29392
|
function restartNotePath(projectPath) {
|
|
29369
|
-
return
|
|
29393
|
+
return join14(projectPath, ".godot", "gdharness-restart.json");
|
|
29370
29394
|
}
|
|
29371
29395
|
function noteRestartBegun(note) {
|
|
29372
29396
|
const path = restartNotePath(note.projectPath);
|
|
29373
29397
|
try {
|
|
29374
29398
|
mkdirSync7(dirname8(path), { recursive: true });
|
|
29375
|
-
|
|
29399
|
+
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
29376
29400
|
`, "utf8");
|
|
29377
29401
|
} catch (error) {
|
|
29378
29402
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -29380,7 +29404,7 @@ function noteRestartBegun(note) {
|
|
|
29380
29404
|
}
|
|
29381
29405
|
function restartOwed(projectPath) {
|
|
29382
29406
|
try {
|
|
29383
|
-
const parsed = JSON.parse(
|
|
29407
|
+
const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
|
|
29384
29408
|
if (typeof parsed !== "object" || parsed === null) {
|
|
29385
29409
|
return null;
|
|
29386
29410
|
}
|
|
@@ -29395,7 +29419,7 @@ function restartOwed(projectPath) {
|
|
|
29395
29419
|
}
|
|
29396
29420
|
function restartSettled(projectPath) {
|
|
29397
29421
|
try {
|
|
29398
|
-
|
|
29422
|
+
rmSync6(restartNotePath(projectPath), { force: true });
|
|
29399
29423
|
} catch (error) {
|
|
29400
29424
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
29401
29425
|
}
|
|
@@ -29403,33 +29427,33 @@ function restartSettled(projectPath) {
|
|
|
29403
29427
|
|
|
29404
29428
|
// src/run-record.ts
|
|
29405
29429
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
29406
|
-
import { createHash as
|
|
29430
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
29407
29431
|
import {
|
|
29408
29432
|
mkdirSync as mkdirSync8,
|
|
29409
29433
|
openSync as openSync2,
|
|
29410
29434
|
readdirSync as readdirSync5,
|
|
29411
|
-
readFileSync as
|
|
29435
|
+
readFileSync as readFileSync15,
|
|
29412
29436
|
realpathSync as realpathSync3,
|
|
29413
|
-
rmSync as
|
|
29414
|
-
statSync as
|
|
29415
|
-
writeFileSync as
|
|
29437
|
+
rmSync as rmSync7,
|
|
29438
|
+
statSync as statSync6,
|
|
29439
|
+
writeFileSync as writeFileSync10
|
|
29416
29440
|
} from "node:fs";
|
|
29417
|
-
import { basename as
|
|
29441
|
+
import { basename as basename3, dirname as dirname9, join as join15, resolve as resolve5 } from "node:path";
|
|
29418
29442
|
function runsDirectory() {
|
|
29419
|
-
return
|
|
29443
|
+
return join15(runtimeDirectory(), "runs");
|
|
29420
29444
|
}
|
|
29421
29445
|
function realAsFarAsItExists(path) {
|
|
29422
29446
|
const missing = [];
|
|
29423
29447
|
let at = path;
|
|
29424
29448
|
for (;; ) {
|
|
29425
29449
|
try {
|
|
29426
|
-
return
|
|
29450
|
+
return join15(realpathSync3.native(at), ...missing);
|
|
29427
29451
|
} catch {
|
|
29428
29452
|
const parent = dirname9(at);
|
|
29429
29453
|
if (parent === at) {
|
|
29430
29454
|
return path;
|
|
29431
29455
|
}
|
|
29432
|
-
missing.unshift(
|
|
29456
|
+
missing.unshift(basename3(at));
|
|
29433
29457
|
at = parent;
|
|
29434
29458
|
}
|
|
29435
29459
|
}
|
|
@@ -29437,27 +29461,27 @@ function realAsFarAsItExists(path) {
|
|
|
29437
29461
|
function projectKey(projectPath) {
|
|
29438
29462
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
29439
29463
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
29440
|
-
return
|
|
29464
|
+
return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29441
29465
|
}
|
|
29442
29466
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29443
|
-
return
|
|
29467
|
+
return join15(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29444
29468
|
}
|
|
29445
29469
|
var SHARED_NOTE = "run.json";
|
|
29446
29470
|
function openTranscript(startedAt) {
|
|
29447
29471
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29448
|
-
const path =
|
|
29472
|
+
const path = join15(runsDirectory(), `run-${startedAt}.log`);
|
|
29449
29473
|
return { path, fd: openSync2(path, "a") };
|
|
29450
29474
|
}
|
|
29451
29475
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29452
|
-
return
|
|
29476
|
+
return join15(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29453
29477
|
}
|
|
29454
29478
|
function writeEditorRunNote(note) {
|
|
29455
29479
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29456
|
-
|
|
29480
|
+
writeFileSync10(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
29457
29481
|
}
|
|
29458
29482
|
function readEditorRunNote(projectPath) {
|
|
29459
29483
|
for (const directory of runtimeDirectories()) {
|
|
29460
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
29484
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join15(directory, "runs", "editor-run.json"));
|
|
29461
29485
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29462
29486
|
return note;
|
|
29463
29487
|
}
|
|
@@ -29467,7 +29491,7 @@ function readEditorRunNote(projectPath) {
|
|
|
29467
29491
|
function editorNoteAt(path) {
|
|
29468
29492
|
let parsed;
|
|
29469
29493
|
try {
|
|
29470
|
-
parsed = JSON.parse(
|
|
29494
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
29471
29495
|
} catch {
|
|
29472
29496
|
return null;
|
|
29473
29497
|
}
|
|
@@ -29489,7 +29513,7 @@ function readRunRecord(projectPath) {
|
|
|
29489
29513
|
if (own !== null) {
|
|
29490
29514
|
return own;
|
|
29491
29515
|
}
|
|
29492
|
-
const shared = recordAt(
|
|
29516
|
+
const shared = recordAt(join15(directory, "runs", SHARED_NOTE));
|
|
29493
29517
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29494
29518
|
return shared;
|
|
29495
29519
|
}
|
|
@@ -29501,13 +29525,13 @@ function everyRunRecord() {
|
|
|
29501
29525
|
for (const directory of runtimeDirectories()) {
|
|
29502
29526
|
let entries;
|
|
29503
29527
|
try {
|
|
29504
|
-
entries = readdirSync5(
|
|
29528
|
+
entries = readdirSync5(join15(directory, "runs"));
|
|
29505
29529
|
} catch {
|
|
29506
29530
|
continue;
|
|
29507
29531
|
}
|
|
29508
29532
|
for (const entry of entries) {
|
|
29509
29533
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29510
|
-
const record = recordAt(
|
|
29534
|
+
const record = recordAt(join15(directory, "runs", entry));
|
|
29511
29535
|
if (record !== null) {
|
|
29512
29536
|
found.push(record);
|
|
29513
29537
|
}
|
|
@@ -29519,7 +29543,7 @@ function everyRunRecord() {
|
|
|
29519
29543
|
function recordAt(path) {
|
|
29520
29544
|
let parsed;
|
|
29521
29545
|
try {
|
|
29522
|
-
parsed = JSON.parse(
|
|
29546
|
+
parsed = JSON.parse(readFileSync15(path, "utf8"));
|
|
29523
29547
|
} catch {
|
|
29524
29548
|
return null;
|
|
29525
29549
|
}
|
|
@@ -29565,7 +29589,7 @@ function runningAs(pid) {
|
|
|
29565
29589
|
const began = startedAt(pid, null);
|
|
29566
29590
|
const when = began === null ? {} : { startedAt: began };
|
|
29567
29591
|
if (process.platform === "linux") {
|
|
29568
|
-
const raw =
|
|
29592
|
+
const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
29569
29593
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
29570
29594
|
}
|
|
29571
29595
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -29668,7 +29692,7 @@ function judgeRun(record, running, needed) {
|
|
|
29668
29692
|
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
29669
29693
|
return false;
|
|
29670
29694
|
}
|
|
29671
|
-
const engine = record.command === undefined ? null :
|
|
29695
|
+
const engine = record.command === undefined ? null : basename3(record.command);
|
|
29672
29696
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
29673
29697
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
29674
29698
|
if (wanted !== null && !said.includes(wanted)) {
|
|
@@ -29704,19 +29728,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
29704
29728
|
}
|
|
29705
29729
|
function clearRunRecord(projectPath) {
|
|
29706
29730
|
for (const directory of runtimeDirectories()) {
|
|
29707
|
-
const shared =
|
|
29731
|
+
const shared = join15(directory, "runs", SHARED_NOTE);
|
|
29708
29732
|
const sharedRecord = recordAt(shared);
|
|
29709
29733
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
29710
29734
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
29711
29735
|
try {
|
|
29712
|
-
|
|
29736
|
+
rmSync7(path, { force: true });
|
|
29713
29737
|
} catch {}
|
|
29714
29738
|
}
|
|
29715
29739
|
}
|
|
29716
29740
|
}
|
|
29717
29741
|
function ageMs(path, now) {
|
|
29718
29742
|
try {
|
|
29719
|
-
return now -
|
|
29743
|
+
return now - statSync6(path).mtimeMs;
|
|
29720
29744
|
} catch {
|
|
29721
29745
|
return 0;
|
|
29722
29746
|
}
|
|
@@ -29734,29 +29758,29 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
29734
29758
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
29735
29759
|
continue;
|
|
29736
29760
|
}
|
|
29737
|
-
const path =
|
|
29761
|
+
const path = join15(directory, entry);
|
|
29738
29762
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
29739
29763
|
continue;
|
|
29740
29764
|
}
|
|
29741
29765
|
try {
|
|
29742
|
-
|
|
29766
|
+
rmSync7(path);
|
|
29743
29767
|
} catch {}
|
|
29744
29768
|
}
|
|
29745
29769
|
}
|
|
29746
29770
|
|
|
29747
29771
|
// src/setup.ts
|
|
29748
|
-
import { createHash as
|
|
29772
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
29749
29773
|
import {
|
|
29750
29774
|
cpSync,
|
|
29751
|
-
existsSync as
|
|
29775
|
+
existsSync as existsSync10,
|
|
29752
29776
|
mkdirSync as mkdirSync9,
|
|
29753
29777
|
readdirSync as readdirSync6,
|
|
29754
|
-
readFileSync as
|
|
29755
|
-
rmSync as
|
|
29756
|
-
statSync as
|
|
29757
|
-
writeFileSync as
|
|
29778
|
+
readFileSync as readFileSync16,
|
|
29779
|
+
rmSync as rmSync8,
|
|
29780
|
+
statSync as statSync7,
|
|
29781
|
+
writeFileSync as writeFileSync11
|
|
29758
29782
|
} from "node:fs";
|
|
29759
|
-
import { dirname as dirname10, join as
|
|
29783
|
+
import { dirname as dirname10, join as join16 } from "node:path";
|
|
29760
29784
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
29761
29785
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
29762
29786
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -29767,27 +29791,27 @@ var RUNTIME_AUTOLOAD = {
|
|
|
29767
29791
|
var VERSION_MARKER = ".gdharness-version";
|
|
29768
29792
|
var DIGEST_MARKER = ".gdharness-digest";
|
|
29769
29793
|
function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
29770
|
-
const hash =
|
|
29794
|
+
const hash = createHash5("sha256");
|
|
29771
29795
|
const walk = (directory, relative) => {
|
|
29772
29796
|
for (const name of readdirSync6(directory).sort()) {
|
|
29773
29797
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
29774
29798
|
continue;
|
|
29775
29799
|
}
|
|
29776
|
-
const path =
|
|
29800
|
+
const path = join16(directory, name);
|
|
29777
29801
|
const inside = `${relative}/${name}`;
|
|
29778
|
-
if (
|
|
29802
|
+
if (statSync7(path).isDirectory()) {
|
|
29779
29803
|
walk(path, inside);
|
|
29780
29804
|
} else {
|
|
29781
29805
|
hash.update(`${inside}
|
|
29782
29806
|
`);
|
|
29783
|
-
hash.update(
|
|
29807
|
+
hash.update(readFileSync16(path));
|
|
29784
29808
|
hash.update(`
|
|
29785
29809
|
`);
|
|
29786
29810
|
}
|
|
29787
29811
|
}
|
|
29788
29812
|
};
|
|
29789
29813
|
for (const name of EDITOR_PLUGINS) {
|
|
29790
|
-
walk(
|
|
29814
|
+
walk(join16(from, name), name);
|
|
29791
29815
|
}
|
|
29792
29816
|
return hash.digest("hex");
|
|
29793
29817
|
}
|
|
@@ -29803,14 +29827,14 @@ function shippedEditorDigest() {
|
|
|
29803
29827
|
return shippedDigest ?? undefined;
|
|
29804
29828
|
}
|
|
29805
29829
|
function shippedAddonsDirectory() {
|
|
29806
|
-
return
|
|
29830
|
+
return join16(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
29807
29831
|
}
|
|
29808
29832
|
function installedAddonVersion(projectPath) {
|
|
29809
|
-
const marker =
|
|
29810
|
-
if (!
|
|
29833
|
+
const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
29834
|
+
if (!existsSync10(marker)) {
|
|
29811
29835
|
return null;
|
|
29812
29836
|
}
|
|
29813
|
-
const said =
|
|
29837
|
+
const said = readFileSync16(marker, "utf8").trim();
|
|
29814
29838
|
return said === "" ? null : said;
|
|
29815
29839
|
}
|
|
29816
29840
|
|
|
@@ -30001,7 +30025,7 @@ var TOOL_SPECS = [
|
|
|
30001
30025
|
},
|
|
30002
30026
|
{
|
|
30003
30027
|
name: "project_settings",
|
|
30004
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses.",
|
|
30028
|
+
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
30029
|
parameters: {
|
|
30006
30030
|
projectPath: PROJECT_PATH,
|
|
30007
30031
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
@@ -30113,19 +30137,30 @@ var TOOL_SPECS = [
|
|
|
30113
30137
|
description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
|
|
30114
30138
|
additionalProperties: true
|
|
30115
30139
|
},
|
|
30116
|
-
reimport: {
|
|
30117
|
-
|
|
30140
|
+
reimport: {
|
|
30141
|
+
type: "boolean",
|
|
30142
|
+
description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
|
|
30143
|
+
},
|
|
30144
|
+
force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
|
|
30145
|
+
timeoutMs: {
|
|
30146
|
+
type: "integer",
|
|
30147
|
+
minimum: 1,
|
|
30148
|
+
description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
|
|
30149
|
+
}
|
|
30118
30150
|
},
|
|
30119
30151
|
requires: ["projectPath"],
|
|
30120
30152
|
operations: {
|
|
30121
30153
|
status: {
|
|
30122
|
-
summary: "which resources are outdated or failed, or one resource with resourcePath,
|
|
30154
|
+
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
30155
|
requires: []
|
|
30124
30156
|
},
|
|
30125
30157
|
options: { summary: "the import options of one resource", requires: ["resourcePath"] },
|
|
30126
|
-
set_options: {
|
|
30158
|
+
set_options: {
|
|
30159
|
+
summary: "change import options in the sidecar, then apply them with a reimport of that resource",
|
|
30160
|
+
requires: ["resourcePath", "options"]
|
|
30161
|
+
},
|
|
30127
30162
|
reimport: {
|
|
30128
|
-
summary: "reimport one resource, or
|
|
30163
|
+
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
30164
|
requires: []
|
|
30130
30165
|
},
|
|
30131
30166
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
@@ -30427,7 +30462,7 @@ var TOOL_SPECS = [
|
|
|
30427
30462
|
},
|
|
30428
30463
|
{
|
|
30429
30464
|
name: "editor_launch",
|
|
30430
|
-
description: "Opens the Godot editor on a project,
|
|
30465
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. An editor this opens is headless: it shows no window and takes no focus, and it serves the bridge, the language server, the debug adapter and the games it plays as a windowed one does. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, headless as before. 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.",
|
|
30431
30466
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
30432
30467
|
requires: [],
|
|
30433
30468
|
operations: {
|
|
@@ -30453,6 +30488,11 @@ var TOOL_SPECS = [
|
|
|
30453
30488
|
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
30489
|
},
|
|
30455
30490
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
30491
|
+
visible: {
|
|
30492
|
+
type: "boolean",
|
|
30493
|
+
ops: ["start"],
|
|
30494
|
+
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 the editor plays goes there too, since an editor this server opens is on that desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
30495
|
+
},
|
|
30456
30496
|
savesIn: {
|
|
30457
30497
|
type: "string",
|
|
30458
30498
|
ops: ["start", "check"],
|
|
@@ -30783,7 +30823,7 @@ var TOOL_SPECS = [
|
|
|
30783
30823
|
},
|
|
30784
30824
|
{
|
|
30785
30825
|
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.",
|
|
30826
|
+
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
30827
|
parameters: {
|
|
30788
30828
|
projectPath: RUNNING_PROJECT_PATH,
|
|
30789
30829
|
pid: RUNNING_PID,
|
|
@@ -31091,15 +31131,15 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
31091
31131
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
31092
31132
|
}
|
|
31093
31133
|
const directory = realPathOr(dirname11(outputPath));
|
|
31094
|
-
if (!
|
|
31134
|
+
if (!existsSync11(directory)) {
|
|
31095
31135
|
return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
|
|
31096
31136
|
}
|
|
31097
|
-
const where =
|
|
31137
|
+
const where = join17(directory, basename4(outputPath));
|
|
31098
31138
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
31099
31139
|
if (inside !== undefined) {
|
|
31100
31140
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
31101
31141
|
}
|
|
31102
|
-
if (
|
|
31142
|
+
if (existsSync11(where)) {
|
|
31103
31143
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
31104
31144
|
}
|
|
31105
31145
|
return null;
|
|
@@ -31477,6 +31517,34 @@ function scanWaitAnswer(scanned) {
|
|
|
31477
31517
|
var PLAYING_STATUS_MS = 1000;
|
|
31478
31518
|
var SCAN_START_MS = 2000;
|
|
31479
31519
|
var SCAN_SETTLED_MS = 500;
|
|
31520
|
+
var REIMPORT_TIMEOUT_MS = 300000;
|
|
31521
|
+
var AUDIO_BUS_OPS = new Set([
|
|
31522
|
+
"add_audio_bus",
|
|
31523
|
+
"set_audio_bus_effect",
|
|
31524
|
+
"set_audio_bus_volume"
|
|
31525
|
+
]);
|
|
31526
|
+
function changedSettings(before, after) {
|
|
31527
|
+
if (before === null || after === null) {
|
|
31528
|
+
return null;
|
|
31529
|
+
}
|
|
31530
|
+
const names = new Set([...before.keys(), ...after.keys()]);
|
|
31531
|
+
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
31532
|
+
}
|
|
31533
|
+
function importStatuses(payload) {
|
|
31534
|
+
const listed = payload["resources"];
|
|
31535
|
+
if (!Array.isArray(listed)) {
|
|
31536
|
+
return [];
|
|
31537
|
+
}
|
|
31538
|
+
return listed.map((entry) => {
|
|
31539
|
+
const one = asParams(entry);
|
|
31540
|
+
const reason = readString(one, "reason");
|
|
31541
|
+
return {
|
|
31542
|
+
path: readString(one, "path") ?? "",
|
|
31543
|
+
status: readString(one, "status") ?? "unknown",
|
|
31544
|
+
...reason === undefined ? {} : { reason }
|
|
31545
|
+
};
|
|
31546
|
+
});
|
|
31547
|
+
}
|
|
31480
31548
|
var WAIT_FOR_RUN_MS = 600000;
|
|
31481
31549
|
var RUN_POLL_MS = 250;
|
|
31482
31550
|
var PATH_SOLUTIONS = [
|
|
@@ -31538,7 +31606,7 @@ function aboveTheRunner(entry) {
|
|
|
31538
31606
|
}
|
|
31539
31607
|
function elsewhereIn(projectPath, asked) {
|
|
31540
31608
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
31541
|
-
if (wanted === "" ||
|
|
31609
|
+
if (wanted === "" || existsSync11(join17(projectPath, wanted))) {
|
|
31542
31610
|
return "";
|
|
31543
31611
|
}
|
|
31544
31612
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -31552,7 +31620,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
31552
31620
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
31553
31621
|
}
|
|
31554
31622
|
function hasMainScene(projectFile) {
|
|
31555
|
-
const scene = parseProjectGodot(
|
|
31623
|
+
const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
31556
31624
|
return typeof scene === "string" && scene !== "";
|
|
31557
31625
|
}
|
|
31558
31626
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -31560,13 +31628,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
31560
31628
|
return false;
|
|
31561
31629
|
}
|
|
31562
31630
|
try {
|
|
31563
|
-
return
|
|
31631
|
+
return readFileSync17(join17(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
31564
31632
|
} catch {
|
|
31565
31633
|
return false;
|
|
31566
31634
|
}
|
|
31567
31635
|
}
|
|
31568
31636
|
function editorPlaysHeadless(projectFile) {
|
|
31569
|
-
const runArgs = parseProjectGodot(
|
|
31637
|
+
const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
31570
31638
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
31571
31639
|
}
|
|
31572
31640
|
function camelCased(params) {
|
|
@@ -31585,7 +31653,7 @@ function needsItsOwnProcess(asked) {
|
|
|
31585
31653
|
class GodotServer {
|
|
31586
31654
|
mcp;
|
|
31587
31655
|
locator = new GodotLocator;
|
|
31588
|
-
operationsScript =
|
|
31656
|
+
operationsScript = join17(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
31589
31657
|
godotBridge;
|
|
31590
31658
|
tools = buildToolDefinitions();
|
|
31591
31659
|
activeProcess = null;
|
|
@@ -32030,7 +32098,14 @@ class GodotServer {
|
|
|
32030
32098
|
}
|
|
32031
32099
|
async dispatch(tool, op, args) {
|
|
32032
32100
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
32033
|
-
return await this.handleRefreshUids(args);
|
|
32101
|
+
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
32102
|
+
}
|
|
32103
|
+
const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
|
|
32104
|
+
if (settingsWrite !== undefined) {
|
|
32105
|
+
return await this.handleProjectSettingsWrite(op, settingsWrite, args);
|
|
32106
|
+
}
|
|
32107
|
+
if (tool === "project_import" && op === "set_options") {
|
|
32108
|
+
return await this.handleSetImportOptions(args);
|
|
32034
32109
|
}
|
|
32035
32110
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
32036
32111
|
if (headless !== undefined) {
|
|
@@ -32289,8 +32364,8 @@ class GodotServer {
|
|
|
32289
32364
|
if (path === undefined) {
|
|
32290
32365
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
32291
32366
|
}
|
|
32292
|
-
const file =
|
|
32293
|
-
if (!
|
|
32367
|
+
const file = join17(path, "project.godot");
|
|
32368
|
+
if (!existsSync11(file)) {
|
|
32294
32369
|
return {
|
|
32295
32370
|
ok: false,
|
|
32296
32371
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -32561,6 +32636,222 @@ class GodotServer {
|
|
|
32561
32636
|
}
|
|
32562
32637
|
}, scanned));
|
|
32563
32638
|
}
|
|
32639
|
+
async handleReimport(args) {
|
|
32640
|
+
const project = this.project(args);
|
|
32641
|
+
if (!project.ok) {
|
|
32642
|
+
return project.response;
|
|
32643
|
+
}
|
|
32644
|
+
const contained = this.containProjectFiles(args);
|
|
32645
|
+
if (!contained.ok) {
|
|
32646
|
+
return contained.response;
|
|
32647
|
+
}
|
|
32648
|
+
const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
32649
|
+
return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
|
|
32650
|
+
}
|
|
32651
|
+
editorHasOpen(projectPath) {
|
|
32652
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
32653
|
+
return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
|
|
32654
|
+
}
|
|
32655
|
+
async handleProjectSettingsWrite(op, operation, args) {
|
|
32656
|
+
const project = this.project(args);
|
|
32657
|
+
if (!project.ok) {
|
|
32658
|
+
return project.response;
|
|
32659
|
+
}
|
|
32660
|
+
const projectPath = project.value.path;
|
|
32661
|
+
const served = this.editorHasOpen(projectPath);
|
|
32662
|
+
const before = served ? this.settingKeysOf(projectPath) : null;
|
|
32663
|
+
const written = await this.headless(operation, args);
|
|
32664
|
+
if (!served || written.isError === true) {
|
|
32665
|
+
return written;
|
|
32666
|
+
}
|
|
32667
|
+
const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
|
|
32668
|
+
if (AUDIO_BUS_OPS.has(op)) {
|
|
32669
|
+
return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
|
|
32670
|
+
}
|
|
32671
|
+
const changed = changedSettings(before, this.settingKeysOf(projectPath));
|
|
32672
|
+
if (changed === null) {
|
|
32673
|
+
return this.jsonTextResponse({
|
|
32674
|
+
...answer,
|
|
32675
|
+
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."
|
|
32676
|
+
});
|
|
32677
|
+
}
|
|
32678
|
+
if (changed.length === 0) {
|
|
32679
|
+
return written;
|
|
32680
|
+
}
|
|
32681
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
|
|
32682
|
+
return this.jsonTextResponse({
|
|
32683
|
+
...answer,
|
|
32684
|
+
...taken,
|
|
32685
|
+
...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
|
|
32686
|
+
});
|
|
32687
|
+
}
|
|
32688
|
+
async editorTakes(command, args) {
|
|
32689
|
+
try {
|
|
32690
|
+
await this.godotBridge.invokeTool(command, args);
|
|
32691
|
+
return {};
|
|
32692
|
+
} catch (error) {
|
|
32693
|
+
const message = errorMessage(error);
|
|
32694
|
+
const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
|
|
32695
|
+
return {
|
|
32696
|
+
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.`
|
|
32697
|
+
};
|
|
32698
|
+
}
|
|
32699
|
+
}
|
|
32700
|
+
async handleSetImportOptions(args) {
|
|
32701
|
+
const project = this.project(args);
|
|
32702
|
+
if (!project.ok) {
|
|
32703
|
+
return project.response;
|
|
32704
|
+
}
|
|
32705
|
+
const contained = this.containProjectFiles(args);
|
|
32706
|
+
if (!contained.ok) {
|
|
32707
|
+
return contained.response;
|
|
32708
|
+
}
|
|
32709
|
+
const {
|
|
32710
|
+
op: _op,
|
|
32711
|
+
projectPath: _projectPath,
|
|
32712
|
+
reimport: _reimport,
|
|
32713
|
+
timeoutMs: _timeoutMs,
|
|
32714
|
+
...params
|
|
32715
|
+
} = contained.value;
|
|
32716
|
+
const written = await this.operation("set_import_options", params, project.value.path);
|
|
32717
|
+
if (!written.ok || args["reimport"] === false) {
|
|
32718
|
+
return this.answer(written.ok ? {
|
|
32719
|
+
...written,
|
|
32720
|
+
payload: {
|
|
32721
|
+
...written.payload,
|
|
32722
|
+
note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
|
|
32723
|
+
}
|
|
32724
|
+
} : written);
|
|
32725
|
+
}
|
|
32726
|
+
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
32727
|
+
if (!applied.ok) {
|
|
32728
|
+
return applied.response;
|
|
32729
|
+
}
|
|
32730
|
+
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
32731
|
+
}
|
|
32732
|
+
async reimport(projectPath, resourcePath, force, timeoutMs) {
|
|
32733
|
+
const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
|
|
32734
|
+
const before = await this.operation("get_import_status", ask, projectPath);
|
|
32735
|
+
if (!before.ok) {
|
|
32736
|
+
return { ok: false, response: this.answer(before) };
|
|
32737
|
+
}
|
|
32738
|
+
const listed = importStatuses(before.payload);
|
|
32739
|
+
const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
|
|
32740
|
+
if (resourcePath !== undefined && missingSource.length > 0) {
|
|
32741
|
+
return {
|
|
32742
|
+
ok: false,
|
|
32743
|
+
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
32744
|
+
};
|
|
32745
|
+
}
|
|
32746
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
32747
|
+
if (targets.length === 0) {
|
|
32748
|
+
return {
|
|
32749
|
+
ok: true,
|
|
32750
|
+
payload: {
|
|
32751
|
+
reimported: [],
|
|
32752
|
+
notReimported: [],
|
|
32753
|
+
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.`
|
|
32754
|
+
}
|
|
32755
|
+
};
|
|
32756
|
+
}
|
|
32757
|
+
const viaEditor = this.editorHasOpen(projectPath);
|
|
32758
|
+
const extra = {};
|
|
32759
|
+
if (viaEditor) {
|
|
32760
|
+
const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
|
|
32761
|
+
if (!inEditor.ok) {
|
|
32762
|
+
return inEditor;
|
|
32763
|
+
}
|
|
32764
|
+
if (!inEditor.finished) {
|
|
32765
|
+
return {
|
|
32766
|
+
ok: true,
|
|
32767
|
+
payload: {
|
|
32768
|
+
via: "editor",
|
|
32769
|
+
stillImporting: true,
|
|
32770
|
+
asked: targets,
|
|
32771
|
+
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.`
|
|
32772
|
+
}
|
|
32773
|
+
};
|
|
32774
|
+
}
|
|
32775
|
+
} else {
|
|
32776
|
+
const engine = await this.engine();
|
|
32777
|
+
if (!engine.ok) {
|
|
32778
|
+
return { ok: false, response: engine.response };
|
|
32779
|
+
}
|
|
32780
|
+
for (const target of targets) {
|
|
32781
|
+
forceNextImport(projectPath, target);
|
|
32782
|
+
}
|
|
32783
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
32784
|
+
const imported = await runImport(engine.value, projectPath);
|
|
32785
|
+
if (!imported.ok) {
|
|
32786
|
+
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
32787
|
+
}
|
|
32788
|
+
if (imported.messages.length > 0) {
|
|
32789
|
+
extra["engine_messages"] = imported.messages;
|
|
32790
|
+
}
|
|
32791
|
+
}
|
|
32792
|
+
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
32793
|
+
if (!after.ok) {
|
|
32794
|
+
return { ok: false, response: this.answer(after) };
|
|
32795
|
+
}
|
|
32796
|
+
const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
|
|
32797
|
+
const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
|
|
32798
|
+
const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
|
|
32799
|
+
path,
|
|
32800
|
+
status: now.get(path)?.status ?? "unknown",
|
|
32801
|
+
...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
|
|
32802
|
+
}));
|
|
32803
|
+
return {
|
|
32804
|
+
ok: true,
|
|
32805
|
+
payload: {
|
|
32806
|
+
via: viaEditor ? "editor" : "engine",
|
|
32807
|
+
reimported,
|
|
32808
|
+
notReimported,
|
|
32809
|
+
...missingSource.length > 0 ? { missingSource } : {},
|
|
32810
|
+
...extra
|
|
32811
|
+
}
|
|
32812
|
+
};
|
|
32813
|
+
}
|
|
32814
|
+
async reimportInTheEditor(paths, timeoutMs) {
|
|
32815
|
+
const started = Date.now();
|
|
32816
|
+
let completedBefore;
|
|
32817
|
+
for (;; ) {
|
|
32818
|
+
while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
|
|
32819
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
32820
|
+
}
|
|
32821
|
+
let answer;
|
|
32822
|
+
try {
|
|
32823
|
+
answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
|
|
32824
|
+
} catch (error) {
|
|
32825
|
+
const message = errorMessage(error);
|
|
32826
|
+
return {
|
|
32827
|
+
ok: false,
|
|
32828
|
+
response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
|
|
32829
|
+
"editor_launch restart loads the addon this server ships",
|
|
32830
|
+
"Or close the editor, and project_import reimport runs the engine import instead"
|
|
32831
|
+
]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
|
|
32832
|
+
};
|
|
32833
|
+
}
|
|
32834
|
+
if (answer["started"] !== false) {
|
|
32835
|
+
completedBefore = readNumber(answer, "reimportsCompletedBefore");
|
|
32836
|
+
break;
|
|
32837
|
+
}
|
|
32838
|
+
if (Date.now() - started >= timeoutMs) {
|
|
32839
|
+
return { ok: true, finished: false };
|
|
32840
|
+
}
|
|
32841
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
32842
|
+
}
|
|
32843
|
+
while (Date.now() - started < timeoutMs) {
|
|
32844
|
+
await new Promise((settle) => setTimeout(settle, 250));
|
|
32845
|
+
try {
|
|
32846
|
+
const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
32847
|
+
const completed = readNumber(status, "reimportsCompleted");
|
|
32848
|
+
if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
|
|
32849
|
+
return { ok: true, finished: true };
|
|
32850
|
+
}
|
|
32851
|
+
} catch {}
|
|
32852
|
+
}
|
|
32853
|
+
return { ok: true, finished: false };
|
|
32854
|
+
}
|
|
32564
32855
|
async operation(operation, params, projectPath) {
|
|
32565
32856
|
const engine = await this.engine();
|
|
32566
32857
|
if (!engine.ok) {
|
|
@@ -32602,11 +32893,11 @@ class GodotServer {
|
|
|
32602
32893
|
if (!engine.ok) {
|
|
32603
32894
|
return engine.response;
|
|
32604
32895
|
}
|
|
32605
|
-
const application = parseProjectGodot(
|
|
32896
|
+
const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
|
|
32606
32897
|
const name = application["config/name"];
|
|
32607
32898
|
const mainScene = application["run/main_scene"];
|
|
32608
32899
|
const info = {
|
|
32609
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
32900
|
+
name: typeof name === "string" && name !== "" ? name : basename4(project.value.path),
|
|
32610
32901
|
path: project.value.path,
|
|
32611
32902
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
32612
32903
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -32670,11 +32961,11 @@ class GodotServer {
|
|
|
32670
32961
|
return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
32671
32962
|
}
|
|
32672
32963
|
const debug = readBoolean(args, "debug") ?? false;
|
|
32673
|
-
const exportLogs = mkdtempSync2(
|
|
32964
|
+
const exportLogs = mkdtempSync2(join17(tmpdir4(), "gdharness-export-"));
|
|
32674
32965
|
const exportArgs = [
|
|
32675
32966
|
"--headless",
|
|
32676
32967
|
"--log-file",
|
|
32677
|
-
|
|
32968
|
+
join17(exportLogs, "engine.log"),
|
|
32678
32969
|
"--path",
|
|
32679
32970
|
project.value.path,
|
|
32680
32971
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -32706,7 +32997,7 @@ class GodotServer {
|
|
|
32706
32997
|
log.finish();
|
|
32707
32998
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
32708
32999
|
const verdict = {
|
|
32709
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
33000
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync11(output.absolutePath),
|
|
32710
33001
|
preset,
|
|
32711
33002
|
outputPath: output.relativePath,
|
|
32712
33003
|
debug,
|
|
@@ -32742,7 +33033,7 @@ class GodotServer {
|
|
|
32742
33033
|
return contained.response;
|
|
32743
33034
|
}
|
|
32744
33035
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
32745
|
-
if (!
|
|
33036
|
+
if (!existsSync11(join17(project.value.path, runner))) {
|
|
32746
33037
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
32747
33038
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
32748
33039
|
]);
|
|
@@ -32778,7 +33069,7 @@ class GodotServer {
|
|
|
32778
33069
|
];
|
|
32779
33070
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
32780
33071
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
32781
|
-
const userData = mkdtempSync2(
|
|
33072
|
+
const userData = mkdtempSync2(join17(tmpdir4(), "gdharness-tests-"));
|
|
32782
33073
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
32783
33074
|
const console2 = [];
|
|
32784
33075
|
run.process.stdout?.on("data", (data) => {
|
|
@@ -32798,14 +33089,14 @@ class GodotServer {
|
|
|
32798
33089
|
resolve(false);
|
|
32799
33090
|
});
|
|
32800
33091
|
});
|
|
32801
|
-
const reportsDir =
|
|
33092
|
+
const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
|
|
32802
33093
|
let report = null;
|
|
32803
33094
|
let reportProblem = null;
|
|
32804
33095
|
try {
|
|
32805
|
-
const written =
|
|
33096
|
+
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
33097
|
const newest = written.at(-1);
|
|
32807
33098
|
if (newest !== undefined) {
|
|
32808
|
-
report = parseJUnit(
|
|
33099
|
+
report = parseJUnit(readFileSync17(join17(reportsDir, newest, "results.xml"), "utf8"));
|
|
32809
33100
|
}
|
|
32810
33101
|
} catch (error) {
|
|
32811
33102
|
reportProblem = errorMessage(error);
|
|
@@ -32948,11 +33239,11 @@ class GodotServer {
|
|
|
32948
33239
|
const cached = cachedClasses(projectPath);
|
|
32949
33240
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
32950
33241
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
32951
|
-
if (!contained.ok || !
|
|
33242
|
+
if (!contained.ok || !existsSync11(contained.absolutePath)) {
|
|
32952
33243
|
return null;
|
|
32953
33244
|
}
|
|
32954
33245
|
try {
|
|
32955
|
-
return
|
|
33246
|
+
return readFileSync17(contained.absolutePath, "utf8");
|
|
32956
33247
|
} catch {
|
|
32957
33248
|
return null;
|
|
32958
33249
|
}
|
|
@@ -33002,11 +33293,11 @@ class GodotServer {
|
|
|
33002
33293
|
}
|
|
33003
33294
|
sourceOfClass(projectPath, resourcePath) {
|
|
33004
33295
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33005
|
-
if (!contained.ok || !
|
|
33296
|
+
if (!contained.ok || !existsSync11(contained.absolutePath)) {
|
|
33006
33297
|
return null;
|
|
33007
33298
|
}
|
|
33008
33299
|
try {
|
|
33009
|
-
return
|
|
33300
|
+
return readFileSync17(contained.absolutePath, "utf8");
|
|
33010
33301
|
} catch {
|
|
33011
33302
|
return null;
|
|
33012
33303
|
}
|
|
@@ -33275,7 +33566,7 @@ class GodotServer {
|
|
|
33275
33566
|
return null;
|
|
33276
33567
|
}
|
|
33277
33568
|
try {
|
|
33278
|
-
return settingKeys(
|
|
33569
|
+
return settingKeys(readFileSync17(join17(projectPath, "project.godot"), "utf8"));
|
|
33279
33570
|
} catch {
|
|
33280
33571
|
return null;
|
|
33281
33572
|
}
|
|
@@ -33415,7 +33706,8 @@ class GodotServer {
|
|
|
33415
33706
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
33416
33707
|
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
33417
33708
|
[OPENED_BY_A_SERVER]: "1"
|
|
33418
|
-
}
|
|
33709
|
+
},
|
|
33710
|
+
desktop: HIDDEN_DESKTOP
|
|
33419
33711
|
});
|
|
33420
33712
|
if ("error" in launched) {
|
|
33421
33713
|
return { pid: null, error: launched.error };
|
|
@@ -33497,7 +33789,7 @@ class GodotServer {
|
|
|
33497
33789
|
userArgs: given.value
|
|
33498
33790
|
});
|
|
33499
33791
|
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);
|
|
33792
|
+
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
33793
|
if ("error" in started) {
|
|
33502
33794
|
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
33503
33795
|
"editor_status says which Godot this server runs and whether it answers"
|
|
@@ -33527,7 +33819,7 @@ class GodotServer {
|
|
|
33527
33819
|
}
|
|
33528
33820
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
33529
33821
|
const budgetMs = budget.ms;
|
|
33530
|
-
if (!
|
|
33822
|
+
if (!existsSync11(join17(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
33531
33823
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
33532
33824
|
}
|
|
33533
33825
|
let asking = null;
|
|
@@ -33590,7 +33882,7 @@ class GodotServer {
|
|
|
33590
33882
|
}
|
|
33591
33883
|
let announcedAt;
|
|
33592
33884
|
try {
|
|
33593
|
-
announcedAt =
|
|
33885
|
+
announcedAt = statSync8(endpoint.file).mtimeMs;
|
|
33594
33886
|
} catch {
|
|
33595
33887
|
return;
|
|
33596
33888
|
}
|
|
@@ -33837,7 +34129,7 @@ class GodotServer {
|
|
|
33837
34129
|
});
|
|
33838
34130
|
return started;
|
|
33839
34131
|
}
|
|
33840
|
-
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
34132
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
33841
34133
|
const startedAt = Date.now();
|
|
33842
34134
|
const transcript = openTranscript(startedAt);
|
|
33843
34135
|
closeSync2(transcript.fd);
|
|
@@ -33845,7 +34137,8 @@ class GodotServer {
|
|
|
33845
34137
|
command: godotPath,
|
|
33846
34138
|
args: cmdArgs,
|
|
33847
34139
|
...env === undefined ? {} : { env },
|
|
33848
|
-
run: { transcript: transcript.path, startedAt, projectPath }
|
|
34140
|
+
run: { transcript: transcript.path, startedAt, projectPath },
|
|
34141
|
+
...desktop === undefined ? {} : { desktop }
|
|
33849
34142
|
});
|
|
33850
34143
|
if ("error" in launched) {
|
|
33851
34144
|
return launched;
|
|
@@ -34340,7 +34633,7 @@ class GodotServer {
|
|
|
34340
34633
|
notes.push(endedWithoutACode({
|
|
34341
34634
|
throughEditor: run.throughEditor,
|
|
34342
34635
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
34343
|
-
couldAnnounce: run.projectPath !== null &&
|
|
34636
|
+
couldAnnounce: run.projectPath !== null && existsSync11(join17(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
34344
34637
|
endedHere: typeof run.endedHere === "string",
|
|
34345
34638
|
pickedUp: run.pickedUp === true
|
|
34346
34639
|
}));
|
|
@@ -34566,6 +34859,7 @@ class GodotServer {
|
|
|
34566
34859
|
const ghosts = blind.stillHeld ?? [];
|
|
34567
34860
|
let waitedForEditorMs = 0;
|
|
34568
34861
|
let first;
|
|
34862
|
+
let completedBefore;
|
|
34569
34863
|
for (;; ) {
|
|
34570
34864
|
const busySince = Date.now();
|
|
34571
34865
|
let wasBusy = false;
|
|
@@ -34580,8 +34874,9 @@ class GodotServer {
|
|
|
34580
34874
|
if (first.isError) {
|
|
34581
34875
|
return first;
|
|
34582
34876
|
}
|
|
34583
|
-
const
|
|
34584
|
-
|
|
34877
|
+
const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
|
|
34878
|
+
completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
|
|
34879
|
+
if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
|
|
34585
34880
|
break;
|
|
34586
34881
|
}
|
|
34587
34882
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -34592,7 +34887,9 @@ class GodotServer {
|
|
|
34592
34887
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
34593
34888
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
34594
34889
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
34595
|
-
|
|
34890
|
+
const completed = readNumber(status, "scansCompleted");
|
|
34891
|
+
const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
|
|
34892
|
+
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending || notYetOver;
|
|
34596
34893
|
if (busy) {
|
|
34597
34894
|
idleSince = null;
|
|
34598
34895
|
} else {
|
|
@@ -34849,7 +35146,7 @@ class GodotServer {
|
|
|
34849
35146
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
34850
35147
|
const run = this.currentRun();
|
|
34851
35148
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
34852
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
35149
|
+
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)) {
|
|
34853
35150
|
return null;
|
|
34854
35151
|
}
|
|
34855
35152
|
const already = runtimesAnnounced().running;
|
|
@@ -34858,7 +35155,7 @@ class GodotServer {
|
|
|
34858
35155
|
return !run.announcedBefore.has(one.pid);
|
|
34859
35156
|
}
|
|
34860
35157
|
try {
|
|
34861
|
-
return
|
|
35158
|
+
return statSync8(one.file).mtimeMs >= run.startedAt;
|
|
34862
35159
|
} catch {
|
|
34863
35160
|
return false;
|
|
34864
35161
|
}
|
|
@@ -34928,8 +35225,8 @@ class GodotServer {
|
|
|
34928
35225
|
return this.createErrorResponse(refusal);
|
|
34929
35226
|
}
|
|
34930
35227
|
}
|
|
34931
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync2(
|
|
34932
|
-
const screenshotPath = screenshotDir ?
|
|
35228
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync2(join17(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
|
|
35229
|
+
const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
|
|
34933
35230
|
try {
|
|
34934
35231
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
34935
35232
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -34970,7 +35267,7 @@ class GodotServer {
|
|
|
34970
35267
|
type: "text",
|
|
34971
35268
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
34972
35269
|
},
|
|
34973
|
-
{ type: "image", data:
|
|
35270
|
+
{ type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
34974
35271
|
]
|
|
34975
35272
|
};
|
|
34976
35273
|
} finally {
|