gdharness 1.3.65 → 1.3.67
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/README.md +1 -1
- package/build/cli.js +371 -138
- package/build/godot/addons/gdharness_editor/bridge_client.gd +13 -2
- package/build/godot/addons/gdharness_editor/picture_saves.gd +48 -0
- package/build/godot/addons/gdharness_editor/picture_saves.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +4 -1
- package/build/godot/addons/gdharness_editor/scene_file.gd +5 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +15 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +8 -6
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +4 -1
- package/build/godot/addons/gdharness_editor/tools/view_tools.gd +121 -0
- package/build/godot/addons/gdharness_editor/tools/view_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/picture_saves.gd +48 -0
- package/build/godot/addons/gdharness_runtime/picture_saves.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +3 -43
- package/build/godot/operations/gdscript_analysis.gd +17 -0
- package/build/godot/operations/picture_saves.gd +48 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -2003,9 +2003,6 @@ function nonEmpty(value) {
|
|
|
2003
2003
|
function keptSlotsOf(count) {
|
|
2004
2004
|
return Math.max(2, Math.floor(count / 4));
|
|
2005
2005
|
}
|
|
2006
|
-
function reservedSlotOf(count) {
|
|
2007
|
-
return count - Math.max(1, count - keptSlotsOf(count)) >= 2 ? count - 1 : null;
|
|
2008
|
-
}
|
|
2009
2006
|
function slotWaiters(directory) {
|
|
2010
2007
|
const marks = join6(directory, WAITING_FOLDER);
|
|
2011
2008
|
let names;
|
|
@@ -2072,8 +2069,20 @@ class SlotLine {
|
|
|
2072
2069
|
return { ...holder, ...this.game ? { game: true } : {}, taker: this.taker, since: this.since / 1000 };
|
|
2073
2070
|
}
|
|
2074
2071
|
ahead(waiters) {
|
|
2075
|
-
const
|
|
2076
|
-
|
|
2072
|
+
const now = this.clock();
|
|
2073
|
+
const young = (taker, game, since) => taker === "other" && (game || now - since < ONE_OFF_AGED_MS) ? 1 : 0;
|
|
2074
|
+
const own = {
|
|
2075
|
+
group: young(this.taker, this.game, this.since),
|
|
2076
|
+
since: this.since,
|
|
2077
|
+
mark: this.mark ?? ""
|
|
2078
|
+
};
|
|
2079
|
+
const placeOf = (one) => ({
|
|
2080
|
+
group: young(one.taker, one.game, one.since),
|
|
2081
|
+
since: one.since,
|
|
2082
|
+
mark: one.mark
|
|
2083
|
+
});
|
|
2084
|
+
const before = (a, b) => a.group - b.group || a.since - b.since || (a.mark < b.mark ? -1 : a.mark > b.mark ? 1 : 0);
|
|
2085
|
+
return waiters.filter((one) => one.mark !== this.mark && before(placeOf(one), own) < 0).toSorted((a, b) => before(placeOf(a), placeOf(b)));
|
|
2077
2086
|
}
|
|
2078
2087
|
admitted(numbers, theirs) {
|
|
2079
2088
|
const now = this.clock();
|
|
@@ -2085,7 +2094,7 @@ class SlotLine {
|
|
|
2085
2094
|
}
|
|
2086
2095
|
}
|
|
2087
2096
|
function gamesKeptOf(kept) {
|
|
2088
|
-
return Math.max(0, Math.min(Math.floor(kept / 2), kept -
|
|
2097
|
+
return Math.max(0, Math.min(Math.floor(kept / 2), kept - 1));
|
|
2089
2098
|
}
|
|
2090
2099
|
function slotsDrawnOn(count, taker, queue) {
|
|
2091
2100
|
const gated = Math.max(1, count - keptSlotsOf(count));
|
|
@@ -2094,14 +2103,35 @@ function slotsDrawnOn(count, taker, queue) {
|
|
|
2094
2103
|
}
|
|
2095
2104
|
const every = Array.from({ length: count }, (_, slot) => count - 1 - slot);
|
|
2096
2105
|
const kept = every.slice(0, count - gated);
|
|
2106
|
+
if (taker === "tier") {
|
|
2107
|
+
return { numbers: [...every.slice(count - gated), ...kept] };
|
|
2108
|
+
}
|
|
2097
2109
|
if (taker === "timing" || queue === "none" || kept.length === 0) {
|
|
2098
2110
|
return { numbers: every };
|
|
2099
2111
|
}
|
|
2100
|
-
if (
|
|
2101
|
-
|
|
2102
|
-
return { numbers: [...kept].reverse().filter((slot) => slot !== reserve), limit: "tier" };
|
|
2112
|
+
if (queue === "spare") {
|
|
2113
|
+
return { numbers: [...kept, ...every.slice(count - gated)] };
|
|
2103
2114
|
}
|
|
2104
|
-
return { numbers: kept
|
|
2115
|
+
return { numbers: kept, limit: "kept" };
|
|
2116
|
+
}
|
|
2117
|
+
function spareSaid(content, now) {
|
|
2118
|
+
let said;
|
|
2119
|
+
try {
|
|
2120
|
+
said = JSON.parse(content ?? "");
|
|
2121
|
+
} catch {
|
|
2122
|
+
return 0;
|
|
2123
|
+
}
|
|
2124
|
+
if (!isRecord(said)) {
|
|
2125
|
+
return 0;
|
|
2126
|
+
}
|
|
2127
|
+
const { spare, written } = said;
|
|
2128
|
+
if (typeof spare !== "number" || !Number.isInteger(spare) || typeof written !== "number") {
|
|
2129
|
+
return 0;
|
|
2130
|
+
}
|
|
2131
|
+
return now - written > SPARE_STALE_MS ? 0 : Math.max(0, spare);
|
|
2132
|
+
}
|
|
2133
|
+
function forAGame(queue, game) {
|
|
2134
|
+
return game && queue === "spare" ? "queued" : queue;
|
|
2105
2135
|
}
|
|
2106
2136
|
function inAGate(env = process4.env) {
|
|
2107
2137
|
return nonEmpty(env[SHARE_VARIABLE]) !== undefined && nonEmpty(env[SHARE_COUNT_VARIABLE]) !== undefined;
|
|
@@ -2294,10 +2324,8 @@ class EngineSlots {
|
|
|
2294
2324
|
const aged = taker === "other" && !game && waitedMs >= ONE_OFF_AGED_MS;
|
|
2295
2325
|
let queue;
|
|
2296
2326
|
const queued = async () => queue ??= await this.gateQueue();
|
|
2297
|
-
this.drawn = slotsDrawnOn(this.count, taker, taker === "
|
|
2327
|
+
this.drawn = slotsDrawnOn(this.count, taker, taker === "other" && !aged ? forAGame(await queued(), game) : "none");
|
|
2298
2328
|
if (game && this.drawn.limit === "kept") {
|
|
2299
|
-
const reserve = reservedSlotOf(this.count);
|
|
2300
|
-
this.drawn = { ...this.drawn, numbers: this.drawn.numbers.filter((slot) => slot !== reserve) };
|
|
2301
2329
|
const kept = this.count - Math.max(1, this.count - keptSlotsOf(this.count));
|
|
2302
2330
|
const games = await this.heldByGames(Array.from({ length: kept }, (_, at) => this.count - kept + at));
|
|
2303
2331
|
if (games.length >= gamesKeptOf(kept)) {
|
|
@@ -2353,12 +2381,11 @@ class EngineSlots {
|
|
|
2353
2381
|
return new Map;
|
|
2354
2382
|
}
|
|
2355
2383
|
const now = Date.now();
|
|
2356
|
-
const reserve = reservedSlotOf(this.count);
|
|
2357
2384
|
const drawn = [];
|
|
2358
2385
|
for (const one of ahead) {
|
|
2359
2386
|
const aged = one.taker === "other" && !one.game && now - one.since >= ONE_OFF_AGED_MS;
|
|
2360
|
-
const theirs = slotsDrawnOn(this.count, one.taker, one.taker === "
|
|
2361
|
-
drawn.push(
|
|
2387
|
+
const theirs = slotsDrawnOn(this.count, one.taker, one.taker === "other" && !aged ? forAGame(await queued(), one.game) : "none");
|
|
2388
|
+
drawn.push(theirs.numbers);
|
|
2362
2389
|
}
|
|
2363
2390
|
return new Map([...slotsClaimed(drawn, open)].map(([slot, at]) => [slot, ahead[at]?.mark ?? ""]));
|
|
2364
2391
|
}
|
|
@@ -2433,7 +2460,12 @@ class EngineSlots {
|
|
|
2433
2460
|
live = "queued";
|
|
2434
2461
|
}
|
|
2435
2462
|
}
|
|
2436
|
-
return live;
|
|
2463
|
+
return live === "queued" && await this.spare() > 0 ? "spare" : live;
|
|
2464
|
+
}
|
|
2465
|
+
async spare() {
|
|
2466
|
+
const path = join6(this.directory, SPARE_FILE);
|
|
2467
|
+
const said = spareSaid(readWhole(path), Date.now());
|
|
2468
|
+
return said > 0 && !await this.free(path) ? said : 0;
|
|
2437
2469
|
}
|
|
2438
2470
|
holders() {
|
|
2439
2471
|
const found = [];
|
|
@@ -2654,7 +2686,7 @@ function slotWaitSaid(waitedMs, count, directory, holders = [], drawn = { number
|
|
|
2654
2686
|
const named = holdersNamed(taking, holders);
|
|
2655
2687
|
const waiting = `${so}: ${slots}`;
|
|
2656
2688
|
const back = `the engine starts when ${taking === 1 ? "it" : "one"} is given back`;
|
|
2657
|
-
const starts = drawn.limit === "kept" || drawn.limit === "
|
|
2689
|
+
const starts = drawn.limit === "kept" || drawn.limit === "games" ? `${back} or no gate is queued` : back;
|
|
2658
2690
|
const said = named.length === 0 ? `${waiting}, by a gate or another tool, and ${starts}.` : taking === 1 ? `${waiting}, by ${named.join(", ")}, and ${starts}.` : `${waiting}: ${listed(named)}. ${starts.charAt(0).toUpperCase()}${starts.slice(1)}.`;
|
|
2659
2691
|
return `${said}${leftSaid(drawn)}`;
|
|
2660
2692
|
}
|
|
@@ -2674,9 +2706,6 @@ function slotsSaid(taking, count, directory, limit) {
|
|
|
2674
2706
|
if (limit === "gate") {
|
|
2675
2707
|
return `${taking === 1 ? "the one engine slot" : `the ${String(taking)} engine slots`} of the ${String(count)} ${where} a gate's engines draw on`;
|
|
2676
2708
|
}
|
|
2677
|
-
if (limit === "tier") {
|
|
2678
|
-
return `${taking === 1 ? "the one engine slot" : `the ${String(taking)} engine slots`} of the ${String(count)} ${where} a gate's own steps take while a gate is queued`;
|
|
2679
|
-
}
|
|
2680
2709
|
if (limit === "games") {
|
|
2681
2710
|
return `${taking === 1 ? "the one engine slot" : `the ${String(taking)} engine slots`} of the ${String(count)} ${where} that games editor_run started may hold while a gate is queued, the rest kept for short calls,`;
|
|
2682
2711
|
}
|
|
@@ -2743,7 +2772,7 @@ async function inASlot(body) {
|
|
|
2743
2772
|
held.release();
|
|
2744
2773
|
}
|
|
2745
2774
|
}
|
|
2746
|
-
var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT",
|
|
2775
|
+
var HELD_VARIABLE = "GODOT_ENGINE_HELD", SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", QUEUED_FOLDER = "queued", SHARE_VARIABLE = "GODOT_ENGINE_SHARE", SHARE_COUNT_VARIABLE = "GODOT_ENGINE_SHARE_COUNT", ONE_OFF_AGED_MS = 120000, SLOT_YIELD_MS = 5000, MEMORY_TAKER = "memory", TIMED_VARIABLE = "GODOT_ENGINE_TIMED", TIER_VARIABLE = "GODOT_ENGINE_TIER", SEATED_SUFFIX = ".seated", SPARE_FILE = "spare.json", SPARE_STALE_MS = 1e4, waitsMarked = 0, ENGINE_PID_LOOK_MS = 100, ours, saidTo, takenBy;
|
|
2747
2776
|
var init_engine_slots = __esm(() => {
|
|
2748
2777
|
init_alive();
|
|
2749
2778
|
init_cache_home();
|
|
@@ -13081,13 +13110,17 @@ function crashIn(lines) {
|
|
|
13081
13110
|
const at = trimmed.findIndex((line) => CRASH_HEADER.test(line));
|
|
13082
13111
|
const said = trimmed.some((line) => line.includes(QUEUE_OVERFLOW_LINE));
|
|
13083
13112
|
if (at === -1) {
|
|
13084
|
-
return said ? { signal:
|
|
13113
|
+
return said ? { reported: false, signal: undefined, overflowed: true } : null;
|
|
13085
13114
|
}
|
|
13086
13115
|
const report = trimmed.slice(at);
|
|
13087
13116
|
const build = report.map((line) => CRASH_BUILD.exec(line)?.[1]).find((commit) => commit !== undefined);
|
|
13088
13117
|
const frames = build === undefined ? undefined : QUEUE_OVERFLOW_FRAMES.get(build);
|
|
13089
13118
|
const framed = frames?.every((frame) => report.some((line) => line.includes(`(${frame})`))) === true;
|
|
13090
|
-
return { signal: CRASH_HEADER.exec(trimmed[at] ?? "")?.[1]
|
|
13119
|
+
return { reported: true, signal: CRASH_HEADER.exec(trimmed[at] ?? "")?.[1], overflowed: said || framed };
|
|
13120
|
+
}
|
|
13121
|
+
function crashSaid(crashed, during) {
|
|
13122
|
+
const signal = crashed.signal === undefined ? "" : `, its crash handler reporting signal ${crashed.signal}`;
|
|
13123
|
+
return `the engine crashed${during}${signal}`;
|
|
13091
13124
|
}
|
|
13092
13125
|
function whileRunning(running) {
|
|
13093
13126
|
if (running === null) {
|
|
@@ -13115,10 +13148,10 @@ function judgeEngine(printed, handed, end) {
|
|
|
13115
13148
|
faults.push(end.ended);
|
|
13116
13149
|
} else if (end.code !== 0 && !explained) {
|
|
13117
13150
|
const free = end.freeBytes === undefined ? "" : `, with ${(end.freeBytes / 1024 ** 3).toFixed(1)} GB of memory free as it ended`;
|
|
13118
|
-
faults.push(crashed
|
|
13151
|
+
faults.push(crashed?.reported === true ? `${crashSaid(crashed, whileRunning(printed.running))}${free}` : end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}${free}` : `the engine exited ${exitNamed(end.code)}${free}`);
|
|
13119
13152
|
}
|
|
13120
13153
|
if (crashed?.overflowed === true) {
|
|
13121
|
-
faults.push(`the engine's call queue overflowed${whileRunning(printed.running)}: something queued deferred calls without end within one frame, most often a layout that changes the size it is laid out from in a deferred handler, so the frame never ends${crashed.
|
|
13154
|
+
faults.push(`the engine's call queue overflowed${whileRunning(printed.running)}: something queued deferred calls without end within one frame, most often a layout that changes the size it is laid out from in a deferred handler, so the frame never ends${crashed.reported ? ", and the engine crashed reporting the overflow" : ""}`);
|
|
13122
13155
|
}
|
|
13123
13156
|
if (printed.lines.every((line) => line.trim() === "")) {
|
|
13124
13157
|
faults.push("the engine printed nothing at all");
|
|
@@ -13250,7 +13283,7 @@ function judgeCompile(printed, end) {
|
|
|
13250
13283
|
if (end.ended !== null) {
|
|
13251
13284
|
faults.push(end.ended);
|
|
13252
13285
|
} else if (end.code !== 0) {
|
|
13253
|
-
faults.push(crashed
|
|
13286
|
+
faults.push(crashed?.reported === true ? crashSaid(crashed, "") : end.code === null ? `the engine was ended by ${end.signal ?? "a signal"}` : `the engine exited ${exitNamed(end.code)}`);
|
|
13254
13287
|
}
|
|
13255
13288
|
if (!printed.lines.some((line) => line.trim() === COMPILED_SAID)) {
|
|
13256
13289
|
faults.push("it never said it had compiled them");
|
|
@@ -13390,7 +13423,7 @@ var init_suite_judge = __esm(() => {
|
|
|
13390
13423
|
[105, "gdUnit4's code for scripts that did not compile"]
|
|
13391
13424
|
]);
|
|
13392
13425
|
GODOT_EXITS = new Set([1, 255]);
|
|
13393
|
-
CRASH_HEADER = /^(?:CrashHandlerException|handle_crash): Program crashed with signal (\d+)
|
|
13426
|
+
CRASH_HEADER = /^(?:CrashHandlerException|handle_crash): Program crashed(?: with signal (\d+))?$/;
|
|
13394
13427
|
CRASH_BUILD = /^Engine version: .*\(([0-9a-f]{40})\)$/;
|
|
13395
13428
|
QUEUE_OVERFLOW_FRAMES = new Map([
|
|
13396
13429
|
["ed1daf0bf001b61586d9930840f2f1394092c079", ["main+3e1082a", "main+3e0f4aa"]]
|
|
@@ -13969,11 +14002,14 @@ function runArguments(options) {
|
|
|
13969
14002
|
}
|
|
13970
14003
|
return args;
|
|
13971
14004
|
}
|
|
13972
|
-
function editorArguments(projectPath, ports, logFile, hidden, silent) {
|
|
14005
|
+
function editorArguments(projectPath, ports, logFile, hidden, silent, platform = process.platform) {
|
|
14006
|
+
const headless = hidden && platform !== "win32";
|
|
14007
|
+
const windowed = hidden && !headless;
|
|
13973
14008
|
return [
|
|
13974
14009
|
"-e",
|
|
13975
|
-
...
|
|
13976
|
-
...
|
|
14010
|
+
...headless ? ["--headless"] : [],
|
|
14011
|
+
...windowed ? hiddenWindowFlags([], false) : [],
|
|
14012
|
+
...(silent || hidden) && !headless ? ["--audio-driver", "Dummy"] : [],
|
|
13977
14013
|
"--path",
|
|
13978
14014
|
projectPath,
|
|
13979
14015
|
"--lsp-port",
|
|
@@ -13981,12 +14017,13 @@ function editorArguments(projectPath, ports, logFile, hidden, silent) {
|
|
|
13981
14017
|
"--dap-port",
|
|
13982
14018
|
String(ports.dap),
|
|
13983
14019
|
"--log-file",
|
|
13984
|
-
logFile
|
|
14020
|
+
logFile,
|
|
14021
|
+
...windowed ? ["--", HIDDEN_EDITOR_MARK] : []
|
|
13985
14022
|
];
|
|
13986
14023
|
}
|
|
13987
14024
|
function startedAs(commandLine) {
|
|
13988
14025
|
const words = commandLineWords(commandLine);
|
|
13989
|
-
const hidden = words.includes("--headless");
|
|
14026
|
+
const hidden = words.includes("--headless") || words.includes(HIDDEN_EDITOR_MARK);
|
|
13990
14027
|
const driver = words.indexOf("--audio-driver");
|
|
13991
14028
|
return { hidden, silent: hidden || driver >= 0 && words[driver + 1] === "Dummy" };
|
|
13992
14029
|
}
|
|
@@ -14035,7 +14072,7 @@ function environmentFor2(options, variables = process.env) {
|
|
|
14035
14072
|
const base = options.savesIn === undefined ? { ...variables } : userDataIn(options.savesIn, variables);
|
|
14036
14073
|
return { ...base, ...options.env, GDHARNESS_RUNTIME_DIR: options.runtimeDirectory };
|
|
14037
14074
|
}
|
|
14038
|
-
var HIDDEN_WINDOW_FPS = 60, ENGINE_FLAGS, VALUE_KIND, SET_BY_ARGUMENT, OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
14075
|
+
var HIDDEN_WINDOW_FPS = 60, ENGINE_FLAGS, VALUE_KIND, SET_BY_ARGUMENT, HIDDEN_EDITOR_MARK = "--gdharness-hidden", OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER", EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID", RESERVED_VARIABLE_PREFIX = "GDHARNESS_", SAVES_NOT_MOVED_NOTE = "On macOS the engine reads user:// off HOME and ignores the variables that move it elsewhere, measured on Godot 4.7.2, so this run wrote its saves where the player keeps theirs. A suite that saves a game has written into the same folder as the copy being played, and one that tidies up after itself may have removed real saves.";
|
|
14039
14076
|
var init_launch = __esm(() => {
|
|
14040
14077
|
ENGINE_FLAGS = Object.freeze({
|
|
14041
14078
|
"--verbose": "none",
|
|
@@ -14452,7 +14489,7 @@ function answersTo(entry, severity, contains) {
|
|
|
14452
14489
|
var HEADLINE, CRASH, GDUNIT_CASE, CRASH_FILLER, COLOUR_CODE, STREAMS, ADMITTED, LOCATION2, FRAME, LONGEST_CYCLE = 64;
|
|
14453
14490
|
var init_game_log = __esm(() => {
|
|
14454
14491
|
HEADLINE = /^(USER )?(SCRIPT ERROR|SHADER ERROR|ERROR|WARNING):\s?(.*)$/;
|
|
14455
|
-
CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed with signal \d
|
|
14492
|
+
CRASH = /^(?:CrashHandlerException|handle_crash): Program crashed(?: with signal \d+)?\s*$/;
|
|
14456
14493
|
GDUNIT_CASE = /^\s+res:\/\/.+?\.(?:gd|cs) > .+? (?:STARTED|PASSED|FAILED|ERROR|SKIPPED|FLAKY)(?:\s|$)/;
|
|
14457
14494
|
CRASH_FILLER = /^(?:=+|-- END OF .* BACKTRACE --|\[\d+\] .* - no debug info in .*)$/;
|
|
14458
14495
|
COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
@@ -15475,6 +15512,7 @@ var init_scratch = __esm(() => {
|
|
|
15475
15512
|
KINDS = [
|
|
15476
15513
|
"answers",
|
|
15477
15514
|
"desktop",
|
|
15515
|
+
"editor-capture",
|
|
15478
15516
|
"export",
|
|
15479
15517
|
"import",
|
|
15480
15518
|
"params",
|
|
@@ -19461,19 +19499,19 @@ var init_tool_definitions = __esm(() => {
|
|
|
19461
19499
|
{
|
|
19462
19500
|
name: "editor_launch",
|
|
19463
19501
|
title: "Open, restart or close the editor, or bind its debug adapter again",
|
|
19464
|
-
description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import. A call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. 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
|
|
19502
|
+
description: "Opens the Godot editor on a project, or restarts or closes the one already connected, or binds its debug adapter again. close saves open scenes, quits the editor and waits for its process to go, which is the only way to end a hidden one short of killing it. An editor this server opened that has not connected yet has nothing of this server's to save, so close stops its process instead. open answers once the process has started, and the editor dials in after its import. A call needing the editor made before then waits for the one this server opened, for as long as a restart waits, and is refused naming it if it has not arrived. open refuses while an editor of the project is already running and has not reached this server, naming its pid, rather than putting a second on the project. 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 out of sight instead. On Windows it gets a desktop of its own, shared by the games it plays, and still draws its views for editor_capture. On Linux and macOS it is headless, so editor_capture refuses it. 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. An editor a server opened is started again by the server, hidden or visible and silent or not as it was started unless hidden or silent says otherwise, once the old one has gone: it waits 90 seconds for that, or GDHARNESS_EDITOR_RESTART_TIMEOUT_MS, and an old editor still running after it is not replaced, since the new one would find its ports held. 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. Godot tries each server port once, as the editor starts, so a restart binds a debug adapter that came back without its port again, in the editor that came back. rebind does the same for the connected editor without restarting it, whoever opened it. A debug session open on the adapter ends with it and is opened again by the next debug call. 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 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 in editor_status rather than waiting for an editor that is not coming, and open finishes it. An open is refused when the editor it would start could never reach this server. Those are a project other than the one this server serves (its editor dials that project's own server), a project without the gdharness editor addon installed and enabled, and a project whose editor a restart is already bringing back.",
|
|
19465
19503
|
answer: "close answers settingsDropped, naming what its save took out of project.godot and left out when it took nothing, and settingsNote when the file could not be read to tell. An editor stopped before it connected is answered with beforeConnecting. close and restart name under savedScenes the open scenes that had unsaved changes and were saved on the way out, an empty list when none had. A scene that would not save refuses the close or restart and leaves the editor running. Both answer tookMs, how long the editor took to go or to come back. restart answers with the version that came back, and hidden, whether it came back with no window, and says under settingsDropped which keys its save dropped, 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 answers lspListening and dapListening once the editor's servers are up. When either came back without its port, serversSaid gives the lines the editor's log panel has about its servers, the one place Godot says why a port would not bind, and when it was the debug adapter, which is bound again in the editor that came back, debugAdapterRebound says whether that took. Neither is there when both came up. Godot clears that panel whenever a game is played, unless its run/output/always_clear_output_on_play setting is off, so after a play the lines from the editor's own start are gone. rebind answers rebound, with lspListening, dapListening and serversSaid, the lines that rebind made the panel print. A restart that a server's end interrupted shows as restartInterrupted in editor_status, and the open that finishes it says so under finishesRestart.",
|
|
19466
19504
|
parameters: {
|
|
19467
19505
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
19468
19506
|
hidden: {
|
|
19469
19507
|
type: "boolean",
|
|
19470
19508
|
ops: ["open", "restart"],
|
|
19471
|
-
description: "Open the editor
|
|
19509
|
+
description: "Open the editor out of sight, with the games it plays: on a desktop of its own on Windows, and headless elsewhere. Default false on open: one visible editor, shown without taking the focus. On restart it applies only to an editor a server opened. The default there is how the replaced editor was started, read from its process. One whose start cannot be read is left running and refused until hidden is passed. One opened by hand comes back as it was, so hidden is refused there."
|
|
19472
19510
|
},
|
|
19473
19511
|
silent: {
|
|
19474
19512
|
type: "boolean",
|
|
19475
19513
|
ops: ["open", "restart"],
|
|
19476
|
-
description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true on open, and on restart how the editor being replaced was started: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is
|
|
19514
|
+
description: "Open the editor with no sound out of the machine, started with --audio-driver Dummy, so a tool script, a preview or a scene playing in its viewport is heard by nobody. Default true on open, and on restart how the editor being replaced was started: pass false to hear it. The answer's arguments are what the editor was started with. The editor hands no audio driver to a game it plays, so editor_run's own silent decides that game's sound. A hidden editor is silent whatever silent says. A headless editor has no sound to turn off, so its arguments show no --audio-driver. On restart only for an editor a server opened. One opened by hand comes back as it was."
|
|
19477
19515
|
}
|
|
19478
19516
|
},
|
|
19479
19517
|
requires: [],
|
|
@@ -19714,7 +19752,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
19714
19752
|
title: "Editor status",
|
|
19715
19753
|
effect: "read",
|
|
19716
19754
|
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and which games with the runtime addon are reachable, of this project and of every other on the machine. Read connected with mayYetConnect: the editor dials this server and backs off between tries, so for the first half-minute after a server starts false means not reached yet as often as no editor. mayYetConnect true is worth waiting out, false is final, and editorCannotConnect, restartInterrupted, restartUnderway, closedHere and editorsNotYetConnected say why. An editor that has connected and not yet greeted reports nothing about itself, and every other call made in that window waits up to ten seconds for it to settle. playingInEditor null means unanswered rather than nothing playing: processActive and runtimes do not depend on the editor answering. game.run names the run editor_output and editor_run stop answer for, and stays named after it ends. addonIsStale, on a connected editor and on each running game (absent with neither), says it runs addon code other than what this server ships, and staleNote says which half moves: the project, by a gdharness upgrade it spells out to run in a shell, the editor by a restart, or this server by a reconnect. A false editor.lspListening or editor.dapListening comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning, which editor_run waits out before starting a game. A game announced in a protocol this server does not speak is listed under unreadable, during an upgrade that moves the protocol, until the halves match.",
|
|
19717
|
-
answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves on this release's runtime addon, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed. It is null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. editor.openScenes lists the scenes open in the editor's tabs, leaving out a tab of a scene never saved, editor.editedScene the one it is showing (null when none is, or when that scene was never saved), and editor.unsavedScenes those with changes not yet written to disk, where an empty entry is a scene never saved. All three come with scan and are absent when it is null. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout. For the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself. A game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say). addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out. False is an editor that is not there. False beside editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open. Its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for. editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on. When that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds. bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it. startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false. One started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening. When it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs headless with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
|
|
19755
|
+
answer: "projectIs, when present, is the gdharness version of the addons in the project, which differs from this server's (project_info says which of the two to move), and configIs, when present, the version the project's MCP config starts, which differs from this server's, so the next connection runs that one. server names this server: its pid, its version, and lifecycleLog, the file every gdharness server of the user appends a line to when it starts, when its client first names itself, when it replaces another server of its project, and when it ends, with why it ended (the client closing its end as stdin:end, a signal by name, superseded with the successor's pid, an uncaught exception or rejection with its stack, or the exit code) and the calls it was answering then. The parent's command line follows on its own line a few seconds after the start. A server killed without warning writes no ended line, and on Windows every kill is that, so a started line with no ended line after it is a server something killed. " + "runtimeConnected counts the games of the project this server serves on this release's runtime addon, and runtimes lists every game on the machine with ofThisProject saying which is which, since a game of another project is reached only by naming its projectPath, and userDir, the directory its user:// is, where its saves and settings are read. playingInEditor is null when the editor was not asked or did not answer, which is not the same as a no: with no editor connected there is nobody to ask, and an editor that does not serve the question is given a moment and then left. A game can be playing while this is null, so read it as unanswered rather than as nothing playing, and use processActive and runtimes, which come from elsewhere and do not depend on the editor answering. Its play says which play the editor is on: play.play counts the plays that editor has started since it opened, whoever started them, and play.editorPid is the editor, so two reads with different pairs are two plays even when both say playing. game.run names the run processActive is about, the one editor_output and editor_run stop answer for: its pid, its project, and adopted true when this server read it back from another server rather than starting it. It stays named after the run ends, beside processActive false, so editor_output can still read what that run printed. It is null when there is no run at all. startedAt says when it began, and endedAt when it ended, wherever something saw the end: a run adopted from a note another server wrote can have ended hours before this server started. editor.lspListening and editor.dapListening say whether anything accepts connections on the ports the editor names, lspPort and dapPort: Godot binds each once, as the editor starts, and reports a failure only in the log panel of the editor itself, so a false comes with serversNote: editor_launch rebind binds the debug adapter again in place, and editor_launch restart the language server. editor.scan says whether the editor is scanning or importing and how many milliseconds ago its last scan finished (finishedMsAgo), or is null when it did not answer: the editor rewrites the class cache just after a scan, and editor_run waits that out before starting a game, saying so under waitedForEditorScanMs. editor.openScenes lists the scenes open in the editor's tabs, leaving out a tab of a scene never saved, editor.editedScene the one it is showing (null when none is, or when that scene was never saved), and editor.unsavedScenes those with changes not yet written to disk, where an empty entry is a scene never saved. All three come with scan and are absent when it is null. Each game under runtimes is pinged with a short wait, so one held at a breakpoint is listed as unreachable rather than making this call wait out the runtime timeout. For the run this server is reporting on, heldAt says whether that is what it is, with the same three answers editor_output gives. A game the editor played announces the editor under editorPid, and so does whatever that game started for itself. A game some other server started announces none, which is how the two are told apart when both are running from one project. Each game also says which runtime addon it loaded, under addonVersion (null when it cannot say). addonIsStale is true when that code is not what this server ships, and staleNote says whether the project needs the gdharness upgrade command, spelled out to run in a shell, the game a restart, or this server a reconnect. A second checkout of a project, such as a git worktree, carries addons of its own and is where this shows. A runtime_* call to such a game is refused with the staleNote and not sent. When connected is false, mayYetConnect says whether that is final: the editor dials this server rather than the other way round and backs off between tries, so for the first half-minute after a server starts, false means \"not reached yet\" as often as it means \"no editor\". True is worth waiting out. False is an editor that is not there. False beside editorCannotConnect is a project without the gdharness editor addon installed or enabled, where no editor can ever dial in, however young the server or whichever editor is open. Its remedies say how to install or enable it. False beside restartInterrupted is one that an editor_launch restart on a previous server asked to quit, where the launch half never ran because that server was ended in between, usually by the reconnect the restart was taken for. editor_launch open finishes it. True beside restartUnderway is a restart still in progress, its server alive and within the wait for the old editor to go: the editor is coming, and opening another would put two on one project. listeningSince is when this server took the bridge port, which is what that window is measured from and what a caller wanting its own window should read. An editor this server opened counts for as long as its process lives, however far past that window the import runs, and awaitingLaunchedEditor names its pid so the two reasons for true are told apart. Any editor of this project running and not yet connected, whoever opened it, makes it true as well and is named under editorsNotYetConnected with its pid and start time, so a restart a reconnect interrupted after its launch is not read as nothing being there. settingsDropped appears here once when the editor this server opened saved project.godot on its way in and took a key out of it, which is the same reading editor_launch restart makes in its own answer. A game that has announced itself in a protocol this server does not speak is listed under unreadable rather than left out, with the protocol it speaks and whether the server or the addon is the older half. This happens during an upgrade that moves the runtime protocol, which most do not: installing replaces the addon on disk as soon as the pin moves, while a server that is already running stays on the version it started with, so the two speak different protocols until the server is reconnected. Those games are real and running, but no tool here can reach them until the halves match, so runtimes stays empty and runtimeConnected stays false. An upgrade that leaves the protocol alone opens no such window and this list stays empty through it. addonIsStale is decided by the code of the editor addons rather than their version, so an editor started before an upgrade that left that code alone reports the older addonVersion, is not stale, and addonNote says so: it needs no restart. A stale editor is asked nothing but to quit: every other call to it is refused with staleNote, which names editor_launch restart, the upgrade command or a reconnect. port is the one the editor bridge is on. When that is not the one this server was configured with, portWanted names the configured one and portNote says why it moved and, when it was the server this one replaced, which pid kept it. A server replacing another of its project that holds the configured port waits for it to stand down and takes the port over, which takes a few seconds. bridgeHandover says so while it waits, and it moves only if that server has not let go within the wait. An editor that has connected and not yet greeted, which lasts a frame and longer while it imports, reports nothing about itself: greeting says so, addonIsStale is absent rather than guessed, and a restart asked then waits for the greeting to learn who opened it. startedByAnEditor is an editor another editor started, by Godot's own restart or the project manager, which has none of the arguments a server gave the one before it. startedByAnEditorNote says what that costs. An engine run that loads the editor addon to do one job and quit, such as a gate's godot --headless --import, an export or a script, is not taken as the editor: a process that greets without a server's mark has its command line read first, and from the socket opening until its greeting and that read are done, identifying is true and connected false. One started with such an option is turned away, and turnedAway names the last one, its pid and the option. Every other call made while an editor is arriving, before its greeting or while it is looked up, waits up to ten seconds for it to settle rather than acting on connected as it reads in that window. serverVersion is the gdharness version of this server. bridgeAvailable says the editor bridge is listening. When it failed to start, startupError gives the error, and retryingBridge is true while this server tries again. announcedAt is the file where this server told editors of its project which port to dial. connectedAt is when the editor connected, and lastPongAt when it last answered this server's keep-alive ping. addonDigest is a hash of the editor addon's code, which addonIsStale compares with what this server ships. debugPort is the port of the editor's remote debugger, the one games it plays connect to. openedByAServer says a gdharness server started this editor, syncsBreakpoints that it keeps its breakpoints when a debug session opens, which Godot otherwise clears, and hidden that it runs with no window anybody can see. pendingRequests counts the requests sent to the editor and not yet answered, and queuedResources the files with writes waiting behind another write to the same file.",
|
|
19718
19756
|
parameters: {},
|
|
19719
19757
|
requires: []
|
|
19720
19758
|
},
|
|
@@ -19744,6 +19782,41 @@ var init_tool_definitions = __esm(() => {
|
|
|
19744
19782
|
},
|
|
19745
19783
|
requires: ["projectPath"]
|
|
19746
19784
|
},
|
|
19785
|
+
{
|
|
19786
|
+
name: "editor_capture",
|
|
19787
|
+
title: "Capture the editor's view of a scene",
|
|
19788
|
+
effect: "read",
|
|
19789
|
+
description: "A picture of a scene as the editor's 2D or 3D view draws it, including what its @tool scripts draw under Engine.is_editor_hint(). No game and no headless engine runs that branch, so runtime_capture cannot show it. The editor opens the scene, shows its 2D or 3D view for a few frames and captures it. It then returns to the scene and view it showed before, and closes the scene again if it was not open. The view is the editor's own, at its zoom and scroll for that scene, and in 3D with its grid and gizmos. A scene open with unsaved changes is captured as the editor holds it. Other calls to the editor wait for the capture. Needs a connected editor that draws: a visible one, or a hidden one on Windows. A hidden editor on Linux or macOS is headless and is refused.",
|
|
19790
|
+
answer: "The picture, with its size in pixels and which view drew it. When width or height scaled the picture, it also gives the size the view drew at. A scene with unsaved changes in the editor says so.",
|
|
19791
|
+
parameters: {
|
|
19792
|
+
projectPath: PROJECT_PATH,
|
|
19793
|
+
scenePath: SCENE_PATH,
|
|
19794
|
+
view: {
|
|
19795
|
+
type: "string",
|
|
19796
|
+
enum: ["2d", "3d"],
|
|
19797
|
+
description: "Which of the editor's views draws the scene. Default 3d for a scene whose root is a Node3D, and 2d otherwise."
|
|
19798
|
+
},
|
|
19799
|
+
width: {
|
|
19800
|
+
type: "integer",
|
|
19801
|
+
minimum: 1,
|
|
19802
|
+
description: "Scale the picture to this width. Given alone, the height keeps the picture's proportions."
|
|
19803
|
+
},
|
|
19804
|
+
height: {
|
|
19805
|
+
type: "integer",
|
|
19806
|
+
minimum: 1,
|
|
19807
|
+
description: "Scale the picture to this height. Given alone, the width keeps the picture's proportions."
|
|
19808
|
+
},
|
|
19809
|
+
outputPath: {
|
|
19810
|
+
type: "string",
|
|
19811
|
+
description: "Also save the picture as a lossless PNG at this absolute path. The answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
19812
|
+
},
|
|
19813
|
+
inline: {
|
|
19814
|
+
type: "boolean",
|
|
19815
|
+
description: "Send the picture back in the answer. Default true, as a JPEG when that is smaller than the PNG and the picture has no transparency. false sends none and needs outputPath."
|
|
19816
|
+
}
|
|
19817
|
+
},
|
|
19818
|
+
requires: ["projectPath", "scenePath"]
|
|
19819
|
+
},
|
|
19747
19820
|
{
|
|
19748
19821
|
name: "editor_classes",
|
|
19749
19822
|
title: "Engine classes",
|
|
@@ -51071,9 +51144,15 @@ function selectTests(projectPath, asked, given) {
|
|
|
51071
51144
|
return { ok: false, reason: `${said}: none of them declares a func test_ at the top level.` };
|
|
51072
51145
|
}
|
|
51073
51146
|
const written = declared.map((one) => `${nameOf(one.suite)}:${one.test}`);
|
|
51074
|
-
const
|
|
51075
|
-
|
|
51076
|
-
|
|
51147
|
+
const bare = (name) => name.replace(/^test_/, "");
|
|
51148
|
+
const inSuite = suiteNamed === null ? declared : declared.filter((one) => one.suite === suitePath || nameOf(one.suite) === suiteNamed);
|
|
51149
|
+
const spell = (one) => suiteNamed === null ? one.test : `${nameOf(one.suite)}:${one.test}`;
|
|
51150
|
+
const close = [
|
|
51151
|
+
...new Set([
|
|
51152
|
+
...nearest(bare(test), [...new Set(inSuite.map((one) => bare(one.test)))]).flatMap((name) => inSuite.filter((one) => bare(one.test) === name).map(spell)),
|
|
51153
|
+
...suiteNamed === null ? [] : declared.filter((one) => one.test === test).map(spell)
|
|
51154
|
+
])
|
|
51155
|
+
];
|
|
51077
51156
|
return {
|
|
51078
51157
|
ok: false,
|
|
51079
51158
|
reason: close.length > 0 ? `${said}. The nearest ${close.length === 1 ? "is" : "are"} ${close.join(", ")}.` : `${said}. The cases there are ${written.slice(0, 12).join(", ")}${written.length > 12 ? `, and ${written.length - 12} more` : ""}.`
|
|
@@ -51096,27 +51175,109 @@ var init_test_selection = __esm(() => {
|
|
|
51096
51175
|
CASE_AFTER_SCRIPT = /^(.*\.gd):(\w+)$/;
|
|
51097
51176
|
});
|
|
51098
51177
|
|
|
51178
|
+
// src/write-turns.ts
|
|
51179
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
51180
|
+
import { closeSync as closeSync6, mkdirSync as mkdirSync23, openSync as openSync7, readFileSync as readFileSync41, rmSync as rmSync21, statSync as statSync20, writeSync as writeSync5 } from "node:fs";
|
|
51181
|
+
import { join as join45, relative as relative10 } from "node:path";
|
|
51182
|
+
import process18 from "node:process";
|
|
51183
|
+
import { setTimeout as delay8 } from "node:timers/promises";
|
|
51184
|
+
function writeLockOf(projectPath, file) {
|
|
51185
|
+
const named = relative10(projectPath, file).split("\\").join("/");
|
|
51186
|
+
const key = process18.platform === "win32" ? named.toLowerCase() : named;
|
|
51187
|
+
const hash = createHash8("sha256").update(key).digest("hex").slice(0, 16);
|
|
51188
|
+
return join45(projectPath, ".godot", WRITES_FOLDER, `${hash}.lock`);
|
|
51189
|
+
}
|
|
51190
|
+
async function inTurnFor(projectPath, file, write) {
|
|
51191
|
+
const lock = writeLockOf(projectPath, file);
|
|
51192
|
+
mkdirSync23(join45(projectPath, ".godot", WRITES_FOLDER), { recursive: true });
|
|
51193
|
+
const holder = JSON.stringify({ pid: process18.pid, started: await ownStart(), file });
|
|
51194
|
+
const starts = new Map;
|
|
51195
|
+
while (!made(lock, holder) && !await takenOver(lock, holder, starts)) {
|
|
51196
|
+
await delay8(TURN_LOOK_MS);
|
|
51197
|
+
}
|
|
51198
|
+
try {
|
|
51199
|
+
return await write();
|
|
51200
|
+
} finally {
|
|
51201
|
+
removeWhole(lock);
|
|
51202
|
+
}
|
|
51203
|
+
}
|
|
51204
|
+
function made(path, content) {
|
|
51205
|
+
let file;
|
|
51206
|
+
try {
|
|
51207
|
+
file = openSync7(path, "wx");
|
|
51208
|
+
} catch {
|
|
51209
|
+
return false;
|
|
51210
|
+
}
|
|
51211
|
+
try {
|
|
51212
|
+
writeSync5(file, content);
|
|
51213
|
+
} finally {
|
|
51214
|
+
closeSync6(file);
|
|
51215
|
+
}
|
|
51216
|
+
return true;
|
|
51217
|
+
}
|
|
51218
|
+
async function takenOver(lock, holder, starts) {
|
|
51219
|
+
if (!await gone2(lock, starts)) {
|
|
51220
|
+
return false;
|
|
51221
|
+
}
|
|
51222
|
+
const steal = `${lock}.steal`;
|
|
51223
|
+
if (!made(steal, holder)) {
|
|
51224
|
+
try {
|
|
51225
|
+
if (Date.now() - statSync20(steal).mtimeMs > UNWRITTEN_MS) {
|
|
51226
|
+
rmSync21(steal, { force: true });
|
|
51227
|
+
}
|
|
51228
|
+
} catch {}
|
|
51229
|
+
return false;
|
|
51230
|
+
}
|
|
51231
|
+
try {
|
|
51232
|
+
return await gone2(lock, starts) && removeWhole(lock) && made(lock, holder);
|
|
51233
|
+
} finally {
|
|
51234
|
+
rmSync21(steal, { force: true });
|
|
51235
|
+
}
|
|
51236
|
+
}
|
|
51237
|
+
async function gone2(lock, starts) {
|
|
51238
|
+
let content;
|
|
51239
|
+
let writtenAt;
|
|
51240
|
+
try {
|
|
51241
|
+
content = readFileSync41(lock, "utf8");
|
|
51242
|
+
writtenAt = statSync20(lock).mtimeMs;
|
|
51243
|
+
} catch {
|
|
51244
|
+
return false;
|
|
51245
|
+
}
|
|
51246
|
+
const pid = holderOf(content)?.pid;
|
|
51247
|
+
if (pid !== undefined && !starts.has(pid) && alive(pid)) {
|
|
51248
|
+
starts.set(pid, (await startTimesOf([pid])).get(pid));
|
|
51249
|
+
}
|
|
51250
|
+
return lockIsFree(content, writtenAt, Date.now(), alive, (asked) => starts.get(asked));
|
|
51251
|
+
}
|
|
51252
|
+
var WRITES_FOLDER = "gdharness-writes", TURN_LOOK_MS = 100;
|
|
51253
|
+
var init_write_turns = __esm(() => {
|
|
51254
|
+
init_alive();
|
|
51255
|
+
init_engine_slots();
|
|
51256
|
+
init_process_children();
|
|
51257
|
+
init_whole_file();
|
|
51258
|
+
});
|
|
51259
|
+
|
|
51099
51260
|
// src/server.ts
|
|
51100
51261
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
51101
51262
|
import {
|
|
51102
51263
|
appendFileSync as appendFileSync2,
|
|
51103
|
-
closeSync as
|
|
51264
|
+
closeSync as closeSync7,
|
|
51104
51265
|
constants,
|
|
51105
51266
|
copyFileSync as copyFileSync2,
|
|
51106
51267
|
existsSync as existsSync36,
|
|
51107
51268
|
fstatSync as fstatSync3,
|
|
51108
|
-
mkdirSync as
|
|
51109
|
-
openSync as
|
|
51269
|
+
mkdirSync as mkdirSync24,
|
|
51270
|
+
openSync as openSync8,
|
|
51110
51271
|
readdirSync as readdirSync21,
|
|
51111
|
-
readFileSync as
|
|
51272
|
+
readFileSync as readFileSync42,
|
|
51112
51273
|
readSync as readSync4,
|
|
51113
51274
|
realpathSync as realpathSync7,
|
|
51114
51275
|
rmdirSync as rmdirSync2,
|
|
51115
|
-
statSync as
|
|
51276
|
+
statSync as statSync21,
|
|
51116
51277
|
writeFileSync as writeFileSync24
|
|
51117
51278
|
} from "node:fs";
|
|
51118
|
-
import { basename as basename14, dirname as dirname25, extname as extname6, isAbsolute as isAbsolute9, join as
|
|
51119
|
-
import { setTimeout as
|
|
51279
|
+
import { basename as basename14, dirname as dirname25, extname as extname6, isAbsolute as isAbsolute9, join as join46, normalize as normalize2, relative as relative11 } from "node:path";
|
|
51280
|
+
import { setTimeout as delay9 } from "node:timers/promises";
|
|
51120
51281
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
51121
51282
|
function editorRestartTimeoutMs() {
|
|
51122
51283
|
return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
|
|
@@ -51129,9 +51290,9 @@ function greetingWaitMs() {
|
|
|
51129
51290
|
return waitFromEnv("GDHARNESS_GREETING_WAIT_MS", 1e4);
|
|
51130
51291
|
}
|
|
51131
51292
|
function pictureToSend(pngPath, jpegPath) {
|
|
51132
|
-
const png =
|
|
51293
|
+
const png = readFileSync42(pngPath);
|
|
51133
51294
|
const beside = pngPath.replace(/\.png$/, ".jpg");
|
|
51134
|
-
const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync36(beside) ?
|
|
51295
|
+
const jpeg = jpegPath !== undefined && normalize2(jpegPath) === normalize2(beside) && existsSync36(beside) ? readFileSync42(beside) : null;
|
|
51135
51296
|
const sent = jpeg !== null && jpeg.length < png.length ? jpeg : png;
|
|
51136
51297
|
return {
|
|
51137
51298
|
data: sent.toString("base64"),
|
|
@@ -51175,7 +51336,7 @@ function writtenFileRefusal(path, rule) {
|
|
|
51175
51336
|
if (!existsSync36(directory)) {
|
|
51176
51337
|
return `${argument}'s directory ${dirname25(path)} does not exist. ${whatOpening} creates no directories.`;
|
|
51177
51338
|
}
|
|
51178
|
-
const where =
|
|
51339
|
+
const where = join46(directory, basename14(path));
|
|
51179
51340
|
const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realAsFarAsItExists(project), where));
|
|
51180
51341
|
if (inside !== undefined) {
|
|
51181
51342
|
return `${argument} ${path} is inside the project at ${inside}. ${whatOpening} is saved outside it, so it never becomes part of the game.`;
|
|
@@ -51271,7 +51432,7 @@ function keptGameRequest(tool, op, args) {
|
|
|
51271
51432
|
}
|
|
51272
51433
|
}
|
|
51273
51434
|
function projectSpelling(projectPath, absolutePath) {
|
|
51274
|
-
return `res://${
|
|
51435
|
+
return `res://${relative11(realAsFarAsItExists(projectPath), realAsFarAsItExists(absolutePath)).replaceAll("\\", "/")}`;
|
|
51275
51436
|
}
|
|
51276
51437
|
function whichGame(args) {
|
|
51277
51438
|
return { projectPath: args["projectPath"], pid: args["pid"] };
|
|
@@ -51402,7 +51563,7 @@ async function untilGone(pids, withinMs = stopWaitMs()) {
|
|
|
51402
51563
|
const gone = () => pids.every((pid) => !alive(pid));
|
|
51403
51564
|
const deadline = Date.now() + withinMs;
|
|
51404
51565
|
while (!gone() && Date.now() < deadline) {
|
|
51405
|
-
await
|
|
51566
|
+
await delay9(50);
|
|
51406
51567
|
}
|
|
51407
51568
|
return gone();
|
|
51408
51569
|
}
|
|
@@ -51606,7 +51767,7 @@ function endedToStartThis(ended) {
|
|
|
51606
51767
|
return ` ${which} was ended to start this one. Its output is no longer what editor_output answers about.`;
|
|
51607
51768
|
}
|
|
51608
51769
|
function lineNothingStopsOn(absolutePath, relativePath, line) {
|
|
51609
|
-
const text =
|
|
51770
|
+
const text = readFileSync42(absolutePath, "utf8");
|
|
51610
51771
|
const lines = text === "" ? 0 : text.split(`
|
|
51611
51772
|
`).length - (text.endsWith(`
|
|
51612
51773
|
`) ? 1 : 0);
|
|
@@ -51814,7 +51975,7 @@ function notRunNote(notRun, failFast) {
|
|
|
51814
51975
|
function presetsIn(projectPath) {
|
|
51815
51976
|
let text;
|
|
51816
51977
|
try {
|
|
51817
|
-
text =
|
|
51978
|
+
text = readFileSync42(join46(projectPath, "export_presets.cfg"), "utf8");
|
|
51818
51979
|
} catch {
|
|
51819
51980
|
return null;
|
|
51820
51981
|
}
|
|
@@ -51921,7 +52082,7 @@ function filesIn(folder) {
|
|
|
51921
52082
|
try {
|
|
51922
52083
|
for (const entry of readdirSync21(folder, { withFileTypes: true })) {
|
|
51923
52084
|
if (entry.isFile()) {
|
|
51924
|
-
found.set(entry.name,
|
|
52085
|
+
found.set(entry.name, statSync21(join46(folder, entry.name)).mtimeMs);
|
|
51925
52086
|
}
|
|
51926
52087
|
}
|
|
51927
52088
|
} catch {}
|
|
@@ -51961,7 +52122,7 @@ function unsavedInEditorNote(scripts) {
|
|
|
51961
52122
|
return `${listed} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
|
|
51962
52123
|
}
|
|
51963
52124
|
function editorAddonMissing(projectPath) {
|
|
51964
|
-
if (!existsSync36(
|
|
52125
|
+
if (!existsSync36(join46(projectPath, "project.godot"))) {
|
|
51965
52126
|
return null;
|
|
51966
52127
|
}
|
|
51967
52128
|
const report = inspectProject(projectPath);
|
|
@@ -51989,8 +52150,8 @@ function rewriteBusNamed(projectPath, from, to) {
|
|
|
51989
52150
|
const giving = [`bus = &"${quoted(from)}"`, `bus = "${quoted(from)}"`];
|
|
51990
52151
|
const rewritten = [];
|
|
51991
52152
|
for (const file of projectFiles(projectPath, [".tscn", ".tres"])) {
|
|
51992
|
-
const absolute =
|
|
51993
|
-
const lines =
|
|
52153
|
+
const absolute = join46(projectPath, file);
|
|
52154
|
+
const lines = readFileSync42(absolute, "utf8").split(`
|
|
51994
52155
|
`);
|
|
51995
52156
|
const at = [];
|
|
51996
52157
|
const after = lines.map((line, index) => {
|
|
@@ -52009,7 +52170,7 @@ function rewriteBusNamed(projectPath, from, to) {
|
|
|
52009
52170
|
}
|
|
52010
52171
|
const namedInScripts = [];
|
|
52011
52172
|
for (const file of projectFiles(projectPath, [".gd"])) {
|
|
52012
|
-
|
|
52173
|
+
readFileSync42(join46(projectPath, file), "utf8").split(`
|
|
52013
52174
|
`).forEach((line, index) => {
|
|
52014
52175
|
if (line.includes(`"${from}"`)) {
|
|
52015
52176
|
namedInScripts.push({ file: `res://${file}`, line: index + 1, text: line.trim() });
|
|
@@ -52300,7 +52461,7 @@ function profileFrameFunctions(projectPath) {
|
|
|
52300
52461
|
let projectGodot = null;
|
|
52301
52462
|
if (projectPath !== null) {
|
|
52302
52463
|
try {
|
|
52303
|
-
projectGodot =
|
|
52464
|
+
projectGodot = readFileSync42(join46(projectPath, "project.godot"), "utf8");
|
|
52304
52465
|
} catch {}
|
|
52305
52466
|
}
|
|
52306
52467
|
return frameFunctionsFor(queuedMessagesOf(projectGodot));
|
|
@@ -52324,7 +52485,7 @@ function projectSourceOf(projectPath) {
|
|
|
52324
52485
|
let source = null;
|
|
52325
52486
|
if (inside.ok) {
|
|
52326
52487
|
try {
|
|
52327
|
-
source =
|
|
52488
|
+
source = readFileSync42(inside.absolutePath, "utf8");
|
|
52328
52489
|
} catch {}
|
|
52329
52490
|
}
|
|
52330
52491
|
read.set(script, source);
|
|
@@ -52496,7 +52657,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
|
|
|
52496
52657
|
}
|
|
52497
52658
|
function gdUnitFailsOnScriptErrors(projectPath) {
|
|
52498
52659
|
try {
|
|
52499
|
-
return settingKeys(
|
|
52660
|
+
return settingKeys(readFileSync42(join46(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
|
|
52500
52661
|
} catch {
|
|
52501
52662
|
return true;
|
|
52502
52663
|
}
|
|
@@ -52511,7 +52672,7 @@ function uncaughtNote(uncaught) {
|
|
|
52511
52672
|
}
|
|
52512
52673
|
function elsewhereIn(projectPath, asked) {
|
|
52513
52674
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
52514
|
-
if (wanted === "" || existsSync36(
|
|
52675
|
+
if (wanted === "" || existsSync36(join46(projectPath, wanted))) {
|
|
52515
52676
|
return "";
|
|
52516
52677
|
}
|
|
52517
52678
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -52534,7 +52695,7 @@ function windowNoteFor(needing, sharing) {
|
|
|
52534
52695
|
return `${named(needing)} ${one ? "needs" : "need"} a window, as gdharness test reads ${one ? "it" : "them"}, so the run had one: the engine ran without --headless, at the size project.godot gives its viewport, with no sound${process.platform === "win32" ? ", on a desktop of its own, where its window neither shows nor takes the keyboard" : ""}. ${why}${shared}`;
|
|
52535
52696
|
}
|
|
52536
52697
|
function testPathsIn(projectPath, given, argument = "path") {
|
|
52537
|
-
const defaults = TEST_DIRECTORIES.filter((directory) => existsSync36(
|
|
52698
|
+
const defaults = TEST_DIRECTORIES.filter((directory) => existsSync36(join46(projectPath, directory)));
|
|
52538
52699
|
const entries = given === undefined || given === null || given === "" ? defaults.length > 0 ? defaults : TEST_DIRECTORIES : given;
|
|
52539
52700
|
const listed = Array.isArray(entries) ? entries : [entries];
|
|
52540
52701
|
if (listed.length === 0) {
|
|
@@ -52637,17 +52798,17 @@ function asWarning(raised) {
|
|
|
52637
52798
|
}
|
|
52638
52799
|
function isDirectory(path) {
|
|
52639
52800
|
try {
|
|
52640
|
-
return
|
|
52801
|
+
return statSync21(path).isDirectory();
|
|
52641
52802
|
} catch {
|
|
52642
52803
|
return false;
|
|
52643
52804
|
}
|
|
52644
52805
|
}
|
|
52645
52806
|
function mainSceneOf(projectFile) {
|
|
52646
|
-
const scene = parseProjectGodot(
|
|
52807
|
+
const scene = parseProjectGodot(readFileSync42(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
52647
52808
|
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
52648
52809
|
}
|
|
52649
52810
|
function editorPlaysHeadless(projectFile) {
|
|
52650
|
-
const runArgs = parseProjectGodot(
|
|
52811
|
+
const runArgs = parseProjectGodot(readFileSync42(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
52651
52812
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
52652
52813
|
}
|
|
52653
52814
|
function ownArguments(params) {
|
|
@@ -52679,7 +52840,7 @@ function unheldNote(changed) {
|
|
|
52679
52840
|
class GodotServer {
|
|
52680
52841
|
stdio = null;
|
|
52681
52842
|
locator = new GodotLocator;
|
|
52682
|
-
operationsScript =
|
|
52843
|
+
operationsScript = join46(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
52683
52844
|
exportOptionsRead = new Map;
|
|
52684
52845
|
heldWhilePending = new Set;
|
|
52685
52846
|
evaluatedWhilePending = new Set;
|
|
@@ -52838,7 +52999,7 @@ class GodotServer {
|
|
|
52838
52999
|
taken = true;
|
|
52839
53000
|
this.logDebug(`Took port ${wanted} over from pid ${announced.pid}`);
|
|
52840
53001
|
} catch {
|
|
52841
|
-
await
|
|
53002
|
+
await delay9(HANDOVER_POLL_MS);
|
|
52842
53003
|
}
|
|
52843
53004
|
}
|
|
52844
53005
|
this.notHandedOverBy = taken ? null : announced.pid;
|
|
@@ -52969,7 +53130,7 @@ class GodotServer {
|
|
|
52969
53130
|
}
|
|
52970
53131
|
noteTheStart() {
|
|
52971
53132
|
const cwd = process.cwd();
|
|
52972
|
-
const mcpJson =
|
|
53133
|
+
const mcpJson = join46(cwd, ".mcp.json");
|
|
52973
53134
|
const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
|
|
52974
53135
|
const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
|
|
52975
53136
|
writeLifecycle("started", {
|
|
@@ -53118,7 +53279,7 @@ class GodotServer {
|
|
|
53118
53279
|
return checked.response;
|
|
53119
53280
|
}
|
|
53120
53281
|
const project = args["projectPath"];
|
|
53121
|
-
if (typeof project === "string" && existsSync36(
|
|
53282
|
+
if (typeof project === "string" && existsSync36(join46(project, "project.godot"))) {
|
|
53122
53283
|
this.lastProjectPath = project;
|
|
53123
53284
|
}
|
|
53124
53285
|
this.updates.refresh();
|
|
@@ -53161,7 +53322,7 @@ class GodotServer {
|
|
|
53161
53322
|
let notSaved;
|
|
53162
53323
|
try {
|
|
53163
53324
|
savedAt = answer.content.map((item, index) => {
|
|
53164
|
-
const file =
|
|
53325
|
+
const file = join46(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
|
|
53165
53326
|
writeFileSync24(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
|
|
53166
53327
|
return file;
|
|
53167
53328
|
});
|
|
@@ -53625,6 +53786,8 @@ class GodotServer {
|
|
|
53625
53786
|
return await this.handleEditorStatus();
|
|
53626
53787
|
case "editor_rescan":
|
|
53627
53788
|
return await this.handleRescanFilesystem(args);
|
|
53789
|
+
case "editor_capture":
|
|
53790
|
+
return await this.handleEditorCapture(args);
|
|
53628
53791
|
case "runtime_inspect": {
|
|
53629
53792
|
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
53630
53793
|
switch (op) {
|
|
@@ -53858,7 +54021,7 @@ class GodotServer {
|
|
|
53858
54021
|
if (path === undefined) {
|
|
53859
54022
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
53860
54023
|
}
|
|
53861
|
-
const file =
|
|
54024
|
+
const file = join46(path, "project.godot");
|
|
53862
54025
|
if (!existsSync36(file)) {
|
|
53863
54026
|
return {
|
|
53864
54027
|
ok: false,
|
|
@@ -54608,7 +54771,7 @@ class GodotServer {
|
|
|
54608
54771
|
if (!engine.ok) {
|
|
54609
54772
|
return engine.response;
|
|
54610
54773
|
}
|
|
54611
|
-
const application = parseProjectGodot(
|
|
54774
|
+
const application = parseProjectGodot(readFileSync42(project.value.file, "utf8"))["application"] ?? {};
|
|
54612
54775
|
const name = application["config/name"];
|
|
54613
54776
|
const mainScene = application["run/main_scene"];
|
|
54614
54777
|
const info = {
|
|
@@ -54762,7 +54925,7 @@ class GodotServer {
|
|
|
54762
54925
|
const platform = presetPlatform(project.value.path, preset);
|
|
54763
54926
|
if (platform !== null) {
|
|
54764
54927
|
const known = await this.exportOptions(platform, {
|
|
54765
|
-
presetsFile:
|
|
54928
|
+
presetsFile: join46(project.value.path, "export_presets.cfg"),
|
|
54766
54929
|
preset,
|
|
54767
54930
|
asked: options
|
|
54768
54931
|
});
|
|
@@ -54813,7 +54976,7 @@ ${platform}`;
|
|
|
54813
54976
|
}
|
|
54814
54977
|
const empty = scratchDirectory("export");
|
|
54815
54978
|
try {
|
|
54816
|
-
writeFileSync24(
|
|
54979
|
+
writeFileSync24(join46(empty, "project.godot"), `config_version=5
|
|
54817
54980
|
`);
|
|
54818
54981
|
const outcome = await runOperation({ godotPath: engine.value, script: this.operationsScript, debug: DEBUG_MODE, editor: true }, "export_options", { platform, ...standing }, empty);
|
|
54819
54982
|
const options = outcome.ok ? outcome.payload["options"] : undefined;
|
|
@@ -54852,7 +55015,7 @@ ${platform}`;
|
|
|
54852
55015
|
}
|
|
54853
55016
|
const folder = dirname25(output.absolutePath);
|
|
54854
55017
|
try {
|
|
54855
|
-
|
|
55018
|
+
mkdirSync24(folder, { recursive: true });
|
|
54856
55019
|
} catch (error) {
|
|
54857
55020
|
return this.createErrorResponse(`Cannot create ${dirname25(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
54858
55021
|
}
|
|
@@ -54870,7 +55033,7 @@ ${platform}`;
|
|
|
54870
55033
|
const exportArgs = [
|
|
54871
55034
|
"--headless",
|
|
54872
55035
|
"--log-file",
|
|
54873
|
-
|
|
55036
|
+
join46(exportLogs, "engine.log"),
|
|
54874
55037
|
"--path",
|
|
54875
55038
|
project.value.path,
|
|
54876
55039
|
pack ? "--export-pack" : debug ? "--export-debug" : "--export-release",
|
|
@@ -54878,7 +55041,7 @@ ${platform}`;
|
|
|
54878
55041
|
output.absolutePath
|
|
54879
55042
|
];
|
|
54880
55043
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
54881
|
-
const writtenAt = () => existsSync36(output.absolutePath) ?
|
|
55044
|
+
const writtenAt = () => existsSync36(output.absolutePath) ? statSync21(output.absolutePath).mtimeMs : null;
|
|
54882
55045
|
const caches = editorCacheDirectory(engine.value);
|
|
54883
55046
|
const exportOnce = async () => {
|
|
54884
55047
|
const before = writtenAt();
|
|
@@ -54908,7 +55071,7 @@ ${platform}`;
|
|
|
54908
55071
|
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
54909
55072
|
}
|
|
54910
55073
|
const beside = writtenBeside(listedBefore, filesIn(folder), basename14(output.absolutePath), dirname25(output.relativePath).replaceAll("\\", "/"));
|
|
54911
|
-
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug, ...pack ? { pack } : {} }, before, writtenAt(), beside, docCacheNote, existsSync36(output.absolutePath) ?
|
|
55074
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug, ...pack ? { pack } : {} }, before, writtenAt(), beside, docCacheNote, existsSync36(output.absolutePath) ? statSync21(output.absolutePath).size : null, importedFirstAnswer(imported));
|
|
54912
55075
|
}
|
|
54913
55076
|
reportTestProgress(log) {
|
|
54914
55077
|
const progress = callProgress();
|
|
@@ -54969,7 +55132,7 @@ ${platform}`;
|
|
|
54969
55132
|
if (movie !== null) {
|
|
54970
55133
|
return this.createErrorResponse(movie);
|
|
54971
55134
|
}
|
|
54972
|
-
const missing = paths.value.filter((path) => !existsSync36(
|
|
55135
|
+
const missing = paths.value.filter((path) => !existsSync36(join46(project.value.path, path.slice("res://".length))));
|
|
54973
55136
|
const scripts = new ProjectScripts(project.value.path);
|
|
54974
55137
|
const present = paths.value.filter((path) => !missing.includes(path));
|
|
54975
55138
|
const asked = scripts.searchedAsTold(present);
|
|
@@ -55061,10 +55224,10 @@ ${platform}`;
|
|
|
55061
55224
|
const ignored = readStringArray(args, "ignore") ?? [];
|
|
55062
55225
|
const declaredUnder = ignored.length === 0 ? [] : asked.flatMap((path) => path.endsWith(".gd") ? scripts.suite(path) ?? [] : scripts.suitesUnder([path]));
|
|
55063
55226
|
const userData = scratchDirectory("tests");
|
|
55064
|
-
const suitesFile =
|
|
55065
|
-
const skippingFile =
|
|
55066
|
-
const engineLog =
|
|
55067
|
-
const expectedFile =
|
|
55227
|
+
const suitesFile = join46(userData, "suites.txt");
|
|
55228
|
+
const skippingFile = join46(userData, "skipping.txt");
|
|
55229
|
+
const engineLog = join46(userData, "engine.log");
|
|
55230
|
+
const expectedFile = join46(userData, "expected.json");
|
|
55068
55231
|
writeFileSync24(suitesFile, `${(selection.value?.suites ?? asked).join(`
|
|
55069
55232
|
`)}
|
|
55070
55233
|
`);
|
|
@@ -55105,11 +55268,11 @@ ${platform}`;
|
|
|
55105
55268
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? TEST_TIMEOUT_MS;
|
|
55106
55269
|
this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
|
|
55107
55270
|
const helperFiles = {
|
|
55108
|
-
spec:
|
|
55109
|
-
pid:
|
|
55110
|
-
error:
|
|
55111
|
-
stop:
|
|
55112
|
-
step:
|
|
55271
|
+
spec: join46(userData, "spec"),
|
|
55272
|
+
pid: join46(userData, "pid"),
|
|
55273
|
+
error: join46(userData, "error"),
|
|
55274
|
+
stop: join46(userData, "stop"),
|
|
55275
|
+
step: join46(userData, "step")
|
|
55113
55276
|
};
|
|
55114
55277
|
const onDesktop = windowed && process.platform === "win32";
|
|
55115
55278
|
const run = await this.spawnGame(engine.value, cmdArgs, userDataIn(userData), onDesktop ? { directory: project.value.path, files: helperFiles } : undefined);
|
|
@@ -55163,7 +55326,7 @@ ${platform}`;
|
|
|
55163
55326
|
await run.settled;
|
|
55164
55327
|
const helperFailed = onDesktop ? helperFailure(helperFiles.error) ?? (existsSync36(helperFiles.pid) ? null : `it never started, and ${helperStep(helperFiles.step)}`) : null;
|
|
55165
55328
|
if (helperFailed !== null) {
|
|
55166
|
-
discard(
|
|
55329
|
+
discard(join46(project.value.path, ".godot", ...ours.split("/")), userData);
|
|
55167
55330
|
return this.createErrorResponse(`No tests ran: ${named(needing)} ${needing.length === 1 ? "needs" : "need"} a window, and the engine could not be started on a desktop of its own: ${helperFailed}`);
|
|
55168
55331
|
}
|
|
55169
55332
|
const endedAt = Date.now();
|
|
@@ -55174,16 +55337,16 @@ ${platform}`;
|
|
|
55174
55337
|
engine: endedAt - importedAt
|
|
55175
55338
|
};
|
|
55176
55339
|
const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
|
|
55177
|
-
const reportsDir =
|
|
55340
|
+
const reportsDir = join46(project.value.path, ".godot", ...ours.split("/"));
|
|
55178
55341
|
let report = null;
|
|
55179
55342
|
let reportProblem = null;
|
|
55180
|
-
const casePrinted = withNestedErrors(readRun(existsSync36(engineLog) ?
|
|
55343
|
+
const casePrinted = withNestedErrors(readRun(existsSync36(engineLog) ? readFileSync42(engineLog, "utf8") : ""), run.log.all);
|
|
55181
55344
|
const declaredErrors = readExpected(expectedFile);
|
|
55182
55345
|
try {
|
|
55183
55346
|
const written = existsSync36(reportsDir) && notCompiled === null ? readdirSync21(reportsDir).filter((name) => name.startsWith("report_")).toSorted((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
55184
55347
|
const newest = written.at(-1);
|
|
55185
55348
|
if (newest !== undefined) {
|
|
55186
|
-
report = parseJUnit(
|
|
55349
|
+
report = parseJUnit(readFileSync42(join46(reportsDir, newest, "results.xml"), "utf8"));
|
|
55187
55350
|
}
|
|
55188
55351
|
} catch (error) {
|
|
55189
55352
|
reportProblem = errorMessage(error);
|
|
@@ -55596,7 +55759,7 @@ ${error.text}`;
|
|
|
55596
55759
|
const scripts = projectFiles(projectPath, [".gd"]).map((script) => `res://${script}`);
|
|
55597
55760
|
const wanted = `res://${relativePath}`;
|
|
55598
55761
|
const folder = dirname25(relativePath.replaceAll("\\", "/"));
|
|
55599
|
-
const inFolder =
|
|
55762
|
+
const inFolder = join46(projectPath, folder);
|
|
55600
55763
|
const beside = existsSync36(inFolder) ? readdirSync21(inFolder, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => `res://${folder === "." ? "" : `${folder}/`}${entry.name}`) : [];
|
|
55601
55764
|
return this.createErrorResponse(withNearestFiles(`Script file does not exist: ${wanted}`, wanted, scripts, beside));
|
|
55602
55765
|
}
|
|
@@ -55729,7 +55892,7 @@ ${error.text}`;
|
|
|
55729
55892
|
return null;
|
|
55730
55893
|
}
|
|
55731
55894
|
try {
|
|
55732
|
-
return
|
|
55895
|
+
return readFileSync42(contained.absolutePath, "utf8");
|
|
55733
55896
|
} catch {
|
|
55734
55897
|
return null;
|
|
55735
55898
|
}
|
|
@@ -55823,7 +55986,7 @@ ${error.text}`;
|
|
|
55823
55986
|
return null;
|
|
55824
55987
|
}
|
|
55825
55988
|
try {
|
|
55826
|
-
return
|
|
55989
|
+
return readFileSync42(contained.absolutePath, "utf8");
|
|
55827
55990
|
} catch {
|
|
55828
55991
|
return null;
|
|
55829
55992
|
}
|
|
@@ -55997,7 +56160,7 @@ ${error.text}`;
|
|
|
55997
56160
|
...Array.isArray(stack) ? {
|
|
55998
56161
|
stack: stack.map((frame) => ({
|
|
55999
56162
|
...frame,
|
|
56000
|
-
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path:
|
|
56163
|
+
...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join46(projectPath, frame["script"].slice("res://".length)) } : {}
|
|
56001
56164
|
}))
|
|
56002
56165
|
} : {}
|
|
56003
56166
|
});
|
|
@@ -56121,7 +56284,7 @@ ${error.text}`;
|
|
|
56121
56284
|
try {
|
|
56122
56285
|
const askedAt = Date.now();
|
|
56123
56286
|
const asked = this.godotBridge.invokeTool("playing_status", {}, { counted: false });
|
|
56124
|
-
const answer = await Promise.race([asked,
|
|
56287
|
+
const answer = await Promise.race([asked, delay9(PLAYING_STATUS_MS).then(() => null)]);
|
|
56125
56288
|
if (answer === null) {
|
|
56126
56289
|
this.logDebug(`playing_status did not answer within ${PLAYING_STATUS_MS}ms`);
|
|
56127
56290
|
asked.then(() => {
|
|
@@ -56146,7 +56309,7 @@ ${error.text}`;
|
|
|
56146
56309
|
try {
|
|
56147
56310
|
const answer = await Promise.race([
|
|
56148
56311
|
this.godotBridge.invokeTool("scan_status", {}, { counted: false }),
|
|
56149
|
-
|
|
56312
|
+
delay9(PLAYING_STATUS_MS).then(() => null)
|
|
56150
56313
|
]);
|
|
56151
56314
|
if (answer === null) {
|
|
56152
56315
|
return null;
|
|
@@ -56288,7 +56451,7 @@ ${error.text}`;
|
|
|
56288
56451
|
if (answer.serversNote === undefined || adapterFailed || Date.now() >= deadline) {
|
|
56289
56452
|
return answer;
|
|
56290
56453
|
}
|
|
56291
|
-
await
|
|
56454
|
+
await delay9(500);
|
|
56292
56455
|
}
|
|
56293
56456
|
}
|
|
56294
56457
|
async rebindTheDebugAdapter(servers) {
|
|
@@ -56342,7 +56505,7 @@ ${error.text}`;
|
|
|
56342
56505
|
return null;
|
|
56343
56506
|
}
|
|
56344
56507
|
try {
|
|
56345
|
-
return settingKeys(
|
|
56508
|
+
return settingKeys(readFileSync42(join46(projectPath, "project.godot"), "utf8"));
|
|
56346
56509
|
} catch {
|
|
56347
56510
|
return null;
|
|
56348
56511
|
}
|
|
@@ -56549,7 +56712,7 @@ ${error.text}`;
|
|
|
56549
56712
|
if (Date.now() >= deadline) {
|
|
56550
56713
|
return false;
|
|
56551
56714
|
}
|
|
56552
|
-
await
|
|
56715
|
+
await delay9(250);
|
|
56553
56716
|
}
|
|
56554
56717
|
}
|
|
56555
56718
|
async handleLaunchEditor(args) {
|
|
@@ -56687,7 +56850,7 @@ ${error.text}`;
|
|
|
56687
56850
|
if (!paths.ok) {
|
|
56688
56851
|
return refused(paths.reason, PATH_SOLUTIONS);
|
|
56689
56852
|
}
|
|
56690
|
-
const missing = paths.value.filter((path) => !existsSync36(
|
|
56853
|
+
const missing = paths.value.filter((path) => !existsSync36(join46(projectPath, path.slice("res://".length))));
|
|
56691
56854
|
if (missing.length > 0) {
|
|
56692
56855
|
return refused(`Nothing at ${missing.join(" and ")} in the project at ${projectPath}.`, PATH_SOLUTIONS);
|
|
56693
56856
|
}
|
|
@@ -56860,7 +57023,7 @@ ${error.text}`;
|
|
|
56860
57023
|
}
|
|
56861
57024
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
56862
57025
|
const budgetMs = budget.ms;
|
|
56863
|
-
if (!existsSync36(
|
|
57026
|
+
if (!existsSync36(join46(projectPath, RUNTIME_SCRIPT))) {
|
|
56864
57027
|
return runtimeVerdict(null, {
|
|
56865
57028
|
addon: false,
|
|
56866
57029
|
budgetMs,
|
|
@@ -56963,7 +57126,7 @@ ${error.text}`;
|
|
|
56963
57126
|
}
|
|
56964
57127
|
let announcedAt;
|
|
56965
57128
|
try {
|
|
56966
|
-
announcedAt =
|
|
57129
|
+
announcedAt = statSync21(endpoint.file).mtimeMs;
|
|
56967
57130
|
} catch {
|
|
56968
57131
|
return;
|
|
56969
57132
|
}
|
|
@@ -57023,7 +57186,7 @@ ${error.text}`;
|
|
|
57023
57186
|
stepFrom = now;
|
|
57024
57187
|
};
|
|
57025
57188
|
const transcript = openTranscript(startedAt);
|
|
57026
|
-
|
|
57189
|
+
closeSync7(transcript.fd);
|
|
57027
57190
|
step("transcript");
|
|
57028
57191
|
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
57029
57192
|
const which = playOf(playAnswer);
|
|
@@ -57124,7 +57287,7 @@ ${error.text}`;
|
|
|
57124
57287
|
}
|
|
57125
57288
|
let handle;
|
|
57126
57289
|
try {
|
|
57127
|
-
handle =
|
|
57290
|
+
handle = openSync8(game.errorReport, "r");
|
|
57128
57291
|
} catch {
|
|
57129
57292
|
return;
|
|
57130
57293
|
}
|
|
@@ -57158,7 +57321,7 @@ ${error.text}`;
|
|
|
57158
57321
|
game.log.append("stderr", reported);
|
|
57159
57322
|
}
|
|
57160
57323
|
} finally {
|
|
57161
|
-
|
|
57324
|
+
closeSync7(handle);
|
|
57162
57325
|
}
|
|
57163
57326
|
}
|
|
57164
57327
|
announcedPidNamedBy(run, line) {
|
|
@@ -57178,7 +57341,7 @@ ${error.text}`;
|
|
|
57178
57341
|
return 0;
|
|
57179
57342
|
}
|
|
57180
57343
|
try {
|
|
57181
|
-
return reportAlreadyInTranscript(
|
|
57344
|
+
return reportAlreadyInTranscript(readFileSync42(game.transcript), readFileSync42(report));
|
|
57182
57345
|
} catch (error) {
|
|
57183
57346
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
57184
57347
|
return 0;
|
|
@@ -57201,7 +57364,7 @@ ${error.text}`;
|
|
|
57201
57364
|
if (Date.now() >= deadline) {
|
|
57202
57365
|
return false;
|
|
57203
57366
|
}
|
|
57204
|
-
await
|
|
57367
|
+
await delay9(100);
|
|
57205
57368
|
}
|
|
57206
57369
|
}
|
|
57207
57370
|
async endActiveGame(reason, going, graceful) {
|
|
@@ -57465,7 +57628,7 @@ ${error.text}`;
|
|
|
57465
57628
|
const slot = await slotForCall(true);
|
|
57466
57629
|
const startedAt = Date.now();
|
|
57467
57630
|
const transcript = openTranscript(startedAt);
|
|
57468
|
-
|
|
57631
|
+
closeSync7(transcript.fd);
|
|
57469
57632
|
const profile = profiled ? profilePath(startedAt) : undefined;
|
|
57470
57633
|
const gameEnv = onTheSlot(profiled ? { ...env ?? process.env, [PROFILE_FROM_START]: "1" } : env ?? process.env, slot.lock);
|
|
57471
57634
|
let launched;
|
|
@@ -57525,7 +57688,7 @@ ${error.text}`;
|
|
|
57525
57688
|
}
|
|
57526
57689
|
let handle;
|
|
57527
57690
|
try {
|
|
57528
|
-
handle =
|
|
57691
|
+
handle = openSync8(game.transcript, "r");
|
|
57529
57692
|
} catch {
|
|
57530
57693
|
return;
|
|
57531
57694
|
}
|
|
@@ -57539,7 +57702,7 @@ ${error.text}`;
|
|
|
57539
57702
|
game.readOffset += read;
|
|
57540
57703
|
game.log.append("transcript", buffer.subarray(0, read));
|
|
57541
57704
|
} finally {
|
|
57542
|
-
|
|
57705
|
+
closeSync7(handle);
|
|
57543
57706
|
}
|
|
57544
57707
|
}
|
|
57545
57708
|
async pickUpWhatTheEditorIsPlaying() {
|
|
@@ -57936,7 +58099,7 @@ ${error.text}`;
|
|
|
57936
58099
|
};
|
|
57937
58100
|
const going = await this.runStillGoing(run);
|
|
57938
58101
|
const projectPath = run.projectPath;
|
|
57939
|
-
const projectGone = projectPath !== null && !existsSync36(
|
|
58102
|
+
const projectGone = projectPath !== null && !existsSync36(join46(projectPath, "project.godot"));
|
|
57940
58103
|
if (projectGone && callersName !== undefined) {
|
|
57941
58104
|
return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
|
|
57942
58105
|
}
|
|
@@ -58094,7 +58257,7 @@ ${error.text}`;
|
|
|
58094
58257
|
return true;
|
|
58095
58258
|
}
|
|
58096
58259
|
}
|
|
58097
|
-
await
|
|
58260
|
+
await delay9(100);
|
|
58098
58261
|
}
|
|
58099
58262
|
return false;
|
|
58100
58263
|
}
|
|
@@ -58124,7 +58287,7 @@ ${error.text}`;
|
|
|
58124
58287
|
break;
|
|
58125
58288
|
}
|
|
58126
58289
|
}
|
|
58127
|
-
await
|
|
58290
|
+
await delay9(RUN_POLL_MS);
|
|
58128
58291
|
}
|
|
58129
58292
|
if (!await this.runStillGoing(run)) {
|
|
58130
58293
|
return await this.handleGetDebugOutput(args);
|
|
@@ -58186,7 +58349,7 @@ ${error.text}`;
|
|
|
58186
58349
|
if (Date.now() >= until) {
|
|
58187
58350
|
return false;
|
|
58188
58351
|
}
|
|
58189
|
-
await
|
|
58352
|
+
await delay9(50);
|
|
58190
58353
|
}
|
|
58191
58354
|
}
|
|
58192
58355
|
async handleEditorConsole(args) {
|
|
@@ -58343,7 +58506,7 @@ ${error.text}`;
|
|
|
58343
58506
|
notes.push(endedWithoutACode({
|
|
58344
58507
|
throughEditor: run.throughEditor,
|
|
58345
58508
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
58346
|
-
couldAnnounce: run.projectPath !== null && existsSync36(
|
|
58509
|
+
couldAnnounce: run.projectPath !== null && existsSync36(join46(run.projectPath, RUNTIME_SCRIPT)),
|
|
58347
58510
|
endedHere: typeof run.endedHere === "string",
|
|
58348
58511
|
pickedUp: run.pickedUp === true
|
|
58349
58512
|
}));
|
|
@@ -58601,6 +58764,66 @@ ${error.text}`;
|
|
|
58601
58764
|
return error instanceof BridgeTimeout;
|
|
58602
58765
|
}
|
|
58603
58766
|
}
|
|
58767
|
+
async handleEditorCapture(args) {
|
|
58768
|
+
const projectPath = readNonEmptyString(args, "projectPath") ?? "";
|
|
58769
|
+
const saveTo = readNonEmptyString(args, "outputPath") ?? null;
|
|
58770
|
+
const sendsThePicture = readBoolean(args, "inline") !== false;
|
|
58771
|
+
if (!sendsThePicture && saveTo === null) {
|
|
58772
|
+
return this.createErrorResponse("inline: false sends no picture back, so it needs outputPath to save one to.", [
|
|
58773
|
+
"Pass outputPath, an absolute path ending in .png outside the project",
|
|
58774
|
+
"Or leave inline out to have the picture sent"
|
|
58775
|
+
]);
|
|
58776
|
+
}
|
|
58777
|
+
if (saveTo !== null) {
|
|
58778
|
+
const refusal = captureDestinationRefusal(saveTo, [
|
|
58779
|
+
...projectPath === "" ? [] : [projectPath],
|
|
58780
|
+
...this.ownProject === null ? [] : [this.ownProject]
|
|
58781
|
+
]);
|
|
58782
|
+
if (refusal !== null) {
|
|
58783
|
+
return this.createErrorResponse(refusal);
|
|
58784
|
+
}
|
|
58785
|
+
}
|
|
58786
|
+
const folder = scratchDirectory("editor-capture");
|
|
58787
|
+
const picture = join46(folder, "capture.png");
|
|
58788
|
+
try {
|
|
58789
|
+
const asked = await this.handleViaBridge("capture_editor_view", { ...args, output_path: picture }, "editor_capture");
|
|
58790
|
+
if (asked.isError === true) {
|
|
58791
|
+
return asked;
|
|
58792
|
+
}
|
|
58793
|
+
const payload = asParams(JSON.parse(asked.content[0]?.text ?? "{}"));
|
|
58794
|
+
const returned = readString(payload, "path");
|
|
58795
|
+
if (readString(payload, "type") !== "screenshot_file" || returned === undefined) {
|
|
58796
|
+
return this.createErrorResponse(`The editor answered a capture with ${JSON.stringify(payload)}`);
|
|
58797
|
+
}
|
|
58798
|
+
if (normalize2(returned) !== normalize2(picture)) {
|
|
58799
|
+
return this.createErrorResponse(`Rejected capture file path outside the managed capture path: '${returned}'`);
|
|
58800
|
+
}
|
|
58801
|
+
if (saveTo !== null) {
|
|
58802
|
+
try {
|
|
58803
|
+
copyFileSync2(picture, saveTo, constants.COPYFILE_EXCL);
|
|
58804
|
+
} catch (error) {
|
|
58805
|
+
return this.createErrorResponse(`The capture was taken and could not be saved at ${saveTo}: ${errorMessage(error)}`);
|
|
58806
|
+
}
|
|
58807
|
+
}
|
|
58808
|
+
const sent = sendsThePicture ? pictureToSend(picture, readString(payload, "jpegPath")) : null;
|
|
58809
|
+
const view = readString(payload, "view") === "3d" ? "3D" : "2D";
|
|
58810
|
+
const drawnAt = payload["drawnAt"];
|
|
58811
|
+
const scaled = isRecord(drawnAt) ? `, scaled from ${String(drawnAt["width"])}x${String(drawnAt["height"])}` : "";
|
|
58812
|
+
const unsaved = payload["unsavedInEditor"] === true ? ". The picture includes the scene's unsaved changes in the editor" : "";
|
|
58813
|
+
const how = sent === null ? ". Not sent, as inline: false asked" : sent.mimeType === "image/jpeg" ? `. Sent as JPEG, ${kilobytes(sent.bytes)} against the PNG's ${kilobytes(sent.pngBytes)}${saveTo === null ? "" : ", and the saved PNG is lossless"}` : "";
|
|
58814
|
+
return {
|
|
58815
|
+
content: [
|
|
58816
|
+
{
|
|
58817
|
+
type: "text",
|
|
58818
|
+
text: `The editor's ${view} view of ${readString(payload, "scenePath") ?? "the scene"}: ${String(readNumber(payload, "width") ?? 0)}x${String(readNumber(payload, "height") ?? 0)} png${scaled}${unsaved}${saveTo === null ? "" : `. Saved to ${saveTo}`}${how}`
|
|
58819
|
+
},
|
|
58820
|
+
...sent === null ? [] : [{ type: "image", data: sent.data, mimeType: sent.mimeType }]
|
|
58821
|
+
]
|
|
58822
|
+
};
|
|
58823
|
+
} finally {
|
|
58824
|
+
discard(folder);
|
|
58825
|
+
}
|
|
58826
|
+
}
|
|
58604
58827
|
async handleRescanFilesystem(args) {
|
|
58605
58828
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 30000;
|
|
58606
58829
|
const started = Date.now();
|
|
@@ -59416,17 +59639,25 @@ ${error.text}`;
|
|
|
59416
59639
|
]);
|
|
59417
59640
|
}
|
|
59418
59641
|
}
|
|
59419
|
-
const
|
|
59420
|
-
|
|
59421
|
-
|
|
59422
|
-
|
|
59423
|
-
|
|
59642
|
+
const run = async () => {
|
|
59643
|
+
const declaring = declaredButUncached(projectPath, typeName);
|
|
59644
|
+
if (declaring !== null) {
|
|
59645
|
+
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
59646
|
+
if (!rebuilt.ok) {
|
|
59647
|
+
return this.answer(rebuilt);
|
|
59648
|
+
}
|
|
59424
59649
|
}
|
|
59425
|
-
|
|
59426
|
-
|
|
59427
|
-
|
|
59428
|
-
|
|
59429
|
-
|
|
59650
|
+
const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
|
|
59651
|
+
if (!outcome.ok) {
|
|
59652
|
+
return this.answer(outcome);
|
|
59653
|
+
}
|
|
59654
|
+
return this.headlessAnswer(outcome, effect, typeName, declaring);
|
|
59655
|
+
};
|
|
59656
|
+
const written = readNonEmptyString(contained.value, "scenePath") ?? readNonEmptyString(contained.value, "resourcePath");
|
|
59657
|
+
const target = effect === "writes" && written !== undefined ? resolveWithinProject(projectPath, written) : null;
|
|
59658
|
+
return target?.ok === true ? await inTurnFor(projectPath, target.absolutePath, run) : await run();
|
|
59659
|
+
}
|
|
59660
|
+
headlessAnswer(outcome, effect, typeName, declaring) {
|
|
59430
59661
|
return this.jsonTextResponse({
|
|
59431
59662
|
...outcome.payload,
|
|
59432
59663
|
...declaring === null ? {} : {
|
|
@@ -59685,7 +59916,7 @@ ${error.text}`;
|
|
|
59685
59916
|
}
|
|
59686
59917
|
const escaped = symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
59687
59918
|
const declared = new RegExp(`^\\s*(?:@\\w+(?:\\([^)]*\\))?\\s+)*class_name\\s+${escaped}\\b`, "m");
|
|
59688
|
-
if (declared.test(
|
|
59919
|
+
if (declared.test(readFileSync42(scriptPath, "utf8"))) {
|
|
59689
59920
|
return null;
|
|
59690
59921
|
}
|
|
59691
59922
|
const checked = await this.classesTheEditorCannotSee(args);
|
|
@@ -59883,7 +60114,7 @@ ${error.text}`;
|
|
|
59883
60114
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
59884
60115
|
const run = await this.currentRun();
|
|
59885
60116
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
59886
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync36(
|
|
60117
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync36(join46(project, RUNTIME_SCRIPT)) || !await this.runStillGoing(run)) {
|
|
59887
60118
|
return null;
|
|
59888
60119
|
}
|
|
59889
60120
|
const already = runtimesAnnounced().running;
|
|
@@ -59892,7 +60123,7 @@ ${error.text}`;
|
|
|
59892
60123
|
return !run.announcedBefore.has(one.pid);
|
|
59893
60124
|
}
|
|
59894
60125
|
try {
|
|
59895
|
-
return
|
|
60126
|
+
return statSync21(one.file).mtimeMs >= run.startedAt;
|
|
59896
60127
|
} catch {
|
|
59897
60128
|
return false;
|
|
59898
60129
|
}
|
|
@@ -59993,7 +60224,7 @@ ${error.text}`;
|
|
|
59993
60224
|
return this.createErrorResponse(staleNote);
|
|
59994
60225
|
}
|
|
59995
60226
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
59996
|
-
const screenshotPath = screenshotDir ?
|
|
60227
|
+
const screenshotPath = screenshotDir ? join46(screenshotDir, "capture.png") : null;
|
|
59997
60228
|
const refused = (message) => this.createErrorResponse(message);
|
|
59998
60229
|
const onGame = {
|
|
59999
60230
|
pid: choice.endpoint.pid,
|
|
@@ -60266,6 +60497,7 @@ var init_server = __esm(() => {
|
|
|
60266
60497
|
init_tool_definitions();
|
|
60267
60498
|
init_tool_reference();
|
|
60268
60499
|
init_update_check();
|
|
60500
|
+
init_write_turns();
|
|
60269
60501
|
TYPE_ARGUMENTS = {
|
|
60270
60502
|
add_node: "nodeType",
|
|
60271
60503
|
create_resource: "resourceType",
|
|
@@ -60338,7 +60570,7 @@ init_bridge_announce();
|
|
|
60338
60570
|
init_engine_launch();
|
|
60339
60571
|
init_engine_slots();
|
|
60340
60572
|
import { existsSync as existsSync37 } from "node:fs";
|
|
60341
|
-
import { dirname as dirname26, join as
|
|
60573
|
+
import { dirname as dirname26, join as join47, resolve as resolve16 } from "node:path";
|
|
60342
60574
|
|
|
60343
60575
|
// src/error-mode.ts
|
|
60344
60576
|
import process8 from "node:process";
|
|
@@ -60688,6 +60920,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
60688
60920
|
| End a game of the project that no server is holding | \`editor_run stop\` with its \`pid\` from \`editor_status\` \`runtimes\`, never a process kill by hand: it is ended only when it announced itself for the project and its command line is the project's engine run |
|
|
60689
60921
|
| Play a game without touching the player's saves | \`editor_run start\` with \`savesIn\`, a directory for this run's \`user://\`: the editor plays with its own environment, so a run given one is started by the server, and the answer says under \`savesNote\` when the platform ignores it |
|
|
60690
60922
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
60923
|
+
| What a scene's @tool scripts draw in the editor | \`editor_capture\`, the editor's own 2D or 3D view of the scene. No game runs their editor branch |
|
|
60691
60924
|
|
|
60692
60925
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
60693
60926
|
window and refuses headless rather than handing back the last frame anything drew. On Windows a
|
|
@@ -62609,7 +62842,7 @@ class UsageError extends Refusal {
|
|
|
62609
62842
|
function projectArgument(at) {
|
|
62610
62843
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
62611
62844
|
const projectPath = resolve16(named ?? ".");
|
|
62612
|
-
if (!existsSync37(
|
|
62845
|
+
if (!existsSync37(join47(projectPath, "project.godot"))) {
|
|
62613
62846
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot. Run from the project directory, or name it on the command line.`);
|
|
62614
62847
|
}
|
|
62615
62848
|
return projectPath;
|
|
@@ -62673,7 +62906,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
62673
62906
|
}
|
|
62674
62907
|
const path = (named ?? "").replace(/^res:\/\//, "");
|
|
62675
62908
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
62676
|
-
if (!existsSync37(
|
|
62909
|
+
if (!existsSync37(join47(projectPath, path))) {
|
|
62677
62910
|
console.log(" that file is not in this project, so it boots with a missing script");
|
|
62678
62911
|
}
|
|
62679
62912
|
console.log(` ${useTheShippedLoader(RUNTIME_AUTOLOAD.name, path)}`);
|