gdharness 1.3.37 → 1.3.39
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 +121 -28
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +20 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +6 -2
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +54 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +18 -59
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +57 -1
- package/build/godot/operations/dependencies.gd +58 -9
- package/build/godot/operations/godot_operations.gd +5 -0
- package/build/godot/operations/script_check.gd +58 -0
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -11334,7 +11334,8 @@ import {
|
|
|
11334
11334
|
readSync,
|
|
11335
11335
|
rmSync as rmSync4,
|
|
11336
11336
|
statSync as statSync5,
|
|
11337
|
-
writeFileSync as writeFileSync4
|
|
11337
|
+
writeFileSync as writeFileSync4,
|
|
11338
|
+
writeSync as writeSync2
|
|
11338
11339
|
} from "node:fs";
|
|
11339
11340
|
import { availableParallelism, freemem, totalmem } from "node:os";
|
|
11340
11341
|
import { isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep2 } from "node:path";
|
|
@@ -11489,6 +11490,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11489
11490
|
const output = openSync2(log, "w");
|
|
11490
11491
|
let child;
|
|
11491
11492
|
let stop;
|
|
11493
|
+
let piped = null;
|
|
11492
11494
|
let helper = null;
|
|
11493
11495
|
try {
|
|
11494
11496
|
if (process6.platform === "win32") {
|
|
@@ -11512,11 +11514,13 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11512
11514
|
stop: stopFile,
|
|
11513
11515
|
step: helper.step
|
|
11514
11516
|
});
|
|
11515
|
-
|
|
11516
|
-
stdio: ["ignore",
|
|
11517
|
+
const helped = spawnHidden(wrapper.command, wrapper.args, {
|
|
11518
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11517
11519
|
env: { ...env, [DESKTOP_VARIABLE]: HIDDEN_DESKTOP },
|
|
11518
11520
|
cwd: directory
|
|
11519
11521
|
});
|
|
11522
|
+
piped = [helped.stdout, helped.stderr];
|
|
11523
|
+
child = helped;
|
|
11520
11524
|
stop = () => {
|
|
11521
11525
|
writeFileSync4(stopFile, "");
|
|
11522
11526
|
};
|
|
@@ -11533,7 +11537,9 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11533
11537
|
};
|
|
11534
11538
|
}
|
|
11535
11539
|
} finally {
|
|
11536
|
-
|
|
11540
|
+
if (piped === null) {
|
|
11541
|
+
closeSync2(output);
|
|
11542
|
+
}
|
|
11537
11543
|
}
|
|
11538
11544
|
launch.running.add(stop);
|
|
11539
11545
|
let ended = null;
|
|
@@ -11546,6 +11552,7 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11546
11552
|
});
|
|
11547
11553
|
})
|
|
11548
11554
|
]);
|
|
11555
|
+
const copied = piped === null ? null : copiedInto(output, piped, exited);
|
|
11549
11556
|
const finished = { now: false };
|
|
11550
11557
|
exited.then(() => {
|
|
11551
11558
|
finished.now = true;
|
|
@@ -11598,12 +11605,42 @@ async function watchedEngine(launch, args, directory, log, silenceSeconds, silen
|
|
|
11598
11605
|
}
|
|
11599
11606
|
const { code, signal } = await exited;
|
|
11600
11607
|
const freeBytes = code !== null && code >>> 0 >= WINDOWS_EXCEPTION ? freemem() : undefined;
|
|
11608
|
+
if (copied !== null) {
|
|
11609
|
+
await copied;
|
|
11610
|
+
closeSync2(output);
|
|
11611
|
+
}
|
|
11601
11612
|
const failed = helper === null ? null : helperFailure(helper.error);
|
|
11602
11613
|
if (failed !== null && helper !== null) {
|
|
11603
11614
|
ended ??= `the engine could not be started ${helper.how}: ${failed}`;
|
|
11604
11615
|
}
|
|
11605
11616
|
return { code, signal, ended, ...freeBytes === undefined ? {} : { freeBytes } };
|
|
11606
11617
|
}
|
|
11618
|
+
function copiedInto(output, streams, exited) {
|
|
11619
|
+
let writing = true;
|
|
11620
|
+
const ended = streams.map((stream) => new Promise((settle) => {
|
|
11621
|
+
stream.on("data", (chunk) => {
|
|
11622
|
+
if (!writing) {
|
|
11623
|
+
return;
|
|
11624
|
+
}
|
|
11625
|
+
try {
|
|
11626
|
+
writeSync2(output, chunk);
|
|
11627
|
+
} catch {
|
|
11628
|
+
writing = false;
|
|
11629
|
+
}
|
|
11630
|
+
});
|
|
11631
|
+
stream.once("close", settle);
|
|
11632
|
+
}));
|
|
11633
|
+
const all = Promise.all(ended).then(() => {
|
|
11634
|
+
return;
|
|
11635
|
+
});
|
|
11636
|
+
const drained = exited.then(() => delay2(DRAIN_MS, undefined, { ref: false }));
|
|
11637
|
+
return Promise.race([all, drained]).then(() => {
|
|
11638
|
+
writing = false;
|
|
11639
|
+
for (const stream of streams) {
|
|
11640
|
+
stream.destroy();
|
|
11641
|
+
}
|
|
11642
|
+
});
|
|
11643
|
+
}
|
|
11607
11644
|
function endTree(child) {
|
|
11608
11645
|
if (child.pid === undefined) {
|
|
11609
11646
|
return;
|
|
@@ -11616,7 +11653,7 @@ function endTree(child) {
|
|
|
11616
11653
|
process6.kill(-child.pid, "SIGKILL");
|
|
11617
11654
|
} catch {}
|
|
11618
11655
|
}
|
|
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;
|
|
11656
|
+
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
11657
|
var init_engine_launch = __esm(() => {
|
|
11621
11658
|
init_alive();
|
|
11622
11659
|
init_desktop();
|
|
@@ -11744,7 +11781,7 @@ import {
|
|
|
11744
11781
|
rmSync as rmSync6,
|
|
11745
11782
|
statSync as statSync6,
|
|
11746
11783
|
watch,
|
|
11747
|
-
writeSync as
|
|
11784
|
+
writeSync as writeSync3
|
|
11748
11785
|
} from "node:fs";
|
|
11749
11786
|
import { availableParallelism as availableParallelism2, homedir as homedir2 } from "node:os";
|
|
11750
11787
|
import { join as join8, resolve as resolve4 } from "node:path";
|
|
@@ -11970,7 +12007,7 @@ class EngineSlots {
|
|
|
11970
12007
|
return false;
|
|
11971
12008
|
}
|
|
11972
12009
|
try {
|
|
11973
|
-
|
|
12010
|
+
writeSync3(made, JSON.stringify(this.holder));
|
|
11974
12011
|
} finally {
|
|
11975
12012
|
closeSync3(made);
|
|
11976
12013
|
}
|
|
@@ -12084,7 +12121,7 @@ async function inASlot(body) {
|
|
|
12084
12121
|
held.release();
|
|
12085
12122
|
}
|
|
12086
12123
|
}
|
|
12087
|
-
var SLOTS_VARIABLE = "GODOT_ENGINE_SLOTS", SLOT_COUNT_VARIABLE = "GODOT_ENGINE_SLOT_COUNT", PROCESSORS_A_SLOT =
|
|
12124
|
+
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
12125
|
var init_engine_slots = __esm(() => {
|
|
12089
12126
|
init_alive();
|
|
12090
12127
|
init_call_signal();
|
|
@@ -16657,7 +16694,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16657
16694
|
direction: {
|
|
16658
16695
|
type: "string",
|
|
16659
16696
|
enum: ["forward", "reverse"],
|
|
16660
|
-
description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path, and a shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
16697
|
+
description: "forward: what this resource loads, with each reference closing a cycle marked circular and the cycle under circular_references, a script's dependencies including every script whose class_name it uses, marked by_class with the name, and the script of every autoload it names, marked by_autoload with the autoload's name. An ext_resource whose uid names another file than its path is followed to that file, as the engine loads it, with the path written under stale_path; one whose uid names no file the project has is followed by its path, as the engine does after warning, with the uid under unknown_uid; and a load or preload of a uid the project does not know is listed under that uid as missing. A shader's #include is followed as the preprocessor reads it, relative to the shader. A file the depth stops at carries beyond_depth, how many files it names that were not walked. summary.total_resources is the file asked about, so 1; total_dependencies counts each file reached, directly or through another, once however many places load it; total_references counts the entries in the tree, and missing names every file referenced that does not exist, with the files referring to it. reverse: every file that refers to it and how, for a file that is gone as well while anything still refers to it, answered with exists: false (a missing file nothing refers to is refused, naming the nearest files), a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name, and for an autoload's script every use of the autoload's name, with autoload naming it. Each reference carries a kind: ext_resource, extends, preload, load, include or path for a reference by path, with stale_path when an ext_resource reaches it by its uid under another path, and unknown_uid when one names it by path beside a uid naming no file; setting, plugin, export_preset and import for one in project.godot, a plugin.cfg, export_presets.cfg or an import sidecar; class_name for a use of the declared name, the declaration itself being the script asked about; autoload for code naming the autoload. Three kinds are not uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs; comment is one in ordinary prose, which it would only make stale; and stale_path is an ext_resource naming it by a path the engine does not load, since its uid leads to the file under loads. summary.in_code is the count with those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
16661
16698
|
},
|
|
16662
16699
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
16663
16700
|
includeBuiltin: {
|
|
@@ -16676,7 +16713,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16676
16713
|
name: "project_import",
|
|
16677
16714
|
title: "Asset imports",
|
|
16678
16715
|
description: "The import pipeline: what needs importing, how a resource is imported, reimports, UIDs, and the global class list the editor and the engine read.",
|
|
16679
|
-
answer: "reimport is answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. importedByTheScan names what the editor's scan imported first, and alsoImported, on a reimport of one resourcePath, the others it imported. wroteInProject names, by source, what an import wrote into the project itself rather than under .godot, such as the .translation a CSV gives each language, which version control sees and internationalization/locale/translations names. When nothing needed reimporting, the two lists are empty, a note says so, and there is no via. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, even when nothing needed reimporting; one resourcePath leaves it out. stillImporting is an editor that had not finished within timeoutMs.",
|
|
16716
|
+
answer: "reimport is answered from a second status read: reimported lists what is current now, notReimported what is not, one entry per status and reason with the paths sharing them, and via says which of the two did the work. importedByTheScan names what the editor's scan imported first, and alsoImported, on a reimport of one resourcePath, the others it imported. wroteInProject names, by source, what an import wrote into the project itself rather than under .godot, such as the .translation a CSV gives each language, which version control sees and internationalization/locale/translations names. The import gives every script without a .uid sidecar one, as the editor's scan does, and uidsWritten names those scripts, counted per addon under uidsWrittenInAddons for the ones inside an addon. When nothing needed reimporting, the two lists are empty, a note says so, and there is no via. A whole-project reimport also answers missingSource, the .import files whose source is not beside them, empty when there are none, even when nothing needed reimporting; one resourcePath leaves it out. stillImporting is an editor that had not finished within timeoutMs.",
|
|
16680
16717
|
parameters: {
|
|
16681
16718
|
projectPath: PROJECT_PATH,
|
|
16682
16719
|
resourcePath: { ...RESOURCE_PATH, ops: ["status", "options", "set_options", "uid", "reimport"] },
|
|
@@ -17343,7 +17380,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17343
17380
|
operations: {
|
|
17344
17381
|
structure: {
|
|
17345
17382
|
effect: "read",
|
|
17346
|
-
summary: "
|
|
17383
|
+
summary: `functions (each parameter's default the source text after its =, so = null reads "null", and null for a parameter with none), variables (static ones marked is_static; one written with := has the type of a literal or built-in constructor it is given, marked inferred, as a constant always does), signals, constants, enums, inner_classes (each with its name, line and extends, and its own members in these same fields), class_name, extends and line_count, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies. A member or inner class with a ## documentation comment directly above it carries the comment's text as doc, and the script's own, the ## block above class_name or extends or one followed by a blank line before any member, comes back as class_doc`,
|
|
17347
17384
|
requires: []
|
|
17348
17385
|
},
|
|
17349
17386
|
symbols: {
|
|
@@ -17364,7 +17401,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17364
17401
|
name: "script_diagnostics",
|
|
17365
17402
|
title: "Script diagnostics",
|
|
17366
17403
|
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
|
|
17404
|
+
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
17405
|
parameters: {
|
|
17369
17406
|
projectPath: PROJECT_PATH,
|
|
17370
17407
|
scriptPath: {
|
|
@@ -18077,7 +18114,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18077
18114
|
name: "runtime_input",
|
|
18078
18115
|
title: "Send input to the game",
|
|
18079
18116
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
18080
|
-
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. A button answers pressed_signal, whether its pressed signal fired during the click. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. key, mouse_click, click, joypad_button and joypad_motion answer actions, the InputMap actions the event also presses; for a motion, those its direction reaches past their deadzone, and none for one inside it. mouse_click, mouse_motion and mouse_drag answer hovered, the control under the pointer once the events are handled, or null over none; the other events are aimed at nothing. mouse_click with a press, and mouse_drag, answer press_went_to, the path of what the press went to (a control, or a window embedded over the point), read before it, or null when nothing there takes the pointer; given nodePath naming a Control, landed says whether that is the control or inside it. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed, and one on a control with something drawn over it names that, a window stacked above the one the control is in among them. An action, a key, a joypad_button or a joypad_motion answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button, and a stick or dpad pressing ui_down moves a focused list's selection, or the focus, and never reaches the game's handler. Where the press moved the focus, focused_after names the Control holding it once the event was handled, or null when nothing does. A mouse_click, mouse_motion or mouse_drag given nodePath answers it under path, its points in the pixels of the window they went to, and that window under window when it is not the game's own. One with a point outside the viewport it went to says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
|
|
18117
|
+
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. A button answers pressed_signal, whether its pressed signal fired during the click. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again and pressed whether it is left held down (true only after pressed true), for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. key, mouse_click, click, joypad_button and joypad_motion answer actions, the InputMap actions the event also presses; for a motion, those its direction reaches past their deadzone, and none for one inside it. mouse_click, mouse_motion and mouse_drag answer hovered, the control under the pointer once the events are handled, or null over none; the other events are aimed at nothing. mouse_click with a press, and mouse_drag, answer press_went_to, the path of what the press went to (a control, or a window embedded over the point), read before it, or null when nothing there takes the pointer; given nodePath naming a Control, landed says whether that is the control or inside it. A click that did not land on a control whose mouse_filter is ignore, as a Label's is, says so under why_not_landed, and one on a control with something drawn over it names that, a window stacked above the one the control is in among them. An action, a key, a joypad_button or a joypad_motion answers focused with the path of the Control holding the keyboard focus when it went in, since that Control gets it before the game's _unhandled_input: Space or Enter on a focused button presses the button, and a stick or dpad pressing ui_down moves a focused list's selection, or the focus, and never reaches the game's handler. Where the press moved the focus, focused_after names the Control holding it once the event was handled, or null when nothing does. A key that a focused LineEdit or TextEdit took as typing names that field under typed_into, with what it holds under holds, since a field being edited keeps the key from the game's _unhandled_input. editing_ended true, on a key or on text, is a LineEdit that stopped being edited, as Enter does by submitting it: it keeps the focus, and nothing typed into it lands until it is clicked again. text with the focus on a control that is no LineEdit or TextEdit answers holds null and a note saying so, since a focused button takes a space or Enter as a press. A mouse_click, mouse_motion or mouse_drag given nodePath answers it under path, its points in the pixels of the window they went to, and that window under window when it is not the game's own. One with a point outside the viewport it went to says so under outside_viewport, with the viewport's rectangle, since no control there receives it.`,
|
|
18081
18118
|
parameters: {
|
|
18082
18119
|
projectPath: RUNNING_PROJECT_PATH,
|
|
18083
18120
|
pid: RUNNING_PID,
|
|
@@ -18665,6 +18702,7 @@ class ProjectScripts {
|
|
|
18665
18702
|
suite(path) {
|
|
18666
18703
|
const cases = new Set;
|
|
18667
18704
|
let said;
|
|
18705
|
+
let together;
|
|
18668
18706
|
let guessed = false;
|
|
18669
18707
|
const seen = new Set;
|
|
18670
18708
|
let at = path;
|
|
@@ -18679,13 +18717,21 @@ class ProjectScripts {
|
|
|
18679
18717
|
}
|
|
18680
18718
|
const declared = NEEDS_WINDOW.exec(read.code)?.[1];
|
|
18681
18719
|
said ??= declared === undefined ? undefined : declared === "true";
|
|
18720
|
+
const kept = TOGETHER.exec(read.code)?.[1];
|
|
18721
|
+
together ??= kept === undefined ? undefined : kept === "true";
|
|
18682
18722
|
guessed ||= this.guessWindow && SCENE_RUNNER.test(read.code);
|
|
18683
18723
|
const base = this.base(at);
|
|
18684
18724
|
if (base === null) {
|
|
18685
18725
|
return null;
|
|
18686
18726
|
}
|
|
18687
18727
|
if ("name" in base ? base.name === SUITE_BASE : base.path === this.classes.get(SUITE_BASE)) {
|
|
18688
|
-
return cases.size > 0 ? {
|
|
18728
|
+
return cases.size > 0 ? {
|
|
18729
|
+
path,
|
|
18730
|
+
cases: [...cases].sort(),
|
|
18731
|
+
windowed: said ?? guessed,
|
|
18732
|
+
headless: said === false,
|
|
18733
|
+
together: together === true
|
|
18734
|
+
} : null;
|
|
18689
18735
|
}
|
|
18690
18736
|
if ("name" in base) {
|
|
18691
18737
|
return null;
|
|
@@ -18808,6 +18854,10 @@ function rowsAfter(before, measured, cutShort) {
|
|
|
18808
18854
|
function round(seconds) {
|
|
18809
18855
|
return Math.round(seconds * 1000) / 1000;
|
|
18810
18856
|
}
|
|
18857
|
+
function splitPastFor(suites, times, engines) {
|
|
18858
|
+
const whole = unitsOf(suites, times, Number.POSITIVE_INFINITY).reduce((sum, unit) => sum + unit.cost, 0);
|
|
18859
|
+
return Math.max(SPLIT_PAST_SECONDS, whole / Math.max(1, engines));
|
|
18860
|
+
}
|
|
18811
18861
|
function median(values) {
|
|
18812
18862
|
if (values.length === 0) {
|
|
18813
18863
|
return;
|
|
@@ -18834,7 +18884,7 @@ function unitsOf(suites, times, splitPast = SPLIT_PAST_SECONDS) {
|
|
|
18834
18884
|
windowed: suite.windowed,
|
|
18835
18885
|
headless: suite.headless
|
|
18836
18886
|
};
|
|
18837
|
-
if (suite.windowed || whole <= splitPast) {
|
|
18887
|
+
if (suite.windowed || suite.together || whole <= splitPast) {
|
|
18838
18888
|
return [wholly];
|
|
18839
18889
|
}
|
|
18840
18890
|
const units = suite.cases.flatMap((func) => {
|
|
@@ -19003,13 +19053,14 @@ function viewportOf(projectPath) {
|
|
|
19003
19053
|
const height = /^window\/size\/viewport_height\s*=\s*(\d+)/m.exec(text)?.[1];
|
|
19004
19054
|
return width === undefined || height === undefined ? null : { width: Number(width), height: Number(height) };
|
|
19005
19055
|
}
|
|
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;
|
|
19056
|
+
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
19057
|
var init_suite_plan = __esm(() => {
|
|
19008
19058
|
init_class_cache();
|
|
19009
19059
|
init_gdscript_source();
|
|
19010
19060
|
init_resources();
|
|
19011
19061
|
init_whole_file();
|
|
19012
19062
|
NEEDS_WINDOW = /^const\s+GDHARNESS_NEEDS_WINDOW\b[^=\n]*=\s*(true|false)\b/m;
|
|
19063
|
+
TOGETHER = /^const\s+GDHARNESS_TOGETHER\b[^=\n]*=\s*(true|false)\b/m;
|
|
19013
19064
|
TEST_DIRECTORIES = ["tests", "test"];
|
|
19014
19065
|
SCENE_RUNNER = /\bscene_runner\s*\(/;
|
|
19015
19066
|
SHARE_FLOOR_SECONDS = 6 * ENGINE_START_SECONDS;
|
|
@@ -50416,9 +50467,13 @@ function engineChecked(why) {
|
|
|
50416
50467
|
return {
|
|
50417
50468
|
checkedBy: "engine",
|
|
50418
50469
|
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.
|
|
50470
|
+
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
50471
|
};
|
|
50421
50472
|
}
|
|
50473
|
+
function asWarning(raised) {
|
|
50474
|
+
const bare = raised.endsWith(RAISED_WARNING) ? raised.slice(0, -RAISED_WARNING.length) : raised;
|
|
50475
|
+
return bare.startsWith(PARSE_ERROR2) ? bare.slice(PARSE_ERROR2.length) : bare;
|
|
50476
|
+
}
|
|
50422
50477
|
function isDirectory(path) {
|
|
50423
50478
|
try {
|
|
50424
50479
|
return statSync20(path).isDirectory();
|
|
@@ -51932,8 +51987,18 @@ class GodotServer {
|
|
|
51932
51987
|
if (!contained.ok) {
|
|
51933
51988
|
return contained.response;
|
|
51934
51989
|
}
|
|
51990
|
+
const unminted = scriptsWithoutUid(project.value.path);
|
|
51935
51991
|
const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
|
|
51936
|
-
|
|
51992
|
+
if (!outcome.ok) {
|
|
51993
|
+
return outcome.response;
|
|
51994
|
+
}
|
|
51995
|
+
const stillUnminted = scriptsWithoutUid(project.value.path);
|
|
51996
|
+
const uids = withAddonPathsCounted(unminted.filter((script) => !stillUnminted.includes(script)).map((script) => `res://${script}`));
|
|
51997
|
+
return this.jsonTextResponse({
|
|
51998
|
+
...outcome.payload,
|
|
51999
|
+
...uids.own.length === 0 ? {} : { uidsWritten: uids.own },
|
|
52000
|
+
...Object.keys(uids.inAddons).length === 0 ? {} : { uidsWrittenInAddons: uids.inAddons }
|
|
52001
|
+
});
|
|
51937
52002
|
}
|
|
51938
52003
|
editorHasOpen(projectPath) {
|
|
51939
52004
|
const open = this.godotBridge.getStatus().projectPath;
|
|
@@ -53196,6 +53261,7 @@ ${error.text}`;
|
|
|
53196
53261
|
const nothingReported = [];
|
|
53197
53262
|
let errors = 0;
|
|
53198
53263
|
let warnings = 0;
|
|
53264
|
+
let engineWarned = 0;
|
|
53199
53265
|
let unread = 0;
|
|
53200
53266
|
let byEngine = null;
|
|
53201
53267
|
let byServer = 0;
|
|
@@ -53214,6 +53280,7 @@ ${error.text}`;
|
|
|
53214
53280
|
}
|
|
53215
53281
|
for (const answer of checked.answers) {
|
|
53216
53282
|
errors += Number(answer["errors"]);
|
|
53283
|
+
engineWarned = answer["warnings"] === null || engineWarned === null ? null : engineWarned + Number(answer["warnings"]);
|
|
53217
53284
|
if (readArray(answer, "diagnostics")?.length === 0) {
|
|
53218
53285
|
nothingReported.push(String(answer["scriptPath"]));
|
|
53219
53286
|
} else {
|
|
@@ -53244,7 +53311,7 @@ ${error.text}`;
|
|
|
53244
53311
|
clean: errors === 0 && unread === 0,
|
|
53245
53312
|
scriptsChecked: scripts.value.length,
|
|
53246
53313
|
errors,
|
|
53247
|
-
warnings:
|
|
53314
|
+
warnings: engineWarned === null ? null : warnings + engineWarned,
|
|
53248
53315
|
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
53249
53316
|
scripts: reported,
|
|
53250
53317
|
nothingReported,
|
|
@@ -53279,6 +53346,20 @@ ${error.text}`;
|
|
|
53279
53346
|
}
|
|
53280
53347
|
return { ok: true, payload, extras: engineExtras({ messages }) };
|
|
53281
53348
|
}
|
|
53349
|
+
async engineWarnings(projectPath, paths) {
|
|
53350
|
+
const ran = await this.operation("check_warnings", { paths: [...paths] }, projectPath);
|
|
53351
|
+
if (!ran.ok) {
|
|
53352
|
+
this.logDebug(`check_warnings did not answer: ${ran.message}`);
|
|
53353
|
+
return null;
|
|
53354
|
+
}
|
|
53355
|
+
return new Map((readArray(ran.payload, "scripts") ?? []).map((entry) => [
|
|
53356
|
+
String(asParams(entry)["path"]),
|
|
53357
|
+
(readArray(asParams(entry), "warnings") ?? []).map((warning) => ({
|
|
53358
|
+
line: Number(asParams(warning)["line"]),
|
|
53359
|
+
message: String(asParams(warning)["message"])
|
|
53360
|
+
}))
|
|
53361
|
+
]));
|
|
53362
|
+
}
|
|
53282
53363
|
async engineDiagnostics(projectPath, scripts, why) {
|
|
53283
53364
|
const named = scripts.map((script) => {
|
|
53284
53365
|
const contained = resolveWithinProject(projectPath, script);
|
|
@@ -53295,21 +53376,25 @@ ${error.text}`;
|
|
|
53295
53376
|
asParams(entry)
|
|
53296
53377
|
]));
|
|
53297
53378
|
const errorsOf = scripts.map((_, at) => readArray(failures.get(named[at] ?? "") ?? {}, "errors") ?? []);
|
|
53379
|
+
const warned = await this.engineWarnings(projectPath, named);
|
|
53298
53380
|
const uncachedPer = uncachedFor(projectPath, errorsOf.map((errors) => errors.map((entry) => String(asParams(entry)["message"]))));
|
|
53299
53381
|
return {
|
|
53300
53382
|
answers: scripts.map((scriptPath, at) => {
|
|
53301
53383
|
const failure = failures.get(named[at] ?? "");
|
|
53302
|
-
const
|
|
53384
|
+
const errors = (errorsOf[at] ?? []).map((entry) => {
|
|
53303
53385
|
const { line, message } = asParams(entry);
|
|
53304
53386
|
return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
|
|
53305
53387
|
});
|
|
53388
|
+
const raised = new Set(errors.map(({ message, line }) => `${line ?? 0}:${String(message)}`));
|
|
53389
|
+
const warnings = warned === null ? [] : (warned.get(named[at] ?? "") ?? []).filter(({ line, message }) => !raised.has(`${line}:${message}`)).map(({ line, message }) => ({ severity: "warning", message: asWarning(message), line }));
|
|
53390
|
+
const diagnostics = [...errors, ...warnings].sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
|
|
53306
53391
|
const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
|
|
53307
53392
|
const uncached = uncachedPer[at] ?? [];
|
|
53308
53393
|
return {
|
|
53309
53394
|
scriptPath,
|
|
53310
|
-
clean:
|
|
53311
|
-
errors:
|
|
53312
|
-
warnings: null,
|
|
53395
|
+
clean: errors.length === 0,
|
|
53396
|
+
errors: errors.length,
|
|
53397
|
+
warnings: warned === null ? null : warnings.length,
|
|
53313
53398
|
diagnostics,
|
|
53314
53399
|
...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies },
|
|
53315
53400
|
...uncached.length === 0 ? {} : {
|
|
@@ -57659,7 +57744,7 @@ async function runGodotServer() {
|
|
|
57659
57744
|
const server = new GodotServer;
|
|
57660
57745
|
await server.run();
|
|
57661
57746
|
}
|
|
57662
|
-
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;
|
|
57747
|
+
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;
|
|
57663
57748
|
var init_server = __esm(() => {
|
|
57664
57749
|
init_dist2();
|
|
57665
57750
|
init_stdio();
|
|
@@ -59106,7 +59191,6 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
59106
59191
|
}
|
|
59107
59192
|
}
|
|
59108
59193
|
const times = readTimes(timesFile);
|
|
59109
|
-
const units = unitsOf(suites, times);
|
|
59110
59194
|
const memory = await memoryAvailable();
|
|
59111
59195
|
if (memory.unread !== undefined) {
|
|
59112
59196
|
console.log(`Windows did not say how much memory it can still commit (${memory.unread}), so engines are sized by the physical memory free.`);
|
|
@@ -59115,9 +59199,13 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
59115
59199
|
if (options.cores !== null && process14.platform === "darwin") {
|
|
59116
59200
|
console.log("macOS offers no way to hold a process to some processors, so --cores holds only the number of engines here.");
|
|
59117
59201
|
}
|
|
59202
|
+
const splitPast = splitPastFor(suites, times, room.engines);
|
|
59203
|
+
const units = unitsOf(suites, times, splitPast);
|
|
59204
|
+
const past = splitPast > SPLIT_PAST_SECONDS ? `an engine's share of this run (${String(Math.round(splitPast))} s)` : `${String(SPLIT_PAST_SECONDS)} s`;
|
|
59118
59205
|
const plans = options.order === null ? dealt(units, declared, room.engines) : [inOneEngine(ordered(units, options.order), declared)];
|
|
59119
59206
|
const functions = suites.reduce((sum, suite) => sum + suite.cases.length, 0);
|
|
59120
|
-
const
|
|
59207
|
+
const asked = unitsOf(suites, times, splitPastFor(suites, times, options.engines));
|
|
59208
|
+
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)}`;
|
|
59121
59209
|
console.log(`${plural(functions, "test function")} in ${plural(suites.length, "suite")} over ${plural(plans.length, "engine")}${how}.`);
|
|
59122
59210
|
const windowedAnyway = headlessInAWindow(plans, units);
|
|
59123
59211
|
if (windowedAnyway.length > 0) {
|
|
@@ -59126,13 +59214,18 @@ ${existsSync23(path) ? readFileSync22(path, "latin1") : "missing"}`)
|
|
|
59126
59214
|
}
|
|
59127
59215
|
const split = [...new Set(units.map((unit) => unit.suite))].filter((path) => units.filter((unit) => unit.suite === path && unit.row === undefined).length > 1);
|
|
59128
59216
|
if (plans.length > 1 && split.length > 0) {
|
|
59129
|
-
console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${
|
|
59217
|
+
console.log(`Dealt out case by case, since ${split.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(split)}.`);
|
|
59130
59218
|
}
|
|
59131
59219
|
const byRows = [
|
|
59132
59220
|
...new Set(units.flatMap((unit) => unit.row === undefined ? [] : [`${unit.suite} > ${unit.cases[0] ?? ""}`]))
|
|
59133
59221
|
];
|
|
59134
59222
|
if (plans.length > 1 && byRows.length > 0) {
|
|
59135
|
-
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${
|
|
59223
|
+
console.log(`Dealt out row by row, since ${byRows.length === 1 ? "it" : "each"} took more than ${past} last time: ${named(byRows)}.`);
|
|
59224
|
+
}
|
|
59225
|
+
const keptWhole = units.flatMap((unit) => unit.cost > splitPast && !unit.windowed && suites.some((s) => s.path === unit.suite && s.together) ? [unit.suite] : []);
|
|
59226
|
+
if (options.order === null && room.engines > 1 && keptWhole.length > 0) {
|
|
59227
|
+
const one = keptWhole.length === 1;
|
|
59228
|
+
console.log(`Kept whole in one engine, since ${one ? "it declares" : "each declares"} GDHARNESS_TOGETHER: ${named(keptWhole)}.`);
|
|
59136
59229
|
}
|
|
59137
59230
|
const lanes = [...new Set(plans.map((plan) => plan.lane))].map((lane) => plans.flatMap((plan, index) => plan.lane === lane ? [{ plan, index }] : []));
|
|
59138
59231
|
const taken = [];
|
|
@@ -296,6 +296,7 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
296
296
|
if flags & Object.CONNECT_APPEND_SOURCE_OBJECT:
|
|
297
297
|
passed.append({"name": "source", "type": TYPE_OBJECT, "class_name": source.get_class()})
|
|
298
298
|
passed = passed.slice(0, maxi(passed.size() - callable.get_unbound_arguments_count(), 0))
|
|
299
|
+
var from_signal: int = passed.size()
|
|
299
300
|
for bound: Variant in callable.get_bound_arguments():
|
|
300
301
|
var bound_class: String = ""
|
|
301
302
|
if bound is Object:
|
|
@@ -310,7 +311,25 @@ static func _misfit(source: Node, signal_name: String, callable: Callable, flags
|
|
|
310
311
|
return "%s passes %s, and %s needs %d%s" % [signal_name, _count(passed.size()), method, needed, FAILS]
|
|
311
312
|
if passed.size() > parameters.size():
|
|
312
313
|
var most: String = "none" if parameters.is_empty() else "at most %d" % parameters.size()
|
|
313
|
-
|
|
314
|
+
# Said with the misfit, since unbinds is what a connection to a method wanting less is for, and
|
|
315
|
+
# only when the extra arguments are the signal's: a bound one is not dropped by unbinding.
|
|
316
|
+
var extra: int = passed.size() - parameters.size()
|
|
317
|
+
var remedy: String = (
|
|
318
|
+
(
|
|
319
|
+
" Connected with unbinds %d, the last %s the signal passes %s left out."
|
|
320
|
+
% [
|
|
321
|
+
extra + callable.get_unbound_arguments_count(),
|
|
322
|
+
"argument" if extra == 1 else "%d arguments" % extra,
|
|
323
|
+
"is" if extra == 1 else "are",
|
|
324
|
+
]
|
|
325
|
+
)
|
|
326
|
+
if extra <= from_signal
|
|
327
|
+
else ""
|
|
328
|
+
)
|
|
329
|
+
return (
|
|
330
|
+
"%s passes %s, and %s takes %s%s%s"
|
|
331
|
+
% [signal_name, _count(passed.size()), method, most, FAILS, remedy]
|
|
332
|
+
)
|
|
314
333
|
for at: int in passed.size():
|
|
315
334
|
var given: Dictionary = passed[at]
|
|
316
335
|
var parameter: Dictionary = parameters[at]
|
|
@@ -16,6 +16,8 @@ const GdhItems = preload("runtime_items.gd")
|
|
|
16
16
|
const GdhScreen = preload("runtime_screen.gd")
|
|
17
17
|
const GdhTargets = preload("runtime_targets.gd")
|
|
18
18
|
const GdhMouse = preload("runtime_mouse.gd")
|
|
19
|
+
const GdhTyping = preload("runtime_typing.gd")
|
|
20
|
+
const GdhSays = preload("runtime_says.gd")
|
|
19
21
|
|
|
20
22
|
## Set, to the desktop's name, for a process gdharness started on a desktop of its own and for
|
|
21
23
|
## everything that process starts. Kept in step with `DESKTOP_VARIABLE` in src/desktop.ts.
|
|
@@ -276,6 +278,8 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
276
278
|
event.unicode = GdhValues.glyph_of(event.keycode, event.shift_pressed)
|
|
277
279
|
|
|
278
280
|
var focused: String = GdhValues.focus_path(_host)
|
|
281
|
+
var field: Control = GdhTyping.focus_owner(_host.get_tree().root)
|
|
282
|
+
var before: Dictionary = GdhTyping.field_before(field)
|
|
279
283
|
Input.parse_input_event(event)
|
|
280
284
|
var lasted: Variant = null
|
|
281
285
|
if whole:
|
|
@@ -300,7 +304,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
300
304
|
"pressed": held and not whole,
|
|
301
305
|
"whole": whole,
|
|
302
306
|
}
|
|
303
|
-
return GdhValues.focus_noted(answer, focused, _host)
|
|
307
|
+
return GdhTyping.field_noted(GdhValues.focus_noted(answer, focused, _host), field, before)
|
|
304
308
|
|
|
305
309
|
|
|
306
310
|
## Clicks a mouse button at a position, and lets go of it again unless asked to hold it.
|
|
@@ -601,7 +605,7 @@ func click(params: Dictionary) -> Dictionary:
|
|
|
601
605
|
item_scrolled = placed_item["scrolled"]
|
|
602
606
|
var rich: RichTextLabel = control as RichTextLabel
|
|
603
607
|
if found != null and rich != null and named_point == null:
|
|
604
|
-
var placed: Dictionary =
|
|
608
|
+
var placed: Dictionary = GdhSays.point_on_words(rich, wanted)
|
|
605
609
|
var found_here: Dictionary = found
|
|
606
610
|
if placed.has("point"):
|
|
607
611
|
aim = placed["point"]
|
|
@@ -170,3 +170,57 @@ static func widened(wanted: String) -> String:
|
|
|
170
170
|
## an n, so the two characters mean the break to everybody who writes them.
|
|
171
171
|
static func as_said(wanted: String) -> String:
|
|
172
172
|
return spaced(wanted.replace("\\n", "\n"))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## Where [param label] draws the words [param wanted] matched in it, in the label's own space, when
|
|
176
|
+
## they are part of its text: {"point"} on their middle, {"unplaced"} saying why there is none, or
|
|
177
|
+
## {} when the words are the whole text, whose middle is the label's.
|
|
178
|
+
##
|
|
179
|
+
## A line of a dispatch names two people, each a link to their page, and a press on the label's
|
|
180
|
+
## middle landed on the second name when the first was asked for, so the game opened the wrong page.
|
|
181
|
+
## Godot answers which line a character is on, where that line is and how wide it is drawn, and
|
|
182
|
+
## nothing narrower, so the place along the line is the font's measure of the text before the
|
|
183
|
+
## words. A line in another font or holding an image measures off from what is drawn, which a wide
|
|
184
|
+
## line with nothing between the words and its start hardly does.
|
|
185
|
+
static func point_on_words(label: RichTextLabel, wanted: String) -> Dictionary:
|
|
186
|
+
var text: String = spaced(GdhWords.said_by(label))
|
|
187
|
+
var start: int = -1
|
|
188
|
+
var length: int = 0
|
|
189
|
+
for words: String in alternatives(wanted):
|
|
190
|
+
if not is_plain(words):
|
|
191
|
+
continue
|
|
192
|
+
var at: int = text.findn(words)
|
|
193
|
+
if at >= 0 and (start < 0 or at < start):
|
|
194
|
+
start = at
|
|
195
|
+
length = words.length()
|
|
196
|
+
if start < 0:
|
|
197
|
+
return {"unplaced": "the words were matched as a pattern, which names no one place in the text"}
|
|
198
|
+
if text.strip_edges().length() == length:
|
|
199
|
+
return {}
|
|
200
|
+
var line: int = label.get_character_line(start)
|
|
201
|
+
var shown: Vector2i = label.get_line_range(line)
|
|
202
|
+
if line < 0 or shown.x > start:
|
|
203
|
+
return {"unplaced": "the label could not say which line the words are on"}
|
|
204
|
+
var on_line: int = mini(length, shown.y - start) if shown.y > start else length
|
|
205
|
+
var font: Font = label.get_theme_font("normal_font")
|
|
206
|
+
var font_size: int = label.get_theme_font_size("normal_font_size")
|
|
207
|
+
var before: float = _width(font, font_size, text.substr(shown.x, start - shown.x))
|
|
208
|
+
var across: float = _width(font, font_size, text.substr(start, on_line))
|
|
209
|
+
var frame: StyleBox = label.get_theme_stylebox("normal")
|
|
210
|
+
var left: float = frame.get_margin(SIDE_LEFT)
|
|
211
|
+
var room: float = label.size.x - left - frame.get_margin(SIDE_RIGHT)
|
|
212
|
+
var spare: float = maxf(room - label.get_line_width(line), 0.0)
|
|
213
|
+
var aligned: float = 0.0
|
|
214
|
+
if label.horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER:
|
|
215
|
+
aligned = spare * 0.5
|
|
216
|
+
elif label.horizontal_alignment == HORIZONTAL_ALIGNMENT_RIGHT:
|
|
217
|
+
aligned = spare
|
|
218
|
+
var scrolled: float = label.get_v_scroll_bar().value if label.scroll_active else 0.0
|
|
219
|
+
var x: float = clampf(left + aligned + before + across * 0.5, left, left + room)
|
|
220
|
+
var top: float = frame.get_margin(SIDE_TOP) + label.get_line_offset(line) - scrolled
|
|
221
|
+
return {"point": Vector2(x, top + label.get_line_height(line) * 0.5)}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## How wide [param font] at [param font_size] draws [param words] on one line.
|
|
225
|
+
static func _width(font: Font, font_size: int, words: String) -> float:
|
|
226
|
+
return font.get_string_size(words, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
@@ -12,6 +12,8 @@ const GdhWords = preload("runtime_words.gd")
|
|
|
12
12
|
|
|
13
13
|
## How many of the controls saying the same words a refusal names, with the index for each.
|
|
14
14
|
const LISTED_CANDIDATES: int = 10
|
|
15
|
+
## How many hidden controls saying the words a refusal names.
|
|
16
|
+
const NAMED_HIDDEN: int = 3
|
|
15
17
|
|
|
16
18
|
## Node.ProcessMode as its names, for a refusal to say which one is in force.
|
|
17
19
|
const PROCESS_MODES: Dictionary[int, String] = {
|
|
@@ -63,7 +65,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
63
65
|
var exact: Array[bool] = []
|
|
64
66
|
# Whether the words are only inside longer words there, "out" in "Scout".
|
|
65
67
|
var inside: Array[bool] = []
|
|
66
|
-
var hidden:
|
|
68
|
+
var hidden: Array[Node] = []
|
|
67
69
|
# Controls saying exactly the words that a click cannot reach, kept to say what a match on screen
|
|
68
70
|
# that only says them as part of more stands in for.
|
|
69
71
|
var exact_out_of_reach: Array[Node] = []
|
|
@@ -77,7 +79,7 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
77
79
|
var node: Node = pending.pop_front()
|
|
78
80
|
if node is Control and GdhSays.says(node, wanted):
|
|
79
81
|
if not GdhQueries.shown(node):
|
|
80
|
-
hidden
|
|
82
|
+
hidden.append(node)
|
|
81
83
|
if _says_exactly(GdhWords.said_by(node), wanted):
|
|
82
84
|
exact_out_of_reach.append(node)
|
|
83
85
|
out_of_reach_as.append("hidden")
|
|
@@ -451,10 +453,21 @@ static func _offered_note(from: Node, drawn: Array[Node], areas: Array[Rect2]) -
|
|
|
451
453
|
return "" if words.is_empty() else "; the buttons on screen say %s" % words
|
|
452
454
|
|
|
453
455
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
+
## What to add to a refusal that found nothing on screen, when words matched hidden controls: how
|
|
457
|
+
## many, and the first few by path with what hides each. A count alone left a caller with nothing to
|
|
458
|
+
## show, since the control saying the words is rarely the node a game hides.
|
|
459
|
+
static func _hidden_note(hidden: Array[Node]) -> String:
|
|
460
|
+
if hidden.is_empty():
|
|
456
461
|
return ""
|
|
457
|
-
|
|
462
|
+
var named: Array[String] = []
|
|
463
|
+
for node: Node in hidden.slice(0, NAMED_HIDDEN):
|
|
464
|
+
named.append("%s%s" % [node.get_path(), why_hidden(node)])
|
|
465
|
+
if hidden.size() == 1:
|
|
466
|
+
return "; 1 hidden control says it, %s" % named[0]
|
|
467
|
+
return (
|
|
468
|
+
"; %d hidden controls say it, among them %s and %s"
|
|
469
|
+
% [hidden.size(), ", ".join(named.slice(0, -1)), named[-1]]
|
|
470
|
+
)
|
|
458
471
|
|
|
459
472
|
|
|
460
473
|
## What to add to a refusal that found nothing on screen, when words matched controls a click cannot
|
|
@@ -870,60 +883,6 @@ static func _clipped_away(
|
|
|
870
883
|
return false
|
|
871
884
|
|
|
872
885
|
|
|
873
|
-
## Where [param label] draws the words [param wanted] matched in it, in the label's own space, when
|
|
874
|
-
## they are part of its text: {"point"} on their middle, {"unplaced"} saying why there is none, or
|
|
875
|
-
## {} when the words are the whole text, whose middle is the label's.
|
|
876
|
-
##
|
|
877
|
-
## A line of a dispatch names two people, each a link to their page, and a press on the label's
|
|
878
|
-
## middle landed on the second name when the first was asked for, so the game opened the wrong page.
|
|
879
|
-
## Godot answers which line a character is on, where that line is and how wide it is drawn, and
|
|
880
|
-
## nothing narrower, so the place along the line is the font's measure of the text before the
|
|
881
|
-
## words. A line in another font or holding an image measures off from what is drawn, which a wide
|
|
882
|
-
## line with nothing between the words and its start hardly does.
|
|
883
|
-
static func point_on_words(label: RichTextLabel, wanted: String) -> Dictionary:
|
|
884
|
-
var text: String = GdhSays.spaced(GdhWords.said_by(label))
|
|
885
|
-
var start: int = -1
|
|
886
|
-
var length: int = 0
|
|
887
|
-
for words: String in GdhSays.alternatives(wanted):
|
|
888
|
-
if not GdhSays.is_plain(words):
|
|
889
|
-
continue
|
|
890
|
-
var at: int = text.findn(words)
|
|
891
|
-
if at >= 0 and (start < 0 or at < start):
|
|
892
|
-
start = at
|
|
893
|
-
length = words.length()
|
|
894
|
-
if start < 0:
|
|
895
|
-
return {"unplaced": "the words were matched as a pattern, which names no one place in the text"}
|
|
896
|
-
if text.strip_edges().length() == length:
|
|
897
|
-
return {}
|
|
898
|
-
var line: int = label.get_character_line(start)
|
|
899
|
-
var shown: Vector2i = label.get_line_range(line)
|
|
900
|
-
if line < 0 or shown.x > start:
|
|
901
|
-
return {"unplaced": "the label could not say which line the words are on"}
|
|
902
|
-
var on_line: int = mini(length, shown.y - start) if shown.y > start else length
|
|
903
|
-
var font: Font = label.get_theme_font("normal_font")
|
|
904
|
-
var font_size: int = label.get_theme_font_size("normal_font_size")
|
|
905
|
-
var before: float = _width(font, font_size, text.substr(shown.x, start - shown.x))
|
|
906
|
-
var across: float = _width(font, font_size, text.substr(start, on_line))
|
|
907
|
-
var frame: StyleBox = label.get_theme_stylebox("normal")
|
|
908
|
-
var left: float = frame.get_margin(SIDE_LEFT)
|
|
909
|
-
var room: float = label.size.x - left - frame.get_margin(SIDE_RIGHT)
|
|
910
|
-
var spare: float = maxf(room - label.get_line_width(line), 0.0)
|
|
911
|
-
var aligned: float = 0.0
|
|
912
|
-
if label.horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER:
|
|
913
|
-
aligned = spare * 0.5
|
|
914
|
-
elif label.horizontal_alignment == HORIZONTAL_ALIGNMENT_RIGHT:
|
|
915
|
-
aligned = spare
|
|
916
|
-
var scrolled: float = label.get_v_scroll_bar().value if label.scroll_active else 0.0
|
|
917
|
-
var x: float = clampf(left + aligned + before + across * 0.5, left, left + room)
|
|
918
|
-
var top: float = frame.get_margin(SIDE_TOP) + label.get_line_offset(line) - scrolled
|
|
919
|
-
return {"point": Vector2(x, top + label.get_line_height(line) * 0.5)}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
## How wide [param font] at [param font_size] draws [param words] on one line.
|
|
923
|
-
static func _width(font: Font, font_size: int, words: String) -> float:
|
|
924
|
-
return font.get_string_size(words, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
|
925
|
-
|
|
926
|
-
|
|
927
886
|
## The canvas layer [param control] is drawn on, 0 for the viewport's own canvas.
|
|
928
887
|
static func _layer_of(control: Control) -> int:
|
|
929
888
|
var layer: CanvasLayer = control.get_canvas_layer_node()
|
|
@@ -59,6 +59,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
59
59
|
if focused != null:
|
|
60
60
|
into = str(focused.get_path())
|
|
61
61
|
|
|
62
|
+
var before: Dictionary = field_before(focused)
|
|
62
63
|
var replaced: bool = over and _select_everything(focused)
|
|
63
64
|
# Emptying a field is a real thing to ask for and the one shape of filling one in that types no
|
|
64
65
|
# characters. The selection is standing, so the key a player presses over one is what clears it.
|
|
@@ -70,7 +71,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
70
71
|
var holds: Variant = null
|
|
71
72
|
if is_instance_valid(focused):
|
|
72
73
|
holds = _what_it_holds(focused)
|
|
73
|
-
|
|
74
|
+
var answer: Dictionary = {
|
|
74
75
|
"type": "input_injected",
|
|
75
76
|
"input_type": "text",
|
|
76
77
|
"text": text,
|
|
@@ -79,6 +80,61 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
79
80
|
"replaced": replaced,
|
|
80
81
|
"holds": holds,
|
|
81
82
|
}
|
|
83
|
+
if ended_editing(focused, before):
|
|
84
|
+
answer["editing_ended"] = true
|
|
85
|
+
# A control drawing its own field takes characters in its gui_input, so typing at one is not
|
|
86
|
+
# refused; but a focused button answered that the text went into it, which read as typed.
|
|
87
|
+
if into != null and before.is_empty():
|
|
88
|
+
answer["note"] = (
|
|
89
|
+
(
|
|
90
|
+
"%s has the focus and is no LineEdit or TextEdit, so each character reached it as a key"
|
|
91
|
+
+ " press and went on to the game's input unless it kept it, and a focused button takes a"
|
|
92
|
+
+ " space or Enter as a press of it. Click a field first to type into one."
|
|
93
|
+
)
|
|
94
|
+
% into
|
|
95
|
+
)
|
|
96
|
+
return answer
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
## What [param focused] says and whether it is being edited, read before keys go in, for
|
|
100
|
+
## [method field_noted] and [method ended_editing] to compare with; empty where the focus is no
|
|
101
|
+
## field. A [TextEdit] has no editing state of its own and counts as always edited.
|
|
102
|
+
static func field_before(focused: Control) -> Dictionary:
|
|
103
|
+
var field: LineEdit = focused as LineEdit
|
|
104
|
+
if field != null:
|
|
105
|
+
return {"text": field.text, "editing": field.is_editing()}
|
|
106
|
+
var box: TextEdit = focused as TextEdit
|
|
107
|
+
if box != null:
|
|
108
|
+
return {"text": box.text, "editing": true}
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Whether [param focused], read as [param before], stopped being edited, as a [LineEdit] does when
|
|
113
|
+
## it is submitted: it keeps the focus and drops every key after, so a caller reading only the focus
|
|
114
|
+
## typed next into nothing.
|
|
115
|
+
static func ended_editing(focused: Control, before: Dictionary) -> bool:
|
|
116
|
+
var was: bool = before.get("editing", false)
|
|
117
|
+
if not was or not is_instance_valid(focused):
|
|
118
|
+
return false
|
|
119
|
+
var field: LineEdit = focused as LineEdit
|
|
120
|
+
return field != null and not field.is_editing()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
## [param said] with what a key did to [param focused], the field read before it as [param before]:
|
|
124
|
+
## typed_into and holds where its text changed, and editing_ended where it stopped being edited. A
|
|
125
|
+
## field being edited takes a printable key as typing and keeps it from the game's _unhandled_input,
|
|
126
|
+
## so Space into a name field pressed no jump, and an answer naming only the focus did not say why.
|
|
127
|
+
static func field_noted(said: Dictionary, focused: Control, before: Dictionary) -> Dictionary:
|
|
128
|
+
if before.is_empty() or not is_instance_valid(focused):
|
|
129
|
+
return said
|
|
130
|
+
var field: LineEdit = focused as LineEdit
|
|
131
|
+
var now: String = field.text if field != null else (focused as TextEdit).text
|
|
132
|
+
if now != before["text"]:
|
|
133
|
+
said["typed_into"] = str(focused.get_path())
|
|
134
|
+
said["holds"] = _what_it_holds(focused)
|
|
135
|
+
if ended_editing(focused, before):
|
|
136
|
+
said["editing_ended"] = true
|
|
137
|
+
return said
|
|
82
138
|
|
|
83
139
|
|
|
84
140
|
## The control keys pushed into [param viewport] go to: its focus, or the focus of the window
|
|
@@ -82,6 +82,8 @@ func get_dependencies(params: Dictionary) -> Dictionary:
|
|
|
82
82
|
var full_path: String = resource_path
|
|
83
83
|
if not full_path.begins_with("res://"):
|
|
84
84
|
full_path = "res://" + full_path
|
|
85
|
+
if DirAccess.dir_exists_absolute(full_path):
|
|
86
|
+
return _log.failure(_folder_refusal(full_path))
|
|
85
87
|
if not FileAccess.file_exists(full_path):
|
|
86
88
|
return _log.failure(
|
|
87
89
|
_files.with_nearest_files("Resource file does not exist: " + full_path, full_path)
|
|
@@ -193,6 +195,11 @@ func _loaded_and_missing(script: String) -> Array[String]:
|
|
|
193
195
|
named[path] = true
|
|
194
196
|
var missing: Array[String] = []
|
|
195
197
|
for path: String in loaded:
|
|
198
|
+
# Left as written by the resolution, which is a uid the project does not know.
|
|
199
|
+
if path.begins_with("uid://"):
|
|
200
|
+
if not named.has(path) and not missing.has(path):
|
|
201
|
+
missing.append(path)
|
|
202
|
+
continue
|
|
196
203
|
if not path.begins_with("res://") or path.begins_with("res://.") or path.get_extension().is_empty():
|
|
197
204
|
continue
|
|
198
205
|
if not named.has(path) and not FileAccess.file_exists(path) and not missing.has(path):
|
|
@@ -217,6 +224,15 @@ static func _named_by_settings() -> Array[String]:
|
|
|
217
224
|
return named
|
|
218
225
|
|
|
219
226
|
|
|
227
|
+
## The refusal for [param path], a folder: a walk follows one file, and a folder was refused as a
|
|
228
|
+
## file that does not exist, about a folder the caller could see.
|
|
229
|
+
static func _folder_refusal(path: String) -> String:
|
|
230
|
+
return (
|
|
231
|
+
"%s is a folder, and project_dependencies follows one file: name a file in it."
|
|
232
|
+
% path.trim_suffix("/")
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
220
236
|
## One line for [param entry], one of [method missing_references].
|
|
221
237
|
static func missing_line(entry: Dictionary) -> String:
|
|
222
238
|
var referring: Array = entry.get("referenced_by", [])
|
|
@@ -235,6 +251,8 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
235
251
|
|
|
236
252
|
if not resource_path.begins_with("res://"):
|
|
237
253
|
resource_path = "res://" + resource_path
|
|
254
|
+
if DirAccess.dir_exists_absolute(resource_path):
|
|
255
|
+
return _log.failure(_folder_refusal(resource_path))
|
|
238
256
|
# A file that is gone is searched for all the same, since what still refers to it is what a
|
|
239
257
|
# caller asks about one, and the forward walk of a scene had just named it missing. Refused only
|
|
240
258
|
# when nothing refers to it either, which is a path written wrong.
|
|
@@ -331,6 +349,11 @@ func find_resource_usages(params: Dictionary) -> Dictionary:
|
|
|
331
349
|
kind = _path_reference_kind(line, file_path)
|
|
332
350
|
if not loaded.is_empty() and written_path != loaded:
|
|
333
351
|
extra["stale_path"] = written_path
|
|
352
|
+
var unknown_uid: String = (
|
|
353
|
+
_unknown_uid_of(line) if trimmed.begins_with("[ext_resource") else ""
|
|
354
|
+
)
|
|
355
|
+
if not unknown_uid.is_empty():
|
|
356
|
+
extra["unknown_uid"] = unknown_uid
|
|
334
357
|
elif by_its_autoload:
|
|
335
358
|
kind = "autoload"
|
|
336
359
|
else:
|
|
@@ -570,6 +593,7 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
570
593
|
elif path.get_extension() in SHADER_KINDS:
|
|
571
594
|
content = _shader_code(content)
|
|
572
595
|
var moved: Dictionary = _moved_by_uid(content)
|
|
596
|
+
var unknown: Dictionary = _unknown_uids(content, path)
|
|
573
597
|
|
|
574
598
|
for pattern: String in REFERENCE_PATTERNS:
|
|
575
599
|
var regex: RegEx = GdhPatterns.compiled(pattern)
|
|
@@ -577,6 +601,12 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
577
601
|
var written: String = _resolved(_referenced_path(m.get_string()), path)
|
|
578
602
|
var dep_path: String = str(moved.get(written, written))
|
|
579
603
|
|
|
604
|
+
# A uid the project does not know names no file, and loading it fails: passed over as
|
|
605
|
+
# no res:// path, a script's load of one went unsaid while the game printed
|
|
606
|
+
# "Unrecognized UID" at that line.
|
|
607
|
+
if dep_path.begins_with("uid://"):
|
|
608
|
+
_depend(deps, path, dep_path, {}, current_depth, walk)
|
|
609
|
+
continue
|
|
580
610
|
if not dep_path.begins_with("res://"):
|
|
581
611
|
continue
|
|
582
612
|
# A literal naming a directory, or the start of a path a script builds as it runs
|
|
@@ -590,14 +620,10 @@ func _analyze_resource(path: String, current_depth: int, walk: GdhDependencyWalk
|
|
|
590
620
|
if not walk.include_built_in and dep_path.begins_with("res://."):
|
|
591
621
|
continue
|
|
592
622
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
{"stale_path": written} if dep_path != written else {},
|
|
598
|
-
current_depth,
|
|
599
|
-
walk
|
|
600
|
-
)
|
|
623
|
+
var named_by: Dictionary = {"stale_path": written} if dep_path != written else {}
|
|
624
|
+
if unknown.has(dep_path):
|
|
625
|
+
named_by["unknown_uid"] = unknown[dep_path]
|
|
626
|
+
_depend(deps, path, dep_path, named_by, current_depth, walk)
|
|
601
627
|
|
|
602
628
|
# A script uses another by its class_name as surely as by a preload, and only the reverse walk
|
|
603
629
|
# looked for that: asked what a script depends on, the answer left out the script it typed a
|
|
@@ -633,7 +659,11 @@ func _depend(
|
|
|
633
659
|
for existing: Dictionary in deps:
|
|
634
660
|
if existing.get("path", "") == dep_path:
|
|
635
661
|
return
|
|
636
|
-
|
|
662
|
+
# A uid still written as one is one the project does not know, and asked about, the engine prints
|
|
663
|
+
# an error for it.
|
|
664
|
+
var dep_info: Dictionary = {
|
|
665
|
+
"path": dep_path, "exists": not dep_path.begins_with("uid://") and FileAccess.file_exists(dep_path)
|
|
666
|
+
}
|
|
637
667
|
dep_info.merge(named_by)
|
|
638
668
|
# Marked where the reference closing the cycle is. Marked inside the file reached again, the tree
|
|
639
669
|
# answered that file as a dependency of itself.
|
|
@@ -743,6 +773,25 @@ static func _moved_by_uid(content: String) -> Dictionary:
|
|
|
743
773
|
return moved
|
|
744
774
|
|
|
745
775
|
|
|
776
|
+
# The ext_resource paths in [param content], read from [param from], whose uid names no file the
|
|
777
|
+
# project has, each with that uid. The engine warns that the uid is invalid and loads the path, and
|
|
778
|
+
# the walk followed the path saying nothing, so an export was the first to mention it.
|
|
779
|
+
static func _unknown_uids(content: String, from: String) -> Dictionary:
|
|
780
|
+
var unknown: Dictionary = {}
|
|
781
|
+
for line: RegExMatch in GdhPatterns.compiled("\\[ext_resource[^\\]]*\\]").search_all(content):
|
|
782
|
+
var uid: String = _unknown_uid_of(line.get_string())
|
|
783
|
+
var at: RegExMatch = GdhPatterns.compiled('path="([^"]+)"').search(line.get_string())
|
|
784
|
+
if at != null and not uid.is_empty():
|
|
785
|
+
unknown[_resolved(at.get_string(1), from)] = uid
|
|
786
|
+
return unknown
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
# The uid an ext_resource [param entry] carries when it names no file the project has, or empty.
|
|
790
|
+
static func _unknown_uid_of(entry: String) -> String:
|
|
791
|
+
var uid: RegExMatch = GdhPatterns.compiled('uid="(uid://[^"]+)"').search(entry)
|
|
792
|
+
return "" if uid == null or not _loaded_by_uid(entry).is_empty() else uid.get_string(1)
|
|
793
|
+
|
|
794
|
+
|
|
746
795
|
# The file an ext_resource [param entry] is loaded from by its uid, or empty when it carries none
|
|
747
796
|
# the project knows, and the engine falls back to the path.
|
|
748
797
|
static func _loaded_by_uid(entry: String) -> String:
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.39",
|
|
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",
|