gdharness 1.1.21 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10058,23 +10058,24 @@ function sameCodeNote(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
|
10058
10058
|
}
|
|
10059
10059
|
return `The editor loaded the ${addonVersion} addons, which are the same code this ${serverVersion} server ships, so nothing needs restarting; addonVersion changes at the next editor start.`;
|
|
10060
10060
|
}
|
|
10061
|
-
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10061
|
+
function addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
10062
10062
|
if (!editorIsStale(addonVersion, serverVersion, addonDigest, shippedDigest)) {
|
|
10063
10063
|
return;
|
|
10064
10064
|
}
|
|
10065
10065
|
const reported = addonVersion ?? "";
|
|
10066
|
+
const restart = onDisk !== undefined && onDisk !== shippedDigest ? `The project's own addons are not this server's either, so a restart would load the same code again: gdharness upgrade in the project installs the ${serverVersion} addons, and editor_launch restart then picks them up.` : "Restart it with editor_launch restart to pick this one up.";
|
|
10066
10067
|
if (reported === serverVersion) {
|
|
10067
|
-
return `The editor is running a different build of the ${reported} addon from the one this server ships.
|
|
10068
|
+
return `The editor is running a different build of the ${reported} addon from the one this server ships. ${restart}`;
|
|
10068
10069
|
}
|
|
10069
10070
|
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
10070
10071
|
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
10071
10072
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
10072
10073
|
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
10073
10074
|
}
|
|
10074
|
-
return `${both}
|
|
10075
|
+
return `${both} ${restart}`;
|
|
10075
10076
|
}
|
|
10076
|
-
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest) {
|
|
10077
|
-
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest);
|
|
10077
|
+
function markIfStale(answer, addonVersion, serverVersion, addonDigest, shippedDigest, onDisk) {
|
|
10078
|
+
const staleNote = addonMismatch(addonVersion, serverVersion, addonDigest, shippedDigest, onDisk);
|
|
10078
10079
|
if (staleNote === undefined || typeof answer !== "object" || answer === null || Array.isArray(answer)) {
|
|
10079
10080
|
return answer;
|
|
10080
10081
|
}
|
|
@@ -10201,27 +10202,27 @@ function defectReport(where, error, context = {}) {
|
|
|
10201
10202
|
}
|
|
10202
10203
|
|
|
10203
10204
|
// src/server.ts
|
|
10204
|
-
import { execFile as
|
|
10205
|
+
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
10205
10206
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10206
10207
|
import {
|
|
10207
10208
|
appendFileSync,
|
|
10208
10209
|
closeSync as closeSync2,
|
|
10209
10210
|
constants,
|
|
10210
10211
|
copyFileSync,
|
|
10211
|
-
existsSync as
|
|
10212
|
+
existsSync as existsSync14,
|
|
10212
10213
|
fstatSync,
|
|
10213
10214
|
mkdirSync as mkdirSync10,
|
|
10214
10215
|
openSync as openSync3,
|
|
10215
10216
|
readdirSync as readdirSync8,
|
|
10216
|
-
readFileSync as
|
|
10217
|
+
readFileSync as readFileSync18,
|
|
10217
10218
|
readSync as readSync2,
|
|
10218
10219
|
rmdirSync as rmdirSync2,
|
|
10219
10220
|
statSync as statSync8
|
|
10220
10221
|
} from "node:fs";
|
|
10221
|
-
import { basename as basename4, dirname as
|
|
10222
|
+
import { basename as basename4, dirname as dirname12, extname as extname2, isAbsolute as isAbsolute3, join as join19, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10222
10223
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10223
10224
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10224
|
-
import { promisify as
|
|
10225
|
+
import { promisify as promisify5 } from "node:util";
|
|
10225
10226
|
|
|
10226
10227
|
// node_modules/zod/v3/helpers/util.js
|
|
10227
10228
|
var util;
|
|
@@ -24863,6 +24864,7 @@ function editorArguments(projectPath, ports, logFile, hidden) {
|
|
|
24863
24864
|
];
|
|
24864
24865
|
}
|
|
24865
24866
|
var OPENED_BY_A_SERVER = "GDHARNESS_OPENED_BY_A_SERVER";
|
|
24867
|
+
var EDITOR_PID_VARIABLE = "GDHARNESS_EDITOR_PID";
|
|
24866
24868
|
function userDataIn(home, variables = process.env) {
|
|
24867
24869
|
const moved = ["APPDATA", "XDG_DATA_HOME"];
|
|
24868
24870
|
const carried = {};
|
|
@@ -24914,6 +24916,12 @@ function resolveWithinProject(projectPath, candidatePath) {
|
|
|
24914
24916
|
return { ok: false, reason: "Path contains a null byte." };
|
|
24915
24917
|
}
|
|
24916
24918
|
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
24919
|
+
if (withoutScheme.startsWith("uid://")) {
|
|
24920
|
+
return {
|
|
24921
|
+
ok: false,
|
|
24922
|
+
reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
|
|
24923
|
+
};
|
|
24924
|
+
}
|
|
24917
24925
|
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
24918
24926
|
if (scheme) {
|
|
24919
24927
|
return {
|
|
@@ -25457,6 +25465,12 @@ function parseAnnouncement(file, pid) {
|
|
|
25457
25465
|
}
|
|
25458
25466
|
};
|
|
25459
25467
|
}
|
|
25468
|
+
function dialAddress(address) {
|
|
25469
|
+
if (address === "*" || address === "0.0.0.0") {
|
|
25470
|
+
return "127.0.0.1";
|
|
25471
|
+
}
|
|
25472
|
+
return address === "::" ? "::1" : address;
|
|
25473
|
+
}
|
|
25460
25474
|
function runtimesAnnounced(directories = runtimeDirectories()) {
|
|
25461
25475
|
const running = new Map;
|
|
25462
25476
|
const unspoken = new Map;
|
|
@@ -25620,13 +25634,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
25620
25634
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
25621
25635
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
25622
25636
|
for (;; ) {
|
|
25623
|
-
const fresh = discoverRuntimes(directories).
|
|
25624
|
-
if (fresh
|
|
25637
|
+
const fresh = discoverRuntimes(directories).filter((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
|
|
25638
|
+
if (fresh.length > 0) {
|
|
25625
25639
|
return fresh;
|
|
25626
25640
|
}
|
|
25627
25641
|
const left = until - Date.now();
|
|
25628
25642
|
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
25629
|
-
return
|
|
25643
|
+
return [];
|
|
25630
25644
|
}
|
|
25631
25645
|
await delay(Math.min(LOOK_EVERY_MS, left));
|
|
25632
25646
|
}
|
|
@@ -25761,7 +25775,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs, keepListening = fa
|
|
|
25761
25775
|
let late = false;
|
|
25762
25776
|
let buffered = "";
|
|
25763
25777
|
let welcomed = false;
|
|
25764
|
-
const socket = createConnection({ port: endpoint.port, host: endpoint.address });
|
|
25778
|
+
const socket = createConnection({ port: endpoint.port, host: dialAddress(endpoint.address) });
|
|
25765
25779
|
let lateTimer = null;
|
|
25766
25780
|
const finish = (reply) => {
|
|
25767
25781
|
if (late) {
|
|
@@ -26033,6 +26047,10 @@ function callSignal() {
|
|
|
26033
26047
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
|
|
26034
26048
|
import { join as join6 } from "node:path";
|
|
26035
26049
|
var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
26050
|
+
var TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
|
|
26051
|
+
function isToolScript(source) {
|
|
26052
|
+
return TOOL.test(codeOf(source));
|
|
26053
|
+
}
|
|
26036
26054
|
function declaredClasses(projectPath) {
|
|
26037
26055
|
const declared = new Map;
|
|
26038
26056
|
eachScript(projectPath, (script, source) => {
|
|
@@ -26072,7 +26090,7 @@ function scriptsNaming(projectPath, classes) {
|
|
|
26072
26090
|
}
|
|
26073
26091
|
const code = codeOf(source);
|
|
26074
26092
|
if (classesNamedIn(code, named).length > 0) {
|
|
26075
|
-
naming.push({ script, tool:
|
|
26093
|
+
naming.push({ script, tool: TOOL.test(code) });
|
|
26076
26094
|
}
|
|
26077
26095
|
});
|
|
26078
26096
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
@@ -26279,6 +26297,19 @@ function classRemedy(entries) {
|
|
|
26279
26297
|
const oneAtATime = declaring.length === 1 ? "" : " Take them one at a time: a rescan that brings in a class also reloads every script naming it and lists them under dependentsReloaded, so a script listed there needs no call of its own.";
|
|
26280
26298
|
return `For ${entries.map(written).join(", ")}, run ${reload} first.${oneAtATime} In the one project ` + "that has reproduced a stale enum member, three times, the reload cleared it at once each time, " + "and the one plain editor_rescan tried left the diagnostic standing.";
|
|
26281
26299
|
}
|
|
26300
|
+
function notReloadedNote(broughtIn, notReloaded) {
|
|
26301
|
+
const tried = notReloaded.filter((one) => one.tool !== true).map((one) => one.scriptPath);
|
|
26302
|
+
const tools = notReloaded.filter((one) => one.tool === true).map((one) => one.scriptPath);
|
|
26303
|
+
const all = [...tried, ...tools];
|
|
26304
|
+
const remedies = [];
|
|
26305
|
+
if (tried.length > 0) {
|
|
26306
|
+
remedies.push(`For ${tried.join(", ")}: editor_rescan with reloadScript on ${tried.length === 1 ? "it" : "each"}, or editor_launch restart.`);
|
|
26307
|
+
}
|
|
26308
|
+
if (tools.length > 0) {
|
|
26309
|
+
remedies.push(`For ${tools.join(", ")}, ${tools.length === 1 ? "a @tool script" : "@tool scripts"}, which ${tools.length === 1 ? "runs" : "run"} inside the editor and ${tools.length === 1 ? "is" : "are"} not reloaded from inside a call: editor_launch restart.`);
|
|
26310
|
+
}
|
|
26311
|
+
return `The scan brought in ${broughtIn.join(", ")}, and ${all.join(", ")} ${all.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. ${remedies.join(" ")}`;
|
|
26312
|
+
}
|
|
26282
26313
|
function failedReloadNote(failed) {
|
|
26283
26314
|
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
26284
26315
|
const lost = [
|
|
@@ -26287,7 +26318,17 @@ function failedReloadNote(failed) {
|
|
|
26287
26318
|
];
|
|
26288
26319
|
const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
|
|
26289
26320
|
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
26290
|
-
const
|
|
26321
|
+
const unusable = "A failed compile leaves the copy the editor holds unusable, measured on 4.7.2: the editor can make no instance of it, and one it already made has lost its methods, while its member lists read as before.";
|
|
26322
|
+
let copy;
|
|
26323
|
+
if (failed.restored === null) {
|
|
26324
|
+
copy = `${unusable} This editor's addon does not put it back, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
26325
|
+
} else if (!failed.restored) {
|
|
26326
|
+
copy = `${unusable} The editor compiled it again from the text it was built from, and that failed too${failed.restoredAs === "" ? "" : ` (${failed.restoredAs})`}, so it stays that way until the script compiles: reload it then, or restart the editor with editor_launch restart.`;
|
|
26327
|
+
} else if (unchanged) {
|
|
26328
|
+
copy = "The copy the editor holds was compiled again from the text it was built from, which puts it back as it was, with the members under heldBeforeReload and heldConstantsBeforeReload.";
|
|
26329
|
+
} else {
|
|
26330
|
+
copy = `The copy the editor holds was compiled again from the text it was built from and changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
|
|
26331
|
+
}
|
|
26291
26332
|
const order = failed.names.length === 0 ? "" : ` If they deny a member of ${failed.names.join(" or ")} that its file declares, the editor's copy of that class is behind: reload that class first with reloadScript and then this script, the order that cleared it in the one project that has met this.`;
|
|
26292
26333
|
return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
|
|
26293
26334
|
}
|
|
@@ -26526,7 +26567,14 @@ function bindable(port, host) {
|
|
|
26526
26567
|
var DEFAULT_DAP_PORT = 6006;
|
|
26527
26568
|
var DAP_REQUEST_TIMEOUT_MS = 1e4;
|
|
26528
26569
|
var SCOPES_TIMEOUT_MS = 20000;
|
|
26570
|
+
var STEP_LANDS_WITHIN_MS = 1e4;
|
|
26529
26571
|
var VARIABLES_POLL_TIMEOUT_MS = 2000;
|
|
26572
|
+
function fileKey(path) {
|
|
26573
|
+
return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
|
|
26574
|
+
}
|
|
26575
|
+
|
|
26576
|
+
class ReadRefusal extends Refusal {
|
|
26577
|
+
}
|
|
26530
26578
|
function said(body, field) {
|
|
26531
26579
|
const value = body?.[field];
|
|
26532
26580
|
return typeof value === "string" ? value : "";
|
|
@@ -26543,17 +26591,25 @@ class GodotDAPClient {
|
|
|
26543
26591
|
outputBuffer = [];
|
|
26544
26592
|
onOutput = null;
|
|
26545
26593
|
maxOutputLines = 1000;
|
|
26594
|
+
outputDropped = 0;
|
|
26595
|
+
outputTaken = 0;
|
|
26546
26596
|
initialized = false;
|
|
26547
26597
|
attached = false;
|
|
26548
26598
|
lastThreadId = 1;
|
|
26549
26599
|
halt = null;
|
|
26550
26600
|
holdKnown = false;
|
|
26601
|
+
stops = 0;
|
|
26551
26602
|
breakpoints = new Map;
|
|
26603
|
+
spelled = new Map;
|
|
26552
26604
|
inEditor = new Map;
|
|
26553
26605
|
onBreakpointsChanged = null;
|
|
26554
|
-
|
|
26606
|
+
scopesPatienceMs;
|
|
26607
|
+
stepPatienceMs;
|
|
26608
|
+
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
|
|
26555
26609
|
this.port = port;
|
|
26556
26610
|
this.host = host;
|
|
26611
|
+
this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
|
|
26612
|
+
this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
|
|
26557
26613
|
this.pendingRequests = new Map;
|
|
26558
26614
|
}
|
|
26559
26615
|
async connect() {
|
|
@@ -26714,6 +26770,7 @@ class GodotDAPClient {
|
|
|
26714
26770
|
}
|
|
26715
26771
|
}
|
|
26716
26772
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
26773
|
+
this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
|
|
26717
26774
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
26718
26775
|
}
|
|
26719
26776
|
return;
|
|
@@ -26730,6 +26787,7 @@ class GodotDAPClient {
|
|
|
26730
26787
|
text: said(body, "text")
|
|
26731
26788
|
};
|
|
26732
26789
|
this.holdKnown = true;
|
|
26790
|
+
this.stops += 1;
|
|
26733
26791
|
return;
|
|
26734
26792
|
}
|
|
26735
26793
|
if (eventName === "continued") {
|
|
@@ -26746,20 +26804,24 @@ class GodotDAPClient {
|
|
|
26746
26804
|
if (typeof path !== "string" || typeof line !== "number") {
|
|
26747
26805
|
return;
|
|
26748
26806
|
}
|
|
26807
|
+
const key = fileKey(path);
|
|
26808
|
+
if (!this.spelled.has(key)) {
|
|
26809
|
+
this.spelled.set(key, path);
|
|
26810
|
+
}
|
|
26749
26811
|
if (said(body, "reason") === "removed") {
|
|
26750
|
-
this.inEditor.get(
|
|
26751
|
-
const held = this.breakpoints.get(
|
|
26812
|
+
this.inEditor.get(key)?.delete(line);
|
|
26813
|
+
const held = this.breakpoints.get(key);
|
|
26752
26814
|
if (held?.delete(line) === true) {
|
|
26753
26815
|
if (held.size === 0) {
|
|
26754
|
-
this.breakpoints.delete(
|
|
26816
|
+
this.breakpoints.delete(key);
|
|
26755
26817
|
}
|
|
26756
26818
|
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
26757
26819
|
}
|
|
26758
26820
|
return;
|
|
26759
26821
|
}
|
|
26760
|
-
const known = this.inEditor.get(
|
|
26822
|
+
const known = this.inEditor.get(key) ?? new Set;
|
|
26761
26823
|
known.add(line);
|
|
26762
|
-
this.inEditor.set(
|
|
26824
|
+
this.inEditor.set(key, known);
|
|
26763
26825
|
return;
|
|
26764
26826
|
}
|
|
26765
26827
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -26842,24 +26904,44 @@ class GodotDAPClient {
|
|
|
26842
26904
|
getOutput(clear = false) {
|
|
26843
26905
|
const lines = [...this.outputBuffer];
|
|
26844
26906
|
if (clear) {
|
|
26907
|
+
this.outputTaken += lines.length;
|
|
26845
26908
|
this.outputBuffer = [];
|
|
26846
26909
|
}
|
|
26847
26910
|
return lines;
|
|
26848
26911
|
}
|
|
26912
|
+
outputSoFar() {
|
|
26913
|
+
const output = this.getOutput(false);
|
|
26914
|
+
const notShown = this.outputDropped + this.outputTaken;
|
|
26915
|
+
if (notShown === 0) {
|
|
26916
|
+
return { lines: output.length, output };
|
|
26917
|
+
}
|
|
26918
|
+
const where = [
|
|
26919
|
+
this.outputDropped > 0 ? `${this.outputDropped} fell out of this buffer, which keeps the last ${this.maxOutputLines}` : "",
|
|
26920
|
+
this.outputTaken > 0 ? `${this.outputTaken} were moved into the run's log by an earlier read` : ""
|
|
26921
|
+
].filter((part) => part !== "");
|
|
26922
|
+
return {
|
|
26923
|
+
lines: output.length,
|
|
26924
|
+
output,
|
|
26925
|
+
notShown,
|
|
26926
|
+
note: `${notShown} earlier ${notShown === 1 ? "line is" : "lines are"} not in this answer: ${where.join(", and ")}. editor_output reads what the server kept of the run's console.`
|
|
26927
|
+
};
|
|
26928
|
+
}
|
|
26849
26929
|
async setBreakpoint(filePath, line) {
|
|
26850
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
26930
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
26851
26931
|
wanted.add(line);
|
|
26852
26932
|
return await this.sendBreakpoints(filePath, wanted);
|
|
26853
26933
|
}
|
|
26854
26934
|
async removeBreakpoint(filePath, line) {
|
|
26855
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
26935
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
26856
26936
|
wanted.delete(line);
|
|
26857
26937
|
return await this.sendBreakpoints(filePath, wanted, line);
|
|
26858
26938
|
}
|
|
26859
26939
|
async sendBreakpoints(filePath, mine, clearing) {
|
|
26860
26940
|
await this.ensureConnected();
|
|
26861
26941
|
await this.initialize();
|
|
26862
|
-
const
|
|
26942
|
+
const key = fileKey(filePath);
|
|
26943
|
+
this.spelled.set(key, filePath);
|
|
26944
|
+
const lines = new Set([...mine, ...this.inEditor.get(key) ?? []]);
|
|
26863
26945
|
if (clearing !== undefined) {
|
|
26864
26946
|
lines.delete(clearing);
|
|
26865
26947
|
}
|
|
@@ -26869,14 +26951,14 @@ class GodotDAPClient {
|
|
|
26869
26951
|
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
26870
26952
|
});
|
|
26871
26953
|
if (mine.size === 0) {
|
|
26872
|
-
this.breakpoints.delete(
|
|
26954
|
+
this.breakpoints.delete(key);
|
|
26873
26955
|
} else {
|
|
26874
|
-
this.breakpoints.set(
|
|
26956
|
+
this.breakpoints.set(key, new Set(mine));
|
|
26875
26957
|
}
|
|
26876
26958
|
if (lines.size === 0) {
|
|
26877
|
-
this.inEditor.delete(
|
|
26959
|
+
this.inEditor.delete(key);
|
|
26878
26960
|
} else {
|
|
26879
|
-
this.inEditor.set(
|
|
26961
|
+
this.inEditor.set(key, lines);
|
|
26880
26962
|
}
|
|
26881
26963
|
return answer;
|
|
26882
26964
|
}
|
|
@@ -26885,11 +26967,11 @@ class GodotDAPClient {
|
|
|
26885
26967
|
}
|
|
26886
26968
|
breakpointsInEditor() {
|
|
26887
26969
|
const theirs = [];
|
|
26888
|
-
for (const [
|
|
26889
|
-
const mine = this.breakpoints.get(
|
|
26970
|
+
for (const [key, lines] of this.inEditor) {
|
|
26971
|
+
const mine = this.breakpoints.get(key);
|
|
26890
26972
|
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
26891
26973
|
if (rest.length > 0) {
|
|
26892
|
-
theirs.push({ scriptPath, lines: rest });
|
|
26974
|
+
theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
|
|
26893
26975
|
}
|
|
26894
26976
|
}
|
|
26895
26977
|
return theirs;
|
|
@@ -26897,12 +26979,13 @@ class GodotDAPClient {
|
|
|
26897
26979
|
async reapplyBreakpoints() {
|
|
26898
26980
|
const applied = [];
|
|
26899
26981
|
const refused = [];
|
|
26900
|
-
for (const [
|
|
26982
|
+
for (const [key, lines] of Array.from(this.breakpoints)) {
|
|
26983
|
+
const filePath = this.spelled.get(key) ?? key;
|
|
26901
26984
|
try {
|
|
26902
26985
|
await this.sendBreakpoints(filePath, lines);
|
|
26903
26986
|
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
26904
26987
|
} catch (error) {
|
|
26905
|
-
this.breakpoints.delete(
|
|
26988
|
+
this.breakpoints.delete(key);
|
|
26906
26989
|
refused.push({
|
|
26907
26990
|
scriptPath: filePath,
|
|
26908
26991
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -26915,19 +26998,21 @@ class GodotDAPClient {
|
|
|
26915
26998
|
return { applied, refused };
|
|
26916
26999
|
}
|
|
26917
27000
|
breakpointsHeld() {
|
|
26918
|
-
return Array.from(this.breakpoints, ([
|
|
26919
|
-
scriptPath,
|
|
27001
|
+
return Array.from(this.breakpoints, ([key, lines]) => ({
|
|
27002
|
+
scriptPath: this.spelled.get(key) ?? key,
|
|
26920
27003
|
lines: Array.from(lines).sort((a, b) => a - b)
|
|
26921
27004
|
}));
|
|
26922
27005
|
}
|
|
26923
27006
|
holdBreakpoints(held) {
|
|
26924
27007
|
for (const { scriptPath, lines } of held) {
|
|
26925
|
-
const
|
|
27008
|
+
const key = fileKey(scriptPath);
|
|
27009
|
+
const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
|
|
26926
27010
|
for (const line of lines) {
|
|
26927
27011
|
fileBreakpoints.add(line);
|
|
26928
27012
|
}
|
|
26929
27013
|
if (fileBreakpoints.size > 0) {
|
|
26930
|
-
this.breakpoints.set(
|
|
27014
|
+
this.breakpoints.set(key, fileBreakpoints);
|
|
27015
|
+
this.spelled.set(key, scriptPath);
|
|
26931
27016
|
}
|
|
26932
27017
|
}
|
|
26933
27018
|
}
|
|
@@ -26938,15 +27023,16 @@ class GodotDAPClient {
|
|
|
26938
27023
|
this.halt = null;
|
|
26939
27024
|
this.holdKnown = true;
|
|
26940
27025
|
}
|
|
26941
|
-
async
|
|
26942
|
-
await this.attach();
|
|
26943
|
-
const resolvedThreadId = await this.resolveThreadId(threadId);
|
|
26944
|
-
await this.sendRequest("next", { threadId: resolvedThreadId });
|
|
26945
|
-
}
|
|
26946
|
-
async stepInto(threadId) {
|
|
27026
|
+
async step(command) {
|
|
26947
27027
|
await this.attach();
|
|
26948
|
-
const resolvedThreadId = await this.resolveThreadId(
|
|
26949
|
-
|
|
27028
|
+
const resolvedThreadId = await this.resolveThreadId();
|
|
27029
|
+
const before = this.stops;
|
|
27030
|
+
await this.sendRequest(command, { threadId: resolvedThreadId });
|
|
27031
|
+
const deadline = Date.now() + this.stepPatienceMs;
|
|
27032
|
+
while (this.stops === before && this.connected && Date.now() < deadline) {
|
|
27033
|
+
await delay2(50);
|
|
27034
|
+
}
|
|
27035
|
+
return this.stops !== before;
|
|
26950
27036
|
}
|
|
26951
27037
|
async getStackTrace(threadId) {
|
|
26952
27038
|
await this.attach();
|
|
@@ -26972,16 +27058,16 @@ class GodotDAPClient {
|
|
|
26972
27058
|
} catch {}
|
|
26973
27059
|
await delay2(200);
|
|
26974
27060
|
}
|
|
26975
|
-
return
|
|
27061
|
+
return null;
|
|
26976
27062
|
}
|
|
26977
27063
|
async getScopes(frameId) {
|
|
26978
27064
|
await this.attach();
|
|
26979
27065
|
const frames = await this.getStackTrace();
|
|
26980
27066
|
const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
|
|
26981
27067
|
if (frame === undefined) {
|
|
26982
|
-
|
|
27068
|
+
throw new ReadRefusal(frames.length === 0 ? "The adapter gave no stack, so there is no frame to read variables from" : `The stack the adapter gives now has no frame ${String(frameId)}; its frames are ${frames.map((each) => String(each["id"])).join(", ")}. Read the stack again for the frames the game is at now`);
|
|
26983
27069
|
}
|
|
26984
|
-
const deadline = Date.now() +
|
|
27070
|
+
const deadline = Date.now() + this.scopesPatienceMs;
|
|
26985
27071
|
let scopes = [];
|
|
26986
27072
|
while (scopes.length === 0 && Date.now() < deadline) {
|
|
26987
27073
|
try {
|
|
@@ -26993,6 +27079,9 @@ class GodotDAPClient {
|
|
|
26993
27079
|
} catch {}
|
|
26994
27080
|
await delay2(200);
|
|
26995
27081
|
}
|
|
27082
|
+
if (scopes.length === 0) {
|
|
27083
|
+
throw new ReadRefusal(`The scopes of frame ${String(frame["id"])} did not arrive from the game within ${this.scopesPatienceMs / 1000}s. They come from the game through the editor, so a busy machine can take longer: ask again`);
|
|
27084
|
+
}
|
|
26996
27085
|
const named = [];
|
|
26997
27086
|
for (const scope of scopes) {
|
|
26998
27087
|
const reference = scope["variablesReference"];
|
|
@@ -27056,9 +27145,8 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27056
27145
|
try {
|
|
27057
27146
|
switch (toolName) {
|
|
27058
27147
|
case "dap_get_output": {
|
|
27059
|
-
const output = client.getOutput(false);
|
|
27060
27148
|
return {
|
|
27061
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
27149
|
+
content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
|
|
27062
27150
|
};
|
|
27063
27151
|
}
|
|
27064
27152
|
case "dap_set_breakpoint": {
|
|
@@ -27085,19 +27173,13 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27085
27173
|
}
|
|
27086
27174
|
case "dap_step_over":
|
|
27087
27175
|
case "dap_step_into": {
|
|
27088
|
-
|
|
27089
|
-
|
|
27090
|
-
|
|
27091
|
-
|
|
27092
|
-
|
|
27093
|
-
return {
|
|
27094
|
-
content: [
|
|
27095
|
-
{
|
|
27096
|
-
type: "text",
|
|
27097
|
-
text: JSON.stringify({ stepped: true, stack: await client.getStackTrace() }, null, 2)
|
|
27098
|
-
}
|
|
27099
|
-
]
|
|
27176
|
+
const heldAgain = await client.step(toolName === "dap_step_into" ? "stepIn" : "next");
|
|
27177
|
+
const answer = heldAgain ? { stepped: true, heldAgain, stack: await client.getStackTrace() } : {
|
|
27178
|
+
stepped: true,
|
|
27179
|
+
heldAgain,
|
|
27180
|
+
note: `The game had not stopped again ${client.stepPatienceMs / 1000}s after the step: the step is still running, it went on without stopping anywhere, or the game was not held when it was asked. debug_state stack answers once it stops.`
|
|
27100
27181
|
};
|
|
27182
|
+
return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
|
|
27101
27183
|
}
|
|
27102
27184
|
case "dap_get_stack_trace": {
|
|
27103
27185
|
const stack = await client.getStackTrace();
|
|
@@ -27108,8 +27190,14 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27108
27190
|
case "dap_get_variables": {
|
|
27109
27191
|
const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
|
|
27110
27192
|
const scopes = await client.getScopes(frameId);
|
|
27193
|
+
const missing = scopes.filter((scope) => scope.variables === null).map((scope) => scope.name);
|
|
27194
|
+
const answer = missing.length === 0 ? { scopes } : {
|
|
27195
|
+
scopes,
|
|
27196
|
+
valuesMissing: missing,
|
|
27197
|
+
note: `The values of ${missing.join(", ")} did not arrive from the game within ${client.scopesPatienceMs / 1000}s, so they are unknown rather than empty. Ask again.`
|
|
27198
|
+
};
|
|
27111
27199
|
return {
|
|
27112
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
27200
|
+
content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
|
|
27113
27201
|
};
|
|
27114
27202
|
}
|
|
27115
27203
|
default:
|
|
@@ -27117,10 +27205,12 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27117
27205
|
}
|
|
27118
27206
|
} catch (error) {
|
|
27119
27207
|
const message = error instanceof Error ? error.message : String(error);
|
|
27120
|
-
const cure = toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
|
|
27208
|
+
const cure = error instanceof ReadRefusal ? "" : toolName === "dap_set_breakpoint" || toolName === "dap_remove_breakpoint" ? "Breakpoints need the editor open, not a running game: editor_status says whether one is connected, and the path is the script as the project spells it." : "The debug tools answer for a game the editor is playing: start one with editor_run.";
|
|
27121
27209
|
return {
|
|
27122
27210
|
isError: true,
|
|
27123
|
-
content: [
|
|
27211
|
+
content: [
|
|
27212
|
+
{ type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
|
|
27213
|
+
]
|
|
27124
27214
|
};
|
|
27125
27215
|
}
|
|
27126
27216
|
}
|
|
@@ -27482,6 +27572,47 @@ function linesBefore(entries, at, collapsed, wanted) {
|
|
|
27482
27572
|
return found.reverse();
|
|
27483
27573
|
}
|
|
27484
27574
|
|
|
27575
|
+
// src/engine-run.ts
|
|
27576
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
27577
|
+
function runEngine(executable, args, options = {}) {
|
|
27578
|
+
return new Promise((resolve) => {
|
|
27579
|
+
const log = new GameLog;
|
|
27580
|
+
let failure = null;
|
|
27581
|
+
const signal = callSignal();
|
|
27582
|
+
const child = spawn2(executable, [...args], {
|
|
27583
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27584
|
+
windowsHide: true,
|
|
27585
|
+
...signal === undefined ? {} : { signal }
|
|
27586
|
+
});
|
|
27587
|
+
child.stdout.on("data", (chunk) => {
|
|
27588
|
+
log.append("stdout", chunk);
|
|
27589
|
+
});
|
|
27590
|
+
child.stderr.on("data", (chunk) => {
|
|
27591
|
+
log.append("stderr", chunk);
|
|
27592
|
+
});
|
|
27593
|
+
const { timeout } = options;
|
|
27594
|
+
const timer = timeout === undefined ? undefined : setTimeout(() => {
|
|
27595
|
+
failure ??= `it ran past its ${timeout.said}`;
|
|
27596
|
+
child.kill();
|
|
27597
|
+
}, timeout.ms);
|
|
27598
|
+
child.on("error", (error) => {
|
|
27599
|
+
failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
|
|
27600
|
+
});
|
|
27601
|
+
child.on("close", (code, closedBy) => {
|
|
27602
|
+
clearTimeout(timer);
|
|
27603
|
+
log.finish();
|
|
27604
|
+
const started = child.pid !== undefined;
|
|
27605
|
+
resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
|
|
27606
|
+
});
|
|
27607
|
+
});
|
|
27608
|
+
}
|
|
27609
|
+
function howItEnded(run) {
|
|
27610
|
+
if (run.failure !== null) {
|
|
27611
|
+
return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
|
|
27612
|
+
}
|
|
27613
|
+
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
|
|
27614
|
+
}
|
|
27615
|
+
|
|
27485
27616
|
// src/godot-bridge.ts
|
|
27486
27617
|
import { randomUUID } from "node:crypto";
|
|
27487
27618
|
import { EventEmitter } from "node:events";
|
|
@@ -27519,7 +27650,10 @@ function mayYetConnect(listeningSince, now = Date.now()) {
|
|
|
27519
27650
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
27520
27651
|
}
|
|
27521
27652
|
function theEditorHasComeBack(status, startedAt) {
|
|
27522
|
-
return status
|
|
27653
|
+
return hasSaidWhoItIs(status) && (status.connectedAt?.getTime() ?? 0) > startedAt;
|
|
27654
|
+
}
|
|
27655
|
+
function hasSaidWhoItIs(status) {
|
|
27656
|
+
return status.connected && status.addonVersion !== undefined;
|
|
27523
27657
|
}
|
|
27524
27658
|
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
27525
27659
|
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
@@ -27669,6 +27803,7 @@ class GodotBridge extends EventEmitter {
|
|
|
27669
27803
|
dapPort: this.connectionInfo?.dapPort,
|
|
27670
27804
|
debugPort: this.connectionInfo?.debugPort,
|
|
27671
27805
|
openedByAServer: this.connectionInfo?.openedByAServer,
|
|
27806
|
+
startedByAnEditor: this.connectionInfo?.startedByAnEditor,
|
|
27672
27807
|
syncsBreakpoints: this.connectionInfo?.syncsBreakpoints,
|
|
27673
27808
|
pendingRequests: this.pendingRequests.size,
|
|
27674
27809
|
queuedResources: this.resourceQueues.size,
|
|
@@ -27814,6 +27949,7 @@ class GodotBridge extends EventEmitter {
|
|
|
27814
27949
|
this.connectionInfo.dapPort = servedPort(message.dap_port);
|
|
27815
27950
|
this.connectionInfo.debugPort = servedPort(message.debug_port);
|
|
27816
27951
|
this.connectionInfo.openedByAServer = message.opened_by_a_server === true;
|
|
27952
|
+
this.connectionInfo.startedByAnEditor = typeof message.started_by_an_editor === "boolean" ? message.started_by_an_editor : undefined;
|
|
27817
27953
|
this.connectionInfo.syncsBreakpoints = typeof message.syncs_breakpoints === "boolean" ? message.syncs_breakpoints : undefined;
|
|
27818
27954
|
this.log("info", `Godot ready: ${message.project_path}`);
|
|
27819
27955
|
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
@@ -28148,10 +28284,8 @@ class GodotLocator {
|
|
|
28148
28284
|
}
|
|
28149
28285
|
|
|
28150
28286
|
// src/headless.ts
|
|
28151
|
-
import {
|
|
28152
|
-
import {
|
|
28153
|
-
import { join as join13 } from "node:path";
|
|
28154
|
-
import { promisify as promisify3 } from "node:util";
|
|
28287
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, realpathSync as realpathSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
28288
|
+
import { dirname as dirname7, join as join13 } from "node:path";
|
|
28155
28289
|
|
|
28156
28290
|
// src/reimport.ts
|
|
28157
28291
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -28277,7 +28411,6 @@ function lastTryOnExit() {
|
|
|
28277
28411
|
}
|
|
28278
28412
|
|
|
28279
28413
|
// src/headless.ts
|
|
28280
|
-
var run3 = promisify3(execFile3);
|
|
28281
28414
|
function snakeCased(params) {
|
|
28282
28415
|
const result = emptyRecord();
|
|
28283
28416
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -28286,34 +28419,58 @@ function snakeCased(params) {
|
|
|
28286
28419
|
}
|
|
28287
28420
|
return result;
|
|
28288
28421
|
}
|
|
28289
|
-
function
|
|
28290
|
-
|
|
28291
|
-
|
|
28292
|
-
|
|
28293
|
-
|
|
28294
|
-
|
|
28295
|
-
return parsed;
|
|
28296
|
-
}
|
|
28297
|
-
} catch {}
|
|
28298
|
-
}
|
|
28299
|
-
return null;
|
|
28422
|
+
function engineExtras(outcome) {
|
|
28423
|
+
return {
|
|
28424
|
+
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
28425
|
+
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
28426
|
+
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
28427
|
+
};
|
|
28300
28428
|
}
|
|
28301
|
-
|
|
28302
|
-
|
|
28303
|
-
|
|
28304
|
-
|
|
28305
|
-
return log.
|
|
28429
|
+
var OWN_ERROR = "[ERROR] ";
|
|
28430
|
+
var MESSAGES_KEPT = 200;
|
|
28431
|
+
var ERRORS_NAMED = 3;
|
|
28432
|
+
function problemsIn(log) {
|
|
28433
|
+
return log.everything().flatMap((entry) => {
|
|
28434
|
+
if (entry.severity !== "info") {
|
|
28435
|
+
return [entry];
|
|
28436
|
+
}
|
|
28437
|
+
if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
|
|
28438
|
+
return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
|
|
28439
|
+
}
|
|
28440
|
+
return [];
|
|
28441
|
+
});
|
|
28306
28442
|
}
|
|
28307
|
-
function
|
|
28308
|
-
const
|
|
28443
|
+
function reported(problems) {
|
|
28444
|
+
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
28445
|
+
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
28446
|
+
}
|
|
28447
|
+
function reason(log, problems) {
|
|
28448
|
+
const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
28309
28449
|
if (own.length > 0) {
|
|
28310
|
-
return own.join("; ");
|
|
28311
|
-
}
|
|
28312
|
-
const
|
|
28313
|
-
if (
|
|
28314
|
-
|
|
28450
|
+
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
28451
|
+
}
|
|
28452
|
+
const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
28453
|
+
if (errors.length > 0) {
|
|
28454
|
+
const rest = errors.length - ERRORS_NAMED;
|
|
28455
|
+
return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
|
|
28456
|
+
}
|
|
28457
|
+
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
28458
|
+
return printed.at(-1)?.text ?? "no output at all";
|
|
28459
|
+
}
|
|
28460
|
+
function scriptFault(problems, script) {
|
|
28461
|
+
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
28462
|
+
const directories = [...new Set([spelled(dirname7(script)), spelled(resolvedOr(dirname7(script)))])];
|
|
28463
|
+
return problems.find((entry) => {
|
|
28464
|
+
const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
|
|
28465
|
+
return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
|
|
28466
|
+
});
|
|
28467
|
+
}
|
|
28468
|
+
function resolvedOr(path) {
|
|
28469
|
+
try {
|
|
28470
|
+
return realpathSync2.native(path);
|
|
28471
|
+
} catch {
|
|
28472
|
+
return path;
|
|
28315
28473
|
}
|
|
28316
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
28317
28474
|
}
|
|
28318
28475
|
function librariesNotCopied(messages) {
|
|
28319
28476
|
const found = new Set;
|
|
@@ -28353,30 +28510,33 @@ async function runImport(godotPath, projectPath, options = {}) {
|
|
|
28353
28510
|
}
|
|
28354
28511
|
async function importOnce(godotPath, projectPath) {
|
|
28355
28512
|
const logDir = scratchDirectory("import");
|
|
28513
|
+
let ran;
|
|
28356
28514
|
try {
|
|
28357
|
-
|
|
28358
|
-
|
|
28359
|
-
|
|
28360
|
-
|
|
28361
|
-
|
|
28362
|
-
|
|
28363
|
-
|
|
28364
|
-
|
|
28365
|
-
messages: problems(failed.stderr)
|
|
28366
|
-
};
|
|
28367
|
-
}
|
|
28368
|
-
return {
|
|
28369
|
-
ok: false,
|
|
28370
|
-
message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
28371
|
-
messages: []
|
|
28372
|
-
};
|
|
28515
|
+
ran = await runEngine(godotPath, [
|
|
28516
|
+
"--headless",
|
|
28517
|
+
"--log-file",
|
|
28518
|
+
join13(logDir, "engine.log"),
|
|
28519
|
+
"--path",
|
|
28520
|
+
projectPath,
|
|
28521
|
+
"--import"
|
|
28522
|
+
]);
|
|
28373
28523
|
} finally {
|
|
28374
28524
|
discard(logDir);
|
|
28375
28525
|
}
|
|
28526
|
+
const problems = problemsIn(ran.log);
|
|
28527
|
+
if (ran.exitCode === 0 && ran.failure === null) {
|
|
28528
|
+
return { ok: true, ...reported(problems) };
|
|
28529
|
+
}
|
|
28530
|
+
return {
|
|
28531
|
+
ok: false,
|
|
28532
|
+
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
28533
|
+
...reported(problems)
|
|
28534
|
+
};
|
|
28376
28535
|
}
|
|
28377
28536
|
async function runOperation(engine, operation, params, projectPath) {
|
|
28378
28537
|
const paramsDir = scratchDirectory("params");
|
|
28379
28538
|
const paramsFile = join13(paramsDir, `${operation}.json`);
|
|
28539
|
+
const answerFile = join13(paramsDir, "answer.json");
|
|
28380
28540
|
writeFileSync8(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
28381
28541
|
const args = [
|
|
28382
28542
|
"--headless",
|
|
@@ -28388,38 +28548,63 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
28388
28548
|
engine.script,
|
|
28389
28549
|
operation,
|
|
28390
28550
|
`@file:${paramsFile}`,
|
|
28551
|
+
answerFile,
|
|
28391
28552
|
...engine.debug ? ["--debug-godot"] : []
|
|
28392
28553
|
];
|
|
28393
|
-
let
|
|
28394
|
-
let
|
|
28554
|
+
let ran;
|
|
28555
|
+
let answer;
|
|
28395
28556
|
try {
|
|
28396
|
-
|
|
28397
|
-
|
|
28398
|
-
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
messages: problems(failed.stderr)
|
|
28404
|
-
};
|
|
28405
|
-
}
|
|
28557
|
+
ran = await runEngine(engine.godotPath, args);
|
|
28558
|
+
answer = answerIn(answerFile);
|
|
28559
|
+
} finally {
|
|
28560
|
+
discard(paramsDir);
|
|
28561
|
+
}
|
|
28562
|
+
const problems = problemsIn(ran.log);
|
|
28563
|
+
if (answer.kind === "unreadable") {
|
|
28406
28564
|
return {
|
|
28407
28565
|
ok: false,
|
|
28408
|
-
message: `${operation} could not be
|
|
28409
|
-
|
|
28566
|
+
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
28567
|
+
...reported(problems)
|
|
28568
|
+
};
|
|
28569
|
+
}
|
|
28570
|
+
if (answer.kind === "none") {
|
|
28571
|
+
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
28572
|
+
return {
|
|
28573
|
+
ok: false,
|
|
28574
|
+
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
28575
|
+
...reported(problems)
|
|
28410
28576
|
};
|
|
28411
|
-
} finally {
|
|
28412
|
-
discard(paramsDir);
|
|
28413
28577
|
}
|
|
28414
|
-
const
|
|
28415
|
-
if (
|
|
28578
|
+
const fault = scriptFault(problems, engine.script);
|
|
28579
|
+
if (fault !== undefined) {
|
|
28416
28580
|
return {
|
|
28417
28581
|
ok: false,
|
|
28418
|
-
message: `${operation}
|
|
28419
|
-
|
|
28582
|
+
message: `${operation} hit an error in the operations script, so its answer is left out: part of it was ` + `never computed. ${fault.text} (${fault.detail[0]?.slice("at: ".length) ?? "no location given"})`,
|
|
28583
|
+
...reported(problems)
|
|
28584
|
+
};
|
|
28585
|
+
}
|
|
28586
|
+
const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under engine_messages.`;
|
|
28587
|
+
return {
|
|
28588
|
+
ok: true,
|
|
28589
|
+
payload: answer.payload,
|
|
28590
|
+
...reported(problems),
|
|
28591
|
+
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
28592
|
+
};
|
|
28593
|
+
}
|
|
28594
|
+
function answerIn(file) {
|
|
28595
|
+
if (!existsSync10(file)) {
|
|
28596
|
+
return { kind: "none" };
|
|
28597
|
+
}
|
|
28598
|
+
const text = readFileSync12(file, "utf8");
|
|
28599
|
+
try {
|
|
28600
|
+
const parsed = JSON.parse(text);
|
|
28601
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? { kind: "answer", payload: parsed } : { kind: "unreadable", why: `it is JSON but not an object: ${text.slice(0, 200)}` };
|
|
28602
|
+
} catch (error) {
|
|
28603
|
+
return {
|
|
28604
|
+
kind: "unreadable",
|
|
28605
|
+
why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
|
|
28420
28606
|
};
|
|
28421
28607
|
}
|
|
28422
|
-
return { ok: true, payload, messages: problems(stderr) };
|
|
28423
28608
|
}
|
|
28424
28609
|
|
|
28425
28610
|
// src/headless-operations.ts
|
|
@@ -28620,13 +28805,18 @@ function parseJUnit(xml) {
|
|
|
28620
28805
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
28621
28806
|
const suites = suiteElements.map((suite) => {
|
|
28622
28807
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
28808
|
+
const failures = cases.filter((entry) => entry.status === "failed").length;
|
|
28809
|
+
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
28810
|
+
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
28623
28811
|
return {
|
|
28624
28812
|
name: suite.attributes["name"] ?? "",
|
|
28625
28813
|
path: pathOf(suite),
|
|
28626
28814
|
tests: cases.length,
|
|
28627
28815
|
discovered: count(suite.attributes, "tests"),
|
|
28628
|
-
failures
|
|
28629
|
-
errors
|
|
28816
|
+
failures,
|
|
28817
|
+
errors,
|
|
28818
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
28819
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
28630
28820
|
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
28631
28821
|
time: count(suite.attributes, "time"),
|
|
28632
28822
|
cases
|
|
@@ -28637,13 +28827,22 @@ function parseJUnit(xml) {
|
|
|
28637
28827
|
tests: all.length,
|
|
28638
28828
|
failures: all.filter((entry) => entry.status === "failed").length,
|
|
28639
28829
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
28830
|
+
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
28831
|
+
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
28640
28832
|
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
28641
28833
|
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
28642
28834
|
suites
|
|
28643
28835
|
};
|
|
28644
28836
|
}
|
|
28837
|
+
var HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
28838
|
+
function hookFailuresPrinted(printed) {
|
|
28839
|
+
return printed.flatMap((line) => {
|
|
28840
|
+
const found = HOOK_FAILURE.exec(line);
|
|
28841
|
+
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
28842
|
+
});
|
|
28843
|
+
}
|
|
28645
28844
|
var NOTHING_RAN = /no test cases found/i;
|
|
28646
|
-
var NO_SUCH_PATH = /given directory or file does not exists:\s*(\
|
|
28845
|
+
var NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
28647
28846
|
function whyNoReport(printed, asked) {
|
|
28648
28847
|
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
28649
28848
|
if (missing !== undefined) {
|
|
@@ -28730,13 +28929,17 @@ function withActualsPrinted(failed, printed) {
|
|
|
28730
28929
|
at: match[5] ?? "",
|
|
28731
28930
|
...readDiff(match[1] ?? "")
|
|
28732
28931
|
}));
|
|
28932
|
+
const used = new Set;
|
|
28733
28933
|
return failed.map((entry) => {
|
|
28734
28934
|
if (entry.detail === null) {
|
|
28735
28935
|
return entry;
|
|
28736
28936
|
}
|
|
28737
28937
|
let detail = entry.detail;
|
|
28738
28938
|
let recovered = false;
|
|
28739
|
-
for (const diff of diffs) {
|
|
28939
|
+
for (const [index, diff] of diffs.entries()) {
|
|
28940
|
+
if (used.has(index)) {
|
|
28941
|
+
continue;
|
|
28942
|
+
}
|
|
28740
28943
|
const wrong = ` but was
|
|
28741
28944
|
'${diff.merged.replaceAll("\r", "")}'${diff.suffix}
|
|
28742
28945
|
at ${diff.at}`;
|
|
@@ -28745,6 +28948,7 @@ function withActualsPrinted(failed, printed) {
|
|
|
28745
28948
|
'${diff.actual}'${diff.suffix}
|
|
28746
28949
|
at ${diff.at}`);
|
|
28747
28950
|
recovered = true;
|
|
28951
|
+
used.add(index);
|
|
28748
28952
|
}
|
|
28749
28953
|
}
|
|
28750
28954
|
const quoted = QUOTED_EQUALITY.exec(detail);
|
|
@@ -28768,7 +28972,7 @@ function masked(expected, actual) {
|
|
|
28768
28972
|
return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
|
|
28769
28973
|
}
|
|
28770
28974
|
var PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
28771
|
-
var RUNNING_SUITE = /Run Test Suite:\s*(
|
|
28975
|
+
var RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
28772
28976
|
var ORPHANS = /(\d+)\s+orphans/;
|
|
28773
28977
|
var OVERALL = "Overall Summary";
|
|
28774
28978
|
function orphansPrinted(printed) {
|
|
@@ -28803,13 +29007,24 @@ function orphansPrinted(printed) {
|
|
|
28803
29007
|
}
|
|
28804
29008
|
|
|
28805
29009
|
// src/lsp_client.ts
|
|
28806
|
-
import { realpathSync as
|
|
29010
|
+
import { existsSync as existsSync11, realpathSync as realpathSync3 } from "node:fs";
|
|
28807
29011
|
import { readFile, realpath } from "node:fs/promises";
|
|
28808
29012
|
import { createConnection as createConnection3 } from "node:net";
|
|
28809
|
-
import { dirname as
|
|
29013
|
+
import { dirname as dirname8, join as join14, resolve as resolve3 } from "node:path";
|
|
28810
29014
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28811
29015
|
var DEFAULT_LSP_PORT = 6005;
|
|
28812
29016
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
29017
|
+
|
|
29018
|
+
class ArgumentRefusal extends Refusal {
|
|
29019
|
+
}
|
|
29020
|
+
|
|
29021
|
+
class AnotherProjectsServer extends Refusal {
|
|
29022
|
+
serves;
|
|
29023
|
+
constructor(port, serves, asked) {
|
|
29024
|
+
super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
|
|
29025
|
+
this.serves = serves;
|
|
29026
|
+
}
|
|
29027
|
+
}
|
|
28813
29028
|
function diagnosticsKey(uri) {
|
|
28814
29029
|
let decoded;
|
|
28815
29030
|
try {
|
|
@@ -28821,7 +29036,7 @@ function diagnosticsKey(uri) {
|
|
|
28821
29036
|
return decoded;
|
|
28822
29037
|
}
|
|
28823
29038
|
try {
|
|
28824
|
-
return
|
|
29039
|
+
return realpathSync3.native(fileURLToPath(decoded)).toLowerCase();
|
|
28825
29040
|
} catch {
|
|
28826
29041
|
return decoded.toLowerCase();
|
|
28827
29042
|
}
|
|
@@ -28838,8 +29053,10 @@ class GodotLSPClient {
|
|
|
28838
29053
|
connectPromise = null;
|
|
28839
29054
|
initialized = false;
|
|
28840
29055
|
rootPath = null;
|
|
29056
|
+
servedWorkspace = null;
|
|
28841
29057
|
diagnosticsWaiters = new Map;
|
|
28842
29058
|
documentVersions = new Map;
|
|
29059
|
+
documentTurns = new Map;
|
|
28843
29060
|
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
|
|
28844
29061
|
this.port = port;
|
|
28845
29062
|
this.host = host;
|
|
@@ -28936,7 +29153,7 @@ class GodotLSPClient {
|
|
|
28936
29153
|
if (this.initialized) {
|
|
28937
29154
|
return;
|
|
28938
29155
|
}
|
|
28939
|
-
const rootPath = this.rootPath ??
|
|
29156
|
+
const rootPath = this.rootPath ?? dirname8(resolve3(filePath));
|
|
28940
29157
|
await this.initialize(rootPath);
|
|
28941
29158
|
}
|
|
28942
29159
|
async sendRequest(method, params) {
|
|
@@ -29011,6 +29228,10 @@ class GodotLSPClient {
|
|
|
29011
29228
|
}
|
|
29012
29229
|
}
|
|
29013
29230
|
}
|
|
29231
|
+
if (message["method"] === "gdscript_client/changeWorkspace") {
|
|
29232
|
+
const path = message["params"]?.["path"];
|
|
29233
|
+
this.servedWorkspace = typeof path === "string" ? path : null;
|
|
29234
|
+
}
|
|
29014
29235
|
if (message["method"] === "textDocument/publishDiagnostics") {
|
|
29015
29236
|
const params = message["params"];
|
|
29016
29237
|
const paramsObject = params && typeof params === "object" ? params : null;
|
|
@@ -29113,10 +29334,14 @@ class GodotLSPClient {
|
|
|
29113
29334
|
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
29114
29335
|
} catch {}
|
|
29115
29336
|
}
|
|
29337
|
+
forgetServedWorkspace() {
|
|
29338
|
+
this.servedWorkspace = null;
|
|
29339
|
+
}
|
|
29116
29340
|
async initialize(rootPath) {
|
|
29117
29341
|
await this.ensureConnected();
|
|
29118
29342
|
const resolvedRootPath = resolve3(rootPath);
|
|
29119
29343
|
const rootUri = pathToFileURL(resolvedRootPath).href;
|
|
29344
|
+
this.forgetServedWorkspace();
|
|
29120
29345
|
const result = await this.sendRequest("initialize", {
|
|
29121
29346
|
processId: process.pid,
|
|
29122
29347
|
rootPath: resolvedRootPath,
|
|
@@ -29142,60 +29367,82 @@ class GodotLSPClient {
|
|
|
29142
29367
|
}
|
|
29143
29368
|
]
|
|
29144
29369
|
});
|
|
29370
|
+
const served = this.servedWorkspace;
|
|
29371
|
+
if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
|
|
29372
|
+
throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
|
|
29373
|
+
}
|
|
29145
29374
|
this.sendNotification("initialized", {});
|
|
29146
29375
|
this.initialized = true;
|
|
29147
29376
|
this.rootPath = resolvedRootPath;
|
|
29148
29377
|
return result;
|
|
29149
29378
|
}
|
|
29150
|
-
async
|
|
29151
|
-
await this.ensureConnected();
|
|
29152
|
-
await this.ensureInitializedForFile(filePath);
|
|
29379
|
+
async withDocument(filePath, work) {
|
|
29153
29380
|
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
29154
|
-
const
|
|
29155
|
-
|
|
29156
|
-
|
|
29157
|
-
|
|
29158
|
-
|
|
29159
|
-
|
|
29160
|
-
|
|
29161
|
-
|
|
29162
|
-
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
29163
|
-
this.diagnosticsWaiters.set(key, {
|
|
29164
|
-
resolve: resolveDiagnostics,
|
|
29165
|
-
reject: rejectDiagnostics,
|
|
29166
|
-
timer
|
|
29167
|
-
});
|
|
29168
|
-
});
|
|
29169
|
-
let opened = null;
|
|
29381
|
+
const before = this.documentTurns.get(key) ?? Promise.resolve();
|
|
29382
|
+
let done = () => {};
|
|
29383
|
+
const mine = new Promise((finished) => {
|
|
29384
|
+
done = finished;
|
|
29385
|
+
});
|
|
29386
|
+
const turn = before.then(() => mine);
|
|
29387
|
+
this.documentTurns.set(key, turn);
|
|
29388
|
+
await before;
|
|
29170
29389
|
try {
|
|
29171
|
-
|
|
29172
|
-
return await diagnosticsPromise;
|
|
29173
|
-
} catch (error) {
|
|
29174
|
-
const waiter = this.diagnosticsWaiters.get(key);
|
|
29175
|
-
if (waiter) {
|
|
29176
|
-
clearTimeout(waiter.timer);
|
|
29177
|
-
this.diagnosticsWaiters.delete(key);
|
|
29178
|
-
}
|
|
29179
|
-
throw error;
|
|
29390
|
+
return await work();
|
|
29180
29391
|
} finally {
|
|
29181
|
-
|
|
29182
|
-
|
|
29392
|
+
done();
|
|
29393
|
+
if (this.documentTurns.get(key) === turn) {
|
|
29394
|
+
this.documentTurns.delete(key);
|
|
29183
29395
|
}
|
|
29184
29396
|
}
|
|
29185
29397
|
}
|
|
29186
|
-
async
|
|
29398
|
+
async getDiagnostics(filePath, content) {
|
|
29187
29399
|
await this.ensureConnected();
|
|
29188
29400
|
await this.ensureInitializedForFile(filePath);
|
|
29189
|
-
|
|
29190
|
-
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29194
|
-
|
|
29401
|
+
return this.withDocument(filePath, async () => {
|
|
29402
|
+
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
29403
|
+
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
29404
|
+
const timer = setTimeout(() => {
|
|
29405
|
+
this.diagnosticsWaiters.delete(key);
|
|
29406
|
+
rejectDiagnostics(new Error(`Godot published no diagnostics for ${key} within ${DIAGNOSTICS_TIMEOUT_MS}ms. ` + "The language server may not be running, or may not have this file in its workspace."));
|
|
29407
|
+
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
29408
|
+
this.diagnosticsWaiters.set(key, {
|
|
29409
|
+
resolve: resolveDiagnostics,
|
|
29410
|
+
reject: rejectDiagnostics,
|
|
29411
|
+
timer
|
|
29412
|
+
});
|
|
29195
29413
|
});
|
|
29196
|
-
|
|
29197
|
-
|
|
29198
|
-
|
|
29414
|
+
let opened = null;
|
|
29415
|
+
try {
|
|
29416
|
+
opened = this.syncDocument(filePath, content);
|
|
29417
|
+
return await diagnosticsPromise;
|
|
29418
|
+
} catch (error) {
|
|
29419
|
+
const waiter = this.diagnosticsWaiters.get(key);
|
|
29420
|
+
if (waiter) {
|
|
29421
|
+
clearTimeout(waiter.timer);
|
|
29422
|
+
this.diagnosticsWaiters.delete(key);
|
|
29423
|
+
}
|
|
29424
|
+
throw error;
|
|
29425
|
+
} finally {
|
|
29426
|
+
if (opened !== null) {
|
|
29427
|
+
this.closeDocument(opened);
|
|
29428
|
+
}
|
|
29429
|
+
}
|
|
29430
|
+
});
|
|
29431
|
+
}
|
|
29432
|
+
async getCompletions(filePath, content, line, character) {
|
|
29433
|
+
await this.ensureConnected();
|
|
29434
|
+
await this.ensureInitializedForFile(filePath);
|
|
29435
|
+
const result = await this.withDocument(filePath, async () => {
|
|
29436
|
+
const uri = this.syncDocument(filePath, content);
|
|
29437
|
+
try {
|
|
29438
|
+
return await this.sendRequest("textDocument/completion", {
|
|
29439
|
+
textDocument: { uri },
|
|
29440
|
+
position: { line, character }
|
|
29441
|
+
});
|
|
29442
|
+
} finally {
|
|
29443
|
+
this.closeDocument(uri);
|
|
29444
|
+
}
|
|
29445
|
+
});
|
|
29199
29446
|
if (Array.isArray(result)) {
|
|
29200
29447
|
return result;
|
|
29201
29448
|
}
|
|
@@ -29210,28 +29457,31 @@ class GodotLSPClient {
|
|
|
29210
29457
|
async getHover(filePath, content, line, character) {
|
|
29211
29458
|
await this.ensureConnected();
|
|
29212
29459
|
await this.ensureInitializedForFile(filePath);
|
|
29213
|
-
|
|
29214
|
-
|
|
29215
|
-
|
|
29216
|
-
textDocument
|
|
29217
|
-
|
|
29218
|
-
|
|
29219
|
-
|
|
29220
|
-
|
|
29221
|
-
|
|
29460
|
+
return this.withDocument(filePath, async () => {
|
|
29461
|
+
const uri = this.syncDocument(filePath, content);
|
|
29462
|
+
try {
|
|
29463
|
+
return await this.sendRequest("textDocument/hover", {
|
|
29464
|
+
textDocument: { uri },
|
|
29465
|
+
position: { line, character }
|
|
29466
|
+
});
|
|
29467
|
+
} finally {
|
|
29468
|
+
this.closeDocument(uri);
|
|
29469
|
+
}
|
|
29470
|
+
});
|
|
29222
29471
|
}
|
|
29223
29472
|
async getDocumentSymbols(filePath, content) {
|
|
29224
29473
|
await this.ensureConnected();
|
|
29225
29474
|
await this.ensureInitializedForFile(filePath);
|
|
29226
|
-
const
|
|
29227
|
-
|
|
29228
|
-
|
|
29229
|
-
|
|
29230
|
-
|
|
29231
|
-
|
|
29232
|
-
|
|
29233
|
-
|
|
29234
|
-
|
|
29475
|
+
const result = await this.withDocument(filePath, async () => {
|
|
29476
|
+
const uri = this.syncDocument(filePath, content);
|
|
29477
|
+
try {
|
|
29478
|
+
return await this.sendRequest("textDocument/documentSymbol", {
|
|
29479
|
+
textDocument: { uri }
|
|
29480
|
+
});
|
|
29481
|
+
} finally {
|
|
29482
|
+
this.closeDocument(uri);
|
|
29483
|
+
}
|
|
29484
|
+
});
|
|
29235
29485
|
return Array.isArray(result) ? result : [];
|
|
29236
29486
|
}
|
|
29237
29487
|
isConnected() {
|
|
@@ -29263,36 +29513,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
29263
29513
|
try {
|
|
29264
29514
|
projectPath = await realpath(requestedProjectPath);
|
|
29265
29515
|
} catch {
|
|
29266
|
-
throw new
|
|
29516
|
+
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
29517
|
+
}
|
|
29518
|
+
if (!existsSync11(join14(projectPath, "project.godot"))) {
|
|
29519
|
+
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
29267
29520
|
}
|
|
29268
29521
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
29269
29522
|
if (!contained.ok) {
|
|
29270
|
-
throw new
|
|
29523
|
+
throw new ArgumentRefusal(contained.reason);
|
|
29271
29524
|
}
|
|
29272
29525
|
let scriptPath;
|
|
29273
29526
|
try {
|
|
29274
29527
|
scriptPath = await realpath(contained.absolutePath);
|
|
29275
29528
|
} catch {
|
|
29276
|
-
throw new
|
|
29529
|
+
throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
|
|
29277
29530
|
}
|
|
29278
29531
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
29279
|
-
throw new
|
|
29532
|
+
throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
|
|
29280
29533
|
}
|
|
29281
29534
|
return { projectPath, scriptPath };
|
|
29282
29535
|
}
|
|
29283
29536
|
async function handleLSPTool(client, toolName, args) {
|
|
29284
29537
|
try {
|
|
29285
29538
|
if (!args || typeof args !== "object") {
|
|
29286
|
-
throw new
|
|
29539
|
+
throw new ArgumentRefusal("Tool arguments must be an object.");
|
|
29287
29540
|
}
|
|
29288
29541
|
const parsedArgs = args;
|
|
29289
29542
|
const projectPathValue = parsedArgs["projectPath"];
|
|
29290
29543
|
const scriptPathValue = parsedArgs["scriptPath"];
|
|
29291
29544
|
if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
|
|
29292
|
-
throw new
|
|
29545
|
+
throw new ArgumentRefusal("Missing required argument: projectPath");
|
|
29293
29546
|
}
|
|
29294
29547
|
if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
|
|
29295
|
-
throw new
|
|
29548
|
+
throw new ArgumentRefusal("Missing required argument: scriptPath");
|
|
29296
29549
|
}
|
|
29297
29550
|
const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
|
|
29298
29551
|
const content = await readFile(scriptPath, "utf8");
|
|
@@ -29306,7 +29559,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
29306
29559
|
const line = Number(parsedArgs["line"]);
|
|
29307
29560
|
const character = Number(parsedArgs["character"]);
|
|
29308
29561
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
29309
|
-
throw new
|
|
29562
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
29310
29563
|
}
|
|
29311
29564
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
29312
29565
|
return asToolResponse({ completions });
|
|
@@ -29315,7 +29568,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
29315
29568
|
const line = Number(parsedArgs["line"]);
|
|
29316
29569
|
const character = Number(parsedArgs["character"]);
|
|
29317
29570
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
29318
|
-
throw new
|
|
29571
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
29319
29572
|
}
|
|
29320
29573
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
29321
29574
|
return asToolResponse({ hover });
|
|
@@ -29329,13 +29582,15 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
29329
29582
|
}
|
|
29330
29583
|
} catch (error) {
|
|
29331
29584
|
return asToolResponse({
|
|
29332
|
-
error: normalizeLSPError(error, client.port)
|
|
29585
|
+
error: normalizeLSPError(error, client.port),
|
|
29586
|
+
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
29587
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
29333
29588
|
});
|
|
29334
29589
|
}
|
|
29335
29590
|
}
|
|
29336
29591
|
|
|
29337
29592
|
// src/outside.ts
|
|
29338
|
-
import { spawn as
|
|
29593
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
29339
29594
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29340
29595
|
function changesFrom(env, base) {
|
|
29341
29596
|
const changes = {};
|
|
@@ -29369,7 +29624,7 @@ function readLaunched(printed) {
|
|
|
29369
29624
|
}
|
|
29370
29625
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
29371
29626
|
return await new Promise((resolve) => {
|
|
29372
|
-
const launcher =
|
|
29627
|
+
const launcher = spawn3(runtime, [keeper, "launch"], {
|
|
29373
29628
|
stdio: ["pipe", "pipe", "pipe"],
|
|
29374
29629
|
windowsHide: true
|
|
29375
29630
|
});
|
|
@@ -29400,9 +29655,9 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
29400
29655
|
}
|
|
29401
29656
|
|
|
29402
29657
|
// src/process-time.ts
|
|
29403
|
-
import { execFile as
|
|
29404
|
-
import { promisify as
|
|
29405
|
-
var
|
|
29658
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
29659
|
+
import { promisify as promisify3 } from "node:util";
|
|
29660
|
+
var run3 = promisify3(execFile3);
|
|
29406
29661
|
var ASK_TIMEOUT_MS2 = 6000;
|
|
29407
29662
|
async function cpuSecondsOf(pid) {
|
|
29408
29663
|
try {
|
|
@@ -29417,7 +29672,7 @@ async function secondsOnWindows(pid) {
|
|
|
29417
29672
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
29418
29673
|
}
|
|
29419
29674
|
async function askPosix2(pid) {
|
|
29420
|
-
const { stdout } = await
|
|
29675
|
+
const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
29421
29676
|
return secondsFromClock(stdout.trim());
|
|
29422
29677
|
}
|
|
29423
29678
|
function secondsFromClock(printed) {
|
|
@@ -29437,16 +29692,46 @@ function secondsFromClock(printed) {
|
|
|
29437
29692
|
}
|
|
29438
29693
|
|
|
29439
29694
|
// src/project-scan.ts
|
|
29440
|
-
import { existsSync as
|
|
29441
|
-
import { join as
|
|
29695
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
29696
|
+
import { join as join15 } from "node:path";
|
|
29442
29697
|
var SKIPPED = new Set(["node_modules"]);
|
|
29443
29698
|
function skipped(name) {
|
|
29444
29699
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
29445
29700
|
}
|
|
29446
29701
|
function steppedOver(directory) {
|
|
29447
|
-
return
|
|
29448
|
-
}
|
|
29449
|
-
var
|
|
29702
|
+
return existsSync12(join15(directory, ".gdignore"));
|
|
29703
|
+
}
|
|
29704
|
+
var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
|
|
29705
|
+
var ASSET_EXTENSIONS = new Set([
|
|
29706
|
+
"png",
|
|
29707
|
+
"jpg",
|
|
29708
|
+
"jpeg",
|
|
29709
|
+
"webp",
|
|
29710
|
+
"svg",
|
|
29711
|
+
"bmp",
|
|
29712
|
+
"tga",
|
|
29713
|
+
"exr",
|
|
29714
|
+
"hdr",
|
|
29715
|
+
"ktx",
|
|
29716
|
+
"dds",
|
|
29717
|
+
"ttf",
|
|
29718
|
+
"otf",
|
|
29719
|
+
"woff",
|
|
29720
|
+
"woff2",
|
|
29721
|
+
"fnt",
|
|
29722
|
+
"wav",
|
|
29723
|
+
"mp3",
|
|
29724
|
+
"ogg",
|
|
29725
|
+
"glb",
|
|
29726
|
+
"gltf",
|
|
29727
|
+
"obj",
|
|
29728
|
+
"fbx",
|
|
29729
|
+
"blend",
|
|
29730
|
+
"dae",
|
|
29731
|
+
"ogv",
|
|
29732
|
+
"gdshader",
|
|
29733
|
+
"shader"
|
|
29734
|
+
]);
|
|
29450
29735
|
function projectStructure(projectPath) {
|
|
29451
29736
|
const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
|
|
29452
29737
|
const visit = (directory) => {
|
|
@@ -29458,10 +29743,10 @@ function projectStructure(projectPath) {
|
|
|
29458
29743
|
continue;
|
|
29459
29744
|
}
|
|
29460
29745
|
if (entry.isDirectory()) {
|
|
29461
|
-
visit(
|
|
29746
|
+
visit(join15(directory, entry.name));
|
|
29462
29747
|
} else if (entry.isFile()) {
|
|
29463
29748
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29464
|
-
if (extension
|
|
29749
|
+
if (SCENE_EXTENSIONS.has(extension)) {
|
|
29465
29750
|
structure.scenes += 1;
|
|
29466
29751
|
} else if (extension === "gd" || extension === "cs") {
|
|
29467
29752
|
structure.scripts += 1;
|
|
@@ -29489,14 +29774,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
29489
29774
|
}
|
|
29490
29775
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
29491
29776
|
if (entry.isDirectory()) {
|
|
29492
|
-
visit(
|
|
29777
|
+
visit(join15(directory, entry.name), spelled);
|
|
29493
29778
|
continue;
|
|
29494
29779
|
}
|
|
29495
29780
|
if (!entry.isFile()) {
|
|
29496
29781
|
continue;
|
|
29497
29782
|
}
|
|
29498
29783
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29499
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
29784
|
+
if (CARRIES_A_UID.has(extension) && !existsSync12(join15(directory, `${entry.name}.uid`))) {
|
|
29500
29785
|
found.push(spelled);
|
|
29501
29786
|
}
|
|
29502
29787
|
}
|
|
@@ -29504,8 +29789,11 @@ function scriptsWithoutUid(projectPath) {
|
|
|
29504
29789
|
visit(projectPath, "");
|
|
29505
29790
|
return found.sort();
|
|
29506
29791
|
}
|
|
29792
|
+
function isBinary(bytes) {
|
|
29793
|
+
return bytes.subarray(0, 8000).includes(0);
|
|
29794
|
+
}
|
|
29507
29795
|
function searchProject(projectPath, options) {
|
|
29508
|
-
const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
29796
|
+
const extensions = options.fileTypes === undefined ? null : new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
|
|
29509
29797
|
const result = {
|
|
29510
29798
|
query: options.query,
|
|
29511
29799
|
results: [],
|
|
@@ -29513,13 +29801,7 @@ function searchProject(projectPath, options) {
|
|
|
29513
29801
|
};
|
|
29514
29802
|
const regex = options.regex ? new RegExp(options.query, options.caseSensitive ? "" : "i") : null;
|
|
29515
29803
|
const needle = options.caseSensitive ? options.query : options.query.toLowerCase();
|
|
29516
|
-
const full = () =>
|
|
29517
|
-
if (result.summary.total_matches >= options.maxResults) {
|
|
29518
|
-
result.summary.truncated = true;
|
|
29519
|
-
return true;
|
|
29520
|
-
}
|
|
29521
|
-
return false;
|
|
29522
|
-
};
|
|
29804
|
+
const full = () => result.summary.truncated;
|
|
29523
29805
|
const visit = (directory) => {
|
|
29524
29806
|
if (steppedOver(directory)) {
|
|
29525
29807
|
return;
|
|
@@ -29531,27 +29813,32 @@ function searchProject(projectPath, options) {
|
|
|
29531
29813
|
if (skipped(entry.name)) {
|
|
29532
29814
|
continue;
|
|
29533
29815
|
}
|
|
29534
|
-
const entryPath =
|
|
29816
|
+
const entryPath = join15(directory, entry.name);
|
|
29535
29817
|
if (entry.isDirectory()) {
|
|
29536
29818
|
visit(entryPath);
|
|
29537
29819
|
continue;
|
|
29538
29820
|
}
|
|
29539
29821
|
const extension = entry.name.includes(".") ? entry.name.split(".").pop()?.toLowerCase() ?? "" : "";
|
|
29540
|
-
if (!entry.isFile() || !extensions.has(extension)) {
|
|
29822
|
+
if (!entry.isFile() || extensions !== null && !extensions.has(extension)) {
|
|
29823
|
+
continue;
|
|
29824
|
+
}
|
|
29825
|
+
const bytes = readFileSync13(entryPath);
|
|
29826
|
+
if (extensions === null && isBinary(bytes)) {
|
|
29541
29827
|
continue;
|
|
29542
29828
|
}
|
|
29543
29829
|
result.summary.files_searched += 1;
|
|
29544
29830
|
const matches = [];
|
|
29545
|
-
for (const [index, line] of
|
|
29546
|
-
|
|
29547
|
-
if (
|
|
29548
|
-
|
|
29831
|
+
for (const [index, line] of bytes.toString("utf8").split(/\r?\n/).entries()) {
|
|
29832
|
+
const found = regex ? regex.exec(line) : (options.caseSensitive ? line : line.toLowerCase()).includes(needle) ? [options.query] : null;
|
|
29833
|
+
if (found === null) {
|
|
29834
|
+
continue;
|
|
29549
29835
|
}
|
|
29550
|
-
|
|
29551
|
-
|
|
29552
|
-
|
|
29553
|
-
result.summary.total_matches += 1;
|
|
29836
|
+
if (result.summary.total_matches >= options.maxResults) {
|
|
29837
|
+
result.summary.truncated = true;
|
|
29838
|
+
break;
|
|
29554
29839
|
}
|
|
29840
|
+
matches.push({ line: index + 1, content: line.trim(), match: found[0] ?? "" });
|
|
29841
|
+
result.summary.total_matches += 1;
|
|
29555
29842
|
}
|
|
29556
29843
|
if (matches.length > 0) {
|
|
29557
29844
|
const relativePath = entryPath.slice(projectPath.length + 1).replace(/\\/g, "/");
|
|
@@ -29565,7 +29852,7 @@ function searchProject(projectPath, options) {
|
|
|
29565
29852
|
}
|
|
29566
29853
|
|
|
29567
29854
|
// src/resources.ts
|
|
29568
|
-
import { readFileSync as
|
|
29855
|
+
import { readFileSync as readFileSync14, realpathSync as realpathSync4 } from "node:fs";
|
|
29569
29856
|
import { extname, resolve as resolve4 } from "node:path";
|
|
29570
29857
|
var STATIC_RESOURCES = [
|
|
29571
29858
|
{
|
|
@@ -29599,7 +29886,7 @@ var RESOURCE_COUNT = STATIC_RESOURCES.length + RESOURCE_TEMPLATES.length;
|
|
|
29599
29886
|
function ensureProjectPath(getProjectPath) {
|
|
29600
29887
|
const projectPath = getProjectPath();
|
|
29601
29888
|
if (!projectPath) {
|
|
29602
|
-
throw new Refusal("
|
|
29889
|
+
throw new Refusal("This server knows no project to read from: start it with GDHARNESS_PROJECT, connect an editor, or make any tool call naming a projectPath that holds project.godot.");
|
|
29603
29890
|
}
|
|
29604
29891
|
return resolve4(projectPath);
|
|
29605
29892
|
}
|
|
@@ -29615,7 +29902,22 @@ function resolveProjectFile(projectPath, resourcePath) {
|
|
|
29615
29902
|
if (!contained.ok) {
|
|
29616
29903
|
throw new Refusal(contained.reason);
|
|
29617
29904
|
}
|
|
29618
|
-
|
|
29905
|
+
let root;
|
|
29906
|
+
try {
|
|
29907
|
+
root = realpathSync4.native(projectPath);
|
|
29908
|
+
} catch {
|
|
29909
|
+
throw new Refusal(`The project directory ${projectPath} is not there.`);
|
|
29910
|
+
}
|
|
29911
|
+
let real;
|
|
29912
|
+
try {
|
|
29913
|
+
real = realpathSync4.native(contained.absolutePath);
|
|
29914
|
+
} catch {
|
|
29915
|
+
throw new Refusal(`No file at ${resourcePath} in the project.`);
|
|
29916
|
+
}
|
|
29917
|
+
if (!isWithinRoot(root, real)) {
|
|
29918
|
+
throw new Refusal(`${resourcePath} leads outside the project through a link, so it is not read.`);
|
|
29919
|
+
}
|
|
29920
|
+
return real;
|
|
29619
29921
|
}
|
|
29620
29922
|
function parseGodotUri(uri) {
|
|
29621
29923
|
if (uri === "godot://project/info") {
|
|
@@ -29760,7 +30062,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29760
30062
|
const parsedUri = parseGodotUri(uri);
|
|
29761
30063
|
if (parsedUri.kind === "project-info") {
|
|
29762
30064
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
29763
|
-
const rawProject =
|
|
30065
|
+
const rawProject = readFileSync14(projectFilePath, "utf-8");
|
|
29764
30066
|
const parsedProject = parseProjectGodot(rawProject);
|
|
29765
30067
|
return {
|
|
29766
30068
|
mimeType: "application/json",
|
|
@@ -29769,7 +30071,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29769
30071
|
}
|
|
29770
30072
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
29771
30073
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
29772
|
-
const text =
|
|
30074
|
+
const text = readFileSync14(filePath, "utf-8");
|
|
29773
30075
|
return {
|
|
29774
30076
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
29775
30077
|
text
|
|
@@ -29806,15 +30108,15 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
29806
30108
|
}
|
|
29807
30109
|
|
|
29808
30110
|
// src/restart-note.ts
|
|
29809
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
29810
|
-
import { dirname as
|
|
30111
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
30112
|
+
import { dirname as dirname9, join as join16 } from "node:path";
|
|
29811
30113
|
function restartNotePath(projectPath) {
|
|
29812
|
-
return
|
|
30114
|
+
return join16(projectPath, ".godot", "gdharness-restart.json");
|
|
29813
30115
|
}
|
|
29814
30116
|
function noteRestartBegun(note) {
|
|
29815
30117
|
const path = restartNotePath(note.projectPath);
|
|
29816
30118
|
try {
|
|
29817
|
-
mkdirSync7(
|
|
30119
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
29818
30120
|
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
29819
30121
|
`, "utf8");
|
|
29820
30122
|
} catch (error) {
|
|
@@ -29823,7 +30125,7 @@ function noteRestartBegun(note) {
|
|
|
29823
30125
|
}
|
|
29824
30126
|
function restartOwed(projectPath) {
|
|
29825
30127
|
try {
|
|
29826
|
-
const parsed = JSON.parse(
|
|
30128
|
+
const parsed = JSON.parse(readFileSync15(restartNotePath(projectPath), "utf8"));
|
|
29827
30129
|
if (typeof parsed !== "object" || parsed === null) {
|
|
29828
30130
|
return null;
|
|
29829
30131
|
}
|
|
@@ -29845,32 +30147,32 @@ function restartSettled(projectPath) {
|
|
|
29845
30147
|
}
|
|
29846
30148
|
|
|
29847
30149
|
// src/run-record.ts
|
|
29848
|
-
import { execFile as
|
|
30150
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
29849
30151
|
import { createHash as createHash3 } from "node:crypto";
|
|
29850
30152
|
import {
|
|
29851
30153
|
mkdirSync as mkdirSync8,
|
|
29852
30154
|
openSync as openSync2,
|
|
29853
30155
|
readdirSync as readdirSync6,
|
|
29854
|
-
readFileSync as
|
|
29855
|
-
realpathSync as
|
|
30156
|
+
readFileSync as readFileSync16,
|
|
30157
|
+
realpathSync as realpathSync5,
|
|
29856
30158
|
rmSync as rmSync7,
|
|
29857
30159
|
statSync as statSync6,
|
|
29858
30160
|
writeFileSync as writeFileSync10
|
|
29859
30161
|
} from "node:fs";
|
|
29860
|
-
import { basename as basename3, dirname as
|
|
29861
|
-
import { promisify as
|
|
29862
|
-
var execFileAsync =
|
|
30162
|
+
import { basename as basename3, dirname as dirname10, join as join17, resolve as resolve5 } from "node:path";
|
|
30163
|
+
import { promisify as promisify4 } from "node:util";
|
|
30164
|
+
var execFileAsync = promisify4(execFile4);
|
|
29863
30165
|
function runsDirectory() {
|
|
29864
|
-
return
|
|
30166
|
+
return join17(runtimeDirectory(), "runs");
|
|
29865
30167
|
}
|
|
29866
30168
|
function realAsFarAsItExists(path) {
|
|
29867
30169
|
const missing = [];
|
|
29868
30170
|
let at = path;
|
|
29869
30171
|
for (;; ) {
|
|
29870
30172
|
try {
|
|
29871
|
-
return
|
|
30173
|
+
return join17(realpathSync5.native(at), ...missing);
|
|
29872
30174
|
} catch {
|
|
29873
|
-
const parent =
|
|
30175
|
+
const parent = dirname10(at);
|
|
29874
30176
|
if (parent === at) {
|
|
29875
30177
|
return path;
|
|
29876
30178
|
}
|
|
@@ -29885,16 +30187,19 @@ function projectKey(projectPath) {
|
|
|
29885
30187
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29886
30188
|
}
|
|
29887
30189
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29888
|
-
return
|
|
30190
|
+
return join17(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29889
30191
|
}
|
|
29890
30192
|
var SHARED_NOTE = "run.json";
|
|
29891
30193
|
function openTranscript(startedAt) {
|
|
29892
30194
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29893
|
-
const path =
|
|
30195
|
+
const path = join17(runsDirectory(), `run-${startedAt}.log`);
|
|
29894
30196
|
return { path, fd: openSync2(path, "a") };
|
|
29895
30197
|
}
|
|
30198
|
+
function noteIsAbout(note, playing) {
|
|
30199
|
+
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
30200
|
+
}
|
|
29896
30201
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29897
|
-
return
|
|
30202
|
+
return join17(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29898
30203
|
}
|
|
29899
30204
|
function writeEditorRunNote(note) {
|
|
29900
30205
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
@@ -29902,7 +30207,7 @@ function writeEditorRunNote(note) {
|
|
|
29902
30207
|
}
|
|
29903
30208
|
function readEditorRunNote(projectPath) {
|
|
29904
30209
|
for (const directory of runtimeDirectories()) {
|
|
29905
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
30210
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join17(directory, "runs", "editor-run.json"));
|
|
29906
30211
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29907
30212
|
return note;
|
|
29908
30213
|
}
|
|
@@ -29912,7 +30217,7 @@ function readEditorRunNote(projectPath) {
|
|
|
29912
30217
|
function editorNoteAt(path) {
|
|
29913
30218
|
let parsed;
|
|
29914
30219
|
try {
|
|
29915
|
-
parsed = JSON.parse(
|
|
30220
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
29916
30221
|
} catch {
|
|
29917
30222
|
return null;
|
|
29918
30223
|
}
|
|
@@ -29926,7 +30231,9 @@ function editorNoteAt(path) {
|
|
|
29926
30231
|
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
29927
30232
|
return null;
|
|
29928
30233
|
}
|
|
29929
|
-
|
|
30234
|
+
const play = fields["play"];
|
|
30235
|
+
const which = typeof play === "object" && play !== null && typeof play["play"] === "number" && typeof play["editorPid"] === "number" ? play : undefined;
|
|
30236
|
+
return { projectPath, transcript, startedAt, ...which === undefined ? {} : { play: which } };
|
|
29930
30237
|
}
|
|
29931
30238
|
function readRunRecord(projectPath) {
|
|
29932
30239
|
for (const directory of runtimeDirectories()) {
|
|
@@ -29934,7 +30241,7 @@ function readRunRecord(projectPath) {
|
|
|
29934
30241
|
if (own !== null) {
|
|
29935
30242
|
return own;
|
|
29936
30243
|
}
|
|
29937
|
-
const shared = recordAt(
|
|
30244
|
+
const shared = recordAt(join17(directory, "runs", SHARED_NOTE));
|
|
29938
30245
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29939
30246
|
return shared;
|
|
29940
30247
|
}
|
|
@@ -29946,13 +30253,13 @@ function everyRunRecord() {
|
|
|
29946
30253
|
for (const directory of runtimeDirectories()) {
|
|
29947
30254
|
let entries;
|
|
29948
30255
|
try {
|
|
29949
|
-
entries = readdirSync6(
|
|
30256
|
+
entries = readdirSync6(join17(directory, "runs"));
|
|
29950
30257
|
} catch {
|
|
29951
30258
|
continue;
|
|
29952
30259
|
}
|
|
29953
30260
|
for (const entry of entries) {
|
|
29954
30261
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29955
|
-
const record = recordAt(
|
|
30262
|
+
const record = recordAt(join17(directory, "runs", entry));
|
|
29956
30263
|
if (record !== null) {
|
|
29957
30264
|
found.push(record);
|
|
29958
30265
|
}
|
|
@@ -29964,7 +30271,7 @@ function everyRunRecord() {
|
|
|
29964
30271
|
function recordAt(path) {
|
|
29965
30272
|
let parsed;
|
|
29966
30273
|
try {
|
|
29967
|
-
parsed = JSON.parse(
|
|
30274
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
29968
30275
|
} catch {
|
|
29969
30276
|
return null;
|
|
29970
30277
|
}
|
|
@@ -29983,7 +30290,9 @@ function recordAt(path) {
|
|
|
29983
30290
|
pid,
|
|
29984
30291
|
transcript,
|
|
29985
30292
|
startedAt,
|
|
30293
|
+
...typeof fields["startedBy"] === "number" ? { startedBy: fields["startedBy"] } : {},
|
|
29986
30294
|
projectPath: typeof fields["projectPath"] === "string" ? fields["projectPath"] : "",
|
|
30295
|
+
...typeof fields["servedBy"] === "string" && fields["servedBy"] !== "" ? { servedBy: fields["servedBy"] } : {},
|
|
29987
30296
|
arguments: Array.isArray(args) ? args.filter((value) => typeof value === "string") : [],
|
|
29988
30297
|
...typeof fields["command"] === "string" ? { command: fields["command"] } : {},
|
|
29989
30298
|
...typeof fields["exitCode"] === "number" ? { exitCode: fields["exitCode"] } : {},
|
|
@@ -29992,7 +30301,8 @@ function recordAt(path) {
|
|
|
29992
30301
|
}
|
|
29993
30302
|
async function runningAs(pid) {
|
|
29994
30303
|
if (process.platform === "win32") {
|
|
29995
|
-
const
|
|
30304
|
+
const read = await readWindowsCommandLine(pid);
|
|
30305
|
+
const answer = "answer" in read ? read.answer : null;
|
|
29996
30306
|
if (answer !== null) {
|
|
29997
30307
|
const [, ...rest] = answer.split(`
|
|
29998
30308
|
`);
|
|
@@ -30004,13 +30314,17 @@ async function runningAs(pid) {
|
|
|
30004
30314
|
}
|
|
30005
30315
|
}
|
|
30006
30316
|
const image = await windowsImage(pid);
|
|
30007
|
-
return image === null ? null : {
|
|
30317
|
+
return image === null ? null : {
|
|
30318
|
+
kind: "image",
|
|
30319
|
+
text: image,
|
|
30320
|
+
unread: "unread" in read ? read.unread : "Windows gave an empty command line"
|
|
30321
|
+
};
|
|
30008
30322
|
}
|
|
30009
30323
|
try {
|
|
30010
30324
|
const began = await startedAt(pid, null);
|
|
30011
30325
|
const when = began === null ? {} : { startedAt: began };
|
|
30012
30326
|
if (process.platform === "linux") {
|
|
30013
|
-
const raw =
|
|
30327
|
+
const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
30014
30328
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
30015
30329
|
}
|
|
30016
30330
|
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
@@ -30055,13 +30369,18 @@ function listeningPidInNetstat(printed, port) {
|
|
|
30055
30369
|
}
|
|
30056
30370
|
return null;
|
|
30057
30371
|
}
|
|
30058
|
-
async function
|
|
30059
|
-
|
|
30060
|
-
|
|
30061
|
-
|
|
30062
|
-
|
|
30063
|
-
|
|
30372
|
+
async function readWindowsCommandLine(pid, ask = askWindows) {
|
|
30373
|
+
const query = `$p = Get-CimInstance Win32_Process -Filter "ProcessId=${pid}"; if ($p) { $p.CreationDate.ToUniversalTime().ToString("o"); $p.CommandLine }`;
|
|
30374
|
+
let unread = "";
|
|
30375
|
+
for (const withinMs of [15000, 45000]) {
|
|
30376
|
+
try {
|
|
30377
|
+
const answer = (await ask(query, withinMs)).trim();
|
|
30378
|
+
return answer === "" ? { unread: "Windows gave no command line for it" } : { answer };
|
|
30379
|
+
} catch (error) {
|
|
30380
|
+
unread = errorMessage(error);
|
|
30381
|
+
}
|
|
30064
30382
|
}
|
|
30383
|
+
return { unread };
|
|
30065
30384
|
}
|
|
30066
30385
|
async function startedAt(pid, windowsAnswer) {
|
|
30067
30386
|
if (process.platform === "win32") {
|
|
@@ -30089,37 +30408,44 @@ async function windowsImage(pid) {
|
|
|
30089
30408
|
return null;
|
|
30090
30409
|
}
|
|
30091
30410
|
}
|
|
30092
|
-
async function
|
|
30093
|
-
return
|
|
30411
|
+
async function whyNotStillTheRecordedRun(record) {
|
|
30412
|
+
return whyNotTheRun(record, await runningAs(record.pid), "confirmed");
|
|
30094
30413
|
}
|
|
30095
30414
|
async function couldStillBeTheRecordedRun(record) {
|
|
30096
30415
|
return judgeRun(record, await runningAs(record.pid), "possible");
|
|
30097
30416
|
}
|
|
30098
30417
|
var SAME_RUN_WINDOW_MS = 1e4;
|
|
30099
30418
|
function judgeRun(record, running, needed) {
|
|
30419
|
+
return whyNotTheRun(record, running, needed) === null;
|
|
30420
|
+
}
|
|
30421
|
+
function whyNotTheRun(record, running, needed) {
|
|
30100
30422
|
if (running === null) {
|
|
30101
|
-
return
|
|
30423
|
+
return "the operating system would not describe that process";
|
|
30102
30424
|
}
|
|
30103
|
-
|
|
30104
|
-
|
|
30425
|
+
const after = running.startedAt === undefined ? 0 : running.startedAt - (record.startedBy ?? record.startedAt);
|
|
30426
|
+
if (after > SAME_RUN_WINDOW_MS) {
|
|
30427
|
+
return `that process started ${Math.round(after / 1000)} seconds after the run had`;
|
|
30105
30428
|
}
|
|
30106
30429
|
const engine = record.command === undefined ? null : basename3(record.command);
|
|
30107
30430
|
const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
|
|
30108
30431
|
const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
|
|
30109
30432
|
if (wanted !== null && !said.includes(wanted)) {
|
|
30110
|
-
return
|
|
30433
|
+
return `that process is not ${engine ?? "the engine"}, which the run was started with`;
|
|
30111
30434
|
}
|
|
30112
30435
|
if (running.kind === "commandLine" && readCommandLine(running.text).editor) {
|
|
30113
|
-
return
|
|
30436
|
+
return "that process is an editor";
|
|
30114
30437
|
}
|
|
30115
30438
|
if (record.projectPath === "") {
|
|
30116
|
-
return wanted !== null;
|
|
30439
|
+
return wanted !== null ? null : "the record names neither the project nor the engine";
|
|
30117
30440
|
}
|
|
30118
30441
|
if (running.kind === "image") {
|
|
30119
|
-
|
|
30442
|
+
if (wanted === null) {
|
|
30443
|
+
return "the operating system named only its executable, and the record names no engine";
|
|
30444
|
+
}
|
|
30445
|
+
return needed === "possible" ? null : `the operating system named only its executable and not its command line (${running.unread ?? "no reason given"}), which is not enough to signal it on`;
|
|
30120
30446
|
}
|
|
30121
30447
|
const project = process.platform === "win32" ? record.projectPath.toLowerCase() : record.projectPath;
|
|
30122
|
-
return said.includes(project)
|
|
30448
|
+
return said.includes(project) ? null : `that process's command line does not name ${record.projectPath}`;
|
|
30123
30449
|
}
|
|
30124
30450
|
async function stillTheAnnouncedGame(pid, projectPath, announcedAt) {
|
|
30125
30451
|
return judgeAnnouncedGame(await runningAs(pid), projectPath, announcedAt);
|
|
@@ -30139,7 +30465,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
30139
30465
|
}
|
|
30140
30466
|
function clearRunRecord(projectPath) {
|
|
30141
30467
|
for (const directory of runtimeDirectories()) {
|
|
30142
|
-
const shared =
|
|
30468
|
+
const shared = join17(directory, "runs", SHARED_NOTE);
|
|
30143
30469
|
const sharedRecord = recordAt(shared);
|
|
30144
30470
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
30145
30471
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -30169,7 +30495,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
30169
30495
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
30170
30496
|
continue;
|
|
30171
30497
|
}
|
|
30172
|
-
const path =
|
|
30498
|
+
const path = join17(directory, entry);
|
|
30173
30499
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
30174
30500
|
continue;
|
|
30175
30501
|
}
|
|
@@ -30180,19 +30506,20 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
30180
30506
|
}
|
|
30181
30507
|
|
|
30182
30508
|
// src/setup.ts
|
|
30509
|
+
import { spawnSync } from "node:child_process";
|
|
30183
30510
|
import { createHash as createHash4 } from "node:crypto";
|
|
30184
30511
|
import {
|
|
30185
30512
|
cpSync,
|
|
30186
|
-
existsSync as
|
|
30513
|
+
existsSync as existsSync13,
|
|
30187
30514
|
mkdirSync as mkdirSync9,
|
|
30188
30515
|
readdirSync as readdirSync7,
|
|
30189
|
-
readFileSync as
|
|
30516
|
+
readFileSync as readFileSync17,
|
|
30190
30517
|
rmdirSync,
|
|
30191
30518
|
rmSync as rmSync8,
|
|
30192
30519
|
statSync as statSync7,
|
|
30193
30520
|
writeFileSync as writeFileSync11
|
|
30194
30521
|
} from "node:fs";
|
|
30195
|
-
import { dirname as
|
|
30522
|
+
import { dirname as dirname11, join as join18 } from "node:path";
|
|
30196
30523
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
30197
30524
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
30198
30525
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -30209,21 +30536,21 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
30209
30536
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
30210
30537
|
continue;
|
|
30211
30538
|
}
|
|
30212
|
-
const path =
|
|
30539
|
+
const path = join18(directory, name);
|
|
30213
30540
|
const inside = `${relative}/${name}`;
|
|
30214
30541
|
if (statSync7(path).isDirectory()) {
|
|
30215
30542
|
walk(path, inside);
|
|
30216
30543
|
} else {
|
|
30217
30544
|
hash.update(`${inside}
|
|
30218
30545
|
`);
|
|
30219
|
-
hash.update(
|
|
30546
|
+
hash.update(readFileSync17(path));
|
|
30220
30547
|
hash.update(`
|
|
30221
30548
|
`);
|
|
30222
30549
|
}
|
|
30223
30550
|
}
|
|
30224
30551
|
};
|
|
30225
30552
|
for (const name of EDITOR_PLUGINS) {
|
|
30226
|
-
walk(
|
|
30553
|
+
walk(join18(from, name), name);
|
|
30227
30554
|
}
|
|
30228
30555
|
return hash.digest("hex");
|
|
30229
30556
|
}
|
|
@@ -30238,17 +30565,132 @@ function shippedEditorDigest() {
|
|
|
30238
30565
|
}
|
|
30239
30566
|
return shippedDigest ?? undefined;
|
|
30240
30567
|
}
|
|
30568
|
+
function installedEditorDigest(projectPath) {
|
|
30569
|
+
const marker = join18(projectPath, "addons", ADDONS[0], DIGEST_MARKER);
|
|
30570
|
+
if (!existsSync13(marker)) {
|
|
30571
|
+
return null;
|
|
30572
|
+
}
|
|
30573
|
+
const said = readFileSync17(marker, "utf8").trim();
|
|
30574
|
+
return said === "" ? null : said;
|
|
30575
|
+
}
|
|
30241
30576
|
function shippedAddonsDirectory() {
|
|
30242
|
-
return
|
|
30577
|
+
return join18(dirname11(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
30243
30578
|
}
|
|
30244
30579
|
function installedAddonVersion(projectPath) {
|
|
30245
|
-
const marker =
|
|
30246
|
-
if (!
|
|
30580
|
+
const marker = join18(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
30581
|
+
if (!existsSync13(marker)) {
|
|
30247
30582
|
return null;
|
|
30248
30583
|
}
|
|
30249
|
-
const said =
|
|
30584
|
+
const said = readFileSync17(marker, "utf8").trim();
|
|
30250
30585
|
return said === "" ? null : said;
|
|
30251
30586
|
}
|
|
30587
|
+
function loaderAmong(projectPath, named) {
|
|
30588
|
+
let byName = null;
|
|
30589
|
+
for (const [entry, path] of named) {
|
|
30590
|
+
if (entry === RUNTIME_AUTOLOAD.name || path === RUNTIME_AUTOLOAD.path) {
|
|
30591
|
+
continue;
|
|
30592
|
+
}
|
|
30593
|
+
let source = null;
|
|
30594
|
+
try {
|
|
30595
|
+
source = readFileSync17(join18(projectPath, path), "utf8");
|
|
30596
|
+
} catch {}
|
|
30597
|
+
if (source?.includes(RUNTIME_AUTOLOAD.path)) {
|
|
30598
|
+
return { name: entry, path, exists: true };
|
|
30599
|
+
}
|
|
30600
|
+
byName ??= path.toLowerCase().includes("gdharness") ? { name: entry, path, exists: source !== null } : null;
|
|
30601
|
+
}
|
|
30602
|
+
return byName;
|
|
30603
|
+
}
|
|
30604
|
+
function autoloadPaths(settings) {
|
|
30605
|
+
const named = new Map;
|
|
30606
|
+
for (const [name, value] of Object.entries(settings["autoload"] ?? {})) {
|
|
30607
|
+
const said = typeof value === "string" ? value.replace(/^\*/, "") : "";
|
|
30608
|
+
if (said.startsWith("res://")) {
|
|
30609
|
+
named.set(name, said.slice("res://".length));
|
|
30610
|
+
}
|
|
30611
|
+
}
|
|
30612
|
+
return named;
|
|
30613
|
+
}
|
|
30614
|
+
function trackedByGit(projectPath, wanted) {
|
|
30615
|
+
if (wanted.length === 0) {
|
|
30616
|
+
return new Set;
|
|
30617
|
+
}
|
|
30618
|
+
const asked = spawnSync("git", ["ls-files", "-z", "--", ...wanted], {
|
|
30619
|
+
cwd: projectPath,
|
|
30620
|
+
encoding: "utf8"
|
|
30621
|
+
});
|
|
30622
|
+
if (asked.status !== 0) {
|
|
30623
|
+
return null;
|
|
30624
|
+
}
|
|
30625
|
+
return new Set(asked.stdout.split("\x00").filter(Boolean));
|
|
30626
|
+
}
|
|
30627
|
+
function enabledPlugins(settings) {
|
|
30628
|
+
const raw = settings["editor_plugins"]?.["enabled"];
|
|
30629
|
+
if (typeof raw !== "string") {
|
|
30630
|
+
return [];
|
|
30631
|
+
}
|
|
30632
|
+
return [...raw.matchAll(/res:\/\/addons\/([^/"]+)\/plugin\.cfg/g)].map((match) => match[1] ?? "").filter(Boolean);
|
|
30633
|
+
}
|
|
30634
|
+
function inspectProject(projectPath) {
|
|
30635
|
+
const problems = [];
|
|
30636
|
+
const addons = ADDONS.map((name) => {
|
|
30637
|
+
const directory = join18(projectPath, "addons", name);
|
|
30638
|
+
const marker = join18(directory, VERSION_MARKER);
|
|
30639
|
+
const installed = existsSync13(directory);
|
|
30640
|
+
const version = existsSync13(marker) ? readFileSync17(marker, "utf8").trim() : null;
|
|
30641
|
+
const current = version === SERVER_VERSION;
|
|
30642
|
+
if (!installed) {
|
|
30643
|
+
problems.push(`addons/${name} is not installed; run gdharness setup`);
|
|
30644
|
+
} else if (!current) {
|
|
30645
|
+
problems.push(`addons/${name} is ${version ?? "of an unknown version"}, this gdharness is ${SERVER_VERSION}; run gdharness setup`);
|
|
30646
|
+
}
|
|
30647
|
+
return { name, installed, version, current };
|
|
30648
|
+
});
|
|
30649
|
+
const settings = parseProjectGodot(readFileSync17(join18(projectPath, "project.godot"), "utf8"));
|
|
30650
|
+
const pluginsEnabled = enabledPlugins(settings);
|
|
30651
|
+
for (const name of EDITOR_PLUGINS) {
|
|
30652
|
+
if (!pluginsEnabled.includes(name)) {
|
|
30653
|
+
problems.push(`the ${name} plugin is not enabled in project.godot; run gdharness setup`);
|
|
30654
|
+
}
|
|
30655
|
+
}
|
|
30656
|
+
const autoload = settings["autoload"];
|
|
30657
|
+
const registered = autoload === undefined ? undefined : readString(autoload, RUNTIME_AUTOLOAD.name);
|
|
30658
|
+
const runtimeAutoloadPath = typeof registered === "string" ? registered.replace(/^\*/, "") : null;
|
|
30659
|
+
const named = autoloadPaths(settings);
|
|
30660
|
+
const runtimeLoaderAutoload = loaderAmong(projectPath, named);
|
|
30661
|
+
const runtimeAutoload = runtimeAutoloadPath !== null || runtimeLoaderAutoload !== null;
|
|
30662
|
+
const tracked = trackedByGit(projectPath, [...named.values()]);
|
|
30663
|
+
const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
|
|
30664
|
+
for (const [name, path] of named) {
|
|
30665
|
+
if (!existsSync13(join18(projectPath, path))) {
|
|
30666
|
+
if (!addonMissing(path)) {
|
|
30667
|
+
problems.push(`the ${name} autoload names res://${path}, which is not in this project, so it boots with a missing script; restore that file, or point the autoload at one that is here`);
|
|
30668
|
+
}
|
|
30669
|
+
continue;
|
|
30670
|
+
}
|
|
30671
|
+
if (tracked !== null && !tracked.has(path)) {
|
|
30672
|
+
problems.push(`the ${name} autoload names res://${path}, which git does not carry, so a clone boots with a missing script; commit that file, or point the autoload at one the project tracks`);
|
|
30673
|
+
}
|
|
30674
|
+
}
|
|
30675
|
+
const cached = cachedClasses(projectPath);
|
|
30676
|
+
const staleClasses = cached === null ? [] : staleAgainst(cached, projectPath);
|
|
30677
|
+
if (cached === null) {
|
|
30678
|
+
problems.push("no .godot/global_script_class_cache.cfg: the engine knows no class_name; run project_import refresh_classes or open the editor");
|
|
30679
|
+
} else if (staleClasses.length > 0) {
|
|
30680
|
+
problems.push(`the class cache does not list ${staleClasses.join(", ")}; run project_import refresh_classes or gdharness classes`);
|
|
30681
|
+
}
|
|
30682
|
+
return {
|
|
30683
|
+
projectPath,
|
|
30684
|
+
addons,
|
|
30685
|
+
pluginsEnabled,
|
|
30686
|
+
runtimeAutoload,
|
|
30687
|
+
runtimeAutoloadPath,
|
|
30688
|
+
runtimeLoaderAutoload,
|
|
30689
|
+
staleClasses,
|
|
30690
|
+
classCacheExists: cached !== null,
|
|
30691
|
+
problems
|
|
30692
|
+
};
|
|
30693
|
+
}
|
|
30252
30694
|
|
|
30253
30695
|
// src/tool-definitions.ts
|
|
30254
30696
|
var PROJECT_INFO_SECTIONS = [
|
|
@@ -30299,7 +30741,7 @@ var NODE_PATH = {
|
|
|
30299
30741
|
};
|
|
30300
30742
|
var PROPERTIES = {
|
|
30301
30743
|
type: "object",
|
|
30302
|
-
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another.',
|
|
30744
|
+
description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
|
|
30303
30745
|
additionalProperties: true
|
|
30304
30746
|
};
|
|
30305
30747
|
var XY = {
|
|
@@ -30454,11 +30896,18 @@ var TOOL_SPECS = [
|
|
|
30454
30896
|
},
|
|
30455
30897
|
value: {
|
|
30456
30898
|
blank: true,
|
|
30457
|
-
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
30899
|
+
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}, and infinity as {"_type": "float", "value": "inf"}, the form a read answers with. "" writes an empty string.'
|
|
30458
30900
|
},
|
|
30459
30901
|
name: { type: "string", description: "Autoload name." },
|
|
30460
30902
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
30461
|
-
enabled: {
|
|
30903
|
+
enabled: {
|
|
30904
|
+
type: "boolean",
|
|
30905
|
+
description: "Autoloads: Godot 4 loads every autoload in the list, so false is refused. Use global: false to keep the name out of the global scope, or remove_autoload to stop it loading."
|
|
30906
|
+
},
|
|
30907
|
+
global: {
|
|
30908
|
+
type: "boolean",
|
|
30909
|
+
description: `Autoloads: make the name a global variable, the * in project.godot and the editor's "Global Variable" column. Default true. The autoload loads either way.`
|
|
30910
|
+
},
|
|
30462
30911
|
scenePath: SCENE_PATH,
|
|
30463
30912
|
actionName: { type: "string", description: 'Input action name, such as "jump".' },
|
|
30464
30913
|
events: INPUT_EVENTS,
|
|
@@ -30470,9 +30919,15 @@ var TOOL_SPECS = [
|
|
|
30470
30919
|
},
|
|
30471
30920
|
pluginName: { type: "string", description: "Folder name under addons/." },
|
|
30472
30921
|
busName: { type: "string", description: "Audio bus name." },
|
|
30473
|
-
parentBusIndex: {
|
|
30922
|
+
parentBusIndex: {
|
|
30923
|
+
type: "number",
|
|
30924
|
+
description: "Audio buses: the bus to send to. Default 0, Master. The new bus goes right after it, so every later bus moves one along, and the answer lists them all."
|
|
30925
|
+
},
|
|
30474
30926
|
busIndex: { type: "number", description: "Audio bus index." },
|
|
30475
|
-
effectIndex: {
|
|
30927
|
+
effectIndex: {
|
|
30928
|
+
type: "number",
|
|
30929
|
+
description: "Slot on the bus for the effect: the effect there is replaced, and one past the last adds one. Any other slot is refused."
|
|
30930
|
+
},
|
|
30476
30931
|
effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
|
|
30477
30932
|
volumeDb: { type: "number", description: "Bus volume in decibels." }
|
|
30478
30933
|
},
|
|
@@ -30491,7 +30946,7 @@ var TOOL_SPECS = [
|
|
|
30491
30946
|
disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
|
|
30492
30947
|
add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
|
|
30493
30948
|
set_audio_bus_effect: {
|
|
30494
|
-
summary: "
|
|
30949
|
+
summary: "put an effect in a slot on a bus",
|
|
30495
30950
|
requires: ["busIndex", "effectIndex", "effectType"]
|
|
30496
30951
|
},
|
|
30497
30952
|
set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
|
|
@@ -30530,7 +30985,11 @@ var TOOL_SPECS = [
|
|
|
30530
30985
|
type: "boolean",
|
|
30531
30986
|
description: "forward: include the engine's own res://. resources. Default false."
|
|
30532
30987
|
},
|
|
30533
|
-
fileTypes: {
|
|
30988
|
+
fileTypes: {
|
|
30989
|
+
type: "array",
|
|
30990
|
+
items: { type: "string" },
|
|
30991
|
+
description: 'reverse: extensions to look in, plus "godot" for project.godot, "cfg" for plugin.cfg and export_presets.cfg, and "import" for import sidecars. Default all of tscn, tres, gd, gdshader, godot, cfg and import, since an autoload, the main scene and a plugin script are named only in the settings files.'
|
|
30992
|
+
}
|
|
30534
30993
|
},
|
|
30535
30994
|
requires: ["projectPath", "resourcePath"]
|
|
30536
30995
|
},
|
|
@@ -30575,7 +31034,10 @@ var TOOL_SPECS = [
|
|
|
30575
31034
|
summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. Answered from a second status read: reimported lists what is current now, notReimported what is not, with its status and reason, and via says which of the two did the work. stillImporting is an editor that had not finished within timeoutMs",
|
|
30576
31035
|
requires: []
|
|
30577
31036
|
},
|
|
30578
|
-
uid: {
|
|
31037
|
+
uid: {
|
|
31038
|
+
summary: "the UID of one file and where it was read: a script's or shader's .uid file, an imported file's .import, or a scene's or resource's own header",
|
|
31039
|
+
requires: ["resourcePath"]
|
|
31040
|
+
},
|
|
30579
31041
|
refresh_uids: {
|
|
30580
31042
|
summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
|
|
30581
31043
|
requires: []
|
|
@@ -30636,11 +31098,11 @@ var TOOL_SPECS = [
|
|
|
30636
31098
|
},
|
|
30637
31099
|
{
|
|
30638
31100
|
name: "scene_create",
|
|
30639
|
-
description: "Creates a scene file, saves one, or saves a copy under a new path. Needs the editor connected.",
|
|
31101
|
+
description: "Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. Needs the editor connected.",
|
|
30640
31102
|
parameters: {
|
|
30641
31103
|
projectPath: PROJECT_PATH,
|
|
30642
31104
|
scenePath: SCENE_PATH,
|
|
30643
|
-
rootNodeType: { type: "string", description: "create: the root node class. Default
|
|
31105
|
+
rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
|
|
30644
31106
|
newPath: { type: "string", description: "save_as: where the copy goes." }
|
|
30645
31107
|
},
|
|
30646
31108
|
requires: ["projectPath", "scenePath"],
|
|
@@ -30664,7 +31126,7 @@ var TOOL_SPECS = [
|
|
|
30664
31126
|
},
|
|
30665
31127
|
{
|
|
30666
31128
|
name: "scene_node",
|
|
30667
|
-
description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. Needs the editor connected.",
|
|
31129
|
+
description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and for a node that belongs to a scene this one instances or inherits. Needs the editor connected.",
|
|
30668
31130
|
parameters: {
|
|
30669
31131
|
projectPath: PROJECT_PATH,
|
|
30670
31132
|
scenePath: SCENE_PATH,
|
|
@@ -30682,7 +31144,10 @@ var TOOL_SPECS = [
|
|
|
30682
31144
|
type: "boolean",
|
|
30683
31145
|
description: "get: include properties still at their default. Default false."
|
|
30684
31146
|
},
|
|
30685
|
-
layer: {
|
|
31147
|
+
layer: {
|
|
31148
|
+
type: "number",
|
|
31149
|
+
description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
|
|
31150
|
+
},
|
|
30686
31151
|
cells: TILEMAP_CELLS
|
|
30687
31152
|
},
|
|
30688
31153
|
requires: ["projectPath", "scenePath"],
|
|
@@ -30693,7 +31158,10 @@ var TOOL_SPECS = [
|
|
|
30693
31158
|
duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
|
|
30694
31159
|
reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
|
|
30695
31160
|
delete: { summary: "remove a node and its children", requires: ["nodePath"] },
|
|
30696
|
-
set_tilemap_cells: {
|
|
31161
|
+
set_tilemap_cells: {
|
|
31162
|
+
summary: "place tiles in a TileMapLayer or TileMap",
|
|
31163
|
+
requires: ["nodePath", "cells"]
|
|
31164
|
+
}
|
|
30697
31165
|
}
|
|
30698
31166
|
},
|
|
30699
31167
|
{
|
|
@@ -30781,13 +31249,16 @@ var TOOL_SPECS = [
|
|
|
30781
31249
|
},
|
|
30782
31250
|
{
|
|
30783
31251
|
name: "script_edit",
|
|
30784
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type.
|
|
31252
|
+
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type. Both load what they wrote under the project's own warning settings and answer with parses; the engine's reasons for a refusal come back under engine_messages. create answers with the extends and class_name the engine read, and a script declaring a class_name is put in the project's class list, with registered saying whether it is there. modify makes every addition or none, and answers with the line each one is on in the file written.",
|
|
30785
31253
|
parameters: {
|
|
30786
31254
|
projectPath: PROJECT_PATH,
|
|
30787
31255
|
scriptPath: SCRIPT_PATH,
|
|
30788
|
-
className: { type: "string", description: "create: a class_name for the script." },
|
|
30789
|
-
extends: { type: "string", description: "create: the base class. Default Node." },
|
|
30790
|
-
content: {
|
|
31256
|
+
className: { type: "string", description: "create: a class_name for the script. Not with content." },
|
|
31257
|
+
extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
|
|
31258
|
+
content: {
|
|
31259
|
+
type: "string",
|
|
31260
|
+
description: "create: the whole file, written as given, instead of a template, className and extends."
|
|
31261
|
+
},
|
|
30791
31262
|
template: {
|
|
30792
31263
|
type: "string",
|
|
30793
31264
|
enum: ["singleton", "state_machine", "component", "resource"],
|
|
@@ -30878,13 +31349,13 @@ var TOOL_SPECS = [
|
|
|
30878
31349
|
},
|
|
30879
31350
|
{
|
|
30880
31351
|
name: "editor_launch",
|
|
30881
|
-
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
|
|
31352
|
+
description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, visible unless hidden: true is passed again, 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. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart. An open is refused when the editor it would start could never reach this server: a project other than the one this server serves, whose 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.",
|
|
30882
31353
|
parameters: {
|
|
30883
31354
|
projectPath: { ...PROJECT_PATH, ops: ["open"] },
|
|
30884
31355
|
hidden: {
|
|
30885
31356
|
type: "boolean",
|
|
30886
31357
|
ops: ["open", "restart"],
|
|
30887
|
-
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus."
|
|
31358
|
+
description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus. On restart only for an editor a server opened; one opened by hand comes back as it was, so hidden is refused there."
|
|
30888
31359
|
}
|
|
30889
31360
|
},
|
|
30890
31361
|
requires: [],
|
|
@@ -30972,7 +31443,7 @@ var TOOL_SPECS = [
|
|
|
30972
31443
|
requires: ["projectPath"]
|
|
30973
31444
|
},
|
|
30974
31445
|
stop: {
|
|
30975
|
-
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled",
|
|
31446
|
+
summary: "end the run and answer with what it printed last, naming the process ended under endedPid: for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. A game that started processes of its own keeps them unless andChildren says to end those too. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, and false with the number under stillGoing when what was told to end was still running ten seconds later",
|
|
30976
31447
|
requires: []
|
|
30977
31448
|
},
|
|
30978
31449
|
check: {
|
|
@@ -30985,7 +31456,7 @@ var TOOL_SPECS = [
|
|
|
30985
31456
|
},
|
|
30986
31457
|
{
|
|
30987
31458
|
name: "editor_output",
|
|
30988
|
-
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
31459
|
+
description: "What the project started by editor_run has printed, as entries with a severity: the errors and warnings the engine reported, each with where it happened, and everything else as info. For a run the editor plays the reports come from the game itself, through the runtime addon, since the editor's debugger relays what a game prints and not what it reports; a played game without the runtime addon answers here with its prints alone, with errorsUnread true and no clean, since what it raised reaches nothing here. Answers with the counts and the verdict as well as the entries. heldAt is three answers: null for a run this server knows is not held, running or over, an object for one it knows is held at a breakpoint, whether the adapter said so or the runtime did by accepting a connection and never answering, and absent with heldUnknown beside it for a server that connected to the debugger after the stop and has no runtime to ask, since the adapter reports a stop only to the sessions connected when it happened. A run that has quit still answers here, with running false and its exit code, until the next one starts; a run ended by a signal has no exit code, and exitSignal names the signal instead. A run this server started also answers with transcript, the file both its streams are written to: uncapped, written while the run is going, and the thing to read or tail for a long run rather than the engine's own log, which every engine start rotates away. Read the file for output and this answer for state: endedBy says whether gdharness ended the run, and a transcript that has stopped growing is a run between prints rather than a run that is over. running is asked of the operating system whenever there is a process to ask about, which is every run this server started and every editor-played run whose game announced its runtime. For an editor-played run that never announced, there is no process id here and running is the editor's answer, which can lag: one measured elsewhere reported a game as playing for fifteen seconds after its process had been ended from outside. editor_status names that game under runtimes, which does not depend on the editor answering. op: \"editor\" answers about the editor itself instead, which is a different log and the only way anything here can read what an editor printed: its console reaches no plugin, so the addon cannot scrape it, and an editor gdharness opened is asked for a log file as it starts. That covers the whole session from the editor's first line, which is where the output worth reading usually is, since the class cache, the plugins and the language server all say what they have to say before any harness has connected. The answer carries the entries in the order they were printed and, beside them under repeated, each message shape several lines share: how many, what varied in each slot of it and how many different values stood there, the first occurrence, and the ordinary lines above the group, found by walking back past the group rather than by counting lines. An editor somebody opened by hand has no console here and is refused saying so rather than answered empty, because an editor that printed nothing and an editor nobody captured are not the same thing.",
|
|
30989
31460
|
parameters: {
|
|
30990
31461
|
severity: {
|
|
30991
31462
|
type: "string",
|
|
@@ -31033,7 +31504,7 @@ var TOOL_SPECS = [
|
|
|
31033
31504
|
},
|
|
31034
31505
|
{
|
|
31035
31506
|
name: "editor_status",
|
|
31036
|
-
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, 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. 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. 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. 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. 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 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. 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. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, 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. 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.",
|
|
31507
|
+
description: "Whether the editor addon is connected, which Godot answers, whether the editor is playing something, and whether a game with the runtime addon is reachable: runtimeConnected counts the games of the project this server serves, 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. 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. 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. 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. 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 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. settingsDropped appears here once when the editor this server opened saved project.godot on its way in, 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. 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.",
|
|
31037
31508
|
parameters: {},
|
|
31038
31509
|
requires: []
|
|
31039
31510
|
},
|
|
@@ -31049,7 +31520,7 @@ var TOOL_SPECS = [
|
|
|
31049
31520
|
},
|
|
31050
31521
|
reloadScript: {
|
|
31051
31522
|
type: "string",
|
|
31052
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before."
|
|
31523
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before. A @tool script is not reloaded either, since it runs inside the editor and reloading one from inside a call can take the editor down: reloadProblem says so, and editor_launch restart recompiles it. A path spelled in another case than the file is reloaded under the spelling on disk, which is the one the editor holds its copy under, and that spelling comes back under reloadedAs."
|
|
31053
31524
|
}
|
|
31054
31525
|
},
|
|
31055
31526
|
requires: ["projectPath"]
|
|
@@ -31072,6 +31543,7 @@ var TOOL_SPECS = [
|
|
|
31072
31543
|
"physics2d",
|
|
31073
31544
|
"physics3d",
|
|
31074
31545
|
"audio",
|
|
31546
|
+
"visual",
|
|
31075
31547
|
"animation",
|
|
31076
31548
|
"ui"
|
|
31077
31549
|
],
|
|
@@ -31111,7 +31583,7 @@ var TOOL_SPECS = [
|
|
|
31111
31583
|
property: {
|
|
31112
31584
|
type: "string",
|
|
31113
31585
|
ops: ["property", "find"],
|
|
31114
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
31586
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
31115
31587
|
},
|
|
31116
31588
|
depth: {
|
|
31117
31589
|
type: "integer",
|
|
@@ -31150,13 +31622,14 @@ var TOOL_SPECS = [
|
|
|
31150
31622
|
limit: {
|
|
31151
31623
|
type: "integer",
|
|
31152
31624
|
minimum: 1,
|
|
31625
|
+
maximum: 5000,
|
|
31153
31626
|
ops: ["find", "text"],
|
|
31154
|
-
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
31627
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. At most 5000 for either. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
31155
31628
|
},
|
|
31156
31629
|
includeHidden: {
|
|
31157
31630
|
type: "boolean",
|
|
31158
31631
|
ops: ["text", "find"],
|
|
31159
|
-
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when
|
|
31632
|
+
description: "text: read hidden nodes as well, for checking that something is not showing. Default false. find: default true, since a find means the node whether or not it is drawn; false answers with what the player can actually see, which is how a panel that keeps a label for every line and hides the ones that do not is read. A node counts as hidden when the engine would not draw it: a hidden Control, Node2D or Node3D above it hides it, a plain Node in between breaks that chain as it does for the engine, and a hidden canvas layer or window hides everything in it. How many matches were left out comes back under hidden."
|
|
31160
31633
|
},
|
|
31161
31634
|
metrics: {
|
|
31162
31635
|
type: "array",
|
|
@@ -31195,7 +31668,7 @@ var TOOL_SPECS = [
|
|
|
31195
31668
|
},
|
|
31196
31669
|
{
|
|
31197
31670
|
name: "runtime_invoke",
|
|
31198
|
-
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
31671
|
+
description: "Sets a property or calls a method on a node in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors and warnings the engine reported while a called method ran come back under errors, the first 20, with errors_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
31199
31672
|
parameters: {
|
|
31200
31673
|
projectPath: RUNNING_PROJECT_PATH,
|
|
31201
31674
|
pid: RUNNING_PID,
|
|
@@ -31207,12 +31680,12 @@ var TOOL_SPECS = [
|
|
|
31207
31680
|
property: {
|
|
31208
31681
|
type: "string",
|
|
31209
31682
|
ops: ["set"],
|
|
31210
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was
|
|
31683
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
31211
31684
|
},
|
|
31212
31685
|
value: {
|
|
31213
31686
|
ops: ["set"],
|
|
31214
31687
|
blank: true,
|
|
31215
|
-
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key.`
|
|
31688
|
+
description: `set: the value, fitted to the property's type. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
|
|
31216
31689
|
},
|
|
31217
31690
|
method: {
|
|
31218
31691
|
type: "string",
|
|
@@ -31222,7 +31695,7 @@ var TOOL_SPECS = [
|
|
|
31222
31695
|
args: {
|
|
31223
31696
|
type: "array",
|
|
31224
31697
|
ops: ["call"],
|
|
31225
|
-
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
31698
|
+
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
31226
31699
|
},
|
|
31227
31700
|
timeoutMs: {
|
|
31228
31701
|
type: "integer",
|
|
@@ -31255,7 +31728,7 @@ var TOOL_SPECS = [
|
|
|
31255
31728
|
},
|
|
31256
31729
|
{
|
|
31257
31730
|
name: "runtime_capture",
|
|
31258
|
-
description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
31731
|
+
description: "A picture of the running game: the whole screen or one viewport, as an image. The whole screen includes the game's own dialogs and popups where they sit, also when a project opens them as windows of their own (subwindows not embedded). Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
|
|
31259
31732
|
parameters: {
|
|
31260
31733
|
projectPath: RUNNING_PROJECT_PATH,
|
|
31261
31734
|
pid: RUNNING_PID,
|
|
@@ -31300,7 +31773,7 @@ var TOOL_SPECS = [
|
|
|
31300
31773
|
says: {
|
|
31301
31774
|
type: "string",
|
|
31302
31775
|
ops: ["click"],
|
|
31303
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it, is not on screen. The matches are ranked the way a person picks what to press:
|
|
31776
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
31304
31777
|
},
|
|
31305
31778
|
action: {
|
|
31306
31779
|
type: "string",
|
|
@@ -31333,7 +31806,7 @@ var TOOL_SPECS = [
|
|
|
31333
31806
|
replace: {
|
|
31334
31807
|
type: "boolean",
|
|
31335
31808
|
ops: ["text"],
|
|
31336
|
-
description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
|
|
31809
|
+
description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way. A read-only field is refused.'
|
|
31337
31810
|
},
|
|
31338
31811
|
index: {
|
|
31339
31812
|
type: "number",
|
|
@@ -31377,11 +31850,11 @@ var TOOL_SPECS = [
|
|
|
31377
31850
|
requires: [],
|
|
31378
31851
|
operations: {
|
|
31379
31852
|
click: {
|
|
31380
|
-
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, and scrolled_into_view says whether the
|
|
31853
|
+
summary: "press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room",
|
|
31381
31854
|
requires: []
|
|
31382
31855
|
},
|
|
31383
31856
|
choose: {
|
|
31384
|
-
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the item takes the focus and
|
|
31857
|
+
summary: "take an item out of a menu, by what it says or by where it is in the list. A menu's items are drawn rather than built, so there is nothing to click: the menu is opened, the item is looked for in it as it stands then, since a game can fill a menu as it opens, and the item takes the focus and ui_accept presses it, which is the engine's own path, needs no window, and does not depend on what ui_accept is bound to. A disabled or hidden button in front of the menu is refused, and so is a press the menu did not take, which is heard rather than assumed. Answers with what was chosen, read before the press, what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick), and what the button in front of it shows now when it is still there",
|
|
31385
31858
|
requires: ["nodePath"]
|
|
31386
31859
|
},
|
|
31387
31860
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
@@ -31390,7 +31863,7 @@ var TOOL_SPECS = [
|
|
|
31390
31863
|
requires: ["keycode"]
|
|
31391
31864
|
},
|
|
31392
31865
|
text: {
|
|
31393
|
-
summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask",
|
|
31866
|
+
summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask. The field is the focus of the dialog that has the focus when one does, which is where the keys go",
|
|
31394
31867
|
requires: ["text"]
|
|
31395
31868
|
},
|
|
31396
31869
|
mouse_click: {
|
|
@@ -31429,7 +31902,7 @@ var TOOL_SPECS = [
|
|
|
31429
31902
|
},
|
|
31430
31903
|
value: {
|
|
31431
31904
|
ops: ["until"],
|
|
31432
|
-
description: "until: the value to wait for, fitted to the property's type. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
31905
|
+
description: "until: the value to wait for, fitted to the property's type and compared at the precision the property keeps, so -1.1 meets a 32-bit float holding -1.10000002. A value the property would have to cut is refused: a fraction on a whole number, a number other than 0 or 1 on a bool. On a property holding an object, whether declared to or untyped and holding one or null, the text null means null. One declared to hold an object refuses any value other than null, since it can never hold it."
|
|
31433
31906
|
},
|
|
31434
31907
|
says: {
|
|
31435
31908
|
type: "string",
|
|
@@ -31478,7 +31951,7 @@ var TOOL_SPECS = [
|
|
|
31478
31951
|
},
|
|
31479
31952
|
{
|
|
31480
31953
|
name: "debug_control",
|
|
31481
|
-
description: "Continues or steps the debugged game through the editor's debug adapter
|
|
31954
|
+
description: "Continues or steps the debugged game through the editor's debug adapter. A step waits for the game to stop again and answers with the stack where it ended up; one that has not stopped the game again within ten seconds answers heldAgain false and no stack. There is no pause and no step_out: Godot's adapter answers a pause by reporting the game stopped and leaving it running, and implements no stepOut at all, so hold the game where you want it with a breakpoint and step over or into from there.",
|
|
31482
31955
|
parameters: {},
|
|
31483
31956
|
requires: [],
|
|
31484
31957
|
operations: {
|
|
@@ -31489,12 +31962,12 @@ var TOOL_SPECS = [
|
|
|
31489
31962
|
},
|
|
31490
31963
|
{
|
|
31491
31964
|
name: "debug_state",
|
|
31492
|
-
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
|
|
31965
|
+
description: "Where the debugged game is stopped: the stack trace, what is in scope at a frame with the values, or the debug adapter's console output so far, its last thousand lines, with any earlier ones counted under notShown. A stop is reported by the editor's adapter only to the sessions connected when it happened, so a server that connected afterwards, which is every server after a reconnect, is shown a thread and no frames about a game still sitting at its breakpoint. That server asks the runtime instead: a held game accepts a connection and never answers, and the refusal then says held rather than running, and that the stack cannot be read from here. debug_control continue lets it go, and the next stop is one this session is told about, with its stack.",
|
|
31493
31966
|
parameters: {
|
|
31494
31967
|
frameId: {
|
|
31495
31968
|
type: "number",
|
|
31496
31969
|
ops: ["variables"],
|
|
31497
|
-
description: "variables: which frame, from a stack answer. Default the innermost."
|
|
31970
|
+
description: "variables: which frame, from a stack answer. Default the innermost. A frame the stack no longer holds is refused, and a scope whose values did not arrive in time answers variables null and is named under valuesMissing."
|
|
31498
31971
|
}
|
|
31499
31972
|
},
|
|
31500
31973
|
requires: [],
|
|
@@ -31555,9 +32028,16 @@ function buildToolDefinitions() {
|
|
|
31555
32028
|
// src/server.ts
|
|
31556
32029
|
var UPDATE_NOTICE_EVERY = 500;
|
|
31557
32030
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
31558
|
-
var
|
|
31559
|
-
var __dirname2 =
|
|
31560
|
-
|
|
32031
|
+
var run4 = promisify5(execFile5);
|
|
32032
|
+
var __dirname2 = dirname12(fileURLToPath4(import.meta.url));
|
|
32033
|
+
function editorRestartTimeoutMs() {
|
|
32034
|
+
const override = Number.parseInt(envValue("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS") ?? "", 10);
|
|
32035
|
+
return Number.isInteger(override) && override > 0 ? override : 90000;
|
|
32036
|
+
}
|
|
32037
|
+
var GREETING_WAIT_MS = 1e4;
|
|
32038
|
+
function restartUnderway(note, now = Date.now()) {
|
|
32039
|
+
return alive(note.byPid) && now - Date.parse(note.quitAt) < editorRestartTimeoutMs() + 30000;
|
|
32040
|
+
}
|
|
31561
32041
|
var HOLD_PING_MS = 2000;
|
|
31562
32042
|
var SLOWEST_FRAME_RATE = 20;
|
|
31563
32043
|
function captureDestinationRefusal(outputPath, projects) {
|
|
@@ -31570,16 +32050,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
31570
32050
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
31571
32051
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
31572
32052
|
}
|
|
31573
|
-
const directory = realPathOr(
|
|
31574
|
-
if (!
|
|
31575
|
-
return `outputPath's directory ${
|
|
32053
|
+
const directory = realPathOr(dirname12(outputPath));
|
|
32054
|
+
if (!existsSync14(directory)) {
|
|
32055
|
+
return `outputPath's directory ${dirname12(outputPath)} does not exist; a capture creates no directories.`;
|
|
31576
32056
|
}
|
|
31577
|
-
const where =
|
|
32057
|
+
const where = join19(directory, basename4(outputPath));
|
|
31578
32058
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
31579
32059
|
if (inside !== undefined) {
|
|
31580
32060
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
31581
32061
|
}
|
|
31582
|
-
if (
|
|
32062
|
+
if (existsSync14(where)) {
|
|
31583
32063
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
31584
32064
|
}
|
|
31585
32065
|
return null;
|
|
@@ -31610,16 +32090,18 @@ function describeHalt(halt) {
|
|
|
31610
32090
|
function exited(run) {
|
|
31611
32091
|
return run.exitCode !== null || run.exitSignal !== null;
|
|
31612
32092
|
}
|
|
31613
|
-
function endingOf(failed) {
|
|
31614
|
-
return {
|
|
31615
|
-
exitCode: typeof failed.code === "number" ? failed.code : null,
|
|
31616
|
-
exitSignal: typeof failed.signal === "string" ? failed.signal : null,
|
|
31617
|
-
failure: typeof failed.code === "string" ? failed.code : null
|
|
31618
|
-
};
|
|
31619
|
-
}
|
|
31620
32093
|
function howItExited(run) {
|
|
31621
32094
|
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
|
|
31622
32095
|
}
|
|
32096
|
+
function errorsUnread(run) {
|
|
32097
|
+
return run.throughEditor && typeof run.errorReport !== "string";
|
|
32098
|
+
}
|
|
32099
|
+
function cleanVerdict(run) {
|
|
32100
|
+
if (run.consoleLost === true || errorsUnread(run)) {
|
|
32101
|
+
return;
|
|
32102
|
+
}
|
|
32103
|
+
return run.log.count("error") === 0;
|
|
32104
|
+
}
|
|
31623
32105
|
function stillRunning(run) {
|
|
31624
32106
|
if (run === null || exited(run) || run.endedUnwatched === true) {
|
|
31625
32107
|
return false;
|
|
@@ -31636,6 +32118,11 @@ function stillRunning(run) {
|
|
|
31636
32118
|
return true;
|
|
31637
32119
|
}
|
|
31638
32120
|
var PLAY_STARTS_WITHIN_MS = 30000;
|
|
32121
|
+
function playOf(answer) {
|
|
32122
|
+
const play = readNumber(answer, "play");
|
|
32123
|
+
const editorPid = readNumber(answer, "editorPid");
|
|
32124
|
+
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
32125
|
+
}
|
|
31639
32126
|
function runIsUp(run, editorSays, now = Date.now()) {
|
|
31640
32127
|
if (run?.throughEditor !== true) {
|
|
31641
32128
|
return stillRunning(run);
|
|
@@ -31685,11 +32172,19 @@ function stopVerdict(stop) {
|
|
|
31685
32172
|
return {
|
|
31686
32173
|
stopped: true,
|
|
31687
32174
|
endedPid: stop.endedPid,
|
|
31688
|
-
clean: stop.
|
|
32175
|
+
clean: stop.clean,
|
|
31689
32176
|
note: "This run was over before the stop, so nothing was ended here: editor_output has what it printed and how it ended. Anything that game started for itself, with OS.create_process or otherwise, is a separate process and may still be running.",
|
|
31690
32177
|
withChildren: false
|
|
31691
32178
|
};
|
|
31692
32179
|
}
|
|
32180
|
+
if (stop.ending === "unreached") {
|
|
32181
|
+
return {
|
|
32182
|
+
stopped: false,
|
|
32183
|
+
notSignalled: stop.endedPid,
|
|
32184
|
+
note: "Nothing was ended: the editor could not be asked to stop the scene it is playing, so the game may still be running. editor_status says whether the editor is connected and playing.",
|
|
32185
|
+
withChildren: false
|
|
32186
|
+
};
|
|
32187
|
+
}
|
|
31693
32188
|
if (stop.ending === "refused") {
|
|
31694
32189
|
return {
|
|
31695
32190
|
stopped: false,
|
|
@@ -31698,22 +32193,31 @@ function stopVerdict(stop) {
|
|
|
31698
32193
|
withChildren: false
|
|
31699
32194
|
};
|
|
31700
32195
|
}
|
|
32196
|
+
if (stop.ending === "lingering") {
|
|
32197
|
+
const told = stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing and was still playing it" : `The editor was asked to stop the scene it is playing, and its game, pid ${stop.endedPid}, was still running` : `pid ${stop.endedPid ?? "unknown"} was told to end and was still running`;
|
|
32198
|
+
return {
|
|
32199
|
+
stopped: false,
|
|
32200
|
+
stillGoing: stop.endedPid,
|
|
32201
|
+
note: `${told} ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped. editor_status says when it has gone, and editor_run stop asks again.`,
|
|
32202
|
+
withChildren: true
|
|
32203
|
+
};
|
|
32204
|
+
}
|
|
31701
32205
|
return {
|
|
31702
32206
|
stopped: true,
|
|
31703
32207
|
endedPid: stop.endedPid,
|
|
31704
|
-
clean: stop.
|
|
31705
|
-
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}
|
|
32208
|
+
clean: stop.clean,
|
|
32209
|
+
note: `${stop.throughEditor ? stop.endedPid === null ? "The editor was asked to stop the scene it is playing. Its game had not announced a runtime, so no process was named under endedPid, and its exit code stays with the editor." : "The editor was asked to stop the scene it is playing." : "The process named under endedPid was ended."}${stop.exitSignal === null ? "" : ` It was ended by ${stop.exitSignal} and so has no exit code.`}`,
|
|
31706
32210
|
withChildren: true
|
|
31707
32211
|
};
|
|
31708
32212
|
}
|
|
31709
32213
|
function endedPreviousRun(ended) {
|
|
31710
|
-
if (ended
|
|
32214
|
+
if (ended?.ending !== "gone") {
|
|
31711
32215
|
return;
|
|
31712
32216
|
}
|
|
31713
32217
|
return ended.pid ?? true;
|
|
31714
32218
|
}
|
|
31715
32219
|
function previousRunLeft(ended) {
|
|
31716
|
-
return ended?.ending === "refused" && ended.pid !== null ? ended.pid : undefined;
|
|
32220
|
+
return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
|
|
31717
32221
|
}
|
|
31718
32222
|
var CONSOLE_WRAPPER = /_console(\.exe)?$/i;
|
|
31719
32223
|
function engineNamesOf(executable) {
|
|
@@ -31790,8 +32294,13 @@ function endedToStartThis(ended) {
|
|
|
31790
32294
|
if (ended.ending === "refused") {
|
|
31791
32295
|
return ` ${which} was not ended: that pid no longer answers as the process the run was started as, so nothing was signalled. If it is still the game, it is running beside this one and you have to end it yourself; if it is not, it belongs to something else.`;
|
|
31792
32296
|
}
|
|
31793
|
-
|
|
31794
|
-
|
|
32297
|
+
if (ended.ending === "unreached") {
|
|
32298
|
+
return ` ${which} was not ended here: the editor could not be asked to stop it. The editor stops a play before starting the next, so it is not running beside this one if this start reached the editor.`;
|
|
32299
|
+
}
|
|
32300
|
+
if (ended.ending === "lingering") {
|
|
32301
|
+
return ` ${which} was told to end to start this one and was still running ${STOP_WAIT_MS / 1000} seconds later, so it is running beside this one: editor_status says when it has gone.`;
|
|
32302
|
+
}
|
|
32303
|
+
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.`;
|
|
31795
32304
|
}
|
|
31796
32305
|
var LONGEST_BOOT_NOTED_MS = 5 * 60000;
|
|
31797
32306
|
var MOST_GROUPS = 20;
|
|
@@ -31919,6 +32428,7 @@ var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
|
31919
32428
|
var HANDOVER_POLL_MS = 250;
|
|
31920
32429
|
var CACHE_WRITE_MS = 2000;
|
|
31921
32430
|
var SCAN_WAIT_MS = 30000;
|
|
32431
|
+
var STREAMS_GRACE_MS = 2000;
|
|
31922
32432
|
function scanWaitIn(payload) {
|
|
31923
32433
|
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
31924
32434
|
const note = readString(payload, "scanNote");
|
|
@@ -31931,19 +32441,58 @@ function withScanWait(outcome, scanned) {
|
|
|
31931
32441
|
if (scanned.waitedMs === 0) {
|
|
31932
32442
|
return outcome;
|
|
31933
32443
|
}
|
|
31934
|
-
const said = scanWaitAnswer(scanned);
|
|
32444
|
+
const said = scanWaitAnswer(scanned, "engine");
|
|
31935
32445
|
if (outcome.ok) {
|
|
31936
32446
|
return { ...outcome, payload: { ...outcome.payload, ...said } };
|
|
31937
32447
|
}
|
|
31938
32448
|
return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
|
|
31939
32449
|
}
|
|
31940
|
-
function
|
|
32450
|
+
function notRunNote(notRun, failFast) {
|
|
32451
|
+
if (notRun <= 0) {
|
|
32452
|
+
return;
|
|
32453
|
+
}
|
|
32454
|
+
const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
|
|
32455
|
+
return failFast ? `This run stopped at the first failure in each suite it failed in, so ${cases} never ran and count as neither passed nor failed. Leave failFast out to run every case.` : `${cases} counted in ${notRun === 1 ? "its suite" : "their suites"} never ran and count as neither passed nor failed. failFast was not set, so it was not that: the suites below with notRun are the ones to read.`;
|
|
32456
|
+
}
|
|
32457
|
+
function exportAnswer(ending, asked, before, after) {
|
|
32458
|
+
const { log } = ending;
|
|
32459
|
+
const written = after !== null && after !== before;
|
|
32460
|
+
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
32461
|
+
const verdict = {
|
|
32462
|
+
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
32463
|
+
...asked,
|
|
32464
|
+
exitCode: ending.exitCode,
|
|
32465
|
+
exitSignal: ending.exitSignal ?? undefined,
|
|
32466
|
+
failure: ending.failure ?? undefined,
|
|
32467
|
+
errors: log.count("error"),
|
|
32468
|
+
warnings: log.count("warning"),
|
|
32469
|
+
entries: forAnswer(problems.entries),
|
|
32470
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
32471
|
+
};
|
|
32472
|
+
if (verdict.exported) {
|
|
32473
|
+
return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
|
|
32474
|
+
}
|
|
32475
|
+
const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
|
|
32476
|
+
const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
|
|
32477
|
+
return {
|
|
32478
|
+
content: [
|
|
32479
|
+
{
|
|
32480
|
+
type: "text",
|
|
32481
|
+
text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
|
|
32482
|
+
},
|
|
32483
|
+
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
32484
|
+
],
|
|
32485
|
+
isError: true
|
|
32486
|
+
};
|
|
32487
|
+
}
|
|
32488
|
+
function scanWaitAnswer(scanned, started = "game") {
|
|
31941
32489
|
if (scanned.waitedMs === 0) {
|
|
31942
32490
|
return {};
|
|
31943
32491
|
}
|
|
32492
|
+
const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
|
|
31944
32493
|
return {
|
|
31945
32494
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
31946
|
-
scanNote: scanned.stillScanning ?
|
|
32495
|
+
scanNote: !scanned.stillScanning ? undefined : started === "game" ? `${still} this game started during the scan and may have read a class cache being rewritten. If ` + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : `${still} the engine answering this started during the scan and may have read a class cache being ` + "rewritten, and a class cache it wrote can be written over when the scan finishes. If " + 'engine_messages shows "Could not find type" for a class that is declared, editor_rescan waits ' + "for the scan to finish, and the same call after it reads the finished cache."
|
|
31947
32496
|
};
|
|
31948
32497
|
}
|
|
31949
32498
|
var PLAYING_STATUS_MS = 1000;
|
|
@@ -32023,16 +32572,17 @@ var PROJECT_FILE_ARGUMENTS = [
|
|
|
32023
32572
|
"path",
|
|
32024
32573
|
"script"
|
|
32025
32574
|
];
|
|
32026
|
-
function escapingPropertyValue(
|
|
32027
|
-
if (typeof
|
|
32575
|
+
function escapingPropertyValue(value) {
|
|
32576
|
+
if (typeof value === "string") {
|
|
32577
|
+
return /^(res|uid):\/\//.test(value) && value.split(/[/\\]/).includes("..") ? JSON.stringify(value) : undefined;
|
|
32578
|
+
}
|
|
32579
|
+
if (typeof value !== "object" || value === null) {
|
|
32028
32580
|
return;
|
|
32029
32581
|
}
|
|
32030
|
-
for (const
|
|
32031
|
-
|
|
32032
|
-
|
|
32033
|
-
|
|
32034
|
-
if (value.split(/[/\\]/).includes("..")) {
|
|
32035
|
-
return JSON.stringify(value);
|
|
32582
|
+
for (const item of Object.values(value)) {
|
|
32583
|
+
const found = escapingPropertyValue(item);
|
|
32584
|
+
if (found !== undefined) {
|
|
32585
|
+
return found;
|
|
32036
32586
|
}
|
|
32037
32587
|
}
|
|
32038
32588
|
return;
|
|
@@ -32070,7 +32620,7 @@ function aboveTheRunner(entry) {
|
|
|
32070
32620
|
}
|
|
32071
32621
|
function elsewhereIn(projectPath, asked) {
|
|
32072
32622
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
32073
|
-
if (wanted === "" ||
|
|
32623
|
+
if (wanted === "" || existsSync14(join19(projectPath, wanted))) {
|
|
32074
32624
|
return "";
|
|
32075
32625
|
}
|
|
32076
32626
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -32084,7 +32634,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
32084
32634
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
32085
32635
|
}
|
|
32086
32636
|
function hasMainScene(projectFile) {
|
|
32087
|
-
const scene = parseProjectGodot(
|
|
32637
|
+
const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
32088
32638
|
return typeof scene === "string" && scene !== "";
|
|
32089
32639
|
}
|
|
32090
32640
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -32092,13 +32642,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
32092
32642
|
return false;
|
|
32093
32643
|
}
|
|
32094
32644
|
try {
|
|
32095
|
-
return
|
|
32645
|
+
return readFileSync18(join19(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
32096
32646
|
} catch {
|
|
32097
32647
|
return false;
|
|
32098
32648
|
}
|
|
32099
32649
|
}
|
|
32100
32650
|
function editorPlaysHeadless(projectFile) {
|
|
32101
|
-
const runArgs = parseProjectGodot(
|
|
32651
|
+
const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
32102
32652
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
32103
32653
|
}
|
|
32104
32654
|
function camelCased(params) {
|
|
@@ -32117,7 +32667,7 @@ function needsItsOwnProcess(asked) {
|
|
|
32117
32667
|
class GodotServer {
|
|
32118
32668
|
mcp;
|
|
32119
32669
|
locator = new GodotLocator;
|
|
32120
|
-
operationsScript =
|
|
32670
|
+
operationsScript = join19(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
32121
32671
|
godotBridge;
|
|
32122
32672
|
tools = buildToolDefinitions();
|
|
32123
32673
|
activeProcess = null;
|
|
@@ -32139,6 +32689,7 @@ class GodotServer {
|
|
|
32139
32689
|
shutdownInitiated = false;
|
|
32140
32690
|
launchedEditor = null;
|
|
32141
32691
|
launchedFrom = null;
|
|
32692
|
+
launchedDropped = null;
|
|
32142
32693
|
ownProject;
|
|
32143
32694
|
announcedAt = null;
|
|
32144
32695
|
handingOverFrom = null;
|
|
@@ -32146,9 +32697,12 @@ class GodotServer {
|
|
|
32146
32697
|
constructor() {
|
|
32147
32698
|
this.ownProject = envValue("GDHARNESS_PROJECT") ?? null;
|
|
32148
32699
|
this.godotBridge = getDefaultBridge(this.ownProject);
|
|
32700
|
+
this.godotBridge.on("godot_connected", () => {
|
|
32701
|
+
this.settleTheLaunchReading();
|
|
32702
|
+
});
|
|
32149
32703
|
this.mcp = new McpServer({ name: "gdharness", version: SERVER_VERSION }, { capabilities: { tools: {}, resources: {} } });
|
|
32150
32704
|
this.setupToolHandlers();
|
|
32151
|
-
setupResourceHandlers(this.mcp, () => this.lastProjectPath);
|
|
32705
|
+
setupResourceHandlers(this.mcp, () => this.ourProject() ?? this.lastProjectPath);
|
|
32152
32706
|
this.mcp.server.onerror = (error) => {
|
|
32153
32707
|
console.error("[MCP Error]", error);
|
|
32154
32708
|
};
|
|
@@ -32414,8 +32968,9 @@ class GodotServer {
|
|
|
32414
32968
|
if (!checked.ok) {
|
|
32415
32969
|
return checked.response;
|
|
32416
32970
|
}
|
|
32417
|
-
|
|
32418
|
-
|
|
32971
|
+
const named = args["projectPath"];
|
|
32972
|
+
if (typeof named === "string" && existsSync14(join19(named, "project.godot"))) {
|
|
32973
|
+
this.lastProjectPath = named;
|
|
32419
32974
|
}
|
|
32420
32975
|
this.updates.refresh();
|
|
32421
32976
|
const answer = await withCallSignal(extra.signal, () => this.answered(spec.name, checked.op ?? "", args));
|
|
@@ -32455,6 +33010,17 @@ class GodotServer {
|
|
|
32455
33010
|
return answer;
|
|
32456
33011
|
}
|
|
32457
33012
|
const moved = this.projectHasMovedOn();
|
|
33013
|
+
if (moved !== null && !isNewer(moved, SERVER_VERSION)) {
|
|
33014
|
+
this.noticedUpdate = true;
|
|
33015
|
+
this.callsSinceNotice = 0;
|
|
33016
|
+
return this.saying(answer, {
|
|
33017
|
+
project_addons_behind_this_server: {
|
|
33018
|
+
server_is: SERVER_VERSION,
|
|
33019
|
+
project_is: moved,
|
|
33020
|
+
what_to_do: `The project's gdharness addons are ${moved}, older than this ${SERVER_VERSION} server, so an ` + "editor restart loads the old code and reconnecting spawns this same server again. Tell the " + "user, and offer to run gdharness upgrade in the project, which replaces the addons with " + "this server's; ask first, since it changes files in their project. editor_launch restart " + "then loads them."
|
|
33021
|
+
}
|
|
33022
|
+
});
|
|
33023
|
+
}
|
|
32458
33024
|
if (moved !== null) {
|
|
32459
33025
|
this.noticedUpdate = true;
|
|
32460
33026
|
this.callsSinceNotice = 0;
|
|
@@ -32487,7 +33053,7 @@ class GodotServer {
|
|
|
32487
33053
|
return this.saying(answer, {
|
|
32488
33054
|
update_available: {
|
|
32489
33055
|
...notice,
|
|
32490
|
-
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it
|
|
33056
|
+
what_to_do: "Tell the user a newer gdharness is out, with what changed, and offer to take it. " + "Only run the upgrade command if they say yes: it replaces the addons in their project, " + "and afterwards the MCP server has to be reconnected and the editor restarted with " + "editor_launch restart."
|
|
32491
33057
|
}
|
|
32492
33058
|
});
|
|
32493
33059
|
}
|
|
@@ -32504,6 +33070,11 @@ class GodotServer {
|
|
|
32504
33070
|
const installed = installedAddonVersion(this.ownProject);
|
|
32505
33071
|
return installed === null || installed === SERVER_VERSION ? null : installed;
|
|
32506
33072
|
}
|
|
33073
|
+
editorCodeOnDisk() {
|
|
33074
|
+
const status = this.godotBridge.getStatus();
|
|
33075
|
+
const project = (status.connected ? status.projectPath : undefined) ?? this.ownProject;
|
|
33076
|
+
return project === null || project === "" ? undefined : installedEditorDigest(project);
|
|
33077
|
+
}
|
|
32507
33078
|
configDisagreement() {
|
|
32508
33079
|
return this.ownProject === null ? null : configDisagrees(this.ownProject, SERVER_VERSION);
|
|
32509
33080
|
}
|
|
@@ -32617,6 +33188,9 @@ class GodotServer {
|
|
|
32617
33188
|
const contained = this.containProjectFiles(asks);
|
|
32618
33189
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
32619
33190
|
}
|
|
33191
|
+
if (headless === "create_script") {
|
|
33192
|
+
return await this.handleCreateScript(answerable);
|
|
33193
|
+
}
|
|
32620
33194
|
if (headless !== "refresh_class_cache") {
|
|
32621
33195
|
return await this.headless(headless, answerable);
|
|
32622
33196
|
}
|
|
@@ -32787,7 +33361,7 @@ class GodotServer {
|
|
|
32787
33361
|
return await this.handleRuntimeCommand("click", {
|
|
32788
33362
|
...whichGame(args),
|
|
32789
33363
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
32790
|
-
button:
|
|
33364
|
+
button: args["button"] ?? "left",
|
|
32791
33365
|
double: readBoolean(args, "doubleClick") ?? false,
|
|
32792
33366
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
32793
33367
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -32859,8 +33433,8 @@ class GodotServer {
|
|
|
32859
33433
|
if (path === undefined) {
|
|
32860
33434
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
32861
33435
|
}
|
|
32862
|
-
const file =
|
|
32863
|
-
if (!
|
|
33436
|
+
const file = join19(path, "project.godot");
|
|
33437
|
+
if (!existsSync14(file)) {
|
|
32864
33438
|
return {
|
|
32865
33439
|
ok: false,
|
|
32866
33440
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -33070,13 +33644,30 @@ class GodotServer {
|
|
|
33070
33644
|
}
|
|
33071
33645
|
contained[key] = `res://${location.relativePath}`;
|
|
33072
33646
|
}
|
|
33073
|
-
const escaping = escapingPropertyValue(args["properties"]);
|
|
33647
|
+
const escaping = escapingPropertyValue([args["properties"], args["track"]]);
|
|
33074
33648
|
if (escaping !== undefined) {
|
|
33075
33649
|
return {
|
|
33076
33650
|
ok: false,
|
|
33077
33651
|
response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
|
|
33078
33652
|
};
|
|
33079
33653
|
}
|
|
33654
|
+
const sources = args["sources"];
|
|
33655
|
+
if (Array.isArray(sources)) {
|
|
33656
|
+
const resolved = [];
|
|
33657
|
+
for (const source of sources) {
|
|
33658
|
+
const texture = typeof source === "object" && source !== null ? source["texture"] : undefined;
|
|
33659
|
+
if (typeof texture !== "string") {
|
|
33660
|
+
resolved.push(source);
|
|
33661
|
+
continue;
|
|
33662
|
+
}
|
|
33663
|
+
const location = resolveWithinProject(projectPath, texture);
|
|
33664
|
+
if (!location.ok) {
|
|
33665
|
+
return { ok: false, response: this.createErrorResponse(location.reason, PATH_SOLUTIONS) };
|
|
33666
|
+
}
|
|
33667
|
+
resolved.push({ ...source, texture: `res://${location.relativePath}` });
|
|
33668
|
+
}
|
|
33669
|
+
contained["sources"] = resolved;
|
|
33670
|
+
}
|
|
33080
33671
|
const pluginName = readNonEmptyString(args, "pluginName");
|
|
33081
33672
|
if (pluginName !== undefined) {
|
|
33082
33673
|
const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
|
|
@@ -33124,6 +33715,7 @@ class GodotServer {
|
|
|
33124
33715
|
return this.answer(withScanWait({
|
|
33125
33716
|
ok: true,
|
|
33126
33717
|
messages: imported.messages,
|
|
33718
|
+
...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
|
|
33127
33719
|
payload: {
|
|
33128
33720
|
uidsCreated: given,
|
|
33129
33721
|
stillWithoutUid: after,
|
|
@@ -33174,16 +33766,25 @@ class GodotServer {
|
|
|
33174
33766
|
if (changed.length === 0) {
|
|
33175
33767
|
return written;
|
|
33176
33768
|
}
|
|
33177
|
-
const
|
|
33769
|
+
const adopting = {};
|
|
33770
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed }, adopting);
|
|
33771
|
+
if (taken["editorNote"] !== undefined) {
|
|
33772
|
+
return this.jsonTextResponse({ ...answer, ...taken });
|
|
33773
|
+
}
|
|
33774
|
+
const adopted = Array.isArray(adopting["adopted"]) ? adopting["adopted"].map(String) : [];
|
|
33775
|
+
const passedOver = changed.filter((name) => !adopted.includes(name));
|
|
33178
33776
|
return this.jsonTextResponse({
|
|
33179
33777
|
...answer,
|
|
33180
|
-
|
|
33181
|
-
...
|
|
33778
|
+
editorAdopted: adopted,
|
|
33779
|
+
...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
|
|
33182
33780
|
});
|
|
33183
33781
|
}
|
|
33184
|
-
async editorTakes(command, args) {
|
|
33782
|
+
async editorTakes(command, args, answered = {}) {
|
|
33185
33783
|
try {
|
|
33186
|
-
await this.godotBridge.invokeTool(command, args);
|
|
33784
|
+
const result = await this.godotBridge.invokeTool(command, args);
|
|
33785
|
+
if (typeof result === "object" && result !== null) {
|
|
33786
|
+
Object.assign(answered, result);
|
|
33787
|
+
}
|
|
33187
33788
|
return {};
|
|
33188
33789
|
} catch (error) {
|
|
33189
33790
|
const message = errorMessage(error);
|
|
@@ -33221,7 +33822,19 @@ class GodotServer {
|
|
|
33221
33822
|
}
|
|
33222
33823
|
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
33223
33824
|
if (!applied.ok) {
|
|
33224
|
-
|
|
33825
|
+
const listed = written.payload["updated_options"];
|
|
33826
|
+
const updated = Array.isArray(listed) ? listed.map(String) : [];
|
|
33827
|
+
const which = updated.length === 0 ? "The options were" : `${updated.join(", ")} ${updated.length === 1 ? "was" : "were"}`;
|
|
33828
|
+
return {
|
|
33829
|
+
...applied.response,
|
|
33830
|
+
content: [
|
|
33831
|
+
{
|
|
33832
|
+
type: "text",
|
|
33833
|
+
text: `${which} written to the import file, and the reimport that applies them failed, so the resource is still imported the old way until the editor's next scan or project_import reimport imports it. Why the reimport failed:`
|
|
33834
|
+
},
|
|
33835
|
+
...applied.response.content
|
|
33836
|
+
]
|
|
33837
|
+
};
|
|
33225
33838
|
}
|
|
33226
33839
|
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
33227
33840
|
}
|
|
@@ -33239,7 +33852,14 @@ class GodotServer {
|
|
|
33239
33852
|
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
33240
33853
|
};
|
|
33241
33854
|
}
|
|
33242
|
-
const
|
|
33855
|
+
const never = listed.find((one) => one.status === "not_imported");
|
|
33856
|
+
if (resourcePath !== undefined && never !== undefined) {
|
|
33857
|
+
return {
|
|
33858
|
+
ok: false,
|
|
33859
|
+
response: this.createErrorResponse(`${never.path} is not something the engine imports: ${never.reason ?? "it has no importer"}.`)
|
|
33860
|
+
};
|
|
33861
|
+
}
|
|
33862
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && one.status !== "not_imported" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
33243
33863
|
if (targets.length === 0) {
|
|
33244
33864
|
return {
|
|
33245
33865
|
ok: true,
|
|
@@ -33278,9 +33898,7 @@ class GodotServer {
|
|
|
33278
33898
|
if (!imported.ok) {
|
|
33279
33899
|
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
33280
33900
|
}
|
|
33281
|
-
|
|
33282
|
-
extra["engine_messages"] = imported.messages;
|
|
33283
|
-
}
|
|
33901
|
+
Object.assign(extra, engineExtras(imported));
|
|
33284
33902
|
if (imported.extensionNote !== undefined) {
|
|
33285
33903
|
extra["extensionNote"] = imported.extensionNote;
|
|
33286
33904
|
}
|
|
@@ -33363,17 +33981,15 @@ class GodotServer {
|
|
|
33363
33981
|
return withScanWait(outcome, scanned);
|
|
33364
33982
|
}
|
|
33365
33983
|
answer(outcome) {
|
|
33984
|
+
const extras = engineExtras(outcome);
|
|
33366
33985
|
if (!outcome.ok) {
|
|
33367
33986
|
const response = this.createErrorResponse(outcome.message);
|
|
33368
|
-
if (
|
|
33369
|
-
response.content.push({
|
|
33370
|
-
type: "text",
|
|
33371
|
-
text: JSON.stringify({ engine_messages: outcome.messages }, null, 2)
|
|
33372
|
-
});
|
|
33987
|
+
if (Object.keys(extras).length > 0) {
|
|
33988
|
+
response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
|
|
33373
33989
|
}
|
|
33374
33990
|
return response;
|
|
33375
33991
|
}
|
|
33376
|
-
return this.jsonTextResponse(
|
|
33992
|
+
return this.jsonTextResponse({ ...outcome.payload, ...extras });
|
|
33377
33993
|
}
|
|
33378
33994
|
async handleProjectInfo(args) {
|
|
33379
33995
|
const include = readStringArray(args, "include") ?? [];
|
|
@@ -33389,7 +34005,7 @@ class GodotServer {
|
|
|
33389
34005
|
if (!engine.ok) {
|
|
33390
34006
|
return engine.response;
|
|
33391
34007
|
}
|
|
33392
|
-
const application = parseProjectGodot(
|
|
34008
|
+
const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
|
|
33393
34009
|
const name = application["config/name"];
|
|
33394
34010
|
const mainScene = application["run/main_scene"];
|
|
33395
34011
|
const info = {
|
|
@@ -33406,10 +34022,14 @@ class GodotServer {
|
|
|
33406
34022
|
continue;
|
|
33407
34023
|
}
|
|
33408
34024
|
const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
|
|
33409
|
-
info[section] = outcome.ok ?
|
|
34025
|
+
info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
|
|
33410
34026
|
}
|
|
33411
34027
|
return this.jsonTextResponse(info);
|
|
33412
34028
|
}
|
|
34029
|
+
fileTypes(args) {
|
|
34030
|
+
const named = readStringArray(args, "fileTypes");
|
|
34031
|
+
return named === undefined ? {} : { fileTypes: named };
|
|
34032
|
+
}
|
|
33413
34033
|
handleSearchProject(args) {
|
|
33414
34034
|
const project = this.project(args);
|
|
33415
34035
|
if (!project.ok) {
|
|
@@ -33418,16 +34038,7 @@ class GodotServer {
|
|
|
33418
34038
|
try {
|
|
33419
34039
|
return this.jsonTextResponse(searchProject(project.value.path, {
|
|
33420
34040
|
query: readString(args, "query") ?? "",
|
|
33421
|
-
fileTypes
|
|
33422
|
-
"gd",
|
|
33423
|
-
"tscn",
|
|
33424
|
-
"tres",
|
|
33425
|
-
"gdshader",
|
|
33426
|
-
"cfg",
|
|
33427
|
-
"md",
|
|
33428
|
-
"txt",
|
|
33429
|
-
"json"
|
|
33430
|
-
],
|
|
34041
|
+
...this.fileTypes(args),
|
|
33431
34042
|
regex: readBoolean(args, "regex") ?? false,
|
|
33432
34043
|
caseSensitive: readBoolean(args, "caseSensitive") ?? false,
|
|
33433
34044
|
maxResults: readPositiveNumber(args, "maxResults") ?? 100
|
|
@@ -33450,18 +34061,18 @@ class GodotServer {
|
|
|
33450
34061
|
if (!engine.ok) {
|
|
33451
34062
|
return engine.response;
|
|
33452
34063
|
}
|
|
33453
|
-
const folder =
|
|
34064
|
+
const folder = dirname12(output.absolutePath);
|
|
33454
34065
|
try {
|
|
33455
34066
|
mkdirSync10(folder, { recursive: true });
|
|
33456
34067
|
} catch (error) {
|
|
33457
|
-
return this.createErrorResponse(`Cannot create ${
|
|
34068
|
+
return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
33458
34069
|
}
|
|
33459
34070
|
const debug = readBoolean(args, "debug") ?? false;
|
|
33460
34071
|
const exportLogs = scratchDirectory("export");
|
|
33461
34072
|
const exportArgs = [
|
|
33462
34073
|
"--headless",
|
|
33463
34074
|
"--log-file",
|
|
33464
|
-
|
|
34075
|
+
join19(exportLogs, "engine.log"),
|
|
33465
34076
|
"--path",
|
|
33466
34077
|
project.value.path,
|
|
33467
34078
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -33469,55 +34080,18 @@ class GodotServer {
|
|
|
33469
34080
|
output.absolutePath
|
|
33470
34081
|
];
|
|
33471
34082
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
33472
|
-
const
|
|
33473
|
-
|
|
34083
|
+
const writtenAt = () => existsSync14(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
|
|
34084
|
+
const before = writtenAt();
|
|
34085
|
+
let ending;
|
|
33474
34086
|
try {
|
|
33475
|
-
|
|
33476
|
-
timeout: 300000,
|
|
33477
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
33478
|
-
signal: callSignal()
|
|
33479
|
-
});
|
|
33480
|
-
log.append("stdout", stdout);
|
|
33481
|
-
log.append("stderr", stderr);
|
|
33482
|
-
} catch (error) {
|
|
33483
|
-
if (!(error instanceof Error && ("stdout" in error) && ("stderr" in error))) {
|
|
33484
|
-
return this.createErrorResponse(`Export could not be run: ${errorMessage(error)}`);
|
|
33485
|
-
}
|
|
33486
|
-
const failed = error;
|
|
33487
|
-
log.append("stdout", failed.stdout);
|
|
33488
|
-
log.append("stderr", failed.stderr);
|
|
33489
|
-
ending = endingOf(failed);
|
|
34087
|
+
ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
|
|
33490
34088
|
} finally {
|
|
33491
34089
|
discard(exportLogs);
|
|
33492
34090
|
}
|
|
33493
|
-
|
|
33494
|
-
|
|
33495
|
-
const verdict = {
|
|
33496
|
-
exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync12(output.absolutePath),
|
|
33497
|
-
preset,
|
|
33498
|
-
outputPath: output.relativePath,
|
|
33499
|
-
debug,
|
|
33500
|
-
exitCode: ending.exitCode,
|
|
33501
|
-
exitSignal: ending.exitSignal ?? undefined,
|
|
33502
|
-
failure: ending.failure ?? undefined,
|
|
33503
|
-
errors: log.count("error"),
|
|
33504
|
-
warnings: log.count("warning"),
|
|
33505
|
-
entries: forAnswer(problems.entries)
|
|
33506
|
-
};
|
|
33507
|
-
if (!verdict.exported) {
|
|
33508
|
-
const why = ending.exitSignal === null ? ending.failure === null ? "" : ` The engine was ended by Node (${ending.failure}), so it has no exit code.` : ` The engine was ended by ${ending.exitSignal}, so it has no exit code: the export ran past its five minutes or the call was cancelled.`;
|
|
33509
|
-
return {
|
|
33510
|
-
content: [
|
|
33511
|
-
{
|
|
33512
|
-
type: "text",
|
|
33513
|
-
text: `Export with preset '${preset}' did not produce ${output.relativePath}.${why}`
|
|
33514
|
-
},
|
|
33515
|
-
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
33516
|
-
],
|
|
33517
|
-
isError: true
|
|
33518
|
-
};
|
|
34091
|
+
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
34092
|
+
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
33519
34093
|
}
|
|
33520
|
-
return
|
|
34094
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
|
|
33521
34095
|
}
|
|
33522
34096
|
async handleRunTests(args) {
|
|
33523
34097
|
const project = this.project(args);
|
|
@@ -33529,7 +34103,7 @@ class GodotServer {
|
|
|
33529
34103
|
return contained.response;
|
|
33530
34104
|
}
|
|
33531
34105
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
33532
|
-
if (!
|
|
34106
|
+
if (!existsSync14(join19(project.value.path, runner))) {
|
|
33533
34107
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
33534
34108
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
33535
34109
|
]);
|
|
@@ -33595,21 +34169,22 @@ class GodotServer {
|
|
|
33595
34169
|
if (timedOut && run.process.pid !== undefined) {
|
|
33596
34170
|
await untilGone([run.process.pid]);
|
|
33597
34171
|
}
|
|
33598
|
-
|
|
34172
|
+
await run.settled;
|
|
34173
|
+
const reportsDir = join19(project.value.path, ".godot", ...ours.split("/"));
|
|
33599
34174
|
let report = null;
|
|
33600
34175
|
let reportProblem = null;
|
|
33601
34176
|
try {
|
|
33602
|
-
const written =
|
|
34177
|
+
const written = existsSync14(reportsDir) ? readdirSync8(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
33603
34178
|
const newest = written.at(-1);
|
|
33604
34179
|
if (newest !== undefined) {
|
|
33605
|
-
report = parseJUnit(
|
|
34180
|
+
report = parseJUnit(readFileSync18(join19(reportsDir, newest, "results.xml"), "utf8"));
|
|
33606
34181
|
}
|
|
33607
34182
|
} catch (error) {
|
|
33608
34183
|
reportProblem = errorMessage(error);
|
|
33609
34184
|
} finally {
|
|
33610
34185
|
discard(reportsDir, userData);
|
|
33611
34186
|
try {
|
|
33612
|
-
rmdirSync2(
|
|
34187
|
+
rmdirSync2(dirname12(reportsDir));
|
|
33613
34188
|
} catch {}
|
|
33614
34189
|
}
|
|
33615
34190
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -33622,7 +34197,11 @@ class GodotServer {
|
|
|
33622
34197
|
105: "script errors"
|
|
33623
34198
|
};
|
|
33624
34199
|
const exitCode = run.exitCode;
|
|
33625
|
-
const printed = run.log.select({
|
|
34200
|
+
const printed = run.log.select({
|
|
34201
|
+
severity: "info",
|
|
34202
|
+
sinceLastCall: false,
|
|
34203
|
+
limit: Number.POSITIVE_INFINITY
|
|
34204
|
+
}).entries;
|
|
33626
34205
|
const said = printed.map((entry) => entry.text);
|
|
33627
34206
|
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
33628
34207
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
@@ -33638,7 +34217,7 @@ class GodotServer {
|
|
|
33638
34217
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
33639
34218
|
};
|
|
33640
34219
|
if (report === null) {
|
|
33641
|
-
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : `The test run
|
|
34220
|
+
const note = nothingRan !== null ? `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}` : scriptErrors.length > 0 ? scriptErrorsNote(scriptErrors) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${verdict}.` : `The test run wrote no report (${verdict}).`;
|
|
33642
34221
|
return {
|
|
33643
34222
|
content: [
|
|
33644
34223
|
{ type: "text", text: note },
|
|
@@ -33675,10 +34254,21 @@ class GodotServer {
|
|
|
33675
34254
|
orphans: suite.orphans,
|
|
33676
34255
|
message: `${suite.orphans} node${suite.orphans === 1 ? "" : "s"} were still in the tree when this suite finished. A node built in a test and neither freed nor added to the tree is the usual cause; free it, or add it with auto_free().`
|
|
33677
34256
|
}));
|
|
33678
|
-
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
34257
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.hookFailures > 0 || suite.hookErrors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
34258
|
+
const hooksPrinted = hookFailuresPrinted(said);
|
|
34259
|
+
const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
|
|
34260
|
+
const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
|
|
34261
|
+
return printedHere.length > 0 ? printedHere.map((hook) => ({ suite: suite.name, ...hook })) : [
|
|
34262
|
+
{
|
|
34263
|
+
suite: suite.name,
|
|
34264
|
+
path: suite.path,
|
|
34265
|
+
message: "gdUnit4 counted a failure in this suite that none of its cases carry, which is what a failed before() or after() leaves, and printed no line saying which."
|
|
34266
|
+
}
|
|
34267
|
+
];
|
|
34268
|
+
});
|
|
33679
34269
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
33680
34270
|
return this.jsonTextResponse({
|
|
33681
|
-
passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
34271
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
33682
34272
|
verdict,
|
|
33683
34273
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
33684
34274
|
exitCode,
|
|
@@ -33689,13 +34279,14 @@ class GodotServer {
|
|
|
33689
34279
|
skipped: report.skipped,
|
|
33690
34280
|
time: report.time,
|
|
33691
34281
|
failed,
|
|
34282
|
+
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
33692
34283
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
33693
34284
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
33694
34285
|
notRun: notRun > 0 ? notRun : undefined,
|
|
33695
34286
|
...printedLines,
|
|
33696
34287
|
savesNote,
|
|
33697
34288
|
...scanned,
|
|
33698
|
-
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRun
|
|
34289
|
+
note: verdict.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true),
|
|
33699
34290
|
suites: unclean.map((suite) => ({
|
|
33700
34291
|
name: suite.name,
|
|
33701
34292
|
path: suite.path,
|
|
@@ -33703,6 +34294,7 @@ class GodotServer {
|
|
|
33703
34294
|
notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
|
|
33704
34295
|
failures: suite.failures,
|
|
33705
34296
|
errors: suite.errors,
|
|
34297
|
+
hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
|
|
33706
34298
|
skipped: suite.skipped,
|
|
33707
34299
|
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
33708
34300
|
time: suite.time
|
|
@@ -33716,7 +34308,7 @@ class GodotServer {
|
|
|
33716
34308
|
}
|
|
33717
34309
|
async godotVersion(godotPath) {
|
|
33718
34310
|
try {
|
|
33719
|
-
const { stdout } = await
|
|
34311
|
+
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
33720
34312
|
this.lastGodotVersion = stdout.trim();
|
|
33721
34313
|
return this.lastGodotVersion;
|
|
33722
34314
|
} catch (error) {
|
|
@@ -33728,8 +34320,18 @@ class GodotServer {
|
|
|
33728
34320
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
33729
34321
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
33730
34322
|
if (payload["error"] !== undefined) {
|
|
33731
|
-
const reason = payload["error"];
|
|
33732
|
-
|
|
34323
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
34324
|
+
if (payload["refusedArguments"] === true) {
|
|
34325
|
+
return this.createErrorResponse(reason);
|
|
34326
|
+
}
|
|
34327
|
+
if (typeof payload["serves"] === "string") {
|
|
34328
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
34329
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
34330
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
34331
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
34332
|
+
]);
|
|
34333
|
+
}
|
|
34334
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
33733
34335
|
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
33734
34336
|
"GDHARNESS_LSP_PORT points this server at another one"
|
|
33735
34337
|
]);
|
|
@@ -33747,7 +34349,7 @@ class GodotServer {
|
|
|
33747
34349
|
warnings: diagnostics.length - errors,
|
|
33748
34350
|
diagnostics,
|
|
33749
34351
|
...disagrees
|
|
33750
|
-
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
34352
|
+
}, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
|
|
33751
34353
|
}
|
|
33752
34354
|
whereTheFileDisagrees(diagnostics, args) {
|
|
33753
34355
|
const messages = diagnostics.map((entry) => asParams(entry)["message"]).filter((message) => typeof message === "string");
|
|
@@ -33760,11 +34362,11 @@ class GodotServer {
|
|
|
33760
34362
|
const cached = cachedClasses(projectPath);
|
|
33761
34363
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
33762
34364
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33763
|
-
if (!contained.ok || !
|
|
34365
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
33764
34366
|
return null;
|
|
33765
34367
|
}
|
|
33766
34368
|
try {
|
|
33767
|
-
return
|
|
34369
|
+
return readFileSync18(contained.absolutePath, "utf8");
|
|
33768
34370
|
} catch {
|
|
33769
34371
|
return null;
|
|
33770
34372
|
}
|
|
@@ -33820,11 +34422,11 @@ class GodotServer {
|
|
|
33820
34422
|
}
|
|
33821
34423
|
sourceOfClass(projectPath, resourcePath) {
|
|
33822
34424
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33823
|
-
if (!contained.ok || !
|
|
34425
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
33824
34426
|
return null;
|
|
33825
34427
|
}
|
|
33826
34428
|
try {
|
|
33827
|
-
return
|
|
34429
|
+
return readFileSync18(contained.absolutePath, "utf8");
|
|
33828
34430
|
} catch {
|
|
33829
34431
|
return null;
|
|
33830
34432
|
}
|
|
@@ -33898,7 +34500,7 @@ class GodotServer {
|
|
|
33898
34500
|
}
|
|
33899
34501
|
breakpointsAtRisk() {
|
|
33900
34502
|
const status = this.godotBridge.getStatus();
|
|
33901
|
-
if (!status
|
|
34503
|
+
if (!hasSaidWhoItIs(status) || status.syncsBreakpoints === true) {
|
|
33902
34504
|
return {};
|
|
33903
34505
|
}
|
|
33904
34506
|
return {
|
|
@@ -33953,26 +34555,34 @@ class GodotServer {
|
|
|
33953
34555
|
restartSettled(this.ownProject);
|
|
33954
34556
|
return null;
|
|
33955
34557
|
}
|
|
33956
|
-
return owed;
|
|
34558
|
+
return { note: owed, underway: restartUnderway(owed) };
|
|
33957
34559
|
}
|
|
33958
34560
|
async getEditorStatusPayload() {
|
|
33959
34561
|
const status = this.godotBridge.getStatus();
|
|
33960
34562
|
const launchedIsUp = !status.connected && await this.launchedEditorIsUp();
|
|
33961
34563
|
const isPortConflict = this.bridgeStartupError?.includes("EADDRINUSE") ?? false;
|
|
33962
|
-
const
|
|
34564
|
+
const greeted = hasSaidWhoItIs(status);
|
|
34565
|
+
const stale = greeted && editorIsStale(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest());
|
|
33963
34566
|
const unfinished = this.restartLeftUnfinished(status.connected);
|
|
33964
34567
|
return {
|
|
33965
34568
|
...status,
|
|
33966
34569
|
serverVersion: SERVER_VERSION,
|
|
33967
|
-
addonIsStale:
|
|
34570
|
+
addonIsStale: greeted ? stale : undefined,
|
|
34571
|
+
greeting: status.connected && !greeted ? "The editor has connected and has not yet said who it is: its project, pid, addon version and ports arrive with its greeting a moment after the socket opens, later while it imports. Ask again for them." : undefined,
|
|
33968
34572
|
bridgeAvailable: this.bridgeStartupError === null,
|
|
33969
|
-
mayYetConnect: status.connected ? undefined : unfinished !== null ?
|
|
33970
|
-
restartInterrupted: unfinished === null ? undefined : {
|
|
33971
|
-
quitEditorPid: unfinished.editorPid,
|
|
33972
|
-
quitAt: unfinished.quitAt,
|
|
33973
|
-
byServerPid: unfinished.byPid,
|
|
34573
|
+
mayYetConnect: status.connected ? undefined : unfinished !== null ? unfinished.underway : anEditorIsStillComing(status.listeningSince, launchedIsUp),
|
|
34574
|
+
restartInterrupted: unfinished === null || unfinished.underway ? undefined : {
|
|
34575
|
+
quitEditorPid: unfinished.note.editorPid,
|
|
34576
|
+
quitAt: unfinished.note.quitAt,
|
|
34577
|
+
byServerPid: unfinished.note.byPid,
|
|
33974
34578
|
note: "A restart begun by a previous gdharness server asked this project's editor to quit, and that server was ended before it could start the editor again, so nothing is coming. editor_launch open starts one, and clears this."
|
|
33975
34579
|
},
|
|
34580
|
+
restartUnderway: unfinished === null || !unfinished.underway ? undefined : {
|
|
34581
|
+
quitEditorPid: unfinished.note.editorPid,
|
|
34582
|
+
quitAt: unfinished.note.quitAt,
|
|
34583
|
+
byServerPid: unfinished.note.byPid,
|
|
34584
|
+
note: `A restart of this project's editor is under way: gdharness server pid ${unfinished.note.byPid} asked it to quit at ${unfinished.note.quitAt} and is starting it again. Wait for it rather than opening another.`
|
|
34585
|
+
},
|
|
33976
34586
|
awaitingLaunchedEditor: status.connected || !launchedIsUp ? undefined : this.launchedEditor?.pid ?? undefined,
|
|
33977
34587
|
...status.connected ? this.whatTheLaunchedEditorDropped() : {},
|
|
33978
34588
|
startupError: this.bridgeStartupError,
|
|
@@ -33982,8 +34592,9 @@ class GodotServer {
|
|
|
33982
34592
|
port: this.godotBridge.configuredPort,
|
|
33983
34593
|
note: `The gdharness server this one replaced, pid ${this.handingOverFrom}, still holds port ${this.godotBridge.configuredPort} and is standing down; this server takes the port when it lets go, and takes another after ${HANDOVER_MS / 1000} seconds if it does not.`
|
|
33984
34594
|
},
|
|
33985
|
-
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
33986
|
-
addonNote:
|
|
34595
|
+
staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()) : undefined,
|
|
34596
|
+
addonNote: greeted ? sameCodeNote(status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest()) : undefined,
|
|
34597
|
+
startedByAnEditorNote: status.startedByAnEditor === true ? "Another editor started this one: Godot's own restart, or the project manager opening a project. None of the arguments a server gave the editor before it reached this one, so it serves the ports its editor settings name (and none at all while another editor holds those, when the script and debug tools reach that editor instead), its console is not captured, and it restarts itself. Close it and use editor_launch open to have an editor this server started." : undefined,
|
|
33987
34598
|
...this.breakpointsAtRisk(),
|
|
33988
34599
|
retryingBridge: this.bridgeRetry === null ? undefined : true,
|
|
33989
34600
|
announcedAt: this.announcedAt ?? undefined,
|
|
@@ -34012,7 +34623,8 @@ class GodotServer {
|
|
|
34012
34623
|
return {
|
|
34013
34624
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
34014
34625
|
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
34015
|
-
debugPort: readNumber(asParams(answer), "debugPort")
|
|
34626
|
+
debugPort: readNumber(asParams(answer), "debugPort"),
|
|
34627
|
+
play: playOf(asParams(answer))
|
|
34016
34628
|
};
|
|
34017
34629
|
} catch {
|
|
34018
34630
|
return null;
|
|
@@ -34108,7 +34720,7 @@ class GodotServer {
|
|
|
34108
34720
|
return null;
|
|
34109
34721
|
}
|
|
34110
34722
|
try {
|
|
34111
|
-
return settingKeys(
|
|
34723
|
+
return settingKeys(readFileSync18(join19(projectPath, "project.godot"), "utf8"));
|
|
34112
34724
|
} catch {
|
|
34113
34725
|
return null;
|
|
34114
34726
|
}
|
|
@@ -34121,17 +34733,33 @@ class GodotServer {
|
|
|
34121
34733
|
"editor_status says whether the bridge is up and what has reached it"
|
|
34122
34734
|
]);
|
|
34123
34735
|
}
|
|
34124
|
-
|
|
34736
|
+
if (!hasSaidWhoItIs(before)) {
|
|
34737
|
+
const settled = await this.waitForBridge(() => hasSaidWhoItIs(this.godotBridge.getStatus()) || !this.godotBridge.isConnected(), Date.now() + GREETING_WAIT_MS);
|
|
34738
|
+
if (!settled) {
|
|
34739
|
+
return this.createErrorResponse(`The editor has connected and has not said who it is within ${GREETING_WAIT_MS / 1000}s, so whether this server or the editor itself restarts it cannot be told.`, [
|
|
34740
|
+
"editor_status says when it has, under greeting while it has not",
|
|
34741
|
+
"Then ask for the restart again"
|
|
34742
|
+
]);
|
|
34743
|
+
}
|
|
34744
|
+
return this.handleRestartEditor(args);
|
|
34745
|
+
}
|
|
34125
34746
|
const mine = before.openedByAServer === true && before.projectPath !== undefined;
|
|
34747
|
+
if (!mine && args["hidden"] === true) {
|
|
34748
|
+
return this.createErrorResponse("This editor was opened by hand, and restarts itself as it was, so it cannot come back hidden.", [
|
|
34749
|
+
"Close it, then editor_launch open with hidden: true starts a hidden one this server can restart",
|
|
34750
|
+
"editor_launch restart without hidden restarts it as it is"
|
|
34751
|
+
]);
|
|
34752
|
+
}
|
|
34753
|
+
const settingsBefore = this.settingKeysOf(before.projectPath);
|
|
34126
34754
|
const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
|
|
34127
34755
|
if (asked.isError === true) {
|
|
34128
34756
|
return asked;
|
|
34129
34757
|
}
|
|
34130
34758
|
const startedAt = before.connectedAt?.getTime() ?? 0;
|
|
34131
34759
|
const began = Date.now();
|
|
34132
|
-
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began +
|
|
34760
|
+
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began + editorRestartTimeoutMs());
|
|
34133
34761
|
if (!back) {
|
|
34134
|
-
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${
|
|
34762
|
+
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${editorRestartTimeoutMs() / 1000}s.`, [
|
|
34135
34763
|
"It may be asking what to do about an unsaved scene: look at the editor window",
|
|
34136
34764
|
"An addon that no longer parses stops the editor reaching this server",
|
|
34137
34765
|
"editor_status says whether anything has reached the bridge since"
|
|
@@ -34145,18 +34773,27 @@ class GodotServer {
|
|
|
34145
34773
|
addonVersion: now.addonVersion,
|
|
34146
34774
|
serverVersion: SERVER_VERSION,
|
|
34147
34775
|
addonIsStale: editorIsStale(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
34148
|
-
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest()),
|
|
34776
|
+
staleNote: addonMismatch(now.addonVersion, SERVER_VERSION, now.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()),
|
|
34149
34777
|
...this.whatTheEditorDropped(settingsBefore, after),
|
|
34150
34778
|
tookMs: Date.now() - began
|
|
34151
34779
|
});
|
|
34152
34780
|
}
|
|
34153
|
-
|
|
34781
|
+
settleTheLaunchReading() {
|
|
34154
34782
|
const watched = this.launchedFrom;
|
|
34155
|
-
|
|
34156
|
-
|
|
34783
|
+
const status = this.godotBridge.getStatus();
|
|
34784
|
+
if (watched === null || !hasSaidWhoItIs(status)) {
|
|
34785
|
+
return;
|
|
34157
34786
|
}
|
|
34158
34787
|
this.launchedFrom = null;
|
|
34159
|
-
|
|
34788
|
+
if (status.editorPid === undefined || status.editorPid !== this.launchedEditor?.pid) {
|
|
34789
|
+
return;
|
|
34790
|
+
}
|
|
34791
|
+
this.launchedDropped = this.whatTheEditorDropped(watched.before, this.settingKeysOf(watched.projectPath));
|
|
34792
|
+
}
|
|
34793
|
+
whatTheLaunchedEditorDropped() {
|
|
34794
|
+
const dropped = this.launchedDropped ?? {};
|
|
34795
|
+
this.launchedDropped = null;
|
|
34796
|
+
return dropped;
|
|
34160
34797
|
}
|
|
34161
34798
|
whatTheEditorDropped(before, after) {
|
|
34162
34799
|
return settingsDroppedReport(before, after);
|
|
@@ -34182,7 +34819,14 @@ class GodotServer {
|
|
|
34182
34819
|
restartSettled(projectPath);
|
|
34183
34820
|
return asked;
|
|
34184
34821
|
}
|
|
34185
|
-
await this.waitForBridge(() => !alive(editorPid), Date.now() +
|
|
34822
|
+
const gone = await this.waitForBridge(() => !alive(editorPid), Date.now() + editorRestartTimeoutMs());
|
|
34823
|
+
if (!gone) {
|
|
34824
|
+
restartSettled(projectPath);
|
|
34825
|
+
return this.createErrorResponse(`The editor was asked to go and pid ${editorPid ?? "unknown"} is still running after ${editorRestartTimeoutMs() / 1000}s, so no replacement was started: it would find the language server and debug adapter ports still held.`, [
|
|
34826
|
+
"It may be asking what to do about unsaved changes, or a script may be holding up its exit",
|
|
34827
|
+
`editor_launch open starts one on ${projectPath} once it has gone`
|
|
34828
|
+
]);
|
|
34829
|
+
}
|
|
34186
34830
|
const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
|
|
34187
34831
|
restartSettled(projectPath);
|
|
34188
34832
|
if (opened.error !== null) {
|
|
@@ -34213,6 +34857,10 @@ class GodotServer {
|
|
|
34213
34857
|
"Another project wants its own gdharness server, which is its own harness session"
|
|
34214
34858
|
]);
|
|
34215
34859
|
}
|
|
34860
|
+
const refused = this.anEditorThatCannotConnect(project.value.path);
|
|
34861
|
+
if (refused !== null) {
|
|
34862
|
+
return refused;
|
|
34863
|
+
}
|
|
34216
34864
|
const engine = await this.engine();
|
|
34217
34865
|
if (!engine.ok) {
|
|
34218
34866
|
return engine.response;
|
|
@@ -34237,6 +34885,27 @@ class GodotServer {
|
|
|
34237
34885
|
settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
|
|
34238
34886
|
});
|
|
34239
34887
|
}
|
|
34888
|
+
anEditorThatCannotConnect(projectPath) {
|
|
34889
|
+
if (this.ownProject !== null && !isSameDirectory(this.ownProject, projectPath)) {
|
|
34890
|
+
return this.createErrorResponse(`This server serves ${this.ownProject}, and an editor of ${projectPath} would dial that project's own server rather than this one.`, [
|
|
34891
|
+
"Open it from that project's own gdharness session",
|
|
34892
|
+
`editor_launch with projectPath ${this.ownProject} opens the editor this server serves`
|
|
34893
|
+
]);
|
|
34894
|
+
}
|
|
34895
|
+
const report = inspectProject(projectPath);
|
|
34896
|
+
const installed = report.addons.some((addon) => addon.name === "gdharness_editor" && addon.installed);
|
|
34897
|
+
if (!installed || !report.pluginsEnabled.includes("gdharness_editor")) {
|
|
34898
|
+
return this.createErrorResponse(`The gdharness editor addon is not ${installed ? "enabled" : "installed"} in ${projectPath}, so an editor opened on it would never reach this server.`, installed ? [
|
|
34899
|
+
"gdharness setup in the project enables it",
|
|
34900
|
+
"Or enable it under Project Settings, Plugins, in an editor opened by hand"
|
|
34901
|
+
] : ["gdharness setup in the project installs and enables it"]);
|
|
34902
|
+
}
|
|
34903
|
+
const owed = restartOwed(projectPath);
|
|
34904
|
+
if (owed !== null && restartUnderway(owed)) {
|
|
34905
|
+
return this.createErrorResponse(`A restart of this project's editor is under way: gdharness server pid ${owed.byPid} asked it to quit at ${owed.quitAt} and is starting it again, so opening another would put two editors on one project.`, ["editor_status says when it has connected"]);
|
|
34906
|
+
}
|
|
34907
|
+
return null;
|
|
34908
|
+
}
|
|
34240
34909
|
async openAnEditor(engine, projectPath, ports, hidden) {
|
|
34241
34910
|
clearEditorLog(projectPath);
|
|
34242
34911
|
const launched = await launchOutsideTheTree({
|
|
@@ -34271,6 +34940,13 @@ class GodotServer {
|
|
|
34271
34940
|
if (!project.ok) {
|
|
34272
34941
|
return project.response;
|
|
34273
34942
|
}
|
|
34943
|
+
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
34944
|
+
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
34945
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and the editor on this bridge has ${open} open.`, [
|
|
34946
|
+
"editor_status names the project this server's editor is showing",
|
|
34947
|
+
"One server serves one editor: start a second server for the other project"
|
|
34948
|
+
]);
|
|
34949
|
+
}
|
|
34274
34950
|
const scene = readNonEmptyString(args, "scene");
|
|
34275
34951
|
const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
|
|
34276
34952
|
if (sceneToRun && !sceneToRun.ok) {
|
|
@@ -34365,7 +35041,7 @@ class GodotServer {
|
|
|
34365
35041
|
}
|
|
34366
35042
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
34367
35043
|
const budgetMs = budget.ms;
|
|
34368
|
-
if (!
|
|
35044
|
+
if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
34369
35045
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
34370
35046
|
}
|
|
34371
35047
|
let asking = null;
|
|
@@ -34374,7 +35050,7 @@ class GodotServer {
|
|
|
34374
35050
|
let editorAsks = 0;
|
|
34375
35051
|
const waitBegan = Date.now();
|
|
34376
35052
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
34377
|
-
const
|
|
35053
|
+
const fresh = await announcedSince(projectPath, before, {
|
|
34378
35054
|
budgetMs,
|
|
34379
35055
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
|
|
34380
35056
|
giveUp: () => {
|
|
@@ -34398,11 +35074,19 @@ class GodotServer {
|
|
|
34398
35074
|
}
|
|
34399
35075
|
});
|
|
34400
35076
|
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
34401
|
-
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${
|
|
35077
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${fresh.length === 0 ? "nothing announced" : `pid ${fresh.map((one) => one.pid).join(", ")} announced`}, ${asked}`);
|
|
34402
35078
|
const going = await this.currentRun();
|
|
35079
|
+
const endpoint = await this.theRunsAmong(going, fresh);
|
|
34403
35080
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
34404
35081
|
this.tie(going, endpoint);
|
|
34405
35082
|
}
|
|
35083
|
+
if (endpoint === null && fresh.length > 1) {
|
|
35084
|
+
return {
|
|
35085
|
+
listening: true,
|
|
35086
|
+
pids: fresh.map((one) => one.pid),
|
|
35087
|
+
note: `${fresh.length} games of this project announced at once and none could be told from the process tree as this run's own game rather than one it started, so none is taken as its runtime. editor_status lists them under runtimes.`
|
|
35088
|
+
};
|
|
35089
|
+
}
|
|
34406
35090
|
return runtimeVerdict(endpoint, {
|
|
34407
35091
|
addon: true,
|
|
34408
35092
|
budgetMs,
|
|
@@ -34486,7 +35170,13 @@ class GodotServer {
|
|
|
34486
35170
|
};
|
|
34487
35171
|
this.activeProcess = played;
|
|
34488
35172
|
this.watchThePlayedConsole();
|
|
34489
|
-
|
|
35173
|
+
const which = playOf(playAnswer);
|
|
35174
|
+
writeEditorRunNote({
|
|
35175
|
+
projectPath,
|
|
35176
|
+
transcript: transcript.path,
|
|
35177
|
+
startedAt,
|
|
35178
|
+
...which === null ? {} : { play: which }
|
|
35179
|
+
});
|
|
34490
35180
|
sweepTranscripts();
|
|
34491
35181
|
return this.jsonTextResponse({
|
|
34492
35182
|
started: true,
|
|
@@ -34601,7 +35291,7 @@ class GodotServer {
|
|
|
34601
35291
|
return 0;
|
|
34602
35292
|
}
|
|
34603
35293
|
try {
|
|
34604
|
-
return reportAlreadyInTranscript(
|
|
35294
|
+
return reportAlreadyInTranscript(readFileSync18(game.transcript), readFileSync18(report));
|
|
34605
35295
|
} catch (error) {
|
|
34606
35296
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
34607
35297
|
return 0;
|
|
@@ -34615,6 +35305,18 @@ class GodotServer {
|
|
|
34615
35305
|
game.brokeOn = halt.text;
|
|
34616
35306
|
game.log.record("error", halt.text);
|
|
34617
35307
|
}
|
|
35308
|
+
async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
|
|
35309
|
+
const deadline = Date.now() + withinMs;
|
|
35310
|
+
for (;; ) {
|
|
35311
|
+
if ((await this.editorPlayingState())?.playing === false) {
|
|
35312
|
+
return true;
|
|
35313
|
+
}
|
|
35314
|
+
if (Date.now() >= deadline) {
|
|
35315
|
+
return false;
|
|
35316
|
+
}
|
|
35317
|
+
await delay3(100);
|
|
35318
|
+
}
|
|
35319
|
+
}
|
|
34618
35320
|
async endActiveGame(reason, going) {
|
|
34619
35321
|
const running = this.activeProcess;
|
|
34620
35322
|
const project = running?.projectPath ?? this.ourProject();
|
|
@@ -34633,8 +35335,16 @@ class GodotServer {
|
|
|
34633
35335
|
running.endedHere = reason;
|
|
34634
35336
|
const announced = this.announcedPidOf(running);
|
|
34635
35337
|
if (running.throughEditor) {
|
|
34636
|
-
await this.handleViaBridge("stop_playing", {});
|
|
34637
|
-
|
|
35338
|
+
const asked = await this.handleViaBridge("stop_playing", {});
|
|
35339
|
+
if (asked.isError === true) {
|
|
35340
|
+
running.endedHere = null;
|
|
35341
|
+
running.log.record("warning", `This run was not ended here: the editor could not be asked to stop it (${asked.content[0]?.text ?? "no reason given"}).`);
|
|
35342
|
+
return "unreached";
|
|
35343
|
+
}
|
|
35344
|
+
if (announced === undefined) {
|
|
35345
|
+
return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
|
|
35346
|
+
}
|
|
35347
|
+
return await untilGone([announced]) ? "gone" : "lingering";
|
|
34638
35348
|
}
|
|
34639
35349
|
if (running.pid === null) {
|
|
34640
35350
|
return "gone";
|
|
@@ -34643,23 +35353,30 @@ class GodotServer {
|
|
|
34643
35353
|
pid: running.pid,
|
|
34644
35354
|
transcript: running.transcript ?? "",
|
|
34645
35355
|
startedAt: running.startedAt,
|
|
35356
|
+
...running.startedBy === undefined ? {} : { startedBy: running.startedBy },
|
|
34646
35357
|
projectPath: running.projectPath ?? "",
|
|
34647
35358
|
arguments: [],
|
|
34648
35359
|
...running.command === undefined ? {} : { command: running.command }
|
|
34649
35360
|
};
|
|
34650
|
-
|
|
35361
|
+
const why = await whyNotStillTheRecordedRun(startedAs);
|
|
35362
|
+
if (why !== null) {
|
|
34651
35363
|
clearTheRecord();
|
|
34652
35364
|
running.endedHere = null;
|
|
34653
|
-
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as, so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
35365
|
+
running.log.record("warning", `This run was not ended here: pid ${running.pid} no longer answers as the process the run was started as (${why}), so nothing was signalled. If that process is still the game, end it yourself; if it is not, it belongs to something else.`);
|
|
34654
35366
|
return "refused";
|
|
34655
35367
|
}
|
|
34656
35368
|
const signalled = await runningAs(running.pid);
|
|
34657
35369
|
const described = signalled === null ? "nothing it would name" : `${signalled.kind}: ${signalled.text}`;
|
|
34658
35370
|
let landed = true;
|
|
35371
|
+
let declined = null;
|
|
34659
35372
|
try {
|
|
34660
35373
|
process.kill(running.pid);
|
|
34661
|
-
} catch {
|
|
35374
|
+
} catch (error) {
|
|
34662
35375
|
landed = false;
|
|
35376
|
+
const code = error.code;
|
|
35377
|
+
if (code !== "ESRCH") {
|
|
35378
|
+
declined = code ?? errorMessage(error);
|
|
35379
|
+
}
|
|
34663
35380
|
}
|
|
34664
35381
|
const gone = await untilGone([running.pid]);
|
|
34665
35382
|
if (gone) {
|
|
@@ -34670,11 +35387,11 @@ class GodotServer {
|
|
|
34670
35387
|
running.exitSignal = "SIGTERM";
|
|
34671
35388
|
}
|
|
34672
35389
|
clearTheRecord();
|
|
34673
|
-
running.log.record("info", landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
35390
|
+
running.log.record(gone ? "info" : "warning", declined !== null ? `gdharness could not signal pid ${running.pid}: the operating system refused it (${declined}). It had described that process as ${described}.` : !gone ? `gdharness signalled pid ${running.pid}, which the operating system described as ${described}, and it was still running ${STOP_WAIT_MS / 1000} seconds later.` : landed ? `gdharness ended pid ${running.pid}, which the operating system described as ${described}.` : `gdharness signalled pid ${running.pid} and it was already gone; the operating system had described it as ${described}.`);
|
|
34674
35391
|
return gone ? "gone" : "lingering";
|
|
34675
35392
|
}
|
|
34676
35393
|
spawnGame(godotPath, cmdArgs, env) {
|
|
34677
|
-
const child =
|
|
35394
|
+
const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
34678
35395
|
const cancelled = callSignal();
|
|
34679
35396
|
if (cancelled !== undefined) {
|
|
34680
35397
|
const end = () => {
|
|
@@ -34714,7 +35431,6 @@ class GodotServer {
|
|
|
34714
35431
|
});
|
|
34715
35432
|
child.on("exit", (code, signal) => {
|
|
34716
35433
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
34717
|
-
log.finish();
|
|
34718
35434
|
started.exitCode = code;
|
|
34719
35435
|
started.exitSignal = code === null ? signal : null;
|
|
34720
35436
|
});
|
|
@@ -34724,7 +35440,28 @@ class GodotServer {
|
|
|
34724
35440
|
`);
|
|
34725
35441
|
started.exitCode = -1;
|
|
34726
35442
|
});
|
|
34727
|
-
|
|
35443
|
+
const settled = new Promise((resolve) => {
|
|
35444
|
+
let open = 2;
|
|
35445
|
+
const done = () => {
|
|
35446
|
+
log.finish();
|
|
35447
|
+
resolve();
|
|
35448
|
+
};
|
|
35449
|
+
const closed = () => {
|
|
35450
|
+
open -= 1;
|
|
35451
|
+
if (open === 0) {
|
|
35452
|
+
done();
|
|
35453
|
+
}
|
|
35454
|
+
};
|
|
35455
|
+
child.stdout.once("close", closed);
|
|
35456
|
+
child.stderr.once("close", closed);
|
|
35457
|
+
child.once("exit", () => {
|
|
35458
|
+
setTimeout(done, STREAMS_GRACE_MS).unref();
|
|
35459
|
+
});
|
|
35460
|
+
child.once("error", () => {
|
|
35461
|
+
done();
|
|
35462
|
+
});
|
|
35463
|
+
});
|
|
35464
|
+
return Object.assign(started, { settled });
|
|
34728
35465
|
}
|
|
34729
35466
|
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
34730
35467
|
const startedAt = Date.now();
|
|
@@ -34734,12 +35471,13 @@ class GodotServer {
|
|
|
34734
35471
|
command: godotPath,
|
|
34735
35472
|
args: cmdArgs,
|
|
34736
35473
|
...env === undefined ? {} : { env },
|
|
34737
|
-
run: { transcript: transcript.path, startedAt, projectPath },
|
|
35474
|
+
run: { transcript: transcript.path, startedAt, projectPath, ...this.servedBy() },
|
|
34738
35475
|
...desktop === undefined ? {} : { desktop }
|
|
34739
35476
|
});
|
|
34740
35477
|
if ("error" in launched) {
|
|
34741
35478
|
return launched;
|
|
34742
35479
|
}
|
|
35480
|
+
const startedBy = Date.now();
|
|
34743
35481
|
sweepTranscripts();
|
|
34744
35482
|
return {
|
|
34745
35483
|
pid: launched.pid,
|
|
@@ -34748,6 +35486,7 @@ class GodotServer {
|
|
|
34748
35486
|
readOffset: 0,
|
|
34749
35487
|
projectPath,
|
|
34750
35488
|
startedAt,
|
|
35489
|
+
startedBy,
|
|
34751
35490
|
command: godotPath,
|
|
34752
35491
|
exitCode: null,
|
|
34753
35492
|
exitSignal: null,
|
|
@@ -34806,7 +35545,8 @@ class GodotServer {
|
|
|
34806
35545
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
34807
35546
|
}
|
|
34808
35547
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
34809
|
-
const
|
|
35548
|
+
const recorded = project === null ? null : readEditorRunNote(project);
|
|
35549
|
+
const note = recorded !== null && noteIsAbout(recorded, playing.play) ? recorded : null;
|
|
34810
35550
|
const picked = {
|
|
34811
35551
|
pid: null,
|
|
34812
35552
|
log: new GameLog,
|
|
@@ -34863,6 +35603,31 @@ class GodotServer {
|
|
|
34863
35603
|
const its = await this.tiedThroughTheTree(run, running);
|
|
34864
35604
|
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
34865
35605
|
}
|
|
35606
|
+
async theRunsAmong(run, fresh) {
|
|
35607
|
+
if (fresh.length === 1) {
|
|
35608
|
+
return fresh[0] ?? null;
|
|
35609
|
+
}
|
|
35610
|
+
if (fresh.length === 0 || run === null) {
|
|
35611
|
+
return null;
|
|
35612
|
+
}
|
|
35613
|
+
const pid = run.announcedPid ?? await this.tiedThroughTheTree(run, fresh);
|
|
35614
|
+
return fresh.find((one) => one.pid === pid) ?? null;
|
|
35615
|
+
}
|
|
35616
|
+
async tieThePlayedGame(run) {
|
|
35617
|
+
if (!run.throughEditor || run.errorReport !== undefined || run.announcedPid !== undefined) {
|
|
35618
|
+
return;
|
|
35619
|
+
}
|
|
35620
|
+
const announced = runtimesAnnounced().running;
|
|
35621
|
+
if (this.announcedPidOf(run, announced) !== undefined || !stillRunning(run)) {
|
|
35622
|
+
return;
|
|
35623
|
+
}
|
|
35624
|
+
const among = this.announcedOfTheRunsProject(run, announced).map((one) => one.pid).sort((one, other) => one - other).join(",");
|
|
35625
|
+
if (among === "" || run.treeReadFor === among) {
|
|
35626
|
+
return;
|
|
35627
|
+
}
|
|
35628
|
+
run.treeReadFor = among;
|
|
35629
|
+
await this.tiedThroughTheTree(run, announced);
|
|
35630
|
+
}
|
|
34866
35631
|
async tiedThroughTheTree(run, announced) {
|
|
34867
35632
|
if (run.announcedPid !== undefined) {
|
|
34868
35633
|
return run.announcedPid;
|
|
@@ -34941,8 +35706,9 @@ class GodotServer {
|
|
|
34941
35706
|
}
|
|
34942
35707
|
async adoptRecordedRun() {
|
|
34943
35708
|
const project = this.ourProject();
|
|
34944
|
-
const
|
|
34945
|
-
|
|
35709
|
+
const found = project === null ? [] : [readRunRecord(project), this.startedForUs(project)];
|
|
35710
|
+
const record = found.reduce((newest, one) => one !== null && (newest === null || one.startedAt > newest.startedAt) ? one : newest, null);
|
|
35711
|
+
if (record === null || !(this.couldBeOurs(record.projectPath) || this.couldBeOurs(record.servedBy ?? ""))) {
|
|
34946
35712
|
return null;
|
|
34947
35713
|
}
|
|
34948
35714
|
const adopted = {
|
|
@@ -34952,6 +35718,7 @@ class GodotServer {
|
|
|
34952
35718
|
readOffset: 0,
|
|
34953
35719
|
projectPath: record.projectPath === "" ? null : record.projectPath,
|
|
34954
35720
|
startedAt: record.startedAt,
|
|
35721
|
+
...record.startedBy === undefined ? {} : { startedBy: record.startedBy },
|
|
34955
35722
|
...record.command === undefined ? {} : { command: record.command },
|
|
34956
35723
|
exitCode: null,
|
|
34957
35724
|
exitSignal: null,
|
|
@@ -34972,6 +35739,13 @@ class GodotServer {
|
|
|
34972
35739
|
this.activeProcess = adopted;
|
|
34973
35740
|
return adopted;
|
|
34974
35741
|
}
|
|
35742
|
+
servedBy() {
|
|
35743
|
+
const mine = this.ourProject();
|
|
35744
|
+
return mine === null ? {} : { servedBy: mine };
|
|
35745
|
+
}
|
|
35746
|
+
startedForUs(project) {
|
|
35747
|
+
return everyRunRecord().find((one) => one.servedBy !== undefined && isSameDirectory(one.servedBy, project) && !(one.projectPath !== "" && isSameDirectory(one.projectPath, project))) ?? null;
|
|
35748
|
+
}
|
|
34975
35749
|
nothingOfOursIsRunning() {
|
|
34976
35750
|
const status = this.godotBridge.getStatus();
|
|
34977
35751
|
const noEditor = !status.connected ? " No editor is connected, so a game the editor is playing cannot be seen from here at all:" + " editor_status says whether one is on its way, and a run of yours that is still up is" + " reachable again once it is." : "";
|
|
@@ -35063,6 +35837,7 @@ class GodotServer {
|
|
|
35063
35837
|
resolve(false);
|
|
35064
35838
|
});
|
|
35065
35839
|
});
|
|
35840
|
+
await boot.settled;
|
|
35066
35841
|
const errors = boot.log.count("error");
|
|
35067
35842
|
const warnings = boot.log.count("warning");
|
|
35068
35843
|
return this.jsonTextResponse({
|
|
@@ -35137,22 +35912,20 @@ class GodotServer {
|
|
|
35137
35912
|
}
|
|
35138
35913
|
async handleEditorConsole(args) {
|
|
35139
35914
|
const status = this.godotBridge.getStatus();
|
|
35140
|
-
const
|
|
35141
|
-
|
|
35142
|
-
|
|
35143
|
-
|
|
35144
|
-
"editor_launch opens one, and an editor opened here has its console captured"
|
|
35145
|
-
]);
|
|
35915
|
+
const greeted = hasSaidWhoItIs(status);
|
|
35916
|
+
const launched = greeted ? null : this.launchedEditor?.pid ?? null;
|
|
35917
|
+
if (!greeted && launched === null && status.connected) {
|
|
35918
|
+
return this.createErrorResponse("The editor has connected and has not yet said who it is, so whose console to read cannot be told yet.", ["Ask again in a moment; editor_status shows greeting until it has"]);
|
|
35146
35919
|
}
|
|
35147
|
-
const
|
|
35148
|
-
if (!
|
|
35920
|
+
const projectPath = status.projectPath ?? this.ownProject ?? undefined;
|
|
35921
|
+
if (projectPath === undefined || !greeted && launched === null) {
|
|
35149
35922
|
return this.createErrorResponse("No editor is connected, so there is no console to read.", [
|
|
35150
35923
|
"editor_status says whether one is connected and whether it may yet be",
|
|
35151
35924
|
"editor_launch opens one, and an editor opened here has its console captured"
|
|
35152
35925
|
]);
|
|
35153
35926
|
}
|
|
35154
|
-
const editorPid =
|
|
35155
|
-
const console2 = editorConsole(projectPath, editorPid,
|
|
35927
|
+
const editorPid = greeted ? status.editorPid ?? null : launched;
|
|
35928
|
+
const console2 = editorConsole(projectPath, editorPid, greeted ? status.openedByAServer === true : true);
|
|
35156
35929
|
if ("kind" in console2) {
|
|
35157
35930
|
return this.createErrorResponse(theConsoleWasNotCaptured(console2, projectPath), [
|
|
35158
35931
|
"editor_launch restart replaces this editor with one whose console is captured",
|
|
@@ -35204,6 +35977,7 @@ class GodotServer {
|
|
|
35204
35977
|
if (!run) {
|
|
35205
35978
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
35206
35979
|
}
|
|
35980
|
+
await this.tieThePlayedGame(run);
|
|
35207
35981
|
this.drainEditorOutput(run);
|
|
35208
35982
|
this.drainTranscript(run);
|
|
35209
35983
|
const going = await this.runStillGoing(run);
|
|
@@ -35239,7 +36013,7 @@ class GodotServer {
|
|
|
35239
36013
|
notes.push(endedWithoutACode({
|
|
35240
36014
|
throughEditor: run.throughEditor,
|
|
35241
36015
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
35242
|
-
couldAnnounce: run.projectPath !== null &&
|
|
36016
|
+
couldAnnounce: run.projectPath !== null && existsSync14(join19(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
35243
36017
|
endedHere: typeof run.endedHere === "string",
|
|
35244
36018
|
pickedUp: run.pickedUp === true
|
|
35245
36019
|
}));
|
|
@@ -35247,6 +36021,9 @@ class GodotServer {
|
|
|
35247
36021
|
if (run.pickedUpPlaying === true) {
|
|
35248
36022
|
notes.push(run.transcript !== null ? "The editor was already playing this when this server reached it, and what it printed before that has been read back from its transcript. It is the run the editor is holding now, which is the one the debug_* tools answer for." : run.consoleFromReport === true ? "The editor was already playing this when this server reached it, and the server that started it left no transcript, so what is below was read back from the file the game's runtime writes its console to, which begins where the runtime started: the engine's own start-up before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for." : "The editor was already playing this when this server reached it, and the server that started it left no transcript, so this is not the run from its start: what it printed before that is only in the editor. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
35249
36023
|
}
|
|
36024
|
+
if (errorsUnread(run)) {
|
|
36025
|
+
notes.push("The editor plays this run and no report from its game's runtime addon has been found, so what the game raised with push_error and the engine's own errors are not counted here: the editor's debugger relays what a game prints and not what it reports. errors counts what was heard, and clean is left out rather than said. A game with the runtime addon writes that report once it announces itself.");
|
|
36026
|
+
}
|
|
35250
36027
|
if (run.consoleLost === true) {
|
|
35251
36028
|
const back = this.dapClient?.isConnected() === true;
|
|
35252
36029
|
notes.push(`The debug adapter this run's console arrives over went away while the run was going, so what is below has a hole in it and the counts are of what was heard rather than of what was printed. ${back ? "It is connected again, and the line marking where is in the log." : "It is still gone, so nothing further will arrive here."} The project's own user://logs/godot.log has the rest, and "no entries" here means "not heard" rather than a quiet run.`);
|
|
@@ -35262,8 +36039,9 @@ class GodotServer {
|
|
|
35262
36039
|
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
35263
36040
|
errors: run.log.count("error"),
|
|
35264
36041
|
warnings: run.log.count("warning"),
|
|
35265
|
-
clean: run
|
|
36042
|
+
clean: cleanVerdict(run),
|
|
35266
36043
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
36044
|
+
errorsUnread: errorsUnread(run) ? true : undefined,
|
|
35267
36045
|
...hold,
|
|
35268
36046
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
35269
36047
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
@@ -35298,6 +36076,7 @@ class GodotServer {
|
|
|
35298
36076
|
if (elsewhere !== null) {
|
|
35299
36077
|
return elsewhere;
|
|
35300
36078
|
}
|
|
36079
|
+
await this.tieThePlayedGame(stopped);
|
|
35301
36080
|
this.drainEditorOutput(stopped);
|
|
35302
36081
|
this.drainTranscript(stopped);
|
|
35303
36082
|
const wasRunning = await this.runStillGoing(stopped);
|
|
@@ -35306,7 +36085,7 @@ class GodotServer {
|
|
|
35306
36085
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
35307
36086
|
this.logDebug("Stopping the running game");
|
|
35308
36087
|
const ending = await this.endActiveGame("editor_run stop", wasRunning);
|
|
35309
|
-
const refused = ending === "refused";
|
|
36088
|
+
const refused = ending === "refused" || ending === "unreached";
|
|
35310
36089
|
const ended = children === null || refused ? null : endChildrenAmong(children);
|
|
35311
36090
|
if (wasRunning && !refused) {
|
|
35312
36091
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
@@ -35321,13 +36100,14 @@ class GodotServer {
|
|
|
35321
36100
|
endedPid,
|
|
35322
36101
|
throughEditor: stopped.throughEditor,
|
|
35323
36102
|
exitSignal: stopped.exitSignal,
|
|
35324
|
-
|
|
36103
|
+
clean: cleanVerdict(stopped)
|
|
35325
36104
|
});
|
|
35326
36105
|
return this.jsonTextResponse({
|
|
35327
36106
|
stopped: verdict.stopped,
|
|
35328
36107
|
through: stopped.throughEditor ? "editor" : "gdharness",
|
|
35329
36108
|
endedPid: verdict.endedPid,
|
|
35330
36109
|
notSignalled: verdict.notSignalled,
|
|
36110
|
+
stillGoing: verdict.stillGoing,
|
|
35331
36111
|
...childrenFields(ended),
|
|
35332
36112
|
exitedBeforeStop: !wasRunning,
|
|
35333
36113
|
exitCode: stopped.exitCode,
|
|
@@ -35335,6 +36115,7 @@ class GodotServer {
|
|
|
35335
36115
|
errors: stopped.log.count("error"),
|
|
35336
36116
|
warnings: stopped.log.count("warning"),
|
|
35337
36117
|
clean: verdict.clean,
|
|
36118
|
+
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
35338
36119
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
35339
36120
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
35340
36121
|
});
|
|
@@ -35384,12 +36165,13 @@ class GodotServer {
|
|
|
35384
36165
|
await announcementEnded(pid);
|
|
35385
36166
|
}
|
|
35386
36167
|
return this.jsonTextResponse({
|
|
35387
|
-
stopped:
|
|
36168
|
+
stopped: gone,
|
|
35388
36169
|
through: "gdharness",
|
|
35389
|
-
endedPid: pid,
|
|
36170
|
+
endedPid: gone ? pid : undefined,
|
|
36171
|
+
stillGoing: gone ? undefined : pid,
|
|
35390
36172
|
...childrenFields(ended),
|
|
35391
36173
|
exitedBeforeStop: !landed,
|
|
35392
|
-
note: `${landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" :
|
|
36174
|
+
note: `${!gone ? `pid ${pid} was told to end and was still running ${STOP_WAIT_MS / 1000} seconds later, so it has not stopped: a game of this project that no server here was holding,` : landed ? `pid ${pid} was ended: a game of this project that no server here was holding,` : `pid ${pid} had already gone when it was signalled: a game of this project that no server here was holding,`} identified by its announcement and by its command line, ${read.executable} run with --path ${read.projectPath ?? ""}. Nothing here read what it printed or holds its exit code, so neither is in this answer.${gone ? "" : " editor_status says when it has gone."} ${aboutTheChildren(ended, false)}`
|
|
35393
36175
|
});
|
|
35394
36176
|
}
|
|
35395
36177
|
async whatTheRunStarted(run) {
|
|
@@ -35536,7 +36318,8 @@ class GodotServer {
|
|
|
35536
36318
|
}
|
|
35537
36319
|
}
|
|
35538
36320
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
35539
|
-
const
|
|
36321
|
+
const declaredNow = before === null || after === null ? null : declaredClasses(projectPath);
|
|
36322
|
+
const lost = before === null || after === null || declaredNow === null ? [] : [...before.keys()].filter((name) => !after.has(name) && declaredNow.has(name));
|
|
35540
36323
|
if (lost.length > 0) {
|
|
35541
36324
|
this.noteAShortListEditor(projectPath);
|
|
35542
36325
|
}
|
|
@@ -35547,7 +36330,12 @@ class GodotServer {
|
|
|
35547
36330
|
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
35548
36331
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
35549
36332
|
let reloaded = {};
|
|
35550
|
-
|
|
36333
|
+
const reloadingSource = reloading === undefined || projectPath === "" ? null : this.sourceOfClass(projectPath, reloading);
|
|
36334
|
+
if (reloading !== undefined && reloadingSource !== null && isToolScript(reloadingSource)) {
|
|
36335
|
+
reloaded = {
|
|
36336
|
+
problem: `${reloading} is a @tool script, which runs inside the editor, so it is not reloaded from inside a call: editor_launch restart recompiles it.`
|
|
36337
|
+
};
|
|
36338
|
+
} else if (reloading !== undefined) {
|
|
35551
36339
|
try {
|
|
35552
36340
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
35553
36341
|
const methods = readArray(answer, "methods");
|
|
@@ -35555,29 +36343,36 @@ class GodotServer {
|
|
|
35555
36343
|
const found = readArray(answer, field);
|
|
35556
36344
|
return found ? { [field]: found.map(String) } : {};
|
|
35557
36345
|
};
|
|
36346
|
+
const asResource = (path) => path.startsWith("res://") ? path : `res://${path.replace(/^\/+/, "")}`;
|
|
36347
|
+
const script = asResource(readString(answer, "script") ?? reloading);
|
|
36348
|
+
const spelling = script === asResource(reloading) ? {} : { as: script };
|
|
35558
36349
|
const failed = answer["failed"];
|
|
35559
36350
|
if (typeof failed === "number" && failed !== 0) {
|
|
35560
36351
|
reloaded = {
|
|
36352
|
+
...spelling,
|
|
35561
36353
|
...listed("heldBefore"),
|
|
35562
36354
|
...listed("heldConstantsBefore"),
|
|
35563
36355
|
problem: failedReloadNote({
|
|
35564
|
-
script
|
|
36356
|
+
script,
|
|
35565
36357
|
code: failed,
|
|
35566
36358
|
codeName: readString(answer, "failedAs") ?? "",
|
|
35567
36359
|
heldBefore: listed("heldBefore")["heldBefore"] ?? [],
|
|
35568
36360
|
heldAfter: methods?.map(String) ?? [],
|
|
35569
36361
|
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
35570
36362
|
constantsAfter: listed("constants")["constants"] ?? [],
|
|
35571
|
-
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath,
|
|
36363
|
+
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
|
|
36364
|
+
restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
|
|
36365
|
+
restoredAs: readString(answer, "restoredAs") ?? ""
|
|
35572
36366
|
})
|
|
35573
36367
|
};
|
|
35574
36368
|
} else {
|
|
35575
36369
|
reloaded = methods ? {
|
|
36370
|
+
...spelling,
|
|
35576
36371
|
methods: methods.map(String),
|
|
35577
36372
|
...listed("heldBefore"),
|
|
35578
36373
|
...listed("constants"),
|
|
35579
36374
|
...listed("heldConstantsBefore")
|
|
35580
|
-
} : { problem: `the editor would not say what ${
|
|
36375
|
+
} : { problem: `the editor would not say what ${script} has after reloading it` };
|
|
35581
36376
|
}
|
|
35582
36377
|
} catch (error) {
|
|
35583
36378
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
@@ -35585,7 +36380,7 @@ class GodotServer {
|
|
|
35585
36380
|
}
|
|
35586
36381
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
35587
36382
|
const unseen = checked.unseen;
|
|
35588
|
-
const broughtIn = blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
36383
|
+
const broughtIn = busy ? [] : blind.unseen.map((one) => one.className).filter((name) => !unseen.some((still) => still.className === name));
|
|
35589
36384
|
const dependents = busy || projectPath === "" || broughtIn.length === 0 ? null : await this.reloadDependents(projectPath, broughtIn);
|
|
35590
36385
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
35591
36386
|
const notes = [];
|
|
@@ -35599,7 +36394,7 @@ class GodotServer {
|
|
|
35599
36394
|
notes.push("The scan finished and these classes are still not in the list the editor resolves against, so every use of them reads as an unknown identifier. Its walk skips a file another engine has already imported. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
|
|
35600
36395
|
}
|
|
35601
36396
|
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
35602
|
-
notes.push(
|
|
36397
|
+
notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
|
|
35603
36398
|
}
|
|
35604
36399
|
for (const group of uidsDuplicatedOnDisk) {
|
|
35605
36400
|
notes.push(`${group.join(" and ")} name one UID in their .uid files, so Godot keeps one path for it and every engine warns "UID duplicate detected". Delete the .uid of the one that is a copy and rescan: the editor writes it a UID of its own.`);
|
|
@@ -35614,6 +36409,7 @@ class GodotServer {
|
|
|
35614
36409
|
stillWorking: busy,
|
|
35615
36410
|
waitedMs: Date.now() - started,
|
|
35616
36411
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
36412
|
+
reloadedAs: reloaded.as,
|
|
35617
36413
|
heldBeforeReload: reloaded.heldBefore,
|
|
35618
36414
|
reloadedMethods: reloaded.methods,
|
|
35619
36415
|
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
@@ -35640,7 +36436,8 @@ class GodotServer {
|
|
|
35640
36436
|
if (tool) {
|
|
35641
36437
|
notReloaded.push({
|
|
35642
36438
|
scriptPath,
|
|
35643
|
-
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
|
|
36439
|
+
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it",
|
|
36440
|
+
tool: true
|
|
35644
36441
|
});
|
|
35645
36442
|
continue;
|
|
35646
36443
|
}
|
|
@@ -35648,9 +36445,10 @@ class GodotServer {
|
|
|
35648
36445
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
35649
36446
|
const failed = answer["failed"];
|
|
35650
36447
|
if (typeof failed === "number" && failed !== 0) {
|
|
36448
|
+
const left = answer["restored"] === true ? "" : answer["restored"] === false ? "; compiling it again from the text it was built from failed too, so the editor holds it unusable until it compiles" : "; a failed compile leaves the copy the editor holds unusable until it compiles, and this editor does not put it back";
|
|
35651
36449
|
notReloaded.push({
|
|
35652
36450
|
scriptPath,
|
|
35653
|
-
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
|
|
36451
|
+
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
|
|
35654
36452
|
});
|
|
35655
36453
|
} else if (readArray(answer, "methods") === undefined) {
|
|
35656
36454
|
notReloaded.push({
|
|
@@ -35722,6 +36520,35 @@ class GodotServer {
|
|
|
35722
36520
|
}
|
|
35723
36521
|
};
|
|
35724
36522
|
}
|
|
36523
|
+
async handleCreateScript(args) {
|
|
36524
|
+
const project = this.project(args);
|
|
36525
|
+
if (!project.ok) {
|
|
36526
|
+
return project.response;
|
|
36527
|
+
}
|
|
36528
|
+
const contained = this.containProjectFiles(args);
|
|
36529
|
+
if (!contained.ok) {
|
|
36530
|
+
return contained.response;
|
|
36531
|
+
}
|
|
36532
|
+
const { op: _op, projectPath: _projectPath, ...params } = contained.value;
|
|
36533
|
+
const created = await this.operation("create_script", params, project.value.path);
|
|
36534
|
+
const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
|
|
36535
|
+
if (!created.ok || declared === "") {
|
|
36536
|
+
return this.answer(created);
|
|
36537
|
+
}
|
|
36538
|
+
const rebuilt = await this.rebuildClassCache(project.value.path);
|
|
36539
|
+
const registered = cachedClasses(project.value.path)?.get(declared) === readString(created.payload, "full_path");
|
|
36540
|
+
const answered = this.answer({
|
|
36541
|
+
...created,
|
|
36542
|
+
payload: {
|
|
36543
|
+
...created.payload,
|
|
36544
|
+
registered,
|
|
36545
|
+
...registered ? {} : {
|
|
36546
|
+
registrationNote: rebuilt.ok ? `The class list was rebuilt and does not hold ${declared} at this script, so nothing started next resolves it by name. project_import refresh_classes says what the list holds and why.` : `The class list could not be rebuilt, so nothing started next resolves ${declared} by name: ${rebuilt.message}`
|
|
36547
|
+
}
|
|
36548
|
+
}
|
|
36549
|
+
});
|
|
36550
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
36551
|
+
}
|
|
35725
36552
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
35726
36553
|
const first = answered.content[0];
|
|
35727
36554
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -35794,7 +36621,7 @@ class GodotServer {
|
|
|
35794
36621
|
]);
|
|
35795
36622
|
}
|
|
35796
36623
|
try {
|
|
35797
|
-
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest()));
|
|
36624
|
+
return this.jsonTextResponse(markIfStale(await this.godotBridge.invokeTool(toolName, args), this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk()));
|
|
35798
36625
|
} catch (error) {
|
|
35799
36626
|
return this.createErrorResponse(`The editor answered ${toolName} with an error: ${errorMessage(error)}`);
|
|
35800
36627
|
}
|
|
@@ -35850,7 +36677,7 @@ class GodotServer {
|
|
|
35850
36677
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
35851
36678
|
const run = await this.currentRun();
|
|
35852
36679
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
35853
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
36680
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync14(join19(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
35854
36681
|
return null;
|
|
35855
36682
|
}
|
|
35856
36683
|
const already = runtimesAnnounced().running;
|
|
@@ -35874,11 +36701,15 @@ class GodotServer {
|
|
|
35874
36701
|
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
35875
36702
|
return `The game this server started (${which}) has been running for ${seconds}s and has not announced its runtime, so there is nothing to talk to. A game that keeps the runtime off, as a project's own loader can for some scenes, never announces; editor_output says what it has printed.`;
|
|
35876
36703
|
}
|
|
35877
|
-
const
|
|
36704
|
+
const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
35878
36705
|
budgetMs: Math.min(budgetMs, remaining),
|
|
35879
36706
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
|
|
35880
36707
|
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
35881
36708
|
});
|
|
36709
|
+
const endpoint = await this.theRunsAmong(run, fresh);
|
|
36710
|
+
if (fresh.length > 0 && endpoint === null) {
|
|
36711
|
+
return null;
|
|
36712
|
+
}
|
|
35882
36713
|
if (endpoint !== null) {
|
|
35883
36714
|
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
35884
36715
|
if (!tiedMeanwhile()) {
|
|
@@ -35930,7 +36761,7 @@ class GodotServer {
|
|
|
35930
36761
|
}
|
|
35931
36762
|
}
|
|
35932
36763
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
35933
|
-
const screenshotPath = screenshotDir ?
|
|
36764
|
+
const screenshotPath = screenshotDir ? join19(screenshotDir, "capture.png") : null;
|
|
35934
36765
|
try {
|
|
35935
36766
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
35936
36767
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -35971,7 +36802,7 @@ class GodotServer {
|
|
|
35971
36802
|
type: "text",
|
|
35972
36803
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
35973
36804
|
},
|
|
35974
|
-
{ type: "image", data:
|
|
36805
|
+
{ type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
35975
36806
|
]
|
|
35976
36807
|
};
|
|
35977
36808
|
} finally {
|
|
@@ -36068,6 +36899,7 @@ class GodotServer {
|
|
|
36068
36899
|
}
|
|
36069
36900
|
}
|
|
36070
36901
|
async function runGodotServer() {
|
|
36902
|
+
delete process.env[EDITOR_PID_VARIABLE];
|
|
36071
36903
|
const server = new GodotServer;
|
|
36072
36904
|
await server.run();
|
|
36073
36905
|
}
|