gdharness 1.0.5 → 1.0.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 +86 -22
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +28 -5
- package/build/index.js +84 -20
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -15694,7 +15694,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15694
15694
|
requires: []
|
|
15695
15695
|
},
|
|
15696
15696
|
refresh_classes: {
|
|
15697
|
-
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
15697
|
+
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong. A class the cache held after the last rebuild and had lost when this one began is named under lostSinceLastRebuild: the editor wrote the file without it in between, on a save or a scan, and will again until restarted',
|
|
15698
15698
|
requires: []
|
|
15699
15699
|
}
|
|
15700
15700
|
}
|
|
@@ -15719,7 +15719,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15719
15719
|
},
|
|
15720
15720
|
{
|
|
15721
15721
|
name: "project_test",
|
|
15722
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
15722
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
15723
15723
|
parameters: {
|
|
15724
15724
|
projectPath: PROJECT_PATH,
|
|
15725
15725
|
path: {
|
|
@@ -15936,7 +15936,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15936
15936
|
},
|
|
15937
15937
|
{
|
|
15938
15938
|
name: "script_diagnostics",
|
|
15939
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
15939
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. The one exception is named when it applies: an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
15940
15940
|
parameters: {
|
|
15941
15941
|
projectPath: PROJECT_PATH,
|
|
15942
15942
|
scriptPath: SCRIPT_PATH
|
|
@@ -31601,11 +31601,13 @@ function announcedIn(directory) {
|
|
|
31601
31601
|
}
|
|
31602
31602
|
const strangers = strangersAmong(candidates, Date.now());
|
|
31603
31603
|
for (const { file, pid, alive } of entries) {
|
|
31604
|
-
const
|
|
31604
|
+
const gone = !alive || strangers.has(file);
|
|
31605
|
+
const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
|
|
31605
31606
|
if (announced.kind === "rubbish") {
|
|
31606
|
-
if (!
|
|
31607
|
-
|
|
31607
|
+
if (!gone) {
|
|
31608
|
+
continue;
|
|
31608
31609
|
}
|
|
31610
|
+
wentAway(file, pid);
|
|
31609
31611
|
try {
|
|
31610
31612
|
unlinkSync(file);
|
|
31611
31613
|
} catch {}
|
|
@@ -31856,11 +31858,14 @@ function writeBootNote(projectPath, announcedAfterMs) {
|
|
|
31856
31858
|
writeFileSync8(path, JSON.stringify({ announcedAfterMs: Math.round(announcedAfterMs), at: new Date().toISOString() }, null, 2), "utf8");
|
|
31857
31859
|
} catch {}
|
|
31858
31860
|
}
|
|
31861
|
+
function halfAsLongAgain(lastBootMs) {
|
|
31862
|
+
return Math.round(lastBootMs * 1.5);
|
|
31863
|
+
}
|
|
31859
31864
|
function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
|
|
31860
31865
|
if (lastBootMs === null) {
|
|
31861
31866
|
return usual;
|
|
31862
31867
|
}
|
|
31863
|
-
return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual,
|
|
31868
|
+
return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual, halfAsLongAgain(lastBootMs)));
|
|
31864
31869
|
}
|
|
31865
31870
|
var LONGEST_SIZED_WAIT_MS = 60000;
|
|
31866
31871
|
var init_boot_note = __esm(() => {
|
|
@@ -37611,7 +37616,7 @@ import {
|
|
|
37611
37616
|
statSync as statSync5
|
|
37612
37617
|
} from "node:fs";
|
|
37613
37618
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
37614
|
-
import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3 } from "node:path";
|
|
37619
|
+
import { basename as basename4, dirname as dirname13, join as join20, normalize as normalize3, relative as relative3, resolve as resolve5 } from "node:path";
|
|
37615
37620
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
37616
37621
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
37617
37622
|
import { promisify as promisify7 } from "node:util";
|
|
@@ -37707,6 +37712,13 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
37707
37712
|
const leftToo = ended.left.length === 0 ? "" : ` ${counted(ended.left.length)} it had started ${ended.left.length === 1 ? "was" : "were"} left, named under childrenLeft: neither announced as a game of this project nor this project's engine run with --path on it, so what ${ended.left.length === 1 ? "it is" : "they are"} cannot be told from here.`;
|
|
37708
37713
|
return `${endedToo}${leftToo}`;
|
|
37709
37714
|
}
|
|
37715
|
+
function uidsLeftNote(left) {
|
|
37716
|
+
if (left === 0) {
|
|
37717
|
+
return "No scene was written: this reads the project and imports it, and does not resave anything.";
|
|
37718
|
+
}
|
|
37719
|
+
const them = left === 1 ? "it" : "them";
|
|
37720
|
+
return `${left === 1 ? "One file" : `${left} files`} still ${left === 1 ? "has" : "have"} no .uid, named under stillWithoutUid, which is the engine declining to import ${them} rather than this op skipping ${them}. No scene was written.`;
|
|
37721
|
+
}
|
|
37710
37722
|
function endedToStartThis(ended) {
|
|
37711
37723
|
if (ended === null) {
|
|
37712
37724
|
return "";
|
|
@@ -37734,7 +37746,7 @@ function runtimeVerdict(endpoint, after) {
|
|
|
37734
37746
|
};
|
|
37735
37747
|
}
|
|
37736
37748
|
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";
|
|
37737
|
-
const sized = after.sizedToMs === undefined ? "" : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
|
|
37749
|
+
const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though its last game took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
|
|
37738
37750
|
return {
|
|
37739
37751
|
listening: false,
|
|
37740
37752
|
mayYetAnnounce: after.running,
|
|
@@ -37838,7 +37850,10 @@ function aboveTheRunner(entry) {
|
|
|
37838
37850
|
const cut = entry.detail.length - runner;
|
|
37839
37851
|
return {
|
|
37840
37852
|
...entry,
|
|
37841
|
-
detail: [
|
|
37853
|
+
detail: [
|
|
37854
|
+
...entry.detail.slice(0, runner),
|
|
37855
|
+
`[and ${cut === 1 ? "one frame" : `${cut} frames`} inside ${RUNNER_DIRECTORY}]`
|
|
37856
|
+
]
|
|
37842
37857
|
};
|
|
37843
37858
|
}
|
|
37844
37859
|
function elsewhereIn(projectPath, asked) {
|
|
@@ -37905,6 +37920,8 @@ class GodotServer {
|
|
|
37905
37920
|
successorWatch = null;
|
|
37906
37921
|
lastProjectPath = null;
|
|
37907
37922
|
shutdownInitiated = false;
|
|
37923
|
+
shortListEditors = new Map;
|
|
37924
|
+
cacheLastRebuilt = new Map;
|
|
37908
37925
|
launchedEditor = null;
|
|
37909
37926
|
launchedFrom = null;
|
|
37910
37927
|
ownProject;
|
|
@@ -38318,8 +38335,15 @@ class GodotServer {
|
|
|
38318
38335
|
const contained = this.containProjectFiles(asks);
|
|
38319
38336
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
38320
38337
|
}
|
|
38321
|
-
|
|
38322
|
-
|
|
38338
|
+
if (headless !== "refresh_class_cache") {
|
|
38339
|
+
return await this.headless(headless, answerable);
|
|
38340
|
+
}
|
|
38341
|
+
const project = this.project(answerable);
|
|
38342
|
+
if (!project.ok) {
|
|
38343
|
+
return project.response;
|
|
38344
|
+
}
|
|
38345
|
+
const answered = this.answer(await this.rebuildClassCache(project.value.path));
|
|
38346
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
38323
38347
|
}
|
|
38324
38348
|
const { op: _op, ...arguments_ } = args;
|
|
38325
38349
|
const bridge = async (command, extra = {}) => {
|
|
@@ -38726,7 +38750,7 @@ class GodotServer {
|
|
|
38726
38750
|
payload: {
|
|
38727
38751
|
uidsCreated: given,
|
|
38728
38752
|
stillWithoutUid: after,
|
|
38729
|
-
note: after.length
|
|
38753
|
+
note: uidsLeftNote(after.length)
|
|
38730
38754
|
}
|
|
38731
38755
|
});
|
|
38732
38756
|
}
|
|
@@ -38908,7 +38932,7 @@ class GodotServer {
|
|
|
38908
38932
|
if (!engine.ok) {
|
|
38909
38933
|
return engine.response;
|
|
38910
38934
|
}
|
|
38911
|
-
const classes = await this.
|
|
38935
|
+
const classes = await this.rebuildClassCache(project.value.path);
|
|
38912
38936
|
if (!classes.ok) {
|
|
38913
38937
|
return this.answer(classes);
|
|
38914
38938
|
}
|
|
@@ -39125,7 +39149,8 @@ class GodotServer {
|
|
|
39125
39149
|
}
|
|
39126
39150
|
const stale = unloaded.filter((type) => type.inTheClassCache);
|
|
39127
39151
|
if (stale.length > 0) {
|
|
39128
|
-
|
|
39152
|
+
const remedy = this.editorWritesAShortList(projectPath) ? "editor_rescan will not clear it here: this editor wrote the class cache from a list shorter than the file on its last scan and writes the same list on every scan, so editor_launch restart is what loads it." : "editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.";
|
|
39153
|
+
notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. ${remedy}`);
|
|
39129
39154
|
}
|
|
39130
39155
|
return {
|
|
39131
39156
|
...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
|
|
@@ -39133,6 +39158,10 @@ class GodotServer {
|
|
|
39133
39158
|
...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
|
|
39134
39159
|
};
|
|
39135
39160
|
}
|
|
39161
|
+
editorWritesAShortList(projectPath) {
|
|
39162
|
+
const key = resolve5(projectPath);
|
|
39163
|
+
return this.shortListEditors.has(key) && this.shortListEditors.get(key) === this.godotBridge.getStatus().editorPid;
|
|
39164
|
+
}
|
|
39136
39165
|
sourceOfClass(projectPath, resourcePath) {
|
|
39137
39166
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
39138
39167
|
if (!contained.ok || !existsSync14(contained.absolutePath)) {
|
|
@@ -39642,7 +39671,8 @@ class GodotServer {
|
|
|
39642
39671
|
return !editorSaysGoing;
|
|
39643
39672
|
}
|
|
39644
39673
|
});
|
|
39645
|
-
|
|
39674
|
+
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
39675
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
|
|
39646
39676
|
const going = this.currentRun();
|
|
39647
39677
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
39648
39678
|
this.tie(going, endpoint);
|
|
@@ -40481,6 +40511,9 @@ class GodotServer {
|
|
|
40481
40511
|
}
|
|
40482
40512
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
40483
40513
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
40514
|
+
if (lost.length > 0) {
|
|
40515
|
+
this.shortListEditors.set(resolve5(projectPath), this.godotBridge.getStatus().editorPid);
|
|
40516
|
+
}
|
|
40484
40517
|
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
40485
40518
|
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
40486
40519
|
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
@@ -40536,7 +40569,7 @@ class GodotServer {
|
|
|
40536
40569
|
if (projectPath === "") {
|
|
40537
40570
|
return [];
|
|
40538
40571
|
}
|
|
40539
|
-
const rebuilt = await this.
|
|
40572
|
+
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
40540
40573
|
if (!rebuilt.ok) {
|
|
40541
40574
|
this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
|
|
40542
40575
|
return [];
|
|
@@ -40544,6 +40577,34 @@ class GodotServer {
|
|
|
40544
40577
|
const now = cachedClasses(projectPath);
|
|
40545
40578
|
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
40546
40579
|
}
|
|
40580
|
+
async rebuildClassCache(projectPath) {
|
|
40581
|
+
const key = resolve5(projectPath);
|
|
40582
|
+
const before = cachedClasses(projectPath);
|
|
40583
|
+
const remembered = this.cacheLastRebuilt.get(key);
|
|
40584
|
+
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
40585
|
+
if (!rebuilt.ok) {
|
|
40586
|
+
return rebuilt;
|
|
40587
|
+
}
|
|
40588
|
+
const after = cachedClasses(projectPath);
|
|
40589
|
+
if (after === null) {
|
|
40590
|
+
return rebuilt;
|
|
40591
|
+
}
|
|
40592
|
+
this.cacheLastRebuilt.set(key, new Set(after.keys()));
|
|
40593
|
+
const lost = remembered === undefined || before === null ? [] : [...remembered].filter((name) => !before.has(name) && after.has(name)).sort();
|
|
40594
|
+
if (lost.length === 0 || !this.godotBridge.isConnected()) {
|
|
40595
|
+
return rebuilt;
|
|
40596
|
+
}
|
|
40597
|
+
this.shortListEditors.set(key, this.godotBridge.getStatus().editorPid);
|
|
40598
|
+
const them = lost.length === 1 ? "it" : "them";
|
|
40599
|
+
return {
|
|
40600
|
+
...rebuilt,
|
|
40601
|
+
payload: {
|
|
40602
|
+
...rebuilt.payload,
|
|
40603
|
+
lostSinceLastRebuild: lost,
|
|
40604
|
+
note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
|
|
40605
|
+
}
|
|
40606
|
+
};
|
|
40607
|
+
}
|
|
40547
40608
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
40548
40609
|
const first = answered.content[0];
|
|
40549
40610
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -40554,15 +40615,18 @@ class GodotServer {
|
|
|
40554
40615
|
if (checked.unseen.length === 0 && stillHeld.length === 0 && checked.unchecked === undefined) {
|
|
40555
40616
|
return answered;
|
|
40556
40617
|
}
|
|
40557
|
-
const
|
|
40618
|
+
const payload = asParams(JSON.parse(first.text));
|
|
40619
|
+
const said = readString(payload, "note");
|
|
40620
|
+
const notes = said === undefined ? [] : [said];
|
|
40558
40621
|
if (checked.unseen.length > 0) {
|
|
40559
|
-
|
|
40622
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
40623
|
+
notes.push(projectPath !== "" && this.editorWritesAShortList(projectPath) ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan will not reach it either on this editor, whose last scan wrote the cache from a list shorter than the file and would again; editor_launch restart is what loads them." : "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
|
|
40560
40624
|
}
|
|
40561
40625
|
if (stillHeld.length > 0) {
|
|
40562
40626
|
notes.push(`The editor is still holding ${stillHeld.join(", ")}, which this project no longer declares, and it will write that list back over the cache this call just corrected. The entry returns pointing at a script that is gone, and the next engine to walk get_global_class_list() dies on "File not found". A rescan does not clear this one: it picks up a class that has appeared and does not drop one that has gone. editor_launch restart does.`);
|
|
40563
40627
|
}
|
|
40564
40628
|
return this.jsonTextResponse({
|
|
40565
|
-
...
|
|
40629
|
+
...payload,
|
|
40566
40630
|
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
40567
40631
|
stillHeldByEditor: stillHeld.length > 0 ? stillHeld : undefined,
|
|
40568
40632
|
classesUnchecked: checked.unchecked,
|
|
@@ -40855,7 +40919,7 @@ var init_server3 = __esm(() => {
|
|
|
40855
40919
|
|
|
40856
40920
|
// src/cli.ts
|
|
40857
40921
|
import { existsSync as existsSync15 } from "fs";
|
|
40858
|
-
import { join as join21, resolve as
|
|
40922
|
+
import { join as join21, resolve as resolve6 } from "path";
|
|
40859
40923
|
|
|
40860
40924
|
// src/errors.ts
|
|
40861
40925
|
class Refusal extends Error {
|
|
@@ -42559,7 +42623,7 @@ class UsageError extends Refusal {
|
|
|
42559
42623
|
}
|
|
42560
42624
|
function projectArgument(at) {
|
|
42561
42625
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
42562
|
-
const projectPath =
|
|
42626
|
+
const projectPath = resolve6(named ?? ".");
|
|
42563
42627
|
if (!existsSync15(join21(projectPath, "project.godot"))) {
|
|
42564
42628
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
42565
42629
|
}
|
|
@@ -267,23 +267,46 @@ func _announce(bind_address: String) -> void:
|
|
|
267
267
|
)
|
|
268
268
|
return
|
|
269
269
|
var path: String = directory.path_join("runtime-%d.json" % OS.get_process_id())
|
|
270
|
-
|
|
270
|
+
# Written whole under a name no server reads and moved into place, so no look ever meets
|
|
271
|
+
# the file empty: a server sweeping the directory between an open and a write took the
|
|
272
|
+
# announcement for a dead game's leftover, and the directory is shared by every server on
|
|
273
|
+
# the machine, older ones included.
|
|
274
|
+
var staged: String = path + ".tmp"
|
|
275
|
+
var file: FileAccess = FileAccess.open(staged, FileAccess.WRITE)
|
|
271
276
|
if file == null:
|
|
272
277
|
push_warning(
|
|
273
278
|
(
|
|
274
279
|
"[gdharness] cannot write %s (%s); the server will not find this game"
|
|
275
|
-
% [
|
|
280
|
+
% [staged, FileAccess.get_open_error()]
|
|
276
281
|
)
|
|
277
282
|
)
|
|
278
283
|
return
|
|
279
|
-
var
|
|
284
|
+
var written: bool = file.store_string(JSON.stringify(_identity(bind_address)))
|
|
280
285
|
file.close()
|
|
281
|
-
if not
|
|
282
|
-
push_warning("[gdharness] cannot write %s; the server will not find this game" %
|
|
286
|
+
if not written:
|
|
287
|
+
push_warning("[gdharness] cannot write %s; the server will not find this game" % staged)
|
|
288
|
+
_unstage(staged)
|
|
289
|
+
return
|
|
290
|
+
var moved: Error = DirAccess.rename_absolute(staged, path)
|
|
291
|
+
if moved != OK:
|
|
292
|
+
push_warning(
|
|
293
|
+
(
|
|
294
|
+
"[gdharness] cannot move %s to %s (%s); the server will not find this game"
|
|
295
|
+
% [staged, path, moved]
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
_unstage(staged)
|
|
283
299
|
return
|
|
284
300
|
_announcement = path
|
|
285
301
|
|
|
286
302
|
|
|
303
|
+
## Takes a staged announcement down after a write or a move that failed. The answer is taken
|
|
304
|
+
## rather than dropped, since these scripts compile under the game's own warning levels; a
|
|
305
|
+
## staged file that will not go either is nothing any server reads.
|
|
306
|
+
func _unstage(staged: String) -> void:
|
|
307
|
+
var _removed: Error = DirAccess.remove_absolute(staged)
|
|
308
|
+
|
|
309
|
+
|
|
287
310
|
## What the announcement file and the welcome both carry: enough to pick this game out of
|
|
288
311
|
## several and to know whether the server speaks its protocol.
|
|
289
312
|
func _identity(bind_address: String) -> Dictionary:
|
package/build/index.js
CHANGED
|
@@ -10184,7 +10184,7 @@ import {
|
|
|
10184
10184
|
statSync as statSync5
|
|
10185
10185
|
} from "node:fs";
|
|
10186
10186
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
10187
|
-
import { basename as basename3, dirname as dirname8, join as join14, normalize as normalize2, relative as relative3 } from "node:path";
|
|
10187
|
+
import { basename as basename3, dirname as dirname8, join as join14, normalize as normalize2, relative as relative3, resolve as resolve5 } from "node:path";
|
|
10188
10188
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
10189
10189
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10190
10190
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -25233,11 +25233,13 @@ function announcedIn(directory) {
|
|
|
25233
25233
|
}
|
|
25234
25234
|
const strangers = strangersAmong(candidates, Date.now());
|
|
25235
25235
|
for (const { file, pid, alive } of entries) {
|
|
25236
|
-
const
|
|
25236
|
+
const gone = !alive || strangers.has(file);
|
|
25237
|
+
const announced = gone ? { kind: "rubbish" } : parseAnnouncement(file, pid);
|
|
25237
25238
|
if (announced.kind === "rubbish") {
|
|
25238
|
-
if (!
|
|
25239
|
-
|
|
25239
|
+
if (!gone) {
|
|
25240
|
+
continue;
|
|
25240
25241
|
}
|
|
25242
|
+
wentAway(file, pid);
|
|
25241
25243
|
try {
|
|
25242
25244
|
unlinkSync(file);
|
|
25243
25245
|
} catch {}
|
|
@@ -25482,11 +25484,14 @@ function writeBootNote(projectPath, announcedAfterMs) {
|
|
|
25482
25484
|
} catch {}
|
|
25483
25485
|
}
|
|
25484
25486
|
var LONGEST_SIZED_WAIT_MS = 60000;
|
|
25487
|
+
function halfAsLongAgain(lastBootMs) {
|
|
25488
|
+
return Math.round(lastBootMs * 1.5);
|
|
25489
|
+
}
|
|
25485
25490
|
function waitSizedTo(lastBootMs, usual = ANNOUNCE_BUDGET_MS) {
|
|
25486
25491
|
if (lastBootMs === null) {
|
|
25487
25492
|
return usual;
|
|
25488
25493
|
}
|
|
25489
|
-
return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual,
|
|
25494
|
+
return Math.min(LONGEST_SIZED_WAIT_MS, Math.max(usual, halfAsLongAgain(lastBootMs)));
|
|
25490
25495
|
}
|
|
25491
25496
|
|
|
25492
25497
|
// src/breakpoint-note.ts
|
|
@@ -29463,7 +29468,7 @@ var TOOL_SPECS = [
|
|
|
29463
29468
|
requires: []
|
|
29464
29469
|
},
|
|
29465
29470
|
refresh_classes: {
|
|
29466
|
-
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
|
|
29471
|
+
summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong. A class the cache held after the last rebuild and had lost when this one began is named under lostSinceLastRebuild: the editor wrote the file without it in between, on a save or a scan, and will again until restarted',
|
|
29467
29472
|
requires: []
|
|
29468
29473
|
}
|
|
29469
29474
|
}
|
|
@@ -29488,7 +29493,7 @@ var TOOL_SPECS = [
|
|
|
29488
29493
|
},
|
|
29489
29494
|
{
|
|
29490
29495
|
name: "project_test",
|
|
29491
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
29496
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
29492
29497
|
parameters: {
|
|
29493
29498
|
projectPath: PROJECT_PATH,
|
|
29494
29499
|
path: {
|
|
@@ -29705,7 +29710,7 @@ var TOOL_SPECS = [
|
|
|
29705
29710
|
},
|
|
29706
29711
|
{
|
|
29707
29712
|
name: "script_diagnostics",
|
|
29708
|
-
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
29713
|
+
description: "Errors and warnings for a script from the editor's language server, and whether the script is clean. Needs the editor running. Diagnostics are checked against the project's own files, and the two that the files disprove are named rather than passed on: a member the class cache's file declares comes back under contradictedByTheFile, and a class this project declares that the diagnostic could not resolve comes back under typesTheEditorHasNotLoaded, with inTheClassCache saying whether a launched game would resolve it. staleAnalysis then says what to do, and for both states it is editor_rescan first: it asks the editor already running, starts nothing, and has cleared each in every reproduction measured. The one exception is named when it applies: an editor whose last scan wrote the class cache shorter than the file holds a list behind the files and writes it on every scan, so on that editor the rescan loads nothing and staleAnalysis says editor_launch restart instead. What follows differs: for a stale analysed type, editor_rescan with reloadScript recompiles the held copy and editor_launch restart is the last resort that costs a window; for a class missing from the cache, project_import refresh_classes rewrites the cache from disk at the cost of a short headless engine, which matters beside a running bench.",
|
|
29709
29714
|
parameters: {
|
|
29710
29715
|
projectPath: PROJECT_PATH,
|
|
29711
29716
|
scriptPath: SCRIPT_PATH
|
|
@@ -30402,6 +30407,13 @@ function aboutTheChildren(ended, throughEditor) {
|
|
|
30402
30407
|
const leftToo = ended.left.length === 0 ? "" : ` ${counted(ended.left.length)} it had started ${ended.left.length === 1 ? "was" : "were"} left, named under childrenLeft: neither announced as a game of this project nor this project's engine run with --path on it, so what ${ended.left.length === 1 ? "it is" : "they are"} cannot be told from here.`;
|
|
30403
30408
|
return `${endedToo}${leftToo}`;
|
|
30404
30409
|
}
|
|
30410
|
+
function uidsLeftNote(left) {
|
|
30411
|
+
if (left === 0) {
|
|
30412
|
+
return "No scene was written: this reads the project and imports it, and does not resave anything.";
|
|
30413
|
+
}
|
|
30414
|
+
const them = left === 1 ? "it" : "them";
|
|
30415
|
+
return `${left === 1 ? "One file" : `${left} files`} still ${left === 1 ? "has" : "have"} no .uid, named under stillWithoutUid, which is the engine declining to import ${them} rather than this op skipping ${them}. No scene was written.`;
|
|
30416
|
+
}
|
|
30405
30417
|
function endedToStartThis(ended) {
|
|
30406
30418
|
if (ended === null) {
|
|
30407
30419
|
return "";
|
|
@@ -30430,7 +30442,7 @@ function runtimeVerdict(endpoint, after) {
|
|
|
30430
30442
|
};
|
|
30431
30443
|
}
|
|
30432
30444
|
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";
|
|
30433
|
-
const sized = after.sizedToMs === undefined ? "" : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
|
|
30445
|
+
const sized = after.sizedToMs === undefined ? "" : halfAsLongAgain(after.sizedToMs) > after.budgetMs ? `, the longest a start waits unasked though its last game took ${after.sizedToMs}ms to announce,` : `, half as long again as the ${after.sizedToMs}ms its last game took,`;
|
|
30434
30446
|
return {
|
|
30435
30447
|
listening: false,
|
|
30436
30448
|
mayYetAnnounce: after.running,
|
|
@@ -30576,7 +30588,10 @@ function aboveTheRunner(entry) {
|
|
|
30576
30588
|
const cut = entry.detail.length - runner;
|
|
30577
30589
|
return {
|
|
30578
30590
|
...entry,
|
|
30579
|
-
detail: [
|
|
30591
|
+
detail: [
|
|
30592
|
+
...entry.detail.slice(0, runner),
|
|
30593
|
+
`[and ${cut === 1 ? "one frame" : `${cut} frames`} inside ${RUNNER_DIRECTORY}]`
|
|
30594
|
+
]
|
|
30580
30595
|
};
|
|
30581
30596
|
}
|
|
30582
30597
|
function elsewhereIn(projectPath, asked) {
|
|
@@ -30643,6 +30658,8 @@ class GodotServer {
|
|
|
30643
30658
|
successorWatch = null;
|
|
30644
30659
|
lastProjectPath = null;
|
|
30645
30660
|
shutdownInitiated = false;
|
|
30661
|
+
shortListEditors = new Map;
|
|
30662
|
+
cacheLastRebuilt = new Map;
|
|
30646
30663
|
launchedEditor = null;
|
|
30647
30664
|
launchedFrom = null;
|
|
30648
30665
|
ownProject;
|
|
@@ -31056,8 +31073,15 @@ class GodotServer {
|
|
|
31056
31073
|
const contained = this.containProjectFiles(asks);
|
|
31057
31074
|
return contained.ok ? await this.handleViaBridge(editorSide, contained.value) : contained.response;
|
|
31058
31075
|
}
|
|
31059
|
-
|
|
31060
|
-
|
|
31076
|
+
if (headless !== "refresh_class_cache") {
|
|
31077
|
+
return await this.headless(headless, answerable);
|
|
31078
|
+
}
|
|
31079
|
+
const project = this.project(answerable);
|
|
31080
|
+
if (!project.ok) {
|
|
31081
|
+
return project.response;
|
|
31082
|
+
}
|
|
31083
|
+
const answered = this.answer(await this.rebuildClassCache(project.value.path));
|
|
31084
|
+
return this.godotBridge.isConnected() ? await this.alsoSayWhatTheEditorCannotSee(answered, args) : answered;
|
|
31061
31085
|
}
|
|
31062
31086
|
const { op: _op, ...arguments_ } = args;
|
|
31063
31087
|
const bridge = async (command, extra = {}) => {
|
|
@@ -31464,7 +31488,7 @@ class GodotServer {
|
|
|
31464
31488
|
payload: {
|
|
31465
31489
|
uidsCreated: given,
|
|
31466
31490
|
stillWithoutUid: after,
|
|
31467
|
-
note: after.length
|
|
31491
|
+
note: uidsLeftNote(after.length)
|
|
31468
31492
|
}
|
|
31469
31493
|
});
|
|
31470
31494
|
}
|
|
@@ -31646,7 +31670,7 @@ class GodotServer {
|
|
|
31646
31670
|
if (!engine.ok) {
|
|
31647
31671
|
return engine.response;
|
|
31648
31672
|
}
|
|
31649
|
-
const classes = await this.
|
|
31673
|
+
const classes = await this.rebuildClassCache(project.value.path);
|
|
31650
31674
|
if (!classes.ok) {
|
|
31651
31675
|
return this.answer(classes);
|
|
31652
31676
|
}
|
|
@@ -31863,7 +31887,8 @@ class GodotServer {
|
|
|
31863
31887
|
}
|
|
31864
31888
|
const stale = unloaded.filter((type) => type.inTheClassCache);
|
|
31865
31889
|
if (stale.length > 0) {
|
|
31866
|
-
|
|
31890
|
+
const remedy = this.editorWritesAShortList(projectPath) ? "editor_rescan will not clear it here: this editor wrote the class cache from a list shorter than the file on its last scan and writes the same list on every scan, so editor_launch restart is what loads it." : "editor_rescan clears it, measured against a real editor with waiting the same length ruled out; editor_launch restart also does, and costs more.";
|
|
31891
|
+
notes.push(`${stale.map((type) => type.type).join(", ")} ${stale.length === 1 ? "is" : "are"} in the class cache and still unresolved here, which is the editor's loaded list being behind rather than anything on disk. A game launched now reads the cache and resolves ${stale.length === 1 ? "it" : "them"}, so the run and the diagnostics disagree. ${remedy}`);
|
|
31867
31892
|
}
|
|
31868
31893
|
return {
|
|
31869
31894
|
...contradicted.length === 0 ? {} : { contradictedByTheFile: contradicted },
|
|
@@ -31871,6 +31896,10 @@ class GodotServer {
|
|
|
31871
31896
|
...notes.length === 0 ? {} : { staleAnalysis: notes.join(" ") }
|
|
31872
31897
|
};
|
|
31873
31898
|
}
|
|
31899
|
+
editorWritesAShortList(projectPath) {
|
|
31900
|
+
const key = resolve5(projectPath);
|
|
31901
|
+
return this.shortListEditors.has(key) && this.shortListEditors.get(key) === this.godotBridge.getStatus().editorPid;
|
|
31902
|
+
}
|
|
31874
31903
|
sourceOfClass(projectPath, resourcePath) {
|
|
31875
31904
|
const contained = resolveWithinProject(projectPath, resourcePath);
|
|
31876
31905
|
if (!contained.ok || !existsSync9(contained.absolutePath)) {
|
|
@@ -32380,7 +32409,8 @@ class GodotServer {
|
|
|
32380
32409
|
return !editorSaysGoing;
|
|
32381
32410
|
}
|
|
32382
32411
|
});
|
|
32383
|
-
|
|
32412
|
+
const asked = editorAsks === 0 ? "editor not asked" : `editor asked ${editorAsks === 1 ? "once" : `${editorAsks} times`}, last said ${lastSaid()}`;
|
|
32413
|
+
this.logDebug(`announce wait ended after ${Date.now() - waitBegan}ms: ${endpoint === null ? "nothing announced" : `pid ${endpoint.pid} announced`}, ${asked}`);
|
|
32384
32414
|
const going = this.currentRun();
|
|
32385
32415
|
if (endpoint !== null && going !== null && going.announcedPid === undefined) {
|
|
32386
32416
|
this.tie(going, endpoint);
|
|
@@ -33219,6 +33249,9 @@ class GodotServer {
|
|
|
33219
33249
|
}
|
|
33220
33250
|
const after = projectPath === "" || busy ? null : cachedClasses(projectPath);
|
|
33221
33251
|
const lost = before === null || after === null ? [] : [...before.keys()].filter((name) => !after.has(name));
|
|
33252
|
+
if (lost.length > 0) {
|
|
33253
|
+
this.shortListEditors.set(resolve5(projectPath), this.godotBridge.getStatus().editorPid);
|
|
33254
|
+
}
|
|
33222
33255
|
const atMissingPaths = after === null ? [] : cachedAtMissingPaths(after, projectPath);
|
|
33223
33256
|
const restored = lost.length > 0 || atMissingPaths.length > 0 ? await this.rebuildCacheAfterLoss(projectPath, lost) : [];
|
|
33224
33257
|
const rebuilt = atMissingPaths.length > 0 ? cachedClasses(projectPath) : null;
|
|
@@ -33274,7 +33307,7 @@ class GodotServer {
|
|
|
33274
33307
|
if (projectPath === "") {
|
|
33275
33308
|
return [];
|
|
33276
33309
|
}
|
|
33277
|
-
const rebuilt = await this.
|
|
33310
|
+
const rebuilt = await this.rebuildClassCache(projectPath);
|
|
33278
33311
|
if (!rebuilt.ok) {
|
|
33279
33312
|
this.logDebug(`Could not rebuild the class cache after a scan: ${rebuilt.message}`);
|
|
33280
33313
|
return [];
|
|
@@ -33282,6 +33315,34 @@ class GodotServer {
|
|
|
33282
33315
|
const now = cachedClasses(projectPath);
|
|
33283
33316
|
return now === null ? [] : lost.filter((name) => now.has(name));
|
|
33284
33317
|
}
|
|
33318
|
+
async rebuildClassCache(projectPath) {
|
|
33319
|
+
const key = resolve5(projectPath);
|
|
33320
|
+
const before = cachedClasses(projectPath);
|
|
33321
|
+
const remembered = this.cacheLastRebuilt.get(key);
|
|
33322
|
+
const rebuilt = await this.operation("refresh_class_cache", {}, projectPath);
|
|
33323
|
+
if (!rebuilt.ok) {
|
|
33324
|
+
return rebuilt;
|
|
33325
|
+
}
|
|
33326
|
+
const after = cachedClasses(projectPath);
|
|
33327
|
+
if (after === null) {
|
|
33328
|
+
return rebuilt;
|
|
33329
|
+
}
|
|
33330
|
+
this.cacheLastRebuilt.set(key, new Set(after.keys()));
|
|
33331
|
+
const lost = remembered === undefined || before === null ? [] : [...remembered].filter((name) => !before.has(name) && after.has(name)).sort();
|
|
33332
|
+
if (lost.length === 0 || !this.godotBridge.isConnected()) {
|
|
33333
|
+
return rebuilt;
|
|
33334
|
+
}
|
|
33335
|
+
this.shortListEditors.set(key, this.godotBridge.getStatus().editorPid);
|
|
33336
|
+
const them = lost.length === 1 ? "it" : "them";
|
|
33337
|
+
return {
|
|
33338
|
+
...rebuilt,
|
|
33339
|
+
payload: {
|
|
33340
|
+
...rebuilt.payload,
|
|
33341
|
+
lostSinceLastRebuild: lost,
|
|
33342
|
+
note: `The cache held ${lost.join(", ")} after the last rebuild and not when this one began, so the editor holding this project wrote it without ${them} in between: it holds a list shorter than the files and writes that list on every save and scan, and each rebuild puts ${them} back. editor_launch restart is what ends it.`
|
|
33343
|
+
}
|
|
33344
|
+
};
|
|
33345
|
+
}
|
|
33285
33346
|
async alsoSayWhatTheEditorCannotSee(answered, args) {
|
|
33286
33347
|
const first = answered.content[0];
|
|
33287
33348
|
if (answered.isError || first?.type !== "text" || typeof first.text !== "string") {
|
|
@@ -33292,15 +33353,18 @@ class GodotServer {
|
|
|
33292
33353
|
if (checked.unseen.length === 0 && stillHeld.length === 0 && checked.unchecked === undefined) {
|
|
33293
33354
|
return answered;
|
|
33294
33355
|
}
|
|
33295
|
-
const
|
|
33356
|
+
const payload = asParams(JSON.parse(first.text));
|
|
33357
|
+
const said = readString(payload, "note");
|
|
33358
|
+
const notes = said === undefined ? [] : [said];
|
|
33296
33359
|
if (checked.unseen.length > 0) {
|
|
33297
|
-
|
|
33360
|
+
const projectPath = typeof args["projectPath"] === "string" ? args["projectPath"] : "";
|
|
33361
|
+
notes.push(projectPath !== "" && this.editorWritesAShortList(projectPath) ? "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan will not reach it either on this editor, whose last scan wrote the cache from a list shorter than the file and would again; editor_launch restart is what loads them." : "The cache on disk is right now, and the editor holding this project is still not resolving these: rewriting the file does not reach the list it already loaded. editor_rescan does, on its own and with no change to the declaring script; editor_launch restart also does, and costs more.");
|
|
33298
33362
|
}
|
|
33299
33363
|
if (stillHeld.length > 0) {
|
|
33300
33364
|
notes.push(`The editor is still holding ${stillHeld.join(", ")}, which this project no longer declares, and it will write that list back over the cache this call just corrected. The entry returns pointing at a script that is gone, and the next engine to walk get_global_class_list() dies on "File not found". A rescan does not clear this one: it picks up a class that has appeared and does not drop one that has gone. editor_launch restart does.`);
|
|
33301
33365
|
}
|
|
33302
33366
|
return this.jsonTextResponse({
|
|
33303
|
-
...
|
|
33367
|
+
...payload,
|
|
33304
33368
|
unseenByEditor: checked.unseen.length > 0 ? checked.unseen : undefined,
|
|
33305
33369
|
stillHeldByEditor: stillHeld.length > 0 ? stillHeld : undefined,
|
|
33306
33370
|
classesUnchecked: checked.unchecked,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.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",
|