gdharness 1.1.16 → 1.1.18
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 +95 -14
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +8 -2
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +58 -0
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +26 -2
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +29 -1
- package/build/index.js +95 -14
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8013,7 +8013,28 @@ function reason(stdout, stderr) {
|
|
|
8013
8013
|
}
|
|
8014
8014
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
8015
8015
|
}
|
|
8016
|
-
|
|
8016
|
+
function librariesNotCopied(messages) {
|
|
8017
|
+
const found = new Set;
|
|
8018
|
+
for (const entry of messages) {
|
|
8019
|
+
const library = /Error copying library: (.+)$/.exec(entry.text)?.[1]?.trim();
|
|
8020
|
+
if (library !== undefined && library !== "") {
|
|
8021
|
+
found.add(library);
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
return [...found];
|
|
8025
|
+
}
|
|
8026
|
+
function extensionRetryNote(libraries) {
|
|
8027
|
+
return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + "editor's copy aside, so the import was run again and loaded the extension, and the messages are from " + "that second run. The editor's copy is left beside the library as a ~RF….TMP file, which can be deleted " + "once the editor has exited.";
|
|
8028
|
+
}
|
|
8029
|
+
async function runImport(godotPath, projectPath, once = importOnce) {
|
|
8030
|
+
const first = await once(godotPath, projectPath);
|
|
8031
|
+
const collided = librariesNotCopied(first.messages);
|
|
8032
|
+
if (collided.length === 0) {
|
|
8033
|
+
return first;
|
|
8034
|
+
}
|
|
8035
|
+
return { ...await once(godotPath, projectPath), librariesRetried: collided };
|
|
8036
|
+
}
|
|
8037
|
+
async function importOnce(godotPath, projectPath) {
|
|
8017
8038
|
const logDir = scratchDirectory("import");
|
|
8018
8039
|
try {
|
|
8019
8040
|
const { stderr } = await run3(godotPath, ["--headless", "--log-file", join6(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
|
|
@@ -8706,6 +8727,18 @@ function classRemedy(entries) {
|
|
|
8706
8727
|
const reload = declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]}` : `editor_rescan with reloadScript, a call each for ${declaring.join(" and ")} since it takes one script,`;
|
|
8707
8728
|
return `For ${entries.map(written).join(", ")}, run ${reload} first. In the one project that has ` + "reproduced a stale enum member, twice, the reload cleared it at once both times, and the one " + "plain editor_rescan tried left the diagnostic standing.";
|
|
8708
8729
|
}
|
|
8730
|
+
function failedReloadNote(failed) {
|
|
8731
|
+
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
8732
|
+
const lost = [
|
|
8733
|
+
...missing(failed.heldBefore, failed.heldAfter),
|
|
8734
|
+
...missing(failed.constantsBefore, failed.constantsAfter)
|
|
8735
|
+
];
|
|
8736
|
+
const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
|
|
8737
|
+
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
8738
|
+
const copy = unchanged ? "The copy the editor holds is as it was, with the members under heldBeforeReload and heldConstantsBeforeReload." : `The copy the editor holds changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
|
|
8739
|
+
const order = failed.names.length === 0 ? "" : ` If they deny a member of ${failed.names.join(" or ")} that its file declares, the editor's copy of that class is behind: reload that class first with reloadScript and then this script, the order that cleared it in the one project that has met this.`;
|
|
8740
|
+
return `${failed.script} did not compile, so nothing was reloaded: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
|
|
8741
|
+
}
|
|
8709
8742
|
function uncachedClassNote(types) {
|
|
8710
8743
|
if (types.length === 0) {
|
|
8711
8744
|
return "";
|
|
@@ -17331,7 +17364,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17331
17364
|
},
|
|
17332
17365
|
{
|
|
17333
17366
|
name: "editor_rescan",
|
|
17334
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok. 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
17367
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate; before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
17335
17368
|
parameters: {
|
|
17336
17369
|
projectPath: PROJECT_PATH,
|
|
17337
17370
|
timeoutMs: {
|
|
@@ -17341,7 +17374,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17341
17374
|
},
|
|
17342
17375
|
reloadScript: {
|
|
17343
17376
|
type: "string",
|
|
17344
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. 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, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods."
|
|
17377
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. 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, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before."
|
|
17345
17378
|
}
|
|
17346
17379
|
},
|
|
17347
17380
|
requires: ["projectPath"]
|
|
@@ -17499,7 +17532,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17499
17532
|
property: {
|
|
17500
17533
|
type: "string",
|
|
17501
17534
|
ops: ["set"],
|
|
17502
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was, because the engine would not take it, is refused and says what the property still holds.'
|
|
17535
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was, because the engine would not take it, is refused and says what the property still holds. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
17503
17536
|
},
|
|
17504
17537
|
value: {
|
|
17505
17538
|
ops: ["set"],
|
|
@@ -17719,7 +17752,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
17719
17752
|
ops: ["until"],
|
|
17720
17753
|
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
17721
17754
|
},
|
|
17722
|
-
value: {
|
|
17755
|
+
value: {
|
|
17756
|
+
ops: ["until"],
|
|
17757
|
+
description: "until: the value to wait for, fitted to the property's type. On a property declared to hold an object, null, which the text null also means; any other value is refused, since the property can never hold it."
|
|
17758
|
+
},
|
|
17723
17759
|
says: {
|
|
17724
17760
|
type: "string",
|
|
17725
17761
|
ops: ["until"],
|
|
@@ -40613,7 +40649,8 @@ class GodotServer {
|
|
|
40613
40649
|
payload: {
|
|
40614
40650
|
uidsCreated: given,
|
|
40615
40651
|
stillWithoutUid: after,
|
|
40616
|
-
note: uidsLeftNote(after.length)
|
|
40652
|
+
note: uidsLeftNote(after.length),
|
|
40653
|
+
...imported.librariesRetried === undefined ? {} : { extensionNote: extensionRetryNote(imported.librariesRetried) }
|
|
40617
40654
|
}
|
|
40618
40655
|
}, scanned));
|
|
40619
40656
|
}
|
|
@@ -40769,6 +40806,9 @@ class GodotServer {
|
|
|
40769
40806
|
if (imported.messages.length > 0) {
|
|
40770
40807
|
extra["engine_messages"] = imported.messages;
|
|
40771
40808
|
}
|
|
40809
|
+
if (imported.librariesRetried !== undefined) {
|
|
40810
|
+
extra["extensionNote"] = extensionRetryNote(imported.librariesRetried);
|
|
40811
|
+
}
|
|
40772
40812
|
}
|
|
40773
40813
|
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
40774
40814
|
if (!after.ok) {
|
|
@@ -41297,6 +41337,11 @@ class GodotServer {
|
|
|
41297
41337
|
shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
|
|
41298
41338
|
});
|
|
41299
41339
|
}
|
|
41340
|
+
classesAScriptNames(projectPath, scriptPath) {
|
|
41341
|
+
const source = this.sourceOfClass(projectPath, scriptPath);
|
|
41342
|
+
const known = cachedClasses(projectPath) ?? declaredClasses(projectPath);
|
|
41343
|
+
return source === null ? [] : classesNamedIn(source, known).filter((name) => known.get(name) !== scriptPath);
|
|
41344
|
+
}
|
|
41300
41345
|
sourceOfClass(projectPath, resourcePath) {
|
|
41301
41346
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
41302
41347
|
if (!contained.ok || !existsSync15(contained.absolutePath)) {
|
|
@@ -42962,6 +43007,8 @@ class GodotServer {
|
|
|
42962
43007
|
let waitedForEditorMs = 0;
|
|
42963
43008
|
let first;
|
|
42964
43009
|
let completedBefore;
|
|
43010
|
+
let uidsReread = [];
|
|
43011
|
+
let uidsDuplicatedOnDisk = [];
|
|
42965
43012
|
for (;; ) {
|
|
42966
43013
|
const busySince = Date.now();
|
|
42967
43014
|
let wasBusy = false;
|
|
@@ -42978,6 +43025,11 @@ class GodotServer {
|
|
|
42978
43025
|
}
|
|
42979
43026
|
const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
|
|
42980
43027
|
completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
|
|
43028
|
+
uidsReread = [...uidsReread, ...(readArray(firstAnswer, "uidsReread") ?? []).map(String)];
|
|
43029
|
+
const duplicated = readArray(firstAnswer, "uidsDuplicatedOnDisk");
|
|
43030
|
+
if (duplicated !== undefined) {
|
|
43031
|
+
uidsDuplicatedOnDisk = duplicated.map((group) => Array.isArray(group) ? group.map(String) : []);
|
|
43032
|
+
}
|
|
42981
43033
|
if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
|
|
42982
43034
|
break;
|
|
42983
43035
|
}
|
|
@@ -43027,12 +43079,30 @@ class GodotServer {
|
|
|
43027
43079
|
const found = readArray(answer, field);
|
|
43028
43080
|
return found ? { [field]: found.map(String) } : {};
|
|
43029
43081
|
};
|
|
43030
|
-
|
|
43031
|
-
|
|
43032
|
-
|
|
43033
|
-
|
|
43034
|
-
|
|
43035
|
-
|
|
43082
|
+
const failed = answer["failed"];
|
|
43083
|
+
if (typeof failed === "number" && failed !== 0) {
|
|
43084
|
+
reloaded = {
|
|
43085
|
+
...listed("heldBefore"),
|
|
43086
|
+
...listed("heldConstantsBefore"),
|
|
43087
|
+
problem: failedReloadNote({
|
|
43088
|
+
script: reloading,
|
|
43089
|
+
code: failed,
|
|
43090
|
+
codeName: readString(answer, "failedAs") ?? "",
|
|
43091
|
+
heldBefore: listed("heldBefore")["heldBefore"] ?? [],
|
|
43092
|
+
heldAfter: methods?.map(String) ?? [],
|
|
43093
|
+
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
43094
|
+
constantsAfter: listed("constants")["constants"] ?? [],
|
|
43095
|
+
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, reloading)
|
|
43096
|
+
})
|
|
43097
|
+
};
|
|
43098
|
+
} else {
|
|
43099
|
+
reloaded = methods ? {
|
|
43100
|
+
methods: methods.map(String),
|
|
43101
|
+
...listed("heldBefore"),
|
|
43102
|
+
...listed("constants"),
|
|
43103
|
+
...listed("heldConstantsBefore")
|
|
43104
|
+
} : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
43105
|
+
}
|
|
43036
43106
|
} catch (error) {
|
|
43037
43107
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
43038
43108
|
}
|
|
@@ -43055,13 +43125,16 @@ class GodotServer {
|
|
|
43055
43125
|
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
43056
43126
|
notes.push(`The scan brought in ${broughtIn.join(", ")}, and ${dependents.notReloaded.map((one) => one.scriptPath).join(", ")} ${dependents.notReloaded.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. editor_rescan with reloadScript on each, or editor_launch restart.`);
|
|
43057
43127
|
}
|
|
43128
|
+
for (const group of uidsDuplicatedOnDisk) {
|
|
43129
|
+
notes.push(`${group.join(" and ")} name one UID in their .uid files, so Godot keeps one path for it and every engine warns "UID duplicate detected". Delete the .uid of the one that is a copy and rescan: the editor writes it a UID of its own.`);
|
|
43130
|
+
}
|
|
43058
43131
|
if (stillAtMissingPaths.length > 0) {
|
|
43059
43132
|
notes.push(`The scan wrote the class cache with ${stillAtMissingPaths.join(", ")} at a path that is not on disk, and rebuilding the cache from the files did not take it out. The next engine to read the cache fails on "Could not parse global class" in whichever correct script shares the bare name. Restart the editor with editor_launch restart, then run project_import refresh_classes.`);
|
|
43060
43133
|
} else if (dropped.length > 0) {
|
|
43061
43134
|
notes.push(`The scan wrote the class cache from the list this editor is holding, and that list still has ${dropped.join(", ")} for a script that is gone, so the cache was rebuilt from the files without it. The editor goes on holding it and writes it back on every scan until it is restarted with editor_launch restart; until then a scan is followed by this same rebuild.`);
|
|
43062
43135
|
}
|
|
43063
43136
|
return this.jsonTextResponse({
|
|
43064
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
|
|
43137
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && reloaded.problem === undefined && checked.unchecked === undefined,
|
|
43065
43138
|
stillWorking: busy,
|
|
43066
43139
|
waitedMs: Date.now() - started,
|
|
43067
43140
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
@@ -43079,6 +43152,8 @@ class GodotServer {
|
|
|
43079
43152
|
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
43080
43153
|
cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
|
|
43081
43154
|
classesUnchecked: checked.unchecked,
|
|
43155
|
+
uidsReread: uidsReread.length > 0 ? uidsReread : undefined,
|
|
43156
|
+
uidsDuplicatedOnDisk: uidsDuplicatedOnDisk.length > 0 ? uidsDuplicatedOnDisk : undefined,
|
|
43082
43157
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
43083
43158
|
});
|
|
43084
43159
|
}
|
|
@@ -43095,7 +43170,13 @@ class GodotServer {
|
|
|
43095
43170
|
}
|
|
43096
43171
|
try {
|
|
43097
43172
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
43098
|
-
|
|
43173
|
+
const failed = answer["failed"];
|
|
43174
|
+
if (typeof failed === "number" && failed !== 0) {
|
|
43175
|
+
notReloaded.push({
|
|
43176
|
+
scriptPath,
|
|
43177
|
+
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
|
|
43178
|
+
});
|
|
43179
|
+
} else if (readArray(answer, "methods") === undefined) {
|
|
43099
43180
|
notReloaded.push({
|
|
43100
43181
|
scriptPath,
|
|
43101
43182
|
problem: "the editor would not say what it has after reloading it"
|
|
@@ -74,16 +74,22 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
74
74
|
var reader: FileAccess = FileAccess.open(path, FileAccess.READ)
|
|
75
75
|
if reader == null:
|
|
76
76
|
return {"ok": false, "error": "Could not read " + path + ": " + str(FileAccess.get_open_error())}
|
|
77
|
+
var previous: String = script.source_code
|
|
77
78
|
script.source_code = reader.get_as_text()
|
|
78
79
|
reader.close()
|
|
79
80
|
|
|
81
|
+
# A reload that fails is still answered as a call that ran, because the readings are what a
|
|
82
|
+
# caller needs then: a failure travels back as its message alone, and a project met one and
|
|
83
|
+
# could not tell whether the copy had been damaged by it or before it. The old text goes back
|
|
84
|
+
# into the object, so what it holds and the source it says it was built from still agree.
|
|
80
85
|
var failed: Error = script.reload(true)
|
|
81
86
|
if failed != OK:
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
script.source_code = previous
|
|
84
88
|
return {
|
|
85
89
|
"ok": true,
|
|
86
90
|
"script": path,
|
|
91
|
+
"failed": failed,
|
|
92
|
+
"failedAs": error_string(failed) if failed != OK else "",
|
|
87
93
|
"heldBefore": held,
|
|
88
94
|
"methods": _method_names(script),
|
|
89
95
|
"heldConstantsBefore": held_constants,
|
|
@@ -665,7 +665,9 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
665
665
|
var busy: bool = filesystem.is_scanning() or filesystem.is_importing()
|
|
666
666
|
var started: bool = false
|
|
667
667
|
var completed_before: int = _scans_completed
|
|
668
|
+
var uids: Dictionary = {"reread": [], "duplicated": []}
|
|
668
669
|
if not Read.as_bool(args.get("statusOnly", false)) and not busy:
|
|
670
|
+
uids = _reread_changed_uids(filesystem)
|
|
669
671
|
filesystem.scan()
|
|
670
672
|
# Whether the scan ran, read off the editor rather than assumed. `scan()` returns without
|
|
671
673
|
# a word while the thread of the scan before is still to be joined, which is a frame or
|
|
@@ -687,9 +689,65 @@ func rescan_filesystem(args: Dictionary) -> Dictionary:
|
|
|
687
689
|
"importing": filesystem.is_importing(),
|
|
688
690
|
"scansCompletedBefore": completed_before,
|
|
689
691
|
"scansCompleted": _scans_completed,
|
|
692
|
+
"uidsReread": uids["reread"],
|
|
693
|
+
"uidsDuplicatedOnDisk": uids["duplicated"],
|
|
690
694
|
}
|
|
691
695
|
|
|
692
696
|
|
|
697
|
+
## Has the editor read again every file whose `.uid` names a UID the editor does not hold for it.
|
|
698
|
+
##
|
|
699
|
+
## The scan re-reads a file when the file changes and not when only the `.uid` beside it does, so a
|
|
700
|
+
## script copied with its `.uid` and then given a fresh one keeps the copied UID in the editor, and
|
|
701
|
+
## every headless engine reading the editor's cache warns of a duplicate. Measured on 4.7.2: a scan
|
|
702
|
+
## after the `.uid` was rewritten left both scripts on one UID in the cache. What the editor holds
|
|
703
|
+
## is read off its UID table, which maps each UID to one path, so a `.uid` whose UID is missing from
|
|
704
|
+
## the table or mapped to another file is one the editor has not taken in.
|
|
705
|
+
##
|
|
706
|
+
## Two files on disk naming one UID are left alone and named: reading them again cannot settle
|
|
707
|
+
## which of them owns it, and doing it on every scan would only move the UID between them.
|
|
708
|
+
func _reread_changed_uids(filesystem: EditorFileSystem) -> Dictionary:
|
|
709
|
+
var sidecars: Array[String] = []
|
|
710
|
+
_uid_files("res://", sidecars)
|
|
711
|
+
var claimed: Dictionary = {}
|
|
712
|
+
for sidecar: String in sidecars:
|
|
713
|
+
var id: int = ResourceUID.text_to_id(FileAccess.get_file_as_string(sidecar).strip_edges())
|
|
714
|
+
var path: String = sidecar.trim_suffix(".uid")
|
|
715
|
+
if id == ResourceUID.INVALID_ID or not FileAccess.file_exists(path):
|
|
716
|
+
continue
|
|
717
|
+
var paths: Array = claimed.get_or_add(id, [])
|
|
718
|
+
paths.append(path)
|
|
719
|
+
var reread: Array[String] = []
|
|
720
|
+
var duplicated: Array[Array] = []
|
|
721
|
+
for id: int in claimed:
|
|
722
|
+
var paths: Array = claimed[id]
|
|
723
|
+
if paths.size() > 1:
|
|
724
|
+
paths.sort()
|
|
725
|
+
duplicated.append(paths)
|
|
726
|
+
continue
|
|
727
|
+
var path: String = str(paths[0])
|
|
728
|
+
if ResourceUID.has_id(id) and ResourceUID.get_id_path(id) == path:
|
|
729
|
+
continue
|
|
730
|
+
filesystem.update_file(path)
|
|
731
|
+
reread.append(path)
|
|
732
|
+
reread.sort()
|
|
733
|
+
duplicated.sort()
|
|
734
|
+
return {"reread": reread, "duplicated": duplicated}
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
## Every `.uid` under [param directory], skipping what the editor skips: hidden directories and any
|
|
738
|
+
## holding a `.gdignore`.
|
|
739
|
+
func _uid_files(directory: String, found: Array[String]) -> void:
|
|
740
|
+
var listing: DirAccess = DirAccess.open(directory)
|
|
741
|
+
if listing == null or FileAccess.file_exists(directory.path_join(".gdignore")):
|
|
742
|
+
return
|
|
743
|
+
for child: String in listing.get_directories():
|
|
744
|
+
if not child.begins_with("."):
|
|
745
|
+
_uid_files(directory.path_join(child), found)
|
|
746
|
+
for file: String in listing.get_files():
|
|
747
|
+
if file.ends_with(".uid"):
|
|
748
|
+
found.append(directory.path_join(file))
|
|
749
|
+
|
|
750
|
+
|
|
693
751
|
## Whether the editor is scanning, and how long ago its last scan finished, without asking for one.
|
|
694
752
|
##
|
|
695
753
|
## For a caller about to start a game: the editor rewrites the class cache a frame after a scan
|
|
@@ -640,9 +640,12 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
640
640
|
# the answer then showed the old value as the new one, shaped as a success: a typed container
|
|
641
641
|
# refusing a plain list did exactly that. A value the property changed on the way in, a setter
|
|
642
642
|
# clamping it, still changed it, so only a write that left the property as it was is refused.
|
|
643
|
+
# And only one that asked for a change: the value the property already held, written again, left
|
|
644
|
+
# it as it was because nothing was asked of it.
|
|
645
|
+
var already: bool = _held_already(given, old_value)
|
|
643
646
|
if (
|
|
644
647
|
Values.comparable(given, old_value)
|
|
645
|
-
and
|
|
648
|
+
and not already
|
|
646
649
|
and Values.comparable(now, old_value)
|
|
647
650
|
and now == old_value
|
|
648
651
|
):
|
|
@@ -663,7 +666,7 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
663
666
|
)
|
|
664
667
|
}
|
|
665
668
|
|
|
666
|
-
|
|
669
|
+
var answer: Dictionary = {
|
|
667
670
|
"type": "property_set",
|
|
668
671
|
"path": node_path,
|
|
669
672
|
"property": property,
|
|
@@ -671,6 +674,27 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
671
674
|
"new_value": _values.serialize(now),
|
|
672
675
|
"elapsed_usec": elapsed
|
|
673
676
|
}
|
|
677
|
+
if already and Values.comparable(now, old_value) and now == old_value:
|
|
678
|
+
answer["unchanged"] = true
|
|
679
|
+
return answer
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
## Whether [param given] is the value [param held] already is, at the precision the property keeps.
|
|
683
|
+
##
|
|
684
|
+
## A float arrives as 64 bits and most engine properties keep 32: Camera3D.h_offset given -1.1
|
|
685
|
+
## reads back as -1.10000002384186, so comparing the two exactly took a write of the value already
|
|
686
|
+
## held for one the engine refused (#780). Rounding both to 32 bits is exact for a 32-bit property.
|
|
687
|
+
## A 64-bit one can take a write this calls the same, and then reads back changed, which the caller
|
|
688
|
+
## checks before calling the write unchanged. Vectors and colours hold 32-bit parts in the value
|
|
689
|
+
## itself, so they already compare at the property's precision.
|
|
690
|
+
static func _held_already(given: Variant, held: Variant) -> bool:
|
|
691
|
+
if not Values.comparable(given, held):
|
|
692
|
+
return false
|
|
693
|
+
if given == held:
|
|
694
|
+
return true
|
|
695
|
+
if typeof(given) != TYPE_FLOAT or typeof(held) != TYPE_FLOAT:
|
|
696
|
+
return false
|
|
697
|
+
return PackedFloat32Array([given])[0] == PackedFloat32Array([held])[0]
|
|
674
698
|
|
|
675
699
|
|
|
676
700
|
func call_method(params: Dictionary) -> Dictionary:
|
|
@@ -140,7 +140,23 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
140
140
|
|
|
141
141
|
var current: Variant = watched["value"]
|
|
142
142
|
var wanted: Variant = _values.fitted(params["value"], typeof(current))
|
|
143
|
+
# Read against what the property is declared as, not only against what it holds this frame: an
|
|
144
|
+
# object property holding null holds nothing typed, so any value was taken as it came and compared
|
|
145
|
+
# with null until the time ran out. #772 waited a minute for "null" that way and answered met
|
|
146
|
+
# false with value null beside it.
|
|
147
|
+
var declared: int = watched["declared"]
|
|
148
|
+
if declared == TYPE_OBJECT and wanted is String and str(wanted).strip_edges() == "null":
|
|
149
|
+
wanted = null
|
|
143
150
|
var refused: String = _not_comparable(current, wanted, node_path, property)
|
|
151
|
+
var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
|
|
152
|
+
if refused.is_empty() and declared == TYPE_OBJECT and never_held:
|
|
153
|
+
refused = (
|
|
154
|
+
(
|
|
155
|
+
"%s.%s is declared to hold an object and the value to wait for is %s, which it can"
|
|
156
|
+
+ " never hold: wait for null, or for the object's own value on a property of it."
|
|
157
|
+
)
|
|
158
|
+
% [node_path, property, type_string(typeof(wanted))]
|
|
159
|
+
)
|
|
144
160
|
if not refused.is_empty():
|
|
145
161
|
return {"type": "error", "message": refused}
|
|
146
162
|
|
|
@@ -224,7 +240,19 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
|
|
|
224
240
|
var missing: String = Paths.nothing_under(holder, named, str(reached["called"]))
|
|
225
241
|
if not missing.is_empty():
|
|
226
242
|
return {"type": "error", "message": missing}
|
|
227
|
-
return {"value": Paths.read_under(holder, named)}
|
|
243
|
+
return {"value": Paths.read_under(holder, named), "declared": _declared_type(holder, named)}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
## The type [param named] is declared with on [param holder], or TYPE_NIL for anything that declares
|
|
247
|
+
## none: an untyped property, a step into a list or map, or a call.
|
|
248
|
+
static func _declared_type(holder: Variant, named: String) -> int:
|
|
249
|
+
if typeof(holder) != TYPE_OBJECT or not Paths.method_of(named).is_empty():
|
|
250
|
+
return TYPE_NIL
|
|
251
|
+
var object: Object = holder
|
|
252
|
+
for entry: Dictionary in object.get_property_list():
|
|
253
|
+
if str(entry.get("name", "")) == named:
|
|
254
|
+
return Read.as_int(entry.get("type", TYPE_NIL), TYPE_NIL)
|
|
255
|
+
return TYPE_NIL
|
|
228
256
|
|
|
229
257
|
|
|
230
258
|
## Waits until something under [param node_path] has [param said] written on it.
|
package/build/index.js
CHANGED
|
@@ -26276,6 +26276,18 @@ function classRemedy(entries) {
|
|
|
26276
26276
|
const reload = declaring.length === 1 ? `editor_rescan with reloadScript set to ${declaring[0]}` : `editor_rescan with reloadScript, a call each for ${declaring.join(" and ")} since it takes one script,`;
|
|
26277
26277
|
return `For ${entries.map(written).join(", ")}, run ${reload} first. In the one project that has ` + "reproduced a stale enum member, twice, the reload cleared it at once both times, and the one " + "plain editor_rescan tried left the diagnostic standing.";
|
|
26278
26278
|
}
|
|
26279
|
+
function failedReloadNote(failed) {
|
|
26280
|
+
const missing = (before, after) => before.filter((name) => !after.includes(name));
|
|
26281
|
+
const lost = [
|
|
26282
|
+
...missing(failed.heldBefore, failed.heldAfter),
|
|
26283
|
+
...missing(failed.constantsBefore, failed.constantsAfter)
|
|
26284
|
+
];
|
|
26285
|
+
const unchanged = lost.length === 0 && missing(failed.heldAfter, failed.heldBefore).length === 0 && missing(failed.constantsAfter, failed.constantsBefore).length === 0;
|
|
26286
|
+
const answered = failed.codeName === "" ? `error ${failed.code}` : `error ${failed.code}, ${failed.codeName}`;
|
|
26287
|
+
const copy = unchanged ? "The copy the editor holds is as it was, with the members under heldBeforeReload and heldConstantsBeforeReload." : `The copy the editor holds changed even so${lost.length === 0 ? "" : `, and has lost ${lost.join(", ")}`}: anything diagnosed against it now is answered from what is left, and reloading it once it compiles rebuilds it.`;
|
|
26288
|
+
const order = failed.names.length === 0 ? "" : ` If they deny a member of ${failed.names.join(" or ")} that its file declares, the editor's copy of that class is behind: reload that class first with reloadScript and then this script, the order that cleared it in the one project that has met this.`;
|
|
26289
|
+
return `${failed.script} did not compile, so nothing was reloaded: Godot answered ${answered}. ${copy} ` + `script_diagnostics on it gives the errors.${order}`;
|
|
26290
|
+
}
|
|
26279
26291
|
function uncachedClassNote(types) {
|
|
26280
26292
|
if (types.length === 0) {
|
|
26281
26293
|
return "";
|
|
@@ -28280,7 +28292,28 @@ function reason(stdout, stderr) {
|
|
|
28280
28292
|
}
|
|
28281
28293
|
return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
|
|
28282
28294
|
}
|
|
28283
|
-
|
|
28295
|
+
function librariesNotCopied(messages) {
|
|
28296
|
+
const found = new Set;
|
|
28297
|
+
for (const entry of messages) {
|
|
28298
|
+
const library = /Error copying library: (.+)$/.exec(entry.text)?.[1]?.trim();
|
|
28299
|
+
if (library !== undefined && library !== "") {
|
|
28300
|
+
found.add(library);
|
|
28301
|
+
}
|
|
28302
|
+
}
|
|
28303
|
+
return [...found];
|
|
28304
|
+
}
|
|
28305
|
+
function extensionRetryNote(libraries) {
|
|
28306
|
+
return `The first import could not load ${libraries.join(" and ")}: on Windows an engine running as the editor ` + "loads a GDExtension from a ~ copy beside the library, and the editor open on this project holds its own " + "copy there, so the first engine started beside it cannot make one. That failed attempt moves the " + "editor's copy aside, so the import was run again and loaded the extension, and the messages are from " + "that second run. The editor's copy is left beside the library as a ~RF….TMP file, which can be deleted " + "once the editor has exited.";
|
|
28307
|
+
}
|
|
28308
|
+
async function runImport(godotPath, projectPath, once = importOnce) {
|
|
28309
|
+
const first = await once(godotPath, projectPath);
|
|
28310
|
+
const collided = librariesNotCopied(first.messages);
|
|
28311
|
+
if (collided.length === 0) {
|
|
28312
|
+
return first;
|
|
28313
|
+
}
|
|
28314
|
+
return { ...await once(godotPath, projectPath), librariesRetried: collided };
|
|
28315
|
+
}
|
|
28316
|
+
async function importOnce(godotPath, projectPath) {
|
|
28284
28317
|
const logDir = scratchDirectory("import");
|
|
28285
28318
|
try {
|
|
28286
28319
|
const { stderr } = await run3(godotPath, ["--headless", "--log-file", join12(logDir, "engine.log"), "--path", projectPath, "--import"], { signal: callSignal() });
|
|
@@ -30988,7 +31021,7 @@ var TOOL_SPECS = [
|
|
|
30988
31021
|
},
|
|
30989
31022
|
{
|
|
30990
31023
|
name: "editor_rescan",
|
|
30991
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok. 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
31024
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. A scan or an import the editor is already running is waited out first rather than scanned over, which would start a second import over it, and the answer says how long under waitedForEditorScanMs. 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 this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. The walk also passes over a file whose .uid alone changed, so the editor keeps the old UID and every engine reading its cache warns of a duplicate; before scanning, each file whose .uid names a UID the editor does not hold for it is read again and named under uidsReread, and files on disk naming one UID between them are named under uidsDuplicatedOnDisk, which reading again cannot settle. A class the scan brings in is named under broughtIn, and every script naming it is reloaded in the editor and named under dependentsReloaded: what the editor compiled from them while it could not see the class stays compiled that way after the scan, and a script extending a new base class went on reporting \"Could not find base class\" until reloaded. One that would not reload is named under dependentsNotReloaded and the call is not ok, as it is not when the script named in reloadScript was not reloaded. 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. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
30992
31025
|
parameters: {
|
|
30993
31026
|
projectPath: PROJECT_PATH,
|
|
30994
31027
|
timeoutMs: {
|
|
@@ -30998,7 +31031,7 @@ var TOOL_SPECS = [
|
|
|
30998
31031
|
},
|
|
30999
31032
|
reloadScript: {
|
|
31000
31033
|
type: "string",
|
|
31001
|
-
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. 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, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods."
|
|
31034
|
+
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. 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, and what it held before under heldBeforeReload. Its constants come back the same way, under heldConstantsBeforeReload and reloadedConstants, with each enum value as Kind.SHORT: an enum member is held as a constant and never shows among the methods. A script that does not compile is not reloaded: reloadProblem gives Godot's error, says whether the copy the editor holds survived, and names the classes the file uses, whose held copies being behind is what failed one before."
|
|
31002
31035
|
}
|
|
31003
31036
|
},
|
|
31004
31037
|
requires: ["projectPath"]
|
|
@@ -31156,7 +31189,7 @@ var TOOL_SPECS = [
|
|
|
31156
31189
|
property: {
|
|
31157
31190
|
type: "string",
|
|
31158
31191
|
ops: ["set"],
|
|
31159
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was, because the engine would not take it, is refused and says what the property still holds.'
|
|
31192
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method taking no arguments returned. The answer reads back off the same holder, and a write that leaves the property as it was, because the engine would not take it, is refused and says what the property still holds. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
31160
31193
|
},
|
|
31161
31194
|
value: {
|
|
31162
31195
|
ops: ["set"],
|
|
@@ -31376,7 +31409,10 @@ var TOOL_SPECS = [
|
|
|
31376
31409
|
ops: ["until"],
|
|
31377
31410
|
description: 'until: the property name, or a colon path through what the node holds, "_game:run:day", read the way runtime_inspect property reads one, a step written as a call included. Walked again on every frame, so a holder the game replaces while the wait is on is followed.'
|
|
31378
31411
|
},
|
|
31379
|
-
value: {
|
|
31412
|
+
value: {
|
|
31413
|
+
ops: ["until"],
|
|
31414
|
+
description: "until: the value to wait for, fitted to the property's type. On a property declared to hold an object, null, which the text null also means; any other value is refused, since the property can never hold it."
|
|
31415
|
+
},
|
|
31380
31416
|
says: {
|
|
31381
31417
|
type: "string",
|
|
31382
31418
|
ops: ["until"],
|
|
@@ -33073,7 +33109,8 @@ class GodotServer {
|
|
|
33073
33109
|
payload: {
|
|
33074
33110
|
uidsCreated: given,
|
|
33075
33111
|
stillWithoutUid: after,
|
|
33076
|
-
note: uidsLeftNote(after.length)
|
|
33112
|
+
note: uidsLeftNote(after.length),
|
|
33113
|
+
...imported.librariesRetried === undefined ? {} : { extensionNote: extensionRetryNote(imported.librariesRetried) }
|
|
33077
33114
|
}
|
|
33078
33115
|
}, scanned));
|
|
33079
33116
|
}
|
|
@@ -33229,6 +33266,9 @@ class GodotServer {
|
|
|
33229
33266
|
if (imported.messages.length > 0) {
|
|
33230
33267
|
extra["engine_messages"] = imported.messages;
|
|
33231
33268
|
}
|
|
33269
|
+
if (imported.librariesRetried !== undefined) {
|
|
33270
|
+
extra["extensionNote"] = extensionRetryNote(imported.librariesRetried);
|
|
33271
|
+
}
|
|
33232
33272
|
}
|
|
33233
33273
|
const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
|
|
33234
33274
|
if (!after.ok) {
|
|
@@ -33757,6 +33797,11 @@ class GodotServer {
|
|
|
33757
33797
|
shortListEditorPid: this.godotBridge.getStatus().editorPid ?? null
|
|
33758
33798
|
});
|
|
33759
33799
|
}
|
|
33800
|
+
classesAScriptNames(projectPath, scriptPath) {
|
|
33801
|
+
const source = this.sourceOfClass(projectPath, scriptPath);
|
|
33802
|
+
const known = cachedClasses(projectPath) ?? declaredClasses(projectPath);
|
|
33803
|
+
return source === null ? [] : classesNamedIn(source, known).filter((name) => known.get(name) !== scriptPath);
|
|
33804
|
+
}
|
|
33760
33805
|
sourceOfClass(projectPath, resourcePath) {
|
|
33761
33806
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
33762
33807
|
if (!contained.ok || !existsSync12(contained.absolutePath)) {
|
|
@@ -35422,6 +35467,8 @@ class GodotServer {
|
|
|
35422
35467
|
let waitedForEditorMs = 0;
|
|
35423
35468
|
let first;
|
|
35424
35469
|
let completedBefore;
|
|
35470
|
+
let uidsReread = [];
|
|
35471
|
+
let uidsDuplicatedOnDisk = [];
|
|
35425
35472
|
for (;; ) {
|
|
35426
35473
|
const busySince = Date.now();
|
|
35427
35474
|
let wasBusy = false;
|
|
@@ -35438,6 +35485,11 @@ class GodotServer {
|
|
|
35438
35485
|
}
|
|
35439
35486
|
const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
|
|
35440
35487
|
completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
|
|
35488
|
+
uidsReread = [...uidsReread, ...(readArray(firstAnswer, "uidsReread") ?? []).map(String)];
|
|
35489
|
+
const duplicated = readArray(firstAnswer, "uidsDuplicatedOnDisk");
|
|
35490
|
+
if (duplicated !== undefined) {
|
|
35491
|
+
uidsDuplicatedOnDisk = duplicated.map((group) => Array.isArray(group) ? group.map(String) : []);
|
|
35492
|
+
}
|
|
35441
35493
|
if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
|
|
35442
35494
|
break;
|
|
35443
35495
|
}
|
|
@@ -35487,12 +35539,30 @@ class GodotServer {
|
|
|
35487
35539
|
const found = readArray(answer, field);
|
|
35488
35540
|
return found ? { [field]: found.map(String) } : {};
|
|
35489
35541
|
};
|
|
35490
|
-
|
|
35491
|
-
|
|
35492
|
-
|
|
35493
|
-
|
|
35494
|
-
|
|
35495
|
-
|
|
35542
|
+
const failed = answer["failed"];
|
|
35543
|
+
if (typeof failed === "number" && failed !== 0) {
|
|
35544
|
+
reloaded = {
|
|
35545
|
+
...listed("heldBefore"),
|
|
35546
|
+
...listed("heldConstantsBefore"),
|
|
35547
|
+
problem: failedReloadNote({
|
|
35548
|
+
script: reloading,
|
|
35549
|
+
code: failed,
|
|
35550
|
+
codeName: readString(answer, "failedAs") ?? "",
|
|
35551
|
+
heldBefore: listed("heldBefore")["heldBefore"] ?? [],
|
|
35552
|
+
heldAfter: methods?.map(String) ?? [],
|
|
35553
|
+
constantsBefore: listed("heldConstantsBefore")["heldConstantsBefore"] ?? [],
|
|
35554
|
+
constantsAfter: listed("constants")["constants"] ?? [],
|
|
35555
|
+
names: projectPath === "" ? [] : this.classesAScriptNames(projectPath, reloading)
|
|
35556
|
+
})
|
|
35557
|
+
};
|
|
35558
|
+
} else {
|
|
35559
|
+
reloaded = methods ? {
|
|
35560
|
+
methods: methods.map(String),
|
|
35561
|
+
...listed("heldBefore"),
|
|
35562
|
+
...listed("constants"),
|
|
35563
|
+
...listed("heldConstantsBefore")
|
|
35564
|
+
} : { problem: `the editor would not say what ${reloading} has after reloading it` };
|
|
35565
|
+
}
|
|
35496
35566
|
} catch (error) {
|
|
35497
35567
|
reloaded = { problem: `${reloading} could not be reloaded: ${errorMessage(error)}` };
|
|
35498
35568
|
}
|
|
@@ -35515,13 +35585,16 @@ class GodotServer {
|
|
|
35515
35585
|
if (dependents !== null && dependents.notReloaded.length > 0) {
|
|
35516
35586
|
notes.push(`The scan brought in ${broughtIn.join(", ")}, and ${dependents.notReloaded.map((one) => one.scriptPath).join(", ")} ${dependents.notReloaded.length === 1 ? "names one of them and" : "name them and"} could not be reloaded, so the editor still holds what it compiled while it could not see the class and reports that to the language server. editor_rescan with reloadScript on each, or editor_launch restart.`);
|
|
35517
35587
|
}
|
|
35588
|
+
for (const group of uidsDuplicatedOnDisk) {
|
|
35589
|
+
notes.push(`${group.join(" and ")} name one UID in their .uid files, so Godot keeps one path for it and every engine warns "UID duplicate detected". Delete the .uid of the one that is a copy and rescan: the editor writes it a UID of its own.`);
|
|
35590
|
+
}
|
|
35518
35591
|
if (stillAtMissingPaths.length > 0) {
|
|
35519
35592
|
notes.push(`The scan wrote the class cache with ${stillAtMissingPaths.join(", ")} at a path that is not on disk, and rebuilding the cache from the files did not take it out. The next engine to read the cache fails on "Could not parse global class" in whichever correct script shares the bare name. Restart the editor with editor_launch restart, then run project_import refresh_classes.`);
|
|
35520
35593
|
} else if (dropped.length > 0) {
|
|
35521
35594
|
notes.push(`The scan wrote the class cache from the list this editor is holding, and that list still has ${dropped.join(", ")} for a script that is gone, so the cache was rebuilt from the files without it. The editor goes on holding it and writes it back on every scan until it is restarted with editor_launch restart; until then a scan is followed by this same rebuild.`);
|
|
35522
35595
|
}
|
|
35523
35596
|
return this.jsonTextResponse({
|
|
35524
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && checked.unchecked === undefined,
|
|
35597
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && (dependents === null || dependents.notReloaded.length === 0) && reloaded.problem === undefined && checked.unchecked === undefined,
|
|
35525
35598
|
stillWorking: busy,
|
|
35526
35599
|
waitedMs: Date.now() - started,
|
|
35527
35600
|
waitedForEditorScanMs: waitedForEditorMs > 0 ? waitedForEditorMs : undefined,
|
|
@@ -35539,6 +35612,8 @@ class GodotServer {
|
|
|
35539
35612
|
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
35540
35613
|
cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
|
|
35541
35614
|
classesUnchecked: checked.unchecked,
|
|
35615
|
+
uidsReread: uidsReread.length > 0 ? uidsReread : undefined,
|
|
35616
|
+
uidsDuplicatedOnDisk: uidsDuplicatedOnDisk.length > 0 ? uidsDuplicatedOnDisk : undefined,
|
|
35542
35617
|
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
35543
35618
|
});
|
|
35544
35619
|
}
|
|
@@ -35555,7 +35630,13 @@ class GodotServer {
|
|
|
35555
35630
|
}
|
|
35556
35631
|
try {
|
|
35557
35632
|
const answer = asParams(await this.godotBridge.invokeTool("reload_script", { scriptPath }));
|
|
35558
|
-
|
|
35633
|
+
const failed = answer["failed"];
|
|
35634
|
+
if (typeof failed === "number" && failed !== 0) {
|
|
35635
|
+
notReloaded.push({
|
|
35636
|
+
scriptPath,
|
|
35637
|
+
problem: `it did not compile: Godot answered error ${failed}${readString(answer, "failedAs") ? `, ${readString(answer, "failedAs")}` : ""}`
|
|
35638
|
+
});
|
|
35639
|
+
} else if (readArray(answer, "methods") === undefined) {
|
|
35559
35640
|
notReloaded.push({
|
|
35560
35641
|
scriptPath,
|
|
35561
35642
|
problem: "the editor would not say what it has after reloading it"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.18",
|
|
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",
|