gdharness 0.13.5 → 0.13.7
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 +64 -13
- package/build/godot/addons/gdharness_editor/property_values.gd +294 -0
- package/build/godot/addons/gdharness_editor/property_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +106 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +9 -26
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +11 -53
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +3 -168
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +10 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +4 -90
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +41 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +106 -0
- package/build/godot/operations/serialisation.gd +106 -0
- package/build/index.js +64 -13
- package/package.json +1 -1
|
@@ -10,6 +10,27 @@ extends RefCounted
|
|
|
10
10
|
|
|
11
11
|
const Read = preload("reading.gd")
|
|
12
12
|
|
|
13
|
+
# What the engine carries from one to another without complaint, which is what a value written to a
|
|
14
|
+
# property or handed to a parameter may arrive as. Numbers include bool because the engine counts it
|
|
15
|
+
# as one, and the three text types are one string wearing three hats.
|
|
16
|
+
const NUMBERS: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT]
|
|
17
|
+
const TEXTS: Array[int] = [TYPE_STRING, TYPE_STRING_NAME, TYPE_NODE_PATH]
|
|
18
|
+
|
|
19
|
+
# The types that cross as a plain JSON list and have to be built back into themselves. A list is
|
|
20
|
+
# what a caller can write by hand and what the serialiser answers with, and the parameter or
|
|
21
|
+
# property being written is the only thing that knows which of these it wanted.
|
|
22
|
+
const LIST_TYPES: Array[int] = [
|
|
23
|
+
TYPE_PACKED_INT32_ARRAY,
|
|
24
|
+
TYPE_PACKED_INT64_ARRAY,
|
|
25
|
+
TYPE_PACKED_FLOAT32_ARRAY,
|
|
26
|
+
TYPE_PACKED_FLOAT64_ARRAY,
|
|
27
|
+
TYPE_PACKED_STRING_ARRAY,
|
|
28
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
29
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
30
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
31
|
+
TYPE_PACKED_COLOR_ARRAY,
|
|
32
|
+
]
|
|
33
|
+
|
|
13
34
|
# What each Godot type becomes on the wire, as one table you can read rather than an order you
|
|
14
35
|
# have to trust. Keyed on typeof() rather than written as a chain of `is` tests, because the
|
|
15
36
|
# order mattered: Resource had to be tested before Object or every resource came back as a bare
|
|
@@ -163,6 +184,91 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
163
184
|
return call(builder, fields)
|
|
164
185
|
|
|
165
186
|
|
|
187
|
+
## Whether two values can be compared without the comparison itself failing.
|
|
188
|
+
##
|
|
189
|
+
## GDScript's `==` is not total. An Object against a String is a hard error rather than false, and
|
|
190
|
+
## an error raised inside a game is not a wrong answer: it stops the game. A caller who asked
|
|
191
|
+
## `runtime_wait until` for the wrong kind of value got their game held at a debugger break, every
|
|
192
|
+
## later call answering "did not respond within 10000ms, it may be stuck in a long frame", and
|
|
193
|
+
## nothing anywhere naming the argument that did it. A wrong argument costs a refusal, never the
|
|
194
|
+
## session's game.
|
|
195
|
+
##
|
|
196
|
+
## Only an object is special. Every other pair the engine answers, false where they differ, and
|
|
197
|
+
## refusing those would refuse calls that work: an int against a float is the same number to a
|
|
198
|
+
## caller waiting for 1 on a property holding 1.0.
|
|
199
|
+
static func comparable(one: Variant, other: Variant) -> bool:
|
|
200
|
+
var left: int = typeof(one)
|
|
201
|
+
var right: int = typeof(other)
|
|
202
|
+
if left != TYPE_OBJECT and right != TYPE_OBJECT:
|
|
203
|
+
return true
|
|
204
|
+
# An object compares with another object and with null. Anything else is the error above.
|
|
205
|
+
return (left == TYPE_OBJECT or left == TYPE_NIL) and (right == TYPE_OBJECT or right == TYPE_NIL)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
## Whether [param value] can be given to something declared as [param type] without damage.
|
|
209
|
+
##
|
|
210
|
+
## Here rather than beside either caller because the running game and the editor ask it of the same
|
|
211
|
+
## values and must not answer differently. What it protects against differs by side and both are
|
|
212
|
+
## silent: `callv` and `==` raise inside a running game, which holds it at a debugger break, while
|
|
213
|
+
## `Object.set` in the editor converts instead and writes the result to the scene file. A word where
|
|
214
|
+
## an int goes is stored as 0, where a bool goes as true, where a Vector2 goes as (0, 0), and the
|
|
215
|
+
## tool reports the change as made. The property keeps a value nobody asked for and the scene is
|
|
216
|
+
## saved with it.
|
|
217
|
+
##
|
|
218
|
+
## A list of what is accepted rather than of what is refused, which is the way round that errs
|
|
219
|
+
## safely. The engine's own conversion table is not readable from GDScript, so either list is a
|
|
220
|
+
## guess at it; guessing narrow costs a caller a refusal naming both types, and guessing wide costs
|
|
221
|
+
## them the damage above. A refusal is the cheaper mistake, and it is the one they can act on.
|
|
222
|
+
static func acceptable(value: Variant, type: int) -> bool:
|
|
223
|
+
if type == TYPE_NIL:
|
|
224
|
+
return true
|
|
225
|
+
var given: int = typeof(value)
|
|
226
|
+
if given == type:
|
|
227
|
+
return true
|
|
228
|
+
# null is a value for anything that holds an object, and for nothing else.
|
|
229
|
+
if given == TYPE_NIL:
|
|
230
|
+
return type == TYPE_OBJECT
|
|
231
|
+
if NUMBERS.has(given) and NUMBERS.has(type):
|
|
232
|
+
return true
|
|
233
|
+
if TEXTS.has(given) and TEXTS.has(type):
|
|
234
|
+
return true
|
|
235
|
+
return given == TYPE_ARRAY and LIST_TYPES.has(type)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
## A value from the wire fitted to the type a property or parameter declares. Arguments arrive
|
|
239
|
+
## as strings often enough, and callv refuses a "2.0" where a float is wanted, so a string that
|
|
240
|
+
## reads as the wanted scalar is read as one.
|
|
241
|
+
##
|
|
242
|
+
## The conversion is also what restores a packed array's exact type. Those cross as the plain JSON
|
|
243
|
+
## lists they fit into, so what comes back from the wire is an Array, and the parameter is what says
|
|
244
|
+
## it was a PackedStringArray.
|
|
245
|
+
func fitted(value: Variant, type: int) -> Variant:
|
|
246
|
+
var rebuilt: Variant = deserialize_value(value)
|
|
247
|
+
if type == TYPE_NIL or typeof(rebuilt) == type:
|
|
248
|
+
return rebuilt
|
|
249
|
+
|
|
250
|
+
if rebuilt is Array and LIST_TYPES.has(type):
|
|
251
|
+
return type_convert(rebuilt, type)
|
|
252
|
+
|
|
253
|
+
var simple: Array[int] = [TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING]
|
|
254
|
+
if not simple.has(type) or not simple.has(typeof(rebuilt)):
|
|
255
|
+
return rebuilt
|
|
256
|
+
|
|
257
|
+
if rebuilt is String and type != TYPE_STRING:
|
|
258
|
+
var text: String = rebuilt
|
|
259
|
+
var parsed: Variant = JSON.parse_string(text)
|
|
260
|
+
# A string that does not read as the type wanted is handed back as the string it is, so
|
|
261
|
+
# whoever asked can refuse it. type_convert answers 0 for "not an index" and true for any
|
|
262
|
+
# text at all, and a caller who sent a word got back a number they never sent: get_child
|
|
263
|
+
# answered about child 0 with no refusal and no note, which is a halt traded for a plausible
|
|
264
|
+
# wrong answer, and the wrong answer is the worse of the two.
|
|
265
|
+
if typeof(parsed) == TYPE_NIL or typeof(parsed) == TYPE_STRING:
|
|
266
|
+
return rebuilt
|
|
267
|
+
rebuilt = parsed
|
|
268
|
+
|
|
269
|
+
return type_convert(rebuilt, type)
|
|
270
|
+
|
|
271
|
+
|
|
166
272
|
## Whether [param fields] carries every key of any one of [param alternatives].
|
|
167
273
|
##
|
|
168
274
|
## More than one spelling because a rectangle has two: the two corners it is answered with, and the
|
package/build/index.js
CHANGED
|
@@ -4060,13 +4060,14 @@ var require_fast_uri = __commonJS(function(exports, module) {
|
|
|
4060
4060
|
if (!malformedIPLiteral) {
|
|
4061
4061
|
malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
|
|
4062
4062
|
}
|
|
4063
|
-
if (
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
4068
|
-
}
|
|
4063
|
+
if (uri.indexOf("%") !== -1 && parsed.host !== undefined && !malformedIPLiteral) {
|
|
4064
|
+
let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
|
|
4065
|
+
if (!isIP) {
|
|
4066
|
+
host = normalizePercentEncoding(host.toLowerCase());
|
|
4069
4067
|
}
|
|
4068
|
+
parsed.host = reescapeHostDelimiters(host, isIP);
|
|
4069
|
+
}
|
|
4070
|
+
if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
|
|
4070
4071
|
if (parsed.path) {
|
|
4071
4072
|
parsed.path = normalizePathEncoding(parsed.path);
|
|
4072
4073
|
}
|
|
@@ -28755,7 +28756,7 @@ var TOOL_SPECS = [
|
|
|
28755
28756
|
},
|
|
28756
28757
|
{
|
|
28757
28758
|
name: "editor_run",
|
|
28758
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for.",
|
|
28759
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
28759
28760
|
parameters: {
|
|
28760
28761
|
projectPath: PROJECT_PATH,
|
|
28761
28762
|
scene: {
|
|
@@ -28831,7 +28832,7 @@ var TOOL_SPECS = [
|
|
|
28831
28832
|
},
|
|
28832
28833
|
{
|
|
28833
28834
|
name: "editor_rescan",
|
|
28834
|
-
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
|
|
28835
|
+
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.",
|
|
28835
28836
|
parameters: {
|
|
28836
28837
|
projectPath: PROJECT_PATH,
|
|
28837
28838
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." }
|
|
@@ -29123,7 +29124,7 @@ var TOOL_SPECS = [
|
|
|
29123
29124
|
says: {
|
|
29124
29125
|
type: "string",
|
|
29125
29126
|
ops: ["until"],
|
|
29126
|
-
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included."
|
|
29127
|
+
description: "until: wait for these words to appear anywhere under nodePath instead of for a property, which is how a panel that rebuilds its labels is waited on at all: the labels are named afresh each redraw and the panel is what stays put. Case-insensitive, part of a line, hidden nodes included. Instead of, not as well as: a call carrying this and a property is refused, because they ask about different things and answering one of them silently is how a caller watches a screen believing they are watching a property."
|
|
29127
29128
|
},
|
|
29128
29129
|
timeoutMs: {
|
|
29129
29130
|
type: "number",
|
|
@@ -30641,6 +30642,7 @@ class GodotServer {
|
|
|
30641
30642
|
};
|
|
30642
30643
|
}));
|
|
30643
30644
|
const playing = await this.editorPlayingState();
|
|
30645
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
30644
30646
|
return this.jsonTextResponse({
|
|
30645
30647
|
editor: {
|
|
30646
30648
|
...this.getEditorStatusPayload(),
|
|
@@ -30851,6 +30853,7 @@ class GodotServer {
|
|
|
30851
30853
|
through: "gdharness",
|
|
30852
30854
|
pid: started.process.pid ?? null,
|
|
30853
30855
|
arguments: cmdArgs,
|
|
30856
|
+
transcript: started.transcript,
|
|
30854
30857
|
refreshedClasses: refreshed.value,
|
|
30855
30858
|
endedPreviousRun: ended ?? undefined,
|
|
30856
30859
|
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs),
|
|
@@ -31075,6 +31078,33 @@ class GodotServer {
|
|
|
31075
31078
|
closeSync(handle);
|
|
31076
31079
|
}
|
|
31077
31080
|
}
|
|
31081
|
+
async pickUpWhatTheEditorIsPlaying() {
|
|
31082
|
+
if (this.activeProcess !== null || !this.godotBridge.isConnected()) {
|
|
31083
|
+
return;
|
|
31084
|
+
}
|
|
31085
|
+
const playing = await this.editorPlayingState();
|
|
31086
|
+
if (playing?.playing !== true) {
|
|
31087
|
+
return;
|
|
31088
|
+
}
|
|
31089
|
+
try {
|
|
31090
|
+
await this.dap().connect();
|
|
31091
|
+
} catch (error) {
|
|
31092
|
+
this.logDebug(`Picked up an editor-played run without its adapter: ${errorMessage(error)}`);
|
|
31093
|
+
}
|
|
31094
|
+
this.activeProcess = {
|
|
31095
|
+
process: null,
|
|
31096
|
+
pid: null,
|
|
31097
|
+
log: new GameLog,
|
|
31098
|
+
transcript: null,
|
|
31099
|
+
readOffset: 0,
|
|
31100
|
+
projectPath: this.godotBridge.getStatus().projectPath ?? null,
|
|
31101
|
+
startedAt: Date.now(),
|
|
31102
|
+
exitCode: null,
|
|
31103
|
+
throughEditor: true,
|
|
31104
|
+
brokeOn: null,
|
|
31105
|
+
pickedUpPlaying: true
|
|
31106
|
+
};
|
|
31107
|
+
}
|
|
31078
31108
|
adoptRecordedRun() {
|
|
31079
31109
|
const record = readRunRecord();
|
|
31080
31110
|
if (record === null || !this.couldBeOurs(record.projectPath)) {
|
|
@@ -31156,6 +31186,7 @@ class GodotServer {
|
|
|
31156
31186
|
});
|
|
31157
31187
|
}
|
|
31158
31188
|
async handleWaitForRun(args) {
|
|
31189
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31159
31190
|
const run = this.currentRun();
|
|
31160
31191
|
if (!run) {
|
|
31161
31192
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -31170,6 +31201,7 @@ class GodotServer {
|
|
|
31170
31201
|
return await this.handleGetDebugOutput(args, stillRunning(run) ? budgetMs : undefined);
|
|
31171
31202
|
}
|
|
31172
31203
|
async handleGetDebugOutput(args, waitedMs) {
|
|
31204
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31173
31205
|
const run = this.currentRun();
|
|
31174
31206
|
if (!run) {
|
|
31175
31207
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -31197,6 +31229,9 @@ class GodotServer {
|
|
|
31197
31229
|
if (run.endedUnwatched === true) {
|
|
31198
31230
|
notes.push("This run outlived the server that started it and is over now, so its exit code was never collected. Everything it printed is below, read back from its transcript.");
|
|
31199
31231
|
}
|
|
31232
|
+
if (run.pickedUpPlaying === true) {
|
|
31233
|
+
notes.push("The editor was already playing this when this server reached it, so this is not the run from its start: what it printed before that went with the server that was listening then. It is the run the editor is holding now, which is the one the debug_* tools answer for.");
|
|
31234
|
+
}
|
|
31200
31235
|
if (typeof run.endedHere === "string") {
|
|
31201
31236
|
notes.push(`This run was ended here, by ${run.endedHere}.`);
|
|
31202
31237
|
} else if (!stillRunning(run) && !run.throughEditor && run.exitCode !== null) {
|
|
@@ -31227,6 +31262,7 @@ class GodotServer {
|
|
|
31227
31262
|
});
|
|
31228
31263
|
}
|
|
31229
31264
|
async handleStopProject() {
|
|
31265
|
+
await this.pickUpWhatTheEditorIsPlaying();
|
|
31230
31266
|
const stopped = this.currentRun();
|
|
31231
31267
|
if (!stopped) {
|
|
31232
31268
|
return this.createErrorResponse(this.nothingOfOursIsRunning());
|
|
@@ -31252,7 +31288,7 @@ class GodotServer {
|
|
|
31252
31288
|
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
31253
31289
|
const before = projectPath === "" ? null : cachedClasses(projectPath);
|
|
31254
31290
|
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
31255
|
-
const blind =
|
|
31291
|
+
const blind = await this.classesTheEditorCannotSee(args);
|
|
31256
31292
|
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
31257
31293
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
31258
31294
|
if (first.isError) {
|
|
@@ -31265,7 +31301,7 @@ class GodotServer {
|
|
|
31265
31301
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
31266
31302
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
31267
31303
|
}
|
|
31268
|
-
if (!busy && atRisk.length > 0) {
|
|
31304
|
+
if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
|
|
31269
31305
|
const until = Date.now() + CACHE_WRITE_MS;
|
|
31270
31306
|
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
31271
31307
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -31273,18 +31309,33 @@ class GodotServer {
|
|
|
31273
31309
|
}
|
|
31274
31310
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
31275
31311
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
31312
|
+
const restored = lost.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
31276
31313
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
31277
31314
|
const unseen = checked.unseen;
|
|
31315
|
+
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
31278
31316
|
return this.jsonTextResponse({
|
|
31279
|
-
ok: !busy && unseen.length === 0 &&
|
|
31317
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
31280
31318
|
stillWorking: busy,
|
|
31281
31319
|
waitedMs: Date.now() - started,
|
|
31282
31320
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
31283
31321
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
31322
|
+
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
31284
31323
|
classesUnchecked: checked.unchecked,
|
|
31285
|
-
note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." :
|
|
31324
|
+
note: busy ? "The editor was still scanning or importing when the wait ran out, so new files may not be visible yet." : stillGone.length > 0 ? "The scan wrote the class cache from the list this editor is holding, that list is shorter than the file was, and rebuilding the cache from the files did not bring these back. Every engine that reads the cache next, including a test run, will report them as unknown identifiers. Restart the editor with editor_launch restart before scanning again." : lost.length > 0 ? "The scan wrote the class cache from the list this editor is holding, which is shorter than the file was, so the cache was rebuilt from the files and these classes are back in it. Nothing was lost, but this editor is still holding the short list: do not rescan again until it has been restarted with editor_launch restart, because it will write the same list over the file each time." : 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
|
|
31286
31325
|
});
|
|
31287
31326
|
}
|
|
31327
|
+
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
31328
|
+
if (projectPath === "") {
|
|
31329
|
+
return [];
|
|
31330
|
+
}
|
|
31331
|
+
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
31332
|
+
if (!rebuilt.ok) {
|
|
31333
|
+
this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
|
|
31334
|
+
return [];
|
|
31335
|
+
}
|
|
31336
|
+
const now = cachedClasses(projectPath);
|
|
31337
|
+
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
31338
|
+
}
|
|
31288
31339
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
31289
31340
|
const first = answered.content[0];
|
|
31290
31341
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.7",
|
|
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",
|