gdharness 0.13.30 → 0.13.32
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 +138 -123
- package/build/godot/operations/class_cache.gd +1 -4
- package/build/godot/operations/dependencies.gd +30 -9
- package/build/godot/operations/gdscript_analysis.gd +333 -48
- package/build/godot/operations/gdscript_authoring.gd +22 -11
- package/build/godot/operations/patterns.gd +64 -0
- package/build/index.js +20 -18
- package/package.json +1 -1
|
@@ -13,10 +13,74 @@ extends RefCounted
|
|
|
13
13
|
# `engine_messages`. The empty RegEx it then returns matches nothing, which is the honest result of
|
|
14
14
|
# a pattern that does not exist.
|
|
15
15
|
|
|
16
|
+
# Kept, because the readers below call these per line of every script in a project and a fresh
|
|
17
|
+
# compile each time is the whole cost of the scan. The patterns are literals in this repository, so
|
|
18
|
+
# the set is bounded by the source rather than by anything a caller passes.
|
|
19
|
+
#
|
|
20
|
+
# Held by `test/support/gd/patterns_cache.gd`, with `is_same` rather than `==`: two RegEx built from
|
|
21
|
+
# one pattern match the same text and compare equal on everything else, so identity is the only
|
|
22
|
+
# question that separates a cache from a recompile. Without that case, taking the cache away would
|
|
23
|
+
# have broken no answer anywhere and failed nothing, which is the argued-and-unheld shape.
|
|
24
|
+
static var _compiled: Dictionary = {}
|
|
25
|
+
|
|
16
26
|
|
|
17
27
|
static func compiled(expression: String) -> RegEx:
|
|
28
|
+
if _compiled.has(expression):
|
|
29
|
+
var held: RegEx = _compiled[expression]
|
|
30
|
+
return held
|
|
18
31
|
var regex: RegEx = RegEx.new()
|
|
19
32
|
var built: Error = regex.compile(expression)
|
|
20
33
|
if built != OK:
|
|
21
34
|
push_error("gdharness: could not compile the pattern " + expression)
|
|
35
|
+
_compiled[expression] = regex
|
|
22
36
|
return regex
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# What a header line declares: the class name, and the base named on the same line if there is one.
|
|
40
|
+
#
|
|
41
|
+
# `class_name X extends Y` is one line, so a reader that takes the rest of the line as the name gets
|
|
42
|
+
# `Blade extends Node2D` for the class and never sees the base at all.
|
|
43
|
+
static func declared_class(line: String) -> RegExMatch:
|
|
44
|
+
return compiled("^class_name\\s+([A-Za-z_][A-Za-z0-9_]*)(?:\\s+extends\\s+(\\S+))?").search(line)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# The annotations at the front of a line, in order, written as they are without the leading `@`.
|
|
48
|
+
#
|
|
49
|
+
# For anything that has to answer about the annotations rather than see past them. Splitting one on
|
|
50
|
+
# whitespace loses half of it, since `@export_range(0, 10)` has a space inside the parentheses and
|
|
51
|
+
# was reported as the hint `range(0,`.
|
|
52
|
+
# An `Array[String]` rather than a `PackedStringArray`, because the packed one's `append` answers
|
|
53
|
+
# with a bool and a project holding `return_value_discarded` at error will not compile a script that
|
|
54
|
+
# throws it away, which is the whole reason `compiled` exists a few lines up.
|
|
55
|
+
static func annotations_on(line: String) -> Array[String]:
|
|
56
|
+
var annotation: RegEx = compiled('^@([a-z_]+)(\\(\\s*(?:"[^"]*")?[^)]*\\))?\\s*')
|
|
57
|
+
var found: Array[String] = []
|
|
58
|
+
var rest: String = line.strip_edges()
|
|
59
|
+
var matched: RegExMatch = annotation.search(rest)
|
|
60
|
+
while matched != null:
|
|
61
|
+
found.append(matched.get_string(1) + matched.get_string(2))
|
|
62
|
+
rest = rest.substr(matched.get_end()).strip_edges()
|
|
63
|
+
matched = annotation.search(rest)
|
|
64
|
+
return found
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# A line with its leading annotations taken off, for anything reading a script's header.
|
|
68
|
+
#
|
|
69
|
+
# Annotations may share a line with what they annotate: `@abstract class_name X` is one line in
|
|
70
|
+
# Godot 4.5 and later, and `@tool` and `@icon("res://x.svg")` sit there too. Four readers here
|
|
71
|
+
# matched on `class_name` at the start of a line and so saw none of those as a declaration at all,
|
|
72
|
+
# which cost a project with gdUnit4 in it 23 classes reported as declared nowhere, and put a
|
|
73
|
+
# `script_edit` insertion above the `class_name` of a script whose whole header is that one line,
|
|
74
|
+
# where it does not parse. The scanner that writes the class cache had its own copy of this and was
|
|
75
|
+
# right, which is why only the readings were wrong.
|
|
76
|
+
#
|
|
77
|
+
# One home, because five spellings of the same rule is five things to remember when the language
|
|
78
|
+
# adds the next annotation.
|
|
79
|
+
static func without_annotations(line: String) -> String:
|
|
80
|
+
var annotation: RegEx = compiled('^@[a-z_]+(?:\\(\\s*(?:"[^"]*")?[^)]*\\))?\\s*')
|
|
81
|
+
var rest: String = line.strip_edges()
|
|
82
|
+
var found: RegExMatch = annotation.search(rest)
|
|
83
|
+
while found != null:
|
|
84
|
+
rest = rest.substr(found.get_end()).strip_edges()
|
|
85
|
+
found = annotation.search(rest)
|
|
86
|
+
return rest
|
package/build/index.js
CHANGED
|
@@ -10028,14 +10028,14 @@ var SERVER_VERSION = (() => {
|
|
|
10028
10028
|
return "0.0.0";
|
|
10029
10029
|
}
|
|
10030
10030
|
})();
|
|
10031
|
-
var UNVERSIONED = "addon from before versions were reported";
|
|
10031
|
+
var UNVERSIONED = "an addon from before versions were reported";
|
|
10032
10032
|
function addonMismatch(addonVersion, serverVersion) {
|
|
10033
10033
|
if (addonVersion === serverVersion) {
|
|
10034
10034
|
return;
|
|
10035
10035
|
}
|
|
10036
10036
|
const reported = addonVersion ?? "";
|
|
10037
|
-
const editor = reported === "" ? UNVERSIONED : reported
|
|
10038
|
-
const both = `The editor is running
|
|
10037
|
+
const editor = reported === "" ? UNVERSIONED : `the ${reported} addon`;
|
|
10038
|
+
const both = `The editor is running ${editor} while this server ships ${serverVersion}.`;
|
|
10039
10039
|
if (reported !== "" && isNewer(reported, serverVersion)) {
|
|
10040
10040
|
return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
|
|
10041
10041
|
}
|
|
@@ -24781,6 +24781,7 @@ function withdrawBridge(path) {
|
|
|
24781
24781
|
// src/class-cache.ts
|
|
24782
24782
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
24783
24783
|
import { join as join3 } from "node:path";
|
|
24784
|
+
var DECLARATION = /^(?:@[a-z_]+(?:\([^)\n]*\))?\s+)*class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m;
|
|
24784
24785
|
function declaredClasses(projectPath) {
|
|
24785
24786
|
const declared = new Map;
|
|
24786
24787
|
const visit = (directory, prefix) => {
|
|
@@ -24795,7 +24796,7 @@ function declaredClasses(projectPath) {
|
|
|
24795
24796
|
if (entry.isDirectory()) {
|
|
24796
24797
|
visit(path, `${prefix}${entry.name}/`);
|
|
24797
24798
|
} else if (entry.isFile() && entry.name.endsWith(".gd")) {
|
|
24798
|
-
const found =
|
|
24799
|
+
const found = DECLARATION.exec(readFileSync4(path, "utf8"));
|
|
24799
24800
|
if (found?.[1]) {
|
|
24800
24801
|
declared.set(found[1], `res://${prefix}${entry.name}`);
|
|
24801
24802
|
}
|
|
@@ -24916,7 +24917,7 @@ function heldButGone(projectPath, editorHolds) {
|
|
|
24916
24917
|
}
|
|
24917
24918
|
function unseenByEditor(projectPath, editorHolds) {
|
|
24918
24919
|
const held = new Set(editorHolds);
|
|
24919
|
-
return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path }));
|
|
24920
|
+
return [...declaredClasses(projectPath)].filter(([name]) => !held.has(name)).map(([className, path]) => ({ className, path })).sort((one, other) => one.className < other.className ? -1 : one.className > other.className ? 1 : 0);
|
|
24920
24921
|
}
|
|
24921
24922
|
|
|
24922
24923
|
// src/config-pin.ts
|
|
@@ -25756,6 +25757,9 @@ var CONNECT_WINDOW_MS = 35000;
|
|
|
25756
25757
|
function mayYetConnect(listeningSince, now = Date.now()) {
|
|
25757
25758
|
return listeningSince === undefined || now - listeningSince.getTime() < CONNECT_WINDOW_MS;
|
|
25758
25759
|
}
|
|
25760
|
+
function theEditorHasComeBack(status, startedAt) {
|
|
25761
|
+
return status.connected && (status.connectedAt?.getTime() ?? 0) > startedAt && status.addonVersion !== undefined;
|
|
25762
|
+
}
|
|
25759
25763
|
function anEditorIsStillComing(listeningSince, launchedEditorIsAlive, now = Date.now()) {
|
|
25760
25764
|
return mayYetConnect(listeningSince, now) || launchedEditorIsAlive;
|
|
25761
25765
|
}
|
|
@@ -28671,7 +28675,7 @@ var TOOL_SPECS = [
|
|
|
28671
28675
|
direction: {
|
|
28672
28676
|
type: "string",
|
|
28673
28677
|
enum: ["forward", "reverse"],
|
|
28674
|
-
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. Default forward."
|
|
28678
|
+
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 two of them 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."
|
|
28675
28679
|
},
|
|
28676
28680
|
depth: { type: "number", description: "forward: how many levels to follow. Default unlimited." },
|
|
28677
28681
|
includeBuiltin: {
|
|
@@ -28941,7 +28945,7 @@ var TOOL_SPECS = [
|
|
|
28941
28945
|
scriptPath: SCRIPT_PATH,
|
|
28942
28946
|
includeInherited: {
|
|
28943
28947
|
type: "boolean",
|
|
28944
|
-
description: "structure:
|
|
28948
|
+
description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
|
|
28945
28949
|
},
|
|
28946
28950
|
line: { type: "number", description: "completion, hover: zero-based line." },
|
|
28947
28951
|
character: { type: "number", description: "completion, hover: zero-based column." }
|
|
@@ -29039,7 +29043,7 @@ var TOOL_SPECS = [
|
|
|
29039
29043
|
runtimeWaitMs: {
|
|
29040
29044
|
type: "number",
|
|
29041
29045
|
ops: ["start"],
|
|
29042
|
-
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000.
|
|
29046
|
+
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000. What it waits for is the first frame, so everything the game does before drawing one is inside it, including work the args just asked for: a flag that simulates six years of game time before anything is drawn makes the announcement that late, on a project that announces promptly without it. Raise it for such a run rather than reading runtime listening false as a fault. 0 does not wait at all, which is the one to pass for a scene that announces nothing by construction, such as a bench that prints and quits."
|
|
29043
29047
|
},
|
|
29044
29048
|
frames: {
|
|
29045
29049
|
type: "number",
|
|
@@ -29560,7 +29564,7 @@ function runtimeVerdict(endpoint, after) {
|
|
|
29560
29564
|
return {
|
|
29561
29565
|
listening: false,
|
|
29562
29566
|
mayYetAnnounce: after.running,
|
|
29563
|
-
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and the game is still running, 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` : `nothing announced itself within ${after.budgetMs}ms and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
29567
|
+
note: after.running ? `nothing announced itself within ${after.budgetMs}ms and the game is still running, 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 and the game is no longer running, so nothing is going to: editor_output has what it printed on the way down`,
|
|
29564
29568
|
heldAt: null
|
|
29565
29569
|
};
|
|
29566
29570
|
}
|
|
@@ -31096,10 +31100,7 @@ class GodotServer {
|
|
|
31096
31100
|
}
|
|
31097
31101
|
const startedAt = before.connectedAt?.getTime() ?? 0;
|
|
31098
31102
|
const began = Date.now();
|
|
31099
|
-
const back = await this.waitForBridge(() =>
|
|
31100
|
-
const status = this.godotBridge.getStatus();
|
|
31101
|
-
return status.connected && (status.connectedAt?.getTime() ?? 0) > startedAt;
|
|
31102
|
-
}, began + EDITOR_RESTART_TIMEOUT_MS);
|
|
31103
|
+
const back = await this.waitForBridge(() => theEditorHasComeBack(this.godotBridge.getStatus(), startedAt), began + EDITOR_RESTART_TIMEOUT_MS);
|
|
31103
31104
|
if (!back) {
|
|
31104
31105
|
return this.createErrorResponse(`The editor was asked to restart and has not come back within ${EDITOR_RESTART_TIMEOUT_MS / 1000}s.`, [
|
|
31105
31106
|
"It may be asking what to do about an unsaved scene: look at the editor window",
|
|
@@ -31294,7 +31295,7 @@ class GodotServer {
|
|
|
31294
31295
|
transcript: started.transcript,
|
|
31295
31296
|
refreshedClasses: refreshed.value,
|
|
31296
31297
|
endedPreviousRun: ended ?? undefined,
|
|
31297
|
-
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs),
|
|
31298
|
+
runtime: await this.runtimeUp(project.value.path, alreadyPlaying, runtimeWaitMs, given.value.length > 0),
|
|
31298
31299
|
message: `Use editor_output for what it prints and editor_run stop to end it.${spawnedInstead}${endedForThis}`
|
|
31299
31300
|
});
|
|
31300
31301
|
}
|
|
@@ -31303,9 +31304,9 @@ class GodotServer {
|
|
|
31303
31304
|
const editorSays = run?.throughEditor === true ? (await this.editorPlayingState())?.playing ?? null : null;
|
|
31304
31305
|
return runIsUp(run, editorSays);
|
|
31305
31306
|
}
|
|
31306
|
-
async runtimeUp(projectPath, before, budgetMs) {
|
|
31307
|
+
async runtimeUp(projectPath, before, budgetMs, withArgs) {
|
|
31307
31308
|
if (!existsSync9(join11(projectPath, RUNTIME_AUTOLOAD.path))) {
|
|
31308
|
-
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false });
|
|
31309
|
+
return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
|
|
31309
31310
|
}
|
|
31310
31311
|
const endpoint = await announcedSince(projectPath, before, {
|
|
31311
31312
|
budgetMs,
|
|
@@ -31315,7 +31316,8 @@ class GodotServer {
|
|
|
31315
31316
|
addon: true,
|
|
31316
31317
|
budgetMs,
|
|
31317
31318
|
heldAt: this.dapClient?.whereItStopped() ?? null,
|
|
31318
|
-
running: await this.gameIsUp()
|
|
31319
|
+
running: await this.gameIsUp(),
|
|
31320
|
+
withArgs
|
|
31319
31321
|
});
|
|
31320
31322
|
}
|
|
31321
31323
|
async playThroughEditor(scene, refreshedClasses, projectPath, alreadyPlaying, runtimeWaitMs, ended) {
|
|
@@ -31366,7 +31368,7 @@ class GodotServer {
|
|
|
31366
31368
|
refreshedClasses,
|
|
31367
31369
|
debugPort: readNumber(asParams(JSON.parse(answer.content[0]?.text ?? "{}")), "debugPort"),
|
|
31368
31370
|
endedPreviousRun: ended ?? undefined,
|
|
31369
|
-
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs),
|
|
31371
|
+
runtime: await this.runtimeUp(projectPath, alreadyPlaying, runtimeWaitMs, false),
|
|
31370
31372
|
message: "The editor is playing it, so its debugger holds it: the debug_* tools can reach it, " + "editor_output reads its console through the debug adapter, and editor_run stop ends it." + (ended === null ? "" : ` The run that was going, pid ${ended}, was ended to start this one; its output is no longer what editor_output answers about.`)
|
|
31371
31373
|
});
|
|
31372
31374
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.32",
|
|
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",
|