gdharness 1.1.5 → 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 +389 -93
- 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 +398 -105
- package/build/keeper.js +205 -23
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -388,6 +388,7 @@ function runArguments(options) {
|
|
|
388
388
|
function editorArguments(projectPath, ports, logFile) {
|
|
389
389
|
return [
|
|
390
390
|
"-e",
|
|
391
|
+
"--headless",
|
|
391
392
|
"--path",
|
|
392
393
|
projectPath,
|
|
393
394
|
"--lsp-port",
|
|
@@ -15874,7 +15875,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15874
15875
|
},
|
|
15875
15876
|
{
|
|
15876
15877
|
name: "project_settings",
|
|
15877
|
-
description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses.",
|
|
15878
|
+
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.",
|
|
15878
15879
|
parameters: {
|
|
15879
15880
|
projectPath: PROJECT_PATH,
|
|
15880
15881
|
setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
|
|
@@ -15986,19 +15987,30 @@ var init_tool_definitions = __esm(() => {
|
|
|
15986
15987
|
description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
|
|
15987
15988
|
additionalProperties: true
|
|
15988
15989
|
},
|
|
15989
|
-
reimport: {
|
|
15990
|
-
|
|
15990
|
+
reimport: {
|
|
15991
|
+
type: "boolean",
|
|
15992
|
+
description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
|
|
15993
|
+
},
|
|
15994
|
+
force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
|
|
15995
|
+
timeoutMs: {
|
|
15996
|
+
type: "integer",
|
|
15997
|
+
minimum: 1,
|
|
15998
|
+
description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
|
|
15999
|
+
}
|
|
15991
16000
|
},
|
|
15992
16001
|
requires: ["projectPath"],
|
|
15993
16002
|
operations: {
|
|
15994
16003
|
status: {
|
|
15995
|
-
summary: "which resources are outdated or failed, or one resource with resourcePath,
|
|
16004
|
+
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",
|
|
15996
16005
|
requires: []
|
|
15997
16006
|
},
|
|
15998
16007
|
options: { summary: "the import options of one resource", requires: ["resourcePath"] },
|
|
15999
|
-
set_options: {
|
|
16008
|
+
set_options: {
|
|
16009
|
+
summary: "change import options in the sidecar, then apply them with a reimport of that resource",
|
|
16010
|
+
requires: ["resourcePath", "options"]
|
|
16011
|
+
},
|
|
16000
16012
|
reimport: {
|
|
16001
|
-
summary: "reimport one resource, or
|
|
16013
|
+
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",
|
|
16002
16014
|
requires: []
|
|
16003
16015
|
},
|
|
16004
16016
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
@@ -16300,7 +16312,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16300
16312
|
},
|
|
16301
16313
|
{
|
|
16302
16314
|
name: "editor_launch",
|
|
16303
|
-
description: "Opens the Godot editor on a project,
|
|
16315
|
+
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.",
|
|
16304
16316
|
parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
|
|
16305
16317
|
requires: [],
|
|
16306
16318
|
operations: {
|
|
@@ -16326,6 +16338,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16326
16338
|
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.`
|
|
16327
16339
|
},
|
|
16328
16340
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
16341
|
+
visible: {
|
|
16342
|
+
type: "boolean",
|
|
16343
|
+
ops: ["start"],
|
|
16344
|
+
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."
|
|
16345
|
+
},
|
|
16329
16346
|
savesIn: {
|
|
16330
16347
|
type: "string",
|
|
16331
16348
|
ops: ["start", "check"],
|
|
@@ -16656,7 +16673,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16656
16673
|
},
|
|
16657
16674
|
{
|
|
16658
16675
|
name: "runtime_capture",
|
|
16659
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
|
|
16676
|
+
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.",
|
|
16660
16677
|
parameters: {
|
|
16661
16678
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16662
16679
|
pid: RUNNING_PID,
|
|
@@ -16923,7 +16940,6 @@ var init_headless_operations = __esm(() => {
|
|
|
16923
16940
|
status: "get_import_status",
|
|
16924
16941
|
options: "get_import_options",
|
|
16925
16942
|
set_options: "set_import_options",
|
|
16926
|
-
reimport: "reimport_resource",
|
|
16927
16943
|
uid: "get_uid",
|
|
16928
16944
|
refresh_classes: "refresh_class_cache"
|
|
16929
16945
|
}),
|
|
@@ -16937,7 +16953,7 @@ var init_headless_operations = __esm(() => {
|
|
|
16937
16953
|
})
|
|
16938
16954
|
});
|
|
16939
16955
|
ENGINE_PASSES = dictionary({
|
|
16940
|
-
project_import: dictionary({ refresh_uids: "import" })
|
|
16956
|
+
project_import: dictionary({ refresh_uids: "import", reimport: "import" })
|
|
16941
16957
|
});
|
|
16942
16958
|
EDITOR_READS = dictionary({
|
|
16943
16959
|
project_settings: dictionary({ get: "get_project_setting" })
|
|
@@ -33091,6 +33107,10 @@ var init_dap_client = __esm(() => {
|
|
|
33091
33107
|
init_ports();
|
|
33092
33108
|
});
|
|
33093
33109
|
|
|
33110
|
+
// src/desktop.ts
|
|
33111
|
+
var HIDDEN_DESKTOP = "gdharness";
|
|
33112
|
+
var init_desktop = () => {};
|
|
33113
|
+
|
|
33094
33114
|
// src/editor-log.ts
|
|
33095
33115
|
import {
|
|
33096
33116
|
closeSync,
|
|
@@ -37786,7 +37806,8 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
37786
37806
|
command: spec.command,
|
|
37787
37807
|
args: spec.args,
|
|
37788
37808
|
envChanges: changesFrom(spec.env, process.env),
|
|
37789
|
-
...spec.run === undefined ? {} : { run: spec.run }
|
|
37809
|
+
...spec.run === undefined ? {} : { run: spec.run },
|
|
37810
|
+
...spec.desktop === undefined ? {} : { desktop: spec.desktop }
|
|
37790
37811
|
});
|
|
37791
37812
|
let printed = "";
|
|
37792
37813
|
let complained = "";
|
|
@@ -37980,17 +38001,38 @@ var init_project_scan = __esm(() => {
|
|
|
37980
38001
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
37981
38002
|
});
|
|
37982
38003
|
|
|
38004
|
+
// src/reimport.ts
|
|
38005
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
38006
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, utimesSync, writeFileSync as writeFileSync14 } from "node:fs";
|
|
38007
|
+
import { basename as basename3, join as join21 } from "node:path";
|
|
38008
|
+
function forceNextImport(projectPath, resourcePath) {
|
|
38009
|
+
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
38010
|
+
const sidecar = join21(projectPath, `${relative}.import`);
|
|
38011
|
+
if (!existsSync15(sidecar)) {
|
|
38012
|
+
return;
|
|
38013
|
+
}
|
|
38014
|
+
const { atime, mtimeMs } = statSync7(sidecar);
|
|
38015
|
+
writeFileSync14(sidecar, readFileSync19(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
38016
|
+
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
38017
|
+
rmSync10(importRecord(projectPath, resourcePath), { force: true });
|
|
38018
|
+
}
|
|
38019
|
+
function importRecord(projectPath, resourcePath) {
|
|
38020
|
+
const digest = createHash5("md5").update(resourcePath).digest("hex");
|
|
38021
|
+
return join21(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
|
|
38022
|
+
}
|
|
38023
|
+
var init_reimport = () => {};
|
|
38024
|
+
|
|
37983
38025
|
// src/restart-note.ts
|
|
37984
|
-
import { mkdirSync as mkdirSync12, readFileSync as
|
|
37985
|
-
import { dirname as dirname15, join as
|
|
38026
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
|
|
38027
|
+
import { dirname as dirname15, join as join22 } from "node:path";
|
|
37986
38028
|
function restartNotePath(projectPath) {
|
|
37987
|
-
return
|
|
38029
|
+
return join22(projectPath, ".godot", "gdharness-restart.json");
|
|
37988
38030
|
}
|
|
37989
38031
|
function noteRestartBegun(note) {
|
|
37990
38032
|
const path = restartNotePath(note.projectPath);
|
|
37991
38033
|
try {
|
|
37992
38034
|
mkdirSync12(dirname15(path), { recursive: true });
|
|
37993
|
-
|
|
38035
|
+
writeFileSync15(path, `${JSON.stringify(note, null, 2)}
|
|
37994
38036
|
`, "utf8");
|
|
37995
38037
|
} catch (error) {
|
|
37996
38038
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -37998,7 +38040,7 @@ function noteRestartBegun(note) {
|
|
|
37998
38040
|
}
|
|
37999
38041
|
function restartOwed(projectPath) {
|
|
38000
38042
|
try {
|
|
38001
|
-
const parsed = JSON.parse(
|
|
38043
|
+
const parsed = JSON.parse(readFileSync20(restartNotePath(projectPath), "utf8"));
|
|
38002
38044
|
if (typeof parsed !== "object" || parsed === null) {
|
|
38003
38045
|
return null;
|
|
38004
38046
|
}
|
|
@@ -38013,7 +38055,7 @@ function restartOwed(projectPath) {
|
|
|
38013
38055
|
}
|
|
38014
38056
|
function restartSettled(projectPath) {
|
|
38015
38057
|
try {
|
|
38016
|
-
|
|
38058
|
+
rmSync11(restartNotePath(projectPath), { force: true });
|
|
38017
38059
|
} catch (error) {
|
|
38018
38060
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
38019
38061
|
}
|
|
@@ -38024,33 +38066,33 @@ var init_restart_note = __esm(() => {
|
|
|
38024
38066
|
|
|
38025
38067
|
// src/run-record.ts
|
|
38026
38068
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
38027
|
-
import { createHash as
|
|
38069
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
38028
38070
|
import {
|
|
38029
38071
|
mkdirSync as mkdirSync13,
|
|
38030
38072
|
openSync as openSync2,
|
|
38031
38073
|
readdirSync as readdirSync8,
|
|
38032
|
-
readFileSync as
|
|
38074
|
+
readFileSync as readFileSync21,
|
|
38033
38075
|
realpathSync as realpathSync3,
|
|
38034
|
-
rmSync as
|
|
38035
|
-
statSync as
|
|
38036
|
-
writeFileSync as
|
|
38076
|
+
rmSync as rmSync12,
|
|
38077
|
+
statSync as statSync8,
|
|
38078
|
+
writeFileSync as writeFileSync16
|
|
38037
38079
|
} from "node:fs";
|
|
38038
|
-
import { basename as
|
|
38080
|
+
import { basename as basename4, dirname as dirname16, join as join23, resolve as resolve5 } from "node:path";
|
|
38039
38081
|
function runsDirectory() {
|
|
38040
|
-
return
|
|
38082
|
+
return join23(runtimeDirectory(), "runs");
|
|
38041
38083
|
}
|
|
38042
38084
|
function realAsFarAsItExists(path) {
|
|
38043
38085
|
const missing = [];
|
|
38044
38086
|
let at = path;
|
|
38045
38087
|
for (;; ) {
|
|
38046
38088
|
try {
|
|
38047
|
-
return
|
|
38089
|
+
return join23(realpathSync3.native(at), ...missing);
|
|
38048
38090
|
} catch {
|
|
38049
38091
|
const parent = dirname16(at);
|
|
38050
38092
|
if (parent === at) {
|
|
38051
38093
|
return path;
|
|
38052
38094
|
}
|
|
38053
|
-
missing.unshift(
|
|
38095
|
+
missing.unshift(basename4(at));
|
|
38054
38096
|
at = parent;
|
|
38055
38097
|
}
|
|
38056
38098
|
}
|
|
@@ -38058,26 +38100,26 @@ function realAsFarAsItExists(path) {
|
|
|
38058
38100
|
function projectKey(projectPath) {
|
|
38059
38101
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
38060
38102
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
38061
|
-
return
|
|
38103
|
+
return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
38062
38104
|
}
|
|
38063
38105
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38064
|
-
return
|
|
38106
|
+
return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
38065
38107
|
}
|
|
38066
38108
|
function openTranscript(startedAt) {
|
|
38067
38109
|
mkdirSync13(runsDirectory(), { recursive: true });
|
|
38068
|
-
const path =
|
|
38110
|
+
const path = join23(runsDirectory(), `run-${startedAt}.log`);
|
|
38069
38111
|
return { path, fd: openSync2(path, "a") };
|
|
38070
38112
|
}
|
|
38071
38113
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38072
|
-
return
|
|
38114
|
+
return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
38073
38115
|
}
|
|
38074
38116
|
function writeEditorRunNote(note) {
|
|
38075
38117
|
mkdirSync13(runsDirectory(), { recursive: true });
|
|
38076
|
-
|
|
38118
|
+
writeFileSync16(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
38077
38119
|
}
|
|
38078
38120
|
function readEditorRunNote(projectPath) {
|
|
38079
38121
|
for (const directory of runtimeDirectories()) {
|
|
38080
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
38122
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join23(directory, "runs", "editor-run.json"));
|
|
38081
38123
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
38082
38124
|
return note;
|
|
38083
38125
|
}
|
|
@@ -38087,7 +38129,7 @@ function readEditorRunNote(projectPath) {
|
|
|
38087
38129
|
function editorNoteAt(path) {
|
|
38088
38130
|
let parsed;
|
|
38089
38131
|
try {
|
|
38090
|
-
parsed = JSON.parse(
|
|
38132
|
+
parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
38091
38133
|
} catch {
|
|
38092
38134
|
return null;
|
|
38093
38135
|
}
|
|
@@ -38109,7 +38151,7 @@ function readRunRecord(projectPath) {
|
|
|
38109
38151
|
if (own !== null) {
|
|
38110
38152
|
return own;
|
|
38111
38153
|
}
|
|
38112
|
-
const shared = recordAt(
|
|
38154
|
+
const shared = recordAt(join23(directory, "runs", SHARED_NOTE));
|
|
38113
38155
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38114
38156
|
return shared;
|
|
38115
38157
|
}
|
|
@@ -38121,13 +38163,13 @@ function everyRunRecord() {
|
|
|
38121
38163
|
for (const directory of runtimeDirectories()) {
|
|
38122
38164
|
let entries;
|
|
38123
38165
|
try {
|
|
38124
|
-
entries = readdirSync8(
|
|
38166
|
+
entries = readdirSync8(join23(directory, "runs"));
|
|
38125
38167
|
} catch {
|
|
38126
38168
|
continue;
|
|
38127
38169
|
}
|
|
38128
38170
|
for (const entry of entries) {
|
|
38129
38171
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38130
|
-
const record = recordAt(
|
|
38172
|
+
const record = recordAt(join23(directory, "runs", entry));
|
|
38131
38173
|
if (record !== null) {
|
|
38132
38174
|
found.push(record);
|
|
38133
38175
|
}
|
|
@@ -38139,7 +38181,7 @@ function everyRunRecord() {
|
|
|
38139
38181
|
function recordAt(path) {
|
|
38140
38182
|
let parsed;
|
|
38141
38183
|
try {
|
|
38142
|
-
parsed = JSON.parse(
|
|
38184
|
+
parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
38143
38185
|
} catch {
|
|
38144
38186
|
return null;
|
|
38145
38187
|
}
|
|
@@ -38185,7 +38227,7 @@ function runningAs(pid) {
|
|
|
38185
38227
|
const began = startedAt(pid, null);
|
|
38186
38228
|
const when = began === null ? {} : { startedAt: began };
|
|
38187
38229
|
if (process.platform === "linux") {
|
|
38188
|
-
const raw =
|
|
38230
|
+
const raw = readFileSync21(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
38189
38231
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
38190
38232
|
}
|
|
38191
38233
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -38287,7 +38329,7 @@ function judgeRun(record, running, needed) {
|
|
|
38287
38329
|
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
38288
38330
|
return false;
|
|
38289
38331
|
}
|
|
38290
|
-
const engine = record.command === undefined ? null :
|
|
38332
|
+
const engine = record.command === undefined ? null : basename4(record.command);
|
|
38291
38333
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
38292
38334
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
38293
38335
|
if (wanted !== null && !said.includes(wanted)) {
|
|
@@ -38323,19 +38365,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
38323
38365
|
}
|
|
38324
38366
|
function clearRunRecord(projectPath) {
|
|
38325
38367
|
for (const directory of runtimeDirectories()) {
|
|
38326
|
-
const shared =
|
|
38368
|
+
const shared = join23(directory, "runs", SHARED_NOTE);
|
|
38327
38369
|
const sharedRecord = recordAt(shared);
|
|
38328
38370
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
38329
38371
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
38330
38372
|
try {
|
|
38331
|
-
|
|
38373
|
+
rmSync12(path, { force: true });
|
|
38332
38374
|
} catch {}
|
|
38333
38375
|
}
|
|
38334
38376
|
}
|
|
38335
38377
|
}
|
|
38336
38378
|
function ageMs(path, now) {
|
|
38337
38379
|
try {
|
|
38338
|
-
return now -
|
|
38380
|
+
return now - statSync8(path).mtimeMs;
|
|
38339
38381
|
} catch {
|
|
38340
38382
|
return 0;
|
|
38341
38383
|
}
|
|
@@ -38353,12 +38395,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38353
38395
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
38354
38396
|
continue;
|
|
38355
38397
|
}
|
|
38356
|
-
const path =
|
|
38398
|
+
const path = join23(directory, entry);
|
|
38357
38399
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
38358
38400
|
continue;
|
|
38359
38401
|
}
|
|
38360
38402
|
try {
|
|
38361
|
-
|
|
38403
|
+
rmSync12(path);
|
|
38362
38404
|
} catch {}
|
|
38363
38405
|
}
|
|
38364
38406
|
}
|
|
@@ -38377,19 +38419,19 @@ import {
|
|
|
38377
38419
|
closeSync as closeSync2,
|
|
38378
38420
|
constants,
|
|
38379
38421
|
copyFileSync,
|
|
38380
|
-
existsSync as
|
|
38422
|
+
existsSync as existsSync16,
|
|
38381
38423
|
fstatSync,
|
|
38382
38424
|
mkdirSync as mkdirSync14,
|
|
38383
38425
|
mkdtempSync as mkdtempSync3,
|
|
38384
38426
|
openSync as openSync3,
|
|
38385
38427
|
readdirSync as readdirSync9,
|
|
38386
|
-
readFileSync as
|
|
38428
|
+
readFileSync as readFileSync22,
|
|
38387
38429
|
readSync as readSync2,
|
|
38388
38430
|
rmdirSync,
|
|
38389
|
-
statSync as
|
|
38431
|
+
statSync as statSync9
|
|
38390
38432
|
} from "node:fs";
|
|
38391
38433
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
38392
|
-
import { basename as
|
|
38434
|
+
import { basename as basename5, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute3, join as join24, normalize as normalize3, relative as relative3 } from "node:path";
|
|
38393
38435
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
38394
38436
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
38395
38437
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -38404,15 +38446,15 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
38404
38446
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
38405
38447
|
}
|
|
38406
38448
|
const directory = realPathOr(dirname17(outputPath));
|
|
38407
|
-
if (!
|
|
38449
|
+
if (!existsSync16(directory)) {
|
|
38408
38450
|
return `outputPath's directory ${dirname17(outputPath)} does not exist; a capture creates no directories.`;
|
|
38409
38451
|
}
|
|
38410
|
-
const where =
|
|
38452
|
+
const where = join24(directory, basename5(outputPath));
|
|
38411
38453
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
38412
38454
|
if (inside !== undefined) {
|
|
38413
38455
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
38414
38456
|
}
|
|
38415
|
-
if (
|
|
38457
|
+
if (existsSync16(where)) {
|
|
38416
38458
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
38417
38459
|
}
|
|
38418
38460
|
return null;
|
|
@@ -38769,6 +38811,28 @@ function scanWaitAnswer(scanned) {
|
|
|
38769
38811
|
scanNote: scanned.stillScanning ? `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so this ` + "game started during the scan and may have read a class cache being rewritten. If " + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : undefined
|
|
38770
38812
|
};
|
|
38771
38813
|
}
|
|
38814
|
+
function changedSettings(before, after) {
|
|
38815
|
+
if (before === null || after === null) {
|
|
38816
|
+
return null;
|
|
38817
|
+
}
|
|
38818
|
+
const names = new Set([...before.keys(), ...after.keys()]);
|
|
38819
|
+
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
38820
|
+
}
|
|
38821
|
+
function importStatuses(payload) {
|
|
38822
|
+
const listed = payload["resources"];
|
|
38823
|
+
if (!Array.isArray(listed)) {
|
|
38824
|
+
return [];
|
|
38825
|
+
}
|
|
38826
|
+
return listed.map((entry) => {
|
|
38827
|
+
const one = asParams(entry);
|
|
38828
|
+
const reason = readString(one, "reason");
|
|
38829
|
+
return {
|
|
38830
|
+
path: readString(one, "path") ?? "",
|
|
38831
|
+
status: readString(one, "status") ?? "unknown",
|
|
38832
|
+
...reason === undefined ? {} : { reason }
|
|
38833
|
+
};
|
|
38834
|
+
});
|
|
38835
|
+
}
|
|
38772
38836
|
function escapingPropertyValue(properties) {
|
|
38773
38837
|
if (typeof properties !== "object" || properties === null) {
|
|
38774
38838
|
return;
|
|
@@ -38799,7 +38863,7 @@ function aboveTheRunner(entry) {
|
|
|
38799
38863
|
}
|
|
38800
38864
|
function elsewhereIn(projectPath, asked) {
|
|
38801
38865
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
38802
|
-
if (wanted === "" ||
|
|
38866
|
+
if (wanted === "" || existsSync16(join24(projectPath, wanted))) {
|
|
38803
38867
|
return "";
|
|
38804
38868
|
}
|
|
38805
38869
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -38813,7 +38877,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
38813
38877
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
38814
38878
|
}
|
|
38815
38879
|
function hasMainScene(projectFile) {
|
|
38816
|
-
const scene = parseProjectGodot(
|
|
38880
|
+
const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
38817
38881
|
return typeof scene === "string" && scene !== "";
|
|
38818
38882
|
}
|
|
38819
38883
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -38821,13 +38885,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
38821
38885
|
return false;
|
|
38822
38886
|
}
|
|
38823
38887
|
try {
|
|
38824
|
-
return
|
|
38888
|
+
return readFileSync22(join24(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
|
|
38825
38889
|
} catch {
|
|
38826
38890
|
return false;
|
|
38827
38891
|
}
|
|
38828
38892
|
}
|
|
38829
38893
|
function editorPlaysHeadless(projectFile) {
|
|
38830
|
-
const runArgs = parseProjectGodot(
|
|
38894
|
+
const runArgs = parseProjectGodot(readFileSync22(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
38831
38895
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
38832
38896
|
}
|
|
38833
38897
|
function camelCased(params) {
|
|
@@ -38846,7 +38910,7 @@ function needsItsOwnProcess(asked) {
|
|
|
38846
38910
|
class GodotServer {
|
|
38847
38911
|
mcp;
|
|
38848
38912
|
locator = new GodotLocator2;
|
|
38849
|
-
operationsScript =
|
|
38913
|
+
operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
38850
38914
|
godotBridge;
|
|
38851
38915
|
tools = buildToolDefinitions();
|
|
38852
38916
|
activeProcess = null;
|
|
@@ -39291,7 +39355,14 @@ class GodotServer {
|
|
|
39291
39355
|
}
|
|
39292
39356
|
async dispatch(tool, op, args) {
|
|
39293
39357
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
39294
|
-
return await this.handleRefreshUids(args);
|
|
39358
|
+
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
39359
|
+
}
|
|
39360
|
+
const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
|
|
39361
|
+
if (settingsWrite !== undefined) {
|
|
39362
|
+
return await this.handleProjectSettingsWrite(op, settingsWrite, args);
|
|
39363
|
+
}
|
|
39364
|
+
if (tool === "project_import" && op === "set_options") {
|
|
39365
|
+
return await this.handleSetImportOptions(args);
|
|
39295
39366
|
}
|
|
39296
39367
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
39297
39368
|
if (headless !== undefined) {
|
|
@@ -39550,8 +39621,8 @@ class GodotServer {
|
|
|
39550
39621
|
if (path === undefined) {
|
|
39551
39622
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
39552
39623
|
}
|
|
39553
|
-
const file =
|
|
39554
|
-
if (!
|
|
39624
|
+
const file = join24(path, "project.godot");
|
|
39625
|
+
if (!existsSync16(file)) {
|
|
39555
39626
|
return {
|
|
39556
39627
|
ok: false,
|
|
39557
39628
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -39822,6 +39893,222 @@ class GodotServer {
|
|
|
39822
39893
|
}
|
|
39823
39894
|
}, scanned));
|
|
39824
39895
|
}
|
|
39896
|
+
async handleReimport(args) {
|
|
39897
|
+
const project = this.project(args);
|
|
39898
|
+
if (!project.ok) {
|
|
39899
|
+
return project.response;
|
|
39900
|
+
}
|
|
39901
|
+
const contained = this.containProjectFiles(args);
|
|
39902
|
+
if (!contained.ok) {
|
|
39903
|
+
return contained.response;
|
|
39904
|
+
}
|
|
39905
|
+
const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
39906
|
+
return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
|
|
39907
|
+
}
|
|
39908
|
+
editorHasOpen(projectPath) {
|
|
39909
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
39910
|
+
return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
|
|
39911
|
+
}
|
|
39912
|
+
async handleProjectSettingsWrite(op, operation, args) {
|
|
39913
|
+
const project = this.project(args);
|
|
39914
|
+
if (!project.ok) {
|
|
39915
|
+
return project.response;
|
|
39916
|
+
}
|
|
39917
|
+
const projectPath = project.value.path;
|
|
39918
|
+
const served = this.editorHasOpen(projectPath);
|
|
39919
|
+
const before = served ? this.settingKeysOf(projectPath) : null;
|
|
39920
|
+
const written = await this.headless(operation, args);
|
|
39921
|
+
if (!served || written.isError === true) {
|
|
39922
|
+
return written;
|
|
39923
|
+
}
|
|
39924
|
+
const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
|
|
39925
|
+
if (AUDIO_BUS_OPS.has(op)) {
|
|
39926
|
+
return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
|
|
39927
|
+
}
|
|
39928
|
+
const changed = changedSettings(before, this.settingKeysOf(projectPath));
|
|
39929
|
+
if (changed === null) {
|
|
39930
|
+
return this.jsonTextResponse({
|
|
39931
|
+
...answer,
|
|
39932
|
+
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."
|
|
39933
|
+
});
|
|
39934
|
+
}
|
|
39935
|
+
if (changed.length === 0) {
|
|
39936
|
+
return written;
|
|
39937
|
+
}
|
|
39938
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
|
|
39939
|
+
return this.jsonTextResponse({
|
|
39940
|
+
...answer,
|
|
39941
|
+
...taken,
|
|
39942
|
+
...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
|
|
39943
|
+
});
|
|
39944
|
+
}
|
|
39945
|
+
async editorTakes(command, args) {
|
|
39946
|
+
try {
|
|
39947
|
+
await this.godotBridge.invokeTool(command, args);
|
|
39948
|
+
return {};
|
|
39949
|
+
} catch (error) {
|
|
39950
|
+
const message = errorMessage(error);
|
|
39951
|
+
const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
|
|
39952
|
+
return {
|
|
39953
|
+
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.`
|
|
39954
|
+
};
|
|
39955
|
+
}
|
|
39956
|
+
}
|
|
39957
|
+
async handleSetImportOptions(args) {
|
|
39958
|
+
const project = this.project(args);
|
|
39959
|
+
if (!project.ok) {
|
|
39960
|
+
return project.response;
|
|
39961
|
+
}
|
|
39962
|
+
const contained = this.containProjectFiles(args);
|
|
39963
|
+
if (!contained.ok) {
|
|
39964
|
+
return contained.response;
|
|
39965
|
+
}
|
|
39966
|
+
const {
|
|
39967
|
+
op: _op,
|
|
39968
|
+
projectPath: _projectPath,
|
|
39969
|
+
reimport: _reimport,
|
|
39970
|
+
timeoutMs: _timeoutMs,
|
|
39971
|
+
...params
|
|
39972
|
+
} = contained.value;
|
|
39973
|
+
const written = await this.operation("set_import_options", params, project.value.path);
|
|
39974
|
+
if (!written.ok || args["reimport"] === false) {
|
|
39975
|
+
return this.answer(written.ok ? {
|
|
39976
|
+
...written,
|
|
39977
|
+
payload: {
|
|
39978
|
+
...written.payload,
|
|
39979
|
+
note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
|
|
39980
|
+
}
|
|
39981
|
+
} : written);
|
|
39982
|
+
}
|
|
39983
|
+
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
39984
|
+
if (!applied.ok) {
|
|
39985
|
+
return applied.response;
|
|
39986
|
+
}
|
|
39987
|
+
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
39988
|
+
}
|
|
39989
|
+
async reimport(projectPath, resourcePath, force, timeoutMs) {
|
|
39990
|
+
const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
|
|
39991
|
+
const before = await this.operation("get_import_status", ask, projectPath);
|
|
39992
|
+
if (!before.ok) {
|
|
39993
|
+
return { ok: false, response: this.answer(before) };
|
|
39994
|
+
}
|
|
39995
|
+
const listed = importStatuses(before.payload);
|
|
39996
|
+
const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
|
|
39997
|
+
if (resourcePath !== undefined && missingSource.length > 0) {
|
|
39998
|
+
return {
|
|
39999
|
+
ok: false,
|
|
40000
|
+
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
40001
|
+
};
|
|
40002
|
+
}
|
|
40003
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
40004
|
+
if (targets.length === 0) {
|
|
40005
|
+
return {
|
|
40006
|
+
ok: true,
|
|
40007
|
+
payload: {
|
|
40008
|
+
reimported: [],
|
|
40009
|
+
notReimported: [],
|
|
40010
|
+
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.`
|
|
40011
|
+
}
|
|
40012
|
+
};
|
|
40013
|
+
}
|
|
40014
|
+
const viaEditor = this.editorHasOpen(projectPath);
|
|
40015
|
+
const extra = {};
|
|
40016
|
+
if (viaEditor) {
|
|
40017
|
+
const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
|
|
40018
|
+
if (!inEditor.ok) {
|
|
40019
|
+
return inEditor;
|
|
40020
|
+
}
|
|
40021
|
+
if (!inEditor.finished) {
|
|
40022
|
+
return {
|
|
40023
|
+
ok: true,
|
|
40024
|
+
payload: {
|
|
40025
|
+
via: "editor",
|
|
40026
|
+
stillImporting: true,
|
|
40027
|
+
asked: targets,
|
|
40028
|
+
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.`
|
|
40029
|
+
}
|
|
40030
|
+
};
|
|
40031
|
+
}
|
|
40032
|
+
} else {
|
|
40033
|
+
const engine = await this.engine();
|
|
40034
|
+
if (!engine.ok) {
|
|
40035
|
+
return { ok: false, response: engine.response };
|
|
40036
|
+
}
|
|
40037
|
+
for (const target of targets) {
|
|
40038
|
+
forceNextImport(projectPath, target);
|
|
40039
|
+
}
|
|
40040
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
40041
|
+
const imported = await runImport(engine.value, projectPath);
|
|
40042
|
+
if (!imported.ok) {
|
|
40043
|
+
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
40044
|
+
}
|
|
40045
|
+
if (imported.messages.length > 0) {
|
|
40046
|
+
extra["engine_messages"] = imported.messages;
|
|
40047
|
+
}
|
|
40048
|
+
}
|
|
40049
|
+
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
40050
|
+
if (!after.ok) {
|
|
40051
|
+
return { ok: false, response: this.answer(after) };
|
|
40052
|
+
}
|
|
40053
|
+
const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
|
|
40054
|
+
const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
|
|
40055
|
+
const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
|
|
40056
|
+
path,
|
|
40057
|
+
status: now.get(path)?.status ?? "unknown",
|
|
40058
|
+
...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
|
|
40059
|
+
}));
|
|
40060
|
+
return {
|
|
40061
|
+
ok: true,
|
|
40062
|
+
payload: {
|
|
40063
|
+
via: viaEditor ? "editor" : "engine",
|
|
40064
|
+
reimported,
|
|
40065
|
+
notReimported,
|
|
40066
|
+
...missingSource.length > 0 ? { missingSource } : {},
|
|
40067
|
+
...extra
|
|
40068
|
+
}
|
|
40069
|
+
};
|
|
40070
|
+
}
|
|
40071
|
+
async reimportInTheEditor(paths, timeoutMs) {
|
|
40072
|
+
const started = Date.now();
|
|
40073
|
+
let completedBefore;
|
|
40074
|
+
for (;; ) {
|
|
40075
|
+
while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
|
|
40076
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
40077
|
+
}
|
|
40078
|
+
let answer;
|
|
40079
|
+
try {
|
|
40080
|
+
answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
|
|
40081
|
+
} catch (error) {
|
|
40082
|
+
const message = errorMessage(error);
|
|
40083
|
+
return {
|
|
40084
|
+
ok: false,
|
|
40085
|
+
response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
|
|
40086
|
+
"editor_launch restart loads the addon this server ships",
|
|
40087
|
+
"Or close the editor, and project_import reimport runs the engine import instead"
|
|
40088
|
+
]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
|
|
40089
|
+
};
|
|
40090
|
+
}
|
|
40091
|
+
if (answer["started"] !== false) {
|
|
40092
|
+
completedBefore = readNumber(answer, "reimportsCompletedBefore");
|
|
40093
|
+
break;
|
|
40094
|
+
}
|
|
40095
|
+
if (Date.now() - started >= timeoutMs) {
|
|
40096
|
+
return { ok: true, finished: false };
|
|
40097
|
+
}
|
|
40098
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
40099
|
+
}
|
|
40100
|
+
while (Date.now() - started < timeoutMs) {
|
|
40101
|
+
await new Promise((settle) => setTimeout(settle, 250));
|
|
40102
|
+
try {
|
|
40103
|
+
const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
40104
|
+
const completed = readNumber(status, "reimportsCompleted");
|
|
40105
|
+
if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
|
|
40106
|
+
return { ok: true, finished: true };
|
|
40107
|
+
}
|
|
40108
|
+
} catch {}
|
|
40109
|
+
}
|
|
40110
|
+
return { ok: true, finished: false };
|
|
40111
|
+
}
|
|
39825
40112
|
async operation(operation, params, projectPath) {
|
|
39826
40113
|
const engine = await this.engine();
|
|
39827
40114
|
if (!engine.ok) {
|
|
@@ -39863,11 +40150,11 @@ class GodotServer {
|
|
|
39863
40150
|
if (!engine.ok) {
|
|
39864
40151
|
return engine.response;
|
|
39865
40152
|
}
|
|
39866
|
-
const application = parseProjectGodot(
|
|
40153
|
+
const application = parseProjectGodot(readFileSync22(project.value.file, "utf8"))["application"] ?? {};
|
|
39867
40154
|
const name = application["config/name"];
|
|
39868
40155
|
const mainScene = application["run/main_scene"];
|
|
39869
40156
|
const info = {
|
|
39870
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
40157
|
+
name: typeof name === "string" && name !== "" ? name : basename5(project.value.path),
|
|
39871
40158
|
path: project.value.path,
|
|
39872
40159
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
39873
40160
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -39931,11 +40218,11 @@ class GodotServer {
|
|
|
39931
40218
|
return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
39932
40219
|
}
|
|
39933
40220
|
const debug = readBoolean(args, "debug") ?? false;
|
|
39934
|
-
const exportLogs = mkdtempSync3(
|
|
40221
|
+
const exportLogs = mkdtempSync3(join24(tmpdir5(), "gdharness-export-"));
|
|
39935
40222
|
const exportArgs = [
|
|
39936
40223
|
"--headless",
|
|
39937
40224
|
"--log-file",
|
|
39938
|
-
|
|
40225
|
+
join24(exportLogs, "engine.log"),
|
|
39939
40226
|
"--path",
|
|
39940
40227
|
project.value.path,
|
|
39941
40228
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -39967,7 +40254,7 @@ class GodotServer {
|
|
|
39967
40254
|
log.finish();
|
|
39968
40255
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
39969
40256
|
const verdict = {
|
|
39970
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
40257
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync16(output.absolutePath),
|
|
39971
40258
|
preset,
|
|
39972
40259
|
outputPath: output.relativePath,
|
|
39973
40260
|
debug,
|
|
@@ -40003,7 +40290,7 @@ class GodotServer {
|
|
|
40003
40290
|
return contained.response;
|
|
40004
40291
|
}
|
|
40005
40292
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
40006
|
-
if (!
|
|
40293
|
+
if (!existsSync16(join24(project.value.path, runner))) {
|
|
40007
40294
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
40008
40295
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
40009
40296
|
]);
|
|
@@ -40039,7 +40326,7 @@ class GodotServer {
|
|
|
40039
40326
|
];
|
|
40040
40327
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
40041
40328
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40042
|
-
const userData = mkdtempSync3(
|
|
40329
|
+
const userData = mkdtempSync3(join24(tmpdir5(), "gdharness-tests-"));
|
|
40043
40330
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
40044
40331
|
const console2 = [];
|
|
40045
40332
|
run.process.stdout?.on("data", (data) => {
|
|
@@ -40059,14 +40346,14 @@ class GodotServer {
|
|
|
40059
40346
|
resolve(false);
|
|
40060
40347
|
});
|
|
40061
40348
|
});
|
|
40062
|
-
const reportsDir =
|
|
40349
|
+
const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
|
|
40063
40350
|
let report = null;
|
|
40064
40351
|
let reportProblem = null;
|
|
40065
40352
|
try {
|
|
40066
|
-
const written =
|
|
40353
|
+
const written = existsSync16(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
40067
40354
|
const newest = written.at(-1);
|
|
40068
40355
|
if (newest !== undefined) {
|
|
40069
|
-
report = parseJUnit(
|
|
40356
|
+
report = parseJUnit(readFileSync22(join24(reportsDir, newest, "results.xml"), "utf8"));
|
|
40070
40357
|
}
|
|
40071
40358
|
} catch (error) {
|
|
40072
40359
|
reportProblem = errorMessage(error);
|
|
@@ -40209,11 +40496,11 @@ class GodotServer {
|
|
|
40209
40496
|
const cached = cachedClasses(projectPath);
|
|
40210
40497
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
40211
40498
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40212
|
-
if (!contained.ok || !
|
|
40499
|
+
if (!contained.ok || !existsSync16(contained.absolutePath)) {
|
|
40213
40500
|
return null;
|
|
40214
40501
|
}
|
|
40215
40502
|
try {
|
|
40216
|
-
return
|
|
40503
|
+
return readFileSync22(contained.absolutePath, "utf8");
|
|
40217
40504
|
} catch {
|
|
40218
40505
|
return null;
|
|
40219
40506
|
}
|
|
@@ -40263,11 +40550,11 @@ class GodotServer {
|
|
|
40263
40550
|
}
|
|
40264
40551
|
sourceOfClass(projectPath, resourcePath) {
|
|
40265
40552
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40266
|
-
if (!contained.ok || !
|
|
40553
|
+
if (!contained.ok || !existsSync16(contained.absolutePath)) {
|
|
40267
40554
|
return null;
|
|
40268
40555
|
}
|
|
40269
40556
|
try {
|
|
40270
|
-
return
|
|
40557
|
+
return readFileSync22(contained.absolutePath, "utf8");
|
|
40271
40558
|
} catch {
|
|
40272
40559
|
return null;
|
|
40273
40560
|
}
|
|
@@ -40536,7 +40823,7 @@ class GodotServer {
|
|
|
40536
40823
|
return null;
|
|
40537
40824
|
}
|
|
40538
40825
|
try {
|
|
40539
|
-
return settingKeys(
|
|
40826
|
+
return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8"));
|
|
40540
40827
|
} catch {
|
|
40541
40828
|
return null;
|
|
40542
40829
|
}
|
|
@@ -40676,7 +40963,8 @@ class GodotServer {
|
|
|
40676
40963
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
40677
40964
|
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
40678
40965
|
[OPENED_BY_A_SERVER]: "1"
|
|
40679
|
-
}
|
|
40966
|
+
},
|
|
40967
|
+
desktop: HIDDEN_DESKTOP
|
|
40680
40968
|
});
|
|
40681
40969
|
if ("error" in launched) {
|
|
40682
40970
|
return { pid: null, error: launched.error };
|
|
@@ -40758,7 +41046,7 @@ class GodotServer {
|
|
|
40758
41046
|
userArgs: given.value
|
|
40759
41047
|
});
|
|
40760
41048
|
this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40761
|
-
const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
|
|
41049
|
+
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);
|
|
40762
41050
|
if ("error" in started) {
|
|
40763
41051
|
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
40764
41052
|
"editor_status says which Godot this server runs and whether it answers"
|
|
@@ -40788,7 +41076,7 @@ class GodotServer {
|
|
|
40788
41076
|
}
|
|
40789
41077
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
40790
41078
|
const budgetMs = budget.ms;
|
|
40791
|
-
if (!
|
|
41079
|
+
if (!existsSync16(join24(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
40792
41080
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
40793
41081
|
}
|
|
40794
41082
|
let asking = null;
|
|
@@ -40851,7 +41139,7 @@ class GodotServer {
|
|
|
40851
41139
|
}
|
|
40852
41140
|
let announcedAt;
|
|
40853
41141
|
try {
|
|
40854
|
-
announcedAt =
|
|
41142
|
+
announcedAt = statSync9(endpoint.file).mtimeMs;
|
|
40855
41143
|
} catch {
|
|
40856
41144
|
return;
|
|
40857
41145
|
}
|
|
@@ -41098,7 +41386,7 @@ class GodotServer {
|
|
|
41098
41386
|
});
|
|
41099
41387
|
return started;
|
|
41100
41388
|
}
|
|
41101
|
-
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
41389
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
41102
41390
|
const startedAt = Date.now();
|
|
41103
41391
|
const transcript = openTranscript(startedAt);
|
|
41104
41392
|
closeSync2(transcript.fd);
|
|
@@ -41106,7 +41394,8 @@ class GodotServer {
|
|
|
41106
41394
|
command: godotPath,
|
|
41107
41395
|
args: cmdArgs,
|
|
41108
41396
|
...env === undefined ? {} : { env },
|
|
41109
|
-
run: { transcript: transcript.path, startedAt, projectPath }
|
|
41397
|
+
run: { transcript: transcript.path, startedAt, projectPath },
|
|
41398
|
+
...desktop === undefined ? {} : { desktop }
|
|
41110
41399
|
});
|
|
41111
41400
|
if ("error" in launched) {
|
|
41112
41401
|
return launched;
|
|
@@ -41601,7 +41890,7 @@ class GodotServer {
|
|
|
41601
41890
|
notes.push(endedWithoutACode({
|
|
41602
41891
|
throughEditor: run.throughEditor,
|
|
41603
41892
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41604
|
-
couldAnnounce: run.projectPath !== null &&
|
|
41893
|
+
couldAnnounce: run.projectPath !== null && existsSync16(join24(run.projectPath, RUNTIME_AUTOLOAD2.path)),
|
|
41605
41894
|
endedHere: typeof run.endedHere === "string",
|
|
41606
41895
|
pickedUp: run.pickedUp === true
|
|
41607
41896
|
}));
|
|
@@ -42114,7 +42403,7 @@ class GodotServer {
|
|
|
42114
42403
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
42115
42404
|
const run = this.currentRun();
|
|
42116
42405
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
42117
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
42406
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync16(join24(project, RUNTIME_AUTOLOAD2.path)) || !await this.runStillGoing(run)) {
|
|
42118
42407
|
return null;
|
|
42119
42408
|
}
|
|
42120
42409
|
const already = runtimesAnnounced().running;
|
|
@@ -42123,7 +42412,7 @@ class GodotServer {
|
|
|
42123
42412
|
return !run.announcedBefore.has(one.pid);
|
|
42124
42413
|
}
|
|
42125
42414
|
try {
|
|
42126
|
-
return
|
|
42415
|
+
return statSync9(one.file).mtimeMs >= run.startedAt;
|
|
42127
42416
|
} catch {
|
|
42128
42417
|
return false;
|
|
42129
42418
|
}
|
|
@@ -42193,8 +42482,8 @@ class GodotServer {
|
|
|
42193
42482
|
return this.createErrorResponse(refusal);
|
|
42194
42483
|
}
|
|
42195
42484
|
}
|
|
42196
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
42197
|
-
const screenshotPath = screenshotDir ?
|
|
42485
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join24(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
42486
|
+
const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
|
|
42198
42487
|
try {
|
|
42199
42488
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
42200
42489
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -42235,7 +42524,7 @@ class GodotServer {
|
|
|
42235
42524
|
type: "text",
|
|
42236
42525
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
42237
42526
|
},
|
|
42238
|
-
{ type: "image", data:
|
|
42527
|
+
{ type: "image", data: readFileSync22(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
42239
42528
|
]
|
|
42240
42529
|
};
|
|
42241
42530
|
} finally {
|
|
@@ -42335,7 +42624,7 @@ async function runGodotServer() {
|
|
|
42335
42624
|
const server = new GodotServer;
|
|
42336
42625
|
await server.run();
|
|
42337
42626
|
}
|
|
42338
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42627
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
42339
42628
|
var init_server3 = __esm(() => {
|
|
42340
42629
|
init_mcp();
|
|
42341
42630
|
init_stdio2();
|
|
@@ -42348,6 +42637,7 @@ var init_server3 = __esm(() => {
|
|
|
42348
42637
|
init_class_note();
|
|
42349
42638
|
init_config_pin();
|
|
42350
42639
|
init_dap_client();
|
|
42640
|
+
init_desktop();
|
|
42351
42641
|
init_editor_log();
|
|
42352
42642
|
init_errors();
|
|
42353
42643
|
init_game_log();
|
|
@@ -42364,6 +42654,7 @@ var init_server3 = __esm(() => {
|
|
|
42364
42654
|
init_process_children();
|
|
42365
42655
|
init_process_time();
|
|
42366
42656
|
init_project_scan();
|
|
42657
|
+
init_reimport();
|
|
42367
42658
|
init_resources();
|
|
42368
42659
|
init_restart_note();
|
|
42369
42660
|
init_run_record();
|
|
@@ -42384,6 +42675,11 @@ var init_server3 = __esm(() => {
|
|
|
42384
42675
|
object: "an object"
|
|
42385
42676
|
};
|
|
42386
42677
|
HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
42678
|
+
AUDIO_BUS_OPS = new Set([
|
|
42679
|
+
"add_audio_bus",
|
|
42680
|
+
"set_audio_bus_effect",
|
|
42681
|
+
"set_audio_bus_volume"
|
|
42682
|
+
]);
|
|
42387
42683
|
PATH_SOLUTIONS = [
|
|
42388
42684
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
42389
42685
|
"Point projectPath at the project the file belongs to"
|
|
@@ -42415,8 +42711,8 @@ var init_server3 = __esm(() => {
|
|
|
42415
42711
|
});
|
|
42416
42712
|
|
|
42417
42713
|
// src/cli.ts
|
|
42418
|
-
import { existsSync as
|
|
42419
|
-
import { join as
|
|
42714
|
+
import { existsSync as existsSync17 } from "fs";
|
|
42715
|
+
import { join as join25, resolve as resolve6 } from "path";
|
|
42420
42716
|
|
|
42421
42717
|
// src/bridge-announce.ts
|
|
42422
42718
|
init_errors();
|
|
@@ -44240,7 +44536,7 @@ class UsageError extends Refusal2 {
|
|
|
44240
44536
|
function projectArgument(at) {
|
|
44241
44537
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
44242
44538
|
const projectPath = resolve6(named ?? ".");
|
|
44243
|
-
if (!
|
|
44539
|
+
if (!existsSync17(join25(projectPath, "project.godot"))) {
|
|
44244
44540
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
44245
44541
|
}
|
|
44246
44542
|
return projectPath;
|
|
@@ -44279,7 +44575,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
44279
44575
|
return;
|
|
44280
44576
|
}
|
|
44281
44577
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
44282
|
-
if (!
|
|
44578
|
+
if (!existsSync17(join25(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
44283
44579
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
44284
44580
|
}
|
|
44285
44581
|
}
|
|
@@ -44411,7 +44707,7 @@ async function setup() {
|
|
|
44411
44707
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
44412
44708
|
}
|
|
44413
44709
|
if (!args.includes("--no-skill")) {
|
|
44414
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
44710
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync17);
|
|
44415
44711
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
44416
44712
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44417
44713
|
}
|
|
@@ -44505,7 +44801,7 @@ async function upgrade() {
|
|
|
44505
44801
|
moved.push(took.from);
|
|
44506
44802
|
}
|
|
44507
44803
|
}
|
|
44508
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
44804
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync17), version)) {
|
|
44509
44805
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44510
44806
|
}
|
|
44511
44807
|
console.log(`
|