gdharness 1.1.45 → 1.1.47
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 +217 -51
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +97 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +13 -2
- package/build/godot/addons/gdharness_runtime/runtime_menus.gd +1 -1
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +160 -41
- package/build/godot/addons/gdharness_runtime/runtime_says.gd +26 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +183 -12
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +14 -3
- package/build/index.js +218 -58
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -17588,13 +17588,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
17588
17588
|
},
|
|
17589
17589
|
{
|
|
17590
17590
|
name: "project_test",
|
|
17591
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
17591
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. Needs gdUnit4 under addons/gdUnit4.",
|
|
17592
17592
|
parameters: {
|
|
17593
17593
|
projectPath: PROJECT_PATH,
|
|
17594
17594
|
path: {
|
|
17595
17595
|
type: ["string", "array"],
|
|
17596
17596
|
items: { type: "string" },
|
|
17597
|
-
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once.
|
|
17597
|
+
description: "A test directory or suite file inside the project, or a list of them, all run on one engine: a named set of suites boots once rather than once per suite. An entry given twice, or a suite inside a directory also listed, runs once. An entry that is not there is left out and named under missing, the rest run, and such a run is never called passed; when no entry is there the run is refused before the engine starts, naming each one. Default test."
|
|
17598
17598
|
},
|
|
17599
17599
|
ignore: {
|
|
17600
17600
|
type: "array",
|
|
@@ -17908,7 +17908,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
17908
17908
|
},
|
|
17909
17909
|
{
|
|
17910
17910
|
name: "editor_run",
|
|
17911
|
-
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
17911
|
+
description: "The run: starting the project, stopping it, or booting it once to see whether it comes up clean. start keeps it running and collecting output until it quits or is stopped, windowed where there is a display and headless where there is not, unless headless says otherwise; only runtime_capture needs the window. A run that quits on its own is kept, so a scene that prints an answer and quits is start, then editor_output until running is false. What runs is a scene: a SceneTree script is not an entry point here, so put the script on the root of a scene of its own and name that in scene. args hands the game its own flags, the ones it reads with OS.get_cmdline_user_args(), and a run carrying any is started by this server rather than by the editor. check boots it headless for a few frames, waits for it to quit, and answers with the verdict: whether it came up, and every error and warning it printed on the way. start and check both wait first for a scan the open editor is running and for the class cache it writes just after, since a game booted in between resolves no global class, as does every other engine this server starts on the project; waitedForEditorScanMs says how long, and scanNote says when a scan ran past thirty seconds and the game started during it. A start waits for the game to become something the runtime_* tools can talk to and says which it is under runtime: listening with the port it took, or why not, so the first call after a start does not have to be made twice. When it is not listening, mayYetAnnounce says whether that is final: false is a runtime that is not coming, true is a game still on its way up, which editor_status will see and runtimeWaitMs waits longer for, and the note then says what a runtime_* call naming no projectPath meets until it announces. A start this server made also answers with transcript, the file both the run's streams are written to, so a watch on it can be armed off the start rather than off a second call.",
|
|
17912
17912
|
parameters: {
|
|
17913
17913
|
projectPath: {
|
|
17914
17914
|
...PROJECT_PATH,
|
|
@@ -18124,7 +18124,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18124
18124
|
property: {
|
|
18125
18125
|
type: "string",
|
|
18126
18126
|
ops: ["property", "find"],
|
|
18127
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that
|
|
18127
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control\'s theme is read where it is drawn: \'get_theme_stylebox("panel"):content_margin_top\' or \'get_theme_constant("separation")\'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
18128
18128
|
},
|
|
18129
18129
|
depth: {
|
|
18130
18130
|
type: "integer",
|
|
@@ -18158,7 +18158,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18158
18158
|
says: {
|
|
18159
18159
|
type: "string",
|
|
18160
18160
|
ops: ["find"],
|
|
18161
|
-
description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
|
|
18161
|
+
description: 'find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively and with a typed space matching any space the game draws (a non-breaking one included), which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern, and a glob that finds nothing says under note how many nodes it would find open at both ends. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one. Alternatives separated by | match when any one of them does, each by these rules ("*won*|*lost*"); write \\| for a bar that is part of the words.'
|
|
18162
18162
|
},
|
|
18163
18163
|
limit: {
|
|
18164
18164
|
type: "integer",
|
|
@@ -18221,7 +18221,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18221
18221
|
property: {
|
|
18222
18222
|
type: "string",
|
|
18223
18223
|
ops: ["set"],
|
|
18224
|
-
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a method
|
|
18224
|
+
description: 'set: which one to write. Colons write through what a node holds, "_game:run:day", and a number or a key steps into a list or a map on the way, "_game:run:roster:0:name", a negative number counting from the end, and a step written as a call, "get_viewport():gui_embed_subwindows", walks through what a reading method returned, as runtime_inspect property describes. A map keyed by numbers is stepped into by the number, "_game:units:3". The answer reads back off the same holder, and a write that leaves the property as it was is refused and says what it still holds: the engine not taking it and a setter keeping the value look the same, so the refusal names neither. A write of the value the property already holds, at its own precision, as a float given in 64 bits to a property keeping 32, is not refused: it answers unchanged: true.'
|
|
18225
18225
|
},
|
|
18226
18226
|
value: {
|
|
18227
18227
|
ops: ["set"],
|
|
@@ -18231,12 +18231,18 @@ var init_tool_definitions = __esm(() => {
|
|
|
18231
18231
|
method: {
|
|
18232
18232
|
type: "string",
|
|
18233
18233
|
ops: ["call"],
|
|
18234
|
-
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a method
|
|
18234
|
+
description: 'call: which one to call. Colons call through what a node holds, "_game:run:advance", the same way a property is written through them, a list or a map is stepped into by index or key, "_game:run:roster:0:retire", and a step written as a call walks through what a reading method returned, "get_viewport():gui_get_focus_owner", as runtime_inspect property describes.'
|
|
18235
18235
|
},
|
|
18236
18236
|
args: {
|
|
18237
18237
|
type: "array",
|
|
18238
18238
|
ops: ["call"],
|
|
18239
|
-
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
18239
|
+
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
|
|
18240
|
+
},
|
|
18241
|
+
properties: {
|
|
18242
|
+
type: ["boolean", "array"],
|
|
18243
|
+
items: { type: "string" },
|
|
18244
|
+
ops: ["call"],
|
|
18245
|
+
description: 'call: read the object the call answered with in the same answer, under result_properties: true for every variable its script declares, or a list of names, each a colon path walked from that object the way property paths are ("dead_allowed", "terms:0:name"). A name that is not there is listed under result_properties_missing with why, and a result that is not an object answers properties_note instead. Without it an object comes back as its class and id only.'
|
|
18240
18246
|
},
|
|
18241
18247
|
timeoutMs: {
|
|
18242
18248
|
type: "integer",
|
|
@@ -18318,7 +18324,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18318
18324
|
},
|
|
18319
18325
|
{
|
|
18320
18326
|
name: "runtime_input",
|
|
18321
|
-
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 or mouse motion. All of it works headless, where the window is 64 by 64 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:
|
|
18327
|
+
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 or mouse motion. All of it works headless, where the window is 64 by 64 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.",
|
|
18322
18328
|
parameters: {
|
|
18323
18329
|
projectPath: RUNNING_PROJECT_PATH,
|
|
18324
18330
|
pid: RUNNING_PID,
|
|
@@ -18330,7 +18336,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
18330
18336
|
says: {
|
|
18331
18337
|
type: "string",
|
|
18332
18338
|
ops: ["click"],
|
|
18333
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one."
|
|
18339
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
18334
18340
|
},
|
|
18335
18341
|
action: {
|
|
18336
18342
|
type: "string",
|
|
@@ -38702,12 +38708,16 @@ function whyNoReport(printed, asked) {
|
|
|
38702
38708
|
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
38703
38709
|
}
|
|
38704
38710
|
function scriptErrorsPrinted(printed) {
|
|
38711
|
+
return discoveryListing(printed)?.errors ?? [];
|
|
38712
|
+
}
|
|
38713
|
+
function discoveryListing(printed) {
|
|
38705
38714
|
const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
|
|
38706
38715
|
if (start < 0) {
|
|
38707
|
-
return
|
|
38716
|
+
return null;
|
|
38708
38717
|
}
|
|
38709
38718
|
const found = [];
|
|
38710
38719
|
let message = null;
|
|
38720
|
+
let to = start + 1;
|
|
38711
38721
|
for (const line of printed.slice(start + 1)) {
|
|
38712
38722
|
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
38713
38723
|
if (at !== null && message !== null) {
|
|
@@ -38718,8 +38728,10 @@ function scriptErrorsPrinted(printed) {
|
|
|
38718
38728
|
} else {
|
|
38719
38729
|
break;
|
|
38720
38730
|
}
|
|
38731
|
+
to += 1;
|
|
38721
38732
|
}
|
|
38722
|
-
|
|
38733
|
+
const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
38734
|
+
return { errors, from: start, to };
|
|
38723
38735
|
}
|
|
38724
38736
|
function unspelled(text) {
|
|
38725
38737
|
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
@@ -41556,6 +41568,104 @@ var init_run_record = __esm(() => {
|
|
|
41556
41568
|
execFileAsync = promisify4(execFile4);
|
|
41557
41569
|
});
|
|
41558
41570
|
|
|
41571
|
+
// src/script-origins.ts
|
|
41572
|
+
import { join as join24 } from "node:path";
|
|
41573
|
+
function unresolvedClassIn(message) {
|
|
41574
|
+
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
41575
|
+
}
|
|
41576
|
+
async function originsOf(errors, declared, check) {
|
|
41577
|
+
const listed = new Set(errors.map((error) => error.path));
|
|
41578
|
+
const origins = [];
|
|
41579
|
+
const unconfirmed = [];
|
|
41580
|
+
const asked = new Set;
|
|
41581
|
+
const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
|
|
41582
|
+
while (waiting.length > 0 && asked.size < MOST_CHECKED) {
|
|
41583
|
+
const name = waiting.shift() ?? "";
|
|
41584
|
+
const path = declared.get(name);
|
|
41585
|
+
if (path === undefined || listed.has(path) || asked.has(path)) {
|
|
41586
|
+
continue;
|
|
41587
|
+
}
|
|
41588
|
+
asked.add(path);
|
|
41589
|
+
const checked = await check(path);
|
|
41590
|
+
const own = checked.errors.filter((error) => error.path === path);
|
|
41591
|
+
if (own.length === 0) {
|
|
41592
|
+
unconfirmed.push({
|
|
41593
|
+
path,
|
|
41594
|
+
declares: name,
|
|
41595
|
+
why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
|
|
41596
|
+
});
|
|
41597
|
+
continue;
|
|
41598
|
+
}
|
|
41599
|
+
for (const error of own) {
|
|
41600
|
+
origins.push({ ...error, declares: name });
|
|
41601
|
+
const deeper = unresolvedClassIn(error.message);
|
|
41602
|
+
if (deeper !== null) {
|
|
41603
|
+
waiting.push(deeper);
|
|
41604
|
+
}
|
|
41605
|
+
}
|
|
41606
|
+
}
|
|
41607
|
+
return { origins, unconfirmed };
|
|
41608
|
+
}
|
|
41609
|
+
function reloadedAt(entry) {
|
|
41610
|
+
for (const line of entry.detail) {
|
|
41611
|
+
const at = RELOADED_AT.exec(line.trim());
|
|
41612
|
+
if (at !== null) {
|
|
41613
|
+
return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
|
|
41614
|
+
}
|
|
41615
|
+
}
|
|
41616
|
+
return null;
|
|
41617
|
+
}
|
|
41618
|
+
function checkWithEngine(godotPath, projectPath) {
|
|
41619
|
+
return async (path) => {
|
|
41620
|
+
const logDir = scratchDirectory("tests");
|
|
41621
|
+
try {
|
|
41622
|
+
const ran = await runEngine(godotPath, [
|
|
41623
|
+
"--headless",
|
|
41624
|
+
"--log-file",
|
|
41625
|
+
join24(logDir, "engine.log"),
|
|
41626
|
+
"--path",
|
|
41627
|
+
projectPath,
|
|
41628
|
+
"--check-only",
|
|
41629
|
+
"-s",
|
|
41630
|
+
path
|
|
41631
|
+
], { timeout: { ms: 60000, said: "minute" } });
|
|
41632
|
+
const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
|
|
41633
|
+
const at = reloadedAt(entry);
|
|
41634
|
+
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
41635
|
+
});
|
|
41636
|
+
return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
|
|
41637
|
+
} finally {
|
|
41638
|
+
discard(logDir);
|
|
41639
|
+
}
|
|
41640
|
+
};
|
|
41641
|
+
}
|
|
41642
|
+
function withoutListed(entries, errors) {
|
|
41643
|
+
if (errors.length === 0) {
|
|
41644
|
+
return [...entries];
|
|
41645
|
+
}
|
|
41646
|
+
const listing = discoveryListing(entries.map((entry) => entry.text));
|
|
41647
|
+
const paths = new Set(errors.map((error) => error.path));
|
|
41648
|
+
return entries.filter((entry, at) => {
|
|
41649
|
+
if (listing !== null && at >= listing.from && at < listing.to) {
|
|
41650
|
+
return false;
|
|
41651
|
+
}
|
|
41652
|
+
const reloaded = reloadedAt(entry);
|
|
41653
|
+
if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
|
|
41654
|
+
return false;
|
|
41655
|
+
}
|
|
41656
|
+
const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
|
|
41657
|
+
return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
|
|
41658
|
+
});
|
|
41659
|
+
}
|
|
41660
|
+
var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT;
|
|
41661
|
+
var init_script_origins = __esm(() => {
|
|
41662
|
+
init_engine_run();
|
|
41663
|
+
init_junit();
|
|
41664
|
+
init_scratch();
|
|
41665
|
+
UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
41666
|
+
RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
41667
|
+
});
|
|
41668
|
+
|
|
41559
41669
|
// src/server.ts
|
|
41560
41670
|
import { execFile as execFile5, spawn as spawn4 } from "node:child_process";
|
|
41561
41671
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -41574,7 +41684,7 @@ import {
|
|
|
41574
41684
|
rmdirSync as rmdirSync2,
|
|
41575
41685
|
statSync as statSync9
|
|
41576
41686
|
} from "node:fs";
|
|
41577
|
-
import { basename as basename7, dirname as dirname18, extname as extname2, isAbsolute as isAbsolute3, join as
|
|
41687
|
+
import { basename as basename7, dirname as dirname18, extname as extname2, isAbsolute as isAbsolute3, join as join25, normalize as normalize2, relative as relative5 } from "node:path";
|
|
41578
41688
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
41579
41689
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
41580
41690
|
import { promisify as promisify5 } from "node:util";
|
|
@@ -41599,7 +41709,7 @@ function captureDestinationRefusal(outputPath, projects) {
|
|
|
41599
41709
|
if (!existsSync19(directory)) {
|
|
41600
41710
|
return `outputPath's directory ${dirname18(outputPath)} does not exist; a capture creates no directories.`;
|
|
41601
41711
|
}
|
|
41602
|
-
const where =
|
|
41712
|
+
const where = join25(directory, basename7(outputPath));
|
|
41603
41713
|
const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
|
|
41604
41714
|
if (inside !== undefined) {
|
|
41605
41715
|
return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
|
|
@@ -41895,10 +42005,13 @@ function runtimeVerdict(endpoint, after) {
|
|
|
41895
42005
|
return {
|
|
41896
42006
|
listening: false,
|
|
41897
42007
|
mayYetAnnounce: after.running,
|
|
41898
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
42008
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms${sized} and ${state}, so it may announce a moment from now: editor_status says whether it has, and editor_run start takes runtimeWaitMs to wait longer than this${after.withArgs ? ", which a run carrying its own arguments may well need, since whatever they ask the game to do before its first frame is inside this wait" : ""}. ${untilItAnnounces(after.servesAProject)}` : `nothing announced itself within ${after.budgetMs}ms${sized} and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
41899
42009
|
heldAt: null
|
|
41900
42010
|
};
|
|
41901
42011
|
}
|
|
42012
|
+
function untilItAnnounces(servesAProject) {
|
|
42013
|
+
return servesAProject ? "Until it does, a runtime_* call for it waits briefly and is then refused, and another project's game answers only a call whose projectPath names that project." : "Until it does, a runtime_* call naming no projectPath reaches whichever game is the only one announced, another project's included, so pass projectPath to be sure the answer is this game's.";
|
|
42014
|
+
}
|
|
41902
42015
|
function isOfType(value, kind) {
|
|
41903
42016
|
switch (kind) {
|
|
41904
42017
|
case "string":
|
|
@@ -42073,18 +42186,50 @@ function settingsNamedBy(op, args) {
|
|
|
42073
42186
|
return [];
|
|
42074
42187
|
}
|
|
42075
42188
|
}
|
|
42076
|
-
function
|
|
42189
|
+
function foldedScriptErrors(errors) {
|
|
42190
|
+
const scriptErrors = errors.filter((error) => !error.message.includes(DEPENDED_FAILED));
|
|
42191
|
+
const own = new Set(scriptErrors.map((error) => error.path));
|
|
42192
|
+
return {
|
|
42193
|
+
scriptErrors,
|
|
42194
|
+
failedThroughDependency: [...new Set(errors.map((error) => error.path))].filter((path) => !own.has(path))
|
|
42195
|
+
};
|
|
42196
|
+
}
|
|
42197
|
+
function nonEmpty(lists) {
|
|
42198
|
+
return Object.fromEntries(Object.entries(lists).filter(([, list]) => list.length > 0));
|
|
42199
|
+
}
|
|
42200
|
+
function scriptErrorsNote(errors, {
|
|
42201
|
+
origins,
|
|
42202
|
+
unconfirmed
|
|
42203
|
+
} = {
|
|
42204
|
+
origins: [],
|
|
42205
|
+
unconfirmed: []
|
|
42206
|
+
}) {
|
|
42207
|
+
const { scriptErrors: own, failedThroughDependency: dependents } = foldedScriptErrors(errors);
|
|
42077
42208
|
const causes = new Map;
|
|
42078
|
-
for (const error of
|
|
42079
|
-
if (!
|
|
42209
|
+
for (const error of own) {
|
|
42210
|
+
if (!causes.has(error.path)) {
|
|
42080
42211
|
causes.set(error.path, error);
|
|
42081
42212
|
}
|
|
42082
42213
|
}
|
|
42083
|
-
const dependents = [...new Set(errors.map((error) => error.path))].filter((path) => !causes.has(path));
|
|
42084
42214
|
const scripts = causes.size + dependents.length;
|
|
42085
|
-
const
|
|
42215
|
+
const at = (error) => error.line === null ? error.path : `${error.path}:${error.line}`;
|
|
42216
|
+
const sentence = (text) => `${text}${/[.!?]\)?$/.test(text) ? "" : "."}`;
|
|
42217
|
+
const traced = new Set;
|
|
42218
|
+
const failedOn = (declares) => {
|
|
42219
|
+
const through = [...causes.values()].filter((error) => unresolvedClassIn(error.message) === declares);
|
|
42220
|
+
for (const error of through) {
|
|
42221
|
+
traced.add(error.path);
|
|
42222
|
+
}
|
|
42223
|
+
return through.length === 0 ? "" : ` ${through.map(at).join(", ")} could not resolve ${declares} because of it.`;
|
|
42224
|
+
};
|
|
42225
|
+
const first = [...new Map(origins.map((origin) => [origin.path, origin])).values()].sort((a, b) => Number(unresolvedClassIn(a.message) !== null) - Number(unresolvedClassIn(b.message) !== null));
|
|
42226
|
+
const named = [
|
|
42227
|
+
...first.map((origin) => `${at(origin)} ${sentence(origin.message)} gdUnit4 did not name this script, which declares ${origin.declares}; the engine's own check of it found the error.${failedOn(origin.declares)}`),
|
|
42228
|
+
...unconfirmed.map((origin) => `${origin.path} declares ${origin.declares}, which gdUnit4 did not name, and ${origin.why}.${failedOn(origin.declares)}`),
|
|
42229
|
+
...[...causes.values()].filter((error) => !traced.has(error.path)).map((error) => `${at(error)} ${sentence(error.message)}`)
|
|
42230
|
+
].join(" ");
|
|
42086
42231
|
const following = dependents.length === 0 ? "" : causes.size === 0 ? `${dependents.join(", ")} ${dependents.length === 1 ? "depends" : "depend"} on a script that did not compile, and gdUnit4 named none with an error of its own: script_diagnostics on one of them finds it.` : `${dependents.length === 1 ? "One more" : `${dependents.length} more`} failed only because a script ${dependents.length === 1 ? "it depends" : "they depend"} on did: ${dependents.join(", ")}.`;
|
|
42087
|
-
const more =
|
|
42232
|
+
const more = own.length - causes.size;
|
|
42088
42233
|
const rest = more > 0 ? `${more === 1 ? "One more error" : `${more} more errors`} in ${causes.size === 1 ? "that script" : "those scripts"} ${more === 1 ? "is" : "are"} under scriptErrors.` : "";
|
|
42089
42234
|
return [
|
|
42090
42235
|
`No tests ran: gdUnit4 could not load ${scripts === 1 ? "one script" : `${scripts} scripts`} while looking for suites, and it runs no suite at all when one fails to load.`,
|
|
@@ -42149,7 +42294,7 @@ function aboveTheRunner(entry) {
|
|
|
42149
42294
|
}
|
|
42150
42295
|
function elsewhereIn(projectPath, asked) {
|
|
42151
42296
|
const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
|
|
42152
|
-
if (wanted === "" || existsSync19(
|
|
42297
|
+
if (wanted === "" || existsSync19(join25(projectPath, wanted))) {
|
|
42153
42298
|
return "";
|
|
42154
42299
|
}
|
|
42155
42300
|
const root = (wanted.split("/")[0] ?? "").toLowerCase();
|
|
@@ -42201,7 +42346,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
|
|
|
42201
42346
|
return false;
|
|
42202
42347
|
}
|
|
42203
42348
|
try {
|
|
42204
|
-
return readFileSync21(
|
|
42349
|
+
return readFileSync21(join25(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
|
|
42205
42350
|
} catch {
|
|
42206
42351
|
return false;
|
|
42207
42352
|
}
|
|
@@ -42226,7 +42371,7 @@ function needsItsOwnProcess(asked) {
|
|
|
42226
42371
|
class GodotServer {
|
|
42227
42372
|
mcp;
|
|
42228
42373
|
locator = new GodotLocator;
|
|
42229
|
-
operationsScript =
|
|
42374
|
+
operationsScript = join25(__dirname2, "godot", "operations", "godot_operations.gd");
|
|
42230
42375
|
godotBridge;
|
|
42231
42376
|
tools = buildToolDefinitions();
|
|
42232
42377
|
activeProcess = null;
|
|
@@ -42529,7 +42674,7 @@ class GodotServer {
|
|
|
42529
42674
|
return checked.response;
|
|
42530
42675
|
}
|
|
42531
42676
|
const named = args["projectPath"];
|
|
42532
|
-
if (typeof named === "string" && existsSync19(
|
|
42677
|
+
if (typeof named === "string" && existsSync19(join25(named, "project.godot"))) {
|
|
42533
42678
|
this.lastProjectPath = named;
|
|
42534
42679
|
}
|
|
42535
42680
|
this.updates.refresh();
|
|
@@ -42917,7 +43062,8 @@ class GodotServer {
|
|
|
42917
43062
|
...whichGame(args),
|
|
42918
43063
|
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
42919
43064
|
method: readString(args, "method") ?? "",
|
|
42920
|
-
args: readArray(args, "args") ?? []
|
|
43065
|
+
args: readArray(args, "args") ?? [],
|
|
43066
|
+
...args["properties"] === undefined ? {} : { properties: args["properties"] }
|
|
42921
43067
|
}, patience);
|
|
42922
43068
|
}
|
|
42923
43069
|
case "runtime_capture":
|
|
@@ -43002,7 +43148,7 @@ class GodotServer {
|
|
|
43002
43148
|
if (path === undefined) {
|
|
43003
43149
|
return { ok: false, response: this.createErrorResponse("projectPath is required.") };
|
|
43004
43150
|
}
|
|
43005
|
-
const file =
|
|
43151
|
+
const file = join25(path, "project.godot");
|
|
43006
43152
|
if (!existsSync19(file)) {
|
|
43007
43153
|
return {
|
|
43008
43154
|
ok: false,
|
|
@@ -43681,7 +43827,7 @@ class GodotServer {
|
|
|
43681
43827
|
const exportArgs = [
|
|
43682
43828
|
"--headless",
|
|
43683
43829
|
"--log-file",
|
|
43684
|
-
|
|
43830
|
+
join25(exportLogs, "engine.log"),
|
|
43685
43831
|
"--path",
|
|
43686
43832
|
project.value.path,
|
|
43687
43833
|
debug ? "--export-debug" : "--export-release",
|
|
@@ -43711,9 +43857,9 @@ class GodotServer {
|
|
|
43711
43857
|
if (!paths.ok) {
|
|
43712
43858
|
return this.createErrorResponse(paths.reason, PATH_SOLUTIONS);
|
|
43713
43859
|
}
|
|
43714
|
-
const
|
|
43715
|
-
const
|
|
43716
|
-
if (
|
|
43860
|
+
const missing = paths.value.filter((path) => !existsSync19(join25(project.value.path, path.slice("res://".length))));
|
|
43861
|
+
const asked = paths.value.filter((path) => !missing.includes(path));
|
|
43862
|
+
if (asked.length === 0) {
|
|
43717
43863
|
const verdict = `nothing at ${missing.join(" and ")}`;
|
|
43718
43864
|
return {
|
|
43719
43865
|
content: [
|
|
@@ -43727,7 +43873,7 @@ class GodotServer {
|
|
|
43727
43873
|
};
|
|
43728
43874
|
}
|
|
43729
43875
|
const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
|
|
43730
|
-
if (!existsSync19(
|
|
43876
|
+
if (!existsSync19(join25(project.value.path, runner))) {
|
|
43731
43877
|
return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
|
|
43732
43878
|
"Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
|
|
43733
43879
|
]);
|
|
@@ -43792,14 +43938,14 @@ class GodotServer {
|
|
|
43792
43938
|
await untilGone([run.process.pid]);
|
|
43793
43939
|
}
|
|
43794
43940
|
await run.settled;
|
|
43795
|
-
const reportsDir =
|
|
43941
|
+
const reportsDir = join25(project.value.path, ".godot", ...ours.split("/"));
|
|
43796
43942
|
let report = null;
|
|
43797
43943
|
let reportProblem = null;
|
|
43798
43944
|
try {
|
|
43799
43945
|
const written = existsSync19(reportsDir) ? readdirSync10(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
|
|
43800
43946
|
const newest = written.at(-1);
|
|
43801
43947
|
if (newest !== undefined) {
|
|
43802
|
-
report = parseJUnit(readFileSync21(
|
|
43948
|
+
report = parseJUnit(readFileSync21(join25(reportsDir, newest, "results.xml"), "utf8"));
|
|
43803
43949
|
}
|
|
43804
43950
|
} catch (error) {
|
|
43805
43951
|
reportProblem = errorMessage(error);
|
|
@@ -43828,9 +43974,14 @@ class GodotServer {
|
|
|
43828
43974
|
const nothingRan = timedOut ? null : whyNoReport(said, asked.join(" and "));
|
|
43829
43975
|
const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
|
|
43830
43976
|
const hung = cutShort?.hung ?? false;
|
|
43831
|
-
const
|
|
43977
|
+
const printedErrors = timedOut || exitCode !== 105 ? [] : scriptErrorsPrinted(said);
|
|
43978
|
+
const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
|
|
43979
|
+
const scriptErrors = [...declaring.origins, ...printedErrors];
|
|
43832
43980
|
const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
|
|
43833
|
-
const
|
|
43981
|
+
const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
|
|
43982
|
+
const nothingAt = missing.join(" and ");
|
|
43983
|
+
const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
|
|
43984
|
+
const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
|
|
43834
43985
|
const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
|
|
43835
43986
|
const marker = readNonEmptyString(args, "printed");
|
|
43836
43987
|
const echoed = marker === undefined ? undefined : run.log.select({ severity: "info", sinceLastCall: false, contains: marker, limit: 200 });
|
|
@@ -43839,7 +43990,8 @@ class GodotServer {
|
|
|
43839
43990
|
...echoed.omitted > 0 ? { printedOmitted: echoed.omitted } : {}
|
|
43840
43991
|
};
|
|
43841
43992
|
if (report === null) {
|
|
43842
|
-
const
|
|
43993
|
+
const said = nothingRan !== null ? `No tests ran: ${ranAs}.` : scriptErrors.length > 0 ? scriptErrorsNote(printedErrors, declaring) : reportProblem !== null ? `The test run's report could not be read (${reportProblem}); the engine's own verdict was ${ranAs}.` : `The test run wrote no report (${ranAs}).`;
|
|
43994
|
+
const note = missingNote === undefined ? said : `${said} ${missingNote}`;
|
|
43843
43995
|
return {
|
|
43844
43996
|
content: [
|
|
43845
43997
|
{ type: "text", text: note },
|
|
@@ -43853,9 +44005,10 @@ class GodotServer {
|
|
|
43853
44005
|
exitSignal: run.exitSignal ?? undefined,
|
|
43854
44006
|
hung,
|
|
43855
44007
|
...timedOut ? { timedOut, silentForMs } : {},
|
|
43856
|
-
...
|
|
44008
|
+
...missing.length > 0 ? { missing } : {},
|
|
44009
|
+
...nonEmpty(foldedScriptErrors(scriptErrors)),
|
|
43857
44010
|
arguments: cmdArgs,
|
|
43858
|
-
entries: forAnswer(printed.slice(0, 60).map(aboveTheRunner)),
|
|
44011
|
+
entries: forAnswer(withoutListed(printed, scriptErrors).slice(0, 60).map(aboveTheRunner)),
|
|
43859
44012
|
...printedLines,
|
|
43860
44013
|
savesNote,
|
|
43861
44014
|
...scanned
|
|
@@ -43890,8 +44043,9 @@ class GodotServer {
|
|
|
43890
44043
|
});
|
|
43891
44044
|
const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
|
|
43892
44045
|
return this.jsonTextResponse({
|
|
43893
|
-
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0,
|
|
44046
|
+
passed: !timedOut && exitCode === 0 && nothingRan === null && report.tests > 0 && report.failures === 0 && report.errors === 0 && hookFailures.length === 0 && missing.length === 0,
|
|
43894
44047
|
verdict,
|
|
44048
|
+
...missing.length > 0 ? { missing } : {},
|
|
43895
44049
|
...timedOut ? { timedOut, hung, silentForMs } : {},
|
|
43896
44050
|
exitCode,
|
|
43897
44051
|
exitSignal: run.exitSignal ?? undefined,
|
|
@@ -43908,7 +44062,10 @@ class GodotServer {
|
|
|
43908
44062
|
...printedLines,
|
|
43909
44063
|
savesNote,
|
|
43910
44064
|
...scanned,
|
|
43911
|
-
note:
|
|
44065
|
+
note: [
|
|
44066
|
+
missingNote,
|
|
44067
|
+
ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
|
|
44068
|
+
].filter((part) => part !== undefined).join(" ") || undefined,
|
|
43912
44069
|
suites: unclean.map((suite) => ({
|
|
43913
44070
|
name: suite.name,
|
|
43914
44071
|
path: suite.path,
|
|
@@ -44359,7 +44516,7 @@ class GodotServer {
|
|
|
44359
44516
|
return null;
|
|
44360
44517
|
}
|
|
44361
44518
|
try {
|
|
44362
|
-
return settingKeys(readFileSync21(
|
|
44519
|
+
return settingKeys(readFileSync21(join25(projectPath, "project.godot"), "utf8"));
|
|
44363
44520
|
} catch {
|
|
44364
44521
|
return null;
|
|
44365
44522
|
}
|
|
@@ -44681,8 +44838,15 @@ class GodotServer {
|
|
|
44681
44838
|
}
|
|
44682
44839
|
async runtimeUp(projectPath, before, budget, withArgs) {
|
|
44683
44840
|
const budgetMs = budget.ms;
|
|
44684
|
-
if (!existsSync19(
|
|
44685
|
-
return runtimeVerdict(null, {
|
|
44841
|
+
if (!existsSync19(join25(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
44842
|
+
return runtimeVerdict(null, {
|
|
44843
|
+
addon: false,
|
|
44844
|
+
budgetMs,
|
|
44845
|
+
heldAt: null,
|
|
44846
|
+
running: false,
|
|
44847
|
+
withArgs,
|
|
44848
|
+
servesAProject: this.ownProject !== null
|
|
44849
|
+
});
|
|
44686
44850
|
}
|
|
44687
44851
|
let asking = null;
|
|
44688
44852
|
let editorAskedAt = 0;
|
|
@@ -44734,6 +44898,7 @@ class GodotServer {
|
|
|
44734
44898
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
44735
44899
|
running: endpoint !== null || going !== null && await this.runStillGoing(going),
|
|
44736
44900
|
withArgs,
|
|
44901
|
+
servesAProject: this.ownProject !== null,
|
|
44737
44902
|
...going?.throughEditor === true ? { playingSeen: going.seenPlaying === true } : {}
|
|
44738
44903
|
});
|
|
44739
44904
|
}
|
|
@@ -45690,7 +45855,7 @@ class GodotServer {
|
|
|
45690
45855
|
notes.push(endedWithoutACode({
|
|
45691
45856
|
throughEditor: run.throughEditor,
|
|
45692
45857
|
announced: announced !== undefined || run.announcedPid !== undefined,
|
|
45693
|
-
couldAnnounce: run.projectPath !== null && existsSync19(
|
|
45858
|
+
couldAnnounce: run.projectPath !== null && existsSync19(join25(run.projectPath, RUNTIME_AUTOLOAD.path)),
|
|
45694
45859
|
endedHere: typeof run.endedHere === "string",
|
|
45695
45860
|
pickedUp: run.pickedUp === true
|
|
45696
45861
|
}));
|
|
@@ -46539,7 +46704,7 @@ class GodotServer {
|
|
|
46539
46704
|
async ownRunOnItsWay(projectPath, budgetMs) {
|
|
46540
46705
|
const run = await this.currentRun();
|
|
46541
46706
|
const project = projectPath ?? run?.projectPath ?? null;
|
|
46542
|
-
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync19(
|
|
46707
|
+
if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync19(join25(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
|
|
46543
46708
|
return null;
|
|
46544
46709
|
}
|
|
46545
46710
|
const already = runtimesAnnounced().running;
|
|
@@ -46623,7 +46788,7 @@ class GodotServer {
|
|
|
46623
46788
|
}
|
|
46624
46789
|
}
|
|
46625
46790
|
const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
|
|
46626
|
-
const screenshotPath = screenshotDir ?
|
|
46791
|
+
const screenshotPath = screenshotDir ? join25(screenshotDir, "capture.png") : null;
|
|
46627
46792
|
const staleNote = this.runtimeStaleNote(choice.endpoint);
|
|
46628
46793
|
const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
|
|
46629
46794
|
const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}
|
|
@@ -46814,6 +46979,7 @@ var init_server3 = __esm(() => {
|
|
|
46814
46979
|
init_run_record();
|
|
46815
46980
|
init_runtime_client();
|
|
46816
46981
|
init_scratch();
|
|
46982
|
+
init_script_origins();
|
|
46817
46983
|
init_server_version();
|
|
46818
46984
|
init_setup();
|
|
46819
46985
|
init_tool_definitions();
|
|
@@ -46870,7 +47036,7 @@ init_bridge_announce();
|
|
|
46870
47036
|
init_errors();
|
|
46871
47037
|
init_godot_path();
|
|
46872
47038
|
import { existsSync as existsSync20 } from "node:fs";
|
|
46873
|
-
import { join as
|
|
47039
|
+
import { join as join26, resolve as resolve8 } from "node:path";
|
|
46874
47040
|
|
|
46875
47041
|
// src/harnesses.ts
|
|
46876
47042
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
@@ -47978,7 +48144,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
47978
48144
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
47979
48145
|
| What a property reads right now, on a node whose path you already hold | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
47980
48146
|
| What a property on an object a node holds reads | the same two, with colons: \`_game:clock:speed\` |
|
|
47981
|
-
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method
|
|
48147
|
+
| Which control holds the focus, or anything only a method answers | the same path with a step written as a call, \`get_viewport():gui_get_focus_owner\`, which calls a method that only reads and walks into what it returned; an engine method takes literal arguments in the brackets, \`get_theme_constant("separation")\` |
|
|
47982
48148
|
| Press a button | \`runtime_input click\` with \`says\` and the words on it, which finds it and presses it in the same frame and says what was under the pointer. A \`find\` and then a click by the path it answered is two calls over a generated path, and a panel that rebuilds between them frees the button first; \`nodePath\` narrows where \`says\` looks, and several matches are refused with an \`index\` for each |
|
|
47983
48149
|
| Press the next thing after a click that changed screen | \`runtime_wait until\` with \`says\` and a word the new screen shows (no \`nodePath\` needed: without one the whole game is searched), then the click. A click sent straight after a screen change lands on a control the new screen is about to free, answers \`landed: true\`, and does nothing; a frame count is a number nobody can pick correctly across a cold start and a warm one, and waiting for the words answered in about 130ms where twenty frames had not been enough |
|
|
47984
48150
|
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
@@ -48201,7 +48367,7 @@ class UsageError extends Refusal {
|
|
|
48201
48367
|
function projectArgument(at) {
|
|
48202
48368
|
const named = args.slice(at).find((value) => !value.startsWith("--"));
|
|
48203
48369
|
const projectPath = resolve8(named ?? ".");
|
|
48204
|
-
if (!existsSync20(
|
|
48370
|
+
if (!existsSync20(join26(projectPath, "project.godot"))) {
|
|
48205
48371
|
throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
|
|
48206
48372
|
}
|
|
48207
48373
|
return projectPath;
|
|
@@ -48246,7 +48412,7 @@ async function registerRuntime(godot, projectPath) {
|
|
|
48246
48412
|
return;
|
|
48247
48413
|
}
|
|
48248
48414
|
console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
|
|
48249
|
-
if (!existsSync20(
|
|
48415
|
+
if (!existsSync20(join26(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
|
|
48250
48416
|
console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
|
|
48251
48417
|
}
|
|
48252
48418
|
}
|