gdharness 0.9.7 → 0.10.1
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 +122 -12
- package/build/godot/addons/gdharness_editor/tool_executor.gd +8 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +56 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd.uid +1 -0
- package/build/godot/operations/project_config.gd +38 -0
- package/build/index.js +116 -10
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -7474,6 +7474,9 @@ import { join as join6 } from "node:path";
|
|
|
7474
7474
|
function declaredClasses(projectPath) {
|
|
7475
7475
|
const declared = new Map;
|
|
7476
7476
|
const visit = (directory, prefix) => {
|
|
7477
|
+
if (existsSync6(join6(directory, ".gdignore"))) {
|
|
7478
|
+
return;
|
|
7479
|
+
}
|
|
7477
7480
|
for (const entry of readdirSync2(directory, { withFileTypes: true })) {
|
|
7478
7481
|
if (entry.name.startsWith(".")) {
|
|
7479
7482
|
continue;
|
|
@@ -7511,6 +7514,12 @@ function staleClassNames(projectPath) {
|
|
|
7511
7514
|
const cached = cachedClasses(projectPath);
|
|
7512
7515
|
return cached === null ? [...declaredClasses(projectPath).keys()] : staleAgainst(cached, projectPath);
|
|
7513
7516
|
}
|
|
7517
|
+
function unseenByEditor(projectPath, editorHolds) {
|
|
7518
|
+
const held = new Set(editorHolds);
|
|
7519
|
+
const declared = declaredClasses(projectPath);
|
|
7520
|
+
const source = cachedClasses(projectPath) ?? declared;
|
|
7521
|
+
return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
|
|
7522
|
+
}
|
|
7514
7523
|
var init_class_cache = () => {};
|
|
7515
7524
|
|
|
7516
7525
|
// src/headless.ts
|
|
@@ -15371,7 +15380,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15371
15380
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
15372
15381
|
refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
|
|
15373
15382
|
refresh_classes: {
|
|
15374
|
-
summary:
|
|
15383
|
+
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
15375
15384
|
requires: []
|
|
15376
15385
|
}
|
|
15377
15386
|
}
|
|
@@ -15671,7 +15680,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15671
15680
|
},
|
|
15672
15681
|
{
|
|
15673
15682
|
name: "editor_run",
|
|
15674
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
|
|
15683
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
|
|
15675
15684
|
parameters: {
|
|
15676
15685
|
projectPath: PROJECT_PATH,
|
|
15677
15686
|
scene: { type: "string", description: "A scene to run instead of the main scene." },
|
|
@@ -15716,7 +15725,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15716
15725
|
},
|
|
15717
15726
|
{
|
|
15718
15727
|
name: "editor_rescan",
|
|
15719
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file
|
|
15728
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
|
|
15720
15729
|
parameters: {
|
|
15721
15730
|
projectPath: PROJECT_PATH,
|
|
15722
15731
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -35771,6 +35780,7 @@ import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync a
|
|
|
35771
35780
|
import { createConnection as createConnection3 } from "node:net";
|
|
35772
35781
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
35773
35782
|
import { join as join13, resolve as resolve4 } from "node:path";
|
|
35783
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
35774
35784
|
function runtimeDirectory(variables = process.env) {
|
|
35775
35785
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
35776
35786
|
if (explicit) {
|
|
@@ -35859,6 +35869,22 @@ function announcedIn(directory) {
|
|
|
35859
35869
|
}
|
|
35860
35870
|
return found;
|
|
35861
35871
|
}
|
|
35872
|
+
async function announcedSince(projectPath, before, waiting = {}) {
|
|
35873
|
+
const wanted = resolve4(projectPath);
|
|
35874
|
+
const directories = waiting.directories ?? runtimeDirectories();
|
|
35875
|
+
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
35876
|
+
for (;; ) {
|
|
35877
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
35878
|
+
if (fresh !== undefined) {
|
|
35879
|
+
return fresh;
|
|
35880
|
+
}
|
|
35881
|
+
const left = until - Date.now();
|
|
35882
|
+
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
35883
|
+
return null;
|
|
35884
|
+
}
|
|
35885
|
+
await delay2(Math.min(LOOK_EVERY_MS, left));
|
|
35886
|
+
}
|
|
35887
|
+
}
|
|
35862
35888
|
function describe2(endpoint) {
|
|
35863
35889
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
35864
35890
|
}
|
|
@@ -35985,7 +36011,7 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
35985
36011
|
});
|
|
35986
36012
|
});
|
|
35987
36013
|
}
|
|
35988
|
-
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, nextRequestId = 1;
|
|
36014
|
+
var RUNTIME_PROTOCOL = 2, ANNOUNCEMENT_PATTERN, LOOK_EVERY_MS = 50, ANNOUNCE_BUDGET_MS = 5000, nextRequestId = 1;
|
|
35989
36015
|
var init_runtime_client = __esm(() => {
|
|
35990
36016
|
ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
|
|
35991
36017
|
});
|
|
@@ -36001,13 +36027,17 @@ function installedAddonVersion(projectPath) {
|
|
|
36001
36027
|
const said = readFileSync12(marker, "utf8").trim();
|
|
36002
36028
|
return said === "" ? null : said;
|
|
36003
36029
|
}
|
|
36004
|
-
var ADDONS2, VERSION_MARKER2 = ".gdharness-version";
|
|
36030
|
+
var ADDONS2, RUNTIME_AUTOLOAD2, VERSION_MARKER2 = ".gdharness-version";
|
|
36005
36031
|
var init_setup = __esm(() => {
|
|
36006
36032
|
init_class_cache();
|
|
36007
36033
|
init_headless();
|
|
36008
36034
|
init_resources();
|
|
36009
36035
|
init_server_version();
|
|
36010
36036
|
ADDONS2 = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
36037
|
+
RUNTIME_AUTOLOAD2 = {
|
|
36038
|
+
name: "GdharnessRuntime",
|
|
36039
|
+
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
36040
|
+
};
|
|
36011
36041
|
});
|
|
36012
36042
|
|
|
36013
36043
|
// src/server.ts
|
|
@@ -36015,7 +36045,7 @@ import { execFile as execFile5, spawn } from "node:child_process";
|
|
|
36015
36045
|
import { existsSync as existsSync12, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync13, realpathSync as realpathSync2, rmSync as rmSync8 } from "node:fs";
|
|
36016
36046
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36017
36047
|
import { basename as basename2, dirname as dirname9, join as join15, normalize as normalize3 } from "node:path";
|
|
36018
|
-
import { setTimeout as
|
|
36048
|
+
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36019
36049
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
36020
36050
|
import { promisify as promisify5 } from "node:util";
|
|
36021
36051
|
function patienceForFrames(frames, atLeast) {
|
|
@@ -36092,6 +36122,10 @@ function realPathOr(path) {
|
|
|
36092
36122
|
return path;
|
|
36093
36123
|
}
|
|
36094
36124
|
}
|
|
36125
|
+
function samePath(one, other) {
|
|
36126
|
+
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
36127
|
+
return settled(one) === settled(other);
|
|
36128
|
+
}
|
|
36095
36129
|
function aboveTheRunner(entry) {
|
|
36096
36130
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
36097
36131
|
if (runner < 0) {
|
|
@@ -36522,7 +36556,8 @@ class GodotServer {
|
|
|
36522
36556
|
async dispatch(tool, op, args) {
|
|
36523
36557
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
36524
36558
|
if (headless !== undefined) {
|
|
36525
|
-
|
|
36559
|
+
const answered = await this.headless(headless, args);
|
|
36560
|
+
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
36526
36561
|
}
|
|
36527
36562
|
const { op: _op, ...arguments_ } = args;
|
|
36528
36563
|
const bridge = async (command, extra = {}) => {
|
|
@@ -37327,7 +37362,7 @@ class GodotServer {
|
|
|
37327
37362
|
if (Date.now() >= deadline) {
|
|
37328
37363
|
return false;
|
|
37329
37364
|
}
|
|
37330
|
-
await
|
|
37365
|
+
await delay3(250);
|
|
37331
37366
|
}
|
|
37332
37367
|
}
|
|
37333
37368
|
async handleLaunchEditor(args) {
|
|
@@ -37423,12 +37458,13 @@ class GodotServer {
|
|
|
37423
37458
|
this.logDebug("Ending the running game before starting another");
|
|
37424
37459
|
await this.endActiveGame();
|
|
37425
37460
|
}
|
|
37461
|
+
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
37426
37462
|
const headless = resolveHeadless(args["headless"], {
|
|
37427
37463
|
platform: process.platform,
|
|
37428
37464
|
variables: process.env
|
|
37429
37465
|
});
|
|
37430
37466
|
if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
|
|
37431
|
-
return await this.playThroughEditor(sceneArgument, refreshed.value);
|
|
37467
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
|
|
37432
37468
|
}
|
|
37433
37469
|
const cmdArgs = runArguments({
|
|
37434
37470
|
projectPath: project.value.path,
|
|
@@ -37444,10 +37480,31 @@ class GodotServer {
|
|
|
37444
37480
|
pid: started.process.pid ?? null,
|
|
37445
37481
|
arguments: cmdArgs,
|
|
37446
37482
|
refreshedClasses: refreshed.value,
|
|
37483
|
+
runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
|
|
37447
37484
|
message: "Use editor_output for what it prints and editor_run stop to end it."
|
|
37448
37485
|
});
|
|
37449
37486
|
}
|
|
37450
|
-
async
|
|
37487
|
+
async runtimeUp(projectPath, before) {
|
|
37488
|
+
if (!existsSync12(join15(projectPath, RUNTIME_AUTOLOAD2.path))) {
|
|
37489
|
+
return {
|
|
37490
|
+
listening: false,
|
|
37491
|
+
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
37492
|
+
};
|
|
37493
|
+
}
|
|
37494
|
+
const endpoint = await announcedSince(projectPath, before, {
|
|
37495
|
+
giveUp: () => this.dapClient?.isStopped() === true
|
|
37496
|
+
});
|
|
37497
|
+
if (endpoint === null) {
|
|
37498
|
+
const held = this.dapClient?.whereItStopped() ?? null;
|
|
37499
|
+
return {
|
|
37500
|
+
listening: false,
|
|
37501
|
+
note: held === null ? `nothing announced itself within ${ANNOUNCE_BUDGET_MS}ms: the game may be slow to boot, or its runtime is not coming up` : "the game stopped before its runtime came up, so the runtime_* tools have nothing to talk to yet: debug_control continue lets it carry on",
|
|
37502
|
+
heldAt: held
|
|
37503
|
+
};
|
|
37504
|
+
}
|
|
37505
|
+
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
37506
|
+
}
|
|
37507
|
+
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying) {
|
|
37451
37508
|
const log = new GameLog;
|
|
37452
37509
|
try {
|
|
37453
37510
|
await this.dap().connect();
|
|
@@ -37476,6 +37533,7 @@ class GodotServer {
|
|
|
37476
37533
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
37477
37534
|
refreshedClasses,
|
|
37478
37535
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
37536
|
+
runtime: await this.runtimeUp(projectPath, alreadyPlaying),
|
|
37479
37537
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
|
|
37480
37538
|
});
|
|
37481
37539
|
}
|
|
@@ -37630,13 +37688,59 @@ class GodotServer {
|
|
|
37630
37688
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
37631
37689
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
|
|
37632
37690
|
}
|
|
37691
|
+
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
37692
|
+
const unseen = checked.unseen;
|
|
37633
37693
|
return this.jsonTextResponse({
|
|
37634
|
-
ok: !busy,
|
|
37694
|
+
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
37635
37695
|
stillWorking: busy,
|
|
37636
37696
|
waitedMs: Date.now() - started,
|
|
37637
|
-
|
|
37697
|
+
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
37698
|
+
classesUnchecked: checked.unchecked,
|
|
37699
|
+
note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : unseen.length > 0 ? "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. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
|
|
37638
37700
|
});
|
|
37639
37701
|
}
|
|
37702
|
+
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
37703
|
+
const first = answered.content[0];
|
|
37704
|
+
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
37705
|
+
return answered;
|
|
37706
|
+
}
|
|
37707
|
+
const checked = await this.classesTheEditorCannotSee(args);
|
|
37708
|
+
if (checked.unseen.length === 0 && checked.unchecked === undefined) {
|
|
37709
|
+
return answered;
|
|
37710
|
+
}
|
|
37711
|
+
return this.jsonTextResponse({
|
|
37712
|
+
...asParams(JSON.parse(first.text)),
|
|
37713
|
+
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
37714
|
+
classesUnchecked: checked.unchecked,
|
|
37715
|
+
note: checked.unseen.length > 0 ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. Change the declaring script and editor_rescan, or restart the editor with editor_launch restart." : undefined
|
|
37716
|
+
});
|
|
37717
|
+
}
|
|
37718
|
+
async classesTheEditorCannotSee(args) {
|
|
37719
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
37720
|
+
if (projectPath === "") {
|
|
37721
|
+
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
37722
|
+
}
|
|
37723
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
37724
|
+
if (open === undefined || !samePath(open, projectPath)) {
|
|
37725
|
+
return {
|
|
37726
|
+
unseen: [],
|
|
37727
|
+
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
37728
|
+
};
|
|
37729
|
+
}
|
|
37730
|
+
let held;
|
|
37731
|
+
try {
|
|
37732
|
+
held = asParams(await this.godotBridge.invokeTool("global_classes", args));
|
|
37733
|
+
} catch (error) {
|
|
37734
|
+
return {
|
|
37735
|
+
unseen: [],
|
|
37736
|
+
unchecked: `the editor would not say what classes it is holding: ${errorMessage(error)}`
|
|
37737
|
+
};
|
|
37738
|
+
}
|
|
37739
|
+
if (!Array.isArray(held["classes"])) {
|
|
37740
|
+
return { unseen: [], unchecked: "the editor answered without a class list in it" };
|
|
37741
|
+
}
|
|
37742
|
+
return { unseen: unseenByEditor(projectPath, held["classes"].map(String)) };
|
|
37743
|
+
}
|
|
37640
37744
|
async handleViaBridge(toolName, args) {
|
|
37641
37745
|
if (!this.godotBridge.isConnected()) {
|
|
37642
37746
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
@@ -39226,6 +39330,12 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
|
|
|
39226
39330
|
\`editor_classes\` need nothing open: they run a short headless engine and are gone before the
|
|
39227
39331
|
answer is printed.
|
|
39228
39332
|
|
|
39333
|
+
A wall of "Could not find type" on the project's own classes means the editor is not holding
|
|
39334
|
+
them. \`editor_rescan\` asks it to scan and names any class it still cannot resolve under
|
|
39335
|
+
\`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
|
|
39336
|
+
declaration and the cache can both be right while the editor stays blind to it. Change the
|
|
39337
|
+
declaring script, or restart the editor.
|
|
39338
|
+
|
|
39229
39339
|
## Refusals are useful
|
|
39230
39340
|
|
|
39231
39341
|
An unknown op, an argument the tool does not name, or a missing required one is refused with the
|
|
@@ -9,6 +9,7 @@ const SceneTools = preload("tools/scene_tools.gd")
|
|
|
9
9
|
const ResourceTools = preload("tools/resource_tools.gd")
|
|
10
10
|
const AnimationTools = preload("tools/animation_tools.gd")
|
|
11
11
|
const PlayTools = preload("tools/play_tools.gd")
|
|
12
|
+
const ClassTools = preload("tools/class_tools.gd")
|
|
12
13
|
|
|
13
14
|
var _editor_plugin: EditorPlugin = null
|
|
14
15
|
|
|
@@ -16,6 +17,7 @@ var _scene_tools: SceneTools = null
|
|
|
16
17
|
var _resource_tools: ResourceTools = null
|
|
17
18
|
var _animation_tools: AnimationTools = null
|
|
18
19
|
var _play_tools: PlayTools = null
|
|
20
|
+
var _class_tools: ClassTools = null
|
|
19
21
|
|
|
20
22
|
var _tool_map: Dictionary = {}
|
|
21
23
|
var _initialized: bool = false
|
|
@@ -28,6 +30,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
28
30
|
_resource_tools.set_editor_plugin(plugin)
|
|
29
31
|
_animation_tools.set_editor_plugin(plugin)
|
|
30
32
|
_play_tools.set_editor_plugin(plugin)
|
|
33
|
+
_class_tools.set_editor_plugin(plugin)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
func _init_tools() -> void:
|
|
@@ -51,6 +54,10 @@ func _init_tools() -> void:
|
|
|
51
54
|
_play_tools.name = "PlayTools"
|
|
52
55
|
add_child(_play_tools)
|
|
53
56
|
|
|
57
|
+
_class_tools = ClassTools.new()
|
|
58
|
+
_class_tools.name = "ClassTools"
|
|
59
|
+
add_child(_class_tools)
|
|
60
|
+
|
|
54
61
|
_tool_map = {
|
|
55
62
|
# Scene tools
|
|
56
63
|
"create_scene": [_scene_tools, "create_scene"],
|
|
@@ -66,6 +73,7 @@ func _init_tools() -> void:
|
|
|
66
73
|
"disconnect_signal": [_scene_tools, "disconnect_signal"],
|
|
67
74
|
"list_connections": [_scene_tools, "list_connections"],
|
|
68
75
|
"rescan_filesystem": [_scene_tools, "rescan_filesystem"],
|
|
76
|
+
"global_classes": [_class_tools, "global_classes"],
|
|
69
77
|
# Resource tools
|
|
70
78
|
"create_resource": [_resource_tools, "create_resource"],
|
|
71
79
|
"modify_resource": [_resource_tools, "modify_resource"],
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends Node
|
|
3
|
+
|
|
4
|
+
## The global classes the running editor is holding.
|
|
5
|
+
##
|
|
6
|
+
## The editor fixes this list when it starts and adds to it only from a filesystem scan, and
|
|
7
|
+
## that scan is change-detecting: a file another engine has already imported looks settled, so
|
|
8
|
+
## the walk goes past it without reading the declaration inside. The class is then declared on
|
|
9
|
+
## disk, listed in `.godot/global_script_class_cache.cfg`, and absent from the editor, whose
|
|
10
|
+
## analyser reports every use of it as an unknown identifier. Rebuilding the cache does not
|
|
11
|
+
## help, because nothing rereads it.
|
|
12
|
+
##
|
|
13
|
+
## Every check there was compares one file on disk against another, so all of them call that
|
|
14
|
+
## state clean. The editor is the only thing that can say otherwise, so it is asked.
|
|
15
|
+
##
|
|
16
|
+
## Read off the editor's own filesystem entries rather than from
|
|
17
|
+
## `ProjectSettings.get_global_class_list()`. The entries are what the scan itself recorded, so
|
|
18
|
+
## nothing but a scan can change them; the settings list is loaded from the cache file when it is
|
|
19
|
+
## first asked for, which makes it a poor witness against a cache that was just rewritten, and
|
|
20
|
+
## rewriting the cache is the call most in need of an honest answer here.
|
|
21
|
+
##
|
|
22
|
+
## A file the scan skipped is either missing from the tree or recorded with no class name against
|
|
23
|
+
## it, and both come back as a class the editor does not have, which is the whole of the question.
|
|
24
|
+
|
|
25
|
+
var _editor_plugin: EditorPlugin = null
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
29
|
+
_editor_plugin = plugin
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
func global_classes(_args: Dictionary) -> Dictionary:
|
|
33
|
+
if not _editor_plugin:
|
|
34
|
+
return {"ok": false, "error": "Editor plugin unavailable"}
|
|
35
|
+
|
|
36
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
37
|
+
var names: PackedStringArray = []
|
|
38
|
+
_walk(filesystem.get_filesystem(), names)
|
|
39
|
+
names.sort()
|
|
40
|
+
return {
|
|
41
|
+
"ok": true,
|
|
42
|
+
"classes": names,
|
|
43
|
+
"scanning": filesystem.is_scanning(),
|
|
44
|
+
"importing": filesystem.is_importing(),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
func _walk(directory: EditorFileSystemDirectory, into: PackedStringArray) -> void:
|
|
49
|
+
if directory == null:
|
|
50
|
+
return
|
|
51
|
+
for index: int in range(directory.get_file_count()):
|
|
52
|
+
var declared: String = directory.get_file_script_class_name(index)
|
|
53
|
+
if not declared.is_empty():
|
|
54
|
+
into.append(declared)
|
|
55
|
+
for index: int in range(directory.get_subdir_count()):
|
|
56
|
+
_walk(directory.get_subdir(index), into)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bodtvclhs3toc
|
|
@@ -45,6 +45,17 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
45
45
|
old_value = ProjectSettings.get_setting(setting_path)
|
|
46
46
|
|
|
47
47
|
var final_value: Variant = _values.deserialize_value(value)
|
|
48
|
+
var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
|
|
49
|
+
if wanted > TYPE_NIL and final_value != null and typeof(final_value) != wanted:
|
|
50
|
+
if not _converts_faithfully(final_value, wanted):
|
|
51
|
+
return _log.failure(
|
|
52
|
+
(
|
|
53
|
+
"%s is declared as %s, and %s (%s) cannot be written as that without losing something."
|
|
54
|
+
% [setting_path, type_string(wanted), str(final_value), type_string(typeof(final_value))]
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
final_value = type_convert(final_value, wanted)
|
|
58
|
+
|
|
48
59
|
ProjectSettings.set_setting(setting_path, final_value)
|
|
49
60
|
var err: Error = ProjectSettings.save()
|
|
50
61
|
if err != OK:
|
|
@@ -59,6 +70,33 @@ func set_project_setting(params: Dictionary) -> Dictionary:
|
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
|
|
73
|
+
# The type a setting wants, or TYPE_NIL where the engine names none and there is nothing to go on.
|
|
74
|
+
#
|
|
75
|
+
# JSON carries one number type, so every number arrives as a float and an int setting was written
|
|
76
|
+
# to project.godot as "50.0". Every reader casts it back, so the tool, the editor and the running
|
|
77
|
+
# game all answer 50 afterwards and only the committed file says otherwise, where it reads as
|
|
78
|
+
# something having gone wrong.
|
|
79
|
+
#
|
|
80
|
+
# The engine's own property list is what is asked, rather than the value that is there: a setting
|
|
81
|
+
# already written as "50.0" reads back as a float, so the value on hand agrees with the mistake and
|
|
82
|
+
# no project that has made it once could be set right again. The property list still says int.
|
|
83
|
+
# A setting the engine does not declare has only its current value to go on, and a new one has
|
|
84
|
+
# neither, which is the caller's type to choose.
|
|
85
|
+
func _wanted_type(setting_path: String, current: Variant) -> int:
|
|
86
|
+
for info: Dictionary in ProjectSettings.get_property_list():
|
|
87
|
+
if str(info.get("name", "")) == setting_path:
|
|
88
|
+
return int(info.get("type", TYPE_NIL))
|
|
89
|
+
return typeof(current)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# What separates a narrowing from a guess: 50.0 through int and out again is 50.0, so it was an int
|
|
93
|
+
# all along, while 50.5 comes back as 50.0 and "loud" comes back as "0". Those are a caller meaning
|
|
94
|
+
# something the setting cannot hold, and are refused rather than written as whatever the cast
|
|
95
|
+
# happened to produce.
|
|
96
|
+
func _converts_faithfully(value: Variant, to: int) -> bool:
|
|
97
|
+
return type_convert(type_convert(value, to), typeof(value)) == value
|
|
98
|
+
|
|
99
|
+
|
|
62
100
|
# Add an autoload singleton
|
|
63
101
|
func add_autoload(params: Dictionary) -> Dictionary:
|
|
64
102
|
var name: String = str(params.get("name", ""))
|
package/build/index.js
CHANGED
|
@@ -10129,7 +10129,7 @@ import { execFile as execFile3, spawn } from "node:child_process";
|
|
|
10129
10129
|
import { existsSync as existsSync7, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync9, realpathSync as realpathSync2, rmSync as rmSync4 } from "node:fs";
|
|
10130
10130
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10131
10131
|
import { basename, dirname as dirname4, join as join9, normalize as normalize2 } from "node:path";
|
|
10132
|
-
import { setTimeout as
|
|
10132
|
+
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10133
10133
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10134
10134
|
import { promisify as promisify3 } from "node:util";
|
|
10135
10135
|
|
|
@@ -24758,6 +24758,9 @@ import { join as join3 } from "node:path";
|
|
|
24758
24758
|
function declaredClasses(projectPath) {
|
|
24759
24759
|
const declared = new Map;
|
|
24760
24760
|
const visit = (directory, prefix) => {
|
|
24761
|
+
if (existsSync2(join3(directory, ".gdignore"))) {
|
|
24762
|
+
return;
|
|
24763
|
+
}
|
|
24761
24764
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
24762
24765
|
if (entry.name.startsWith(".")) {
|
|
24763
24766
|
continue;
|
|
@@ -24795,6 +24798,12 @@ function staleClassNames(projectPath) {
|
|
|
24795
24798
|
const cached = cachedClasses(projectPath);
|
|
24796
24799
|
return cached === null ? [...declaredClasses(projectPath).keys()] : staleAgainst(cached, projectPath);
|
|
24797
24800
|
}
|
|
24801
|
+
function unseenByEditor(projectPath, editorHolds) {
|
|
24802
|
+
const held = new Set(editorHolds);
|
|
24803
|
+
const declared = declaredClasses(projectPath);
|
|
24804
|
+
const source = cachedClasses(projectPath) ?? declared;
|
|
24805
|
+
return [...source].filter(([name]) => !held.has(name) && declared.has(name)).map(([className, path]) => ({ className, path: declared.get(className) ?? path }));
|
|
24806
|
+
}
|
|
24798
24807
|
|
|
24799
24808
|
// src/dap_client.ts
|
|
24800
24809
|
import { createConnection } from "node:net";
|
|
@@ -27408,6 +27417,7 @@ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as
|
|
|
27408
27417
|
import { createConnection as createConnection3 } from "node:net";
|
|
27409
27418
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
27410
27419
|
import { join as join7, resolve as resolve4 } from "node:path";
|
|
27420
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
27411
27421
|
|
|
27412
27422
|
// src/tool-args.ts
|
|
27413
27423
|
function asParams(value) {
|
|
@@ -27551,6 +27561,24 @@ function announcedIn(directory) {
|
|
|
27551
27561
|
}
|
|
27552
27562
|
return found;
|
|
27553
27563
|
}
|
|
27564
|
+
var LOOK_EVERY_MS = 50;
|
|
27565
|
+
var ANNOUNCE_BUDGET_MS = 5000;
|
|
27566
|
+
async function announcedSince(projectPath, before, waiting = {}) {
|
|
27567
|
+
const wanted = resolve4(projectPath);
|
|
27568
|
+
const directories = waiting.directories ?? runtimeDirectories();
|
|
27569
|
+
const until = Date.now() + Math.max(waiting.budgetMs ?? ANNOUNCE_BUDGET_MS, 0);
|
|
27570
|
+
for (;; ) {
|
|
27571
|
+
const fresh = discoverRuntimes(directories).find((endpoint) => !before.has(endpoint.pid) && resolve4(endpoint.project.path) === wanted);
|
|
27572
|
+
if (fresh !== undefined) {
|
|
27573
|
+
return fresh;
|
|
27574
|
+
}
|
|
27575
|
+
const left = until - Date.now();
|
|
27576
|
+
if (left <= 0 || waiting.giveUp?.() === true) {
|
|
27577
|
+
return null;
|
|
27578
|
+
}
|
|
27579
|
+
await delay2(Math.min(LOOK_EVERY_MS, left));
|
|
27580
|
+
}
|
|
27581
|
+
}
|
|
27554
27582
|
function describe2(endpoint) {
|
|
27555
27583
|
return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
|
|
27556
27584
|
}
|
|
@@ -27683,6 +27711,10 @@ function runtimeRequest(endpoint, command, params, timeoutMs) {
|
|
|
27683
27711
|
import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
27684
27712
|
import { dirname as dirname3, join as join8 } from "node:path";
|
|
27685
27713
|
var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
|
|
27714
|
+
var RUNTIME_AUTOLOAD = {
|
|
27715
|
+
name: "GdharnessRuntime",
|
|
27716
|
+
path: "addons/gdharness_runtime/runtime_autoload.gd"
|
|
27717
|
+
};
|
|
27686
27718
|
var VERSION_MARKER = ".gdharness-version";
|
|
27687
27719
|
function installedAddonVersion(projectPath) {
|
|
27688
27720
|
const marker = join8(projectPath, "addons", ADDONS[0], VERSION_MARKER);
|
|
@@ -27981,7 +28013,7 @@ var TOOL_SPECS = [
|
|
|
27981
28013
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
27982
28014
|
refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
|
|
27983
28015
|
refresh_classes: {
|
|
27984
|
-
summary:
|
|
28016
|
+
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
27985
28017
|
requires: []
|
|
27986
28018
|
}
|
|
27987
28019
|
}
|
|
@@ -28281,7 +28313,7 @@ var TOOL_SPECS = [
|
|
|
28281
28313
|
},
|
|
28282
28314
|
{
|
|
28283
28315
|
name: "editor_run",
|
|
28284
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way.",
|
|
28316
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice.",
|
|
28285
28317
|
parameters: {
|
|
28286
28318
|
projectPath: PROJECT_PATH,
|
|
28287
28319
|
scene: { type: "string", description: "A scene to run instead of the main scene." },
|
|
@@ -28326,7 +28358,7 @@ var TOOL_SPECS = [
|
|
|
28326
28358
|
},
|
|
28327
28359
|
{
|
|
28328
28360
|
name: "editor_rescan",
|
|
28329
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file
|
|
28361
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is a change to the declaring script, or editor_launch restart; project_import refresh_classes rewrites the cache and does not touch what the editor is holding. Needs the editor connected.",
|
|
28330
28362
|
parameters: {
|
|
28331
28363
|
projectPath: PROJECT_PATH,
|
|
28332
28364
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -28871,6 +28903,10 @@ function realPathOr(path) {
|
|
|
28871
28903
|
return path;
|
|
28872
28904
|
}
|
|
28873
28905
|
}
|
|
28906
|
+
function samePath(one, other) {
|
|
28907
|
+
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
28908
|
+
return settled(one) === settled(other);
|
|
28909
|
+
}
|
|
28874
28910
|
var RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
28875
28911
|
function aboveTheRunner(entry) {
|
|
28876
28912
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
@@ -29302,7 +29338,8 @@ class GodotServer {
|
|
|
29302
29338
|
async dispatch(tool, op, args) {
|
|
29303
29339
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
29304
29340
|
if (headless !== undefined) {
|
|
29305
|
-
|
|
29341
|
+
const answered = await this.headless(headless, args);
|
|
29342
|
+
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
29306
29343
|
}
|
|
29307
29344
|
const { op: _op, ...arguments_ } = args;
|
|
29308
29345
|
const bridge = async (command, extra = {}) => {
|
|
@@ -30107,7 +30144,7 @@ class GodotServer {
|
|
|
30107
30144
|
if (Date.now() >= deadline) {
|
|
30108
30145
|
return false;
|
|
30109
30146
|
}
|
|
30110
|
-
await
|
|
30147
|
+
await delay3(250);
|
|
30111
30148
|
}
|
|
30112
30149
|
}
|
|
30113
30150
|
async handleLaunchEditor(args) {
|
|
@@ -30203,12 +30240,13 @@ class GodotServer {
|
|
|
30203
30240
|
this.logDebug("Ending the running game before starting another");
|
|
30204
30241
|
await this.endActiveGame();
|
|
30205
30242
|
}
|
|
30243
|
+
const alreadyPlaying = new Set(discoverRuntimes().map((endpoint) => endpoint.pid));
|
|
30206
30244
|
const headless = resolveHeadless(args["headless"], {
|
|
30207
30245
|
platform: process.platform,
|
|
30208
30246
|
variables: process.env
|
|
30209
30247
|
});
|
|
30210
30248
|
if (this.godotBridge.isConnected() && (!headless || editorPlaysHeadless(project.value.file))) {
|
|
30211
|
-
return await this.playThroughEditor(sceneArgument, refreshed.value);
|
|
30249
|
+
return await this.playThroughEditor(sceneArgument, refreshed.value, project.value.path, alreadyPlaying);
|
|
30212
30250
|
}
|
|
30213
30251
|
const cmdArgs = runArguments({
|
|
30214
30252
|
projectPath: project.value.path,
|
|
@@ -30224,10 +30262,31 @@ class GodotServer {
|
|
|
30224
30262
|
pid: started.process.pid ?? null,
|
|
30225
30263
|
arguments: cmdArgs,
|
|
30226
30264
|
refreshedClasses: refreshed.value,
|
|
30265
|
+
runtime: await this.runtimeUp(project.value.path, alreadyPlaying),
|
|
30227
30266
|
message: "Use editor_output for what it prints and editor_run stop to end it."
|
|
30228
30267
|
});
|
|
30229
30268
|
}
|
|
30230
|
-
async
|
|
30269
|
+
async runtimeUp(projectPath, before) {
|
|
30270
|
+
if (!existsSync7(join9(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
30271
|
+
return {
|
|
30272
|
+
listening: false,
|
|
30273
|
+
note: "this project has no gdharness_runtime addon, so the runtime_* tools have nothing to talk to"
|
|
30274
|
+
};
|
|
30275
|
+
}
|
|
30276
|
+
const endpoint = await announcedSince(projectPath, before, {
|
|
30277
|
+
giveUp: () => this.dapClient?.isStopped() === true
|
|
30278
|
+
});
|
|
30279
|
+
if (endpoint === null) {
|
|
30280
|
+
const held = this.dapClient?.whereItStopped() ?? null;
|
|
30281
|
+
return {
|
|
30282
|
+
listening: false,
|
|
30283
|
+
note: held === null ? `nothing announced itself within ${ANNOUNCE_BUDGET_MS}ms: the game may be slow to boot, or its runtime is not coming up` : "the game stopped before its runtime came up, so the runtime_* tools have nothing to talk to yet: debug_control continue lets it carry on",
|
|
30284
|
+
heldAt: held
|
|
30285
|
+
};
|
|
30286
|
+
}
|
|
30287
|
+
return { listening: true, pid: endpoint.pid, port: endpoint.port };
|
|
30288
|
+
}
|
|
30289
|
+
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying) {
|
|
30231
30290
|
const log = new GameLog;
|
|
30232
30291
|
try {
|
|
30233
30292
|
await this.dap().connect();
|
|
@@ -30256,6 +30315,7 @@ class GodotServer {
|
|
|
30256
30315
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
30257
30316
|
refreshedClasses,
|
|
30258
30317
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
30318
|
+
runtime: await this.runtimeUp(projectPath, alreadyPlaying),
|
|
30259
30319
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it."
|
|
30260
30320
|
});
|
|
30261
30321
|
}
|
|
@@ -30410,13 +30470,59 @@ class GodotServer {
|
|
|
30410
30470
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
30411
30471
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
|
|
30412
30472
|
}
|
|
30473
|
+
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
30474
|
+
const unseen = checked.unseen;
|
|
30413
30475
|
return this.jsonTextResponse({
|
|
30414
|
-
ok: !busy,
|
|
30476
|
+
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
30415
30477
|
stillWorking: busy,
|
|
30416
30478
|
waitedMs: Date.now() - started,
|
|
30417
|
-
|
|
30479
|
+
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
30480
|
+
classesUnchecked: checked.unchecked,
|
|
30481
|
+
note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : unseen.length > 0 ? "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. Change the declaring script and rescan, or restart the editor with editor_launch restart." : undefined
|
|
30418
30482
|
});
|
|
30419
30483
|
}
|
|
30484
|
+
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
30485
|
+
const first = answered.content[0];
|
|
30486
|
+
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
30487
|
+
return answered;
|
|
30488
|
+
}
|
|
30489
|
+
const checked = await this.classesTheEditorCannotSee(args);
|
|
30490
|
+
if (checked.unseen.length === 0 && checked.unchecked === undefined) {
|
|
30491
|
+
return answered;
|
|
30492
|
+
}
|
|
30493
|
+
return this.jsonTextResponse({
|
|
30494
|
+
...asParams(JSON.parse(first.text)),
|
|
30495
|
+
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
30496
|
+
classesUnchecked: checked.unchecked,
|
|
30497
|
+
note: checked.unseen.length > 0 ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. Change the declaring script and editor_rescan, or restart the editor with editor_launch restart." : undefined
|
|
30498
|
+
});
|
|
30499
|
+
}
|
|
30500
|
+
async classesTheEditorCannotSee(args) {
|
|
30501
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
30502
|
+
if (projectPath === "") {
|
|
30503
|
+
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
30504
|
+
}
|
|
30505
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
30506
|
+
if (open === undefined || !samePath(open, projectPath)) {
|
|
30507
|
+
return {
|
|
30508
|
+
unseen: [],
|
|
30509
|
+
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
30510
|
+
};
|
|
30511
|
+
}
|
|
30512
|
+
let held;
|
|
30513
|
+
try {
|
|
30514
|
+
held = asParams(await this.godotBridge.invokeTool("global_classes", args));
|
|
30515
|
+
} catch (error) {
|
|
30516
|
+
return {
|
|
30517
|
+
unseen: [],
|
|
30518
|
+
unchecked: `the editor would not say what classes it is holding: ${errorMessage(error)}`
|
|
30519
|
+
};
|
|
30520
|
+
}
|
|
30521
|
+
if (!Array.isArray(held["classes"])) {
|
|
30522
|
+
return { unseen: [], unchecked: "the editor answered without a class list in it" };
|
|
30523
|
+
}
|
|
30524
|
+
return { unseen: unseenByEditor(projectPath, held["classes"].map(String)) };
|
|
30525
|
+
}
|
|
30420
30526
|
async handleViaBridge(toolName, args) {
|
|
30421
30527
|
if (!this.godotBridge.isConnected()) {
|
|
30422
30528
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"mcpName": "io.github.Aureliolo/gdharness",
|
|
5
5
|
"description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
|
|
6
6
|
"type": "module",
|