gdharness 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +447 -115
- package/build/godot/addons/gdharness_editor/tool_executor.gd +3 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +44 -0
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +50 -1
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +10 -3
- package/build/godot/operations/godot_operations.gd +0 -2
- package/build/godot/operations/import_pipeline.gd +226 -97
- package/build/index.js +454 -124
- package/build/keeper.js +272 -23
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -385,9 +385,10 @@ function runArguments(options) {
|
|
|
385
385
|
}
|
|
386
386
|
return args;
|
|
387
387
|
}
|
|
388
|
-
function editorArguments(projectPath, ports, logFile) {
|
|
388
|
+
function editorArguments(projectPath, ports, logFile, hidden) {
|
|
389
389
|
return [
|
|
390
390
|
"-e",
|
|
391
|
+
...hidden ? ["--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,8 +16312,15 @@ var init_tool_definitions = __esm(() => {
|
|
|
16300
16312
|
},
|
|
16301
16313
|
{
|
|
16302
16314
|
name: "editor_launch",
|
|
16303
|
-
description: "Opens the Godot editor on a project, in
|
|
16304
|
-
parameters: {
|
|
16315
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
|
|
16316
|
+
parameters: {
|
|
16317
|
+
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
16318
|
+
hidden: {
|
|
16319
|
+
type: "boolean",
|
|
16320
|
+
ops: ["open", "restart"],
|
|
16321
|
+
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus."
|
|
16322
|
+
}
|
|
16323
|
+
},
|
|
16305
16324
|
requires: [],
|
|
16306
16325
|
operations: {
|
|
16307
16326
|
open: { summary: "open the editor on a project", requires: ["projectPath"] },
|
|
@@ -16326,6 +16345,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16326
16345
|
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
16346
|
},
|
|
16328
16347
|
headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
|
|
16348
|
+
visible: {
|
|
16349
|
+
type: "boolean",
|
|
16350
|
+
ops: ["start"],
|
|
16351
|
+
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
16352
|
+
},
|
|
16329
16353
|
savesIn: {
|
|
16330
16354
|
type: "string",
|
|
16331
16355
|
ops: ["start", "check"],
|
|
@@ -16656,7 +16680,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16656
16680
|
},
|
|
16657
16681
|
{
|
|
16658
16682
|
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.",
|
|
16683
|
+
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
16684
|
parameters: {
|
|
16661
16685
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16662
16686
|
pid: RUNNING_PID,
|
|
@@ -16923,7 +16947,6 @@ var init_headless_operations = __esm(() => {
|
|
|
16923
16947
|
status: "get_import_status",
|
|
16924
16948
|
options: "get_import_options",
|
|
16925
16949
|
set_options: "set_import_options",
|
|
16926
|
-
reimport: "reimport_resource",
|
|
16927
16950
|
uid: "get_uid",
|
|
16928
16951
|
refresh_classes: "refresh_class_cache"
|
|
16929
16952
|
}),
|
|
@@ -16937,7 +16960,7 @@ var init_headless_operations = __esm(() => {
|
|
|
16937
16960
|
})
|
|
16938
16961
|
});
|
|
16939
16962
|
ENGINE_PASSES = dictionary({
|
|
16940
|
-
project_import: dictionary({ refresh_uids: "import" })
|
|
16963
|
+
project_import: dictionary({ refresh_uids: "import", reimport: "import" })
|
|
16941
16964
|
});
|
|
16942
16965
|
EDITOR_READS = dictionary({
|
|
16943
16966
|
project_settings: dictionary({ get: "get_project_setting" })
|
|
@@ -33091,6 +33114,10 @@ var init_dap_client = __esm(() => {
|
|
|
33091
33114
|
init_ports();
|
|
33092
33115
|
});
|
|
33093
33116
|
|
|
33117
|
+
// src/desktop.ts
|
|
33118
|
+
var HIDDEN_DESKTOP = "gdharness";
|
|
33119
|
+
var init_desktop = () => {};
|
|
33120
|
+
|
|
33094
33121
|
// src/editor-log.ts
|
|
33095
33122
|
import {
|
|
33096
33123
|
closeSync,
|
|
@@ -37128,6 +37155,7 @@ function withActualsPrinted(failed, printed) {
|
|
|
37128
37155
|
return entry;
|
|
37129
37156
|
}
|
|
37130
37157
|
let detail = entry.detail;
|
|
37158
|
+
let recovered = false;
|
|
37131
37159
|
for (const diff of diffs) {
|
|
37132
37160
|
const wrong = ` but was
|
|
37133
37161
|
'${diff.merged.replaceAll("\r", "")}'
|
|
@@ -37136,12 +37164,13 @@ function withActualsPrinted(failed, printed) {
|
|
|
37136
37164
|
detail = detail.replace(wrong, () => ` but was
|
|
37137
37165
|
'${diff.actual}'
|
|
37138
37166
|
at ${diff.at}`);
|
|
37167
|
+
recovered = true;
|
|
37139
37168
|
}
|
|
37140
37169
|
}
|
|
37141
|
-
|
|
37142
|
-
|
|
37170
|
+
if (!recovered && /Expecting:\n '[\s\S]*'\n but was\n '[\s\S]*'\n\tat /.test(detail)) {
|
|
37171
|
+
const alike = /Expecting:\n '([\s\S]*)'\n but was\n '\1'\n\tat /.test(detail);
|
|
37143
37172
|
detail = `${detail}
|
|
37144
|
-
${BOTH_SIDES_ALIKE_NOTE}`;
|
|
37173
|
+
${alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE}`;
|
|
37145
37174
|
}
|
|
37146
37175
|
return detail === entry.detail ? entry : { ...entry, detail };
|
|
37147
37176
|
});
|
|
@@ -37176,7 +37205,7 @@ function orphansPrinted(printed) {
|
|
|
37176
37205
|
suites
|
|
37177
37206
|
};
|
|
37178
37207
|
}
|
|
37179
|
-
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, ESCAPE, PRINTED_ACTUAL, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
37208
|
+
var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, PRINTED_ACTUAL, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
|
|
37180
37209
|
var init_junit = __esm(() => {
|
|
37181
37210
|
MalformedReportError = class MalformedReportError extends Error {
|
|
37182
37211
|
constructor(message) {
|
|
@@ -37189,7 +37218,7 @@ var init_junit = __esm(() => {
|
|
|
37189
37218
|
NOTHING_RAN = /no test cases found/i;
|
|
37190
37219
|
NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
|
|
37191
37220
|
ESCAPE = String.fromCharCode(27);
|
|
37192
|
-
PRINTED_ACTUAL = new RegExp(` but was\\n '([\\s\\S]*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (
|
|
37221
|
+
PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
37193
37222
|
COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
37194
37223
|
SPELLED = {
|
|
37195
37224
|
"<LF>": `
|
|
@@ -37786,7 +37815,9 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
37786
37815
|
command: spec.command,
|
|
37787
37816
|
args: spec.args,
|
|
37788
37817
|
envChanges: changesFrom(spec.env, process.env),
|
|
37789
|
-
...spec.run === undefined ? {} : { run: spec.run }
|
|
37818
|
+
...spec.run === undefined ? {} : { run: spec.run },
|
|
37819
|
+
...spec.desktop === undefined ? {} : { desktop: spec.desktop },
|
|
37820
|
+
...spec.noActivate === true ? { noActivate: true } : {}
|
|
37790
37821
|
});
|
|
37791
37822
|
let printed = "";
|
|
37792
37823
|
let complained = "";
|
|
@@ -37980,17 +38011,38 @@ var init_project_scan = __esm(() => {
|
|
|
37980
38011
|
CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
|
|
37981
38012
|
});
|
|
37982
38013
|
|
|
38014
|
+
// src/reimport.ts
|
|
38015
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
38016
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, utimesSync, writeFileSync as writeFileSync14 } from "node:fs";
|
|
38017
|
+
import { basename as basename3, join as join21 } from "node:path";
|
|
38018
|
+
function forceNextImport(projectPath, resourcePath) {
|
|
38019
|
+
const relative = resourcePath.replace(/^res:\/\//, "");
|
|
38020
|
+
const sidecar = join21(projectPath, `${relative}.import`);
|
|
38021
|
+
if (!existsSync15(sidecar)) {
|
|
38022
|
+
return;
|
|
38023
|
+
}
|
|
38024
|
+
const { atime, mtimeMs } = statSync7(sidecar);
|
|
38025
|
+
writeFileSync14(sidecar, readFileSync19(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
|
|
38026
|
+
utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
|
|
38027
|
+
rmSync10(importRecord(projectPath, resourcePath), { force: true });
|
|
38028
|
+
}
|
|
38029
|
+
function importRecord(projectPath, resourcePath) {
|
|
38030
|
+
const digest = createHash5("md5").update(resourcePath).digest("hex");
|
|
38031
|
+
return join21(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
|
|
38032
|
+
}
|
|
38033
|
+
var init_reimport = () => {};
|
|
38034
|
+
|
|
37983
38035
|
// src/restart-note.ts
|
|
37984
|
-
import { mkdirSync as mkdirSync12, readFileSync as
|
|
37985
|
-
import { dirname as dirname15, join as
|
|
38036
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
|
|
38037
|
+
import { dirname as dirname15, join as join22 } from "node:path";
|
|
37986
38038
|
function restartNotePath(projectPath) {
|
|
37987
|
-
return
|
|
38039
|
+
return join22(projectPath, ".godot", "gdharness-restart.json");
|
|
37988
38040
|
}
|
|
37989
38041
|
function noteRestartBegun(note) {
|
|
37990
38042
|
const path = restartNotePath(note.projectPath);
|
|
37991
38043
|
try {
|
|
37992
38044
|
mkdirSync12(dirname15(path), { recursive: true });
|
|
37993
|
-
|
|
38045
|
+
writeFileSync15(path, `${JSON.stringify(note, null, 2)}
|
|
37994
38046
|
`, "utf8");
|
|
37995
38047
|
} catch (error) {
|
|
37996
38048
|
console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
|
|
@@ -37998,7 +38050,7 @@ function noteRestartBegun(note) {
|
|
|
37998
38050
|
}
|
|
37999
38051
|
function restartOwed(projectPath) {
|
|
38000
38052
|
try {
|
|
38001
|
-
const parsed = JSON.parse(
|
|
38053
|
+
const parsed = JSON.parse(readFileSync20(restartNotePath(projectPath), "utf8"));
|
|
38002
38054
|
if (typeof parsed !== "object" || parsed === null) {
|
|
38003
38055
|
return null;
|
|
38004
38056
|
}
|
|
@@ -38013,7 +38065,7 @@ function restartOwed(projectPath) {
|
|
|
38013
38065
|
}
|
|
38014
38066
|
function restartSettled(projectPath) {
|
|
38015
38067
|
try {
|
|
38016
|
-
|
|
38068
|
+
rmSync11(restartNotePath(projectPath), { force: true });
|
|
38017
38069
|
} catch (error) {
|
|
38018
38070
|
console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
|
|
38019
38071
|
}
|
|
@@ -38024,33 +38076,33 @@ var init_restart_note = __esm(() => {
|
|
|
38024
38076
|
|
|
38025
38077
|
// src/run-record.ts
|
|
38026
38078
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
38027
|
-
import { createHash as
|
|
38079
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
38028
38080
|
import {
|
|
38029
38081
|
mkdirSync as mkdirSync13,
|
|
38030
38082
|
openSync as openSync2,
|
|
38031
38083
|
readdirSync as readdirSync8,
|
|
38032
|
-
readFileSync as
|
|
38084
|
+
readFileSync as readFileSync21,
|
|
38033
38085
|
realpathSync as realpathSync3,
|
|
38034
|
-
rmSync as
|
|
38035
|
-
statSync as
|
|
38036
|
-
writeFileSync as
|
|
38086
|
+
rmSync as rmSync12,
|
|
38087
|
+
statSync as statSync8,
|
|
38088
|
+
writeFileSync as writeFileSync16
|
|
38037
38089
|
} from "node:fs";
|
|
38038
|
-
import { basename as
|
|
38090
|
+
import { basename as basename4, dirname as dirname16, join as join23, resolve as resolve5 } from "node:path";
|
|
38039
38091
|
function runsDirectory() {
|
|
38040
|
-
return
|
|
38092
|
+
return join23(runtimeDirectory(), "runs");
|
|
38041
38093
|
}
|
|
38042
38094
|
function realAsFarAsItExists(path) {
|
|
38043
38095
|
const missing = [];
|
|
38044
38096
|
let at = path;
|
|
38045
38097
|
for (;; ) {
|
|
38046
38098
|
try {
|
|
38047
|
-
return
|
|
38099
|
+
return join23(realpathSync3.native(at), ...missing);
|
|
38048
38100
|
} catch {
|
|
38049
38101
|
const parent = dirname16(at);
|
|
38050
38102
|
if (parent === at) {
|
|
38051
38103
|
return path;
|
|
38052
38104
|
}
|
|
38053
|
-
missing.unshift(
|
|
38105
|
+
missing.unshift(basename4(at));
|
|
38054
38106
|
at = parent;
|
|
38055
38107
|
}
|
|
38056
38108
|
}
|
|
@@ -38058,26 +38110,26 @@ function realAsFarAsItExists(path) {
|
|
|
38058
38110
|
function projectKey(projectPath) {
|
|
38059
38111
|
const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
|
|
38060
38112
|
const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
|
|
38061
|
-
return
|
|
38113
|
+
return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
38062
38114
|
}
|
|
38063
38115
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
38064
|
-
return
|
|
38116
|
+
return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
38065
38117
|
}
|
|
38066
38118
|
function openTranscript(startedAt) {
|
|
38067
38119
|
mkdirSync13(runsDirectory(), { recursive: true });
|
|
38068
|
-
const path =
|
|
38120
|
+
const path = join23(runsDirectory(), `run-${startedAt}.log`);
|
|
38069
38121
|
return { path, fd: openSync2(path, "a") };
|
|
38070
38122
|
}
|
|
38071
38123
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
38072
|
-
return
|
|
38124
|
+
return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
38073
38125
|
}
|
|
38074
38126
|
function writeEditorRunNote(note) {
|
|
38075
38127
|
mkdirSync13(runsDirectory(), { recursive: true });
|
|
38076
|
-
|
|
38128
|
+
writeFileSync16(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
|
|
38077
38129
|
}
|
|
38078
38130
|
function readEditorRunNote(projectPath) {
|
|
38079
38131
|
for (const directory of runtimeDirectories()) {
|
|
38080
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
38132
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join23(directory, "runs", "editor-run.json"));
|
|
38081
38133
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
38082
38134
|
return note;
|
|
38083
38135
|
}
|
|
@@ -38087,7 +38139,7 @@ function readEditorRunNote(projectPath) {
|
|
|
38087
38139
|
function editorNoteAt(path) {
|
|
38088
38140
|
let parsed;
|
|
38089
38141
|
try {
|
|
38090
|
-
parsed = JSON.parse(
|
|
38142
|
+
parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
38091
38143
|
} catch {
|
|
38092
38144
|
return null;
|
|
38093
38145
|
}
|
|
@@ -38109,7 +38161,7 @@ function readRunRecord(projectPath) {
|
|
|
38109
38161
|
if (own !== null) {
|
|
38110
38162
|
return own;
|
|
38111
38163
|
}
|
|
38112
|
-
const shared = recordAt(
|
|
38164
|
+
const shared = recordAt(join23(directory, "runs", SHARED_NOTE));
|
|
38113
38165
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
38114
38166
|
return shared;
|
|
38115
38167
|
}
|
|
@@ -38121,13 +38173,13 @@ function everyRunRecord() {
|
|
|
38121
38173
|
for (const directory of runtimeDirectories()) {
|
|
38122
38174
|
let entries;
|
|
38123
38175
|
try {
|
|
38124
|
-
entries = readdirSync8(
|
|
38176
|
+
entries = readdirSync8(join23(directory, "runs"));
|
|
38125
38177
|
} catch {
|
|
38126
38178
|
continue;
|
|
38127
38179
|
}
|
|
38128
38180
|
for (const entry of entries) {
|
|
38129
38181
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
38130
|
-
const record = recordAt(
|
|
38182
|
+
const record = recordAt(join23(directory, "runs", entry));
|
|
38131
38183
|
if (record !== null) {
|
|
38132
38184
|
found.push(record);
|
|
38133
38185
|
}
|
|
@@ -38139,7 +38191,7 @@ function everyRunRecord() {
|
|
|
38139
38191
|
function recordAt(path) {
|
|
38140
38192
|
let parsed;
|
|
38141
38193
|
try {
|
|
38142
|
-
parsed = JSON.parse(
|
|
38194
|
+
parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
38143
38195
|
} catch {
|
|
38144
38196
|
return null;
|
|
38145
38197
|
}
|
|
@@ -38185,7 +38237,7 @@ function runningAs(pid) {
|
|
|
38185
38237
|
const began = startedAt(pid, null);
|
|
38186
38238
|
const when = began === null ? {} : { startedAt: began };
|
|
38187
38239
|
if (process.platform === "linux") {
|
|
38188
|
-
const raw =
|
|
38240
|
+
const raw = readFileSync21(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
38189
38241
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
38190
38242
|
}
|
|
38191
38243
|
const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
|
|
@@ -38287,7 +38339,7 @@ function judgeRun(record, running, needed) {
|
|
|
38287
38339
|
if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
|
|
38288
38340
|
return false;
|
|
38289
38341
|
}
|
|
38290
|
-
const engine = record.command === undefined ? null :
|
|
38342
|
+
const engine = record.command === undefined ? null : basename4(record.command);
|
|
38291
38343
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
38292
38344
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
38293
38345
|
if (wanted !== null && !said.includes(wanted)) {
|
|
@@ -38323,19 +38375,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
38323
38375
|
}
|
|
38324
38376
|
function clearRunRecord(projectPath) {
|
|
38325
38377
|
for (const directory of runtimeDirectories()) {
|
|
38326
|
-
const shared =
|
|
38378
|
+
const shared = join23(directory, "runs", SHARED_NOTE);
|
|
38327
38379
|
const sharedRecord = recordAt(shared);
|
|
38328
38380
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
38329
38381
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
38330
38382
|
try {
|
|
38331
|
-
|
|
38383
|
+
rmSync12(path, { force: true });
|
|
38332
38384
|
} catch {}
|
|
38333
38385
|
}
|
|
38334
38386
|
}
|
|
38335
38387
|
}
|
|
38336
38388
|
function ageMs(path, now) {
|
|
38337
38389
|
try {
|
|
38338
|
-
return now -
|
|
38390
|
+
return now - statSync8(path).mtimeMs;
|
|
38339
38391
|
} catch {
|
|
38340
38392
|
return 0;
|
|
38341
38393
|
}
|
|
@@ -38353,12 +38405,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
38353
38405
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
38354
38406
|
continue;
|
|
38355
38407
|
}
|
|
38356
|
-
const path =
|
|
38408
|
+
const path = join23(directory, entry);
|
|
38357
38409
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
38358
38410
|
continue;
|
|
38359
38411
|
}
|
|
38360
38412
|
try {
|
|
38361
|
-
|
|
38413
|
+
rmSync12(path);
|
|
38362
38414
|
} catch {}
|
|
38363
38415
|
}
|
|
38364
38416
|
}
|
|
@@ -38377,19 +38429,19 @@ import {
|
|
|
38377
38429
|
closeSync as closeSync2,
|
|
38378
38430
|
constants,
|
|
38379
38431
|
copyFileSync,
|
|
38380
|
-
existsSync as
|
|
38432
|
+
existsSync as existsSync16,
|
|
38381
38433
|
fstatSync,
|
|
38382
38434
|
mkdirSync as mkdirSync14,
|
|
38383
38435
|
mkdtempSync as mkdtempSync3,
|
|
38384
38436
|
openSync as openSync3,
|
|
38385
38437
|
readdirSync as readdirSync9,
|
|
38386
|
-
readFileSync as
|
|
38438
|
+
readFileSync as readFileSync22,
|
|
38387
38439
|
readSync as readSync2,
|
|
38388
38440
|
rmdirSync,
|
|
38389
|
-
statSync as
|
|
38441
|
+
statSync as statSync9
|
|
38390
38442
|
} from "node:fs";
|
|
38391
38443
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
38392
|
-
import { basename as
|
|
38444
|
+
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
38445
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
38394
38446
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
38395
38447
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -38404,15 +38456,15 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
38404
38456
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
38405
38457
|
}
|
|
38406
38458
|
const directory = realPathOr(dirname17(outputPath));
|
|
38407
|
-
if (!
|
|
38459
|
+
if (!existsSync16(directory)) {
|
|
38408
38460
|
return `outputPath's directory ${dirname17(outputPath)} does not exist; a capture creates no directories.`;
|
|
38409
38461
|
}
|
|
38410
|
-
const where =
|
|
38462
|
+
const where = join24(directory, basename5(outputPath));
|
|
38411
38463
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
38412
38464
|
if (inside !== undefined) {
|
|
38413
38465
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
38414
38466
|
}
|
|
38415
|
-
if (
|
|
38467
|
+
if (existsSync16(where)) {
|
|
38416
38468
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
38417
38469
|
}
|
|
38418
38470
|
return null;
|
|
@@ -38769,6 +38821,38 @@ function scanWaitAnswer(scanned) {
|
|
|
38769
38821
|
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
38822
|
};
|
|
38771
38823
|
}
|
|
38824
|
+
function changedSettings(before, after) {
|
|
38825
|
+
if (before === null || after === null) {
|
|
38826
|
+
return null;
|
|
38827
|
+
}
|
|
38828
|
+
const names = new Set([...before.keys(), ...after.keys()]);
|
|
38829
|
+
return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
|
|
38830
|
+
}
|
|
38831
|
+
function timedOutVerdict(timeoutMs, silentForMs) {
|
|
38832
|
+
const quiet = Math.round(silentForMs);
|
|
38833
|
+
return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
|
|
38834
|
+
verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
|
|
38835
|
+
hung: true
|
|
38836
|
+
} : {
|
|
38837
|
+
verdict: `timed out after ${timeoutMs} ms while still running: it last printed ${quiet} ms before it was killed, so pass a longer timeoutMs or a narrower path`,
|
|
38838
|
+
hung: false
|
|
38839
|
+
};
|
|
38840
|
+
}
|
|
38841
|
+
function importStatuses(payload) {
|
|
38842
|
+
const listed = payload["resources"];
|
|
38843
|
+
if (!Array.isArray(listed)) {
|
|
38844
|
+
return [];
|
|
38845
|
+
}
|
|
38846
|
+
return listed.map((entry) => {
|
|
38847
|
+
const one = asParams(entry);
|
|
38848
|
+
const reason = readString(one, "reason");
|
|
38849
|
+
return {
|
|
38850
|
+
path: readString(one, "path") ?? "",
|
|
38851
|
+
status: readString(one, "status") ?? "unknown",
|
|
38852
|
+
...reason === undefined ? {} : { reason }
|
|
38853
|
+
};
|
|
38854
|
+
});
|
|
38855
|
+
}
|
|
38772
38856
|
function escapingPropertyValue(properties) {
|
|
38773
38857
|
if (typeof properties !== "object" || properties === null) {
|
|
38774
38858
|
return;
|
|
@@ -38799,7 +38883,7 @@ function aboveTheRunner(entry) {
|
|
|
38799
38883
|
}
|
|
38800
38884
|
function elsewhereIn(projectPath, asked) {
|
|
38801
38885
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
38802
|
-
if (wanted === "" ||
|
|
38886
|
+
if (wanted === "" || existsSync16(join24(projectPath, wanted))) {
|
|
38803
38887
|
return "";
|
|
38804
38888
|
}
|
|
38805
38889
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -38813,7 +38897,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
38813
38897
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
38814
38898
|
}
|
|
38815
38899
|
function hasMainScene(projectFile) {
|
|
38816
|
-
const scene = parseProjectGodot(
|
|
38900
|
+
const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
38817
38901
|
return typeof scene === "string" && scene !== "";
|
|
38818
38902
|
}
|
|
38819
38903
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -38821,13 +38905,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
38821
38905
|
return false;
|
|
38822
38906
|
}
|
|
38823
38907
|
try {
|
|
38824
|
-
return
|
|
38908
|
+
return readFileSync22(join24(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
|
|
38825
38909
|
} catch {
|
|
38826
38910
|
return false;
|
|
38827
38911
|
}
|
|
38828
38912
|
}
|
|
38829
38913
|
function editorPlaysHeadless(projectFile) {
|
|
38830
|
-
const runArgs = parseProjectGodot(
|
|
38914
|
+
const runArgs = parseProjectGodot(readFileSync22(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
38831
38915
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
38832
38916
|
}
|
|
38833
38917
|
function camelCased(params) {
|
|
@@ -38846,7 +38930,7 @@ function needsItsOwnProcess(asked) {
|
|
|
38846
38930
|
class GodotServer {
|
|
38847
38931
|
mcp;
|
|
38848
38932
|
locator = new GodotLocator2;
|
|
38849
|
-
operationsScript =
|
|
38933
|
+
operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
38850
38934
|
godotBridge;
|
|
38851
38935
|
tools = buildToolDefinitions();
|
|
38852
38936
|
activeProcess = null;
|
|
@@ -39132,9 +39216,12 @@ class GodotServer {
|
|
|
39132
39216
|
try {
|
|
39133
39217
|
return await this.dispatch(tool, op, args);
|
|
39134
39218
|
} catch (error) {
|
|
39135
|
-
if (error instanceof McpError
|
|
39219
|
+
if (error instanceof McpError) {
|
|
39136
39220
|
throw error;
|
|
39137
39221
|
}
|
|
39222
|
+
if (error instanceof Refusal) {
|
|
39223
|
+
return this.createErrorResponse(error.message);
|
|
39224
|
+
}
|
|
39138
39225
|
const where = op === "" ? tool : `${tool} op=${op}`;
|
|
39139
39226
|
console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
|
|
39140
39227
|
const seenBefore = this.defectsSeen.before(where, errorMessage(error));
|
|
@@ -39291,7 +39378,14 @@ class GodotServer {
|
|
|
39291
39378
|
}
|
|
39292
39379
|
async dispatch(tool, op, args) {
|
|
39293
39380
|
if (ENGINE_PASSES[tool]?.[op] !== undefined) {
|
|
39294
|
-
return await this.handleRefreshUids(args);
|
|
39381
|
+
return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
|
|
39382
|
+
}
|
|
39383
|
+
const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
|
|
39384
|
+
if (settingsWrite !== undefined) {
|
|
39385
|
+
return await this.handleProjectSettingsWrite(op, settingsWrite, args);
|
|
39386
|
+
}
|
|
39387
|
+
if (tool === "project_import" && op === "set_options") {
|
|
39388
|
+
return await this.handleSetImportOptions(args);
|
|
39295
39389
|
}
|
|
39296
39390
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
39297
39391
|
if (headless !== undefined) {
|
|
@@ -39407,7 +39501,7 @@ class GodotServer {
|
|
|
39407
39501
|
return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
|
|
39408
39502
|
}
|
|
39409
39503
|
case "editor_launch":
|
|
39410
|
-
return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
|
|
39504
|
+
return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
|
|
39411
39505
|
case "editor_run":
|
|
39412
39506
|
if (op === "stop") {
|
|
39413
39507
|
return await this.handleStopProject(args);
|
|
@@ -39550,8 +39644,8 @@ class GodotServer {
|
|
|
39550
39644
|
if (path === undefined) {
|
|
39551
39645
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
39552
39646
|
}
|
|
39553
|
-
const file =
|
|
39554
|
-
if (!
|
|
39647
|
+
const file = join24(path, "project.godot");
|
|
39648
|
+
if (!existsSync16(file)) {
|
|
39555
39649
|
return {
|
|
39556
39650
|
ok: false,
|
|
39557
39651
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -39822,6 +39916,222 @@ class GodotServer {
|
|
|
39822
39916
|
}
|
|
39823
39917
|
}, scanned));
|
|
39824
39918
|
}
|
|
39919
|
+
async handleReimport(args) {
|
|
39920
|
+
const project = this.project(args);
|
|
39921
|
+
if (!project.ok) {
|
|
39922
|
+
return project.response;
|
|
39923
|
+
}
|
|
39924
|
+
const contained = this.containProjectFiles(args);
|
|
39925
|
+
if (!contained.ok) {
|
|
39926
|
+
return contained.response;
|
|
39927
|
+
}
|
|
39928
|
+
const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
39929
|
+
return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
|
|
39930
|
+
}
|
|
39931
|
+
editorHasOpen(projectPath) {
|
|
39932
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
39933
|
+
return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
|
|
39934
|
+
}
|
|
39935
|
+
async handleProjectSettingsWrite(op, operation, args) {
|
|
39936
|
+
const project = this.project(args);
|
|
39937
|
+
if (!project.ok) {
|
|
39938
|
+
return project.response;
|
|
39939
|
+
}
|
|
39940
|
+
const projectPath = project.value.path;
|
|
39941
|
+
const served = this.editorHasOpen(projectPath);
|
|
39942
|
+
const before = served ? this.settingKeysOf(projectPath) : null;
|
|
39943
|
+
const written = await this.headless(operation, args);
|
|
39944
|
+
if (!served || written.isError === true) {
|
|
39945
|
+
return written;
|
|
39946
|
+
}
|
|
39947
|
+
const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
|
|
39948
|
+
if (AUDIO_BUS_OPS.has(op)) {
|
|
39949
|
+
return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
|
|
39950
|
+
}
|
|
39951
|
+
const changed = changedSettings(before, this.settingKeysOf(projectPath));
|
|
39952
|
+
if (changed === null) {
|
|
39953
|
+
return this.jsonTextResponse({
|
|
39954
|
+
...answer,
|
|
39955
|
+
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."
|
|
39956
|
+
});
|
|
39957
|
+
}
|
|
39958
|
+
if (changed.length === 0) {
|
|
39959
|
+
return written;
|
|
39960
|
+
}
|
|
39961
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
|
|
39962
|
+
return this.jsonTextResponse({
|
|
39963
|
+
...answer,
|
|
39964
|
+
...taken,
|
|
39965
|
+
...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
|
|
39966
|
+
});
|
|
39967
|
+
}
|
|
39968
|
+
async editorTakes(command, args) {
|
|
39969
|
+
try {
|
|
39970
|
+
await this.godotBridge.invokeTool(command, args);
|
|
39971
|
+
return {};
|
|
39972
|
+
} catch (error) {
|
|
39973
|
+
const message = errorMessage(error);
|
|
39974
|
+
const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
|
|
39975
|
+
return {
|
|
39976
|
+
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.`
|
|
39977
|
+
};
|
|
39978
|
+
}
|
|
39979
|
+
}
|
|
39980
|
+
async handleSetImportOptions(args) {
|
|
39981
|
+
const project = this.project(args);
|
|
39982
|
+
if (!project.ok) {
|
|
39983
|
+
return project.response;
|
|
39984
|
+
}
|
|
39985
|
+
const contained = this.containProjectFiles(args);
|
|
39986
|
+
if (!contained.ok) {
|
|
39987
|
+
return contained.response;
|
|
39988
|
+
}
|
|
39989
|
+
const {
|
|
39990
|
+
op: _op,
|
|
39991
|
+
projectPath: _projectPath,
|
|
39992
|
+
reimport: _reimport,
|
|
39993
|
+
timeoutMs: _timeoutMs,
|
|
39994
|
+
...params
|
|
39995
|
+
} = contained.value;
|
|
39996
|
+
const written = await this.operation("set_import_options", params, project.value.path);
|
|
39997
|
+
if (!written.ok || args["reimport"] === false) {
|
|
39998
|
+
return this.answer(written.ok ? {
|
|
39999
|
+
...written,
|
|
40000
|
+
payload: {
|
|
40001
|
+
...written.payload,
|
|
40002
|
+
note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
|
|
40003
|
+
}
|
|
40004
|
+
} : written);
|
|
40005
|
+
}
|
|
40006
|
+
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
40007
|
+
if (!applied.ok) {
|
|
40008
|
+
return applied.response;
|
|
40009
|
+
}
|
|
40010
|
+
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
40011
|
+
}
|
|
40012
|
+
async reimport(projectPath, resourcePath, force, timeoutMs) {
|
|
40013
|
+
const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
|
|
40014
|
+
const before = await this.operation("get_import_status", ask, projectPath);
|
|
40015
|
+
if (!before.ok) {
|
|
40016
|
+
return { ok: false, response: this.answer(before) };
|
|
40017
|
+
}
|
|
40018
|
+
const listed = importStatuses(before.payload);
|
|
40019
|
+
const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
|
|
40020
|
+
if (resourcePath !== undefined && missingSource.length > 0) {
|
|
40021
|
+
return {
|
|
40022
|
+
ok: false,
|
|
40023
|
+
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
40024
|
+
};
|
|
40025
|
+
}
|
|
40026
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
40027
|
+
if (targets.length === 0) {
|
|
40028
|
+
return {
|
|
40029
|
+
ok: true,
|
|
40030
|
+
payload: {
|
|
40031
|
+
reimported: [],
|
|
40032
|
+
notReimported: [],
|
|
40033
|
+
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.`
|
|
40034
|
+
}
|
|
40035
|
+
};
|
|
40036
|
+
}
|
|
40037
|
+
const viaEditor = this.editorHasOpen(projectPath);
|
|
40038
|
+
const extra = {};
|
|
40039
|
+
if (viaEditor) {
|
|
40040
|
+
const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
|
|
40041
|
+
if (!inEditor.ok) {
|
|
40042
|
+
return inEditor;
|
|
40043
|
+
}
|
|
40044
|
+
if (!inEditor.finished) {
|
|
40045
|
+
return {
|
|
40046
|
+
ok: true,
|
|
40047
|
+
payload: {
|
|
40048
|
+
via: "editor",
|
|
40049
|
+
stillImporting: true,
|
|
40050
|
+
asked: targets,
|
|
40051
|
+
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.`
|
|
40052
|
+
}
|
|
40053
|
+
};
|
|
40054
|
+
}
|
|
40055
|
+
} else {
|
|
40056
|
+
const engine = await this.engine();
|
|
40057
|
+
if (!engine.ok) {
|
|
40058
|
+
return { ok: false, response: engine.response };
|
|
40059
|
+
}
|
|
40060
|
+
for (const target of targets) {
|
|
40061
|
+
forceNextImport(projectPath, target);
|
|
40062
|
+
}
|
|
40063
|
+
const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
|
|
40064
|
+
const imported = await runImport(engine.value, projectPath);
|
|
40065
|
+
if (!imported.ok) {
|
|
40066
|
+
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
40067
|
+
}
|
|
40068
|
+
if (imported.messages.length > 0) {
|
|
40069
|
+
extra["engine_messages"] = imported.messages;
|
|
40070
|
+
}
|
|
40071
|
+
}
|
|
40072
|
+
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
40073
|
+
if (!after.ok) {
|
|
40074
|
+
return { ok: false, response: this.answer(after) };
|
|
40075
|
+
}
|
|
40076
|
+
const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
|
|
40077
|
+
const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
|
|
40078
|
+
const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
|
|
40079
|
+
path,
|
|
40080
|
+
status: now.get(path)?.status ?? "unknown",
|
|
40081
|
+
...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
|
|
40082
|
+
}));
|
|
40083
|
+
return {
|
|
40084
|
+
ok: true,
|
|
40085
|
+
payload: {
|
|
40086
|
+
via: viaEditor ? "editor" : "engine",
|
|
40087
|
+
reimported,
|
|
40088
|
+
notReimported,
|
|
40089
|
+
...missingSource.length > 0 ? { missingSource } : {},
|
|
40090
|
+
...extra
|
|
40091
|
+
}
|
|
40092
|
+
};
|
|
40093
|
+
}
|
|
40094
|
+
async reimportInTheEditor(paths, timeoutMs) {
|
|
40095
|
+
const started = Date.now();
|
|
40096
|
+
let completedBefore;
|
|
40097
|
+
for (;; ) {
|
|
40098
|
+
while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
|
|
40099
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
40100
|
+
}
|
|
40101
|
+
let answer;
|
|
40102
|
+
try {
|
|
40103
|
+
answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
|
|
40104
|
+
} catch (error) {
|
|
40105
|
+
const message = errorMessage(error);
|
|
40106
|
+
return {
|
|
40107
|
+
ok: false,
|
|
40108
|
+
response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
|
|
40109
|
+
"editor_launch restart loads the addon this server ships",
|
|
40110
|
+
"Or close the editor, and project_import reimport runs the engine import instead"
|
|
40111
|
+
]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
|
|
40112
|
+
};
|
|
40113
|
+
}
|
|
40114
|
+
if (answer["started"] !== false) {
|
|
40115
|
+
completedBefore = readNumber(answer, "reimportsCompletedBefore");
|
|
40116
|
+
break;
|
|
40117
|
+
}
|
|
40118
|
+
if (Date.now() - started >= timeoutMs) {
|
|
40119
|
+
return { ok: true, finished: false };
|
|
40120
|
+
}
|
|
40121
|
+
await new Promise((settle) => setTimeout(settle, 100));
|
|
40122
|
+
}
|
|
40123
|
+
while (Date.now() - started < timeoutMs) {
|
|
40124
|
+
await new Promise((settle) => setTimeout(settle, 250));
|
|
40125
|
+
try {
|
|
40126
|
+
const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
|
|
40127
|
+
const completed = readNumber(status, "reimportsCompleted");
|
|
40128
|
+
if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
|
|
40129
|
+
return { ok: true, finished: true };
|
|
40130
|
+
}
|
|
40131
|
+
} catch {}
|
|
40132
|
+
}
|
|
40133
|
+
return { ok: true, finished: false };
|
|
40134
|
+
}
|
|
39825
40135
|
async operation(operation, params, projectPath) {
|
|
39826
40136
|
const engine = await this.engine();
|
|
39827
40137
|
if (!engine.ok) {
|
|
@@ -39863,11 +40173,11 @@ class GodotServer {
|
|
|
39863
40173
|
if (!engine.ok) {
|
|
39864
40174
|
return engine.response;
|
|
39865
40175
|
}
|
|
39866
|
-
const application = parseProjectGodot(
|
|
40176
|
+
const application = parseProjectGodot(readFileSync22(project.value.file, "utf8"))["application"] ?? {};
|
|
39867
40177
|
const name = application["config/name"];
|
|
39868
40178
|
const mainScene = application["run/main_scene"];
|
|
39869
40179
|
const info = {
|
|
39870
|
-
name: typeof name === "string" && name !== "" ? name :
|
|
40180
|
+
name: typeof name === "string" && name !== "" ? name : basename5(project.value.path),
|
|
39871
40181
|
path: project.value.path,
|
|
39872
40182
|
mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
|
|
39873
40183
|
godotVersion: await this.godotVersion(engine.value),
|
|
@@ -39931,11 +40241,11 @@ class GodotServer {
|
|
|
39931
40241
|
return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
39932
40242
|
}
|
|
39933
40243
|
const debug = readBoolean(args, "debug") ?? false;
|
|
39934
|
-
const exportLogs = mkdtempSync3(
|
|
40244
|
+
const exportLogs = mkdtempSync3(join24(tmpdir5(), "gdharness-export-"));
|
|
39935
40245
|
const exportArgs = [
|
|
39936
40246
|
"--headless",
|
|
39937
40247
|
"--log-file",
|
|
39938
|
-
|
|
40248
|
+
join24(exportLogs, "engine.log"),
|
|
39939
40249
|
"--path",
|
|
39940
40250
|
project.value.path,
|
|
39941
40251
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -39967,7 +40277,7 @@ class GodotServer {
|
|
|
39967
40277
|
log.finish();
|
|
39968
40278
|
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
39969
40279
|
const verdict = {
|
|
39970
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 &&
|
|
40280
|
+
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync16(output.absolutePath),
|
|
39971
40281
|
preset,
|
|
39972
40282
|
outputPath: output.relativePath,
|
|
39973
40283
|
debug,
|
|
@@ -40003,7 +40313,7 @@ class GodotServer {
|
|
|
40003
40313
|
return contained.response;
|
|
40004
40314
|
}
|
|
40005
40315
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
40006
|
-
if (!
|
|
40316
|
+
if (!existsSync16(join24(project.value.path, runner))) {
|
|
40007
40317
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
40008
40318
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
40009
40319
|
]);
|
|
@@ -40039,14 +40349,21 @@ class GodotServer {
|
|
|
40039
40349
|
];
|
|
40040
40350
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
|
|
40041
40351
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
40042
|
-
const userData = mkdtempSync3(
|
|
40352
|
+
const userData = mkdtempSync3(join24(tmpdir5(), "gdharness-tests-"));
|
|
40043
40353
|
const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
|
|
40044
40354
|
const console2 = [];
|
|
40355
|
+
let lastSaid = Date.now();
|
|
40045
40356
|
run.process.stdout?.on("data", (data) => {
|
|
40046
40357
|
console2.push(data);
|
|
40358
|
+
lastSaid = Date.now();
|
|
40047
40359
|
});
|
|
40048
|
-
|
|
40360
|
+
run.process.stderr?.on("data", () => {
|
|
40361
|
+
lastSaid = Date.now();
|
|
40362
|
+
});
|
|
40363
|
+
let silentForMs = 0;
|
|
40364
|
+
const timedOut = await new Promise((resolve) => {
|
|
40049
40365
|
const timer = setTimeout(() => {
|
|
40366
|
+
silentForMs = Date.now() - lastSaid;
|
|
40050
40367
|
run.process.kill();
|
|
40051
40368
|
resolve(true);
|
|
40052
40369
|
}, timeoutMs);
|
|
@@ -40059,14 +40376,14 @@ class GodotServer {
|
|
|
40059
40376
|
resolve(false);
|
|
40060
40377
|
});
|
|
40061
40378
|
});
|
|
40062
|
-
const reportsDir =
|
|
40379
|
+
const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
|
|
40063
40380
|
let report = null;
|
|
40064
40381
|
let reportProblem = null;
|
|
40065
40382
|
try {
|
|
40066
|
-
const written =
|
|
40383
|
+
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
40384
|
const newest = written.at(-1);
|
|
40068
40385
|
if (newest !== undefined) {
|
|
40069
|
-
report = parseJUnit(
|
|
40386
|
+
report = parseJUnit(readFileSync22(join24(reportsDir, newest, "results.xml"), "utf8"));
|
|
40070
40387
|
}
|
|
40071
40388
|
} catch (error) {
|
|
40072
40389
|
reportProblem = errorMessage(error);
|
|
@@ -40088,8 +40405,10 @@ class GodotServer {
|
|
|
40088
40405
|
const exitCode = run.exitCode;
|
|
40089
40406
|
const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
|
|
40090
40407
|
const said = printed.map((entry) => entry.text);
|
|
40091
|
-
const nothingRan =
|
|
40092
|
-
const
|
|
40408
|
+
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
40409
|
+
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
40410
|
+
const hung = cutShort?.hung ?? false;
|
|
40411
|
+
const verdict = cutShort !== null ? cutShort.verdict : nothingRan ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
40093
40412
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
40094
40413
|
if (report === null) {
|
|
40095
40414
|
const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
|
|
@@ -40105,6 +40424,7 @@ class GodotServer {
|
|
|
40105
40424
|
exitCode,
|
|
40106
40425
|
exitSignal: run.exitSignal ?? undefined,
|
|
40107
40426
|
hung,
|
|
40427
|
+
...timedOut ? { timedOut, silentForMs } : {},
|
|
40108
40428
|
arguments: cmdArgs,
|
|
40109
40429
|
entries: forAnswer(printed.slice(0, 60)),
|
|
40110
40430
|
savesNote,
|
|
@@ -40129,8 +40449,9 @@ class GodotServer {
|
|
|
40129
40449
|
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
40130
40450
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
40131
40451
|
return this.jsonTextResponse({
|
|
40132
|
-
passed: !
|
|
40452
|
+
passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
40133
40453
|
verdict,
|
|
40454
|
+
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
40134
40455
|
exitCode,
|
|
40135
40456
|
exitSignal: run.exitSignal ?? undefined,
|
|
40136
40457
|
tests: report.tests,
|
|
@@ -40209,11 +40530,11 @@ class GodotServer {
|
|
|
40209
40530
|
const cached = cachedClasses(projectPath);
|
|
40210
40531
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
40211
40532
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40212
|
-
if (!contained.ok || !
|
|
40533
|
+
if (!contained.ok || !existsSync16(contained.absolutePath)) {
|
|
40213
40534
|
return null;
|
|
40214
40535
|
}
|
|
40215
40536
|
try {
|
|
40216
|
-
return
|
|
40537
|
+
return readFileSync22(contained.absolutePath, "utf8");
|
|
40217
40538
|
} catch {
|
|
40218
40539
|
return null;
|
|
40219
40540
|
}
|
|
@@ -40263,11 +40584,11 @@ class GodotServer {
|
|
|
40263
40584
|
}
|
|
40264
40585
|
sourceOfClass(projectPath, resourcePath) {
|
|
40265
40586
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
40266
|
-
if (!contained.ok || !
|
|
40587
|
+
if (!contained.ok || !existsSync16(contained.absolutePath)) {
|
|
40267
40588
|
return null;
|
|
40268
40589
|
}
|
|
40269
40590
|
try {
|
|
40270
|
-
return
|
|
40591
|
+
return readFileSync22(contained.absolutePath, "utf8");
|
|
40271
40592
|
} catch {
|
|
40272
40593
|
return null;
|
|
40273
40594
|
}
|
|
@@ -40536,12 +40857,12 @@ class GodotServer {
|
|
|
40536
40857
|
return null;
|
|
40537
40858
|
}
|
|
40538
40859
|
try {
|
|
40539
|
-
return settingKeys(
|
|
40860
|
+
return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8"));
|
|
40540
40861
|
} catch {
|
|
40541
40862
|
return null;
|
|
40542
40863
|
}
|
|
40543
40864
|
}
|
|
40544
|
-
async handleRestartEditor() {
|
|
40865
|
+
async handleRestartEditor(args) {
|
|
40545
40866
|
const before = this.godotBridge.getStatus();
|
|
40546
40867
|
if (!before.connected) {
|
|
40547
40868
|
return this.createErrorResponse("No editor is connected, so there is nothing to restart.", [
|
|
@@ -40551,7 +40872,7 @@ class GodotServer {
|
|
|
40551
40872
|
}
|
|
40552
40873
|
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
40553
40874
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
40554
|
-
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
|
|
40875
|
+
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
40555
40876
|
if (asked.isError === true) {
|
|
40556
40877
|
return asked;
|
|
40557
40878
|
}
|
|
@@ -40589,7 +40910,7 @@ class GodotServer {
|
|
|
40589
40910
|
whatTheEditorDropped(before, after) {
|
|
40590
40911
|
return settingsDroppedReport(before, after);
|
|
40591
40912
|
}
|
|
40592
|
-
async startItAgain(projectPath, editorPid) {
|
|
40913
|
+
async startItAgain(projectPath, editorPid, hidden) {
|
|
40593
40914
|
const engine = await this.engine();
|
|
40594
40915
|
if (!engine.ok) {
|
|
40595
40916
|
return engine.response;
|
|
@@ -40611,7 +40932,7 @@ class GodotServer {
|
|
|
40611
40932
|
return asked;
|
|
40612
40933
|
}
|
|
40613
40934
|
await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
|
|
40614
|
-
const opened = await this.openAnEditor(engine.value, projectPath, ports);
|
|
40935
|
+
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
40615
40936
|
restartSettled(projectPath);
|
|
40616
40937
|
if (opened.error !== null) {
|
|
40617
40938
|
return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
|
|
@@ -40649,7 +40970,7 @@ class GodotServer {
|
|
|
40649
40970
|
const ports = await this.portsForAnEditor();
|
|
40650
40971
|
const before = this.settingKeysOf(project.value.path);
|
|
40651
40972
|
const unfinished = restartOwed(project.value.path);
|
|
40652
|
-
const opened = await this.openAnEditor(engine.value, project.value.path, ports);
|
|
40973
|
+
const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true);
|
|
40653
40974
|
if (opened.error !== null) {
|
|
40654
40975
|
return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
|
|
40655
40976
|
}
|
|
@@ -40665,18 +40986,19 @@ class GodotServer {
|
|
|
40665
40986
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
40666
40987
|
});
|
|
40667
40988
|
}
|
|
40668
|
-
async openAnEditor(engine, projectPath, ports) {
|
|
40989
|
+
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
40669
40990
|
clearEditorLog(projectPath);
|
|
40670
40991
|
const launched = await launchOutsideTheTree({
|
|
40671
40992
|
command: engine,
|
|
40672
|
-
args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
|
|
40993
|
+
args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
|
|
40673
40994
|
env: {
|
|
40674
40995
|
...process.env,
|
|
40675
40996
|
GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
|
|
40676
40997
|
GDHARNESS_LSP_PORT: String(ports.lsp),
|
|
40677
40998
|
GDHARNESS_DAP_PORT: String(ports.dap),
|
|
40678
40999
|
[OPENED_BY_A_SERVER]: "1"
|
|
40679
|
-
}
|
|
41000
|
+
},
|
|
41001
|
+
...hidden ? { desktop: HIDDEN_DESKTOP } : { noActivate: true }
|
|
40680
41002
|
});
|
|
40681
41003
|
if ("error" in launched) {
|
|
40682
41004
|
return { pid: null, error: launched.error };
|
|
@@ -40758,7 +41080,7 @@ class GodotServer {
|
|
|
40758
41080
|
userArgs: given.value
|
|
40759
41081
|
});
|
|
40760
41082
|
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);
|
|
41083
|
+
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
41084
|
if ("error" in started) {
|
|
40763
41085
|
return this.createErrorResponse(`The game could not be started: ${started.error}`, [
|
|
40764
41086
|
"editor_status says which Godot this server runs and whether it answers"
|
|
@@ -40788,7 +41110,7 @@ class GodotServer {
|
|
|
40788
41110
|
}
|
|
40789
41111
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
40790
41112
|
const budgetMs = budget.ms;
|
|
40791
|
-
if (!
|
|
41113
|
+
if (!existsSync16(join24(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
40792
41114
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
40793
41115
|
}
|
|
40794
41116
|
let asking = null;
|
|
@@ -40851,7 +41173,7 @@ class GodotServer {
|
|
|
40851
41173
|
}
|
|
40852
41174
|
let announcedAt;
|
|
40853
41175
|
try {
|
|
40854
|
-
announcedAt =
|
|
41176
|
+
announcedAt = statSync9(endpoint.file).mtimeMs;
|
|
40855
41177
|
} catch {
|
|
40856
41178
|
return;
|
|
40857
41179
|
}
|
|
@@ -41098,7 +41420,7 @@ class GodotServer {
|
|
|
41098
41420
|
});
|
|
41099
41421
|
return started;
|
|
41100
41422
|
}
|
|
41101
|
-
async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
|
|
41423
|
+
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
41102
41424
|
const startedAt = Date.now();
|
|
41103
41425
|
const transcript = openTranscript(startedAt);
|
|
41104
41426
|
closeSync2(transcript.fd);
|
|
@@ -41106,7 +41428,8 @@ class GodotServer {
|
|
|
41106
41428
|
command: godotPath,
|
|
41107
41429
|
args: cmdArgs,
|
|
41108
41430
|
...env === undefined ? {} : { env },
|
|
41109
|
-
run: { transcript: transcript.path, startedAt, projectPath }
|
|
41431
|
+
run: { transcript: transcript.path, startedAt, projectPath },
|
|
41432
|
+
...desktop === undefined ? {} : { desktop }
|
|
41110
41433
|
});
|
|
41111
41434
|
if ("error" in launched) {
|
|
41112
41435
|
return launched;
|
|
@@ -41601,7 +41924,7 @@ class GodotServer {
|
|
|
41601
41924
|
notes.push(endedWithoutACode({
|
|
41602
41925
|
throughEditor: run.throughEditor,
|
|
41603
41926
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
41604
|
-
couldAnnounce: run.projectPath !== null &&
|
|
41927
|
+
couldAnnounce: run.projectPath !== null && existsSync16(join24(run.projectPath, RUNTIME_AUTOLOAD2.path)),
|
|
41605
41928
|
endedHere: typeof run.endedHere === "string",
|
|
41606
41929
|
pickedUp: run.pickedUp === true
|
|
41607
41930
|
}));
|
|
@@ -42114,7 +42437,7 @@ class GodotServer {
|
|
|
42114
42437
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
42115
42438
|
const run = this.currentRun();
|
|
42116
42439
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
42117
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
42440
|
+
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
42441
|
return null;
|
|
42119
42442
|
}
|
|
42120
42443
|
const already = runtimesAnnounced().running;
|
|
@@ -42123,7 +42446,7 @@ class GodotServer {
|
|
|
42123
42446
|
return !run.announcedBefore.has(one.pid);
|
|
42124
42447
|
}
|
|
42125
42448
|
try {
|
|
42126
|
-
return
|
|
42449
|
+
return statSync9(one.file).mtimeMs >= run.startedAt;
|
|
42127
42450
|
} catch {
|
|
42128
42451
|
return false;
|
|
42129
42452
|
}
|
|
@@ -42193,8 +42516,8 @@ class GodotServer {
|
|
|
42193
42516
|
return this.createErrorResponse(refusal);
|
|
42194
42517
|
}
|
|
42195
42518
|
}
|
|
42196
|
-
const screenshotDir = expectsScreenshot ? mkdtempSync3(
|
|
42197
|
-
const screenshotPath = screenshotDir ?
|
|
42519
|
+
const screenshotDir = expectsScreenshot ? mkdtempSync3(join24(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
|
|
42520
|
+
const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
|
|
42198
42521
|
try {
|
|
42199
42522
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
42200
42523
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -42235,7 +42558,7 @@ class GodotServer {
|
|
|
42235
42558
|
type: "text",
|
|
42236
42559
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
42237
42560
|
},
|
|
42238
|
-
{ type: "image", data:
|
|
42561
|
+
{ type: "image", data: readFileSync22(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
42239
42562
|
]
|
|
42240
42563
|
};
|
|
42241
42564
|
} finally {
|
|
@@ -42335,7 +42658,7 @@ async function runGodotServer() {
|
|
|
42335
42658
|
const server = new GodotServer;
|
|
42336
42659
|
await server.run();
|
|
42337
42660
|
}
|
|
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/";
|
|
42661
|
+
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, STALLED_AFTER_MS = 30000, 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
42662
|
var init_server3 = __esm(() => {
|
|
42340
42663
|
init_mcp();
|
|
42341
42664
|
init_stdio2();
|
|
@@ -42348,6 +42671,7 @@ var init_server3 = __esm(() => {
|
|
|
42348
42671
|
init_class_note();
|
|
42349
42672
|
init_config_pin();
|
|
42350
42673
|
init_dap_client();
|
|
42674
|
+
init_desktop();
|
|
42351
42675
|
init_editor_log();
|
|
42352
42676
|
init_errors();
|
|
42353
42677
|
init_game_log();
|
|
@@ -42364,6 +42688,7 @@ var init_server3 = __esm(() => {
|
|
|
42364
42688
|
init_process_children();
|
|
42365
42689
|
init_process_time();
|
|
42366
42690
|
init_project_scan();
|
|
42691
|
+
init_reimport();
|
|
42367
42692
|
init_resources();
|
|
42368
42693
|
init_restart_note();
|
|
42369
42694
|
init_run_record();
|
|
@@ -42384,6 +42709,11 @@ var init_server3 = __esm(() => {
|
|
|
42384
42709
|
object: "an object"
|
|
42385
42710
|
};
|
|
42386
42711
|
HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
42712
|
+
AUDIO_BUS_OPS = new Set([
|
|
42713
|
+
"add_audio_bus",
|
|
42714
|
+
"set_audio_bus_effect",
|
|
42715
|
+
"set_audio_bus_volume"
|
|
42716
|
+
]);
|
|
42387
42717
|
PATH_SOLUTIONS = [
|
|
42388
42718
|
'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
|
|
42389
42719
|
"Point projectPath at the project the file belongs to"
|
|
@@ -42415,8 +42745,8 @@ var init_server3 = __esm(() => {
|
|
|
42415
42745
|
});
|
|
42416
42746
|
|
|
42417
42747
|
// src/cli.ts
|
|
42418
|
-
import { existsSync as
|
|
42419
|
-
import { join as
|
|
42748
|
+
import { existsSync as existsSync17 } from "fs";
|
|
42749
|
+
import { join as join25, resolve as resolve6 } from "path";
|
|
42420
42750
|
|
|
42421
42751
|
// src/bridge-announce.ts
|
|
42422
42752
|
init_errors();
|
|
@@ -44050,7 +44380,9 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
44050
44380
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
44051
44381
|
|
|
44052
44382
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
44053
|
-
window and refuses headless rather than handing back the last frame anything drew.
|
|
44383
|
+
window and refuses headless rather than handing back the last frame anything drew. On Windows a
|
|
44384
|
+
windowed run this server starts is on a desktop of its own, where it draws and can be captured but
|
|
44385
|
+
never shows or takes the keyboard.
|
|
44054
44386
|
|
|
44055
44387
|
The autoload reads these settings and no others. \`${BIND_ADDRESS_SETTING}\`
|
|
44056
44388
|
is \`127.0.0.1\` and should stay there: the command set includes \`call_method\`, \`set_property\`
|
|
@@ -44102,7 +44434,7 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
|
|
|
44102
44434
|
\`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
|
|
44103
44435
|
declaration and the cache can both be right while the editor stays blind to it. Rescan again on
|
|
44104
44436
|
its own, which is the measured cure and needs no change to the declaring script; \`editor_launch
|
|
44105
|
-
restart\` also does it and costs a
|
|
44437
|
+
restart\` also does it and costs a restart of the editor.
|
|
44106
44438
|
|
|
44107
44439
|
The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
|
|
44108
44440
|
a class it cannot resolve would go out of that file with it. The answer names that loss under
|
|
@@ -44240,7 +44572,7 @@ class UsageError extends Refusal2 {
|
|
|
44240
44572
|
function projectArgument(at) {
|
|
44241
44573
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
44242
44574
|
const projectPath = resolve6(named ?? ".");
|
|
44243
|
-
if (!
|
|
44575
|
+
if (!existsSync17(join25(projectPath, "project.godot"))) {
|
|
44244
44576
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
44245
44577
|
}
|
|
44246
44578
|
return projectPath;
|
|
@@ -44279,7 +44611,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
44279
44611
|
return;
|
|
44280
44612
|
}
|
|
44281
44613
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
44282
|
-
if (!
|
|
44614
|
+
if (!existsSync17(join25(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
44283
44615
|
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
44616
|
}
|
|
44285
44617
|
}
|
|
@@ -44411,7 +44743,7 @@ async function setup() {
|
|
|
44411
44743
|
console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
|
|
44412
44744
|
}
|
|
44413
44745
|
if (!args.includes("--no-skill")) {
|
|
44414
|
-
const directories = skillDirectories(chosen.harnesses, projectPath,
|
|
44746
|
+
const directories = skillDirectories(chosen.harnesses, projectPath, existsSync17);
|
|
44415
44747
|
for (const written of writeSkill(directories, getLocalVersion())) {
|
|
44416
44748
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44417
44749
|
}
|
|
@@ -44505,7 +44837,7 @@ async function upgrade() {
|
|
|
44505
44837
|
moved.push(took.from);
|
|
44506
44838
|
}
|
|
44507
44839
|
}
|
|
44508
|
-
for (const written of writeSkill(skillDirectories(already, projectPath,
|
|
44840
|
+
for (const written of writeSkill(skillDirectories(already, projectPath, existsSync17), version)) {
|
|
44509
44841
|
console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
|
|
44510
44842
|
}
|
|
44511
44843
|
console.log(`
|