gdharness 0.13.21 → 0.13.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js
CHANGED
|
@@ -15964,7 +15964,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
15964
15964
|
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 measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
|
|
15965
15965
|
parameters: {
|
|
15966
15966
|
projectPath: PROJECT_PATH,
|
|
15967
|
-
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
15967
|
+
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
15968
|
+
reloadScript: {
|
|
15969
|
+
type: "string",
|
|
15970
|
+
description: "A script to recompile in the editor after the scan, for the fault a scan cannot reach: a script the editor has loaded keeps the copy it built, and that copy is refreshed when one of its dependencies changes rather than when it changes itself, so a method added to a loaded class reads as missing at every caller. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
15971
|
+
}
|
|
15968
15972
|
},
|
|
15969
15973
|
requires: ["projectPath"]
|
|
15970
15974
|
},
|
|
@@ -38990,6 +38994,17 @@ class GodotServer {
|
|
|
38990
38994
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
38991
38995
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
38992
38996
|
const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
38997
|
+
const reloading = readNonEmptyString(args, "reloadScript");
|
|
38998
|
+
let reloaded = {};
|
|
38999
|
+
if (reloading !== undefined) {
|
|
39000
|
+
try {
|
|
39001
|
+
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
39002
|
+
const methods = readArray(answer, "methods");
|
|
39003
|
+
reloaded = methods ? { methods: methods.map(String) } : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
39004
|
+
} catch (error) {
|
|
39005
|
+
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
39006
|
+
}
|
|
39007
|
+
}
|
|
38993
39008
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
38994
39009
|
const unseen = checked.unseen;
|
|
38995
39010
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
@@ -38997,6 +39012,8 @@ class GodotServer {
|
|
|
38997
39012
|
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
38998
39013
|
stillWorking: busy,
|
|
38999
39014
|
waitedMs: Date.now() - started,
|
|
39015
|
+
reloadedMethods: reloaded.methods,
|
|
39016
|
+
reloadProblem: reloaded.problem,
|
|
39000
39017
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
39001
39018
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
39002
39019
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
@@ -80,6 +80,7 @@ func _init_tools() -> void:
|
|
|
80
80
|
"list_connections": [_scene_tools, "list_connections"],
|
|
81
81
|
"rescan_filesystem": [_scene_tools, "rescan_filesystem"],
|
|
82
82
|
"global_classes": [_class_tools, "global_classes"],
|
|
83
|
+
"reload_script": [_class_tools, "reload_script"],
|
|
83
84
|
# Settings, for a caller that asked the editor rather than the file on disk
|
|
84
85
|
"get_project_setting": [_settings_tools, "get_project_setting"],
|
|
85
86
|
# Resource tools
|
|
@@ -29,6 +29,59 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
29
29
|
_editor_plugin = plugin
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
## Recompiles a script the editor is already holding, in place.
|
|
33
|
+
##
|
|
34
|
+
## The fault this exists for: a script the editor has loaded keeps the copy it compiled, and that
|
|
35
|
+
## copy is refreshed when one of its own dependencies changes rather than when it changes itself.
|
|
36
|
+
## So a method added to a loaded class is reported missing at every caller until something
|
|
37
|
+
## recompiles it, and a filesystem scan does not, because the scan updates what the editor knows
|
|
38
|
+
## about files rather than what it has already built from them.
|
|
39
|
+
##
|
|
40
|
+
## `reload` is the only call that reaches the built copy. It recompiles from source into the same
|
|
41
|
+
## object, so everything holding a reference sees the new version rather than a second one, which
|
|
42
|
+
## is the whole point: the holders are why the stale copy survived a scan.
|
|
43
|
+
##
|
|
44
|
+
## `keep_state` is true so instances that already exist keep their properties. A caller asking for
|
|
45
|
+
## this is mid-edit with a scene open, and dropping the state of every node using the script would
|
|
46
|
+
## be a larger surprise than the wrong diagnostic it is here to clear.
|
|
47
|
+
##
|
|
48
|
+
## The list of methods afterwards is the evidence rather than an `ok` beside it. A reload that
|
|
49
|
+
## returned OK and compiled nothing is exactly the failure worth catching, and the members it now
|
|
50
|
+
## has are the only thing that can tell those apart.
|
|
51
|
+
func reload_script(args: Dictionary) -> Dictionary:
|
|
52
|
+
var path: String = str(args.get("scriptPath", ""))
|
|
53
|
+
if path.is_empty():
|
|
54
|
+
return {"ok": false, "error": "scriptPath is required"}
|
|
55
|
+
if not ResourceLoader.exists(path):
|
|
56
|
+
return {"ok": false, "error": "No script at " + path}
|
|
57
|
+
|
|
58
|
+
var loaded: Resource = load(path)
|
|
59
|
+
var script: GDScript = loaded as GDScript
|
|
60
|
+
if script == null:
|
|
61
|
+
return {"ok": false, "error": path + " is not a GDScript"}
|
|
62
|
+
|
|
63
|
+
# The source is read off disk and put back before reloading, which is the whole of the fix and
|
|
64
|
+
# is not obvious. `reload` recompiles from the object's own `source_code`, not from the file, so
|
|
65
|
+
# a script the editor loaded before the change recompiles the text it was holding and comes back
|
|
66
|
+
# with exactly the members it already had. Measured: reloading without this answers OK and the
|
|
67
|
+
# method added since is still absent from the method list.
|
|
68
|
+
var reader: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
69
|
+
if reader == null:
|
|
70
|
+
return {"ok": false, "error": "Could not read " + path + ": " + str(FileAccess.get_open_error())}
|
|
71
|
+
script.source_code = reader.get_as_text()
|
|
72
|
+
reader.close()
|
|
73
|
+
|
|
74
|
+
var failed: Error = script.reload(true)
|
|
75
|
+
if failed != OK:
|
|
76
|
+
return {"ok": false, "error": "Reloading " + path + " answered error " + str(failed)}
|
|
77
|
+
|
|
78
|
+
var methods: Array[String] = []
|
|
79
|
+
for entry: Dictionary in script.get_script_method_list():
|
|
80
|
+
methods.append(str(entry.get("name", "")))
|
|
81
|
+
methods.sort()
|
|
82
|
+
return {"ok": true, "script": path, "methods": methods}
|
|
83
|
+
|
|
84
|
+
|
|
32
85
|
func global_classes(_args: Dictionary) -> Dictionary:
|
|
33
86
|
if not _editor_plugin:
|
|
34
87
|
return {"ok": false, "error": "Editor plugin unavailable"}
|
package/build/index.js
CHANGED
|
@@ -29081,7 +29081,11 @@ var TOOL_SPECS = [
|
|
|
29081
29081
|
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 measured downstream is a change to the declaring script, saved, and then a rescan: the change on its own left every affected file reporting the same errors, and the rescan after it cleared them. Whether a rescan alone would have done it there is not known, because the rescans in that report may have answered before the scan ran, which is the timing this tool no longer has. editor_launch restart also does it, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. Needs the editor connected.",
|
|
29082
29082
|
parameters: {
|
|
29083
29083
|
projectPath: PROJECT_PATH,
|
|
29084
|
-
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
29084
|
+
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
29085
|
+
reloadScript: {
|
|
29086
|
+
type: "string",
|
|
29087
|
+
description: "A script to recompile in the editor after the scan, for the fault a scan cannot reach: a script the editor has loaded keeps the copy it built, and that copy is refreshed when one of its dependencies changes rather than when it changes itself, so a method added to a loaded class reads as missing at every caller. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
29088
|
+
}
|
|
29085
29089
|
},
|
|
29086
29090
|
requires: ["projectPath"]
|
|
29087
29091
|
},
|
|
@@ -31742,6 +31746,17 @@ class GodotServer {
|
|
|
31742
31746
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
31743
31747
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
31744
31748
|
const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
31749
|
+
const reloading = readNonEmptyString(args, "reloadScript");
|
|
31750
|
+
let reloaded = {};
|
|
31751
|
+
if (reloading !== undefined) {
|
|
31752
|
+
try {
|
|
31753
|
+
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { ...args, scriptPath: reloading }));
|
|
31754
|
+
const methods = readArray(answer, "methods");
|
|
31755
|
+
reloaded = methods ? { methods: methods.map(String) } : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
31756
|
+
} catch (error) {
|
|
31757
|
+
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
31758
|
+
}
|
|
31759
|
+
}
|
|
31745
31760
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
31746
31761
|
const unseen = checked.unseen;
|
|
31747
31762
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
@@ -31749,6 +31764,8 @@ class GodotServer {
|
|
|
31749
31764
|
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
31750
31765
|
stillWorking: busy,
|
|
31751
31766
|
waitedMs: Date.now() - started,
|
|
31767
|
+
reloadedMethods: reloaded.methods,
|
|
31768
|
+
reloadProblem: reloaded.problem,
|
|
31752
31769
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
31753
31770
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
31754
31771
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.22",
|
|
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",
|