gdharness 1.3.38 → 1.3.40
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 +146 -30
- package/build/godot/addons/gdharness_editor/animation_reads.gd +7 -6
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +5 -3
- package/build/godot/operations/animation_reads.gd +7 -6
- package/build/godot/operations/godot_operations.gd +5 -0
- package/build/godot/operations/script_check.gd +58 -0
- package/build/keeper.js +38 -5
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -852,7 +852,18 @@ public static class GdharnessDesktop {
|
|
|
852
852
|
}
|
|
853
853
|
|
|
854
854
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
855
|
-
static extern IntPtr
|
|
855
|
+
static extern IntPtr CreateDesktopExW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes, uint heapKilobytes, IntPtr reserved);
|
|
856
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
857
|
+
static extern IntPtr CreateWindowStationW(string name, uint flags, uint access, IntPtr attributes);
|
|
858
|
+
[DllImport("user32.dll", SetLastError = true)] static extern IntPtr GetProcessWindowStation();
|
|
859
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool SetProcessWindowStation(IntPtr station);
|
|
860
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
861
|
+
static extern bool GetUserObjectInformationW(IntPtr handle, int index, StringBuilder into, int size, out int needed);
|
|
862
|
+
const uint WindowStationAllAccess = 0x37F;
|
|
863
|
+
const int ObjectName = 2;
|
|
864
|
+
// What Windows gives an interactive desktop. A non-interactive station's desktops get a small
|
|
865
|
+
// fraction of that by default, and one desktop here holds every engine a run starts.
|
|
866
|
+
const uint DesktopHeapKilobytes = 20480;
|
|
856
867
|
|
|
857
868
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
858
869
|
static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out Started started);
|
|
@@ -943,12 +954,34 @@ public static class GdharnessDesktop {
|
|
|
943
954
|
StartupInfo startup = new StartupInfo();
|
|
944
955
|
startup.cb = Marshal.SizeOf(typeof(StartupInfo));
|
|
945
956
|
if (desktop.Length > 0) {
|
|
946
|
-
Step(stepFile, "
|
|
947
|
-
IntPtr
|
|
957
|
+
Step(stepFile, "opening the session's own window station");
|
|
958
|
+
IntPtr station = CreateWindowStationW(null, 0, WindowStationAllAccess, IntPtr.Zero);
|
|
959
|
+
if (station == IntPtr.Zero) {
|
|
960
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateWindowStationW");
|
|
961
|
+
}
|
|
962
|
+
StringBuilder stationName = new StringBuilder(256);
|
|
963
|
+
int needed;
|
|
964
|
+
if (!GetUserObjectInformationW(station, ObjectName, stationName, stationName.Capacity * 2, out needed)) {
|
|
965
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetUserObjectInformationW");
|
|
966
|
+
}
|
|
967
|
+
// A desktop is made in the calling process's station, so this one is the caller's while it is.
|
|
968
|
+
IntPtr own = GetProcessWindowStation();
|
|
969
|
+
if (!SetProcessWindowStation(station)) {
|
|
970
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetProcessWindowStation");
|
|
971
|
+
}
|
|
972
|
+
IntPtr held;
|
|
973
|
+
int made;
|
|
974
|
+
try {
|
|
975
|
+
Step(stepFile, "creating the " + desktop + " desktop");
|
|
976
|
+
held = CreateDesktopExW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero, DesktopHeapKilobytes, IntPtr.Zero);
|
|
977
|
+
made = Marshal.GetLastWin32Error();
|
|
978
|
+
} finally {
|
|
979
|
+
SetProcessWindowStation(own);
|
|
980
|
+
}
|
|
948
981
|
if (held == IntPtr.Zero) {
|
|
949
|
-
throw new Win32Exception(
|
|
982
|
+
throw new Win32Exception(made, "CreateDesktopExW " + stationName + "\\\\" + desktop);
|
|
950
983
|
}
|
|
951
|
-
startup.desktop = desktop;
|
|
984
|
+
startup.desktop = stationName + "\\\\" + desktop;
|
|
952
985
|
}
|
|
953
986
|
startup.flags = UseStdHandles;
|
|
954
987
|
if (noActivate) {
|
|
@@ -11334,7 +11367,8 @@ import {
|
|
|
11334
11367
|
readSync,
|
|
11335
11368
|
rmSync as rmSync4,
|
|
11336
11369
|
statSync as statSync5,
|
|
11337
|
-
writeFileSync as writeFileSync4
|
|
11370
|
+
writeFileSync as writeFileSync4,
|
|
11371
|
+
writeSync as writeSync2
|
|
11338
11372
|
} from "node:fs";
|
|
11339
11373
|
import { availableParallelism, freemem, totalmem } from "node:os";
|
|
11340
11374
|
import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
@@ -11489,6 +11523,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11489
11523
|
const output = openSync2(log, "w");
|
|
11490
11524
|
let child;
|
|
11491
11525
|
let stop;
|
|
11526
|
+
let piped = null;
|
|
11492
11527
|
let helper = null;
|
|
11493
11528
|
try {
|
|
11494
11529
|
if (process6.platform === "win32") {
|
|
@@ -11512,11 +11547,13 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11512
11547
|
stop: stopFile,
|
|
11513
11548
|
step: helper.step
|
|
11514
11549
|
});
|
|
11515
|
-
|
|
11516
|
-
stdio: ["ignore",
|
|
11550
|
+
const helped = spawnHidden(wrapper.command, wrapper.args, {
|
|
11551
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11517
11552
|
env: { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP },
|
|
11518
11553
|
cwd: directory
|
|
11519
11554
|
});
|
|
11555
|
+
piped = [helped.stdout, helped.stderr];
|
|
11556
|
+
child = helped;
|
|
11520
11557
|
stop = () => {
|
|
11521
11558
|
writeFileSync4(stopFile, "");
|
|
11522
11559
|
};
|
|
@@ -11533,7 +11570,9 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11533
11570
|
};
|
|
11534
11571
|
}
|
|
11535
11572
|
} finally {
|
|
11536
|
-
|
|
11573
|
+
if (piped === null) {
|
|
11574
|
+
closeSync2(output);
|
|
11575
|
+
}
|
|
11537
11576
|
}
|
|
11538
11577
|
launch.running.add(stop);
|
|
11539
11578
|
let ended = null;
|
|
@@ -11546,6 +11585,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11546
11585
|
});
|
|
11547
11586
|
})
|
|
11548
11587
|
]);
|
|
11588
|
+
const copied = piped === null ? null : copiedInto(output, piped, exited);
|
|
11549
11589
|
const finished = { now: false };
|
|
11550
11590
|
exited.then(() => {
|
|
11551
11591
|
finished.now = true;
|
|
@@ -11598,12 +11638,42 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11598
11638
|
}
|
|
11599
11639
|
const { code, signal } = await exited;
|
|
11600
11640
|
const freeBytes = code !== null && code >>> 0 >= WINDOWS_EXCEPTION ? freemem() : undefined;
|
|
11641
|
+
if (copied !== null) {
|
|
11642
|
+
await copied;
|
|
11643
|
+
closeSync2(output);
|
|
11644
|
+
}
|
|
11601
11645
|
const failed = helper === null ? null : helperFailure(helper.error);
|
|
11602
11646
|
if (failed !== null && helper !== null) {
|
|
11603
11647
|
ended ??= `the engine could not be started ${helper.how}: ${failed}`;
|
|
11604
11648
|
}
|
|
11605
11649
|
return { code, signal, ended, ...freeBytes === undefined ? {} : { freeBytes } };
|
|
11606
11650
|
}
|
|
11651
|
+
function copiedInto(output, streams, exited) {
|
|
11652
|
+
let writing = true;
|
|
11653
|
+
const ended = streams.map((stream) => new Promise((settle) => {
|
|
11654
|
+
stream.on("data", (chunk) => {
|
|
11655
|
+
if (!writing) {
|
|
11656
|
+
return;
|
|
11657
|
+
}
|
|
11658
|
+
try {
|
|
11659
|
+
writeSync2(output, chunk);
|
|
11660
|
+
} catch {
|
|
11661
|
+
writing = false;
|
|
11662
|
+
}
|
|
11663
|
+
});
|
|
11664
|
+
stream.once("close", settle);
|
|
11665
|
+
}));
|
|
11666
|
+
const all = Promise.all(ended).then(() => {
|
|
11667
|
+
return;
|
|
11668
|
+
});
|
|
11669
|
+
const drained = exited.then(() => delay2(DRAIN_MS, undefined, { ref: false }));
|
|
11670
|
+
return Promise.race([all, drained]).then(() => {
|
|
11671
|
+
writing = false;
|
|
11672
|
+
for (const stream of streams) {
|
|
11673
|
+
stream.destroy();
|
|
11674
|
+
}
|
|
11675
|
+
});
|
|
11676
|
+
}
|
|
11607
11677
|
function endTree(child) {
|
|
11608
11678
|
if (child.pid === undefined) {
|
|
11609
11679
|
return;
|
|
@@ -11616,7 +11686,7 @@ function endTree(child) {
|
|
|
11616
11686
|
process6.kill(-child.pid, "SIGKILL");
|
|
11617
11687
|
} catch {}
|
|
11618
11688
|
}
|
|
11619
|
-
var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5;
|
|
11689
|
+
var EngineUsageError, MOST_ENGINES = 4, SILENCE_FLOOR_SECONDS = 300, CONTROL_SEQUENCE, WINDOWS_EXCEPTION = 3221225472, MEMORY_KEPT, MEMORY_PER_ENGINE, LOOK_MS = 500, COMMIT_ASK_MS = 15000, AUTOLOAD_FAILED = "Failed to instantiate an autoload", KEPT_RUN_DIRECTORIES = 5, DRAIN_MS = 5000;
|
|
11620
11690
|
var init_engine_launch = __esm(() => {
|
|
11621
11691
|
init_alive();
|
|
11622
11692
|
init_desktop();
|
|
@@ -11744,7 +11814,7 @@ import {
|
|
|
11744
11814
|
rmSync as rmSync6,
|
|
11745
11815
|
statSync as statSync6,
|
|
11746
11816
|
watch,
|
|
11747
|
-
writeSync as
|
|
11817
|
+
writeSync as writeSync3
|
|
11748
11818
|
} from "node:fs";
|
|
11749
11819
|
import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
|
|
11750
11820
|
import { join as join8, resolve as resolve4 } from "node:path";
|
|
@@ -11970,7 +12040,7 @@ class EngineSlots {
|
|
|
11970
12040
|
return false;
|
|
11971
12041
|
}
|
|
11972
12042
|
try {
|
|
11973
|
-
|
|
12043
|
+
writeSync3(made, JSON.stringify(this.holder));
|
|
11974
12044
|
} finally {
|
|
11975
12045
|
closeSync3(made);
|
|
11976
12046
|
}
|
|
@@ -12084,7 +12154,7 @@ async function inASlot(body) {
|
|
|
12084
12154
|
held.release();
|
|
12085
12155
|
}
|
|
12086
12156
|
}
|
|
12087
|
-
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT =
|
|
12157
|
+
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT = 2, START_TOLERANCE_MS = 2000, UNWRITTEN_MS = 1e4, SLOT_LOOK_MS = 1000, SLOT_SETTLE_MS = 50, WAIT_SAID_MS = 20000, COMMAND_WAIT_SAID_MS = 2000, START_READ_MS = 1e4, WAITING_FOLDER = "waiting", waitsMarked = 0, ours, saidTo, takenBy;
|
|
12088
12158
|
var init_engine_slots = __esm(() => {
|
|
12089
12159
|
init_alive();
|
|
12090
12160
|
init_call_signal();
|
|
@@ -17364,7 +17434,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17364
17434
|
name: "script_diagnostics",
|
|
17365
17435
|
title: "Script diagnostics",
|
|
17366
17436
|
effect: "read",
|
|
17367
|
-
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine rather than language server: every error that stops a script compiling, up to its first syntax error, with its line and no column (a call to a method another class lacks is not one, since GDScript checks those only on self: the object may be a subclass that has it, so it fails at run time instead), dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and warnings
|
|
17437
|
+
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean. With no editor of this project open, so no language server of it to ask, a short headless engine compiles the scripts instead and the answer says checkedBy: engine rather than language server: every error that stops a script compiling, up to its first syntax error, with its line and no column (a call to a method another class lacks is not one, since GDScript checks those only on self: the object may be a subclass that has it, so it fails at run time instead), dependenciesWithErrors naming the other scripts that failed while it loaded, typesMissingFromTheClassCache naming a class the project declares that the engine could not resolve because the class cache lacks it, with staleAnalysis saying what rewrites the cache, and the warnings at the levels the project sets, which a second engine reports by raising each warning set to warn as an error for that run only, each at its line with no column, warnings being null when that engine could not answer. Of several scripts, a language server that stops answering partway leaves the rest to the engine, and checkedBy then says language server, then engine, with checkedByLanguageServer counting the scripts it answered. Each diagnostic carries its severity (error or warning), its message, and where it is as the editor shows it: line and column counted from 1, and endLine and endColumn just past its end. 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, or a project class it extends does, a method, property, constant, static function or enum member, comes back under contradictedByTheFile with the script declaring it, and so does one a diagnostic names alone as an external class member, read against the class it is looked up on in the line it is on; 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. Two exceptions are named when they apply. For an enum member, constant or static function of a global class the reload comes first: in the one project that has reproduced a stale enum member, editor_rescan with reloadScript on the declaring script cleared it and a plain rescan did not. And 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.",
|
|
17368
17438
|
parameters: {
|
|
17369
17439
|
projectPath: PROJECT_PATH,
|
|
17370
17440
|
scriptPath: {
|
|
@@ -17567,7 +17637,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17567
17637
|
visible: {
|
|
17568
17638
|
type: "boolean",
|
|
17569
17639
|
ops: ["start"],
|
|
17570
|
-
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
17640
|
+
description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard or the pointer. A game a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
|
|
17571
17641
|
},
|
|
17572
17642
|
silent: {
|
|
17573
17643
|
type: "boolean",
|
|
@@ -18665,6 +18735,7 @@ class ProjectScripts {
|
|
|
18665
18735
|
suite(path) {
|
|
18666
18736
|
const cases = new Set;
|
|
18667
18737
|
let said;
|
|
18738
|
+
let together;
|
|
18668
18739
|
let guessed = false;
|
|
18669
18740
|
const seen = new Set;
|
|
18670
18741
|
let at = path;
|
|
@@ -18679,13 +18750,21 @@ class ProjectScripts {
|
|
|
18679
18750
|
}
|
|
18680
18751
|
const declared = NEEDS_WINDOW.exec(read.code)?.[1];
|
|
18681
18752
|
said ??= declared === undefined ? undefined : declared === "true";
|
|
18753
|
+
const kept = TOGETHER.exec(read.code)?.[1];
|
|
18754
|
+
together ??= kept === undefined ? undefined : kept === "true";
|
|
18682
18755
|
guessed ||= this.guessWindow && SCENE_RUNNER.test(read.code);
|
|
18683
18756
|
const base = this.base(at);
|
|
18684
18757
|
if (base === null) {
|
|
18685
18758
|
return null;
|
|
18686
18759
|
}
|
|
18687
18760
|
if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
|
|
18688
|
-
return cases.size > 0 ? {
|
|
18761
|
+
return cases.size > 0 ? {
|
|
18762
|
+
path,
|
|
18763
|
+
cases: [...cases].sort(),
|
|
18764
|
+
windowed: said ?? guessed,
|
|
18765
|
+
headless: said === false,
|
|
18766
|
+
together: together === true
|
|
18767
|
+
} : null;
|
|
18689
18768
|
}
|
|
18690
18769
|
if ("name" in base) {
|
|
18691
18770
|
return null;
|
|
@@ -18808,6 +18887,10 @@ function rowsAfter(before, measured, cutShort) {
|
|
|
18808
18887
|
function round(seconds) {
|
|
18809
18888
|
return Math.round(seconds * 1000) / 1000;
|
|
18810
18889
|
}
|
|
18890
|
+
function splitPastFor(suites, times, engines) {
|
|
18891
|
+
const whole = unitsOf(suites, times, Number.POSITIVE_INFINITY).reduce((sum, unit) => sum + unit.cost, 0);
|
|
18892
|
+
return Math.max(SPLIT_PAST_SECONDS, whole / Math.max(1, engines));
|
|
18893
|
+
}
|
|
18811
18894
|
function median(values) {
|
|
18812
18895
|
if (values.length === 0) {
|
|
18813
18896
|
return;
|
|
@@ -18834,7 +18917,7 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
|
|
|
18834
18917
|
windowed: suite.windowed,
|
|
18835
18918
|
headless: suite.headless
|
|
18836
18919
|
};
|
|
18837
|
-
if (suite.windowed || whole <= splitPast) {
|
|
18920
|
+
if (suite.windowed || suite.together || whole <= splitPast) {
|
|
18838
18921
|
return [wholly];
|
|
18839
18922
|
}
|
|
18840
18923
|
const units = suite.cases.flatMap((func) => {
|
|
@@ -19003,13 +19086,14 @@ function viewportOf(projectPath) {
|
|
|
19003
19086
|
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
19004
19087
|
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
19005
19088
|
}
|
|
19006
|
-
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
|
|
19089
|
+
var SUITE_BASE = "GdUnitTestSuite", CASE_PREFIX = "test_", NEEDS_WINDOW, TOGETHER, TEST_DIRECTORIES, SCENE_RUNNER, NOT_SEARCHED_SETTING = "gdharness/tests/not_searched", GUESS_WINDOW_SETTING = "gdharness/tests/scene_runner_needs_window", UNTIMED_CASE = 0.02, SPLIT_PAST_SECONDS = 10, ENGINE_START_SECONDS = 5, SHARE_FLOOR_SECONDS;
|
|
19007
19090
|
var init_suite_plan = __esm(() => {
|
|
19008
19091
|
init_class_cache();
|
|
19009
19092
|
init_gdscript_source();
|
|
19010
19093
|
init_resources();
|
|
19011
19094
|
init_whole_file();
|
|
19012
19095
|
NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
|
|
19096
|
+
TOGETHER = /^const\s+GDHARNESS_TOGETHER\b[^=\n]*=\s*(true|false)\b/m;
|
|
19013
19097
|
TEST_DIRECTORIES = ["tests", "test"];
|
|
19014
19098
|
SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
19015
19099
|
SHARE_FLOOR_SECONDS = 6 * ENGINE_START_SECONDS;
|
|
@@ -50416,9 +50500,13 @@ function engineChecked(why) {
|
|
|
50416
50500
|
return {
|
|
50417
50501
|
checkedBy: "engine",
|
|
50418
50502
|
languageServer: why,
|
|
50419
|
-
note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, but stops reading a script at its first syntax error, so an error after one shows once it is fixed.
|
|
50503
|
+
note: "No editor of this project is open to ask, so a headless engine compiled the scripts the way loading them does. It reports every error that stops a script compiling, at its line and without a column, but stops reading a script at its first syntax error, so an error after one shows once it is fixed. A second engine reports the warnings, at the levels the project sets, by raising each warning set to warn as an error for that run only, since the engine tells a warning as one only to an editor; each comes at its line without a column, and warnings is null when that engine could not answer. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both with their columns."
|
|
50420
50504
|
};
|
|
50421
50505
|
}
|
|
50506
|
+
function asWarning(raised) {
|
|
50507
|
+
const bare = raised.endsWith(RAISED_WARNING) ? raised.slice(0, -RAISED_WARNING.length) : raised;
|
|
50508
|
+
return bare.startsWith(PARSE_ERROR2) ? bare.slice(PARSE_ERROR2.length) : bare;
|
|
50509
|
+
}
|
|
50422
50510
|
function isDirectory(path) {
|
|
50423
50511
|
try {
|
|
50424
50512
|
return statSync20(path).isDirectory();
|
|
@@ -53206,6 +53294,7 @@ ${error.text}`;
|
|
|
53206
53294
|
const nothingReported = [];
|
|
53207
53295
|
let errors = 0;
|
|
53208
53296
|
let warnings = 0;
|
|
53297
|
+
let engineWarned = 0;
|
|
53209
53298
|
let unread = 0;
|
|
53210
53299
|
let byEngine = null;
|
|
53211
53300
|
let byServer = 0;
|
|
@@ -53224,6 +53313,7 @@ ${error.text}`;
|
|
|
53224
53313
|
}
|
|
53225
53314
|
for (const answer of checked.answers) {
|
|
53226
53315
|
errors += Number(answer["errors"]);
|
|
53316
|
+
engineWarned = answer["warnings"] === null || engineWarned === null ? null : engineWarned + Number(answer["warnings"]);
|
|
53227
53317
|
if (readArray(answer, "diagnostics")?.length === 0) {
|
|
53228
53318
|
nothingReported.push(String(answer["scriptPath"]));
|
|
53229
53319
|
} else {
|
|
@@ -53254,7 +53344,7 @@ ${error.text}`;
|
|
|
53254
53344
|
clean: errors === 0 && unread === 0,
|
|
53255
53345
|
scriptsChecked: scripts.value.length,
|
|
53256
53346
|
errors,
|
|
53257
|
-
warnings:
|
|
53347
|
+
warnings: engineWarned === null ? null : warnings + engineWarned,
|
|
53258
53348
|
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
53259
53349
|
scripts: reported,
|
|
53260
53350
|
nothingReported,
|
|
@@ -53289,6 +53379,20 @@ ${error.text}`;
|
|
|
53289
53379
|
}
|
|
53290
53380
|
return { ok: true, payload, extras: engineExtras({ messages }) };
|
|
53291
53381
|
}
|
|
53382
|
+
async engineWarnings(projectPath, paths) {
|
|
53383
|
+
const ran = await this.operation("check_warnings", { paths: [...paths] }, projectPath);
|
|
53384
|
+
if (!ran.ok) {
|
|
53385
|
+
this.logDebug(`check_warnings did not answer: ${ran.message}`);
|
|
53386
|
+
return null;
|
|
53387
|
+
}
|
|
53388
|
+
return new Map((readArray(ran.payload, "scripts") ?? []).map((entry) => [
|
|
53389
|
+
String(asParams(entry)["path"]),
|
|
53390
|
+
(readArray(asParams(entry), "warnings") ?? []).map((warning) => ({
|
|
53391
|
+
line: Number(asParams(warning)["line"]),
|
|
53392
|
+
message: String(asParams(warning)["message"])
|
|
53393
|
+
}))
|
|
53394
|
+
]));
|
|
53395
|
+
}
|
|
53292
53396
|
async engineDiagnostics(projectPath, scripts, why) {
|
|
53293
53397
|
const named = scripts.map((script) => {
|
|
53294
53398
|
const contained = resolveWithinProject(projectPath, script);
|
|
@@ -53305,21 +53409,25 @@ ${error.text}`;
|
|
|
53305
53409
|
asParams(entry)
|
|
53306
53410
|
]));
|
|
53307
53411
|
const errorsOf = scripts.map((_, at) => readArray(failures.get(named[at] ?? "") ?? {}, "errors") ?? []);
|
|
53412
|
+
const warned = await this.engineWarnings(projectPath, named);
|
|
53308
53413
|
const uncachedPer = uncachedFor(projectPath, errorsOf.map((errors) => errors.map((entry) => String(asParams(entry)["message"]))));
|
|
53309
53414
|
return {
|
|
53310
53415
|
answers: scripts.map((scriptPath, at) => {
|
|
53311
53416
|
const failure = failures.get(named[at] ?? "");
|
|
53312
|
-
const
|
|
53417
|
+
const errors = (errorsOf[at] ?? []).map((entry) => {
|
|
53313
53418
|
const { line, message } = asParams(entry);
|
|
53314
53419
|
return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
|
|
53315
53420
|
});
|
|
53421
|
+
const raised = new Set(errors.map(({ message, line }) => `${line ?? 0}:${String(message)}`));
|
|
53422
|
+
const warnings = warned === null ? [] : (warned.get(named[at] ?? "") ?? []).filter(({ line, message }) => !raised.has(`${line}:${message}`)).map(({ line, message }) => ({ severity: "warning", message: asWarning(message), line }));
|
|
53423
|
+
const diagnostics = [...errors, ...warnings].sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
|
|
53316
53424
|
const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
|
|
53317
53425
|
const uncached = uncachedPer[at] ?? [];
|
|
53318
53426
|
return {
|
|
53319
53427
|
scriptPath,
|
|
53320
|
-
clean:
|
|
53321
|
-
errors:
|
|
53322
|
-
warnings: null,
|
|
53428
|
+
clean: errors.length === 0,
|
|
53429
|
+
errors: errors.length,
|
|
53430
|
+
warnings: warned === null ? null : warnings.length,
|
|
53323
53431
|
diagnostics,
|
|
53324
53432
|
...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies },
|
|
53325
53433
|
...uncached.length === 0 ? {} : {
|
|
@@ -57669,7 +57777,7 @@ async function runGodotServer() {
|
|
|
57669
57777
|
const server = new GodotServer;
|
|
57670
57778
|
await server.run();
|
|
57671
57779
|
}
|
|
57672
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
57780
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
|
|
57673
57781
|
var init_server = __esm(() => {
|
|
57674
57782
|
init_dist2();
|
|
57675
57783
|
init_stdio();
|
|
@@ -58160,7 +58268,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
58160
58268
|
These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
|
|
58161
58269
|
window and refuses headless rather than handing back the last frame anything drew. On Windows a
|
|
58162
58270
|
windowed run this server starts is on a desktop of its own, where it draws and can be captured but
|
|
58163
|
-
never shows or takes the keyboard.
|
|
58271
|
+
never shows or takes the keyboard or the pointer.
|
|
58164
58272
|
|
|
58165
58273
|
The autoload reads these settings and no others. \`${BIND_ADDRESS_SETTING}\`
|
|
58166
58274
|
is \`127.0.0.1\` and should stay there: the command set includes \`call_method\`, \`set_property\`
|
|
@@ -59116,7 +59224,6 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
59116
59224
|
}
|
|
59117
59225
|
}
|
|
59118
59226
|
const times = readTimes(timesFile);
|
|
59119
|
-
const units = unitsOf(suites, times);
|
|
59120
59227
|
const memory = await memoryAvailable();
|
|
59121
59228
|
if (memory.unread !== undefined) {
|
|
59122
59229
|
console.log(`Windows did not say how much memory it can still commit (${memory.unread}), so engines are sized by the physical memory free.`);
|
|
@@ -59125,9 +59232,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
59125
59232
|
if (options.cores !== null && process14.platform === "darwin") {
|
|
59126
59233
|
console.log("macOS offers no way to hold a process to some processors, so --cores holds only the number of engines here.");
|
|
59127
59234
|
}
|
|
59235
|
+
const splitPast = splitPastFor(suites, times, room.engines);
|
|
59236
|
+
const units = unitsOf(suites, times, splitPast);
|
|
59237
|
+
const past = splitPast > SPLIT_PAST_SECONDS ? `an engine's share of this run (${String(Math.round(splitPast))} s)` : `${String(SPLIT_PAST_SECONDS)} s`;
|
|
59128
59238
|
const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
|
|
59129
59239
|
const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
|
|
59130
|
-
const
|
|
59240
|
+
const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
|
|
59241
|
+
const how = options.order === null ? `${windowedSaid(plans)}${roomSaid(plans.length, lanesOf(plans), lanesOf(dealt(asked, declared, options.engines)), room.held, options.engines)}` : options.order === "reversed" ? ", in reverse order of path" : `, shuffled by ${String(options.order)}`;
|
|
59131
59242
|
console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
|
|
59132
59243
|
const windowedAnyway = headlessInAWindow(plans, units);
|
|
59133
59244
|
if (windowedAnyway.length > 0) {
|
|
@@ -59136,13 +59247,18 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
59136
59247
|
}
|
|
59137
59248
|
const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
|
|
59138
59249
|
if (plans.length > 1 && split.length > 0) {
|
|
59139
|
-
console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${
|
|
59250
|
+
console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
|
|
59140
59251
|
}
|
|
59141
59252
|
const byRows = [
|
|
59142
59253
|
...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
|
|
59143
59254
|
];
|
|
59144
59255
|
if (plans.length > 1 && byRows.length > 0) {
|
|
59145
|
-
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${
|
|
59256
|
+
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
|
|
59257
|
+
}
|
|
59258
|
+
const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
|
|
59259
|
+
if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
|
|
59260
|
+
const one = keptWhole.length === 1;
|
|
59261
|
+
console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
|
|
59146
59262
|
}
|
|
59147
59263
|
const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
|
|
59148
59264
|
const taken = [];
|
|
@@ -125,15 +125,16 @@ static func no_animation(player: AnimationPlayer, called: String, animation_name
|
|
|
125
125
|
|
|
126
126
|
static func _summary(anim: Animation) -> Dictionary:
|
|
127
127
|
return {
|
|
128
|
-
"length": anim.length,
|
|
128
|
+
"length": held_seconds(anim.length),
|
|
129
129
|
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
130
|
-
"step": anim.step,
|
|
130
|
+
"step": held_seconds(anim.step),
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
## [param seconds], a key's time, as the shortest decimal holding
|
|
135
|
-
##
|
|
136
|
-
|
|
134
|
+
## [param seconds], a key's time or an animation's length or step, as the shortest decimal holding
|
|
135
|
+
## the same 32-bit float. An animation keeps all three as 32-bit floats, so a key put at 0.7 read
|
|
136
|
+
## back as 0.699999988079071 and a step of 0.1 as 0.100000001490116.
|
|
137
|
+
static func held_seconds(seconds: float) -> float:
|
|
137
138
|
var held: float = PackedFloat32Array([seconds])[0]
|
|
138
139
|
for places: int in range(0, 10):
|
|
139
140
|
var shorter: float = String.num(held, places).to_float()
|
|
@@ -159,7 +160,7 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
159
160
|
var keyframes: Array[Dictionary] = []
|
|
160
161
|
var methods: Dictionary = {}
|
|
161
162
|
for key: int in anim.track_get_key_count(track):
|
|
162
|
-
var keyframe: Dictionary = {"time":
|
|
163
|
+
var keyframe: Dictionary = {"time": held_seconds(anim.track_get_key_time(track, key))}
|
|
163
164
|
if kind == Animation.TYPE_METHOD:
|
|
164
165
|
var method: String = str(anim.method_track_get_name(track, key))
|
|
165
166
|
methods[method] = true
|
|
@@ -320,7 +320,10 @@ func create_animation(args: Dictionary) -> Dictionary:
|
|
|
320
320
|
return "%s would not be in the scene when it loads." % animation_name
|
|
321
321
|
return ""
|
|
322
322
|
var answer: Dictionary = {
|
|
323
|
-
"ok": true,
|
|
323
|
+
"ok": true,
|
|
324
|
+
"animationName": animation_name,
|
|
325
|
+
"length": GdhAnimationReads.held_seconds(anim.length),
|
|
326
|
+
"loopMode": loop_mode_name
|
|
324
327
|
}
|
|
325
328
|
if library[2]:
|
|
326
329
|
answer["libraryMade"] = animation_name.get_slice("/", 0) if animation_name.contains("/") else ""
|
|
@@ -477,9 +480,9 @@ func set_animation(args: Dictionary) -> Dictionary:
|
|
|
477
480
|
var answer: Dictionary = {
|
|
478
481
|
"ok": true,
|
|
479
482
|
"animationName": animation_name,
|
|
480
|
-
"length": anim.length,
|
|
483
|
+
"length": GdhAnimationReads.held_seconds(anim.length),
|
|
481
484
|
"loopMode": GdhAnimationReads.LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
482
|
-
"step": anim.step,
|
|
485
|
+
"step": GdhAnimationReads.held_seconds(anim.step),
|
|
483
486
|
}
|
|
484
487
|
var past: String = _past_length(anim, animation_name)
|
|
485
488
|
if not past.is_empty():
|
|
@@ -444,7 +444,7 @@ static func _keys_at(anim: Animation, calling: Array, times: Array) -> Array:
|
|
|
444
444
|
if pair[1] >= 0 and key != pair[1]:
|
|
445
445
|
continue
|
|
446
446
|
var time: float = anim.track_get_key_time(index, key)
|
|
447
|
-
var _listed: bool = held.append(str(GdhAnimationReads.
|
|
447
|
+
var _listed: bool = held.append(str(GdhAnimationReads.held_seconds(time)))
|
|
448
448
|
for given: float in times:
|
|
449
449
|
if GdhAnimationReads.same_time(time, given):
|
|
450
450
|
at.append([index, key])
|
|
@@ -61,9 +61,11 @@ func _init(host: Node, values: GdhValues) -> void:
|
|
|
61
61
|
## pointer when the game is on one.
|
|
62
62
|
##
|
|
63
63
|
## For the root window the engine asks Windows where the pointer is, and GetCursorPos refuses a
|
|
64
|
-
## process on a desktop that is not in use:
|
|
65
|
-
## with access denied
|
|
66
|
-
##
|
|
64
|
+
## process on a desktop that is not in use: on one of gdharness's own it answers false and leaves the
|
|
65
|
+
## point unwritten, with access denied in the user's window station and with
|
|
66
|
+
## ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION in the one gdharness's desktop is in. The engine does
|
|
67
|
+
## not check, so a game driven there read get_mouse_position() as x 836852416 after a motion to
|
|
68
|
+
## 1310, 314. Nothing here can
|
|
67
69
|
## put a pointer on that desktop: Windows moves one only on the desktop in use, and making this one
|
|
68
70
|
## that would take the screen. What reads the injected event is right, tooltips included, since the
|
|
69
71
|
## engine places a tooltip from the motion that reached the control.
|
|
@@ -125,15 +125,16 @@ static func no_animation(player: AnimationPlayer, called: String, animation_name
|
|
|
125
125
|
|
|
126
126
|
static func _summary(anim: Animation) -> Dictionary:
|
|
127
127
|
return {
|
|
128
|
-
"length": anim.length,
|
|
128
|
+
"length": held_seconds(anim.length),
|
|
129
129
|
"loopMode": LOOP_MODE_NAMES.get(anim.loop_mode, str(anim.loop_mode)),
|
|
130
|
-
"step": anim.step,
|
|
130
|
+
"step": held_seconds(anim.step),
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
|
|
134
|
-
## [param seconds], a key's time, as the shortest decimal holding
|
|
135
|
-
##
|
|
136
|
-
|
|
134
|
+
## [param seconds], a key's time or an animation's length or step, as the shortest decimal holding
|
|
135
|
+
## the same 32-bit float. An animation keeps all three as 32-bit floats, so a key put at 0.7 read
|
|
136
|
+
## back as 0.699999988079071 and a step of 0.1 as 0.100000001490116.
|
|
137
|
+
static func held_seconds(seconds: float) -> float:
|
|
137
138
|
var held: float = PackedFloat32Array([seconds])[0]
|
|
138
139
|
for places: int in range(0, 10):
|
|
139
140
|
var shorter: float = String.num(held, places).to_float()
|
|
@@ -159,7 +160,7 @@ static func _track(anim: Animation, track: int, values: GdhSerialisation) -> Dic
|
|
|
159
160
|
var keyframes: Array[Dictionary] = []
|
|
160
161
|
var methods: Dictionary = {}
|
|
161
162
|
for key: int in anim.track_get_key_count(track):
|
|
162
|
-
var keyframe: Dictionary = {"time":
|
|
163
|
+
var keyframe: Dictionary = {"time": held_seconds(anim.track_get_key_time(track, key))}
|
|
163
164
|
if kind == Animation.TYPE_METHOD:
|
|
164
165
|
var method: String = str(anim.method_track_get_name(track, key))
|
|
165
166
|
methods[method] = true
|
|
@@ -51,6 +51,7 @@ const BEGUN: String = "[gdharness] begun"
|
|
|
51
51
|
## scene or a resource, loads the scripts of the file it opens.
|
|
52
52
|
const AFTER_THE_AUTOLOADS: Array[String] = [
|
|
53
53
|
"check_scripts",
|
|
54
|
+
"check_warnings",
|
|
54
55
|
"create_script",
|
|
55
56
|
# An exporter made in _init, before an editor's main loop has started, crashed the engine.
|
|
56
57
|
"export_options",
|
|
@@ -99,6 +100,8 @@ func _init() -> void:
|
|
|
99
100
|
var params: Dictionary = read
|
|
100
101
|
|
|
101
102
|
_log.info("Executing operation: " + operation)
|
|
103
|
+
if operation == "check_warnings":
|
|
104
|
+
GdhScriptCheck.raise_warnings()
|
|
102
105
|
|
|
103
106
|
var answer_path: String = args[answer_index]
|
|
104
107
|
if operation in AFTER_THE_AUTOLOADS:
|
|
@@ -279,6 +282,8 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
|
|
|
279
282
|
payload = GdhRenameChecks.new(_log).names_taken(params)
|
|
280
283
|
"check_scripts":
|
|
281
284
|
payload = GdhScriptCheck.new(_log).check_scripts(params)
|
|
285
|
+
"check_warnings":
|
|
286
|
+
payload = GdhScriptCheck.new(_log).check_warnings(params)
|
|
282
287
|
|
|
283
288
|
# Plugins and input
|
|
284
289
|
"list_plugins":
|
|
@@ -19,6 +19,15 @@ const GdhLog = preload("logger.gd")
|
|
|
19
19
|
## Where the engine raises a script's compile errors from.
|
|
20
20
|
const RELOAD: String = "GDScript::reload"
|
|
21
21
|
|
|
22
|
+
## What the engine puts on the end of a warning it raised as an error. Kept in step with
|
|
23
|
+
## `RAISED_WARNING` in src/server.ts.
|
|
24
|
+
const RAISED: String = " (Warning treated as error.)"
|
|
25
|
+
|
|
26
|
+
## Where the project's warning levels are, each 0 to ignore, 1 to warn and 2 to raise as an error.
|
|
27
|
+
const WARNING_LEVELS: String = "debug/gdscript/warnings/"
|
|
28
|
+
const WARN: int = 1
|
|
29
|
+
const ERROR_LEVEL: int = 2
|
|
30
|
+
|
|
22
31
|
var _log: GdhLog
|
|
23
32
|
|
|
24
33
|
|
|
@@ -112,3 +121,52 @@ func check_scripts(params: Dictionary) -> Dictionary:
|
|
|
112
121
|
break
|
|
113
122
|
OS.remove_logger(caught)
|
|
114
123
|
return {"checked": checked, "failed": failed, "scripts": scripts}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## Raises every warning the project sets to warn so that the engine reports it as an error, which is
|
|
127
|
+
## the one way it reports a warning outside an editor or a debugger. Not saved. The analyser reads
|
|
128
|
+
## the levels again only once the settings change has been flushed, at the next frame, so this is
|
|
129
|
+
## called in `_init` and the scripts are loaded after it; the project's autoloads, compiled before
|
|
130
|
+
## that frame, compile under the project's own levels. A warning set to ignore stays ignored, and
|
|
131
|
+
## @warning_ignore and the directory rules hold as they do in the editor, since the analyser applies
|
|
132
|
+
## them itself.
|
|
133
|
+
static func raise_warnings() -> void:
|
|
134
|
+
for property: Dictionary in ProjectSettings.get_property_list():
|
|
135
|
+
var setting: String = str(property.get("name", ""))
|
|
136
|
+
if (
|
|
137
|
+
not setting.begins_with(WARNING_LEVELS)
|
|
138
|
+
or GdhRead.as_int(property.get("type", TYPE_NIL)) != TYPE_INT
|
|
139
|
+
):
|
|
140
|
+
continue
|
|
141
|
+
if GdhRead.as_int(ProjectSettings.get_setting(setting)) == WARN:
|
|
142
|
+
ProjectSettings.set_setting(setting, ERROR_LEVEL)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## The warnings of each script in [code]paths[/code], under the levels [method raise_warnings] left:
|
|
146
|
+
## [code]scripts[/code] holds each that has any, with its warnings' lines and the engine's messages
|
|
147
|
+
## as raised. The analyser runs before anything is compiled, so a script whose dependency failed on a
|
|
148
|
+
## warning still reports its own, and loading on past a failure loses nothing; only what is raised at
|
|
149
|
+
## the script's own path is its, since a dependency is read on its own entry as well.
|
|
150
|
+
func check_warnings(params: Dictionary) -> Dictionary:
|
|
151
|
+
var asked: Array = params.get("paths", [])
|
|
152
|
+
var caught: GdhCaught = GdhCaught.new()
|
|
153
|
+
OS.add_logger(caught)
|
|
154
|
+
var checked: Array[String] = []
|
|
155
|
+
var scripts: Array[Dictionary] = []
|
|
156
|
+
for entry: Variant in asked:
|
|
157
|
+
var path: String = str(entry)
|
|
158
|
+
checked.append(path)
|
|
159
|
+
caught.raised.clear()
|
|
160
|
+
var _loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
161
|
+
var warnings: Array[Dictionary] = []
|
|
162
|
+
for error: Dictionary in caught.raised:
|
|
163
|
+
var message: String = str(error["message"])
|
|
164
|
+
if str(error["path"]) != path or not message.ends_with(RAISED):
|
|
165
|
+
continue
|
|
166
|
+
var found: Dictionary = {"line": GdhRead.as_int(error["line"]), "message": message}
|
|
167
|
+
if not found in warnings:
|
|
168
|
+
warnings.append(found)
|
|
169
|
+
if not warnings.is_empty():
|
|
170
|
+
scripts.append({"path": path, "warnings": warnings})
|
|
171
|
+
OS.remove_logger(caught)
|
|
172
|
+
return {"checked": checked, "scripts": scripts}
|
package/build/keeper.js
CHANGED
|
@@ -7053,7 +7053,18 @@ public static class GdharnessDesktop {
|
|
|
7053
7053
|
}
|
|
7054
7054
|
|
|
7055
7055
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
7056
|
-
static extern IntPtr
|
|
7056
|
+
static extern IntPtr CreateDesktopExW(string name, IntPtr device, IntPtr mode, uint flags, uint access, IntPtr attributes, uint heapKilobytes, IntPtr reserved);
|
|
7057
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
7058
|
+
static extern IntPtr CreateWindowStationW(string name, uint flags, uint access, IntPtr attributes);
|
|
7059
|
+
[DllImport("user32.dll", SetLastError = true)] static extern IntPtr GetProcessWindowStation();
|
|
7060
|
+
[DllImport("user32.dll", SetLastError = true)] static extern bool SetProcessWindowStation(IntPtr station);
|
|
7061
|
+
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
7062
|
+
static extern bool GetUserObjectInformationW(IntPtr handle, int index, StringBuilder into, int size, out int needed);
|
|
7063
|
+
const uint WindowStationAllAccess = 0x37F;
|
|
7064
|
+
const int ObjectName = 2;
|
|
7065
|
+
// What Windows gives an interactive desktop. A non-interactive station's desktops get a small
|
|
7066
|
+
// fraction of that by default, and one desktop here holds every engine a run starts.
|
|
7067
|
+
const uint DesktopHeapKilobytes = 20480;
|
|
7057
7068
|
|
|
7058
7069
|
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
7059
7070
|
static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out Started started);
|
|
@@ -7144,12 +7155,34 @@ public static class GdharnessDesktop {
|
|
|
7144
7155
|
StartupInfo startup = new StartupInfo();
|
|
7145
7156
|
startup.cb = Marshal.SizeOf(typeof(StartupInfo));
|
|
7146
7157
|
if (desktop.Length > 0) {
|
|
7147
|
-
Step(stepFile, "
|
|
7148
|
-
IntPtr
|
|
7158
|
+
Step(stepFile, "opening the session's own window station");
|
|
7159
|
+
IntPtr station = CreateWindowStationW(null, 0, WindowStationAllAccess, IntPtr.Zero);
|
|
7160
|
+
if (station == IntPtr.Zero) {
|
|
7161
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateWindowStationW");
|
|
7162
|
+
}
|
|
7163
|
+
StringBuilder stationName = new StringBuilder(256);
|
|
7164
|
+
int needed;
|
|
7165
|
+
if (!GetUserObjectInformationW(station, ObjectName, stationName, stationName.Capacity * 2, out needed)) {
|
|
7166
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "GetUserObjectInformationW");
|
|
7167
|
+
}
|
|
7168
|
+
// A desktop is made in the calling process's station, so this one is the caller's while it is.
|
|
7169
|
+
IntPtr own = GetProcessWindowStation();
|
|
7170
|
+
if (!SetProcessWindowStation(station)) {
|
|
7171
|
+
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetProcessWindowStation");
|
|
7172
|
+
}
|
|
7173
|
+
IntPtr held;
|
|
7174
|
+
int made;
|
|
7175
|
+
try {
|
|
7176
|
+
Step(stepFile, "creating the " + desktop + " desktop");
|
|
7177
|
+
held = CreateDesktopExW(desktop, IntPtr.Zero, IntPtr.Zero, 0, GenericAll, IntPtr.Zero, DesktopHeapKilobytes, IntPtr.Zero);
|
|
7178
|
+
made = Marshal.GetLastWin32Error();
|
|
7179
|
+
} finally {
|
|
7180
|
+
SetProcessWindowStation(own);
|
|
7181
|
+
}
|
|
7149
7182
|
if (held == IntPtr.Zero) {
|
|
7150
|
-
throw new Win32Exception(
|
|
7183
|
+
throw new Win32Exception(made, "CreateDesktopExW " + stationName + "\\\\" + desktop);
|
|
7151
7184
|
}
|
|
7152
|
-
startup.desktop = desktop;
|
|
7185
|
+
startup.desktop = stationName + "\\\\" + desktop;
|
|
7153
7186
|
}
|
|
7154
7187
|
startup.flags = UseStdHandles;
|
|
7155
7188
|
if (noActivate) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.40",
|
|
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",
|