gdharness 0.9.7 → 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 +71 -5
- 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 +65 -5
- 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 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." }
|
|
@@ -36092,6 +36101,10 @@ function realPathOr(path) {
|
|
|
36092
36101
|
return path;
|
|
36093
36102
|
}
|
|
36094
36103
|
}
|
|
36104
|
+
function samePath(one, other) {
|
|
36105
|
+
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
36106
|
+
return settled(one) === settled(other);
|
|
36107
|
+
}
|
|
36095
36108
|
function aboveTheRunner(entry) {
|
|
36096
36109
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
36097
36110
|
if (runner < 0) {
|
|
@@ -36522,7 +36535,8 @@ class GodotServer {
|
|
|
36522
36535
|
async dispatch(tool, op, args) {
|
|
36523
36536
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
36524
36537
|
if (headless !== undefined) {
|
|
36525
|
-
|
|
36538
|
+
const answered = await this.headless(headless, args);
|
|
36539
|
+
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
36526
36540
|
}
|
|
36527
36541
|
const { op: _op, ...arguments_ } = args;
|
|
36528
36542
|
const bridge = async (command, extra = {}) => {
|
|
@@ -37630,13 +37644,59 @@ class GodotServer {
|
|
|
37630
37644
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
37631
37645
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
|
|
37632
37646
|
}
|
|
37647
|
+
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
37648
|
+
const unseen = checked.unseen;
|
|
37633
37649
|
return this.jsonTextResponse({
|
|
37634
|
-
ok: !busy,
|
|
37650
|
+
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
37635
37651
|
stillWorking: busy,
|
|
37636
37652
|
waitedMs: Date.now() - started,
|
|
37637
|
-
|
|
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
|
|
37638
37656
|
});
|
|
37639
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
|
+
}
|
|
37640
37700
|
async handleViaBridge(toolName, args) {
|
|
37641
37701
|
if (!this.godotBridge.isConnected()) {
|
|
37642
37702
|
return this.createErrorResponse("Editor not connected: no Godot editor with the gdharness editor addon has reached this server.", [
|
|
@@ -39226,6 +39286,12 @@ formatting. \`script_diagnostics\` and \`script_info\` come from the editor's ow
|
|
|
39226
39286
|
\`editor_classes\` need nothing open: they run a short headless engine and are gone before the
|
|
39227
39287
|
answer is printed.
|
|
39228
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
|
+
|
|
39229
39295
|
## Refusals are useful
|
|
39230
39296
|
|
|
39231
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";
|
|
@@ -27981,7 +27990,7 @@ var TOOL_SPECS = [
|
|
|
27981
27990
|
uid: { summary: "the UID of one file", requires: ["resourcePath"] },
|
|
27982
27991
|
refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
|
|
27983
27992
|
refresh_classes: {
|
|
27984
|
-
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',
|
|
27985
27994
|
requires: []
|
|
27986
27995
|
}
|
|
27987
27996
|
}
|
|
@@ -28326,7 +28335,7 @@ var TOOL_SPECS = [
|
|
|
28326
28335
|
},
|
|
28327
28336
|
{
|
|
28328
28337
|
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
|
|
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.",
|
|
28330
28339
|
parameters: {
|
|
28331
28340
|
projectPath: PROJECT_PATH,
|
|
28332
28341
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -28871,6 +28880,10 @@ function realPathOr(path) {
|
|
|
28871
28880
|
return path;
|
|
28872
28881
|
}
|
|
28873
28882
|
}
|
|
28883
|
+
function samePath(one, other) {
|
|
28884
|
+
const settled = (path) => realPathOr(path).replaceAll("\\", "/").replace(/\/+$/, "").toLowerCase();
|
|
28885
|
+
return settled(one) === settled(other);
|
|
28886
|
+
}
|
|
28874
28887
|
var RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
28875
28888
|
function aboveTheRunner(entry) {
|
|
28876
28889
|
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
@@ -29302,7 +29315,8 @@ class GodotServer {
|
|
|
29302
29315
|
async dispatch(tool, op, args) {
|
|
29303
29316
|
const headless = HEADLESS_OPERATIONS[tool]?.[op];
|
|
29304
29317
|
if (headless !== undefined) {
|
|
29305
|
-
|
|
29318
|
+
const answered = await this.headless(headless, args);
|
|
29319
|
+
return headless === "refresh_class_cache" && this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
29306
29320
|
}
|
|
29307
29321
|
const { op: _op, ...arguments_ } = args;
|
|
29308
29322
|
const bridge = async (command, extra = {}) => {
|
|
@@ -30410,13 +30424,59 @@ class GodotServer {
|
|
|
30410
30424
|
const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
|
|
30411
30425
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]);
|
|
30412
30426
|
}
|
|
30427
|
+
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
30428
|
+
const unseen = checked.unseen;
|
|
30413
30429
|
return this.jsonTextResponse({
|
|
30414
|
-
ok: !busy,
|
|
30430
|
+
ok: !busy && unseen.length === 0 && checked.unchecked === undefined,
|
|
30415
30431
|
stillWorking: busy,
|
|
30416
30432
|
waitedMs: Date.now() - started,
|
|
30417
|
-
|
|
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
|
|
30436
|
+
});
|
|
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
|
|
30418
30452
|
});
|
|
30419
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
|
+
}
|
|
30420
30480
|
async handleViaBridge(toolName, args) {
|
|
30421
30481
|
if (!this.godotBridge.isConnected()) {
|
|
30422
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",
|