gdharness 1.2.35 → 1.2.36
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 +166 -34
- package/build/godot/addons/gdharness_runtime/call_errors.gd +7 -2
- package/build/godot/addons/gdharness_runtime/runtime_accessibility.gd +9 -5
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +33 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +12 -4
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +7 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +113 -3
- package/build/godot/operations/classdb_queries.gd +22 -0
- package/build/godot/operations/dependencies.gd +90 -20
- package/build/godot/operations/gdscript_analysis.gd +19 -0
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/rename_checks.gd +0 -19
- package/build/godot/operations/script_check.gd +98 -0
- package/build/index.js +163 -33
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -35215,7 +35215,7 @@ var TOOL_SPECS = [
|
|
|
35215
35215
|
direction: {
|
|
35216
35216
|
type: "string",
|
|
35217
35217
|
enum: ["forward", "reverse"],
|
|
35218
|
-
description: "forward: what this resource loads, with cycles reported. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind, and
|
|
35218
|
+
description: "forward: what this resource loads, with cycles reported, a script's dependencies including every script whose class_name it uses, marked by_class with the name. summary.total_dependencies counts each file once however many places load it, and total_references counts the entries in the tree. reverse: every file that refers to it and how, a scene instancing it, a script extending, preloading or loading it, and for a script with a class_name every use of that name. Each reference carries a kind: ext_resource, extends, preload, load or path for a reference by 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. Two kinds are mentions rather than uses: doc is a name inside a ## documentation comment, which renaming would break in the generated docs, and comment is one in ordinary prose, which it would only make stale. summary.in_code is the count with both of those left out, which is the number to read when the question is whether anything still uses this. Default forward."
|
|
35219
35219
|
},
|
|
35220
35220
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
35221
35221
|
includeBuiltin: {
|
|
@@ -35692,7 +35692,7 @@ var TOOL_SPECS = [
|
|
|
35692
35692
|
operations: {
|
|
35693
35693
|
structure: {
|
|
35694
35694
|
effect: "read",
|
|
35695
|
-
summary: "functions, variables, signals, class_name and extends, read from the file",
|
|
35695
|
+
summary: "functions, variables, signals, class_name and extends, 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",
|
|
35696
35696
|
requires: []
|
|
35697
35697
|
},
|
|
35698
35698
|
symbols: {
|
|
@@ -35713,7 +35713,7 @@ var TOOL_SPECS = [
|
|
|
35713
35713
|
name: "script_diagnostics",
|
|
35714
35714
|
title: "Script diagnostics",
|
|
35715
35715
|
effect: "read",
|
|
35716
|
-
description: "Errors and warnings for a script, or for several, from the editor's language server, and whether each is clean.
|
|
35716
|
+
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: every error that stops a script compiling, with its line and no column, dependenciesWithErrors naming the other scripts that failed while it loaded, and warnings null, since the engine reports warnings only to an editor. 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.",
|
|
35717
35717
|
parameters: {
|
|
35718
35718
|
projectPath: PROJECT_PATH2,
|
|
35719
35719
|
scriptPath: {
|
|
@@ -36171,7 +36171,7 @@ var TOOL_SPECS = [
|
|
|
36171
36171
|
says: {
|
|
36172
36172
|
type: "string",
|
|
36173
36173
|
ops: ["find", "accessibility"],
|
|
36174
|
-
description: 'accessibility: only the elements whose name or
|
|
36174
|
+
description: 'accessibility: only the elements whose name, value or value_text says these words, matched as find matches them, with what they hold. 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.'
|
|
36175
36175
|
},
|
|
36176
36176
|
limit: {
|
|
36177
36177
|
type: "integer",
|
|
@@ -36202,7 +36202,7 @@ var TOOL_SPECS = [
|
|
|
36202
36202
|
tree: { effect: "read", summary: "the live scene tree", requires: [] },
|
|
36203
36203
|
text: {
|
|
36204
36204
|
effect: "read",
|
|
36205
|
-
summary: "every line of text under nodePath, the values in its fields included, in
|
|
36205
|
+
summary: "every line of text under nodePath, the values in its fields included, in reading order, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items and the titles of its separators read a line each",
|
|
36206
36206
|
requires: []
|
|
36207
36207
|
},
|
|
36208
36208
|
find: {
|
|
@@ -36222,12 +36222,12 @@ var TOOL_SPECS = [
|
|
|
36222
36222
|
},
|
|
36223
36223
|
metrics: {
|
|
36224
36224
|
effect: "read",
|
|
36225
|
-
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game
|
|
36225
|
+
summary: "frame time, memory, draw calls and the rest. frame_time is the mean wall time of a frame over the last frames_timed frames (up to 120, timed by the game, physics and the wait for the display included), frame_time_max the slowest of them, both in seconds and null before a frame has passed; vsync says whether a frame waits for the display, and max_fps is the game's frame-rate cap, 0 for none. physics_steps_per_second is the physics steps taken a second over those frames, fewer than physics_ticks_per_second when a frame needs more than max_physics_steps_per_frame or the editor's Game view slows it. fps, process_time_max and physics_time are the engine's monitors, which it sets once a second: the frames counted in the last second, and the slowest process and physics step in it, so they trail a change by up to a second and the last two are a worst case rather than the frame. Memory, object and render counts are read as asked. memory_static is what the engine allocated on the CPU side, in bytes; render_video_mem_used, render_texture_mem_used and render_buffer_mem_used are what the renderer has on the GPU, in bytes, which memory_static does not count",
|
|
36226
36226
|
requires: []
|
|
36227
36227
|
},
|
|
36228
36228
|
accessibility: {
|
|
36229
36229
|
effect: "read",
|
|
36230
|
-
summary:
|
|
36230
|
+
summary: `what the game tells a screen reader, read through UI Automation as a Windows screen reader does, under windows: one tree per window, each element with its role (text for static text, button, check_box, group for a control holding others), name, value, value_text (a range's spoken words, such as "75%"), description, placeholder, bounds in the window's pixels, the actions a reader is offered (click, focus, toggle, set_value, select, expand_collapse, scroll_into_view), and focused on the focused one. node is the node that made the element; a part a node made of itself has part_of, the node drawn around it that holds its words, or part_of_candidates where several are, and part: announcement marks the text a window announces through. An empty name reads as nothing: static text takes it from accessibility_update_set_value, and _note counts the text elements on screen with none. tooltip is the node's tooltip_text, which a Windows reader is not told. With nothing focused, the engine calls GdharnessRuntime focused. Needs a windowed Windows run started with engineArgs ["--accessibility", "always"] or accessibility_support at Always Active, since the engine builds the tree only when asked; reading takes a second or two`,
|
|
36231
36231
|
requires: []
|
|
36232
36232
|
}
|
|
36233
36233
|
},
|
|
@@ -36236,7 +36236,7 @@ var TOOL_SPECS = [
|
|
|
36236
36236
|
{
|
|
36237
36237
|
name: "runtime_invoke",
|
|
36238
36238
|
title: "Set or call in the running game",
|
|
36239
|
-
description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors
|
|
36239
|
+
description: "Sets a property or calls a method on a node, or on one of the engine singletons nodePath names, in the running game. Needs the game running with the runtime addon. The answer carries elapsed_usec, how long the call or the write took inside the game, measured around it there: the round trip to the game is a second or more, so timing from outside measures that instead. Errors the engine reported while a called method ran come back under errors and warnings under warnings, the first 20 of each, with errors_not_shown and warnings_not_shown counting the rest. A call that takes longer than timeoutMs is not cancelled: the answer is pending: true with a requestId, the call goes on and does everything it was asked, and op result with that requestId collects its reply once it comes.",
|
|
36240
36240
|
parameters: {
|
|
36241
36241
|
projectPath: RUNNING_PROJECT_PATH,
|
|
36242
36242
|
pid: RUNNING_PID,
|
|
@@ -36356,7 +36356,7 @@ var TOOL_SPECS = [
|
|
|
36356
36356
|
{
|
|
36357
36357
|
name: "runtime_input",
|
|
36358
36358
|
title: "Send input to the game",
|
|
36359
|
-
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
|
|
36359
|
+
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.",
|
|
36360
36360
|
answer: 'click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, 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, 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. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.',
|
|
36361
36361
|
parameters: {
|
|
36362
36362
|
projectPath: RUNNING_PROJECT_PATH,
|
|
@@ -36553,10 +36553,16 @@ var TOOL_SPECS = [
|
|
|
36553
36553
|
},
|
|
36554
36554
|
nodePath: {
|
|
36555
36555
|
type: "string",
|
|
36556
|
-
ops: ["signal", "until"],
|
|
36557
|
-
description: "signal, until: the node. Needed for a signal
|
|
36556
|
+
ops: ["signal", "watch", "until"],
|
|
36557
|
+
description: "signal, watch, until: the node. Needed for a signal without a watch, a watch and a property; for says it defaults to /root, the whole screen."
|
|
36558
|
+
},
|
|
36559
|
+
signal: { type: "string", ops: ["signal", "watch"], description: "signal, watch: the signal name." },
|
|
36560
|
+
watch: {
|
|
36561
|
+
type: "integer",
|
|
36562
|
+
minimum: 1,
|
|
36563
|
+
ops: ["signal"],
|
|
36564
|
+
description: "signal: the watch an earlier watch answered, in place of nodePath and signal. Answers at once, with fired_before_wait, when the signal fired after the watch was armed, and otherwise waits for it; times counts how often it fired, and args is what the first carried. The watch goes with the answer."
|
|
36558
36565
|
},
|
|
36559
|
-
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
36560
36566
|
property: {
|
|
36561
36567
|
type: "string",
|
|
36562
36568
|
ops: ["until"],
|
|
@@ -36596,6 +36602,11 @@ var TOOL_SPECS = [
|
|
|
36596
36602
|
signal: {
|
|
36597
36603
|
effect: "read",
|
|
36598
36604
|
summary: "wait for a signal and answer with what it carried",
|
|
36605
|
+
requires: []
|
|
36606
|
+
},
|
|
36607
|
+
watch: {
|
|
36608
|
+
effect: "read",
|
|
36609
|
+
summary: "start recording a signal and answer at once with a watch; arm it before the call meant to set the signal off, then collect it with signal and the watch. A client can run a call sent beside a wait only after the wait answers, so a wait sent with the call misses it",
|
|
36599
36610
|
requires: ["nodePath", "signal"]
|
|
36600
36611
|
},
|
|
36601
36612
|
until: {
|
|
@@ -37249,6 +37260,12 @@ function endedWithoutACode(run) {
|
|
|
37249
37260
|
}
|
|
37250
37261
|
return `The editor stopped playing this run. ${kept} What it printed is below.`;
|
|
37251
37262
|
}
|
|
37263
|
+
function ourRunEnded(run, now) {
|
|
37264
|
+
const which = `${run.throughEditor ? "played in the editor" : "started"}${run.pid === null ? "" : ` (pid ${run.pid})`}`;
|
|
37265
|
+
const when = run.endedAt === undefined ? "" : ` ${Math.max(0, Math.round((now - run.endedAt) / 1000))}s ago`;
|
|
37266
|
+
const how = run.endedThrough === "close" ? "this server asked it to close" : run.endedThrough === "keeper" ? "this server ended it through the keeper holding it" : run.endedThrough === "signal" ? "this server signalled it" : run.endedThrough === "editor" ? "this server asked the editor to stop it" : run.exitCode !== null ? `it exited by itself with code ${run.exitCode}` : run.exitSignal === null ? "it is no longer running" : `it was ended by ${run.exitSignal}`;
|
|
37267
|
+
return `No game with the runtime addon is running: the game this server ${which} ended${when}, and ${how}. editor_output has what it printed, and editor_run start runs it again.`;
|
|
37268
|
+
}
|
|
37252
37269
|
function stopVerdict(stop) {
|
|
37253
37270
|
const notClosed = stop.notClosedBecause === undefined ? "" : ` It was asked to close first and was not ended by that: ${stop.notClosedBecause}.`;
|
|
37254
37271
|
if (!stop.wasRunning) {
|
|
@@ -38252,6 +38269,13 @@ function scriptsToDiagnose(projectPath, given) {
|
|
|
38252
38269
|
}
|
|
38253
38270
|
return { ok: true, value: unique };
|
|
38254
38271
|
}
|
|
38272
|
+
function engineChecked(why) {
|
|
38273
|
+
return {
|
|
38274
|
+
checkedBy: "engine",
|
|
38275
|
+
languageServer: why,
|
|
38276
|
+
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, and no warnings, which the engine tells only an editor: warnings is null rather than 0. editor_launch with hidden: true opens this project's editor without a window, and its language server reports both."
|
|
38277
|
+
};
|
|
38278
|
+
}
|
|
38255
38279
|
var EXTERNAL_MEMBER = 'Could not resolve external class member "';
|
|
38256
38280
|
function isDirectory(path) {
|
|
38257
38281
|
try {
|
|
@@ -40442,6 +40466,10 @@ class GodotServer {
|
|
|
40442
40466
|
}
|
|
40443
40467
|
if (scripts?.value == null) {
|
|
40444
40468
|
const one = await this.diagnosticsOf(args);
|
|
40469
|
+
if ("noLanguageServer" in one) {
|
|
40470
|
+
const checked = project.ok ? await this.engineDiagnostics(project.value.path, [readString(args, "scriptPath") ?? ""], one.noLanguageServer) : { refusal: project.response };
|
|
40471
|
+
return "refusal" in checked ? checked.refusal : this.jsonTextResponse({ ...checked.answers[0], ...engineChecked(one.noLanguageServer.reason) });
|
|
40472
|
+
}
|
|
40445
40473
|
return "refusal" in one ? one.refusal : this.jsonTextResponse(this.markedIfStale(one.answer));
|
|
40446
40474
|
}
|
|
40447
40475
|
const progress = callProgress();
|
|
@@ -40451,12 +40479,31 @@ class GodotServer {
|
|
|
40451
40479
|
let errors = 0;
|
|
40452
40480
|
let warnings = 0;
|
|
40453
40481
|
let unread = 0;
|
|
40482
|
+
let byEngine = null;
|
|
40454
40483
|
for (const [at, scriptPath] of scripts.value.entries()) {
|
|
40455
40484
|
if (cancelled?.aborted === true) {
|
|
40456
40485
|
return this.createErrorResponse(`Cancelled after ${at} of ${scripts.value.length} scripts.`);
|
|
40457
40486
|
}
|
|
40458
40487
|
progress?.(at + 1, `${at + 1} of ${scripts.value.length}: ${scriptPath}`);
|
|
40459
40488
|
const one = await this.diagnosticsOf({ ...args, scriptPath });
|
|
40489
|
+
if ("noLanguageServer" in one) {
|
|
40490
|
+
const rest = scripts.value.slice(at);
|
|
40491
|
+
progress?.(at + 1, `${rest.length} script(s) left, compiled by a headless engine`);
|
|
40492
|
+
const checked = await this.engineDiagnostics(project.ok ? project.value.path : "", rest, one.noLanguageServer);
|
|
40493
|
+
if ("refusal" in checked) {
|
|
40494
|
+
return checked.refusal;
|
|
40495
|
+
}
|
|
40496
|
+
for (const answer of checked.answers) {
|
|
40497
|
+
errors += Number(answer["errors"]);
|
|
40498
|
+
if (readArray(answer, "diagnostics")?.length === 0) {
|
|
40499
|
+
nothingReported.push(String(answer["scriptPath"]));
|
|
40500
|
+
} else {
|
|
40501
|
+
reported.push(answer);
|
|
40502
|
+
}
|
|
40503
|
+
}
|
|
40504
|
+
byEngine = one.noLanguageServer.reason;
|
|
40505
|
+
break;
|
|
40506
|
+
}
|
|
40460
40507
|
if ("refusal" in one) {
|
|
40461
40508
|
if (!one.perScript) {
|
|
40462
40509
|
return one.refusal;
|
|
@@ -40477,12 +40524,68 @@ class GodotServer {
|
|
|
40477
40524
|
clean: errors === 0 && unread === 0,
|
|
40478
40525
|
scriptsChecked: scripts.value.length,
|
|
40479
40526
|
errors,
|
|
40480
|
-
warnings,
|
|
40527
|
+
warnings: byEngine === null ? warnings : null,
|
|
40481
40528
|
...unread > 0 ? { scriptsNotRead: unread } : {},
|
|
40482
40529
|
scripts: reported,
|
|
40483
|
-
nothingReported
|
|
40530
|
+
nothingReported,
|
|
40531
|
+
...byEngine === null ? {} : engineChecked(byEngine)
|
|
40484
40532
|
}));
|
|
40485
40533
|
}
|
|
40534
|
+
async compileScripts(projectPath, paths) {
|
|
40535
|
+
const payload = { checked: [], failed: [], scripts: [] };
|
|
40536
|
+
const messages = [];
|
|
40537
|
+
let left = [...paths];
|
|
40538
|
+
while (left.length > 0) {
|
|
40539
|
+
const ran = await this.operation("check_scripts", { paths: left }, projectPath);
|
|
40540
|
+
if (!ran.ok) {
|
|
40541
|
+
return { ok: false, message: ran.message };
|
|
40542
|
+
}
|
|
40543
|
+
const checked = (readArray(ran.payload, "checked") ?? []).map(String);
|
|
40544
|
+
if (checked.length === 0) {
|
|
40545
|
+
return { ok: false, message: `check_scripts read none of ${left.length} script(s)` };
|
|
40546
|
+
}
|
|
40547
|
+
payload.checked.push(...checked);
|
|
40548
|
+
payload.failed.push(...(readArray(ran.payload, "failed") ?? []).map(String));
|
|
40549
|
+
payload.scripts.push(...readArray(ran.payload, "scripts") ?? []);
|
|
40550
|
+
messages.push(...ran.messages);
|
|
40551
|
+
left = left.slice(checked.length);
|
|
40552
|
+
}
|
|
40553
|
+
return { ok: true, payload, extras: engineExtras({ messages }) };
|
|
40554
|
+
}
|
|
40555
|
+
async engineDiagnostics(projectPath, scripts, why) {
|
|
40556
|
+
const named = scripts.map((script) => {
|
|
40557
|
+
const contained = resolveWithinProject(projectPath, script);
|
|
40558
|
+
return contained.ok ? `res://${contained.relativePath}` : script;
|
|
40559
|
+
});
|
|
40560
|
+
const checked = await this.compileScripts(projectPath, named);
|
|
40561
|
+
if (!checked.ok) {
|
|
40562
|
+
return {
|
|
40563
|
+
refusal: this.createErrorResponse(`Diagnostics unavailable: ${why.reason.replace(/\.?$/, ".")} No headless engine could compile the scripts instead: ${checked.message}`, why.solutions)
|
|
40564
|
+
};
|
|
40565
|
+
}
|
|
40566
|
+
const failures = new Map((readArray(checked.payload, "scripts") ?? []).map((entry) => [
|
|
40567
|
+
String(asParams(entry)["path"]),
|
|
40568
|
+
asParams(entry)
|
|
40569
|
+
]));
|
|
40570
|
+
return {
|
|
40571
|
+
answers: scripts.map((scriptPath, at) => {
|
|
40572
|
+
const failure = failures.get(named[at] ?? "");
|
|
40573
|
+
const diagnostics = (readArray(failure ?? {}, "errors") ?? []).map((entry) => {
|
|
40574
|
+
const { line, message } = asParams(entry);
|
|
40575
|
+
return { severity: "error", message, ...typeof line === "number" && line > 0 ? { line } : {} };
|
|
40576
|
+
});
|
|
40577
|
+
const dependencies = readArray(failure ?? {}, "dependencies_with_errors");
|
|
40578
|
+
return {
|
|
40579
|
+
scriptPath,
|
|
40580
|
+
clean: diagnostics.length === 0,
|
|
40581
|
+
errors: diagnostics.length,
|
|
40582
|
+
warnings: null,
|
|
40583
|
+
diagnostics,
|
|
40584
|
+
...dependencies === undefined ? {} : { dependenciesWithErrors: dependencies }
|
|
40585
|
+
};
|
|
40586
|
+
})
|
|
40587
|
+
};
|
|
40588
|
+
}
|
|
40486
40589
|
markedIfStale(answer) {
|
|
40487
40590
|
return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
|
|
40488
40591
|
}
|
|
@@ -40494,6 +40597,16 @@ class GodotServer {
|
|
|
40494
40597
|
const answer = await this.handleLSP("lsp_get_diagnostics", args);
|
|
40495
40598
|
const payload = asParams(JSON.parse(answer.content[0]?.text ?? "{}"));
|
|
40496
40599
|
const refused = this.languageServerRefusal(payload, "Diagnostics");
|
|
40600
|
+
const project = this.project(args);
|
|
40601
|
+
const ownEditor = project.ok && this.editorHasOpen(project.value.path);
|
|
40602
|
+
if (refused !== null && !refused.perScript && payload["stillReading"] !== true && !ownEditor) {
|
|
40603
|
+
return {
|
|
40604
|
+
noLanguageServer: {
|
|
40605
|
+
reason: typeof payload["error"] === "string" ? payload["error"] : JSON.stringify(payload["error"]),
|
|
40606
|
+
solutions: this.languageServerSolutions(payload)
|
|
40607
|
+
}
|
|
40608
|
+
};
|
|
40609
|
+
}
|
|
40497
40610
|
if (refused !== null) {
|
|
40498
40611
|
return refused;
|
|
40499
40612
|
}
|
|
@@ -40653,24 +40766,24 @@ class GodotServer {
|
|
|
40653
40766
|
if (payload["stillReading"] === true) {
|
|
40654
40767
|
return { refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`), perScript: false };
|
|
40655
40768
|
}
|
|
40656
|
-
if (typeof payload["serves"] === "string") {
|
|
40657
|
-
return {
|
|
40658
|
-
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, [
|
|
40659
|
-
"editor_status names the editor on the bridge and the port it says it serves",
|
|
40660
|
-
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
40661
|
-
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
40662
|
-
]),
|
|
40663
|
-
perScript: false
|
|
40664
|
-
};
|
|
40665
|
-
}
|
|
40666
40769
|
return {
|
|
40667
|
-
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`,
|
|
40668
|
-
|
|
40669
|
-
"GDHARNESS_LSP_PORT points this server at another one"
|
|
40670
|
-
]),
|
|
40671
|
-
perScript: payload["aboutThisFile"] === true
|
|
40770
|
+
refusal: this.createErrorResponse(`${asked} unavailable: ${reason}`, this.languageServerSolutions(payload)),
|
|
40771
|
+
perScript: typeof payload["serves"] !== "string" && payload["aboutThisFile"] === true
|
|
40672
40772
|
};
|
|
40673
40773
|
}
|
|
40774
|
+
languageServerSolutions(payload) {
|
|
40775
|
+
if (typeof payload["serves"] === "string") {
|
|
40776
|
+
return [
|
|
40777
|
+
...this.godotBridge.getStatus().connected ? ["editor_status names the editor on the bridge and the port it says it serves"] : [],
|
|
40778
|
+
"editor_launch opens this project's own editor, on a language server port of its own",
|
|
40779
|
+
"GDHARNESS_LSP_PORT points this server at the right one"
|
|
40780
|
+
];
|
|
40781
|
+
}
|
|
40782
|
+
return [
|
|
40783
|
+
`Ensure the Godot editor is running with its language server enabled, on port ${this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT)}`,
|
|
40784
|
+
"GDHARNESS_LSP_PORT points this server at another one"
|
|
40785
|
+
];
|
|
40786
|
+
}
|
|
40674
40787
|
async lsp() {
|
|
40675
40788
|
const port = this.editorServes("lspPort", "GDHARNESS_LSP_PORT", DEFAULT_LSP_PORT);
|
|
40676
40789
|
if (this.lspClient !== null && this.lspClient.port !== port) {
|
|
@@ -43724,9 +43837,9 @@ class GodotServer {
|
|
|
43724
43837
|
ok &&= rebuilt.ok;
|
|
43725
43838
|
}
|
|
43726
43839
|
if (changed.length > 0) {
|
|
43727
|
-
const compiled = await this.
|
|
43728
|
-
report["compiled"] = compiled.ok ? { ...compiled.payload, ...
|
|
43729
|
-
ok &&= compiled.ok &&
|
|
43840
|
+
const compiled = await this.compileScripts(projectPath, [...changed]);
|
|
43841
|
+
report["compiled"] = compiled.ok ? { ...compiled.payload, ...compiled.extras } : { problem: compiled.message };
|
|
43842
|
+
ok &&= compiled.ok && compiled.payload.failed.length === 0;
|
|
43730
43843
|
}
|
|
43731
43844
|
return { ok, report };
|
|
43732
43845
|
}
|
|
@@ -43992,6 +44105,11 @@ class GodotServer {
|
|
|
43992
44105
|
const own = typeof pid === "number" ? undefined : await this.ownGameAmong(announced.running, wanted) ?? await this.ownGameThroughTheTree(announced.running, wanted);
|
|
43993
44106
|
const choice = chooseRuntime(announced.running, wanted, announced.unspoken, typeof pid === "number" ? pid : own);
|
|
43994
44107
|
if ("problem" in choice) {
|
|
44108
|
+
const ended = this.activeProcess;
|
|
44109
|
+
const nothingAnnounced = typeof pid !== "number" && announced.unspoken.length === 0 && !announced.running.some((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
|
|
44110
|
+
if (nothingAnnounced && ended !== null && !stillRunning(ended) && (wanted === undefined || ended.projectPath === null || isSameDirectory(ended.projectPath, wanted))) {
|
|
44111
|
+
return this.createErrorResponse(ourRunEnded({ ...ended, pid: ended.pid ?? ended.announcedPid ?? null }, Date.now()));
|
|
44112
|
+
}
|
|
43995
44113
|
return this.createErrorResponse(choice.problem);
|
|
43996
44114
|
}
|
|
43997
44115
|
const ofProject = announced.running.filter((one) => wanted === undefined || isSameDirectory(one.project.path, wanted));
|
|
@@ -44157,7 +44275,18 @@ ${staleNote}`}`
|
|
|
44157
44275
|
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
44158
44276
|
return await this.handleRuntimeCommand("wait_frames", { ...whichGame(args), frames }, { tool: "runtime_wait", op, patience: waited });
|
|
44159
44277
|
}
|
|
44278
|
+
if (op === "watch") {
|
|
44279
|
+
return await this.handleRuntimeCommand("watch_signal", {
|
|
44280
|
+
...whichGame(args),
|
|
44281
|
+
path: readNonEmptyString(args, "nodePath") ?? "",
|
|
44282
|
+
signal: readString(args, "signal") ?? ""
|
|
44283
|
+
}, { tool: "runtime_wait", op });
|
|
44284
|
+
}
|
|
44160
44285
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
44286
|
+
const watch = readPositiveNumber(args, "watch");
|
|
44287
|
+
if (op === "signal" && watch === undefined && (readNonEmptyString(args, "nodePath") === undefined || readNonEmptyString(args, "signal") === undefined)) {
|
|
44288
|
+
return this.createErrorResponse("runtime_wait signal needs nodePath and signal, or the watch an earlier runtime_wait watch answered.");
|
|
44289
|
+
}
|
|
44161
44290
|
const asked = readNonEmptyString(args, "nodePath");
|
|
44162
44291
|
if (asked === undefined && op === "until" && readNonEmptyString(args, "says") === undefined) {
|
|
44163
44292
|
return this.createErrorResponse("runtime_wait until needs nodePath to wait for a property: the property belongs to that node.", ["With says instead of property, nodePath can be left out and the whole game is searched"]);
|
|
@@ -44168,7 +44297,8 @@ ${staleNote}`}`
|
|
|
44168
44297
|
...whichGame(args),
|
|
44169
44298
|
path: nodePath,
|
|
44170
44299
|
signal: readString(args, "signal") ?? "",
|
|
44171
|
-
timeout_ms: timeoutMs
|
|
44300
|
+
timeout_ms: timeoutMs,
|
|
44301
|
+
...watch === undefined ? {} : { watch }
|
|
44172
44302
|
}, { tool: "runtime_wait", op, patience }) : await this.handleRuntimeCommand("wait_until", {
|
|
44173
44303
|
...whichGame(args),
|
|
44174
44304
|
path: nodePath,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.36",
|
|
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",
|