gdharness 1.1.21 → 1.1.22
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 +1036 -525
- 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 +8 -4
- 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 +28 -8
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +7 -299
- 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 +20 -9
- 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/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 +25 -10
- 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 +949 -435
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -10201,27 +10201,27 @@ function defectReport(where, error, context = {}) {
|
|
|
10201
10201
|
}
|
|
10202
10202
|
|
|
10203
10203
|
// src/server.ts
|
|
10204
|
-
import { execFile as
|
|
10204
|
+
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
10205
10205
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
10206
10206
|
import {
|
|
10207
10207
|
appendFileSync,
|
|
10208
10208
|
closeSync as closeSync2,
|
|
10209
10209
|
constants,
|
|
10210
10210
|
copyFileSync,
|
|
10211
|
-
existsSync as
|
|
10211
|
+
existsSync as existsSync14,
|
|
10212
10212
|
fstatSync,
|
|
10213
10213
|
mkdirSync as mkdirSync10,
|
|
10214
10214
|
openSync as openSync3,
|
|
10215
10215
|
readdirSync as readdirSync8,
|
|
10216
|
-
readFileSync as
|
|
10216
|
+
readFileSync as readFileSync18,
|
|
10217
10217
|
readSync as readSync2,
|
|
10218
10218
|
rmdirSync as rmdirSync2,
|
|
10219
10219
|
statSync as statSync8
|
|
10220
10220
|
} from "node:fs";
|
|
10221
|
-
import { basename as basename4, dirname as
|
|
10221
|
+
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
10222
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10223
10223
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
10224
|
-
import { promisify as
|
|
10224
|
+
import { promisify as promisify5 } from "node:util";
|
|
10225
10225
|
|
|
10226
10226
|
// node_modules/zod/v3/helpers/util.js
|
|
10227
10227
|
var util;
|
|
@@ -24914,6 +24914,12 @@ function resolveWithinProject(projectPath, candidatePath) {
|
|
|
24914
24914
|
return { ok: false, reason: "Path contains a null byte." };
|
|
24915
24915
|
}
|
|
24916
24916
|
const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : trimmed;
|
|
24917
|
+
if (withoutScheme.startsWith("uid://")) {
|
|
24918
|
+
return {
|
|
24919
|
+
ok: false,
|
|
24920
|
+
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.`
|
|
24921
|
+
};
|
|
24922
|
+
}
|
|
24917
24923
|
const scheme = OTHER_SCHEME.exec(withoutScheme);
|
|
24918
24924
|
if (scheme) {
|
|
24919
24925
|
return {
|
|
@@ -25620,13 +25626,13 @@ async function announcedSince(projectPath, before, waiting = {}) {
|
|
|
25620
25626
|
const directories = waiting.directories ?? runtimeDirectories();
|
|
25621
25627
|
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
25622
25628
|
for (;; ) {
|
|
25623
|
-
const fresh = discoverRuntimes(directories).
|
|
25624
|
-
if (fresh
|
|
25629
|
+
const fresh = discoverRuntimes(directories).filter((endpoint) => !before.has(endpoint.pid) && isSameDirectory(endpoint.project.path, projectPath) && (waiting.accept?.(endpoint) ?? true));
|
|
25630
|
+
if (fresh.length > 0) {
|
|
25625
25631
|
return fresh;
|
|
25626
25632
|
}
|
|
25627
25633
|
const left = until - Date.now();
|
|
25628
25634
|
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
25629
|
-
return
|
|
25635
|
+
return [];
|
|
25630
25636
|
}
|
|
25631
25637
|
await delay(Math.min(LOOK_EVERY_MS, left));
|
|
25632
25638
|
}
|
|
@@ -26033,6 +26039,10 @@ function callSignal() {
|
|
|
26033
26039
|
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
|
|
26034
26040
|
import { join as join6 } from "node:path";
|
|
26035
26041
|
var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
26042
|
+
var TOOL = /^[ \t]*(?:@[a-z_]+(?:\([^)\n]*\))?[ \t]+)*@tool\b/m;
|
|
26043
|
+
function isToolScript(source) {
|
|
26044
|
+
return TOOL.test(codeOf(source));
|
|
26045
|
+
}
|
|
26036
26046
|
function declaredClasses(projectPath) {
|
|
26037
26047
|
const declared = new Map;
|
|
26038
26048
|
eachScript(projectPath, (script, source) => {
|
|
@@ -26072,7 +26082,7 @@ function scriptsNaming(projectPath, classes) {
|
|
|
26072
26082
|
}
|
|
26073
26083
|
const code = codeOf(source);
|
|
26074
26084
|
if (classesNamedIn(code, named).length > 0) {
|
|
26075
|
-
naming.push({ script, tool:
|
|
26085
|
+
naming.push({ script, tool: TOOL.test(code) });
|
|
26076
26086
|
}
|
|
26077
26087
|
});
|
|
26078
26088
|
return naming.sort((a, b) => a.script.localeCompare(b.script));
|
|
@@ -26279,6 +26289,19 @@ function classRemedy(entries) {
|
|
|
26279
26289
|
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
26290
|
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
26291
|
}
|
|
26292
|
+
function notReloadedNote(broughtIn, notReloaded) {
|
|
26293
|
+
const tried = notReloaded.filter((one) => one.tool !== true).map((one) => one.scriptPath);
|
|
26294
|
+
const tools = notReloaded.filter((one) => one.tool === true).map((one) => one.scriptPath);
|
|
26295
|
+
const all = [...tried, ...tools];
|
|
26296
|
+
const remedies = [];
|
|
26297
|
+
if (tried.length > 0) {
|
|
26298
|
+
remedies.push(`For ${tried.join(", ")}: editor_rescan with reloadScript on ${tried.length === 1 ? "it" : "each"}, or editor_launch restart.`);
|
|
26299
|
+
}
|
|
26300
|
+
if (tools.length > 0) {
|
|
26301
|
+
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.`);
|
|
26302
|
+
}
|
|
26303
|
+
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(" ")}`;
|
|
26304
|
+
}
|
|
26282
26305
|
function failedReloadNote(failed) {
|
|
26283
26306
|
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
26284
26307
|
const lost = [
|
|
@@ -26287,7 +26310,17 @@ function failedReloadNote(failed) {
|
|
|
26287
26310
|
];
|
|
26288
26311
|
const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
|
|
26289
26312
|
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
26290
|
-
const
|
|
26313
|
+
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.";
|
|
26314
|
+
let copy;
|
|
26315
|
+
if (failed.restored === null) {
|
|
26316
|
+
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.`;
|
|
26317
|
+
} else if (!failed.restored) {
|
|
26318
|
+
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.`;
|
|
26319
|
+
} else if (unchanged) {
|
|
26320
|
+
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.";
|
|
26321
|
+
} else {
|
|
26322
|
+
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.`;
|
|
26323
|
+
}
|
|
26291
26324
|
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
26325
|
return `${failed.script} did not compile: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
|
|
26293
26326
|
}
|
|
@@ -26526,7 +26559,14 @@ function bindable(port, host) {
|
|
|
26526
26559
|
var DEFAULT_DAP_PORT = 6006;
|
|
26527
26560
|
var DAP_REQUEST_TIMEOUT_MS = 1e4;
|
|
26528
26561
|
var SCOPES_TIMEOUT_MS = 20000;
|
|
26562
|
+
var STEP_LANDS_WITHIN_MS = 1e4;
|
|
26529
26563
|
var VARIABLES_POLL_TIMEOUT_MS = 2000;
|
|
26564
|
+
function fileKey(path) {
|
|
26565
|
+
return process.platform === "win32" ? path.replaceAll("\\", "/").toLowerCase() : path;
|
|
26566
|
+
}
|
|
26567
|
+
|
|
26568
|
+
class ReadRefusal extends Refusal {
|
|
26569
|
+
}
|
|
26530
26570
|
function said(body, field) {
|
|
26531
26571
|
const value = body?.[field];
|
|
26532
26572
|
return typeof value === "string" ? value : "";
|
|
@@ -26543,17 +26583,25 @@ class GodotDAPClient {
|
|
|
26543
26583
|
outputBuffer = [];
|
|
26544
26584
|
onOutput = null;
|
|
26545
26585
|
maxOutputLines = 1000;
|
|
26586
|
+
outputDropped = 0;
|
|
26587
|
+
outputTaken = 0;
|
|
26546
26588
|
initialized = false;
|
|
26547
26589
|
attached = false;
|
|
26548
26590
|
lastThreadId = 1;
|
|
26549
26591
|
halt = null;
|
|
26550
26592
|
holdKnown = false;
|
|
26593
|
+
stops = 0;
|
|
26551
26594
|
breakpoints = new Map;
|
|
26595
|
+
spelled = new Map;
|
|
26552
26596
|
inEditor = new Map;
|
|
26553
26597
|
onBreakpointsChanged = null;
|
|
26554
|
-
|
|
26598
|
+
scopesPatienceMs;
|
|
26599
|
+
stepPatienceMs;
|
|
26600
|
+
constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
|
|
26555
26601
|
this.port = port;
|
|
26556
26602
|
this.host = host;
|
|
26603
|
+
this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
|
|
26604
|
+
this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
|
|
26557
26605
|
this.pendingRequests = new Map;
|
|
26558
26606
|
}
|
|
26559
26607
|
async connect() {
|
|
@@ -26714,6 +26762,7 @@ class GodotDAPClient {
|
|
|
26714
26762
|
}
|
|
26715
26763
|
}
|
|
26716
26764
|
if (this.outputBuffer.length > this.maxOutputLines) {
|
|
26765
|
+
this.outputDropped += this.outputBuffer.length - this.maxOutputLines;
|
|
26717
26766
|
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - this.maxOutputLines);
|
|
26718
26767
|
}
|
|
26719
26768
|
return;
|
|
@@ -26730,6 +26779,7 @@ class GodotDAPClient {
|
|
|
26730
26779
|
text: said(body, "text")
|
|
26731
26780
|
};
|
|
26732
26781
|
this.holdKnown = true;
|
|
26782
|
+
this.stops += 1;
|
|
26733
26783
|
return;
|
|
26734
26784
|
}
|
|
26735
26785
|
if (eventName === "continued") {
|
|
@@ -26746,20 +26796,24 @@ class GodotDAPClient {
|
|
|
26746
26796
|
if (typeof path !== "string" || typeof line !== "number") {
|
|
26747
26797
|
return;
|
|
26748
26798
|
}
|
|
26799
|
+
const key = fileKey(path);
|
|
26800
|
+
if (!this.spelled.has(key)) {
|
|
26801
|
+
this.spelled.set(key, path);
|
|
26802
|
+
}
|
|
26749
26803
|
if (said(body, "reason") === "removed") {
|
|
26750
|
-
this.inEditor.get(
|
|
26751
|
-
const held = this.breakpoints.get(
|
|
26804
|
+
this.inEditor.get(key)?.delete(line);
|
|
26805
|
+
const held = this.breakpoints.get(key);
|
|
26752
26806
|
if (held?.delete(line) === true) {
|
|
26753
26807
|
if (held.size === 0) {
|
|
26754
|
-
this.breakpoints.delete(
|
|
26808
|
+
this.breakpoints.delete(key);
|
|
26755
26809
|
}
|
|
26756
26810
|
this.onBreakpointsChanged?.(this.breakpointsHeld());
|
|
26757
26811
|
}
|
|
26758
26812
|
return;
|
|
26759
26813
|
}
|
|
26760
|
-
const known = this.inEditor.get(
|
|
26814
|
+
const known = this.inEditor.get(key) ?? new Set;
|
|
26761
26815
|
known.add(line);
|
|
26762
|
-
this.inEditor.set(
|
|
26816
|
+
this.inEditor.set(key, known);
|
|
26763
26817
|
return;
|
|
26764
26818
|
}
|
|
26765
26819
|
if (eventName === "terminated" || eventName === "exited") {
|
|
@@ -26842,24 +26896,44 @@ class GodotDAPClient {
|
|
|
26842
26896
|
getOutput(clear = false) {
|
|
26843
26897
|
const lines = [...this.outputBuffer];
|
|
26844
26898
|
if (clear) {
|
|
26899
|
+
this.outputTaken += lines.length;
|
|
26845
26900
|
this.outputBuffer = [];
|
|
26846
26901
|
}
|
|
26847
26902
|
return lines;
|
|
26848
26903
|
}
|
|
26904
|
+
outputSoFar() {
|
|
26905
|
+
const output = this.getOutput(false);
|
|
26906
|
+
const notShown = this.outputDropped + this.outputTaken;
|
|
26907
|
+
if (notShown === 0) {
|
|
26908
|
+
return { lines: output.length, output };
|
|
26909
|
+
}
|
|
26910
|
+
const where = [
|
|
26911
|
+
this.outputDropped > 0 ? `${this.outputDropped} fell out of this buffer, which keeps the last ${this.maxOutputLines}` : "",
|
|
26912
|
+
this.outputTaken > 0 ? `${this.outputTaken} were moved into the run's log by an earlier read` : ""
|
|
26913
|
+
].filter((part) => part !== "");
|
|
26914
|
+
return {
|
|
26915
|
+
lines: output.length,
|
|
26916
|
+
output,
|
|
26917
|
+
notShown,
|
|
26918
|
+
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.`
|
|
26919
|
+
};
|
|
26920
|
+
}
|
|
26849
26921
|
async setBreakpoint(filePath, line) {
|
|
26850
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
26922
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
26851
26923
|
wanted.add(line);
|
|
26852
26924
|
return await this.sendBreakpoints(filePath, wanted);
|
|
26853
26925
|
}
|
|
26854
26926
|
async removeBreakpoint(filePath, line) {
|
|
26855
|
-
const wanted = new Set(this.breakpoints.get(filePath) ?? []);
|
|
26927
|
+
const wanted = new Set(this.breakpoints.get(fileKey(filePath)) ?? []);
|
|
26856
26928
|
wanted.delete(line);
|
|
26857
26929
|
return await this.sendBreakpoints(filePath, wanted, line);
|
|
26858
26930
|
}
|
|
26859
26931
|
async sendBreakpoints(filePath, mine, clearing) {
|
|
26860
26932
|
await this.ensureConnected();
|
|
26861
26933
|
await this.initialize();
|
|
26862
|
-
const
|
|
26934
|
+
const key = fileKey(filePath);
|
|
26935
|
+
this.spelled.set(key, filePath);
|
|
26936
|
+
const lines = new Set([...mine, ...this.inEditor.get(key) ?? []]);
|
|
26863
26937
|
if (clearing !== undefined) {
|
|
26864
26938
|
lines.delete(clearing);
|
|
26865
26939
|
}
|
|
@@ -26869,14 +26943,14 @@ class GodotDAPClient {
|
|
|
26869
26943
|
breakpoints: sorted.map((breakpointLine) => ({ line: breakpointLine }))
|
|
26870
26944
|
});
|
|
26871
26945
|
if (mine.size === 0) {
|
|
26872
|
-
this.breakpoints.delete(
|
|
26946
|
+
this.breakpoints.delete(key);
|
|
26873
26947
|
} else {
|
|
26874
|
-
this.breakpoints.set(
|
|
26948
|
+
this.breakpoints.set(key, new Set(mine));
|
|
26875
26949
|
}
|
|
26876
26950
|
if (lines.size === 0) {
|
|
26877
|
-
this.inEditor.delete(
|
|
26951
|
+
this.inEditor.delete(key);
|
|
26878
26952
|
} else {
|
|
26879
|
-
this.inEditor.set(
|
|
26953
|
+
this.inEditor.set(key, lines);
|
|
26880
26954
|
}
|
|
26881
26955
|
return answer;
|
|
26882
26956
|
}
|
|
@@ -26885,11 +26959,11 @@ class GodotDAPClient {
|
|
|
26885
26959
|
}
|
|
26886
26960
|
breakpointsInEditor() {
|
|
26887
26961
|
const theirs = [];
|
|
26888
|
-
for (const [
|
|
26889
|
-
const mine = this.breakpoints.get(
|
|
26962
|
+
for (const [key, lines] of this.inEditor) {
|
|
26963
|
+
const mine = this.breakpoints.get(key);
|
|
26890
26964
|
const rest = Array.from(lines).filter((line) => !mine?.has(line)).sort((a, b) => a - b);
|
|
26891
26965
|
if (rest.length > 0) {
|
|
26892
|
-
theirs.push({ scriptPath, lines: rest });
|
|
26966
|
+
theirs.push({ scriptPath: this.spelled.get(key) ?? key, lines: rest });
|
|
26893
26967
|
}
|
|
26894
26968
|
}
|
|
26895
26969
|
return theirs;
|
|
@@ -26897,12 +26971,13 @@ class GodotDAPClient {
|
|
|
26897
26971
|
async reapplyBreakpoints() {
|
|
26898
26972
|
const applied = [];
|
|
26899
26973
|
const refused = [];
|
|
26900
|
-
for (const [
|
|
26974
|
+
for (const [key, lines] of Array.from(this.breakpoints)) {
|
|
26975
|
+
const filePath = this.spelled.get(key) ?? key;
|
|
26901
26976
|
try {
|
|
26902
26977
|
await this.sendBreakpoints(filePath, lines);
|
|
26903
26978
|
applied.push({ scriptPath: filePath, lines: Array.from(lines).sort((a, b) => a - b) });
|
|
26904
26979
|
} catch (error) {
|
|
26905
|
-
this.breakpoints.delete(
|
|
26980
|
+
this.breakpoints.delete(key);
|
|
26906
26981
|
refused.push({
|
|
26907
26982
|
scriptPath: filePath,
|
|
26908
26983
|
reason: error instanceof Error ? error.message : String(error)
|
|
@@ -26915,19 +26990,21 @@ class GodotDAPClient {
|
|
|
26915
26990
|
return { applied, refused };
|
|
26916
26991
|
}
|
|
26917
26992
|
breakpointsHeld() {
|
|
26918
|
-
return Array.from(this.breakpoints, ([
|
|
26919
|
-
scriptPath,
|
|
26993
|
+
return Array.from(this.breakpoints, ([key, lines]) => ({
|
|
26994
|
+
scriptPath: this.spelled.get(key) ?? key,
|
|
26920
26995
|
lines: Array.from(lines).sort((a, b) => a - b)
|
|
26921
26996
|
}));
|
|
26922
26997
|
}
|
|
26923
26998
|
holdBreakpoints(held) {
|
|
26924
26999
|
for (const { scriptPath, lines } of held) {
|
|
26925
|
-
const
|
|
27000
|
+
const key = fileKey(scriptPath);
|
|
27001
|
+
const fileBreakpoints = this.breakpoints.get(key) ?? new Set;
|
|
26926
27002
|
for (const line of lines) {
|
|
26927
27003
|
fileBreakpoints.add(line);
|
|
26928
27004
|
}
|
|
26929
27005
|
if (fileBreakpoints.size > 0) {
|
|
26930
|
-
this.breakpoints.set(
|
|
27006
|
+
this.breakpoints.set(key, fileBreakpoints);
|
|
27007
|
+
this.spelled.set(key, scriptPath);
|
|
26931
27008
|
}
|
|
26932
27009
|
}
|
|
26933
27010
|
}
|
|
@@ -26938,15 +27015,16 @@ class GodotDAPClient {
|
|
|
26938
27015
|
this.halt = null;
|
|
26939
27016
|
this.holdKnown = true;
|
|
26940
27017
|
}
|
|
26941
|
-
async
|
|
27018
|
+
async step(command) {
|
|
26942
27019
|
await this.attach();
|
|
26943
|
-
const resolvedThreadId = await this.resolveThreadId(
|
|
26944
|
-
|
|
26945
|
-
|
|
26946
|
-
|
|
26947
|
-
|
|
26948
|
-
|
|
26949
|
-
|
|
27020
|
+
const resolvedThreadId = await this.resolveThreadId();
|
|
27021
|
+
const before = this.stops;
|
|
27022
|
+
await this.sendRequest(command, { threadId: resolvedThreadId });
|
|
27023
|
+
const deadline = Date.now() + this.stepPatienceMs;
|
|
27024
|
+
while (this.stops === before && this.connected && Date.now() < deadline) {
|
|
27025
|
+
await delay2(50);
|
|
27026
|
+
}
|
|
27027
|
+
return this.stops !== before;
|
|
26950
27028
|
}
|
|
26951
27029
|
async getStackTrace(threadId) {
|
|
26952
27030
|
await this.attach();
|
|
@@ -26972,16 +27050,16 @@ class GodotDAPClient {
|
|
|
26972
27050
|
} catch {}
|
|
26973
27051
|
await delay2(200);
|
|
26974
27052
|
}
|
|
26975
|
-
return
|
|
27053
|
+
return null;
|
|
26976
27054
|
}
|
|
26977
27055
|
async getScopes(frameId) {
|
|
26978
27056
|
await this.attach();
|
|
26979
27057
|
const frames = await this.getStackTrace();
|
|
26980
27058
|
const frame = frameId === undefined ? frames[0] : frames.find((each) => each["id"] === frameId);
|
|
26981
27059
|
if (frame === undefined) {
|
|
26982
|
-
|
|
27060
|
+
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
27061
|
}
|
|
26984
|
-
const deadline = Date.now() +
|
|
27062
|
+
const deadline = Date.now() + this.scopesPatienceMs;
|
|
26985
27063
|
let scopes = [];
|
|
26986
27064
|
while (scopes.length === 0 && Date.now() < deadline) {
|
|
26987
27065
|
try {
|
|
@@ -26993,6 +27071,9 @@ class GodotDAPClient {
|
|
|
26993
27071
|
} catch {}
|
|
26994
27072
|
await delay2(200);
|
|
26995
27073
|
}
|
|
27074
|
+
if (scopes.length === 0) {
|
|
27075
|
+
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`);
|
|
27076
|
+
}
|
|
26996
27077
|
const named = [];
|
|
26997
27078
|
for (const scope of scopes) {
|
|
26998
27079
|
const reference = scope["variablesReference"];
|
|
@@ -27056,9 +27137,8 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27056
27137
|
try {
|
|
27057
27138
|
switch (toolName) {
|
|
27058
27139
|
case "dap_get_output": {
|
|
27059
|
-
const output = client.getOutput(false);
|
|
27060
27140
|
return {
|
|
27061
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
27141
|
+
content: [{ type: "text", text: JSON.stringify(client.outputSoFar(), null, 2) }]
|
|
27062
27142
|
};
|
|
27063
27143
|
}
|
|
27064
27144
|
case "dap_set_breakpoint": {
|
|
@@ -27085,19 +27165,13 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27085
27165
|
}
|
|
27086
27166
|
case "dap_step_over":
|
|
27087
27167
|
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
|
-
]
|
|
27168
|
+
const heldAgain = await client.step(toolName === "dap_step_into" ? "stepIn" : "next");
|
|
27169
|
+
const answer = heldAgain ? { stepped: true, heldAgain, stack: await client.getStackTrace() } : {
|
|
27170
|
+
stepped: true,
|
|
27171
|
+
heldAgain,
|
|
27172
|
+
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
27173
|
};
|
|
27174
|
+
return { content: [{ type: "text", text: JSON.stringify(answer, null, 2) }] };
|
|
27101
27175
|
}
|
|
27102
27176
|
case "dap_get_stack_trace": {
|
|
27103
27177
|
const stack = await client.getStackTrace();
|
|
@@ -27108,8 +27182,14 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27108
27182
|
case "dap_get_variables": {
|
|
27109
27183
|
const frameId = typeof safeArgs.frameId === "number" ? safeArgs.frameId : undefined;
|
|
27110
27184
|
const scopes = await client.getScopes(frameId);
|
|
27185
|
+
const missing = scopes.filter((scope) => scope.variables === null).map((scope) => scope.name);
|
|
27186
|
+
const answer = missing.length === 0 ? { scopes } : {
|
|
27187
|
+
scopes,
|
|
27188
|
+
valuesMissing: missing,
|
|
27189
|
+
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.`
|
|
27190
|
+
};
|
|
27111
27191
|
return {
|
|
27112
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
27192
|
+
content: [{ type: "text", text: JSON.stringify(answer, null, 2) }]
|
|
27113
27193
|
};
|
|
27114
27194
|
}
|
|
27115
27195
|
default:
|
|
@@ -27117,10 +27197,12 @@ async function handleDAPTool(client, toolName, args) {
|
|
|
27117
27197
|
}
|
|
27118
27198
|
} catch (error) {
|
|
27119
27199
|
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.";
|
|
27200
|
+
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
27201
|
return {
|
|
27122
27202
|
isError: true,
|
|
27123
|
-
content: [
|
|
27203
|
+
content: [
|
|
27204
|
+
{ type: "text", text: `DAP tool '${toolName}' failed: ${message}.${cure === "" ? "" : ` ${cure}`}` }
|
|
27205
|
+
]
|
|
27124
27206
|
};
|
|
27125
27207
|
}
|
|
27126
27208
|
}
|
|
@@ -27482,6 +27564,47 @@ function linesBefore(entries, at, collapsed, wanted) {
|
|
|
27482
27564
|
return found.reverse();
|
|
27483
27565
|
}
|
|
27484
27566
|
|
|
27567
|
+
// src/engine-run.ts
|
|
27568
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
27569
|
+
function runEngine(executable, args, options = {}) {
|
|
27570
|
+
return new Promise((resolve) => {
|
|
27571
|
+
const log = new GameLog;
|
|
27572
|
+
let failure = null;
|
|
27573
|
+
const signal = callSignal();
|
|
27574
|
+
const child = spawn2(executable, [...args], {
|
|
27575
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27576
|
+
windowsHide: true,
|
|
27577
|
+
...signal === undefined ? {} : { signal }
|
|
27578
|
+
});
|
|
27579
|
+
child.stdout.on("data", (chunk) => {
|
|
27580
|
+
log.append("stdout", chunk);
|
|
27581
|
+
});
|
|
27582
|
+
child.stderr.on("data", (chunk) => {
|
|
27583
|
+
log.append("stderr", chunk);
|
|
27584
|
+
});
|
|
27585
|
+
const { timeout } = options;
|
|
27586
|
+
const timer = timeout === undefined ? undefined : setTimeout(() => {
|
|
27587
|
+
failure ??= `it ran past its ${timeout.said}`;
|
|
27588
|
+
child.kill();
|
|
27589
|
+
}, timeout.ms);
|
|
27590
|
+
child.on("error", (error) => {
|
|
27591
|
+
failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
|
|
27592
|
+
});
|
|
27593
|
+
child.on("close", (code, closedBy) => {
|
|
27594
|
+
clearTimeout(timer);
|
|
27595
|
+
log.finish();
|
|
27596
|
+
const started = child.pid !== undefined;
|
|
27597
|
+
resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
|
|
27598
|
+
});
|
|
27599
|
+
});
|
|
27600
|
+
}
|
|
27601
|
+
function howItEnded(run) {
|
|
27602
|
+
if (run.failure !== null) {
|
|
27603
|
+
return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
|
|
27604
|
+
}
|
|
27605
|
+
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
|
|
27606
|
+
}
|
|
27607
|
+
|
|
27485
27608
|
// src/godot-bridge.ts
|
|
27486
27609
|
import { randomUUID } from "node:crypto";
|
|
27487
27610
|
import { EventEmitter } from "node:events";
|
|
@@ -28148,10 +28271,8 @@ class GodotLocator {
|
|
|
28148
28271
|
}
|
|
28149
28272
|
|
|
28150
28273
|
// src/headless.ts
|
|
28151
|
-
import {
|
|
28152
|
-
import {
|
|
28153
|
-
import { join as join13 } from "node:path";
|
|
28154
|
-
import { promisify as promisify3 } from "node:util";
|
|
28274
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12, realpathSync as realpathSync2, writeFileSync as writeFileSync8 } from "node:fs";
|
|
28275
|
+
import { dirname as dirname7, join as join13 } from "node:path";
|
|
28155
28276
|
|
|
28156
28277
|
// src/reimport.ts
|
|
28157
28278
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -28277,7 +28398,6 @@ function lastTryOnExit() {
|
|
|
28277
28398
|
}
|
|
28278
28399
|
|
|
28279
28400
|
// src/headless.ts
|
|
28280
|
-
var run3 = promisify3(execFile3);
|
|
28281
28401
|
function snakeCased(params) {
|
|
28282
28402
|
const result = emptyRecord();
|
|
28283
28403
|
for (const [key, value] of Object.entries(params)) {
|
|
@@ -28286,34 +28406,58 @@ function snakeCased(params) {
|
|
|
28286
28406
|
}
|
|
28287
28407
|
return result;
|
|
28288
28408
|
}
|
|
28289
|
-
function
|
|
28290
|
-
|
|
28291
|
-
|
|
28292
|
-
|
|
28293
|
-
|
|
28294
|
-
|
|
28295
|
-
return parsed;
|
|
28296
|
-
}
|
|
28297
|
-
} catch {}
|
|
28298
|
-
}
|
|
28299
|
-
return null;
|
|
28409
|
+
function engineExtras(outcome) {
|
|
28410
|
+
return {
|
|
28411
|
+
...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
|
|
28412
|
+
...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
|
|
28413
|
+
...outcome.afterAnswer === undefined ? {} : { engine_after_answer: outcome.afterAnswer }
|
|
28414
|
+
};
|
|
28300
28415
|
}
|
|
28301
|
-
|
|
28302
|
-
|
|
28303
|
-
|
|
28304
|
-
|
|
28305
|
-
return log.
|
|
28416
|
+
var OWN_ERROR = "[ERROR] ";
|
|
28417
|
+
var MESSAGES_KEPT = 200;
|
|
28418
|
+
var ERRORS_NAMED = 3;
|
|
28419
|
+
function problemsIn(log) {
|
|
28420
|
+
return log.everything().flatMap((entry) => {
|
|
28421
|
+
if (entry.severity !== "info") {
|
|
28422
|
+
return [entry];
|
|
28423
|
+
}
|
|
28424
|
+
if (entry.source === "stderr" && entry.text.startsWith(OWN_ERROR)) {
|
|
28425
|
+
return [{ ...entry, severity: "error", text: entry.text.slice(OWN_ERROR.length) }];
|
|
28426
|
+
}
|
|
28427
|
+
return [];
|
|
28428
|
+
});
|
|
28429
|
+
}
|
|
28430
|
+
function reported(problems) {
|
|
28431
|
+
const omitted = Math.max(0, problems.length - MESSAGES_KEPT);
|
|
28432
|
+
return omitted === 0 ? { messages: problems } : { messages: problems.slice(omitted), messagesOmitted: omitted };
|
|
28306
28433
|
}
|
|
28307
|
-
function reason(
|
|
28308
|
-
const own =
|
|
28434
|
+
function reason(log, problems) {
|
|
28435
|
+
const own = log.everything().filter((entry) => entry.source === "stderr" && entry.severity === "info" && entry.text.startsWith(OWN_ERROR));
|
|
28309
28436
|
if (own.length > 0) {
|
|
28310
|
-
return own.join("; ");
|
|
28311
|
-
}
|
|
28312
|
-
const
|
|
28313
|
-
if (
|
|
28314
|
-
|
|
28437
|
+
return own.map((entry) => entry.text.slice(OWN_ERROR.length)).join("; ");
|
|
28438
|
+
}
|
|
28439
|
+
const errors = problems.filter((entry) => entry.severity === "error").map((entry) => entry.text);
|
|
28440
|
+
if (errors.length > 0) {
|
|
28441
|
+
const rest = errors.length - ERRORS_NAMED;
|
|
28442
|
+
return rest > 0 ? `${errors.slice(0, ERRORS_NAMED).join("; ")}; and ${rest} more error${rest === 1 ? "" : "s"}` : errors.join("; ");
|
|
28443
|
+
}
|
|
28444
|
+
const printed = log.everything().filter((entry) => entry.source === "stdout");
|
|
28445
|
+
return printed.at(-1)?.text ?? "no output at all";
|
|
28446
|
+
}
|
|
28447
|
+
function scriptFault(problems, script) {
|
|
28448
|
+
const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
|
|
28449
|
+
const directories = [...new Set([spelled(dirname7(script)), spelled(resolvedOr(dirname7(script)))])];
|
|
28450
|
+
return problems.find((entry) => {
|
|
28451
|
+
const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
|
|
28452
|
+
return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
|
|
28453
|
+
});
|
|
28454
|
+
}
|
|
28455
|
+
function resolvedOr(path) {
|
|
28456
|
+
try {
|
|
28457
|
+
return realpathSync2.native(path);
|
|
28458
|
+
} catch {
|
|
28459
|
+
return path;
|
|
28315
28460
|
}
|
|
28316
|
-
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
28317
28461
|
}
|
|
28318
28462
|
function librariesNotCopied(messages) {
|
|
28319
28463
|
const found = new Set;
|
|
@@ -28353,30 +28497,33 @@ async function runImport(godotPath, projectPath, options = {}) {
|
|
|
28353
28497
|
}
|
|
28354
28498
|
async function importOnce(godotPath, projectPath) {
|
|
28355
28499
|
const logDir = scratchDirectory("import");
|
|
28500
|
+
let ran;
|
|
28356
28501
|
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
|
-
};
|
|
28502
|
+
ran = await runEngine(godotPath, [
|
|
28503
|
+
"--headless",
|
|
28504
|
+
"--log-file",
|
|
28505
|
+
join13(logDir, "engine.log"),
|
|
28506
|
+
"--path",
|
|
28507
|
+
projectPath,
|
|
28508
|
+
"--import"
|
|
28509
|
+
]);
|
|
28373
28510
|
} finally {
|
|
28374
28511
|
discard(logDir);
|
|
28375
28512
|
}
|
|
28513
|
+
const problems = problemsIn(ran.log);
|
|
28514
|
+
if (ran.exitCode === 0 && ran.failure === null) {
|
|
28515
|
+
return { ok: true, ...reported(problems) };
|
|
28516
|
+
}
|
|
28517
|
+
return {
|
|
28518
|
+
ok: false,
|
|
28519
|
+
message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
|
|
28520
|
+
...reported(problems)
|
|
28521
|
+
};
|
|
28376
28522
|
}
|
|
28377
28523
|
async function runOperation(engine, operation, params, projectPath) {
|
|
28378
28524
|
const paramsDir = scratchDirectory("params");
|
|
28379
28525
|
const paramsFile = join13(paramsDir, `${operation}.json`);
|
|
28526
|
+
const answerFile = join13(paramsDir, "answer.json");
|
|
28380
28527
|
writeFileSync8(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
|
|
28381
28528
|
const args = [
|
|
28382
28529
|
"--headless",
|
|
@@ -28388,38 +28535,63 @@ async function runOperation(engine, operation, params, projectPath) {
|
|
|
28388
28535
|
engine.script,
|
|
28389
28536
|
operation,
|
|
28390
28537
|
`@file:${paramsFile}`,
|
|
28538
|
+
answerFile,
|
|
28391
28539
|
...engine.debug ? ["--debug-godot"] : []
|
|
28392
28540
|
];
|
|
28393
|
-
let
|
|
28394
|
-
let
|
|
28541
|
+
let ran;
|
|
28542
|
+
let answer;
|
|
28395
28543
|
try {
|
|
28396
|
-
|
|
28397
|
-
|
|
28398
|
-
|
|
28399
|
-
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
messages: problems(failed.stderr)
|
|
28404
|
-
};
|
|
28405
|
-
}
|
|
28544
|
+
ran = await runEngine(engine.godotPath, args);
|
|
28545
|
+
answer = answerIn(answerFile);
|
|
28546
|
+
} finally {
|
|
28547
|
+
discard(paramsDir);
|
|
28548
|
+
}
|
|
28549
|
+
const problems = problemsIn(ran.log);
|
|
28550
|
+
if (answer.kind === "unreadable") {
|
|
28406
28551
|
return {
|
|
28407
28552
|
ok: false,
|
|
28408
|
-
message: `${operation} could not be
|
|
28409
|
-
|
|
28553
|
+
message: `${operation} wrote an answer that could not be read (${answer.why})`,
|
|
28554
|
+
...reported(problems)
|
|
28410
28555
|
};
|
|
28411
|
-
} finally {
|
|
28412
|
-
discard(paramsDir);
|
|
28413
28556
|
}
|
|
28414
|
-
|
|
28415
|
-
|
|
28557
|
+
if (answer.kind === "none") {
|
|
28558
|
+
const ending = ran.exitCode === 0 && ran.failure === null ? "produced no result" : `failed (${howItEnded(ran)})`;
|
|
28416
28559
|
return {
|
|
28417
28560
|
ok: false,
|
|
28418
|
-
message: `${operation}
|
|
28419
|
-
|
|
28561
|
+
message: `${operation} ${ending}: ${reason(ran.log, problems)}`,
|
|
28562
|
+
...reported(problems)
|
|
28563
|
+
};
|
|
28564
|
+
}
|
|
28565
|
+
const fault = scriptFault(problems, engine.script);
|
|
28566
|
+
if (fault !== undefined) {
|
|
28567
|
+
return {
|
|
28568
|
+
ok: false,
|
|
28569
|
+
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"})`,
|
|
28570
|
+
...reported(problems)
|
|
28571
|
+
};
|
|
28572
|
+
}
|
|
28573
|
+
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.`;
|
|
28574
|
+
return {
|
|
28575
|
+
ok: true,
|
|
28576
|
+
payload: answer.payload,
|
|
28577
|
+
...reported(problems),
|
|
28578
|
+
...afterAnswer === undefined ? {} : { afterAnswer }
|
|
28579
|
+
};
|
|
28580
|
+
}
|
|
28581
|
+
function answerIn(file) {
|
|
28582
|
+
if (!existsSync10(file)) {
|
|
28583
|
+
return { kind: "none" };
|
|
28584
|
+
}
|
|
28585
|
+
const text = readFileSync12(file, "utf8");
|
|
28586
|
+
try {
|
|
28587
|
+
const parsed = JSON.parse(text);
|
|
28588
|
+
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)}` };
|
|
28589
|
+
} catch (error) {
|
|
28590
|
+
return {
|
|
28591
|
+
kind: "unreadable",
|
|
28592
|
+
why: `${error instanceof Error ? error.message : String(error)}: ${text.slice(0, 200)}`
|
|
28420
28593
|
};
|
|
28421
28594
|
}
|
|
28422
|
-
return { ok: true, payload, messages: problems(stderr) };
|
|
28423
28595
|
}
|
|
28424
28596
|
|
|
28425
28597
|
// src/headless-operations.ts
|
|
@@ -28620,13 +28792,17 @@ function parseJUnit(xml) {
|
|
|
28620
28792
|
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
28621
28793
|
const suites = suiteElements.map((suite) => {
|
|
28622
28794
|
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
28795
|
+
const failures = cases.filter((entry) => entry.status === "failed").length;
|
|
28796
|
+
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
28623
28797
|
return {
|
|
28624
28798
|
name: suite.attributes["name"] ?? "",
|
|
28625
28799
|
path: pathOf(suite),
|
|
28626
28800
|
tests: cases.length,
|
|
28627
28801
|
discovered: count(suite.attributes, "tests"),
|
|
28628
|
-
failures
|
|
28629
|
-
errors
|
|
28802
|
+
failures,
|
|
28803
|
+
errors,
|
|
28804
|
+
hookFailures: Math.max(0, count(suite.attributes, "failures") - failures),
|
|
28805
|
+
hookErrors: Math.max(0, count(suite.attributes, "errors") - errors),
|
|
28630
28806
|
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
28631
28807
|
time: count(suite.attributes, "time"),
|
|
28632
28808
|
cases
|
|
@@ -28637,13 +28813,22 @@ function parseJUnit(xml) {
|
|
|
28637
28813
|
tests: all.length,
|
|
28638
28814
|
failures: all.filter((entry) => entry.status === "failed").length,
|
|
28639
28815
|
errors: all.filter((entry) => entry.status === "error").length,
|
|
28816
|
+
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
28817
|
+
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
28640
28818
|
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
28641
28819
|
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
28642
28820
|
suites
|
|
28643
28821
|
};
|
|
28644
28822
|
}
|
|
28823
|
+
var HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
28824
|
+
function hookFailuresPrinted(printed) {
|
|
28825
|
+
return printed.flatMap((line) => {
|
|
28826
|
+
const found = HOOK_FAILURE.exec(line);
|
|
28827
|
+
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
28828
|
+
});
|
|
28829
|
+
}
|
|
28645
28830
|
var NOTHING_RAN = /no test cases found/i;
|
|
28646
|
-
var NO_SUCH_PATH = /given directory or file does not exists:\s*(\
|
|
28831
|
+
var NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
28647
28832
|
function whyNoReport(printed, asked) {
|
|
28648
28833
|
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
28649
28834
|
if (missing !== undefined) {
|
|
@@ -28730,13 +28915,17 @@ function withActualsPrinted(failed, printed) {
|
|
|
28730
28915
|
at: match[5] ?? "",
|
|
28731
28916
|
...readDiff(match[1] ?? "")
|
|
28732
28917
|
}));
|
|
28918
|
+
const used = new Set;
|
|
28733
28919
|
return failed.map((entry) => {
|
|
28734
28920
|
if (entry.detail === null) {
|
|
28735
28921
|
return entry;
|
|
28736
28922
|
}
|
|
28737
28923
|
let detail = entry.detail;
|
|
28738
28924
|
let recovered = false;
|
|
28739
|
-
for (const diff of diffs) {
|
|
28925
|
+
for (const [index, diff] of diffs.entries()) {
|
|
28926
|
+
if (used.has(index)) {
|
|
28927
|
+
continue;
|
|
28928
|
+
}
|
|
28740
28929
|
const wrong = ` but was
|
|
28741
28930
|
'${diff.merged.replaceAll("\r", "")}'${diff.suffix}
|
|
28742
28931
|
at ${diff.at}`;
|
|
@@ -28745,6 +28934,7 @@ function withActualsPrinted(failed, printed) {
|
|
|
28745
28934
|
'${diff.actual}'${diff.suffix}
|
|
28746
28935
|
at ${diff.at}`);
|
|
28747
28936
|
recovered = true;
|
|
28937
|
+
used.add(index);
|
|
28748
28938
|
}
|
|
28749
28939
|
}
|
|
28750
28940
|
const quoted = QUOTED_EQUALITY.exec(detail);
|
|
@@ -28768,7 +28958,7 @@ function masked(expected, actual) {
|
|
|
28768
28958
|
return `${expected}${actual}`.includes(MASKED_BRACKET) && allSpelled(expected) && allSpelled(actual);
|
|
28769
28959
|
}
|
|
28770
28960
|
var PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
28771
|
-
var RUNNING_SUITE = /Run Test Suite:\s*(
|
|
28961
|
+
var RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
28772
28962
|
var ORPHANS = /(\d+)\s+orphans/;
|
|
28773
28963
|
var OVERALL = "Overall Summary";
|
|
28774
28964
|
function orphansPrinted(printed) {
|
|
@@ -28803,13 +28993,24 @@ function orphansPrinted(printed) {
|
|
|
28803
28993
|
}
|
|
28804
28994
|
|
|
28805
28995
|
// src/lsp_client.ts
|
|
28806
|
-
import { realpathSync as
|
|
28996
|
+
import { existsSync as existsSync11, realpathSync as realpathSync3 } from "node:fs";
|
|
28807
28997
|
import { readFile, realpath } from "node:fs/promises";
|
|
28808
28998
|
import { createConnection as createConnection3 } from "node:net";
|
|
28809
|
-
import { dirname as
|
|
28999
|
+
import { dirname as dirname8, join as join14, resolve as resolve3 } from "node:path";
|
|
28810
29000
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28811
29001
|
var DEFAULT_LSP_PORT = 6005;
|
|
28812
29002
|
var DIAGNOSTICS_TIMEOUT_MS = 5000;
|
|
29003
|
+
|
|
29004
|
+
class ArgumentRefusal extends Refusal {
|
|
29005
|
+
}
|
|
29006
|
+
|
|
29007
|
+
class AnotherProjectsServer extends Refusal {
|
|
29008
|
+
serves;
|
|
29009
|
+
constructor(port, serves, asked) {
|
|
29010
|
+
super(`The language server on port ${port} belongs to the editor of ${serves}, not ${asked}.`);
|
|
29011
|
+
this.serves = serves;
|
|
29012
|
+
}
|
|
29013
|
+
}
|
|
28813
29014
|
function diagnosticsKey(uri) {
|
|
28814
29015
|
let decoded;
|
|
28815
29016
|
try {
|
|
@@ -28821,7 +29022,7 @@ function diagnosticsKey(uri) {
|
|
|
28821
29022
|
return decoded;
|
|
28822
29023
|
}
|
|
28823
29024
|
try {
|
|
28824
|
-
return
|
|
29025
|
+
return realpathSync3.native(fileURLToPath(decoded)).toLowerCase();
|
|
28825
29026
|
} catch {
|
|
28826
29027
|
return decoded.toLowerCase();
|
|
28827
29028
|
}
|
|
@@ -28838,8 +29039,10 @@ class GodotLSPClient {
|
|
|
28838
29039
|
connectPromise = null;
|
|
28839
29040
|
initialized = false;
|
|
28840
29041
|
rootPath = null;
|
|
29042
|
+
servedWorkspace = null;
|
|
28841
29043
|
diagnosticsWaiters = new Map;
|
|
28842
29044
|
documentVersions = new Map;
|
|
29045
|
+
documentTurns = new Map;
|
|
28843
29046
|
constructor(port = portFromEnv("GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT), host = "127.0.0.1") {
|
|
28844
29047
|
this.port = port;
|
|
28845
29048
|
this.host = host;
|
|
@@ -28936,7 +29139,7 @@ class GodotLSPClient {
|
|
|
28936
29139
|
if (this.initialized) {
|
|
28937
29140
|
return;
|
|
28938
29141
|
}
|
|
28939
|
-
const rootPath = this.rootPath ??
|
|
29142
|
+
const rootPath = this.rootPath ?? dirname8(resolve3(filePath));
|
|
28940
29143
|
await this.initialize(rootPath);
|
|
28941
29144
|
}
|
|
28942
29145
|
async sendRequest(method, params) {
|
|
@@ -29011,6 +29214,10 @@ class GodotLSPClient {
|
|
|
29011
29214
|
}
|
|
29012
29215
|
}
|
|
29013
29216
|
}
|
|
29217
|
+
if (message["method"] === "gdscript_client/changeWorkspace") {
|
|
29218
|
+
const path = message["params"]?.["path"];
|
|
29219
|
+
this.servedWorkspace = typeof path === "string" ? path : null;
|
|
29220
|
+
}
|
|
29014
29221
|
if (message["method"] === "textDocument/publishDiagnostics") {
|
|
29015
29222
|
const params = message["params"];
|
|
29016
29223
|
const paramsObject = params && typeof params === "object" ? params : null;
|
|
@@ -29113,10 +29320,14 @@ class GodotLSPClient {
|
|
|
29113
29320
|
this.sendNotification("textDocument/didClose", { textDocument: { uri } });
|
|
29114
29321
|
} catch {}
|
|
29115
29322
|
}
|
|
29323
|
+
forgetServedWorkspace() {
|
|
29324
|
+
this.servedWorkspace = null;
|
|
29325
|
+
}
|
|
29116
29326
|
async initialize(rootPath) {
|
|
29117
29327
|
await this.ensureConnected();
|
|
29118
29328
|
const resolvedRootPath = resolve3(rootPath);
|
|
29119
29329
|
const rootUri = pathToFileURL(resolvedRootPath).href;
|
|
29330
|
+
this.forgetServedWorkspace();
|
|
29120
29331
|
const result = await this.sendRequest("initialize", {
|
|
29121
29332
|
processId: process.pid,
|
|
29122
29333
|
rootPath: resolvedRootPath,
|
|
@@ -29142,60 +29353,82 @@ class GodotLSPClient {
|
|
|
29142
29353
|
}
|
|
29143
29354
|
]
|
|
29144
29355
|
});
|
|
29356
|
+
const served = this.servedWorkspace;
|
|
29357
|
+
if (served !== null && !isSameDirectory(served, resolvedRootPath)) {
|
|
29358
|
+
throw new AnotherProjectsServer(this.port, served, resolvedRootPath);
|
|
29359
|
+
}
|
|
29145
29360
|
this.sendNotification("initialized", {});
|
|
29146
29361
|
this.initialized = true;
|
|
29147
29362
|
this.rootPath = resolvedRootPath;
|
|
29148
29363
|
return result;
|
|
29149
29364
|
}
|
|
29150
|
-
async
|
|
29151
|
-
await this.ensureConnected();
|
|
29152
|
-
await this.ensureInitializedForFile(filePath);
|
|
29365
|
+
async withDocument(filePath, work) {
|
|
29153
29366
|
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;
|
|
29367
|
+
const before = this.documentTurns.get(key) ?? Promise.resolve();
|
|
29368
|
+
let done = () => {};
|
|
29369
|
+
const mine = new Promise((finished) => {
|
|
29370
|
+
done = finished;
|
|
29371
|
+
});
|
|
29372
|
+
const turn = before.then(() => mine);
|
|
29373
|
+
this.documentTurns.set(key, turn);
|
|
29374
|
+
await before;
|
|
29170
29375
|
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;
|
|
29376
|
+
return await work();
|
|
29180
29377
|
} finally {
|
|
29181
|
-
|
|
29182
|
-
|
|
29378
|
+
done();
|
|
29379
|
+
if (this.documentTurns.get(key) === turn) {
|
|
29380
|
+
this.documentTurns.delete(key);
|
|
29183
29381
|
}
|
|
29184
29382
|
}
|
|
29185
29383
|
}
|
|
29186
|
-
async
|
|
29384
|
+
async getDiagnostics(filePath, content) {
|
|
29187
29385
|
await this.ensureConnected();
|
|
29188
29386
|
await this.ensureInitializedForFile(filePath);
|
|
29189
|
-
|
|
29190
|
-
|
|
29191
|
-
|
|
29192
|
-
|
|
29193
|
-
|
|
29194
|
-
|
|
29387
|
+
return this.withDocument(filePath, async () => {
|
|
29388
|
+
const key = diagnosticsKey(this.toFileUri(filePath));
|
|
29389
|
+
const diagnosticsPromise = new Promise((resolveDiagnostics, rejectDiagnostics) => {
|
|
29390
|
+
const timer = setTimeout(() => {
|
|
29391
|
+
this.diagnosticsWaiters.delete(key);
|
|
29392
|
+
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."));
|
|
29393
|
+
}, DIAGNOSTICS_TIMEOUT_MS);
|
|
29394
|
+
this.diagnosticsWaiters.set(key, {
|
|
29395
|
+
resolve: resolveDiagnostics,
|
|
29396
|
+
reject: rejectDiagnostics,
|
|
29397
|
+
timer
|
|
29398
|
+
});
|
|
29195
29399
|
});
|
|
29196
|
-
|
|
29197
|
-
|
|
29198
|
-
|
|
29400
|
+
let opened = null;
|
|
29401
|
+
try {
|
|
29402
|
+
opened = this.syncDocument(filePath, content);
|
|
29403
|
+
return await diagnosticsPromise;
|
|
29404
|
+
} catch (error) {
|
|
29405
|
+
const waiter = this.diagnosticsWaiters.get(key);
|
|
29406
|
+
if (waiter) {
|
|
29407
|
+
clearTimeout(waiter.timer);
|
|
29408
|
+
this.diagnosticsWaiters.delete(key);
|
|
29409
|
+
}
|
|
29410
|
+
throw error;
|
|
29411
|
+
} finally {
|
|
29412
|
+
if (opened !== null) {
|
|
29413
|
+
this.closeDocument(opened);
|
|
29414
|
+
}
|
|
29415
|
+
}
|
|
29416
|
+
});
|
|
29417
|
+
}
|
|
29418
|
+
async getCompletions(filePath, content, line, character) {
|
|
29419
|
+
await this.ensureConnected();
|
|
29420
|
+
await this.ensureInitializedForFile(filePath);
|
|
29421
|
+
const result = await this.withDocument(filePath, async () => {
|
|
29422
|
+
const uri = this.syncDocument(filePath, content);
|
|
29423
|
+
try {
|
|
29424
|
+
return await this.sendRequest("textDocument/completion", {
|
|
29425
|
+
textDocument: { uri },
|
|
29426
|
+
position: { line, character }
|
|
29427
|
+
});
|
|
29428
|
+
} finally {
|
|
29429
|
+
this.closeDocument(uri);
|
|
29430
|
+
}
|
|
29431
|
+
});
|
|
29199
29432
|
if (Array.isArray(result)) {
|
|
29200
29433
|
return result;
|
|
29201
29434
|
}
|
|
@@ -29210,28 +29443,31 @@ class GodotLSPClient {
|
|
|
29210
29443
|
async getHover(filePath, content, line, character) {
|
|
29211
29444
|
await this.ensureConnected();
|
|
29212
29445
|
await this.ensureInitializedForFile(filePath);
|
|
29213
|
-
|
|
29214
|
-
|
|
29215
|
-
|
|
29216
|
-
textDocument
|
|
29217
|
-
|
|
29218
|
-
|
|
29219
|
-
|
|
29220
|
-
|
|
29221
|
-
|
|
29446
|
+
return this.withDocument(filePath, async () => {
|
|
29447
|
+
const uri = this.syncDocument(filePath, content);
|
|
29448
|
+
try {
|
|
29449
|
+
return await this.sendRequest("textDocument/hover", {
|
|
29450
|
+
textDocument: { uri },
|
|
29451
|
+
position: { line, character }
|
|
29452
|
+
});
|
|
29453
|
+
} finally {
|
|
29454
|
+
this.closeDocument(uri);
|
|
29455
|
+
}
|
|
29456
|
+
});
|
|
29222
29457
|
}
|
|
29223
29458
|
async getDocumentSymbols(filePath, content) {
|
|
29224
29459
|
await this.ensureConnected();
|
|
29225
29460
|
await this.ensureInitializedForFile(filePath);
|
|
29226
|
-
const
|
|
29227
|
-
|
|
29228
|
-
|
|
29229
|
-
|
|
29230
|
-
|
|
29231
|
-
|
|
29232
|
-
|
|
29233
|
-
|
|
29234
|
-
|
|
29461
|
+
const result = await this.withDocument(filePath, async () => {
|
|
29462
|
+
const uri = this.syncDocument(filePath, content);
|
|
29463
|
+
try {
|
|
29464
|
+
return await this.sendRequest("textDocument/documentSymbol", {
|
|
29465
|
+
textDocument: { uri }
|
|
29466
|
+
});
|
|
29467
|
+
} finally {
|
|
29468
|
+
this.closeDocument(uri);
|
|
29469
|
+
}
|
|
29470
|
+
});
|
|
29235
29471
|
return Array.isArray(result) ? result : [];
|
|
29236
29472
|
}
|
|
29237
29473
|
isConnected() {
|
|
@@ -29263,36 +29499,39 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
29263
29499
|
try {
|
|
29264
29500
|
projectPath = await realpath(requestedProjectPath);
|
|
29265
29501
|
} catch {
|
|
29266
|
-
throw new
|
|
29502
|
+
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
29503
|
+
}
|
|
29504
|
+
if (!existsSync11(join14(projectPath, "project.godot"))) {
|
|
29505
|
+
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
29267
29506
|
}
|
|
29268
29507
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
29269
29508
|
if (!contained.ok) {
|
|
29270
|
-
throw new
|
|
29509
|
+
throw new ArgumentRefusal(contained.reason);
|
|
29271
29510
|
}
|
|
29272
29511
|
let scriptPath;
|
|
29273
29512
|
try {
|
|
29274
29513
|
scriptPath = await realpath(contained.absolutePath);
|
|
29275
29514
|
} catch {
|
|
29276
|
-
throw new
|
|
29515
|
+
throw new ArgumentRefusal(`Script file does not exist: ${contained.absolutePath}`);
|
|
29277
29516
|
}
|
|
29278
29517
|
if (!isWithinRoot(projectPath, scriptPath)) {
|
|
29279
|
-
throw new
|
|
29518
|
+
throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
|
|
29280
29519
|
}
|
|
29281
29520
|
return { projectPath, scriptPath };
|
|
29282
29521
|
}
|
|
29283
29522
|
async function handleLSPTool(client, toolName, args) {
|
|
29284
29523
|
try {
|
|
29285
29524
|
if (!args || typeof args !== "object") {
|
|
29286
|
-
throw new
|
|
29525
|
+
throw new ArgumentRefusal("Tool arguments must be an object.");
|
|
29287
29526
|
}
|
|
29288
29527
|
const parsedArgs = args;
|
|
29289
29528
|
const projectPathValue = parsedArgs["projectPath"];
|
|
29290
29529
|
const scriptPathValue = parsedArgs["scriptPath"];
|
|
29291
29530
|
if (typeof projectPathValue !== "string" || projectPathValue.length === 0) {
|
|
29292
|
-
throw new
|
|
29531
|
+
throw new ArgumentRefusal("Missing required argument: projectPath");
|
|
29293
29532
|
}
|
|
29294
29533
|
if (typeof scriptPathValue !== "string" || scriptPathValue.length === 0) {
|
|
29295
|
-
throw new
|
|
29534
|
+
throw new ArgumentRefusal("Missing required argument: scriptPath");
|
|
29296
29535
|
}
|
|
29297
29536
|
const { projectPath, scriptPath } = await resolveLSPPaths(projectPathValue, scriptPathValue);
|
|
29298
29537
|
const content = await readFile(scriptPath, "utf8");
|
|
@@ -29306,7 +29545,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
29306
29545
|
const line = Number(parsedArgs["line"]);
|
|
29307
29546
|
const character = Number(parsedArgs["character"]);
|
|
29308
29547
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
29309
|
-
throw new
|
|
29548
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
29310
29549
|
}
|
|
29311
29550
|
const completions = await client.getCompletions(scriptPath, content, line, character);
|
|
29312
29551
|
return asToolResponse({ completions });
|
|
@@ -29315,7 +29554,7 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
29315
29554
|
const line = Number(parsedArgs["line"]);
|
|
29316
29555
|
const character = Number(parsedArgs["character"]);
|
|
29317
29556
|
if (!Number.isFinite(line) || !Number.isFinite(character)) {
|
|
29318
|
-
throw new
|
|
29557
|
+
throw new ArgumentRefusal("Arguments line and character must be numbers.");
|
|
29319
29558
|
}
|
|
29320
29559
|
const hover = await client.getHover(scriptPath, content, line, character);
|
|
29321
29560
|
return asToolResponse({ hover });
|
|
@@ -29329,13 +29568,15 @@ async function handleLSPTool(client, toolName, args) {
|
|
|
29329
29568
|
}
|
|
29330
29569
|
} catch (error) {
|
|
29331
29570
|
return asToolResponse({
|
|
29332
|
-
error: normalizeLSPError(error, client.port)
|
|
29571
|
+
error: normalizeLSPError(error, client.port),
|
|
29572
|
+
...error instanceof AnotherProjectsServer ? { serves: error.serves } : {},
|
|
29573
|
+
...error instanceof ArgumentRefusal ? { refusedArguments: true } : {}
|
|
29333
29574
|
});
|
|
29334
29575
|
}
|
|
29335
29576
|
}
|
|
29336
29577
|
|
|
29337
29578
|
// src/outside.ts
|
|
29338
|
-
import { spawn as
|
|
29579
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
29339
29580
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
29340
29581
|
function changesFrom(env, base) {
|
|
29341
29582
|
const changes = {};
|
|
@@ -29369,7 +29610,7 @@ function readLaunched(printed) {
|
|
|
29369
29610
|
}
|
|
29370
29611
|
async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = KEEPER_SCRIPT) {
|
|
29371
29612
|
return await new Promise((resolve) => {
|
|
29372
|
-
const launcher =
|
|
29613
|
+
const launcher = spawn3(runtime, [keeper, "launch"], {
|
|
29373
29614
|
stdio: ["pipe", "pipe", "pipe"],
|
|
29374
29615
|
windowsHide: true
|
|
29375
29616
|
});
|
|
@@ -29400,9 +29641,9 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
|
|
|
29400
29641
|
}
|
|
29401
29642
|
|
|
29402
29643
|
// src/process-time.ts
|
|
29403
|
-
import { execFile as
|
|
29404
|
-
import { promisify as
|
|
29405
|
-
var
|
|
29644
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
29645
|
+
import { promisify as promisify3 } from "node:util";
|
|
29646
|
+
var run3 = promisify3(execFile3);
|
|
29406
29647
|
var ASK_TIMEOUT_MS2 = 6000;
|
|
29407
29648
|
async function cpuSecondsOf(pid) {
|
|
29408
29649
|
try {
|
|
@@ -29417,7 +29658,7 @@ async function secondsOnWindows(pid) {
|
|
|
29417
29658
|
return Number.isFinite(seconds) ? seconds : undefined;
|
|
29418
29659
|
}
|
|
29419
29660
|
async function askPosix2(pid) {
|
|
29420
|
-
const { stdout } = await
|
|
29661
|
+
const { stdout } = await run3("ps", ["-o", "time=", "-p", String(pid)], { timeout: ASK_TIMEOUT_MS2 });
|
|
29421
29662
|
return secondsFromClock(stdout.trim());
|
|
29422
29663
|
}
|
|
29423
29664
|
function secondsFromClock(printed) {
|
|
@@ -29437,14 +29678,14 @@ function secondsFromClock(printed) {
|
|
|
29437
29678
|
}
|
|
29438
29679
|
|
|
29439
29680
|
// src/project-scan.ts
|
|
29440
|
-
import { existsSync as
|
|
29441
|
-
import { join as
|
|
29681
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5, readFileSync as readFileSync13 } from "node:fs";
|
|
29682
|
+
import { join as join15 } from "node:path";
|
|
29442
29683
|
var SKIPPED = new Set(["node_modules"]);
|
|
29443
29684
|
function skipped(name) {
|
|
29444
29685
|
return name.startsWith(".") || SKIPPED.has(name);
|
|
29445
29686
|
}
|
|
29446
29687
|
function steppedOver(directory) {
|
|
29447
|
-
return
|
|
29688
|
+
return existsSync12(join15(directory, ".gdignore"));
|
|
29448
29689
|
}
|
|
29449
29690
|
var ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
|
|
29450
29691
|
function projectStructure(projectPath) {
|
|
@@ -29458,7 +29699,7 @@ function projectStructure(projectPath) {
|
|
|
29458
29699
|
continue;
|
|
29459
29700
|
}
|
|
29460
29701
|
if (entry.isDirectory()) {
|
|
29461
|
-
visit(
|
|
29702
|
+
visit(join15(directory, entry.name));
|
|
29462
29703
|
} else if (entry.isFile()) {
|
|
29463
29704
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29464
29705
|
if (extension === "tscn") {
|
|
@@ -29489,14 +29730,14 @@ function scriptsWithoutUid(projectPath) {
|
|
|
29489
29730
|
}
|
|
29490
29731
|
const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
29491
29732
|
if (entry.isDirectory()) {
|
|
29492
|
-
visit(
|
|
29733
|
+
visit(join15(directory, entry.name), spelled);
|
|
29493
29734
|
continue;
|
|
29494
29735
|
}
|
|
29495
29736
|
if (!entry.isFile()) {
|
|
29496
29737
|
continue;
|
|
29497
29738
|
}
|
|
29498
29739
|
const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
|
|
29499
|
-
if (CARRIES_A_UID.has(extension) && !
|
|
29740
|
+
if (CARRIES_A_UID.has(extension) && !existsSync12(join15(directory, `${entry.name}.uid`))) {
|
|
29500
29741
|
found.push(spelled);
|
|
29501
29742
|
}
|
|
29502
29743
|
}
|
|
@@ -29531,7 +29772,7 @@ function searchProject(projectPath, options) {
|
|
|
29531
29772
|
if (skipped(entry.name)) {
|
|
29532
29773
|
continue;
|
|
29533
29774
|
}
|
|
29534
|
-
const entryPath =
|
|
29775
|
+
const entryPath = join15(directory, entry.name);
|
|
29535
29776
|
if (entry.isDirectory()) {
|
|
29536
29777
|
visit(entryPath);
|
|
29537
29778
|
continue;
|
|
@@ -29542,7 +29783,7 @@ function searchProject(projectPath, options) {
|
|
|
29542
29783
|
}
|
|
29543
29784
|
result.summary.files_searched += 1;
|
|
29544
29785
|
const matches = [];
|
|
29545
|
-
for (const [index, line] of
|
|
29786
|
+
for (const [index, line] of readFileSync13(entryPath, "utf8").split(`
|
|
29546
29787
|
`).entries()) {
|
|
29547
29788
|
if (full()) {
|
|
29548
29789
|
break;
|
|
@@ -29565,7 +29806,7 @@ function searchProject(projectPath, options) {
|
|
|
29565
29806
|
}
|
|
29566
29807
|
|
|
29567
29808
|
// src/resources.ts
|
|
29568
|
-
import { readFileSync as
|
|
29809
|
+
import { readFileSync as readFileSync14 } from "node:fs";
|
|
29569
29810
|
import { extname, resolve as resolve4 } from "node:path";
|
|
29570
29811
|
var STATIC_RESOURCES = [
|
|
29571
29812
|
{
|
|
@@ -29760,7 +30001,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29760
30001
|
const parsedUri = parseGodotUri(uri);
|
|
29761
30002
|
if (parsedUri.kind === "project-info") {
|
|
29762
30003
|
const projectFilePath = resolveProjectFile(projectPath, "project.godot");
|
|
29763
|
-
const rawProject =
|
|
30004
|
+
const rawProject = readFileSync14(projectFilePath, "utf-8");
|
|
29764
30005
|
const parsedProject = parseProjectGodot(rawProject);
|
|
29765
30006
|
return {
|
|
29766
30007
|
mimeType: "application/json",
|
|
@@ -29769,7 +30010,7 @@ function readResourceText(uri, getProjectPath) {
|
|
|
29769
30010
|
}
|
|
29770
30011
|
const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
|
|
29771
30012
|
ensureAllowedExtension(parsedUri.kind, filePath);
|
|
29772
|
-
const text =
|
|
30013
|
+
const text = readFileSync14(filePath, "utf-8");
|
|
29773
30014
|
return {
|
|
29774
30015
|
mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
|
|
29775
30016
|
text
|
|
@@ -29806,15 +30047,15 @@ function setupResourceHandlers(mcp, getProjectPath) {
|
|
|
29806
30047
|
}
|
|
29807
30048
|
|
|
29808
30049
|
// src/restart-note.ts
|
|
29809
|
-
import { mkdirSync as mkdirSync7, readFileSync as
|
|
29810
|
-
import { dirname as
|
|
30050
|
+
import { mkdirSync as mkdirSync7, readFileSync as readFileSync15, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
30051
|
+
import { dirname as dirname9, join as join16 } from "node:path";
|
|
29811
30052
|
function restartNotePath(projectPath) {
|
|
29812
|
-
return
|
|
30053
|
+
return join16(projectPath, ".godot", "gdharness-restart.json");
|
|
29813
30054
|
}
|
|
29814
30055
|
function noteRestartBegun(note) {
|
|
29815
30056
|
const path = restartNotePath(note.projectPath);
|
|
29816
30057
|
try {
|
|
29817
|
-
mkdirSync7(
|
|
30058
|
+
mkdirSync7(dirname9(path), { recursive: true });
|
|
29818
30059
|
writeFileSync9(path, `${JSON.stringify(note, null, 2)}
|
|
29819
30060
|
`, "utf8");
|
|
29820
30061
|
} catch (error) {
|
|
@@ -29823,7 +30064,7 @@ function noteRestartBegun(note) {
|
|
|
29823
30064
|
}
|
|
29824
30065
|
function restartOwed(projectPath) {
|
|
29825
30066
|
try {
|
|
29826
|
-
const parsed = JSON.parse(
|
|
30067
|
+
const parsed = JSON.parse(readFileSync15(restartNotePath(projectPath), "utf8"));
|
|
29827
30068
|
if (typeof parsed !== "object" || parsed === null) {
|
|
29828
30069
|
return null;
|
|
29829
30070
|
}
|
|
@@ -29845,32 +30086,32 @@ function restartSettled(projectPath) {
|
|
|
29845
30086
|
}
|
|
29846
30087
|
|
|
29847
30088
|
// src/run-record.ts
|
|
29848
|
-
import { execFile as
|
|
30089
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
29849
30090
|
import { createHash as createHash3 } from "node:crypto";
|
|
29850
30091
|
import {
|
|
29851
30092
|
mkdirSync as mkdirSync8,
|
|
29852
30093
|
openSync as openSync2,
|
|
29853
30094
|
readdirSync as readdirSync6,
|
|
29854
|
-
readFileSync as
|
|
29855
|
-
realpathSync as
|
|
30095
|
+
readFileSync as readFileSync16,
|
|
30096
|
+
realpathSync as realpathSync4,
|
|
29856
30097
|
rmSync as rmSync7,
|
|
29857
30098
|
statSync as statSync6,
|
|
29858
30099
|
writeFileSync as writeFileSync10
|
|
29859
30100
|
} from "node:fs";
|
|
29860
|
-
import { basename as basename3, dirname as
|
|
29861
|
-
import { promisify as
|
|
29862
|
-
var execFileAsync =
|
|
30101
|
+
import { basename as basename3, dirname as dirname10, join as join17, resolve as resolve5 } from "node:path";
|
|
30102
|
+
import { promisify as promisify4 } from "node:util";
|
|
30103
|
+
var execFileAsync = promisify4(execFile4);
|
|
29863
30104
|
function runsDirectory() {
|
|
29864
|
-
return
|
|
30105
|
+
return join17(runtimeDirectory(), "runs");
|
|
29865
30106
|
}
|
|
29866
30107
|
function realAsFarAsItExists(path) {
|
|
29867
30108
|
const missing = [];
|
|
29868
30109
|
let at = path;
|
|
29869
30110
|
for (;; ) {
|
|
29870
30111
|
try {
|
|
29871
|
-
return
|
|
30112
|
+
return join17(realpathSync4.native(at), ...missing);
|
|
29872
30113
|
} catch {
|
|
29873
|
-
const parent =
|
|
30114
|
+
const parent = dirname10(at);
|
|
29874
30115
|
if (parent === at) {
|
|
29875
30116
|
return path;
|
|
29876
30117
|
}
|
|
@@ -29885,16 +30126,19 @@ function projectKey(projectPath) {
|
|
|
29885
30126
|
return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
29886
30127
|
}
|
|
29887
30128
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
29888
|
-
return
|
|
30129
|
+
return join17(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
29889
30130
|
}
|
|
29890
30131
|
var SHARED_NOTE = "run.json";
|
|
29891
30132
|
function openTranscript(startedAt) {
|
|
29892
30133
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
29893
|
-
const path =
|
|
30134
|
+
const path = join17(runsDirectory(), `run-${startedAt}.log`);
|
|
29894
30135
|
return { path, fd: openSync2(path, "a") };
|
|
29895
30136
|
}
|
|
30137
|
+
function noteIsAbout(note, playing) {
|
|
30138
|
+
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
30139
|
+
}
|
|
29896
30140
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
29897
|
-
return
|
|
30141
|
+
return join17(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
29898
30142
|
}
|
|
29899
30143
|
function writeEditorRunNote(note) {
|
|
29900
30144
|
mkdirSync8(runsDirectory(), { recursive: true });
|
|
@@ -29902,7 +30146,7 @@ function writeEditorRunNote(note) {
|
|
|
29902
30146
|
}
|
|
29903
30147
|
function readEditorRunNote(projectPath) {
|
|
29904
30148
|
for (const directory of runtimeDirectories()) {
|
|
29905
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
30149
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join17(directory, "runs", "editor-run.json"));
|
|
29906
30150
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
29907
30151
|
return note;
|
|
29908
30152
|
}
|
|
@@ -29912,7 +30156,7 @@ function readEditorRunNote(projectPath) {
|
|
|
29912
30156
|
function editorNoteAt(path) {
|
|
29913
30157
|
let parsed;
|
|
29914
30158
|
try {
|
|
29915
|
-
parsed = JSON.parse(
|
|
30159
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
29916
30160
|
} catch {
|
|
29917
30161
|
return null;
|
|
29918
30162
|
}
|
|
@@ -29926,7 +30170,9 @@ function editorNoteAt(path) {
|
|
|
29926
30170
|
if (typeof projectPath !== "string" || typeof transcript !== "string" || typeof startedAt !== "number") {
|
|
29927
30171
|
return null;
|
|
29928
30172
|
}
|
|
29929
|
-
|
|
30173
|
+
const play = fields["play"];
|
|
30174
|
+
const which = typeof play === "object" && play !== null && typeof play["play"] === "number" && typeof play["editorPid"] === "number" ? play : undefined;
|
|
30175
|
+
return { projectPath, transcript, startedAt, ...which === undefined ? {} : { play: which } };
|
|
29930
30176
|
}
|
|
29931
30177
|
function readRunRecord(projectPath) {
|
|
29932
30178
|
for (const directory of runtimeDirectories()) {
|
|
@@ -29934,7 +30180,7 @@ function readRunRecord(projectPath) {
|
|
|
29934
30180
|
if (own !== null) {
|
|
29935
30181
|
return own;
|
|
29936
30182
|
}
|
|
29937
|
-
const shared = recordAt(
|
|
30183
|
+
const shared = recordAt(join17(directory, "runs", SHARED_NOTE));
|
|
29938
30184
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
29939
30185
|
return shared;
|
|
29940
30186
|
}
|
|
@@ -29946,13 +30192,13 @@ function everyRunRecord() {
|
|
|
29946
30192
|
for (const directory of runtimeDirectories()) {
|
|
29947
30193
|
let entries;
|
|
29948
30194
|
try {
|
|
29949
|
-
entries = readdirSync6(
|
|
30195
|
+
entries = readdirSync6(join17(directory, "runs"));
|
|
29950
30196
|
} catch {
|
|
29951
30197
|
continue;
|
|
29952
30198
|
}
|
|
29953
30199
|
for (const entry of entries) {
|
|
29954
30200
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
29955
|
-
const record = recordAt(
|
|
30201
|
+
const record = recordAt(join17(directory, "runs", entry));
|
|
29956
30202
|
if (record !== null) {
|
|
29957
30203
|
found.push(record);
|
|
29958
30204
|
}
|
|
@@ -29964,7 +30210,7 @@ function everyRunRecord() {
|
|
|
29964
30210
|
function recordAt(path) {
|
|
29965
30211
|
let parsed;
|
|
29966
30212
|
try {
|
|
29967
|
-
parsed = JSON.parse(
|
|
30213
|
+
parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
29968
30214
|
} catch {
|
|
29969
30215
|
return null;
|
|
29970
30216
|
}
|
|
@@ -30010,7 +30256,7 @@ async function runningAs(pid) {
|
|
|
30010
30256
|
const began = await startedAt(pid, null);
|
|
30011
30257
|
const when = began === null ? {} : { startedAt: began };
|
|
30012
30258
|
if (process.platform === "linux") {
|
|
30013
|
-
const raw =
|
|
30259
|
+
const raw = readFileSync16(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
|
|
30014
30260
|
return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
|
|
30015
30261
|
}
|
|
30016
30262
|
const args = (await execFileAsync("ps", ["-p", String(pid), "-o", "args="], { encoding: "utf8", timeout: 15000 })).stdout.trim();
|
|
@@ -30139,7 +30385,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
30139
30385
|
}
|
|
30140
30386
|
function clearRunRecord(projectPath) {
|
|
30141
30387
|
for (const directory of runtimeDirectories()) {
|
|
30142
|
-
const shared =
|
|
30388
|
+
const shared = join17(directory, "runs", SHARED_NOTE);
|
|
30143
30389
|
const sharedRecord = recordAt(shared);
|
|
30144
30390
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
30145
30391
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -30169,7 +30415,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
30169
30415
|
if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
|
|
30170
30416
|
continue;
|
|
30171
30417
|
}
|
|
30172
|
-
const path =
|
|
30418
|
+
const path = join17(directory, entry);
|
|
30173
30419
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
30174
30420
|
continue;
|
|
30175
30421
|
}
|
|
@@ -30183,16 +30429,16 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
30183
30429
|
import { createHash as createHash4 } from "node:crypto";
|
|
30184
30430
|
import {
|
|
30185
30431
|
cpSync,
|
|
30186
|
-
existsSync as
|
|
30432
|
+
existsSync as existsSync13,
|
|
30187
30433
|
mkdirSync as mkdirSync9,
|
|
30188
30434
|
readdirSync as readdirSync7,
|
|
30189
|
-
readFileSync as
|
|
30435
|
+
readFileSync as readFileSync17,
|
|
30190
30436
|
rmdirSync,
|
|
30191
30437
|
rmSync as rmSync8,
|
|
30192
30438
|
statSync as statSync7,
|
|
30193
30439
|
writeFileSync as writeFileSync11
|
|
30194
30440
|
} from "node:fs";
|
|
30195
|
-
import { dirname as
|
|
30441
|
+
import { dirname as dirname11, join as join18 } from "node:path";
|
|
30196
30442
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
30197
30443
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
30198
30444
|
var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
|
|
@@ -30209,21 +30455,21 @@ function editorAddonDigest(from = shippedAddonsDirectory()) {
|
|
|
30209
30455
|
if (name === VERSION_MARKER || name === DIGEST_MARKER) {
|
|
30210
30456
|
continue;
|
|
30211
30457
|
}
|
|
30212
|
-
const path =
|
|
30458
|
+
const path = join18(directory, name);
|
|
30213
30459
|
const inside = `${relative}/${name}`;
|
|
30214
30460
|
if (statSync7(path).isDirectory()) {
|
|
30215
30461
|
walk(path, inside);
|
|
30216
30462
|
} else {
|
|
30217
30463
|
hash.update(`${inside}
|
|
30218
30464
|
`);
|
|
30219
|
-
hash.update(
|
|
30465
|
+
hash.update(readFileSync17(path));
|
|
30220
30466
|
hash.update(`
|
|
30221
30467
|
`);
|
|
30222
30468
|
}
|
|
30223
30469
|
}
|
|
30224
30470
|
};
|
|
30225
30471
|
for (const name of EDITOR_PLUGINS) {
|
|
30226
|
-
walk(
|
|
30472
|
+
walk(join18(from, name), name);
|
|
30227
30473
|
}
|
|
30228
30474
|
return hash.digest("hex");
|
|
30229
30475
|
}
|
|
@@ -30239,14 +30485,14 @@ function shippedEditorDigest() {
|
|
|
30239
30485
|
return shippedDigest ?? undefined;
|
|
30240
30486
|
}
|
|
30241
30487
|
function shippedAddonsDirectory() {
|
|
30242
|
-
return
|
|
30488
|
+
return join18(dirname11(fileURLToPath3(import.meta.url)), "godot", "addons");
|
|
30243
30489
|
}
|
|
30244
30490
|
function installedAddonVersion(projectPath) {
|
|
30245
|
-
const marker =
|
|
30246
|
-
if (!
|
|
30491
|
+
const marker = join18(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
30492
|
+
if (!existsSync13(marker)) {
|
|
30247
30493
|
return null;
|
|
30248
30494
|
}
|
|
30249
|
-
const said =
|
|
30495
|
+
const said = readFileSync17(marker, "utf8").trim();
|
|
30250
30496
|
return said === "" ? null : said;
|
|
30251
30497
|
}
|
|
30252
30498
|
|
|
@@ -30299,7 +30545,7 @@ var NODE_PATH = {
|
|
|
30299
30545
|
};
|
|
30300
30546
|
var PROPERTIES = {
|
|
30301
30547
|
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.',
|
|
30548
|
+
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
30549
|
additionalProperties: true
|
|
30304
30550
|
};
|
|
30305
30551
|
var XY = {
|
|
@@ -30454,11 +30700,18 @@ var TOOL_SPECS = [
|
|
|
30454
30700
|
},
|
|
30455
30701
|
value: {
|
|
30456
30702
|
blank: true,
|
|
30457
|
-
description: 'The value to write. Engine types may be tagged, {"_type": "Vector2", "x": 1, "y": 2}. "" writes an empty string.'
|
|
30703
|
+
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
30704
|
},
|
|
30459
30705
|
name: { type: "string", description: "Autoload name." },
|
|
30460
30706
|
path: { type: "string", description: "Autoload script or scene inside the project." },
|
|
30461
|
-
enabled: {
|
|
30707
|
+
enabled: {
|
|
30708
|
+
type: "boolean",
|
|
30709
|
+
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."
|
|
30710
|
+
},
|
|
30711
|
+
global: {
|
|
30712
|
+
type: "boolean",
|
|
30713
|
+
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.`
|
|
30714
|
+
},
|
|
30462
30715
|
scenePath: SCENE_PATH,
|
|
30463
30716
|
actionName: { type: "string", description: 'Input action name, such as "jump".' },
|
|
30464
30717
|
events: INPUT_EVENTS,
|
|
@@ -30470,9 +30723,15 @@ var TOOL_SPECS = [
|
|
|
30470
30723
|
},
|
|
30471
30724
|
pluginName: { type: "string", description: "Folder name under addons/." },
|
|
30472
30725
|
busName: { type: "string", description: "Audio bus name." },
|
|
30473
|
-
parentBusIndex: {
|
|
30726
|
+
parentBusIndex: {
|
|
30727
|
+
type: "number",
|
|
30728
|
+
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."
|
|
30729
|
+
},
|
|
30474
30730
|
busIndex: { type: "number", description: "Audio bus index." },
|
|
30475
|
-
effectIndex: {
|
|
30731
|
+
effectIndex: {
|
|
30732
|
+
type: "number",
|
|
30733
|
+
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."
|
|
30734
|
+
},
|
|
30476
30735
|
effectType: { type: "string", description: 'Effect class, such as "AudioEffectReverb".' },
|
|
30477
30736
|
volumeDb: { type: "number", description: "Bus volume in decibels." }
|
|
30478
30737
|
},
|
|
@@ -30491,7 +30750,7 @@ var TOOL_SPECS = [
|
|
|
30491
30750
|
disable_plugin: { summary: "disable an addon", requires: ["pluginName"] },
|
|
30492
30751
|
add_audio_bus: { summary: "add an audio bus", requires: ["busName"] },
|
|
30493
30752
|
set_audio_bus_effect: {
|
|
30494
|
-
summary: "
|
|
30753
|
+
summary: "put an effect in a slot on a bus",
|
|
30495
30754
|
requires: ["busIndex", "effectIndex", "effectType"]
|
|
30496
30755
|
},
|
|
30497
30756
|
set_audio_bus_volume: { summary: "set a bus volume", requires: ["busIndex", "volumeDb"] }
|
|
@@ -30530,7 +30789,11 @@ var TOOL_SPECS = [
|
|
|
30530
30789
|
type: "boolean",
|
|
30531
30790
|
description: "forward: include the engine's own res://. resources. Default false."
|
|
30532
30791
|
},
|
|
30533
|
-
fileTypes: {
|
|
30792
|
+
fileTypes: {
|
|
30793
|
+
type: "array",
|
|
30794
|
+
items: { type: "string" },
|
|
30795
|
+
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.'
|
|
30796
|
+
}
|
|
30534
30797
|
},
|
|
30535
30798
|
requires: ["projectPath", "resourcePath"]
|
|
30536
30799
|
},
|
|
@@ -30575,7 +30838,10 @@ var TOOL_SPECS = [
|
|
|
30575
30838
|
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
30839
|
requires: []
|
|
30577
30840
|
},
|
|
30578
|
-
uid: {
|
|
30841
|
+
uid: {
|
|
30842
|
+
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",
|
|
30843
|
+
requires: ["resourcePath"]
|
|
30844
|
+
},
|
|
30579
30845
|
refresh_uids: {
|
|
30580
30846
|
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
30847
|
requires: []
|
|
@@ -30636,11 +30902,11 @@ var TOOL_SPECS = [
|
|
|
30636
30902
|
},
|
|
30637
30903
|
{
|
|
30638
30904
|
name: "scene_create",
|
|
30639
|
-
description: "Creates a scene file, saves one, or saves a copy under a new path. Needs the editor connected.",
|
|
30905
|
+
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
30906
|
parameters: {
|
|
30641
30907
|
projectPath: PROJECT_PATH,
|
|
30642
30908
|
scenePath: SCENE_PATH,
|
|
30643
|
-
rootNodeType: { type: "string", description: "create: the root node class. Default
|
|
30909
|
+
rootNodeType: { type: "string", description: "create: the root node class. Default Node." },
|
|
30644
30910
|
newPath: { type: "string", description: "save_as: where the copy goes." }
|
|
30645
30911
|
},
|
|
30646
30912
|
requires: ["projectPath", "scenePath"],
|
|
@@ -30664,7 +30930,7 @@ var TOOL_SPECS = [
|
|
|
30664
30930
|
},
|
|
30665
30931
|
{
|
|
30666
30932
|
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.",
|
|
30933
|
+
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
30934
|
parameters: {
|
|
30669
30935
|
projectPath: PROJECT_PATH,
|
|
30670
30936
|
scenePath: SCENE_PATH,
|
|
@@ -30682,7 +30948,10 @@ var TOOL_SPECS = [
|
|
|
30682
30948
|
type: "boolean",
|
|
30683
30949
|
description: "get: include properties still at their default. Default false."
|
|
30684
30950
|
},
|
|
30685
|
-
layer: {
|
|
30951
|
+
layer: {
|
|
30952
|
+
type: "number",
|
|
30953
|
+
description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
|
|
30954
|
+
},
|
|
30686
30955
|
cells: TILEMAP_CELLS
|
|
30687
30956
|
},
|
|
30688
30957
|
requires: ["projectPath", "scenePath"],
|
|
@@ -30693,7 +30962,10 @@ var TOOL_SPECS = [
|
|
|
30693
30962
|
duplicate: { summary: "copy a node and its children", requires: ["nodePath", "newName"] },
|
|
30694
30963
|
reparent: { summary: "move a node under another parent", requires: ["nodePath", "newParentPath"] },
|
|
30695
30964
|
delete: { summary: "remove a node and its children", requires: ["nodePath"] },
|
|
30696
|
-
set_tilemap_cells: {
|
|
30965
|
+
set_tilemap_cells: {
|
|
30966
|
+
summary: "place tiles in a TileMapLayer or TileMap",
|
|
30967
|
+
requires: ["nodePath", "cells"]
|
|
30968
|
+
}
|
|
30697
30969
|
}
|
|
30698
30970
|
},
|
|
30699
30971
|
{
|
|
@@ -30781,13 +31053,16 @@ var TOOL_SPECS = [
|
|
|
30781
31053
|
},
|
|
30782
31054
|
{
|
|
30783
31055
|
name: "script_edit",
|
|
30784
|
-
description: "Creates a GDScript file, or adds functions, variables and signals to one. Every declaration written carries a type.
|
|
31056
|
+
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
31057
|
parameters: {
|
|
30786
31058
|
projectPath: PROJECT_PATH,
|
|
30787
31059
|
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: {
|
|
31060
|
+
className: { type: "string", description: "create: a class_name for the script. Not with content." },
|
|
31061
|
+
extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
|
|
31062
|
+
content: {
|
|
31063
|
+
type: "string",
|
|
31064
|
+
description: "create: the whole file, written as given, instead of a template, className and extends."
|
|
31065
|
+
},
|
|
30791
31066
|
template: {
|
|
30792
31067
|
type: "string",
|
|
30793
31068
|
enum: ["singleton", "state_machine", "component", "resource"],
|
|
@@ -30985,7 +31260,7 @@ var TOOL_SPECS = [
|
|
|
30985
31260
|
},
|
|
30986
31261
|
{
|
|
30987
31262
|
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.",
|
|
31263
|
+
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
31264
|
parameters: {
|
|
30990
31265
|
severity: {
|
|
30991
31266
|
type: "string",
|
|
@@ -31049,7 +31324,7 @@ var TOOL_SPECS = [
|
|
|
31049
31324
|
},
|
|
31050
31325
|
reloadScript: {
|
|
31051
31326
|
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."
|
|
31327
|
+
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
31328
|
}
|
|
31054
31329
|
},
|
|
31055
31330
|
requires: ["projectPath"]
|
|
@@ -31195,7 +31470,7 @@ var TOOL_SPECS = [
|
|
|
31195
31470
|
},
|
|
31196
31471
|
{
|
|
31197
31472
|
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.",
|
|
31473
|
+
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
31474
|
parameters: {
|
|
31200
31475
|
projectPath: RUNNING_PROJECT_PATH,
|
|
31201
31476
|
pid: RUNNING_PID,
|
|
@@ -31207,12 +31482,12 @@ var TOOL_SPECS = [
|
|
|
31207
31482
|
property: {
|
|
31208
31483
|
type: "string",
|
|
31209
31484
|
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
|
|
31485
|
+
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
31486
|
},
|
|
31212
31487
|
value: {
|
|
31213
31488
|
ops: ["set"],
|
|
31214
31489
|
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.`
|
|
31490
|
+
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
31491
|
},
|
|
31217
31492
|
method: {
|
|
31218
31493
|
type: "string",
|
|
@@ -31222,7 +31497,7 @@ var TOOL_SPECS = [
|
|
|
31222
31497
|
args: {
|
|
31223
31498
|
type: "array",
|
|
31224
31499
|
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.`
|
|
31500
|
+
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
31501
|
},
|
|
31227
31502
|
timeoutMs: {
|
|
31228
31503
|
type: "integer",
|
|
@@ -31300,7 +31575,7 @@ var TOOL_SPECS = [
|
|
|
31300
31575
|
says: {
|
|
31301
31576
|
type: "string",
|
|
31302
31577
|
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:
|
|
31578
|
+
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
31579
|
},
|
|
31305
31580
|
action: {
|
|
31306
31581
|
type: "string",
|
|
@@ -31333,7 +31608,7 @@ var TOOL_SPECS = [
|
|
|
31333
31608
|
replace: {
|
|
31334
31609
|
type: "boolean",
|
|
31335
31610
|
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.'
|
|
31611
|
+
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
31612
|
},
|
|
31338
31613
|
index: {
|
|
31339
31614
|
type: "number",
|
|
@@ -31377,11 +31652,11 @@ var TOOL_SPECS = [
|
|
|
31377
31652
|
requires: [],
|
|
31378
31653
|
operations: {
|
|
31379
31654
|
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
|
|
31655
|
+
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
31656
|
requires: []
|
|
31382
31657
|
},
|
|
31383
31658
|
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
|
|
31659
|
+
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
31660
|
requires: ["nodePath"]
|
|
31386
31661
|
},
|
|
31387
31662
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
@@ -31390,7 +31665,7 @@ var TOOL_SPECS = [
|
|
|
31390
31665
|
requires: ["keycode"]
|
|
31391
31666
|
},
|
|
31392
31667
|
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",
|
|
31668
|
+
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
31669
|
requires: ["text"]
|
|
31395
31670
|
},
|
|
31396
31671
|
mouse_click: {
|
|
@@ -31478,7 +31753,7 @@ var TOOL_SPECS = [
|
|
|
31478
31753
|
},
|
|
31479
31754
|
{
|
|
31480
31755
|
name: "debug_control",
|
|
31481
|
-
description: "Continues or steps the debugged game through the editor's debug adapter
|
|
31756
|
+
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
31757
|
parameters: {},
|
|
31483
31758
|
requires: [],
|
|
31484
31759
|
operations: {
|
|
@@ -31489,12 +31764,12 @@ var TOOL_SPECS = [
|
|
|
31489
31764
|
},
|
|
31490
31765
|
{
|
|
31491
31766
|
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.",
|
|
31767
|
+
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
31768
|
parameters: {
|
|
31494
31769
|
frameId: {
|
|
31495
31770
|
type: "number",
|
|
31496
31771
|
ops: ["variables"],
|
|
31497
|
-
description: "variables: which frame, from a stack answer. Default the innermost."
|
|
31772
|
+
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
31773
|
}
|
|
31499
31774
|
},
|
|
31500
31775
|
requires: [],
|
|
@@ -31555,8 +31830,8 @@ function buildToolDefinitions() {
|
|
|
31555
31830
|
// src/server.ts
|
|
31556
31831
|
var UPDATE_NOTICE_EVERY = 500;
|
|
31557
31832
|
var FEEDBACK_NOTICE_EVERY = 250;
|
|
31558
|
-
var
|
|
31559
|
-
var __dirname2 =
|
|
31833
|
+
var run4 = promisify5(execFile5);
|
|
31834
|
+
var __dirname2 = dirname12(fileURLToPath4(import.meta.url));
|
|
31560
31835
|
var EDITOR_RESTART_TIMEOUT_MS = 90000;
|
|
31561
31836
|
var HOLD_PING_MS = 2000;
|
|
31562
31837
|
var SLOWEST_FRAME_RATE = 20;
|
|
@@ -31570,16 +31845,16 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
31570
31845
|
if (extname2(outputPath).toLowerCase() !== ".png") {
|
|
31571
31846
|
return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
|
|
31572
31847
|
}
|
|
31573
|
-
const directory = realPathOr(
|
|
31574
|
-
if (!
|
|
31575
|
-
return `outputPath's directory ${
|
|
31848
|
+
const directory = realPathOr(dirname12(outputPath));
|
|
31849
|
+
if (!existsSync14(directory)) {
|
|
31850
|
+
return `outputPath's directory ${dirname12(outputPath)} does not exist; a capture creates no directories.`;
|
|
31576
31851
|
}
|
|
31577
|
-
const where =
|
|
31852
|
+
const where = join19(directory, basename4(outputPath));
|
|
31578
31853
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
31579
31854
|
if (inside !== undefined) {
|
|
31580
31855
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
31581
31856
|
}
|
|
31582
|
-
if (
|
|
31857
|
+
if (existsSync14(where)) {
|
|
31583
31858
|
return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
|
|
31584
31859
|
}
|
|
31585
31860
|
return null;
|
|
@@ -31610,16 +31885,18 @@ function describeHalt(halt) {
|
|
|
31610
31885
|
function exited(run) {
|
|
31611
31886
|
return run.exitCode !== null || run.exitSignal !== null;
|
|
31612
31887
|
}
|
|
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
31888
|
function howItExited(run) {
|
|
31621
31889
|
return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
|
|
31622
31890
|
}
|
|
31891
|
+
function errorsUnread(run) {
|
|
31892
|
+
return run.throughEditor && typeof run.errorReport !== "string";
|
|
31893
|
+
}
|
|
31894
|
+
function cleanVerdict(run) {
|
|
31895
|
+
if (run.consoleLost === true || errorsUnread(run)) {
|
|
31896
|
+
return;
|
|
31897
|
+
}
|
|
31898
|
+
return run.log.count("error") === 0;
|
|
31899
|
+
}
|
|
31623
31900
|
function stillRunning(run) {
|
|
31624
31901
|
if (run === null || exited(run) || run.endedUnwatched === true) {
|
|
31625
31902
|
return false;
|
|
@@ -31636,6 +31913,11 @@ function stillRunning(run) {
|
|
|
31636
31913
|
return true;
|
|
31637
31914
|
}
|
|
31638
31915
|
var PLAY_STARTS_WITHIN_MS = 30000;
|
|
31916
|
+
function playOf(answer) {
|
|
31917
|
+
const play = readNumber(answer, "play");
|
|
31918
|
+
const editorPid = readNumber(answer, "editorPid");
|
|
31919
|
+
return play === undefined || editorPid === undefined ? null : { play, editorPid };
|
|
31920
|
+
}
|
|
31639
31921
|
function runIsUp(run, editorSays, now = Date.now()) {
|
|
31640
31922
|
if (run?.throughEditor !== true) {
|
|
31641
31923
|
return stillRunning(run);
|
|
@@ -31685,11 +31967,19 @@ function stopVerdict(stop) {
|
|
|
31685
31967
|
return {
|
|
31686
31968
|
stopped: true,
|
|
31687
31969
|
endedPid: stop.endedPid,
|
|
31688
|
-
clean: stop.
|
|
31970
|
+
clean: stop.clean,
|
|
31689
31971
|
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
31972
|
withChildren: false
|
|
31691
31973
|
};
|
|
31692
31974
|
}
|
|
31975
|
+
if (stop.ending === "unreached") {
|
|
31976
|
+
return {
|
|
31977
|
+
stopped: false,
|
|
31978
|
+
notSignalled: stop.endedPid,
|
|
31979
|
+
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.",
|
|
31980
|
+
withChildren: false
|
|
31981
|
+
};
|
|
31982
|
+
}
|
|
31693
31983
|
if (stop.ending === "refused") {
|
|
31694
31984
|
return {
|
|
31695
31985
|
stopped: false,
|
|
@@ -31701,13 +31991,13 @@ function stopVerdict(stop) {
|
|
|
31701
31991
|
return {
|
|
31702
31992
|
stopped: true,
|
|
31703
31993
|
endedPid: stop.endedPid,
|
|
31704
|
-
clean: stop.
|
|
31994
|
+
clean: stop.clean,
|
|
31705
31995
|
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.`}${stop.ending === "gone" ? "" : ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be going: editor_status says whether it is.`}`,
|
|
31706
31996
|
withChildren: true
|
|
31707
31997
|
};
|
|
31708
31998
|
}
|
|
31709
31999
|
function endedPreviousRun(ended) {
|
|
31710
|
-
if (ended === null || ended.ending === "refused") {
|
|
32000
|
+
if (ended === null || ended.ending === "refused" || ended.ending === "unreached") {
|
|
31711
32001
|
return;
|
|
31712
32002
|
}
|
|
31713
32003
|
return ended.pid ?? true;
|
|
@@ -31790,6 +32080,9 @@ function endedToStartThis(ended) {
|
|
|
31790
32080
|
if (ended.ending === "refused") {
|
|
31791
32081
|
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
32082
|
}
|
|
32083
|
+
if (ended.ending === "unreached") {
|
|
32084
|
+
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.`;
|
|
32085
|
+
}
|
|
31793
32086
|
const after = ended.ending === "lingering" ? ` It had not exited ${STOP_WAIT_MS / 1000} seconds after being told to, so it may still be running beside this one: editor_status says whether it is.` : "";
|
|
31794
32087
|
return ` ${which} was ended to start this one; its output is no longer what editor_output answers about.${after}`;
|
|
31795
32088
|
}
|
|
@@ -31919,6 +32212,7 @@ var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
|
31919
32212
|
var HANDOVER_POLL_MS = 250;
|
|
31920
32213
|
var CACHE_WRITE_MS = 2000;
|
|
31921
32214
|
var SCAN_WAIT_MS = 30000;
|
|
32215
|
+
var STREAMS_GRACE_MS = 2000;
|
|
31922
32216
|
function scanWaitIn(payload) {
|
|
31923
32217
|
const waited = readNumber(payload, "waitedForEditorScanMs");
|
|
31924
32218
|
const note = readString(payload, "scanNote");
|
|
@@ -31931,19 +32225,58 @@ function withScanWait(outcome, scanned) {
|
|
|
31931
32225
|
if (scanned.waitedMs === 0) {
|
|
31932
32226
|
return outcome;
|
|
31933
32227
|
}
|
|
31934
|
-
const said = scanWaitAnswer(scanned);
|
|
32228
|
+
const said = scanWaitAnswer(scanned, "engine");
|
|
31935
32229
|
if (outcome.ok) {
|
|
31936
32230
|
return { ...outcome, payload: { ...outcome.payload, ...said } };
|
|
31937
32231
|
}
|
|
31938
32232
|
return said.scanNote === undefined ? outcome : { ...outcome, message: `${outcome.message} ${said.scanNote}` };
|
|
31939
32233
|
}
|
|
31940
|
-
function
|
|
32234
|
+
function notRunNote(notRun, failFast) {
|
|
32235
|
+
if (notRun <= 0) {
|
|
32236
|
+
return;
|
|
32237
|
+
}
|
|
32238
|
+
const cases = `${notRun} case${notRun === 1 ? "" : "s"}`;
|
|
32239
|
+
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.`;
|
|
32240
|
+
}
|
|
32241
|
+
function exportAnswer(ending, asked, before, after) {
|
|
32242
|
+
const { log } = ending;
|
|
32243
|
+
const written = after !== null && after !== before;
|
|
32244
|
+
const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
|
|
32245
|
+
const verdict = {
|
|
32246
|
+
exported: ending.exitCode === 0 && ending.failure === null && log.count("error") === 0 && written,
|
|
32247
|
+
...asked,
|
|
32248
|
+
exitCode: ending.exitCode,
|
|
32249
|
+
exitSignal: ending.exitSignal ?? undefined,
|
|
32250
|
+
failure: ending.failure ?? undefined,
|
|
32251
|
+
errors: log.count("error"),
|
|
32252
|
+
warnings: log.count("warning"),
|
|
32253
|
+
entries: forAnswer(problems.entries),
|
|
32254
|
+
...problems.omitted === 0 ? {} : { entriesOmitted: problems.omitted }
|
|
32255
|
+
};
|
|
32256
|
+
if (verdict.exported) {
|
|
32257
|
+
return { content: [{ type: "text", text: JSON.stringify(verdict, null, 2) }] };
|
|
32258
|
+
}
|
|
32259
|
+
const stale = !written && after !== null ? ` The ${asked.outputPath} there is from before this export, which did not write it.` : "";
|
|
32260
|
+
const why = ending.exitSignal === null ? stale : ` The engine was ${howItEnded(ending)}, so it has no exit code.${stale}`;
|
|
32261
|
+
return {
|
|
32262
|
+
content: [
|
|
32263
|
+
{
|
|
32264
|
+
type: "text",
|
|
32265
|
+
text: `Export with preset '${asked.preset}' did not produce ${asked.outputPath}.${why}`
|
|
32266
|
+
},
|
|
32267
|
+
{ type: "text", text: JSON.stringify(verdict, null, 2) }
|
|
32268
|
+
],
|
|
32269
|
+
isError: true
|
|
32270
|
+
};
|
|
32271
|
+
}
|
|
32272
|
+
function scanWaitAnswer(scanned, started = "game") {
|
|
31941
32273
|
if (scanned.waitedMs === 0) {
|
|
31942
32274
|
return {};
|
|
31943
32275
|
}
|
|
32276
|
+
const still = `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so`;
|
|
31944
32277
|
return {
|
|
31945
32278
|
waitedForEditorScanMs: scanned.waitedMs,
|
|
31946
|
-
scanNote: scanned.stillScanning ?
|
|
32279
|
+
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
32280
|
};
|
|
31948
32281
|
}
|
|
31949
32282
|
var PLAYING_STATUS_MS = 1000;
|
|
@@ -32023,16 +32356,17 @@ var PROJECT_FILE_ARGUMENTS = [
|
|
|
32023
32356
|
"path",
|
|
32024
32357
|
"script"
|
|
32025
32358
|
];
|
|
32026
|
-
function escapingPropertyValue(
|
|
32027
|
-
if (typeof
|
|
32359
|
+
function escapingPropertyValue(value) {
|
|
32360
|
+
if (typeof value === "string") {
|
|
32361
|
+
return /^(res|uid):\/\//.test(value) && value.split(/[/\\]/).includes("..") ? JSON.stringify(value) : undefined;
|
|
32362
|
+
}
|
|
32363
|
+
if (typeof value !== "object" || value === null) {
|
|
32028
32364
|
return;
|
|
32029
32365
|
}
|
|
32030
|
-
for (const
|
|
32031
|
-
|
|
32032
|
-
|
|
32033
|
-
|
|
32034
|
-
if (value.split(/[/\\]/).includes("..")) {
|
|
32035
|
-
return JSON.stringify(value);
|
|
32366
|
+
for (const item of Object.values(value)) {
|
|
32367
|
+
const found = escapingPropertyValue(item);
|
|
32368
|
+
if (found !== undefined) {
|
|
32369
|
+
return found;
|
|
32036
32370
|
}
|
|
32037
32371
|
}
|
|
32038
32372
|
return;
|
|
@@ -32070,7 +32404,7 @@ function aboveTheRunner(entry) {
|
|
|
32070
32404
|
}
|
|
32071
32405
|
function elsewhereIn(projectPath, asked) {
|
|
32072
32406
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
32073
|
-
if (wanted === "" ||
|
|
32407
|
+
if (wanted === "" || existsSync14(join19(projectPath, wanted))) {
|
|
32074
32408
|
return "";
|
|
32075
32409
|
}
|
|
32076
32410
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -32084,7 +32418,7 @@ function elsewhereIn(projectPath, asked) {
|
|
|
32084
32418
|
return near.length === 0 ? "" : ` The project does have ${there}.`;
|
|
32085
32419
|
}
|
|
32086
32420
|
function hasMainScene(projectFile) {
|
|
32087
|
-
const scene = parseProjectGodot(
|
|
32421
|
+
const scene = parseProjectGodot(readFileSync18(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
32088
32422
|
return typeof scene === "string" && scene !== "";
|
|
32089
32423
|
}
|
|
32090
32424
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
@@ -32092,13 +32426,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
32092
32426
|
return false;
|
|
32093
32427
|
}
|
|
32094
32428
|
try {
|
|
32095
|
-
return
|
|
32429
|
+
return readFileSync18(join19(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
32096
32430
|
} catch {
|
|
32097
32431
|
return false;
|
|
32098
32432
|
}
|
|
32099
32433
|
}
|
|
32100
32434
|
function editorPlaysHeadless(projectFile) {
|
|
32101
|
-
const runArgs = parseProjectGodot(
|
|
32435
|
+
const runArgs = parseProjectGodot(readFileSync18(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
|
|
32102
32436
|
return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
|
|
32103
32437
|
}
|
|
32104
32438
|
function camelCased(params) {
|
|
@@ -32117,7 +32451,7 @@ function needsItsOwnProcess(asked) {
|
|
|
32117
32451
|
class GodotServer {
|
|
32118
32452
|
mcp;
|
|
32119
32453
|
locator = new GodotLocator;
|
|
32120
|
-
operationsScript =
|
|
32454
|
+
operationsScript = join19(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
32121
32455
|
godotBridge;
|
|
32122
32456
|
tools = buildToolDefinitions();
|
|
32123
32457
|
activeProcess = null;
|
|
@@ -32617,6 +32951,9 @@ class GodotServer {
|
|
|
32617
32951
|
const contained = this.containProjectFiles(asks);
|
|
32618
32952
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
32619
32953
|
}
|
|
32954
|
+
if (headless === "create_script") {
|
|
32955
|
+
return await this.handleCreateScript(answerable);
|
|
32956
|
+
}
|
|
32620
32957
|
if (headless !== "refresh_class_cache") {
|
|
32621
32958
|
return await this.headless(headless, answerable);
|
|
32622
32959
|
}
|
|
@@ -32787,7 +33124,7 @@ class GodotServer {
|
|
|
32787
33124
|
return await this.handleRuntimeCommand("click", {
|
|
32788
33125
|
...whichGame(args),
|
|
32789
33126
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
32790
|
-
button:
|
|
33127
|
+
button: args["button"] ?? "left",
|
|
32791
33128
|
double: readBoolean(args, "doubleClick") ?? false,
|
|
32792
33129
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
32793
33130
|
...args["index"] === undefined ? {} : { index: args["index"] }
|
|
@@ -32859,8 +33196,8 @@ class GodotServer {
|
|
|
32859
33196
|
if (path === undefined) {
|
|
32860
33197
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
32861
33198
|
}
|
|
32862
|
-
const file =
|
|
32863
|
-
if (!
|
|
33199
|
+
const file = join19(path, "project.godot");
|
|
33200
|
+
if (!existsSync14(file)) {
|
|
32864
33201
|
return {
|
|
32865
33202
|
ok: false,
|
|
32866
33203
|
response: this.createErrorResponse(`Not a Godot project: ${path}`, [
|
|
@@ -33070,13 +33407,30 @@ class GodotServer {
|
|
|
33070
33407
|
}
|
|
33071
33408
|
contained[key] = `res://${location.relativePath}`;
|
|
33072
33409
|
}
|
|
33073
|
-
const escaping = escapingPropertyValue(args["properties"]);
|
|
33410
|
+
const escaping = escapingPropertyValue([args["properties"], args["track"]]);
|
|
33074
33411
|
if (escaping !== undefined) {
|
|
33075
33412
|
return {
|
|
33076
33413
|
ok: false,
|
|
33077
33414
|
response: this.createErrorResponse(`The property value ${escaping} resolves outside the project directory.`, PATH_SOLUTIONS)
|
|
33078
33415
|
};
|
|
33079
33416
|
}
|
|
33417
|
+
const sources = args["sources"];
|
|
33418
|
+
if (Array.isArray(sources)) {
|
|
33419
|
+
const resolved = [];
|
|
33420
|
+
for (const source of sources) {
|
|
33421
|
+
const texture = typeof source === "object" && source !== null ? source["texture"] : undefined;
|
|
33422
|
+
if (typeof texture !== "string") {
|
|
33423
|
+
resolved.push(source);
|
|
33424
|
+
continue;
|
|
33425
|
+
}
|
|
33426
|
+
const location = resolveWithinProject(projectPath, texture);
|
|
33427
|
+
if (!location.ok) {
|
|
33428
|
+
return { ok: false, response: this.createErrorResponse(location.reason, PATH_SOLUTIONS) };
|
|
33429
|
+
}
|
|
33430
|
+
resolved.push({ ...source, texture: `res://${location.relativePath}` });
|
|
33431
|
+
}
|
|
33432
|
+
contained["sources"] = resolved;
|
|
33433
|
+
}
|
|
33080
33434
|
const pluginName = readNonEmptyString(args, "pluginName");
|
|
33081
33435
|
if (pluginName !== undefined) {
|
|
33082
33436
|
const plugin = resolveWithinProject(projectPath, `addons/${pluginName}/plugin.cfg`);
|
|
@@ -33124,6 +33478,7 @@ class GodotServer {
|
|
|
33124
33478
|
return this.answer(withScanWait({
|
|
33125
33479
|
ok: true,
|
|
33126
33480
|
messages: imported.messages,
|
|
33481
|
+
...imported.messagesOmitted === undefined ? {} : { messagesOmitted: imported.messagesOmitted },
|
|
33127
33482
|
payload: {
|
|
33128
33483
|
uidsCreated: given,
|
|
33129
33484
|
stillWithoutUid: after,
|
|
@@ -33174,16 +33529,25 @@ class GodotServer {
|
|
|
33174
33529
|
if (changed.length === 0) {
|
|
33175
33530
|
return written;
|
|
33176
33531
|
}
|
|
33177
|
-
const
|
|
33532
|
+
const adopting = {};
|
|
33533
|
+
const taken = await this.editorTakes("adopt_project_settings", { settings: changed }, adopting);
|
|
33534
|
+
if (taken["editorNote"] !== undefined) {
|
|
33535
|
+
return this.jsonTextResponse({ ...answer, ...taken });
|
|
33536
|
+
}
|
|
33537
|
+
const adopted = Array.isArray(adopting["adopted"]) ? adopting["adopted"].map(String) : [];
|
|
33538
|
+
const passedOver = changed.filter((name) => !adopted.includes(name));
|
|
33178
33539
|
return this.jsonTextResponse({
|
|
33179
33540
|
...answer,
|
|
33180
|
-
|
|
33181
|
-
...
|
|
33541
|
+
editorAdopted: adopted,
|
|
33542
|
+
...passedOver.length > 0 ? { editorPassedOver: passedOver } : {}
|
|
33182
33543
|
});
|
|
33183
33544
|
}
|
|
33184
|
-
async editorTakes(command, args) {
|
|
33545
|
+
async editorTakes(command, args, answered = {}) {
|
|
33185
33546
|
try {
|
|
33186
|
-
await this.godotBridge.invokeTool(command, args);
|
|
33547
|
+
const result = await this.godotBridge.invokeTool(command, args);
|
|
33548
|
+
if (typeof result === "object" && result !== null) {
|
|
33549
|
+
Object.assign(answered, result);
|
|
33550
|
+
}
|
|
33187
33551
|
return {};
|
|
33188
33552
|
} catch (error) {
|
|
33189
33553
|
const message = errorMessage(error);
|
|
@@ -33221,7 +33585,19 @@ class GodotServer {
|
|
|
33221
33585
|
}
|
|
33222
33586
|
const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
33223
33587
|
if (!applied.ok) {
|
|
33224
|
-
|
|
33588
|
+
const listed = written.payload["updated_options"];
|
|
33589
|
+
const updated = Array.isArray(listed) ? listed.map(String) : [];
|
|
33590
|
+
const which = updated.length === 0 ? "The options were" : `${updated.join(", ")} ${updated.length === 1 ? "was" : "were"}`;
|
|
33591
|
+
return {
|
|
33592
|
+
...applied.response,
|
|
33593
|
+
content: [
|
|
33594
|
+
{
|
|
33595
|
+
type: "text",
|
|
33596
|
+
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:`
|
|
33597
|
+
},
|
|
33598
|
+
...applied.response.content
|
|
33599
|
+
]
|
|
33600
|
+
};
|
|
33225
33601
|
}
|
|
33226
33602
|
return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
|
|
33227
33603
|
}
|
|
@@ -33239,7 +33615,14 @@ class GodotServer {
|
|
|
33239
33615
|
response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
|
|
33240
33616
|
};
|
|
33241
33617
|
}
|
|
33242
|
-
const
|
|
33618
|
+
const never = listed.find((one) => one.status === "not_imported");
|
|
33619
|
+
if (resourcePath !== undefined && never !== undefined) {
|
|
33620
|
+
return {
|
|
33621
|
+
ok: false,
|
|
33622
|
+
response: this.createErrorResponse(`${never.path} is not something the engine imports: ${never.reason ?? "it has no importer"}.`)
|
|
33623
|
+
};
|
|
33624
|
+
}
|
|
33625
|
+
const targets = listed.filter((one) => one.status !== "missing_source" && one.status !== "not_imported" && (force || one.status !== "up_to_date")).map((one) => one.path);
|
|
33243
33626
|
if (targets.length === 0) {
|
|
33244
33627
|
return {
|
|
33245
33628
|
ok: true,
|
|
@@ -33278,9 +33661,7 @@ class GodotServer {
|
|
|
33278
33661
|
if (!imported.ok) {
|
|
33279
33662
|
return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
|
|
33280
33663
|
}
|
|
33281
|
-
|
|
33282
|
-
extra["engine_messages"] = imported.messages;
|
|
33283
|
-
}
|
|
33664
|
+
Object.assign(extra, engineExtras(imported));
|
|
33284
33665
|
if (imported.extensionNote !== undefined) {
|
|
33285
33666
|
extra["extensionNote"] = imported.extensionNote;
|
|
33286
33667
|
}
|
|
@@ -33363,17 +33744,15 @@ class GodotServer {
|
|
|
33363
33744
|
return withScanWait(outcome, scanned);
|
|
33364
33745
|
}
|
|
33365
33746
|
answer(outcome) {
|
|
33747
|
+
const extras = engineExtras(outcome);
|
|
33366
33748
|
if (!outcome.ok) {
|
|
33367
33749
|
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
|
-
});
|
|
33750
|
+
if (Object.keys(extras).length > 0) {
|
|
33751
|
+
response.content.push({ type: "text", text: JSON.stringify(extras, null, 2) });
|
|
33373
33752
|
}
|
|
33374
33753
|
return response;
|
|
33375
33754
|
}
|
|
33376
|
-
return this.jsonTextResponse(
|
|
33755
|
+
return this.jsonTextResponse({ ...outcome.payload, ...extras });
|
|
33377
33756
|
}
|
|
33378
33757
|
async handleProjectInfo(args) {
|
|
33379
33758
|
const include = readStringArray(args, "include") ?? [];
|
|
@@ -33389,7 +33768,7 @@ class GodotServer {
|
|
|
33389
33768
|
if (!engine.ok) {
|
|
33390
33769
|
return engine.response;
|
|
33391
33770
|
}
|
|
33392
|
-
const application = parseProjectGodot(
|
|
33771
|
+
const application = parseProjectGodot(readFileSync18(project.value.file, "utf8"))["application"] ?? {};
|
|
33393
33772
|
const name = application["config/name"];
|
|
33394
33773
|
const mainScene = application["run/main_scene"];
|
|
33395
33774
|
const info = {
|
|
@@ -33406,7 +33785,7 @@ class GodotServer {
|
|
|
33406
33785
|
continue;
|
|
33407
33786
|
}
|
|
33408
33787
|
const outcome = await this.operation(spec.operation, spec.params(args, detailed), project.value.path);
|
|
33409
|
-
info[section] = outcome.ok ?
|
|
33788
|
+
info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
|
|
33410
33789
|
}
|
|
33411
33790
|
return this.jsonTextResponse(info);
|
|
33412
33791
|
}
|
|
@@ -33450,18 +33829,18 @@ class GodotServer {
|
|
|
33450
33829
|
if (!engine.ok) {
|
|
33451
33830
|
return engine.response;
|
|
33452
33831
|
}
|
|
33453
|
-
const folder =
|
|
33832
|
+
const folder = dirname12(output.absolutePath);
|
|
33454
33833
|
try {
|
|
33455
33834
|
mkdirSync10(folder, { recursive: true });
|
|
33456
33835
|
} catch (error) {
|
|
33457
|
-
return this.createErrorResponse(`Cannot create ${
|
|
33836
|
+
return this.createErrorResponse(`Cannot create ${dirname12(output.relativePath)} to export into: ${errorMessage(error)}`);
|
|
33458
33837
|
}
|
|
33459
33838
|
const debug = readBoolean(args, "debug") ?? false;
|
|
33460
33839
|
const exportLogs = scratchDirectory("export");
|
|
33461
33840
|
const exportArgs = [
|
|
33462
33841
|
"--headless",
|
|
33463
33842
|
"--log-file",
|
|
33464
|
-
|
|
33843
|
+
join19(exportLogs, "engine.log"),
|
|
33465
33844
|
"--path",
|
|
33466
33845
|
project.value.path,
|
|
33467
33846
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -33469,55 +33848,18 @@ class GodotServer {
|
|
|
33469
33848
|
output.absolutePath
|
|
33470
33849
|
];
|
|
33471
33850
|
this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
|
|
33472
|
-
const
|
|
33473
|
-
|
|
33851
|
+
const writtenAt = () => existsSync14(output.absolutePath) ? statSync8(output.absolutePath).mtimeMs : null;
|
|
33852
|
+
const before = writtenAt();
|
|
33853
|
+
let ending;
|
|
33474
33854
|
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);
|
|
33855
|
+
ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
|
|
33490
33856
|
} finally {
|
|
33491
33857
|
discard(exportLogs);
|
|
33492
33858
|
}
|
|
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
|
-
};
|
|
33859
|
+
if (ending.exitSignal === null && ending.exitCode === null) {
|
|
33860
|
+
return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
|
|
33519
33861
|
}
|
|
33520
|
-
return
|
|
33862
|
+
return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt());
|
|
33521
33863
|
}
|
|
33522
33864
|
async handleRunTests(args) {
|
|
33523
33865
|
const project = this.project(args);
|
|
@@ -33529,7 +33871,7 @@ class GodotServer {
|
|
|
33529
33871
|
return contained.response;
|
|
33530
33872
|
}
|
|
33531
33873
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
33532
|
-
if (!
|
|
33874
|
+
if (!existsSync14(join19(project.value.path, runner))) {
|
|
33533
33875
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
33534
33876
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
33535
33877
|
]);
|
|
@@ -33595,21 +33937,22 @@ class GodotServer {
|
|
|
33595
33937
|
if (timedOut && run.process.pid !== undefined) {
|
|
33596
33938
|
await untilGone([run.process.pid]);
|
|
33597
33939
|
}
|
|
33598
|
-
|
|
33940
|
+
await run.settled;
|
|
33941
|
+
const reportsDir = join19(project.value.path, ".godot", ...ours.split("/"));
|
|
33599
33942
|
let report = null;
|
|
33600
33943
|
let reportProblem = null;
|
|
33601
33944
|
try {
|
|
33602
|
-
const written =
|
|
33945
|
+
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
33946
|
const newest = written.at(-1);
|
|
33604
33947
|
if (newest !== undefined) {
|
|
33605
|
-
report = parseJUnit(
|
|
33948
|
+
report = parseJUnit(readFileSync18(join19(reportsDir, newest, "results.xml"), "utf8"));
|
|
33606
33949
|
}
|
|
33607
33950
|
} catch (error) {
|
|
33608
33951
|
reportProblem = errorMessage(error);
|
|
33609
33952
|
} finally {
|
|
33610
33953
|
discard(reportsDir, userData);
|
|
33611
33954
|
try {
|
|
33612
|
-
rmdirSync2(
|
|
33955
|
+
rmdirSync2(dirname12(reportsDir));
|
|
33613
33956
|
} catch {}
|
|
33614
33957
|
}
|
|
33615
33958
|
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
@@ -33622,7 +33965,11 @@ class GodotServer {
|
|
|
33622
33965
|
105: "script errors"
|
|
33623
33966
|
};
|
|
33624
33967
|
const exitCode = run.exitCode;
|
|
33625
|
-
const printed = run.log.select({
|
|
33968
|
+
const printed = run.log.select({
|
|
33969
|
+
severity: "info",
|
|
33970
|
+
sinceLastCall: false,
|
|
33971
|
+
limit: Number.POSITIVE_INFINITY
|
|
33972
|
+
}).entries;
|
|
33626
33973
|
const said = printed.map((entry) => entry.text);
|
|
33627
33974
|
const nothingRan = timedOut ? null : whyNoReport(said, asked);
|
|
33628
33975
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
@@ -33638,7 +33985,7 @@ class GodotServer {
|
|
|
33638
33985
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
33639
33986
|
};
|
|
33640
33987
|
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
|
|
33988
|
+
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
33989
|
return {
|
|
33643
33990
|
content: [
|
|
33644
33991
|
{ type: "text", text: note },
|
|
@@ -33675,10 +34022,21 @@ class GodotServer {
|
|
|
33675
34022
|
orphans: suite.orphans,
|
|
33676
34023
|
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
34024
|
}));
|
|
33678
|
-
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
|
|
34025
|
+
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);
|
|
34026
|
+
const hooksPrinted = hookFailuresPrinted(said);
|
|
34027
|
+
const hookFailures = report.suites.filter((suite) => suite.hookFailures + suite.hookErrors > 0).flatMap((suite) => {
|
|
34028
|
+
const printedHere = hooksPrinted.filter((hook) => hook.path === suite.path);
|
|
34029
|
+
return printedHere.length > 0 ? printedHere.map((hook) => ({ suite: suite.name, ...hook })) : [
|
|
34030
|
+
{
|
|
34031
|
+
suite: suite.name,
|
|
34032
|
+
path: suite.path,
|
|
34033
|
+
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."
|
|
34034
|
+
}
|
|
34035
|
+
];
|
|
34036
|
+
});
|
|
33679
34037
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
33680
34038
|
return this.jsonTextResponse({
|
|
33681
|
-
passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
34039
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
33682
34040
|
verdict,
|
|
33683
34041
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
33684
34042
|
exitCode,
|
|
@@ -33689,13 +34047,14 @@ class GodotServer {
|
|
|
33689
34047
|
skipped: report.skipped,
|
|
33690
34048
|
time: report.time,
|
|
33691
34049
|
failed,
|
|
34050
|
+
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
33692
34051
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
33693
34052
|
orphans: orphans.total > 0 ? orphans.total : undefined,
|
|
33694
34053
|
notRun: notRun > 0 ? notRun : undefined,
|
|
33695
34054
|
...printedLines,
|
|
33696
34055
|
savesNote,
|
|
33697
34056
|
...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
|
|
34057
|
+
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
34058
|
suites: unclean.map((suite) => ({
|
|
33700
34059
|
name: suite.name,
|
|
33701
34060
|
path: suite.path,
|
|
@@ -33703,6 +34062,7 @@ class GodotServer {
|
|
|
33703
34062
|
notRun: suite.discovered > suite.tests ? suite.discovered - suite.tests : undefined,
|
|
33704
34063
|
failures: suite.failures,
|
|
33705
34064
|
errors: suite.errors,
|
|
34065
|
+
hookFailures: suite.hookFailures + suite.hookErrors > 0 ? suite.hookFailures + suite.hookErrors : undefined,
|
|
33706
34066
|
skipped: suite.skipped,
|
|
33707
34067
|
orphans: orphansIn(suite.path) > 0 ? orphansIn(suite.path) : undefined,
|
|
33708
34068
|
time: suite.time
|
|
@@ -33716,7 +34076,7 @@ class GodotServer {
|
|
|
33716
34076
|
}
|
|
33717
34077
|
async godotVersion(godotPath) {
|
|
33718
34078
|
try {
|
|
33719
|
-
const { stdout } = await
|
|
34079
|
+
const { stdout } = await run4(godotPath, ["--version"], { timeout: 1e4 });
|
|
33720
34080
|
this.lastGodotVersion = stdout.trim();
|
|
33721
34081
|
return this.lastGodotVersion;
|
|
33722
34082
|
} catch (error) {
|
|
@@ -33728,8 +34088,18 @@ class GodotServer {
|
|
|
33728
34088
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
33729
34089
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
33730
34090
|
if (payload["error"] !== undefined) {
|
|
33731
|
-
const reason = payload["error"];
|
|
33732
|
-
|
|
34091
|
+
const reason = typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]);
|
|
34092
|
+
if (payload["refusedArguments"] === true) {
|
|
34093
|
+
return this.createErrorResponse(reason);
|
|
34094
|
+
}
|
|
34095
|
+
if (typeof payload["serves"] === "string") {
|
|
34096
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
34097
|
+
"editor_status names the editor on the bridge and the port it says it serves",
|
|
34098
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
34099
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
34100
|
+
]);
|
|
34101
|
+
}
|
|
34102
|
+
return this.createErrorResponse(`Diagnostics unavailable: ${reason}`, [
|
|
33733
34103
|
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
33734
34104
|
"GDHARNESS_LSP_PORT points this server at another one"
|
|
33735
34105
|
]);
|
|
@@ -33760,11 +34130,11 @@ class GodotServer {
|
|
|
33760
34130
|
const cached = cachedClasses(projectPath);
|
|
33761
34131
|
const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
|
|
33762
34132
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33763
|
-
if (!contained.ok || !
|
|
34133
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
33764
34134
|
return null;
|
|
33765
34135
|
}
|
|
33766
34136
|
try {
|
|
33767
|
-
return
|
|
34137
|
+
return readFileSync18(contained.absolutePath, "utf8");
|
|
33768
34138
|
} catch {
|
|
33769
34139
|
return null;
|
|
33770
34140
|
}
|
|
@@ -33820,11 +34190,11 @@ class GodotServer {
|
|
|
33820
34190
|
}
|
|
33821
34191
|
sourceOfClass(projectPath, resourcePath) {
|
|
33822
34192
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33823
|
-
if (!contained.ok || !
|
|
34193
|
+
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
33824
34194
|
return null;
|
|
33825
34195
|
}
|
|
33826
34196
|
try {
|
|
33827
|
-
return
|
|
34197
|
+
return readFileSync18(contained.absolutePath, "utf8");
|
|
33828
34198
|
} catch {
|
|
33829
34199
|
return null;
|
|
33830
34200
|
}
|
|
@@ -34012,7 +34382,8 @@ class GodotServer {
|
|
|
34012
34382
|
return {
|
|
34013
34383
|
playing: readBoolean(asParams(answer), "playing") ?? false,
|
|
34014
34384
|
scene: readString(asParams(answer), "scenePath") ?? "",
|
|
34015
|
-
debugPort: readNumber(asParams(answer), "debugPort")
|
|
34385
|
+
debugPort: readNumber(asParams(answer), "debugPort"),
|
|
34386
|
+
play: playOf(asParams(answer))
|
|
34016
34387
|
};
|
|
34017
34388
|
} catch {
|
|
34018
34389
|
return null;
|
|
@@ -34108,7 +34479,7 @@ class GodotServer {
|
|
|
34108
34479
|
return null;
|
|
34109
34480
|
}
|
|
34110
34481
|
try {
|
|
34111
|
-
return settingKeys(
|
|
34482
|
+
return settingKeys(readFileSync18(join19(projectPath, "project.godot"), "utf8"));
|
|
34112
34483
|
} catch {
|
|
34113
34484
|
return null;
|
|
34114
34485
|
}
|
|
@@ -34271,6 +34642,13 @@ class GodotServer {
|
|
|
34271
34642
|
if (!project.ok) {
|
|
34272
34643
|
return project.response;
|
|
34273
34644
|
}
|
|
34645
|
+
const open = this.godotBridge.isConnected() ? this.godotBridge.getStatus().projectPath : undefined;
|
|
34646
|
+
if (op !== "check" && open !== undefined && open !== "" && !isSameDirectory(open, project.value.path)) {
|
|
34647
|
+
return this.createErrorResponse(`This start names ${project.value.path}, and the editor on this bridge has ${open} open.`, [
|
|
34648
|
+
"editor_status names the project this server's editor is showing",
|
|
34649
|
+
"One server serves one editor: start a second server for the other project"
|
|
34650
|
+
]);
|
|
34651
|
+
}
|
|
34274
34652
|
const scene = readNonEmptyString(args, "scene");
|
|
34275
34653
|
const sceneToRun = scene !== undefined ? resolveWithinProject(project.value.path, scene) : null;
|
|
34276
34654
|
if (sceneToRun && !sceneToRun.ok) {
|
|
@@ -34365,7 +34743,7 @@ class GodotServer {
|
|
|
34365
34743
|
}
|
|
34366
34744
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
34367
34745
|
const budgetMs = budget.ms;
|
|
34368
|
-
if (!
|
|
34746
|
+
if (!existsSync14(join19(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
34369
34747
|
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
34370
34748
|
}
|
|
34371
34749
|
let asking = null;
|
|
@@ -34374,7 +34752,7 @@ class GodotServer {
|
|
|
34374
34752
|
let editorAsks = 0;
|
|
34375
34753
|
const waitBegan = Date.now();
|
|
34376
34754
|
const lastSaid = () => editorSaysGoing ? "playing" : "not playing";
|
|
34377
|
-
const
|
|
34755
|
+
const fresh = await announcedSince(projectPath, before, {
|
|
34378
34756
|
budgetMs,
|
|
34379
34757
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, projectPath),
|
|
34380
34758
|
giveUp: () => {
|
|
@@ -34398,11 +34776,19 @@ class GodotServer {
|
|
|
34398
34776
|
}
|
|
34399
34777
|
});
|
|
34400
34778
|
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: ${
|
|
34779
|
+
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
34780
|
const going = await this.currentRun();
|
|
34781
|
+
const endpoint = await this.theRunsAmong(going, fresh);
|
|
34403
34782
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
34404
34783
|
this.tie(going, endpoint);
|
|
34405
34784
|
}
|
|
34785
|
+
if (endpoint === null && fresh.length > 1) {
|
|
34786
|
+
return {
|
|
34787
|
+
listening: true,
|
|
34788
|
+
pids: fresh.map((one) => one.pid),
|
|
34789
|
+
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.`
|
|
34790
|
+
};
|
|
34791
|
+
}
|
|
34406
34792
|
return runtimeVerdict(endpoint, {
|
|
34407
34793
|
addon: true,
|
|
34408
34794
|
budgetMs,
|
|
@@ -34486,7 +34872,13 @@ class GodotServer {
|
|
|
34486
34872
|
};
|
|
34487
34873
|
this.activeProcess = played;
|
|
34488
34874
|
this.watchThePlayedConsole();
|
|
34489
|
-
|
|
34875
|
+
const which = playOf(playAnswer);
|
|
34876
|
+
writeEditorRunNote({
|
|
34877
|
+
projectPath,
|
|
34878
|
+
transcript: transcript.path,
|
|
34879
|
+
startedAt,
|
|
34880
|
+
...which === null ? {} : { play: which }
|
|
34881
|
+
});
|
|
34490
34882
|
sweepTranscripts();
|
|
34491
34883
|
return this.jsonTextResponse({
|
|
34492
34884
|
started: true,
|
|
@@ -34601,7 +34993,7 @@ class GodotServer {
|
|
|
34601
34993
|
return 0;
|
|
34602
34994
|
}
|
|
34603
34995
|
try {
|
|
34604
|
-
return reportAlreadyInTranscript(
|
|
34996
|
+
return reportAlreadyInTranscript(readFileSync18(game.transcript), readFileSync18(report));
|
|
34605
34997
|
} catch (error) {
|
|
34606
34998
|
this.logDebug(`Could not compare a picked-up run's report with its transcript: ${errorMessage(error)}`);
|
|
34607
34999
|
return 0;
|
|
@@ -34615,6 +35007,18 @@ class GodotServer {
|
|
|
34615
35007
|
game.brokeOn = halt.text;
|
|
34616
35008
|
game.log.record("error", halt.text);
|
|
34617
35009
|
}
|
|
35010
|
+
async untilTheEditorStopsPlaying(withinMs = STOP_WAIT_MS) {
|
|
35011
|
+
const deadline = Date.now() + withinMs;
|
|
35012
|
+
for (;; ) {
|
|
35013
|
+
if ((await this.editorPlayingState())?.playing === false) {
|
|
35014
|
+
return true;
|
|
35015
|
+
}
|
|
35016
|
+
if (Date.now() >= deadline) {
|
|
35017
|
+
return false;
|
|
35018
|
+
}
|
|
35019
|
+
await delay3(100);
|
|
35020
|
+
}
|
|
35021
|
+
}
|
|
34618
35022
|
async endActiveGame(reason, going) {
|
|
34619
35023
|
const running = this.activeProcess;
|
|
34620
35024
|
const project = running?.projectPath ?? this.ourProject();
|
|
@@ -34633,8 +35037,16 @@ class GodotServer {
|
|
|
34633
35037
|
running.endedHere = reason;
|
|
34634
35038
|
const announced = this.announcedPidOf(running);
|
|
34635
35039
|
if (running.throughEditor) {
|
|
34636
|
-
await this.handleViaBridge("stop_playing", {});
|
|
34637
|
-
|
|
35040
|
+
const asked = await this.handleViaBridge("stop_playing", {});
|
|
35041
|
+
if (asked.isError === true) {
|
|
35042
|
+
running.endedHere = null;
|
|
35043
|
+
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"}).`);
|
|
35044
|
+
return "unreached";
|
|
35045
|
+
}
|
|
35046
|
+
if (announced === undefined) {
|
|
35047
|
+
return await this.untilTheEditorStopsPlaying() ? "gone" : "lingering";
|
|
35048
|
+
}
|
|
35049
|
+
return await untilGone([announced]) ? "gone" : "lingering";
|
|
34638
35050
|
}
|
|
34639
35051
|
if (running.pid === null) {
|
|
34640
35052
|
return "gone";
|
|
@@ -34674,7 +35086,7 @@ class GodotServer {
|
|
|
34674
35086
|
return gone ? "gone" : "lingering";
|
|
34675
35087
|
}
|
|
34676
35088
|
spawnGame(godotPath, cmdArgs, env) {
|
|
34677
|
-
const child =
|
|
35089
|
+
const child = spawn4(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
|
|
34678
35090
|
const cancelled = callSignal();
|
|
34679
35091
|
if (cancelled !== undefined) {
|
|
34680
35092
|
const end = () => {
|
|
@@ -34714,7 +35126,6 @@ class GodotServer {
|
|
|
34714
35126
|
});
|
|
34715
35127
|
child.on("exit", (code, signal) => {
|
|
34716
35128
|
this.logDebug(`Godot process exited with code ${code ?? "none"}, signal ${signal ?? "none"}`);
|
|
34717
|
-
log.finish();
|
|
34718
35129
|
started.exitCode = code;
|
|
34719
35130
|
started.exitSignal = code === null ? signal : null;
|
|
34720
35131
|
});
|
|
@@ -34724,7 +35135,28 @@ class GodotServer {
|
|
|
34724
35135
|
`);
|
|
34725
35136
|
started.exitCode = -1;
|
|
34726
35137
|
});
|
|
34727
|
-
|
|
35138
|
+
const settled = new Promise((resolve) => {
|
|
35139
|
+
let open = 2;
|
|
35140
|
+
const done = () => {
|
|
35141
|
+
log.finish();
|
|
35142
|
+
resolve();
|
|
35143
|
+
};
|
|
35144
|
+
const closed = () => {
|
|
35145
|
+
open -= 1;
|
|
35146
|
+
if (open === 0) {
|
|
35147
|
+
done();
|
|
35148
|
+
}
|
|
35149
|
+
};
|
|
35150
|
+
child.stdout.once("close", closed);
|
|
35151
|
+
child.stderr.once("close", closed);
|
|
35152
|
+
child.once("exit", () => {
|
|
35153
|
+
setTimeout(done, STREAMS_GRACE_MS).unref();
|
|
35154
|
+
});
|
|
35155
|
+
child.once("error", () => {
|
|
35156
|
+
done();
|
|
35157
|
+
});
|
|
35158
|
+
});
|
|
35159
|
+
return Object.assign(started, { settled });
|
|
34728
35160
|
}
|
|
34729
35161
|
async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
|
|
34730
35162
|
const startedAt = Date.now();
|
|
@@ -34806,7 +35238,8 @@ class GodotServer {
|
|
|
34806
35238
|
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
34807
35239
|
}
|
|
34808
35240
|
const project = this.godotBridge.getStatus().projectPath ?? null;
|
|
34809
|
-
const
|
|
35241
|
+
const recorded = project === null ? null : readEditorRunNote(project);
|
|
35242
|
+
const note = recorded !== null && noteIsAbout(recorded, playing.play) ? recorded : null;
|
|
34810
35243
|
const picked = {
|
|
34811
35244
|
pid: null,
|
|
34812
35245
|
log: new GameLog,
|
|
@@ -34863,6 +35296,31 @@ class GodotServer {
|
|
|
34863
35296
|
const its = await this.tiedThroughTheTree(run, running);
|
|
34864
35297
|
return its !== undefined && running.some((one) => one.pid === its) ? its : undefined;
|
|
34865
35298
|
}
|
|
35299
|
+
async theRunsAmong(run, fresh) {
|
|
35300
|
+
if (fresh.length === 1) {
|
|
35301
|
+
return fresh[0] ?? null;
|
|
35302
|
+
}
|
|
35303
|
+
if (fresh.length === 0 || run === null) {
|
|
35304
|
+
return null;
|
|
35305
|
+
}
|
|
35306
|
+
const pid = run.announcedPid ?? await this.tiedThroughTheTree(run, fresh);
|
|
35307
|
+
return fresh.find((one) => one.pid === pid) ?? null;
|
|
35308
|
+
}
|
|
35309
|
+
async tieThePlayedGame(run) {
|
|
35310
|
+
if (!run.throughEditor || run.errorReport !== undefined || run.announcedPid !== undefined) {
|
|
35311
|
+
return;
|
|
35312
|
+
}
|
|
35313
|
+
const announced = runtimesAnnounced().running;
|
|
35314
|
+
if (this.announcedPidOf(run, announced) !== undefined || !stillRunning(run)) {
|
|
35315
|
+
return;
|
|
35316
|
+
}
|
|
35317
|
+
const among = this.announcedOfTheRunsProject(run, announced).map((one) => one.pid).sort((one, other) => one - other).join(",");
|
|
35318
|
+
if (among === "" || run.treeReadFor === among) {
|
|
35319
|
+
return;
|
|
35320
|
+
}
|
|
35321
|
+
run.treeReadFor = among;
|
|
35322
|
+
await this.tiedThroughTheTree(run, announced);
|
|
35323
|
+
}
|
|
34866
35324
|
async tiedThroughTheTree(run, announced) {
|
|
34867
35325
|
if (run.announcedPid !== undefined) {
|
|
34868
35326
|
return run.announcedPid;
|
|
@@ -35063,6 +35521,7 @@ class GodotServer {
|
|
|
35063
35521
|
resolve(false);
|
|
35064
35522
|
});
|
|
35065
35523
|
});
|
|
35524
|
+
await boot.settled;
|
|
35066
35525
|
const errors = boot.log.count("error");
|
|
35067
35526
|
const warnings = boot.log.count("warning");
|
|
35068
35527
|
return this.jsonTextResponse({
|
|
@@ -35204,6 +35663,7 @@ class GodotServer {
|
|
|
35204
35663
|
if (!run) {
|
|
35205
35664
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
35206
35665
|
}
|
|
35666
|
+
await this.tieThePlayedGame(run);
|
|
35207
35667
|
this.drainEditorOutput(run);
|
|
35208
35668
|
this.drainTranscript(run);
|
|
35209
35669
|
const going = await this.runStillGoing(run);
|
|
@@ -35239,7 +35699,7 @@ class GodotServer {
|
|
|
35239
35699
|
notes.push(endedWithoutACode({
|
|
35240
35700
|
throughEditor: run.throughEditor,
|
|
35241
35701
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
35242
|
-
couldAnnounce: run.projectPath !== null &&
|
|
35702
|
+
couldAnnounce: run.projectPath !== null && existsSync14(join19(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
35243
35703
|
endedHere: typeof run.endedHere === "string",
|
|
35244
35704
|
pickedUp: run.pickedUp === true
|
|
35245
35705
|
}));
|
|
@@ -35247,6 +35707,9 @@ class GodotServer {
|
|
|
35247
35707
|
if (run.pickedUpPlaying === true) {
|
|
35248
35708
|
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
35709
|
}
|
|
35710
|
+
if (errorsUnread(run)) {
|
|
35711
|
+
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.");
|
|
35712
|
+
}
|
|
35250
35713
|
if (run.consoleLost === true) {
|
|
35251
35714
|
const back = this.dapClient?.isConnected() === true;
|
|
35252
35715
|
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 +35725,9 @@ class GodotServer {
|
|
|
35262
35725
|
pid: run.pid ?? announced ?? run.announcedPid ?? null,
|
|
35263
35726
|
errors: run.log.count("error"),
|
|
35264
35727
|
warnings: run.log.count("warning"),
|
|
35265
|
-
clean: run
|
|
35728
|
+
clean: cleanVerdict(run),
|
|
35266
35729
|
consoleLost: run.consoleLost === true ? true : undefined,
|
|
35730
|
+
errorsUnread: errorsUnread(run) ? true : undefined,
|
|
35267
35731
|
...hold,
|
|
35268
35732
|
endedUnwatched: run.endedUnwatched === true ? true : undefined,
|
|
35269
35733
|
endedBy: stillRunning(run) ? undefined : run.endedHere ?? null,
|
|
@@ -35298,6 +35762,7 @@ class GodotServer {
|
|
|
35298
35762
|
if (elsewhere !== null) {
|
|
35299
35763
|
return elsewhere;
|
|
35300
35764
|
}
|
|
35765
|
+
await this.tieThePlayedGame(stopped);
|
|
35301
35766
|
this.drainEditorOutput(stopped);
|
|
35302
35767
|
this.drainTranscript(stopped);
|
|
35303
35768
|
const wasRunning = await this.runStillGoing(stopped);
|
|
@@ -35306,7 +35771,7 @@ class GodotServer {
|
|
|
35306
35771
|
const children = readBoolean(args, "andChildren") === true && wasRunning ? await this.whatTheRunStarted(stopped) : null;
|
|
35307
35772
|
this.logDebug("Stopping the running game");
|
|
35308
35773
|
const ending = await this.endActiveGame("editor_run stop", wasRunning);
|
|
35309
|
-
const refused = ending === "refused";
|
|
35774
|
+
const refused = ending === "refused" || ending === "unreached";
|
|
35310
35775
|
const ended = children === null || refused ? null : endChildrenAmong(children);
|
|
35311
35776
|
if (wasRunning && !refused) {
|
|
35312
35777
|
for (const pid of new Set([endedPid, announcedPid])) {
|
|
@@ -35321,7 +35786,7 @@ class GodotServer {
|
|
|
35321
35786
|
endedPid,
|
|
35322
35787
|
throughEditor: stopped.throughEditor,
|
|
35323
35788
|
exitSignal: stopped.exitSignal,
|
|
35324
|
-
|
|
35789
|
+
clean: cleanVerdict(stopped)
|
|
35325
35790
|
});
|
|
35326
35791
|
return this.jsonTextResponse({
|
|
35327
35792
|
stopped: verdict.stopped,
|
|
@@ -35335,6 +35800,7 @@ class GodotServer {
|
|
|
35335
35800
|
errors: stopped.log.count("error"),
|
|
35336
35801
|
warnings: stopped.log.count("warning"),
|
|
35337
35802
|
clean: verdict.clean,
|
|
35803
|
+
errorsUnread: errorsUnread(stopped) ? true : undefined,
|
|
35338
35804
|
note: verdict.withChildren ? `${verdict.note} ${aboutTheChildren(ended, stopped.throughEditor)}` : verdict.note,
|
|
35339
35805
|
entries: forAnswer(stopped.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries)
|
|
35340
35806
|
});
|
|
@@ -35547,7 +36013,12 @@ class GodotServer {
|
|
|
35547
36013
|
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
35548
36014
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
35549
36015
|
let reloaded = {};
|
|
35550
|
-
|
|
36016
|
+
const reloadingSource = reloading === undefined || projectPath === "" ? null : this.sourceOfClass(projectPath, reloading);
|
|
36017
|
+
if (reloading !== undefined && reloadingSource !== null && isToolScript(reloadingSource)) {
|
|
36018
|
+
reloaded = {
|
|
36019
|
+
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.`
|
|
36020
|
+
};
|
|
36021
|
+
} else if (reloading !== undefined) {
|
|
35551
36022
|
try {
|
|
35552
36023
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
35553
36024
|
const methods = readArray(answer, "methods");
|
|
@@ -35555,29 +36026,36 @@ class GodotServer {
|
|
|
35555
36026
|
const found = readArray(answer, field);
|
|
35556
36027
|
return found ? { [field]: found.map(String) } : {};
|
|
35557
36028
|
};
|
|
36029
|
+
const asResource = (path) => path.startsWith("res://") ? path : `res://${path.replace(/^\/+/, "")}`;
|
|
36030
|
+
const script = asResource(readString(answer, "script") ?? reloading);
|
|
36031
|
+
const spelling = script === asResource(reloading) ? {} : { as: script };
|
|
35558
36032
|
const failed = answer["failed"];
|
|
35559
36033
|
if (typeof failed === "number" && failed !== 0) {
|
|
35560
36034
|
reloaded = {
|
|
36035
|
+
...spelling,
|
|
35561
36036
|
...listed("heldBefore"),
|
|
35562
36037
|
...listed("heldConstantsBefore"),
|
|
35563
36038
|
problem: failedReloadNote({
|
|
35564
|
-
script
|
|
36039
|
+
script,
|
|
35565
36040
|
code: failed,
|
|
35566
36041
|
codeName: readString(answer, "failedAs") ?? "",
|
|
35567
36042
|
heldBefore: listed("heldBefore")["heldBefore"] ?? [],
|
|
35568
36043
|
heldAfter: methods?.map(String) ?? [],
|
|
35569
36044
|
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
35570
36045
|
constantsAfter: listed("constants")["constants"] ?? [],
|
|
35571
|
-
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath,
|
|
36046
|
+
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, script),
|
|
36047
|
+
restored: typeof answer["restored"] === "boolean" ? answer["restored"] : null,
|
|
36048
|
+
restoredAs: readString(answer, "restoredAs") ?? ""
|
|
35572
36049
|
})
|
|
35573
36050
|
};
|
|
35574
36051
|
} else {
|
|
35575
36052
|
reloaded = methods ? {
|
|
36053
|
+
...spelling,
|
|
35576
36054
|
methods: methods.map(String),
|
|
35577
36055
|
...listed("heldBefore"),
|
|
35578
36056
|
...listed("constants"),
|
|
35579
36057
|
...listed("heldConstantsBefore")
|
|
35580
|
-
} : { problem: `the editor would not say what ${
|
|
36058
|
+
} : { problem: `the editor would not say what ${script} has after reloading it` };
|
|
35581
36059
|
}
|
|
35582
36060
|
} catch (error) {
|
|
35583
36061
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
@@ -35599,7 +36077,7 @@ class GodotServer {
|
|
|
35599
36077
|
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
36078
|
}
|
|
35601
36079
|
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
35602
|
-
notes.push(
|
|
36080
|
+
notes.push(notReloadedNote(broughtIn, dependents.notReloaded));
|
|
35603
36081
|
}
|
|
35604
36082
|
for (const group of uidsDuplicatedOnDisk) {
|
|
35605
36083
|
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 +36092,7 @@ class GodotServer {
|
|
|
35614
36092
|
stillWorking: busy,
|
|
35615
36093
|
waitedMs: Date.now() - started,
|
|
35616
36094
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
36095
|
+
reloadedAs: reloaded.as,
|
|
35617
36096
|
heldBeforeReload: reloaded.heldBefore,
|
|
35618
36097
|
reloadedMethods: reloaded.methods,
|
|
35619
36098
|
heldConstantsBeforeReload: reloaded.heldConstantsBefore,
|
|
@@ -35640,7 +36119,8 @@ class GodotServer {
|
|
|
35640
36119
|
if (tool) {
|
|
35641
36120
|
notReloaded.push({
|
|
35642
36121
|
scriptPath,
|
|
35643
|
-
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it"
|
|
36122
|
+
problem: "a @tool script, which runs inside the editor, so it is not reloaded from here; editor_launch restart recompiles it",
|
|
36123
|
+
tool: true
|
|
35644
36124
|
});
|
|
35645
36125
|
continue;
|
|
35646
36126
|
}
|
|
@@ -35648,9 +36128,10 @@ class GodotServer {
|
|
|
35648
36128
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
35649
36129
|
const failed = answer["failed"];
|
|
35650
36130
|
if (typeof failed === "number" && failed !== 0) {
|
|
36131
|
+
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
36132
|
notReloaded.push({
|
|
35652
36133
|
scriptPath,
|
|
35653
|
-
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
|
|
36134
|
+
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}${left}`
|
|
35654
36135
|
});
|
|
35655
36136
|
} else if (readArray(answer, "methods") === undefined) {
|
|
35656
36137
|
notReloaded.push({
|
|
@@ -35722,6 +36203,35 @@ class GodotServer {
|
|
|
35722
36203
|
}
|
|
35723
36204
|
};
|
|
35724
36205
|
}
|
|
36206
|
+
async handleCreateScript(args) {
|
|
36207
|
+
const project = this.project(args);
|
|
36208
|
+
if (!project.ok) {
|
|
36209
|
+
return project.response;
|
|
36210
|
+
}
|
|
36211
|
+
const contained = this.containProjectFiles(args);
|
|
36212
|
+
if (!contained.ok) {
|
|
36213
|
+
return contained.response;
|
|
36214
|
+
}
|
|
36215
|
+
const { op: _op, projectPath: _projectPath, ...params } = contained.value;
|
|
36216
|
+
const created = await this.operation("create_script", params, project.value.path);
|
|
36217
|
+
const declared = created.ok ? readString(created.payload, "class_name") ?? "" : "";
|
|
36218
|
+
if (!created.ok || declared === "") {
|
|
36219
|
+
return this.answer(created);
|
|
36220
|
+
}
|
|
36221
|
+
const rebuilt = await this.rebuildClassCache(project.value.path);
|
|
36222
|
+
const registered = cachedClasses(project.value.path)?.get(declared) === readString(created.payload, "full_path");
|
|
36223
|
+
const answered = this.answer({
|
|
36224
|
+
...created,
|
|
36225
|
+
payload: {
|
|
36226
|
+
...created.payload,
|
|
36227
|
+
registered,
|
|
36228
|
+
...registered ? {} : {
|
|
36229
|
+
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}`
|
|
36230
|
+
}
|
|
36231
|
+
}
|
|
36232
|
+
});
|
|
36233
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
36234
|
+
}
|
|
35725
36235
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
35726
36236
|
const first = answered.content[0];
|
|
35727
36237
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -35850,7 +36360,7 @@ class GodotServer {
|
|
|
35850
36360
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
35851
36361
|
const run = await this.currentRun();
|
|
35852
36362
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
35853
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !
|
|
36363
|
+
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
36364
|
return null;
|
|
35855
36365
|
}
|
|
35856
36366
|
const already = runtimesAnnounced().running;
|
|
@@ -35874,11 +36384,15 @@ class GodotServer {
|
|
|
35874
36384
|
const seconds = Math.round((Date.now() - run.startedAt) / 1000);
|
|
35875
36385
|
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
36386
|
}
|
|
35877
|
-
const
|
|
36387
|
+
const fresh = await announcedSince(project, new Set(already.map((one) => one.pid)), {
|
|
35878
36388
|
budgetMs: Math.min(budgetMs, remaining),
|
|
35879
36389
|
accept: (one) => this.isTheGameOf(this.activeProcess, one, project),
|
|
35880
36390
|
giveUp: () => this.activeProcess !== run || this.dapClient?.isStopped() === true || !stillRunning(run)
|
|
35881
36391
|
});
|
|
36392
|
+
const endpoint = await this.theRunsAmong(run, fresh);
|
|
36393
|
+
if (fresh.length > 0 && endpoint === null) {
|
|
36394
|
+
return null;
|
|
36395
|
+
}
|
|
35882
36396
|
if (endpoint !== null) {
|
|
35883
36397
|
const tiedMeanwhile = () => run.announcedPid !== undefined;
|
|
35884
36398
|
if (!tiedMeanwhile()) {
|
|
@@ -35930,7 +36444,7 @@ class GodotServer {
|
|
|
35930
36444
|
}
|
|
35931
36445
|
}
|
|
35932
36446
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
35933
|
-
const screenshotPath = screenshotDir ?
|
|
36447
|
+
const screenshotPath = screenshotDir ? join19(screenshotDir, "capture.png") : null;
|
|
35934
36448
|
try {
|
|
35935
36449
|
const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
|
|
35936
36450
|
if (!reply.ok && reply.requestId !== undefined) {
|
|
@@ -35971,7 +36485,7 @@ class GodotServer {
|
|
|
35971
36485
|
type: "text",
|
|
35972
36486
|
text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
|
|
35973
36487
|
},
|
|
35974
|
-
{ type: "image", data:
|
|
36488
|
+
{ type: "image", data: readFileSync18(screenshotPath).toString("base64"), mimeType: "image/png" }
|
|
35975
36489
|
]
|
|
35976
36490
|
};
|
|
35977
36491
|
} finally {
|