gdharness 1.0.0 → 1.0.2
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/README.md +2 -3
- package/build/cli.js +92 -24
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +60 -6
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +42 -6
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +16 -9
- package/build/godot/operations/classdb_queries.gd +75 -0
- package/build/index.js +88 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,9 +26,8 @@ property reads right now, what the console printed, what broke and on which line
|
|
|
26
26
|
Hand this to your agent:
|
|
27
27
|
|
|
28
28
|
```text
|
|
29
|
-
Install gdharness
|
|
30
|
-
|
|
31
|
-
recommend back to me.
|
|
29
|
+
Install gdharness here by following https://aureliolo.github.io/gdharness/agent.md,
|
|
30
|
+
then tell me what it asked you to recommend back to me.
|
|
32
31
|
```
|
|
33
32
|
|
|
34
33
|
Or do it yourself, and pick your harness:
|
package/build/cli.js
CHANGED
|
@@ -7673,6 +7673,9 @@ function cacheWrittenAt(projectPath) {
|
|
|
7673
7673
|
return null;
|
|
7674
7674
|
}
|
|
7675
7675
|
}
|
|
7676
|
+
function cachedAtMissingPaths(cached, projectPath) {
|
|
7677
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync6(join6(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
7678
|
+
}
|
|
7676
7679
|
function staleAgainst(cached, projectPath) {
|
|
7677
7680
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
7678
7681
|
}
|
|
@@ -16084,7 +16087,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16084
16087
|
},
|
|
16085
16088
|
{
|
|
16086
16089
|
name: "editor_rescan",
|
|
16087
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is 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 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.",
|
|
16090
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is 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 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.",
|
|
16088
16091
|
parameters: {
|
|
16089
16092
|
projectPath: PROJECT_PATH,
|
|
16090
16093
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
@@ -16120,12 +16123,20 @@ var init_tool_definitions = __esm(() => {
|
|
|
16120
16123
|
},
|
|
16121
16124
|
instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
|
|
16122
16125
|
className: { type: "string", description: "info, inheritance: the class." },
|
|
16123
|
-
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." }
|
|
16126
|
+
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
|
|
16127
|
+
member: {
|
|
16128
|
+
type: "string",
|
|
16129
|
+
ops: ["info"],
|
|
16130
|
+
description: 'info: one member by name, a method, property, signal, enum or constant, with its signature and which class declares it, looked for on the class and every ancestor. The whole of Control is seventy thousand characters; the question "does this engine have it, and what does it take" is this. A name nothing declares is refused with the members whose names contain it.'
|
|
16131
|
+
}
|
|
16124
16132
|
},
|
|
16125
16133
|
requires: ["projectPath"],
|
|
16126
16134
|
operations: {
|
|
16127
16135
|
query: { summary: "classes matching a filter or category", requires: [] },
|
|
16128
|
-
info: {
|
|
16136
|
+
info: {
|
|
16137
|
+
summary: "methods, properties, signals and enums of one class, or one member of it by name",
|
|
16138
|
+
requires: ["className"]
|
|
16139
|
+
},
|
|
16129
16140
|
inheritance: { summary: "ancestors and descendants of one class", requires: ["className"] }
|
|
16130
16141
|
},
|
|
16131
16142
|
defaultOperation: "query"
|
|
@@ -16150,7 +16161,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
16150
16161
|
includeProperties: {
|
|
16151
16162
|
type: "boolean",
|
|
16152
16163
|
ops: ["tree"],
|
|
16153
|
-
description: "tree: include each node
|
|
16164
|
+
description: "tree: include every stored property of each node. Default false, and one row of a form answered seventy-six thousand characters with it; properties names the few that are wanted."
|
|
16165
|
+
},
|
|
16166
|
+
properties: {
|
|
16167
|
+
type: "array",
|
|
16168
|
+
items: { type: "string" },
|
|
16169
|
+
ops: ["tree"],
|
|
16170
|
+
description: "tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find."
|
|
16154
16171
|
},
|
|
16155
16172
|
className: {
|
|
16156
16173
|
type: "string",
|
|
@@ -16339,12 +16356,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
16339
16356
|
relativeX: {
|
|
16340
16357
|
type: "number",
|
|
16341
16358
|
ops: ["mouse_motion"],
|
|
16342
|
-
description: "mouse_motion: movement
|
|
16359
|
+
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the last injected pointer event put the pointer, and none for the first; that is what a control that drags reads. Give it to send a motion the position does not show."
|
|
16343
16360
|
},
|
|
16344
16361
|
relativeY: {
|
|
16345
16362
|
type: "number",
|
|
16346
16363
|
ops: ["mouse_motion"],
|
|
16347
|
-
description: "mouse_motion:
|
|
16364
|
+
description: "mouse_motion: as relativeX."
|
|
16348
16365
|
}
|
|
16349
16366
|
},
|
|
16350
16367
|
requires: [],
|
|
@@ -16358,13 +16375,22 @@ var init_tool_definitions = __esm(() => {
|
|
|
16358
16375
|
requires: ["nodePath"]
|
|
16359
16376
|
},
|
|
16360
16377
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
16361
|
-
key: {
|
|
16378
|
+
key: {
|
|
16379
|
+
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there. The limit is that menu's own navigation, not windows in general: an embedded dialog whose owner reads keys in _input gets them, and a binding catcher up in one takes the key first time",
|
|
16380
|
+
requires: ["keycode"]
|
|
16381
|
+
},
|
|
16362
16382
|
text: {
|
|
16363
16383
|
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
16364
16384
|
requires: ["text"]
|
|
16365
16385
|
},
|
|
16366
|
-
mouse_click: {
|
|
16367
|
-
|
|
16386
|
+
mouse_click: {
|
|
16387
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
16388
|
+
requires: ["x", "y"]
|
|
16389
|
+
},
|
|
16390
|
+
mouse_motion: {
|
|
16391
|
+
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
16392
|
+
requires: ["x", "y"]
|
|
16393
|
+
}
|
|
16368
16394
|
}
|
|
16369
16395
|
},
|
|
16370
16396
|
{
|
|
@@ -16385,7 +16411,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
16385
16411
|
says: {
|
|
16386
16412
|
type: "string",
|
|
16387
16413
|
ops: ["until"],
|
|
16388
|
-
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,
|
|
16414
|
+
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, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. 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."
|
|
16415
|
+
},
|
|
16416
|
+
includeHidden: {
|
|
16417
|
+
type: "boolean",
|
|
16418
|
+
ops: ["until"],
|
|
16419
|
+
description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
|
|
16389
16420
|
},
|
|
16390
16421
|
timeoutMs: {
|
|
16391
16422
|
type: "number",
|
|
@@ -37449,13 +37480,16 @@ function stillRunning(run) {
|
|
|
37449
37480
|
}
|
|
37450
37481
|
return true;
|
|
37451
37482
|
}
|
|
37452
|
-
function runIsUp(run, editorSays) {
|
|
37483
|
+
function runIsUp(run, editorSays, now = Date.now()) {
|
|
37453
37484
|
if (run?.throughEditor !== true) {
|
|
37454
37485
|
return stillRunning(run);
|
|
37455
37486
|
}
|
|
37456
37487
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
37457
37488
|
return false;
|
|
37458
37489
|
}
|
|
37490
|
+
if (editorSays === false && run.seenPlaying !== true && now - run.startedAt < PLAY_STARTS_WITHIN_MS) {
|
|
37491
|
+
return true;
|
|
37492
|
+
}
|
|
37459
37493
|
return editorSays ?? stillRunning(run);
|
|
37460
37494
|
}
|
|
37461
37495
|
function endedPreviousRun(ended) {
|
|
@@ -37529,10 +37563,11 @@ function runtimeVerdict(endpoint, after) {
|
|
|
37529
37563
|
heldAt: after.heldAt
|
|
37530
37564
|
};
|
|
37531
37565
|
}
|
|
37566
|
+
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
37532
37567
|
return {
|
|
37533
37568
|
listening: false,
|
|
37534
37569
|
mayYetAnnounce: after.running,
|
|
37535
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and
|
|
37570
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
37536
37571
|
heldAt: null
|
|
37537
37572
|
};
|
|
37538
37573
|
}
|
|
@@ -38221,7 +38256,8 @@ class GodotServer {
|
|
|
38221
38256
|
...whichGame(args),
|
|
38222
38257
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
38223
38258
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
38224
|
-
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
38259
|
+
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
38260
|
+
properties: readArray(args, "properties") ?? []
|
|
38225
38261
|
});
|
|
38226
38262
|
case "find":
|
|
38227
38263
|
return await this.handleFindRuntimeNodes(args);
|
|
@@ -39436,7 +39472,8 @@ class GodotServer {
|
|
|
39436
39472
|
budgetMs,
|
|
39437
39473
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
39438
39474
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
39439
|
-
withArgs
|
|
39475
|
+
withArgs,
|
|
39476
|
+
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
39440
39477
|
});
|
|
39441
39478
|
}
|
|
39442
39479
|
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
@@ -39466,6 +39503,7 @@ class GodotServer {
|
|
|
39466
39503
|
const startedAt = Date.now();
|
|
39467
39504
|
const transcript = openTranscript(startedAt);
|
|
39468
39505
|
closeSync(transcript.fd);
|
|
39506
|
+
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
39469
39507
|
const played = {
|
|
39470
39508
|
process: null,
|
|
39471
39509
|
pid: null,
|
|
@@ -39477,7 +39515,8 @@ class GodotServer {
|
|
|
39477
39515
|
exitCode: null,
|
|
39478
39516
|
throughEditor: true,
|
|
39479
39517
|
brokeOn: null,
|
|
39480
|
-
announcedBefore: alreadyPlaying
|
|
39518
|
+
announcedBefore: alreadyPlaying,
|
|
39519
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
39481
39520
|
};
|
|
39482
39521
|
this.activeProcess = played;
|
|
39483
39522
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -39487,7 +39526,7 @@ class GodotServer {
|
|
|
39487
39526
|
through: "editor",
|
|
39488
39527
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
39489
39528
|
refreshedClasses,
|
|
39490
|
-
debugPort: readNumber(
|
|
39529
|
+
debugPort: readNumber(playAnswer, "debugPort"),
|
|
39491
39530
|
endedPreviousRun: endedPreviousRun(ended),
|
|
39492
39531
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
39493
39532
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
@@ -40013,7 +40052,11 @@ class GodotServer {
|
|
|
40013
40052
|
if (announced !== undefined && editorSays === undefined) {
|
|
40014
40053
|
return stillRunning(run);
|
|
40015
40054
|
}
|
|
40016
|
-
|
|
40055
|
+
const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
|
|
40056
|
+
if (said === true) {
|
|
40057
|
+
run.seenPlaying = true;
|
|
40058
|
+
}
|
|
40059
|
+
return runIsUp(run, said);
|
|
40017
40060
|
}
|
|
40018
40061
|
async handleGetDebugOutput(args, waitedMs) {
|
|
40019
40062
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -40196,6 +40239,7 @@ class GodotServer {
|
|
|
40196
40239
|
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
40197
40240
|
const blind = await this.classesTheEditorCannotSee(args);
|
|
40198
40241
|
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
40242
|
+
const ghosts = blind.stillHeld ?? [];
|
|
40199
40243
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
40200
40244
|
if (first.isError) {
|
|
40201
40245
|
return first;
|
|
@@ -40207,7 +40251,7 @@ class GodotServer {
|
|
|
40207
40251
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
40208
40252
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
40209
40253
|
}
|
|
40210
|
-
if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
|
|
40254
|
+
if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
|
|
40211
40255
|
const until = Date.now() + CACHE_WRITE_MS;
|
|
40212
40256
|
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
40213
40257
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -40215,7 +40259,11 @@ class GodotServer {
|
|
|
40215
40259
|
}
|
|
40216
40260
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
40217
40261
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
40218
|
-
const
|
|
40262
|
+
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
40263
|
+
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
40264
|
+
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
40265
|
+
const dropped = rebuilt === null ? [] : atMissingPaths.filter((name) => !cachedAtMissingPaths(rebuilt, projectPath).includes(name));
|
|
40266
|
+
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
40219
40267
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
40220
40268
|
let reloaded = {};
|
|
40221
40269
|
if (reloading !== undefined) {
|
|
@@ -40231,8 +40279,23 @@ class GodotServer {
|
|
|
40231
40279
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
40232
40280
|
const unseen = checked.unseen;
|
|
40233
40281
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
40282
|
+
const notes = [];
|
|
40283
|
+
if (busy) {
|
|
40284
|
+
notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
|
|
40285
|
+
} else if (stillGone.length > 0) {
|
|
40286
|
+
notes.push("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.");
|
|
40287
|
+
} else if (lost.length > 0) {
|
|
40288
|
+
notes.push("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.");
|
|
40289
|
+
} else if (unseen.length > 0) {
|
|
40290
|
+
notes.push("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. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
|
|
40291
|
+
}
|
|
40292
|
+
if (stillAtMissingPaths.length > 0) {
|
|
40293
|
+
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.`);
|
|
40294
|
+
} else if (dropped.length > 0) {
|
|
40295
|
+
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.`);
|
|
40296
|
+
}
|
|
40234
40297
|
return this.jsonTextResponse({
|
|
40235
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
40298
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
|
|
40236
40299
|
stillWorking: busy,
|
|
40237
40300
|
waitedMs: Date.now() - started,
|
|
40238
40301
|
heldBeforeReload: reloaded.heldBefore,
|
|
@@ -40241,8 +40304,10 @@ class GodotServer {
|
|
|
40241
40304
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
40242
40305
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
40243
40306
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
40307
|
+
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
40308
|
+
cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
|
|
40244
40309
|
classesUnchecked: checked.unchecked,
|
|
40245
|
-
note:
|
|
40310
|
+
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
40246
40311
|
});
|
|
40247
40312
|
}
|
|
40248
40313
|
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
@@ -40483,6 +40548,7 @@ class GodotServer {
|
|
|
40483
40548
|
property: readString(args, "property") ?? "",
|
|
40484
40549
|
value: args["value"],
|
|
40485
40550
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
40551
|
+
...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
|
|
40486
40552
|
timeout_ms: timeoutMs
|
|
40487
40553
|
}, patience);
|
|
40488
40554
|
}
|
|
@@ -40491,7 +40557,7 @@ async function runGodotServer() {
|
|
|
40491
40557
|
const server = new GodotServer;
|
|
40492
40558
|
await server.run();
|
|
40493
40559
|
}
|
|
40494
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, CONSOLE_WRAPPER, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40560
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, CONSOLE_WRAPPER, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CACHE_WRITE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
40495
40561
|
var init_server3 = __esm(() => {
|
|
40496
40562
|
init_mcp();
|
|
40497
40563
|
init_stdio2();
|
|
@@ -42080,11 +42146,13 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
42080
42146
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows, then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
42081
42147
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
42082
42148
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
42083
|
-
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
42149
|
+
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says, rather than a click and keys: the menu opens as a window of its own that an injected key never reaches, so Enter closes it with nothing chosen |
|
|
42084
42150
|
| Answer a dialog | \`runtime_input click\` on its button, or \`key\` Escape to dismiss it: an [AcceptDialog] reads that key itself and never asks the InputMap, so \`ui_cancel\` leaves it standing |
|
|
42085
42151
|
| Press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
42086
42152
|
| Where a 3D node is on screen | \`runtime_inspect\` \`rect\`, rather than unprojecting by hand |
|
|
42087
|
-
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels |
|
|
42153
|
+
| Wait for something in a running game | \`runtime_wait\`, never a sleep; \`until\` with \`says\` for a panel that rebuilds its own labels. The words have to be shown: a button that exists hidden through an animation does not satisfy it until it appears, unless \`includeHidden\` says otherwise |
|
|
42154
|
+
| What every label on a screen says | \`runtime_inspect tree\` with \`properties\` naming \`text\`, which reads it off every node that has one; \`includeProperties\` is every stored property of every node and ran to seventy-six thousand characters for one row |
|
|
42155
|
+
| Whether this engine has a method or property, and what it takes | \`editor_classes info\` with \`member\`, which answers that one member and which class declares it rather than the whole class |
|
|
42088
42156
|
| Wait for a run to finish | \`editor_run wait\`, never a sleep or a shell loop on the pid: it answers as \`editor_output\` does, once the run is over |
|
|
42089
42157
|
| End a bench and the workers it opened | \`editor_run stop\` with \`andChildren\`, never a process listing by scene name: it ends every process under the run's, to any depth, that is a game of the project, announced as one or the project's engine run with \`--path\` on it, names them under \`endedChildren\` with what identified each, and leaves and names any other |
|
|
42090
42158
|
| A picture, for a person who asked to see one | \`runtime_capture\` |
|
|
@@ -25,12 +25,31 @@ const HEADLESS_VIEWPORT: Vector2 = Vector2(64, 64)
|
|
|
25
25
|
var _host: Node
|
|
26
26
|
var _values: Values
|
|
27
27
|
|
|
28
|
+
## Where the last injected pointer event put the pointer, once one has.
|
|
29
|
+
##
|
|
30
|
+
## Kept here rather than asked of the viewport: the root viewport answers get_mouse_position
|
|
31
|
+
## from the operating system's pointer, which an injected event never moves, so on a desktop it
|
|
32
|
+
## is wherever the user's mouse is, another monitor included, and every motion sent through the
|
|
33
|
+
## bridge carried the distance from there. Only a headless run, with no pointer to read, followed
|
|
34
|
+
## the injected events, which is why that reading measured right and a played game read it wrong.
|
|
35
|
+
var _pointer: Vector2 = Vector2.ZERO
|
|
36
|
+
var _pointer_placed: bool = false
|
|
37
|
+
|
|
28
38
|
|
|
29
39
|
func _init(host: Node, values: Values) -> void:
|
|
30
40
|
_host = host
|
|
31
41
|
_values = values
|
|
32
42
|
|
|
33
43
|
|
|
44
|
+
## The movement a pointer arriving at [param position] carries, which is the distance from where
|
|
45
|
+
## the last injected event put it, and none at all for the first. The arrival is recorded.
|
|
46
|
+
func _arrive(position: Vector2) -> Vector2:
|
|
47
|
+
var relative: Vector2 = position - _pointer if _pointer_placed else Vector2.ZERO
|
|
48
|
+
_pointer = position
|
|
49
|
+
_pointer_placed = true
|
|
50
|
+
return relative
|
|
51
|
+
|
|
52
|
+
|
|
34
53
|
## Presses [param params].action, and lets go of it again unless asked to hold it.
|
|
35
54
|
##
|
|
36
55
|
## The whole press by default, for the reason [method click] is the whole click: an action left
|
|
@@ -329,6 +348,9 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
329
348
|
var pressed: bool = Read.as_bool(params.get("pressed", true), true)
|
|
330
349
|
var double: bool = Read.as_bool(params.get("doubleClick", false))
|
|
331
350
|
|
|
351
|
+
# A button pressed somewhere puts the pointer there: the motion after a held press measures
|
|
352
|
+
# its drag from the press, which is where a real pointer would be.
|
|
353
|
+
var _moved: Vector2 = _arrive(position)
|
|
332
354
|
Input.parse_input_event(_button(position, button, pressed, double))
|
|
333
355
|
|
|
334
356
|
return {
|
|
@@ -341,15 +363,29 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
341
363
|
}
|
|
342
364
|
|
|
343
365
|
|
|
366
|
+
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
367
|
+
## last injected event put it unless the caller says otherwise.
|
|
368
|
+
##
|
|
369
|
+
## A real pointer never arrives without a `relative`, and a control that drags reads that field
|
|
370
|
+
## rather than the position, so a motion carrying none leaves a grip where it was however far the
|
|
371
|
+
## position moved. The distance from where the bridge last put the pointer is what a real move
|
|
372
|
+
## would have carried. A relative the caller gives is kept, since a game may want to read a motion
|
|
373
|
+
## the position does not show.
|
|
344
374
|
func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
345
375
|
var point: Variant = _read_point(params, "x", "y", "position")
|
|
346
376
|
if point is String:
|
|
347
377
|
return {"type": "error", "message": point}
|
|
348
378
|
var position: Vector2 = point
|
|
349
|
-
var
|
|
350
|
-
if
|
|
351
|
-
|
|
352
|
-
|
|
379
|
+
var relative: Vector2 = _arrive(position)
|
|
380
|
+
if params.has("relativeX") or params.has("relativeY"):
|
|
381
|
+
relative = Vector2(
|
|
382
|
+
Read.as_float(params.get("relativeX", 0.0)), Read.as_float(params.get("relativeY", 0.0))
|
|
383
|
+
)
|
|
384
|
+
elif params.has("relative"):
|
|
385
|
+
var movement: Variant = _read_point(params, "relativeX", "relativeY", "relative")
|
|
386
|
+
if movement is String:
|
|
387
|
+
return {"type": "error", "message": movement}
|
|
388
|
+
relative = movement
|
|
353
389
|
|
|
354
390
|
Input.parse_input_event(_motion(position, relative))
|
|
355
391
|
|
|
@@ -502,7 +538,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
502
538
|
# Pushed into the viewport rather than through Input: Input accumulates events and flushes
|
|
503
539
|
# them at the next frame, so the hovered control read below would be the one from before
|
|
504
540
|
# the pointer moved. The viewport delivers it to the GUI the same way a real one arrives.
|
|
505
|
-
viewport.push_input(_motion(position,
|
|
541
|
+
viewport.push_input(_motion(position, _arrive(position)))
|
|
506
542
|
# What the engine itself thinks is under the pointer, which is the answer to "did it land",
|
|
507
543
|
# read before the press so the caller learns about a control on top rather than a click
|
|
508
544
|
# that went to it. Read in full here, path included, because nothing about that control
|
|
@@ -762,7 +798,7 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
762
798
|
return _no_such_button(params.get("button"))
|
|
763
799
|
var double: bool = Read.as_bool(params.get("double", false))
|
|
764
800
|
|
|
765
|
-
viewport.push_input(_motion(position,
|
|
801
|
+
viewport.push_input(_motion(position, _arrive(position)))
|
|
766
802
|
# Read before the press, for the reason the Control click reads it: what the caller needs to
|
|
767
803
|
# know is whether a panel is sitting over the room, and the press is what would change it.
|
|
768
804
|
var hovered: Control = viewport.gui_get_hovered_control()
|
|
@@ -790,14 +826,21 @@ func _click_in_the_world(node_path: String, item: Node3D, params: Dictionary) ->
|
|
|
790
826
|
}
|
|
791
827
|
|
|
792
828
|
|
|
829
|
+
## The buttons a real pointer event carries are the ones held as it happens, and Input keeps that
|
|
830
|
+
## from every button event it is given, injected ones included, so a motion between a held click
|
|
831
|
+
## and its release is a drag to a control that reads the mask rather than remembering the click.
|
|
793
832
|
func _motion(position: Vector2, relative: Vector2) -> InputEventMouseMotion:
|
|
794
833
|
var event: InputEventMouseMotion = InputEventMouseMotion.new()
|
|
795
834
|
event.position = position
|
|
796
835
|
event.global_position = position
|
|
797
836
|
event.relative = relative
|
|
837
|
+
event.screen_relative = relative
|
|
838
|
+
event.button_mask = Input.get_mouse_button_mask()
|
|
798
839
|
return event
|
|
799
840
|
|
|
800
841
|
|
|
842
|
+
## A button event carries the buttons held once it has happened: the pressed one among them, the
|
|
843
|
+
## released one out. A wheel step is a button with no mask, as it is from a real mouse.
|
|
801
844
|
func _button(position: Vector2, button: int, pressed: bool, double: bool) -> InputEventMouseButton:
|
|
802
845
|
var event: InputEventMouseButton = InputEventMouseButton.new()
|
|
803
846
|
event.position = position
|
|
@@ -805,9 +848,20 @@ func _button(position: Vector2, button: int, pressed: bool, double: bool) -> Inp
|
|
|
805
848
|
event.button_index = button as MouseButton
|
|
806
849
|
event.pressed = pressed
|
|
807
850
|
event.double_click = double
|
|
851
|
+
var held: int = Input.get_mouse_button_mask()
|
|
852
|
+
var own: int = _mask_of(button)
|
|
853
|
+
event.button_mask = (held | own) if pressed else (held & ~own)
|
|
808
854
|
return event
|
|
809
855
|
|
|
810
856
|
|
|
857
|
+
## The mask bit for a button, which is the engine's own mapping: left 1, right 2, middle 4, the
|
|
858
|
+
## two extra buttons 128 and 256. The wheel steps between have none.
|
|
859
|
+
func _mask_of(button: int) -> int:
|
|
860
|
+
if button >= MOUSE_BUTTON_WHEEL_UP and button <= MOUSE_BUTTON_WHEEL_RIGHT:
|
|
861
|
+
return 0
|
|
862
|
+
return 1 << (button - 1)
|
|
863
|
+
|
|
864
|
+
|
|
811
865
|
## What to say about a name that is not a mouse button, with the ones that are.
|
|
812
866
|
func _no_such_button(named: Variant) -> Dictionary:
|
|
813
867
|
return {
|
|
@@ -30,17 +30,33 @@ func _init(host: Node, values: Values) -> void:
|
|
|
30
30
|
_values = values
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
## The tree under a node, to a depth, with every stored property on each node when asked for all
|
|
34
|
+
## of them or the named ones when a list is given.
|
|
35
|
+
##
|
|
36
|
+
## The list is the shape most questions about a screen take: the `text` of every label and
|
|
37
|
+
## button under a panel. All of them for one row of a form answered seventy-six thousand
|
|
38
|
+
## characters, and the same question as a find with one property answered two.
|
|
33
39
|
func get_tree(params: Dictionary) -> Dictionary:
|
|
34
40
|
var root_path: String = str(params.get("root", "/root"))
|
|
35
41
|
var max_depth: int = Read.as_int(params.get("depth", 3), 3)
|
|
36
42
|
var include_properties: bool = Read.as_bool(params.get("include_properties", false))
|
|
43
|
+
var named: Array[String] = []
|
|
44
|
+
var listed: Variant = params.get("properties", [])
|
|
45
|
+
if listed is Array:
|
|
46
|
+
for each: Variant in listed:
|
|
47
|
+
named.append(str(each))
|
|
48
|
+
else:
|
|
49
|
+
return {"type": "error", "message": "properties must be a list of property names"}
|
|
37
50
|
|
|
38
51
|
var reached: Dictionary = Values.node_at(_host.get_tree().root, root_path)
|
|
39
52
|
if reached.has("message"):
|
|
40
53
|
return reached
|
|
41
54
|
var root: Node = reached["node"]
|
|
42
55
|
|
|
43
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
"type": "tree",
|
|
58
|
+
"root": _serialize_node_tree(root, 0, max_depth, include_properties, named),
|
|
59
|
+
}
|
|
44
60
|
|
|
45
61
|
|
|
46
62
|
## Nodes matching every filter given, as paths, so a caller can name what it wants without
|
|
@@ -91,7 +107,7 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
91
107
|
var node: Node = pending.pop_front()
|
|
92
108
|
var rest: bool = _matches_apart_from_name(node, wanted)
|
|
93
109
|
if rest and _named(node, wanted["name"]):
|
|
94
|
-
if not include_hidden and not
|
|
110
|
+
if not include_hidden and not shown(node):
|
|
95
111
|
hidden += 1
|
|
96
112
|
elif found.size() >= limit:
|
|
97
113
|
truncated = true
|
|
@@ -144,8 +160,8 @@ func find_nodes(params: Dictionary) -> Dictionary:
|
|
|
144
160
|
## a plain Node sitting between a hidden panel and a label has no visibility to ask about, and the
|
|
145
161
|
## label answers that it is visible while nothing of it is on screen. Walking up is what makes a
|
|
146
162
|
## row hidden because the panel holding it is hidden, which is what somebody checking a screen is
|
|
147
|
-
## asking about.
|
|
148
|
-
static func
|
|
163
|
+
## asking about. A wait on words asks the same, so it reads this too.
|
|
164
|
+
static func shown(node: Node) -> bool:
|
|
149
165
|
var walk: Node = node
|
|
150
166
|
while walk != null:
|
|
151
167
|
if not _drawn(walk):
|
|
@@ -846,8 +862,12 @@ func get_metrics(params: Dictionary) -> Dictionary:
|
|
|
846
862
|
return {"type": "metrics", "data": selected}
|
|
847
863
|
|
|
848
864
|
|
|
849
|
-
func _serialize_node_tree(
|
|
865
|
+
func _serialize_node_tree(
|
|
866
|
+
node: Node, depth: int, max_depth: int, include_properties: bool, named: Array[String]
|
|
867
|
+
) -> Dictionary:
|
|
850
868
|
var result: Dictionary = _serialize_node(node, include_properties)
|
|
869
|
+
if not named.is_empty():
|
|
870
|
+
result["properties"] = _named_properties_of(node, str(result["path"]), named)
|
|
851
871
|
|
|
852
872
|
if depth < max_depth:
|
|
853
873
|
var children: Array = []
|
|
@@ -856,12 +876,28 @@ func _serialize_node_tree(node: Node, depth: int, max_depth: int, include_proper
|
|
|
856
876
|
# it is wrong, and it is what sends somebody looking for another way to press the button.
|
|
857
877
|
# `depth` is what keeps the answer a size worth reading.
|
|
858
878
|
for child: Node in node.get_children(true):
|
|
859
|
-
children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties))
|
|
879
|
+
children.append(_serialize_node_tree(child, depth + 1, max_depth, include_properties, named))
|
|
860
880
|
result["children"] = children
|
|
861
881
|
|
|
862
882
|
return result
|
|
863
883
|
|
|
864
884
|
|
|
885
|
+
## The named properties a node has, read the way a find reads its one: through a colon path as
|
|
886
|
+
## well, and a name this node has not got is left out rather than answered null, since a tree
|
|
887
|
+
## holds nodes of every class and a label's `text` is not a container's.
|
|
888
|
+
func _named_properties_of(node: Node, path: String, named: Array[String]) -> Dictionary:
|
|
889
|
+
var properties: Dictionary = {}
|
|
890
|
+
for wanted: String in named:
|
|
891
|
+
var reached: Dictionary = _reached(node, path, wanted)
|
|
892
|
+
if reached.has("message"):
|
|
893
|
+
continue
|
|
894
|
+
var holder: Variant = reached["holder"]
|
|
895
|
+
var name: String = reached["name"]
|
|
896
|
+
if _can_read(holder, name):
|
|
897
|
+
properties[wanted] = _values.serialize(_read(holder, name))
|
|
898
|
+
return properties
|
|
899
|
+
|
|
900
|
+
|
|
865
901
|
func _serialize_node(node: Node, include_properties: bool) -> Dictionary:
|
|
866
902
|
var result: Dictionary = {"name": node.name, "type": node.get_class(), "path": str(node.get_path())}
|
|
867
903
|
|
|
@@ -115,7 +115,8 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
if not says.is_empty():
|
|
118
|
-
|
|
118
|
+
var include_hidden: bool = Read.as_bool(params.get("include_hidden", false))
|
|
119
|
+
return await _wait_until_said(node_path, says, timeout_ms, include_hidden)
|
|
119
120
|
if property.is_empty():
|
|
120
121
|
return {"type": "error", "message": "A property and a value, or says, are required"}
|
|
121
122
|
if not params.has("value"):
|
|
@@ -196,34 +197,40 @@ func wait_until(params: Dictionary) -> Dictionary:
|
|
|
196
197
|
## on a node that was freed a frame later, and that is what it answered: the date along the top of a
|
|
197
198
|
## hall could not be waited on at all. What a caller is watching for is a word arriving on a screen,
|
|
198
199
|
## and the screen is the part that stays put.
|
|
199
|
-
|
|
200
|
+
##
|
|
201
|
+
## On the screen, which is to say shown: a control carrying the words while hidden satisfied the
|
|
202
|
+
## wait at once, so a duel screen whose "Carry on" button exists hidden through the whole sweep
|
|
203
|
+
## answered met in two milliseconds while the sweep was still animating, and every wait on that
|
|
204
|
+
## screen fell back to counting frames. [param include_hidden] asks the other question, for a
|
|
205
|
+
## caller waiting on words a hidden node holds.
|
|
206
|
+
func _wait_until_said(node_path: String, said: String, timeout_ms: int, include_hidden: bool) -> Dictionary:
|
|
200
207
|
var started: int = Time.get_ticks_msec()
|
|
201
208
|
var words: String = Queries.as_said(said)
|
|
202
|
-
var found: bool = _anything_says(node_path, words)
|
|
209
|
+
var found: bool = _anything_says(node_path, words, include_hidden)
|
|
203
210
|
while not found and Time.get_ticks_msec() - started < timeout_ms:
|
|
204
211
|
await _host.get_tree().process_frame
|
|
205
|
-
found = _anything_says(node_path, words)
|
|
212
|
+
found = _anything_says(node_path, words, include_hidden)
|
|
206
213
|
|
|
207
214
|
return {
|
|
208
215
|
"type": "condition",
|
|
209
216
|
"path": node_path,
|
|
210
217
|
"says": said,
|
|
211
218
|
"met": found,
|
|
219
|
+
"include_hidden": include_hidden,
|
|
212
220
|
"elapsed_ms": Time.get_ticks_msec() - started,
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
|
|
216
|
-
## Whether anything under [param node_path] says [param said], the node itself included
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
func _anything_says(node_path: String, said: String) -> bool:
|
|
224
|
+
## Whether anything under [param node_path] says [param said], the node itself included, and shown
|
|
225
|
+
## unless hidden ones are wanted too.
|
|
226
|
+
func _anything_says(node_path: String, said: String, include_hidden: bool) -> bool:
|
|
220
227
|
var root: Node = _host.get_tree().root.get_node_or_null(node_path)
|
|
221
228
|
if root == null:
|
|
222
229
|
return false
|
|
223
230
|
var pending: Array[Node] = [root]
|
|
224
231
|
while not pending.is_empty():
|
|
225
232
|
var node: Node = pending.pop_back()
|
|
226
|
-
if Queries.said_by(node).containsn(said):
|
|
233
|
+
if Queries.said_by(node).containsn(said) and (include_hidden or Queries.shown(node)):
|
|
227
234
|
return true
|
|
228
235
|
pending.append_array(node.get_children(true))
|
|
229
236
|
return false
|
|
@@ -92,6 +92,10 @@ func query_class_info(params: Dictionary) -> Dictionary:
|
|
|
92
92
|
if not ClassDB.class_exists(class_name_str):
|
|
93
93
|
return _log.failure("Class not found: " + class_name_str)
|
|
94
94
|
|
|
95
|
+
var member: String = str(params.get("member", ""))
|
|
96
|
+
if not member.is_empty():
|
|
97
|
+
return _member_of(class_name_str, member)
|
|
98
|
+
|
|
95
99
|
var methods: Array[Dictionary] = _methods_of(class_name_str, include_inherited)
|
|
96
100
|
var properties: Array[Dictionary] = _properties_of(class_name_str, include_inherited)
|
|
97
101
|
var signals: Array[Dictionary] = _signals_of(class_name_str, include_inherited)
|
|
@@ -130,6 +134,77 @@ func query_class_info(params: Dictionary) -> Dictionary:
|
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
|
|
137
|
+
# One member of a class by name, wherever in its ancestry it is declared.
|
|
138
|
+
#
|
|
139
|
+
# The question a member is asked by name for is "does this engine have it, and what does it take":
|
|
140
|
+
# the whole class in answer to that ran to seventy thousand characters for Control, and the member
|
|
141
|
+
# is as often an ancestor's as the class's own, so the ancestry is walked without being asked. Each
|
|
142
|
+
# match says which class declares it. Nothing found names the members whose names contain the one
|
|
143
|
+
# asked for, since the usual miss is a spelling.
|
|
144
|
+
func _member_of(class_name_str: String, member: String) -> Dictionary:
|
|
145
|
+
var found: Array[Dictionary] = []
|
|
146
|
+
var current: String = class_name_str
|
|
147
|
+
while not current.is_empty():
|
|
148
|
+
for m: Dictionary in _methods_of(current, false):
|
|
149
|
+
if m["name"] == member:
|
|
150
|
+
found.append({"kind": "method", "declared_in": current, "member": m})
|
|
151
|
+
for p: Dictionary in _properties_of(current, false):
|
|
152
|
+
if p["name"] == member:
|
|
153
|
+
found.append({"kind": "property", "declared_in": current, "member": p})
|
|
154
|
+
for s: Dictionary in _signals_of(current, false):
|
|
155
|
+
if s["name"] == member:
|
|
156
|
+
found.append({"kind": "signal", "declared_in": current, "member": s})
|
|
157
|
+
for e: String in ClassDB.class_get_enum_list(current, true):
|
|
158
|
+
if e == member:
|
|
159
|
+
var values: Dictionary = {}
|
|
160
|
+
for c: String in ClassDB.class_get_enum_constants(current, e, true):
|
|
161
|
+
values[c] = ClassDB.class_get_integer_constant(current, c)
|
|
162
|
+
found.append(
|
|
163
|
+
{"kind": "enum", "declared_in": current, "member": {"name": e, "values": values}}
|
|
164
|
+
)
|
|
165
|
+
for c: String in ClassDB.class_get_integer_constant_list(current, true):
|
|
166
|
+
if c == member:
|
|
167
|
+
var value: int = ClassDB.class_get_integer_constant(current, c)
|
|
168
|
+
found.append(
|
|
169
|
+
{"kind": "constant", "declared_in": current, "member": {"name": c, "value": value}}
|
|
170
|
+
)
|
|
171
|
+
current = ClassDB.get_parent_class(current)
|
|
172
|
+
|
|
173
|
+
if found.is_empty():
|
|
174
|
+
var alike: Array[String] = _members_alike(class_name_str, member)
|
|
175
|
+
var hint: String = (
|
|
176
|
+
"" if alike.is_empty() else " Members with names containing it: " + ", ".join(alike) + "."
|
|
177
|
+
)
|
|
178
|
+
return _log.failure(
|
|
179
|
+
"%s and its ancestors declare no member named %s.%s" % [class_name_str, member, hint]
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return {"class_name": class_name_str, "member": member, "found": found}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# The names, across a class and its ancestors, that contain the asked-for one, case-insensitively.
|
|
186
|
+
func _members_alike(class_name_str: String, member: String) -> Array[String]:
|
|
187
|
+
var alike: Array[String] = []
|
|
188
|
+
var wanted: String = member.to_lower()
|
|
189
|
+
var current: String = class_name_str
|
|
190
|
+
while not current.is_empty():
|
|
191
|
+
for m: Dictionary in _methods_of(current, false):
|
|
192
|
+
var name: String = m["name"]
|
|
193
|
+
if name.to_lower().contains(wanted) and not alike.has(name):
|
|
194
|
+
alike.append(name)
|
|
195
|
+
for p: Dictionary in _properties_of(current, false):
|
|
196
|
+
var name: String = p["name"]
|
|
197
|
+
if name.to_lower().contains(wanted) and not alike.has(name):
|
|
198
|
+
alike.append(name)
|
|
199
|
+
for s: Dictionary in _signals_of(current, false):
|
|
200
|
+
var name: String = s["name"]
|
|
201
|
+
if name.to_lower().contains(wanted) and not alike.has(name):
|
|
202
|
+
alike.append(name)
|
|
203
|
+
current = ClassDB.get_parent_class(current)
|
|
204
|
+
alike.sort()
|
|
205
|
+
return alike
|
|
206
|
+
|
|
207
|
+
|
|
133
208
|
# Inspect class inheritance hierarchy
|
|
134
209
|
func inspect_inheritance(params: Dictionary) -> Dictionary:
|
|
135
210
|
var class_name_str: String = str(params.get("class_name", ""))
|
package/build/index.js
CHANGED
|
@@ -25052,6 +25052,9 @@ function cacheWrittenAt(projectPath) {
|
|
|
25052
25052
|
return null;
|
|
25053
25053
|
}
|
|
25054
25054
|
}
|
|
25055
|
+
function cachedAtMissingPaths(cached, projectPath) {
|
|
25056
|
+
return [...cached].filter(([, path]) => path.startsWith("res://") && !existsSync2(join4(projectPath, path.slice("res://".length)))).map(([name]) => name).sort();
|
|
25057
|
+
}
|
|
25055
25058
|
function staleAgainst(cached, projectPath) {
|
|
25056
25059
|
return [...declaredClasses(projectPath)].filter(([name, path]) => cached.get(name) !== path).map(([name]) => name);
|
|
25057
25060
|
}
|
|
@@ -29687,7 +29690,7 @@ var TOOL_SPECS = [
|
|
|
29687
29690
|
},
|
|
29688
29691
|
{
|
|
29689
29692
|
name: "editor_rescan",
|
|
29690
|
-
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is 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 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.",
|
|
29693
|
+
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is 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 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.",
|
|
29691
29694
|
parameters: {
|
|
29692
29695
|
projectPath: PROJECT_PATH,
|
|
29693
29696
|
timeoutMs: { type: "number", description: "How long to wait for the scan. Default 30000." },
|
|
@@ -29723,12 +29726,20 @@ var TOOL_SPECS = [
|
|
|
29723
29726
|
},
|
|
29724
29727
|
instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
|
|
29725
29728
|
className: { type: "string", description: "info, inheritance: the class." },
|
|
29726
|
-
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." }
|
|
29729
|
+
includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
|
|
29730
|
+
member: {
|
|
29731
|
+
type: "string",
|
|
29732
|
+
ops: ["info"],
|
|
29733
|
+
description: 'info: one member by name, a method, property, signal, enum or constant, with its signature and which class declares it, looked for on the class and every ancestor. The whole of Control is seventy thousand characters; the question "does this engine have it, and what does it take" is this. A name nothing declares is refused with the members whose names contain it.'
|
|
29734
|
+
}
|
|
29727
29735
|
},
|
|
29728
29736
|
requires: ["projectPath"],
|
|
29729
29737
|
operations: {
|
|
29730
29738
|
query: { summary: "classes matching a filter or category", requires: [] },
|
|
29731
|
-
info: {
|
|
29739
|
+
info: {
|
|
29740
|
+
summary: "methods, properties, signals and enums of one class, or one member of it by name",
|
|
29741
|
+
requires: ["className"]
|
|
29742
|
+
},
|
|
29732
29743
|
inheritance: { summary: "ancestors and descendants of one class", requires: ["className"] }
|
|
29733
29744
|
},
|
|
29734
29745
|
defaultOperation: "query"
|
|
@@ -29753,7 +29764,13 @@ var TOOL_SPECS = [
|
|
|
29753
29764
|
includeProperties: {
|
|
29754
29765
|
type: "boolean",
|
|
29755
29766
|
ops: ["tree"],
|
|
29756
|
-
description: "tree: include each node
|
|
29767
|
+
description: "tree: include every stored property of each node. Default false, and one row of a form answered seventy-six thousand characters with it; properties names the few that are wanted."
|
|
29768
|
+
},
|
|
29769
|
+
properties: {
|
|
29770
|
+
type: "array",
|
|
29771
|
+
items: { type: "string" },
|
|
29772
|
+
ops: ["tree"],
|
|
29773
|
+
description: "tree: the properties to read off each node, by name, so the text of every label and button under a panel is one call: a node that has not got one leaves it out. Colons read through what a node holds, as property does for find."
|
|
29757
29774
|
},
|
|
29758
29775
|
className: {
|
|
29759
29776
|
type: "string",
|
|
@@ -29942,12 +29959,12 @@ var TOOL_SPECS = [
|
|
|
29942
29959
|
relativeX: {
|
|
29943
29960
|
type: "number",
|
|
29944
29961
|
ops: ["mouse_motion"],
|
|
29945
|
-
description: "mouse_motion: movement
|
|
29962
|
+
description: "mouse_motion: the movement the event carries. Left out, it is the distance from where the last injected pointer event put the pointer, and none for the first; that is what a control that drags reads. Give it to send a motion the position does not show."
|
|
29946
29963
|
},
|
|
29947
29964
|
relativeY: {
|
|
29948
29965
|
type: "number",
|
|
29949
29966
|
ops: ["mouse_motion"],
|
|
29950
|
-
description: "mouse_motion:
|
|
29967
|
+
description: "mouse_motion: as relativeX."
|
|
29951
29968
|
}
|
|
29952
29969
|
},
|
|
29953
29970
|
requires: [],
|
|
@@ -29961,13 +29978,22 @@ var TOOL_SPECS = [
|
|
|
29961
29978
|
requires: ["nodePath"]
|
|
29962
29979
|
},
|
|
29963
29980
|
action: { summary: "press an action, or hold it", requires: ["action"] },
|
|
29964
|
-
key: {
|
|
29981
|
+
key: {
|
|
29982
|
+
summary: "press a key, or hold it. Not for a menu a click has opened: an OptionButton or PopupMenu pops up as a window of its own that a key sent to the game never reaches, so an arrow moves nothing in it and Enter closes it with nothing chosen. choose is what selects there. The limit is that menu's own navigation, not windows in general: an embedded dialog whose owner reads keys in _input gets them, and a binding catcher up in one takes the key first time",
|
|
29983
|
+
requires: ["keycode"]
|
|
29984
|
+
},
|
|
29965
29985
|
text: {
|
|
29966
29986
|
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
29967
29987
|
requires: ["text"]
|
|
29968
29988
|
},
|
|
29969
|
-
mouse_click: {
|
|
29970
|
-
|
|
29989
|
+
mouse_click: {
|
|
29990
|
+
summary: "one mouse button event at a position, carrying the buttons held once it has happened. A drag is a press held with pressed true, motions, and a release with pressed false",
|
|
29991
|
+
requires: ["x", "y"]
|
|
29992
|
+
},
|
|
29993
|
+
mouse_motion: {
|
|
29994
|
+
summary: "move the mouse to a position, carrying the distance from where the last injected event put it unless relativeX and relativeY say otherwise, and the buttons held, so a control that drags moves under a run of these between a held mouse_click and its release",
|
|
29995
|
+
requires: ["x", "y"]
|
|
29996
|
+
}
|
|
29971
29997
|
}
|
|
29972
29998
|
},
|
|
29973
29999
|
{
|
|
@@ -29988,7 +30014,12 @@ var TOOL_SPECS = [
|
|
|
29988
30014
|
says: {
|
|
29989
30015
|
type: "string",
|
|
29990
30016
|
ops: ["until"],
|
|
29991
|
-
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,
|
|
30017
|
+
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, on a node the player can see unless includeHidden says otherwise; a label with a line break is matched with the break in the words, and a backslash followed by n counts as one. 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."
|
|
30018
|
+
},
|
|
30019
|
+
includeHidden: {
|
|
30020
|
+
type: "boolean",
|
|
30021
|
+
ops: ["until"],
|
|
30022
|
+
description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
|
|
29992
30023
|
},
|
|
29993
30024
|
timeoutMs: {
|
|
29994
30025
|
type: "number",
|
|
@@ -30142,13 +30173,17 @@ function stillRunning(run) {
|
|
|
30142
30173
|
}
|
|
30143
30174
|
return true;
|
|
30144
30175
|
}
|
|
30145
|
-
|
|
30176
|
+
var PLAY_STARTS_WITHIN_MS = 30000;
|
|
30177
|
+
function runIsUp(run, editorSays, now = Date.now()) {
|
|
30146
30178
|
if (run?.throughEditor !== true) {
|
|
30147
30179
|
return stillRunning(run);
|
|
30148
30180
|
}
|
|
30149
30181
|
if (run.announcedPid !== undefined && !alive(run.announcedPid)) {
|
|
30150
30182
|
return false;
|
|
30151
30183
|
}
|
|
30184
|
+
if (editorSays === false && run.seenPlaying !== true && now - run.startedAt < PLAY_STARTS_WITHIN_MS) {
|
|
30185
|
+
return true;
|
|
30186
|
+
}
|
|
30152
30187
|
return editorSays ?? stillRunning(run);
|
|
30153
30188
|
}
|
|
30154
30189
|
function endedPreviousRun(ended) {
|
|
@@ -30223,10 +30258,11 @@ function runtimeVerdict(endpoint, after) {
|
|
|
30223
30258
|
heldAt: after.heldAt
|
|
30224
30259
|
};
|
|
30225
30260
|
}
|
|
30261
|
+
const state = after.playingSeen === false ? "the editor has not yet reported the game as playing, which a restarted editor does not until its scan is over" : "the game is still running";
|
|
30226
30262
|
return {
|
|
30227
30263
|
listening: false,
|
|
30228
30264
|
mayYetAnnounce: after.running,
|
|
30229
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and
|
|
30265
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
30230
30266
|
heldAt: null
|
|
30231
30267
|
};
|
|
30232
30268
|
}
|
|
@@ -30957,7 +30993,8 @@ class GodotServer {
|
|
|
30957
30993
|
...whichGame(args),
|
|
30958
30994
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
30959
30995
|
depth: readPositiveNumber(args, "depth") ?? 3,
|
|
30960
|
-
include_properties: readBoolean(args, "includeProperties") ?? false
|
|
30996
|
+
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
30997
|
+
properties: readArray(args, "properties") ?? []
|
|
30961
30998
|
});
|
|
30962
30999
|
case "find":
|
|
30963
31000
|
return await this.handleFindRuntimeNodes(args);
|
|
@@ -32172,7 +32209,8 @@ class GodotServer {
|
|
|
32172
32209
|
budgetMs,
|
|
32173
32210
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
32174
32211
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
32175
|
-
withArgs
|
|
32212
|
+
withArgs,
|
|
32213
|
+
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
32176
32214
|
});
|
|
32177
32215
|
}
|
|
32178
32216
|
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
@@ -32202,6 +32240,7 @@ class GodotServer {
|
|
|
32202
32240
|
const startedAt = Date.now();
|
|
32203
32241
|
const transcript = openTranscript(startedAt);
|
|
32204
32242
|
closeSync(transcript.fd);
|
|
32243
|
+
const playAnswer = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
32205
32244
|
const played = {
|
|
32206
32245
|
process: null,
|
|
32207
32246
|
pid: null,
|
|
@@ -32213,7 +32252,8 @@ class GodotServer {
|
|
|
32213
32252
|
exitCode: null,
|
|
32214
32253
|
throughEditor: true,
|
|
32215
32254
|
brokeOn: null,
|
|
32216
|
-
announcedBefore: alreadyPlaying
|
|
32255
|
+
announcedBefore: alreadyPlaying,
|
|
32256
|
+
seenPlaying: readBoolean(playAnswer, "playing") === true
|
|
32217
32257
|
};
|
|
32218
32258
|
this.activeProcess = played;
|
|
32219
32259
|
writeEditorRunNote({ projectPath, transcript: transcript.path, startedAt });
|
|
@@ -32223,7 +32263,7 @@ class GodotServer {
|
|
|
32223
32263
|
through: "editor",
|
|
32224
32264
|
scene: scene === null ? "the main scene" : `res://${scene}`,
|
|
32225
32265
|
refreshedClasses,
|
|
32226
|
-
debugPort: readNumber(
|
|
32266
|
+
debugPort: readNumber(playAnswer, "debugPort"),
|
|
32227
32267
|
endedPreviousRun: endedPreviousRun(ended),
|
|
32228
32268
|
breakpoints: breakpoints.applied.length === 0 ? undefined : this.breakpointsAsProjectSpells(projectPath, breakpoints.applied),
|
|
32229
32269
|
breakpointsRefused: breakpoints.refused.length === 0 ? undefined : breakpoints.refused.map((one) => ({
|
|
@@ -32749,7 +32789,11 @@ class GodotServer {
|
|
|
32749
32789
|
if (announced !== undefined && editorSays === undefined) {
|
|
32750
32790
|
return stillRunning(run);
|
|
32751
32791
|
}
|
|
32752
|
-
|
|
32792
|
+
const said = editorSays === undefined ? (await this.editorPlayingState())?.playing ?? null : editorSays;
|
|
32793
|
+
if (said === true) {
|
|
32794
|
+
run.seenPlaying = true;
|
|
32795
|
+
}
|
|
32796
|
+
return runIsUp(run, said);
|
|
32753
32797
|
}
|
|
32754
32798
|
async handleGetDebugOutput(args, waitedMs) {
|
|
32755
32799
|
await this.pickUpWhatTheEditorIsPlaying();
|
|
@@ -32932,6 +32976,7 @@ class GodotServer {
|
|
|
32932
32976
|
const writtenBefore = projectPath === "" ? null : cacheWrittenAt(projectPath);
|
|
32933
32977
|
const blind = await this.classesTheEditorCannotSee(args);
|
|
32934
32978
|
const atRisk = before === null ? [] : blind.unseen.filter((one) => before.has(one.className));
|
|
32979
|
+
const ghosts = blind.stillHeld ?? [];
|
|
32935
32980
|
const first = await this.handleViaBridge("rescan_filesystem", args);
|
|
32936
32981
|
if (first.isError) {
|
|
32937
32982
|
return first;
|
|
@@ -32943,7 +32988,7 @@ class GodotServer {
|
|
|
32943
32988
|
const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
|
|
32944
32989
|
busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
|
|
32945
32990
|
}
|
|
32946
|
-
if (!busy && projectPath !== "" && (atRisk.length > 0 || blind.unchecked !== undefined)) {
|
|
32991
|
+
if (!busy && projectPath !== "" && (atRisk.length > 0 || ghosts.length > 0 || blind.unchecked !== undefined)) {
|
|
32947
32992
|
const until = Date.now() + CACHE_WRITE_MS;
|
|
32948
32993
|
while (Date.now() < until && cacheWrittenAt(projectPath) === writtenBefore) {
|
|
32949
32994
|
await new Promise((settle) => setTimeout(settle, 100));
|
|
@@ -32951,7 +32996,11 @@ class GodotServer {
|
|
|
32951
32996
|
}
|
|
32952
32997
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
32953
32998
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
32954
|
-
const
|
|
32999
|
+
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
33000
|
+
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
33001
|
+
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
33002
|
+
const dropped = rebuilt === null ? [] : atMissingPaths.filter((name) => !cachedAtMissingPaths(rebuilt, projectPath).includes(name));
|
|
33003
|
+
const stillAtMissingPaths = atMissingPaths.filter((name) => !dropped.includes(name));
|
|
32955
33004
|
const reloading = readNonEmptyString(args, "reloadScript");
|
|
32956
33005
|
let reloaded = {};
|
|
32957
33006
|
if (reloading !== undefined) {
|
|
@@ -32967,8 +33016,23 @@ class GodotServer {
|
|
|
32967
33016
|
const checked = busy ? { unseen: [] } : await this.classesTheEditorCannotSee(args);
|
|
32968
33017
|
const unseen = checked.unseen;
|
|
32969
33018
|
const stillGone = lost.filter((name) => !restored.includes(name));
|
|
33019
|
+
const notes = [];
|
|
33020
|
+
if (busy) {
|
|
33021
|
+
notes.push("The editor was still scanning or importing when the wait ran out, so new files may not be visible yet.");
|
|
33022
|
+
} else if (stillGone.length > 0) {
|
|
33023
|
+
notes.push("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.");
|
|
33024
|
+
} else if (lost.length > 0) {
|
|
33025
|
+
notes.push("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.");
|
|
33026
|
+
} else if (unseen.length > 0) {
|
|
33027
|
+
notes.push("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. Rescan again on its own, which is the measured cure and needs no change to the declaring script, or restart the editor with editor_launch restart.");
|
|
33028
|
+
}
|
|
33029
|
+
if (stillAtMissingPaths.length > 0) {
|
|
33030
|
+
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.`);
|
|
33031
|
+
} else if (dropped.length > 0) {
|
|
33032
|
+
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.`);
|
|
33033
|
+
}
|
|
32970
33034
|
return this.jsonTextResponse({
|
|
32971
|
-
ok: !busy && unseen.length === 0 && stillGone.length === 0 && checked.unchecked === undefined,
|
|
33035
|
+
ok: !busy && unseen.length === 0 && stillGone.length === 0 && stillAtMissingPaths.length === 0 && checked.unchecked === undefined,
|
|
32972
33036
|
stillWorking: busy,
|
|
32973
33037
|
waitedMs: Date.now() - started,
|
|
32974
33038
|
heldBeforeReload: reloaded.heldBefore,
|
|
@@ -32977,8 +33041,10 @@ class GodotServer {
|
|
|
32977
33041
|
unseenByEditor: unseen.length > 0 ? unseen : undefined,
|
|
32978
33042
|
cacheLost: lost.length > 0 ? lost : undefined,
|
|
32979
33043
|
cacheRestored: restored.length > 0 ? restored : undefined,
|
|
33044
|
+
cacheDropped: dropped.length > 0 ? dropped : undefined,
|
|
33045
|
+
cacheAtMissingPaths: stillAtMissingPaths.length > 0 ? stillAtMissingPaths : undefined,
|
|
32980
33046
|
classesUnchecked: checked.unchecked,
|
|
32981
|
-
note:
|
|
33047
|
+
note: notes.length > 0 ? notes.join(" ") : undefined
|
|
32982
33048
|
});
|
|
32983
33049
|
}
|
|
32984
33050
|
async rebuildCacheAfterLoss(projectPath, lost) {
|
|
@@ -33219,6 +33285,7 @@ class GodotServer {
|
|
|
33219
33285
|
property: readString(args, "property") ?? "",
|
|
33220
33286
|
value: args["value"],
|
|
33221
33287
|
...readNonEmptyString(args, "says") === undefined ? {} : { says: readNonEmptyString(args, "says") },
|
|
33288
|
+
...readBoolean(args, "includeHidden") === undefined ? {} : { include_hidden: readBoolean(args, "includeHidden") },
|
|
33222
33289
|
timeout_ms: timeoutMs
|
|
33223
33290
|
}, patience);
|
|
33224
33291
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|