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.21",
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",