gdharness 0.9.6 → 0.10.0
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 +80 -6
- 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 +74 -6
- 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
|
}
|
|
@@ -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, and
|
|
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." }
|
|
@@ -35264,12 +35273,14 @@ class GodotLSPClient {
|
|
|
35264
35273
|
if (!this.socket) {
|
|
35265
35274
|
this.connected = false;
|
|
35266
35275
|
this.initialized = false;
|
|
35276
|
+
this.forgetOpenDocuments();
|
|
35267
35277
|
return;
|
|
35268
35278
|
}
|
|
35269
35279
|
const socketToClose = this.socket;
|
|
35270
35280
|
this.socket = null;
|
|
35271
35281
|
this.connected = false;
|
|
35272
35282
|
this.initialized = false;
|
|
35283
|
+
this.forgetOpenDocuments();
|
|
35273
35284
|
await new Promise((resolveClose) => {
|
|
35274
35285
|
socketToClose.once("close", () => {
|
|
35275
35286
|
resolveClose();
|
|
@@ -35401,6 +35412,7 @@ class GodotLSPClient {
|
|
|
35401
35412
|
this.connected = false;
|
|
35402
35413
|
this.initialized = false;
|
|
35403
35414
|
this.socket = null;
|
|
35415
|
+
this.forgetOpenDocuments();
|
|
35404
35416
|
const failure = new Error(`Godot LSP connection error: ${error.message}`);
|
|
35405
35417
|
this.rejectAllPending(failure);
|
|
35406
35418
|
this.rejectAllDiagnosticsWaiters(failure);
|
|
@@ -35409,10 +35421,14 @@ class GodotLSPClient {
|
|
|
35409
35421
|
this.connected = false;
|
|
35410
35422
|
this.initialized = false;
|
|
35411
35423
|
this.socket = null;
|
|
35424
|
+
this.forgetOpenDocuments();
|
|
35412
35425
|
const closed = new Error("Godot LSP socket closed");
|
|
35413
35426
|
this.rejectAllPending(closed);
|
|
35414
35427
|
this.rejectAllDiagnosticsWaiters(closed);
|
|
35415
35428
|
}
|
|
35429
|
+
forgetOpenDocuments() {
|
|
35430
|
+
this.documentVersions.clear();
|
|
35431
|
+
}
|
|
35416
35432
|
rejectAllPending(error) {
|
|
35417
35433
|
this.pendingRequests.forEach((pending, id) => {
|
|
35418
35434
|
clearTimeout(pending.timer);
|
|
@@ -36085,6 +36101,10 @@ function realPathOr(path) {
|
|
|
36085
36101
|
return path;
|
|
36086
36102
|
}
|
|
36087
36103
|
}
|
|
36104
|
+
function samePath(one, other) {
|
|
36105
|
+
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
36106
|
+
return settled(one) === settled(other);
|
|
36107
|
+
}
|
|
36088
36108
|
function aboveTheRunner(entry) {
|
|
36089
36109
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
36090
36110
|
if (runner < 0) {
|
|
@@ -36457,9 +36477,10 @@ class GodotServer {
|
|
|
36457
36477
|
const known = new Set([...Object.keys(spec.parameters), ...spec.operations ? ["op"] : []]);
|
|
36458
36478
|
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
36459
36479
|
if (unknown.length > 0) {
|
|
36480
|
+
const takes = known.size === 0 ? "It takes no arguments." : `It takes: ${[...known].join(", ")}.`;
|
|
36460
36481
|
return {
|
|
36461
36482
|
ok: false,
|
|
36462
|
-
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}.
|
|
36483
|
+
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. ${takes}`)
|
|
36463
36484
|
};
|
|
36464
36485
|
}
|
|
36465
36486
|
const wrong = wrongTypes(spec, args);
|
|
@@ -36514,7 +36535,8 @@ class GodotServer {
|
|
|
36514
36535
|
async dispatch(tool, op, args) {
|
|
36515
36536
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
36516
36537
|
if (headless !== undefined) {
|
|
36517
|
-
|
|
36538
|
+
const answered = await this.headless(headless, args);
|
|
36539
|
+
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
36518
36540
|
}
|
|
36519
36541
|
const { op: _op, ...arguments_ } = args;
|
|
36520
36542
|
const bridge = async (command, extra = {}) => {
|
|
@@ -37622,13 +37644,59 @@ class GodotServer {
|
|
|
37622
37644
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
37623
37645
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
|
|
37624
37646
|
}
|
|
37647
|
+
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
37648
|
+
const unseen = checked.unseen;
|
|
37625
37649
|
return this.jsonTextResponse({
|
|
37626
|
-
ok: !busy,
|
|
37650
|
+
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
37627
37651
|
stillWorking: busy,
|
|
37628
37652
|
waitedMs: Date.now() - started,
|
|
37629
|
-
|
|
37653
|
+
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
37654
|
+
classesUnchecked: checked.unchecked,
|
|
37655
|
+
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
|
|
37630
37656
|
});
|
|
37631
37657
|
}
|
|
37658
|
+
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
37659
|
+
const first = answered.content[0];
|
|
37660
|
+
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
37661
|
+
return answered;
|
|
37662
|
+
}
|
|
37663
|
+
const checked = await this.classesTheEditorCannotSee(args);
|
|
37664
|
+
if (checked.unseen.length === 0 && checked.unchecked === undefined) {
|
|
37665
|
+
return answered;
|
|
37666
|
+
}
|
|
37667
|
+
return this.jsonTextResponse({
|
|
37668
|
+
...asParams(JSON.parse(first.text)),
|
|
37669
|
+
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
37670
|
+
classesUnchecked: checked.unchecked,
|
|
37671
|
+
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
|
|
37672
|
+
});
|
|
37673
|
+
}
|
|
37674
|
+
async classesTheEditorCannotSee(args) {
|
|
37675
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
37676
|
+
if (projectPath === "") {
|
|
37677
|
+
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
37678
|
+
}
|
|
37679
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
37680
|
+
if (open === undefined || !samePath(open, projectPath)) {
|
|
37681
|
+
return {
|
|
37682
|
+
unseen: [],
|
|
37683
|
+
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
37684
|
+
};
|
|
37685
|
+
}
|
|
37686
|
+
let held;
|
|
37687
|
+
try {
|
|
37688
|
+
held = asParams(await this.godotBridge.invokeTool("global_classes", args));
|
|
37689
|
+
} catch (error) {
|
|
37690
|
+
return {
|
|
37691
|
+
unseen: [],
|
|
37692
|
+
unchecked: `the editor would not say what classes it is holding: ${errorMessage(error)}`
|
|
37693
|
+
};
|
|
37694
|
+
}
|
|
37695
|
+
if (!Array.isArray(held["classes"])) {
|
|
37696
|
+
return { unseen: [], unchecked: "the editor answered without a class list in it" };
|
|
37697
|
+
}
|
|
37698
|
+
return { unseen: unseenByEditor(projectPath, held["classes"].map(String)) };
|
|
37699
|
+
}
|
|
37632
37700
|
async handleViaBridge(toolName, args) {
|
|
37633
37701
|
if (!this.godotBridge.isConnected()) {
|
|
37634
37702
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
@@ -39218,6 +39286,12 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
|
|
|
39218
39286
|
\`editor_classes\` need nothing open: they run a short headless engine and are gone before the
|
|
39219
39287
|
answer is printed.
|
|
39220
39288
|
|
|
39289
|
+
A wall of "Could not find type" on the project's own classes means the editor is not holding
|
|
39290
|
+
them. \`editor_rescan\` asks it to scan and names any class it still cannot resolve under
|
|
39291
|
+
\`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
|
|
39292
|
+
declaration and the cache can both be right while the editor stays blind to it. Change the
|
|
39293
|
+
declaring script, or restart the editor.
|
|
39294
|
+
|
|
39221
39295
|
## Refusals are useful
|
|
39222
39296
|
|
|
39223
39297
|
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
|
@@ -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";
|
|
@@ -26694,12 +26703,14 @@ class GodotLSPClient {
|
|
|
26694
26703
|
if (!this.socket) {
|
|
26695
26704
|
this.connected = false;
|
|
26696
26705
|
this.initialized = false;
|
|
26706
|
+
this.forgetOpenDocuments();
|
|
26697
26707
|
return;
|
|
26698
26708
|
}
|
|
26699
26709
|
const socketToClose = this.socket;
|
|
26700
26710
|
this.socket = null;
|
|
26701
26711
|
this.connected = false;
|
|
26702
26712
|
this.initialized = false;
|
|
26713
|
+
this.forgetOpenDocuments();
|
|
26703
26714
|
await new Promise((resolveClose) => {
|
|
26704
26715
|
socketToClose.once("close", () => {
|
|
26705
26716
|
resolveClose();
|
|
@@ -26831,6 +26842,7 @@ class GodotLSPClient {
|
|
|
26831
26842
|
this.connected = false;
|
|
26832
26843
|
this.initialized = false;
|
|
26833
26844
|
this.socket = null;
|
|
26845
|
+
this.forgetOpenDocuments();
|
|
26834
26846
|
const failure = new Error(`Godot LSP connection error: ${error.message}`);
|
|
26835
26847
|
this.rejectAllPending(failure);
|
|
26836
26848
|
this.rejectAllDiagnosticsWaiters(failure);
|
|
@@ -26839,10 +26851,14 @@ class GodotLSPClient {
|
|
|
26839
26851
|
this.connected = false;
|
|
26840
26852
|
this.initialized = false;
|
|
26841
26853
|
this.socket = null;
|
|
26854
|
+
this.forgetOpenDocuments();
|
|
26842
26855
|
const closed = new Error("Godot LSP socket closed");
|
|
26843
26856
|
this.rejectAllPending(closed);
|
|
26844
26857
|
this.rejectAllDiagnosticsWaiters(closed);
|
|
26845
26858
|
}
|
|
26859
|
+
forgetOpenDocuments() {
|
|
26860
|
+
this.documentVersions.clear();
|
|
26861
|
+
}
|
|
26846
26862
|
rejectAllPending(error) {
|
|
26847
26863
|
this.pendingRequests.forEach((pending, id) => {
|
|
26848
26864
|
clearTimeout(pending.timer);
|
|
@@ -27974,7 +27990,7 @@ var TOOL_SPECS = [
|
|
|
27974
27990
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
27975
27991
|
refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
|
|
27976
27992
|
refresh_classes: {
|
|
27977
|
-
summary:
|
|
27993
|
+
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',
|
|
27978
27994
|
requires: []
|
|
27979
27995
|
}
|
|
27980
27996
|
}
|
|
@@ -28319,7 +28335,7 @@ var TOOL_SPECS = [
|
|
|
28319
28335
|
},
|
|
28320
28336
|
{
|
|
28321
28337
|
name: "editor_rescan",
|
|
28322
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it, and
|
|
28338
|
+
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.",
|
|
28323
28339
|
parameters: {
|
|
28324
28340
|
projectPath: PROJECT_PATH,
|
|
28325
28341
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -28864,6 +28880,10 @@ function realPathOr(path) {
|
|
|
28864
28880
|
return path;
|
|
28865
28881
|
}
|
|
28866
28882
|
}
|
|
28883
|
+
function samePath(one, other) {
|
|
28884
|
+
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
28885
|
+
return settled(one) === settled(other);
|
|
28886
|
+
}
|
|
28867
28887
|
var RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
28868
28888
|
function aboveTheRunner(entry) {
|
|
28869
28889
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
@@ -29237,9 +29257,10 @@ class GodotServer {
|
|
|
29237
29257
|
const known = new Set([...Object.keys(spec.parameters), ...spec.operations ? ["op"] : []]);
|
|
29238
29258
|
const unknown = Object.keys(args).filter((key) => !known.has(key));
|
|
29239
29259
|
if (unknown.length > 0) {
|
|
29260
|
+
const takes = known.size === 0 ? "It takes no arguments." : `It takes: ${[...known].join(", ")}.`;
|
|
29240
29261
|
return {
|
|
29241
29262
|
ok: false,
|
|
29242
|
-
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}.
|
|
29263
|
+
response: this.createErrorResponse(`${spec.name} does not take ${unknown.join(", ")}. ${takes}`)
|
|
29243
29264
|
};
|
|
29244
29265
|
}
|
|
29245
29266
|
const wrong = wrongTypes(spec, args);
|
|
@@ -29294,7 +29315,8 @@ class GodotServer {
|
|
|
29294
29315
|
async dispatch(tool, op, args) {
|
|
29295
29316
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
29296
29317
|
if (headless !== undefined) {
|
|
29297
|
-
|
|
29318
|
+
const answered = await this.headless(headless, args);
|
|
29319
|
+
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
29298
29320
|
}
|
|
29299
29321
|
const { op: _op, ...arguments_ } = args;
|
|
29300
29322
|
const bridge = async (command, extra = {}) => {
|
|
@@ -30402,13 +30424,59 @@ class GodotServer {
|
|
|
30402
30424
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
30403
30425
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
|
|
30404
30426
|
}
|
|
30427
|
+
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
30428
|
+
const unseen = checked.unseen;
|
|
30405
30429
|
return this.jsonTextResponse({
|
|
30406
|
-
ok: !busy,
|
|
30430
|
+
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
30407
30431
|
stillWorking: busy,
|
|
30408
30432
|
waitedMs: Date.now() - started,
|
|
30409
|
-
|
|
30433
|
+
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
30434
|
+
classesUnchecked: checked.unchecked,
|
|
30435
|
+
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
|
|
30410
30436
|
});
|
|
30411
30437
|
}
|
|
30438
|
+
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
30439
|
+
const first = answered.content[0];
|
|
30440
|
+
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
30441
|
+
return answered;
|
|
30442
|
+
}
|
|
30443
|
+
const checked = await this.classesTheEditorCannotSee(args);
|
|
30444
|
+
if (checked.unseen.length === 0 && checked.unchecked === undefined) {
|
|
30445
|
+
return answered;
|
|
30446
|
+
}
|
|
30447
|
+
return this.jsonTextResponse({
|
|
30448
|
+
...asParams(JSON.parse(first.text)),
|
|
30449
|
+
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
30450
|
+
classesUnchecked: checked.unchecked,
|
|
30451
|
+
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
|
|
30452
|
+
});
|
|
30453
|
+
}
|
|
30454
|
+
async classesTheEditorCannotSee(args) {
|
|
30455
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
30456
|
+
if (projectPath === "") {
|
|
30457
|
+
return { unseen: [], unchecked: "no projectPath, so there was nothing to read the cache from" };
|
|
30458
|
+
}
|
|
30459
|
+
const open = this.godotBridge.getStatus().projectPath;
|
|
30460
|
+
if (open === undefined || !samePath(open, projectPath)) {
|
|
30461
|
+
return {
|
|
30462
|
+
unseen: [],
|
|
30463
|
+
unchecked: `the connected editor is open on ${open ?? "a project it did not name"}, not this one`
|
|
30464
|
+
};
|
|
30465
|
+
}
|
|
30466
|
+
let held;
|
|
30467
|
+
try {
|
|
30468
|
+
held = asParams(await this.godotBridge.invokeTool("global_classes", args));
|
|
30469
|
+
} catch (error) {
|
|
30470
|
+
return {
|
|
30471
|
+
unseen: [],
|
|
30472
|
+
unchecked: `the editor would not say what classes it is holding: ${errorMessage(error)}`
|
|
30473
|
+
};
|
|
30474
|
+
}
|
|
30475
|
+
if (!Array.isArray(held["classes"])) {
|
|
30476
|
+
return { unseen: [], unchecked: "the editor answered without a class list in it" };
|
|
30477
|
+
}
|
|
30478
|
+
return { unseen: unseenByEditor(projectPath, held["classes"].map(String)) };
|
|
30479
|
+
}
|
|
30412
30480
|
async handleViaBridge(toolName, args) {
|
|
30413
30481
|
if (!this.godotBridge.isConnected()) {
|
|
30414
30482
|
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.0",
|
|
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",
|