gdharness 0.6.8 → 0.7.0
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
CHANGED
|
@@ -15034,6 +15034,13 @@ function readParams(params, key) {
|
|
|
15034
15034
|
}
|
|
15035
15035
|
|
|
15036
15036
|
// src/tool-definitions.ts
|
|
15037
|
+
function opTakes(spec, op, name) {
|
|
15038
|
+
const ops = spec.parameters[name]?.ops;
|
|
15039
|
+
return ops === undefined || ops.includes(op);
|
|
15040
|
+
}
|
|
15041
|
+
function argumentsOf(spec, op) {
|
|
15042
|
+
return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
|
|
15043
|
+
}
|
|
15037
15044
|
function toolSpec(name) {
|
|
15038
15045
|
return SPECS_BY_NAME[name];
|
|
15039
15046
|
}
|
|
@@ -15059,7 +15066,8 @@ function buildToolDefinitions() {
|
|
|
15059
15066
|
};
|
|
15060
15067
|
}
|
|
15061
15068
|
for (const [name, schema] of Object.entries(spec.parameters)) {
|
|
15062
|
-
|
|
15069
|
+
const { ops: _ops, ...carried } = schema;
|
|
15070
|
+
properties[name] = carried;
|
|
15063
15071
|
}
|
|
15064
15072
|
const required = [...spec.requires];
|
|
15065
15073
|
if (spec.operations && !spec.defaultOperation) {
|
|
@@ -15380,7 +15388,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15380
15388
|
},
|
|
15381
15389
|
{
|
|
15382
15390
|
name: "project_test",
|
|
15383
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case:
|
|
15391
|
+
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. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
15384
15392
|
parameters: {
|
|
15385
15393
|
projectPath: PROJECT_PATH,
|
|
15386
15394
|
path: {
|
|
@@ -15749,35 +15757,46 @@ var init_tool_definitions = __esm(() => {
|
|
|
15749
15757
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15750
15758
|
nodePath: {
|
|
15751
15759
|
type: "string",
|
|
15760
|
+
ops: ["tree", "text", "find", "rect", "property"],
|
|
15752
15761
|
description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
|
|
15753
15762
|
},
|
|
15754
15763
|
property: {
|
|
15755
15764
|
type: "string",
|
|
15765
|
+
ops: ["property", "find"],
|
|
15756
15766
|
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."
|
|
15757
15767
|
},
|
|
15758
|
-
depth: { type: "number", description: "tree: levels to descend. Default 3." },
|
|
15768
|
+
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
15759
15769
|
includeProperties: {
|
|
15760
15770
|
type: "boolean",
|
|
15771
|
+
ops: ["tree"],
|
|
15761
15772
|
description: "tree: include each node's properties. Default false."
|
|
15762
15773
|
},
|
|
15763
15774
|
className: {
|
|
15764
15775
|
type: "string",
|
|
15776
|
+
ops: ["find"],
|
|
15765
15777
|
description: "find: a native class, matching its subclasses too, or a class_name."
|
|
15766
15778
|
},
|
|
15767
|
-
script: { type: "string", description: "find: the script file the node carries." },
|
|
15779
|
+
script: { type: "string", ops: ["find"], description: "find: the script file the node carries." },
|
|
15768
15780
|
namePattern: {
|
|
15769
15781
|
type: "string",
|
|
15782
|
+
ops: ["find"],
|
|
15770
15783
|
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
15771
15784
|
},
|
|
15772
|
-
group: { type: "string", description: "find: a group the node is in." },
|
|
15773
|
-
limit: {
|
|
15785
|
+
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
15786
|
+
limit: {
|
|
15787
|
+
type: "number",
|
|
15788
|
+
ops: ["find", "text"],
|
|
15789
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with truncated saying whether there were more."
|
|
15790
|
+
},
|
|
15774
15791
|
includeHidden: {
|
|
15775
15792
|
type: "boolean",
|
|
15793
|
+
ops: ["text"],
|
|
15776
15794
|
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
15777
15795
|
},
|
|
15778
15796
|
metrics: {
|
|
15779
15797
|
type: "array",
|
|
15780
15798
|
items: { type: "string" },
|
|
15799
|
+
ops: ["metrics"],
|
|
15781
15800
|
description: "metrics: which to read. Default all."
|
|
15782
15801
|
}
|
|
15783
15802
|
},
|
|
@@ -15810,10 +15829,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
15810
15829
|
parameters: {
|
|
15811
15830
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15812
15831
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
15813
|
-
property: { type: "string" },
|
|
15814
|
-
value: { description: "set: the value, fitted to the property's type." },
|
|
15815
|
-
method: { type: "string" },
|
|
15816
|
-
args: {
|
|
15832
|
+
property: { type: "string", ops: ["set"] },
|
|
15833
|
+
value: { ops: ["set"], description: "set: the value, fitted to the property's type." },
|
|
15834
|
+
method: { type: "string", ops: ["call"] },
|
|
15835
|
+
args: {
|
|
15836
|
+
type: "array",
|
|
15837
|
+
ops: ["call"],
|
|
15838
|
+
description: "call: the arguments, fitted to the method's parameter types."
|
|
15839
|
+
}
|
|
15817
15840
|
},
|
|
15818
15841
|
requires: ["nodePath"],
|
|
15819
15842
|
operations: {
|
|
@@ -15828,6 +15851,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15828
15851
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15829
15852
|
viewportPath: {
|
|
15830
15853
|
type: "string",
|
|
15854
|
+
ops: ["viewport"],
|
|
15831
15855
|
description: "viewport: the Viewport node. Default the root viewport."
|
|
15832
15856
|
},
|
|
15833
15857
|
width: { type: "number", description: "Scale the image to this width." },
|
|
@@ -15847,38 +15871,69 @@ var init_tool_definitions = __esm(() => {
|
|
|
15847
15871
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15848
15872
|
nodePath: {
|
|
15849
15873
|
type: "string",
|
|
15874
|
+
ops: ["click", "choose"],
|
|
15850
15875
|
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
15851
15876
|
},
|
|
15852
|
-
action: { type: "string", description: "action: the InputMap action name." },
|
|
15877
|
+
action: { type: "string", ops: ["action"], description: "action: the InputMap action name." },
|
|
15853
15878
|
pressed: {
|
|
15854
15879
|
type: "boolean",
|
|
15855
|
-
|
|
15880
|
+
ops: ["action", "key", "mouse_click"],
|
|
15881
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
15856
15882
|
},
|
|
15857
|
-
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
15883
|
+
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
15858
15884
|
keycode: {
|
|
15859
15885
|
type: ["string", "number"],
|
|
15886
|
+
ops: ["key"],
|
|
15860
15887
|
description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
|
|
15861
15888
|
},
|
|
15862
15889
|
text: {
|
|
15863
15890
|
type: "string",
|
|
15891
|
+
ops: ["text", "choose"],
|
|
15864
15892
|
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
15865
15893
|
},
|
|
15894
|
+
replace: {
|
|
15895
|
+
type: "boolean",
|
|
15896
|
+
ops: ["text"],
|
|
15897
|
+
description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
|
|
15898
|
+
},
|
|
15866
15899
|
index: {
|
|
15867
15900
|
type: "number",
|
|
15901
|
+
ops: ["choose"],
|
|
15868
15902
|
description: "choose: the item to take, by where it is in the list, when text will not do."
|
|
15869
15903
|
},
|
|
15870
|
-
shift: { type: "boolean" },
|
|
15871
|
-
ctrl: { type: "boolean" },
|
|
15872
|
-
alt: { type: "boolean" },
|
|
15873
|
-
x: {
|
|
15874
|
-
|
|
15904
|
+
shift: { type: "boolean", ops: ["key"] },
|
|
15905
|
+
ctrl: { type: "boolean", ops: ["key"] },
|
|
15906
|
+
alt: { type: "boolean", ops: ["key"] },
|
|
15907
|
+
x: {
|
|
15908
|
+
type: "number",
|
|
15909
|
+
ops: ["mouse_click", "mouse_motion"],
|
|
15910
|
+
description: "mouse_click, mouse_motion: window pixels."
|
|
15911
|
+
},
|
|
15912
|
+
y: {
|
|
15913
|
+
type: "number",
|
|
15914
|
+
ops: ["mouse_click", "mouse_motion"],
|
|
15915
|
+
description: "mouse_click, mouse_motion: window pixels."
|
|
15916
|
+
},
|
|
15875
15917
|
button: {
|
|
15876
15918
|
type: ["string", "number"],
|
|
15919
|
+
ops: ["click", "mouse_click"],
|
|
15877
15920
|
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
15878
15921
|
},
|
|
15879
|
-
doubleClick: {
|
|
15880
|
-
|
|
15881
|
-
|
|
15922
|
+
doubleClick: {
|
|
15923
|
+
type: "boolean",
|
|
15924
|
+
ops: ["click", "mouse_click"],
|
|
15925
|
+
description: "click, mouse_click: default false."
|
|
15926
|
+
},
|
|
15927
|
+
relativeX: {
|
|
15928
|
+
type: "number",
|
|
15929
|
+
ops: ["mouse_motion"],
|
|
15930
|
+
description: "mouse_motion: movement since the last event."
|
|
15931
|
+
},
|
|
15932
|
+
relativeY: {
|
|
15933
|
+
type: "number",
|
|
15934
|
+
ops: ["mouse_motion"],
|
|
15935
|
+
description: "mouse_motion: movement since the last event."
|
|
15936
|
+
}
|
|
15882
15937
|
},
|
|
15883
15938
|
requires: [],
|
|
15884
15939
|
operations: {
|
|
@@ -15907,14 +15962,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
15907
15962
|
projectPath: RUNNING_PROJECT_PATH,
|
|
15908
15963
|
frames: {
|
|
15909
15964
|
type: "number",
|
|
15965
|
+
ops: ["frames"],
|
|
15910
15966
|
description: "frames: how many to let pass, 1 to 600. More than that is refused."
|
|
15911
15967
|
},
|
|
15912
|
-
nodePath: { type: "string", description: "signal, until: the node." },
|
|
15913
|
-
signal: { type: "string", description: "signal: the signal name." },
|
|
15914
|
-
property: { type: "string", description: "until: the property name." },
|
|
15915
|
-
value: { description: "until: the value to wait for, fitted to the property's type." },
|
|
15968
|
+
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
15969
|
+
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
15970
|
+
property: { type: "string", ops: ["until"], description: "until: the property name." },
|
|
15971
|
+
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
15916
15972
|
timeoutMs: {
|
|
15917
15973
|
type: "number",
|
|
15974
|
+
ops: ["signal", "until"],
|
|
15918
15975
|
description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
|
|
15919
15976
|
}
|
|
15920
15977
|
},
|
|
@@ -15962,6 +16019,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
15962
16019
|
parameters: {
|
|
15963
16020
|
frameId: {
|
|
15964
16021
|
type: "number",
|
|
16022
|
+
ops: ["variables"],
|
|
15965
16023
|
description: "variables: which frame, from a stack answer. Default the innermost."
|
|
15966
16024
|
}
|
|
15967
16025
|
},
|
|
@@ -35923,6 +35981,17 @@ function realPathOr(path) {
|
|
|
35923
35981
|
return path;
|
|
35924
35982
|
}
|
|
35925
35983
|
}
|
|
35984
|
+
function aboveTheRunner(entry) {
|
|
35985
|
+
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
35986
|
+
if (runner < 0) {
|
|
35987
|
+
return entry;
|
|
35988
|
+
}
|
|
35989
|
+
const cut = entry.detail.length - runner;
|
|
35990
|
+
return {
|
|
35991
|
+
...entry,
|
|
35992
|
+
detail: [...entry.detail.slice(0, runner), `[and ${cut} frames inside ${RUNNER_DIRECTORY}]`]
|
|
35993
|
+
};
|
|
35994
|
+
}
|
|
35926
35995
|
function hasMainScene(projectFile) {
|
|
35927
35996
|
const scene = parseProjectGodot(readFileSync12(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
35928
35997
|
return typeof scene === "string" && scene !== "";
|
|
@@ -36265,6 +36334,15 @@ class GodotServer {
|
|
|
36265
36334
|
};
|
|
36266
36335
|
}
|
|
36267
36336
|
}
|
|
36337
|
+
if (op !== null) {
|
|
36338
|
+
const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
|
|
36339
|
+
if (elsewhere.length > 0) {
|
|
36340
|
+
return {
|
|
36341
|
+
ok: false,
|
|
36342
|
+
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
|
|
36343
|
+
};
|
|
36344
|
+
}
|
|
36345
|
+
}
|
|
36268
36346
|
const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
|
|
36269
36347
|
const missing = required.filter((field) => {
|
|
36270
36348
|
const value = Object.hasOwn(args, field) ? args[field] : undefined;
|
|
@@ -36389,7 +36467,8 @@ class GodotServer {
|
|
|
36389
36467
|
return await this.handleRuntimeCommand("read_text", {
|
|
36390
36468
|
projectPath: args["projectPath"],
|
|
36391
36469
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
36392
|
-
include_hidden: readBoolean(args, "includeHidden") ?? false
|
|
36470
|
+
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
36471
|
+
limit: readPositiveNumber(args, "limit") ?? 500
|
|
36393
36472
|
});
|
|
36394
36473
|
case "rect":
|
|
36395
36474
|
return await this.handleRuntimeCommand("get_rect", {
|
|
@@ -36815,7 +36894,7 @@ class GodotServer {
|
|
|
36815
36894
|
rmSync8(reportsDir, { recursive: true, force: true });
|
|
36816
36895
|
rmSync8(userData, { recursive: true, force: true });
|
|
36817
36896
|
}
|
|
36818
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
36897
|
+
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
36819
36898
|
const verdicts = {
|
|
36820
36899
|
0: "passed",
|
|
36821
36900
|
100: "failures",
|
|
@@ -36847,6 +36926,7 @@ class GodotServer {
|
|
|
36847
36926
|
};
|
|
36848
36927
|
}
|
|
36849
36928
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
36929
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0);
|
|
36850
36930
|
return this.jsonTextResponse({
|
|
36851
36931
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
36852
36932
|
verdict,
|
|
@@ -36857,7 +36937,7 @@ class GodotServer {
|
|
|
36857
36937
|
skipped: report.skipped,
|
|
36858
36938
|
time: report.time,
|
|
36859
36939
|
failed,
|
|
36860
|
-
suites:
|
|
36940
|
+
suites: unclean.map((suite) => ({
|
|
36861
36941
|
name: suite.name,
|
|
36862
36942
|
path: suite.path,
|
|
36863
36943
|
tests: suite.tests,
|
|
@@ -36866,6 +36946,7 @@ class GodotServer {
|
|
|
36866
36946
|
skipped: suite.skipped,
|
|
36867
36947
|
time: suite.time
|
|
36868
36948
|
})),
|
|
36949
|
+
suitesPassed: report.suites.length - unclean.length,
|
|
36869
36950
|
engineErrors: run.log.count("error"),
|
|
36870
36951
|
engineWarnings: run.log.count("warning"),
|
|
36871
36952
|
engineEntries,
|
|
@@ -37431,38 +37512,33 @@ class GodotServer {
|
|
|
37431
37512
|
});
|
|
37432
37513
|
}
|
|
37433
37514
|
async handleRuntimeWait(op, args) {
|
|
37515
|
+
if (op === "frames") {
|
|
37516
|
+
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
37517
|
+
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
37518
|
+
return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
|
|
37519
|
+
}
|
|
37434
37520
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
37435
37521
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
37436
37522
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
37437
|
-
|
|
37438
|
-
|
|
37439
|
-
|
|
37440
|
-
|
|
37441
|
-
|
|
37442
|
-
|
|
37443
|
-
|
|
37444
|
-
|
|
37445
|
-
|
|
37446
|
-
|
|
37447
|
-
|
|
37448
|
-
|
|
37449
|
-
}, patience);
|
|
37450
|
-
default:
|
|
37451
|
-
return await this.handleRuntimeCommand("wait_until", {
|
|
37452
|
-
projectPath: args["projectPath"],
|
|
37453
|
-
path: nodePath,
|
|
37454
|
-
property: readString(args, "property") ?? "",
|
|
37455
|
-
value: args["value"],
|
|
37456
|
-
timeout_ms: timeoutMs
|
|
37457
|
-
}, patience);
|
|
37458
|
-
}
|
|
37523
|
+
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
37524
|
+
projectPath: args["projectPath"],
|
|
37525
|
+
path: nodePath,
|
|
37526
|
+
signal: readString(args, "signal") ?? "",
|
|
37527
|
+
timeout_ms: timeoutMs
|
|
37528
|
+
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
37529
|
+
projectPath: args["projectPath"],
|
|
37530
|
+
path: nodePath,
|
|
37531
|
+
property: readString(args, "property") ?? "",
|
|
37532
|
+
value: args["value"],
|
|
37533
|
+
timeout_ms: timeoutMs
|
|
37534
|
+
}, patience);
|
|
37459
37535
|
}
|
|
37460
37536
|
}
|
|
37461
37537
|
async function runGodotServer() {
|
|
37462
37538
|
const server = new GodotServer;
|
|
37463
37539
|
await server.run();
|
|
37464
37540
|
}
|
|
37465
|
-
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS;
|
|
37541
|
+
var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run5, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, SLOWEST_FRAME_RATE = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, HEADLESS_OPERATIONS, PROJECT_INFO_SECTIONS, RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
37466
37542
|
var init_server3 = __esm(() => {
|
|
37467
37543
|
init_mcp();
|
|
37468
37544
|
init_stdio2();
|
|
@@ -38766,7 +38842,9 @@ function renderToolsMarkdown() {
|
|
|
38766
38842
|
const needs = [...tool.requires, ...spec.requires];
|
|
38767
38843
|
const isDefault = tool.defaultOperation === op ? " (default)" : "";
|
|
38768
38844
|
const wants = needs.length > 0 ? ` Needs: ${needs.map((name) => `\`${name}\``).join(", ")}.` : "";
|
|
38769
|
-
|
|
38845
|
+
const spare = argumentsOf(tool, op).filter((name) => !needs.includes(name) && name !== "projectPath");
|
|
38846
|
+
const takes = spare.length > 0 ? ` Takes: ${spare.map((name) => `\`${name}\``).join(", ")}.` : "";
|
|
38847
|
+
lines.push(`- \`op: ${op}\`${isDefault}: ${spec.summary}.${wants}${takes}`);
|
|
38770
38848
|
}
|
|
38771
38849
|
lines.push("");
|
|
38772
38850
|
} else if (tool.requires.length > 0) {
|
|
@@ -38830,7 +38908,7 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
|
|
|
38830
38908
|
| Where a control is, and whether it is visible | \`runtime_inspect\` \`find\`, \`rect\` |
|
|
38831
38909
|
| What a property reads right now | \`runtime_inspect\` \`property\`, \`runtime_invoke\` |
|
|
38832
38910
|
| Press a button | \`runtime_input click\`, which says what was under the pointer |
|
|
38833
|
-
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\`; a submitted field has to be clicked again |
|
|
38911
|
+
| Fill in a field | \`runtime_input click\` on it, then \`runtime_input text\` with \`replace\`; a submitted field has to be clicked again |
|
|
38834
38912
|
| Click somebody standing in a 3D room | \`runtime_input click\` on the Node3D, which aims at what it draws |
|
|
38835
38913
|
| Pick something out of a dropdown | \`runtime_input choose\` on it, by what the item says |
|
|
38836
38914
|
| Answer a dialog, or press a bound key | \`runtime_input action\` or \`key\`, a whole press unless you hold it |
|
|
@@ -155,6 +155,11 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
155
155
|
## Pushed into the viewport for the reason [method click] is, and it matters more here: the focus
|
|
156
156
|
## is what decides where a character lands, so a caller that clicked a field and then typed would
|
|
157
157
|
## otherwise have both waiting in the same queue with nothing said about the order.
|
|
158
|
+
##
|
|
159
|
+
## [code]replace[/code] is for a field that already says something, which is most of them: typing
|
|
160
|
+
## lands at the caret, so a spin box reading 2.1 typed "0.3" at reads 2.10.3 and parses back to
|
|
161
|
+
## 2.1, and the answer said four characters had gone in. What the field holds afterwards is read
|
|
162
|
+
## back now rather than echoed from the request, so a call that typed somewhere unhelpful says so.
|
|
158
163
|
func inject_text(params: Dictionary) -> Dictionary:
|
|
159
164
|
var text: String = String(params.get("text", ""))
|
|
160
165
|
if text.is_empty():
|
|
@@ -166,6 +171,7 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
166
171
|
if not shut.is_empty():
|
|
167
172
|
return {"type": "error", "message": shut}
|
|
168
173
|
|
|
174
|
+
var replaced: bool = bool(params.get("replace", false)) and _select_everything(focused)
|
|
169
175
|
for index: int in text.length():
|
|
170
176
|
var down: InputEventKey = _typed(text.unicode_at(index))
|
|
171
177
|
viewport.push_input(down)
|
|
@@ -186,9 +192,44 @@ func inject_text(params: Dictionary) -> Dictionary:
|
|
|
186
192
|
"text": text,
|
|
187
193
|
"characters": text.length(),
|
|
188
194
|
"into": into,
|
|
195
|
+
"replaced": replaced,
|
|
196
|
+
"holds": _what_it_holds(focused),
|
|
189
197
|
}
|
|
190
198
|
|
|
191
199
|
|
|
200
|
+
## Selects everything in [param focused] so the next character typed writes over it, and answers
|
|
201
|
+
## whether there was a field to select in.
|
|
202
|
+
##
|
|
203
|
+
## Through the control rather than through a Ctrl+A, which is the one part of filling a field that
|
|
204
|
+
## cannot honestly be sent as a key: the shortcut is Cmd+A on macOS and is an [InputMap] action a
|
|
205
|
+
## project is free to unbind, and a caller asking for the field to be replaced would then be typing
|
|
206
|
+
## on the end of what was there. The replacement itself is still every character a player types.
|
|
207
|
+
static func _select_everything(focused: Control) -> bool:
|
|
208
|
+
var field: LineEdit = focused as LineEdit
|
|
209
|
+
if field != null:
|
|
210
|
+
field.select_all()
|
|
211
|
+
return true
|
|
212
|
+
var box: TextEdit = focused as TextEdit
|
|
213
|
+
if box == null:
|
|
214
|
+
return false
|
|
215
|
+
box.select_all()
|
|
216
|
+
return true
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## What the field says now, or null where the focus is not a field at all.
|
|
220
|
+
##
|
|
221
|
+
## A spin box rewrites its own text out of the number it parsed, so this is the only thing that
|
|
222
|
+
## says whether what was typed became what the field means.
|
|
223
|
+
static func _what_it_holds(focused: Control) -> Variant:
|
|
224
|
+
var field: LineEdit = focused as LineEdit
|
|
225
|
+
if field != null:
|
|
226
|
+
return field.text
|
|
227
|
+
var box: TextEdit = focused as TextEdit
|
|
228
|
+
if box == null:
|
|
229
|
+
return null
|
|
230
|
+
return box.text
|
|
231
|
+
|
|
232
|
+
|
|
192
233
|
## Why nothing typed would reach [param focused], or "" when it would.
|
|
193
234
|
##
|
|
194
235
|
## Having the focus is not the same as being edited. Since Godot 4.4 a field is focused and shut
|
|
@@ -10,8 +10,8 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
const FIND_LIMIT: int = 100
|
|
11
11
|
const FIND_LIMIT_CEILING: int = 1000
|
|
12
12
|
|
|
13
|
-
## The most lines one read answers with. A screen is a few dozen; a
|
|
14
|
-
## pointed this at by mistake.
|
|
13
|
+
## The most lines one read answers with, unless asked for fewer. A screen is a few dozen; a
|
|
14
|
+
## thousand is a tree somebody pointed this at by mistake.
|
|
15
15
|
const READ_LIMIT: int = 500
|
|
16
16
|
|
|
17
17
|
var _host: Node
|
|
@@ -153,22 +153,32 @@ func _matches(
|
|
|
153
153
|
## A hidden node is left out and so is everything under it, because what a player reads is what is
|
|
154
154
|
## drawn. [param include_hidden] asks for the lot instead, which is what a caller checking that
|
|
155
155
|
## something is not showing wants.
|
|
156
|
+
##
|
|
157
|
+
## [param limit] is the first few lines rather than all of them, which is how the top of a screen
|
|
158
|
+
## is read without the hall under it: the bar along the top of a guild is eleven lines and the
|
|
159
|
+
## panel it sits on is two hundred.
|
|
156
160
|
func read_text(params: Dictionary) -> Dictionary:
|
|
157
161
|
var root_path: String = str(params.get("root", "/root"))
|
|
158
162
|
var include_hidden: bool = bool(params.get("include_hidden", false))
|
|
163
|
+
var limit: int = clampi(int(params.get("limit", READ_LIMIT)), 1, READ_LIMIT)
|
|
159
164
|
|
|
160
165
|
var root: Node = _host.get_tree().root.get_node_or_null(root_path)
|
|
161
166
|
if root == null:
|
|
162
167
|
return {"type": "error", "message": "Node not found: " + root_path}
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
# One line further than asked for, so that whether anything was left behind is read off the
|
|
170
|
+
# walk rather than guessed at from the count: a panel of exactly as many lines as the caller
|
|
171
|
+
# asked for is one they have read all of, and saying otherwise sends them back for nothing.
|
|
172
|
+
var read: PackedStringArray = PackedStringArray()
|
|
173
|
+
_read_into(root, include_hidden, limit + 1, read)
|
|
174
|
+
var more: bool = read.size() > limit
|
|
175
|
+
var lines: PackedStringArray = read.slice(0, limit) if more else read
|
|
166
176
|
return {
|
|
167
177
|
"type": "text",
|
|
168
178
|
"root": root_path,
|
|
169
179
|
"lines": lines,
|
|
170
180
|
"count": lines.size(),
|
|
171
|
-
"truncated":
|
|
181
|
+
"truncated": more,
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
|
|
@@ -180,8 +190,8 @@ func read_text(params: Dictionary) -> Dictionary:
|
|
|
180
190
|
## screen full of forms answered with every label on it and none of the values in it. The hidden
|
|
181
191
|
## check covers a [Window] for the same walk: a dropdown's popup is a child that is not drawn
|
|
182
192
|
## until it is opened, and reading a closed menu would put every item on the screen.
|
|
183
|
-
func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> void:
|
|
184
|
-
if into.size() >=
|
|
193
|
+
func _read_into(node: Node, include_hidden: bool, most: int, into: PackedStringArray) -> void:
|
|
194
|
+
if into.size() >= most:
|
|
185
195
|
return
|
|
186
196
|
if not include_hidden and not _drawn(node):
|
|
187
197
|
return
|
|
@@ -189,7 +199,7 @@ func _read_into(node: Node, include_hidden: bool, into: PackedStringArray) -> vo
|
|
|
189
199
|
if not said.is_empty():
|
|
190
200
|
into.append(said)
|
|
191
201
|
for child: Node in node.get_children(true):
|
|
192
|
-
_read_into(child, include_hidden, into)
|
|
202
|
+
_read_into(child, include_hidden, most, into)
|
|
193
203
|
|
|
194
204
|
|
|
195
205
|
## Whether [param node] is on the screen at all, for the two kinds of thing that can be hidden.
|
package/build/index.js
CHANGED
|
@@ -27633,6 +27633,13 @@ function installedAddonVersion(projectPath) {
|
|
|
27633
27633
|
}
|
|
27634
27634
|
|
|
27635
27635
|
// src/tool-definitions.ts
|
|
27636
|
+
function opTakes(spec, op, name) {
|
|
27637
|
+
const ops = spec.parameters[name]?.ops;
|
|
27638
|
+
return ops === undefined || ops.includes(op);
|
|
27639
|
+
}
|
|
27640
|
+
function argumentsOf(spec, op) {
|
|
27641
|
+
return Object.keys(spec.parameters).filter((name) => opTakes(spec, op, name));
|
|
27642
|
+
}
|
|
27636
27643
|
var PROJECT_PATH = {
|
|
27637
27644
|
type: "string",
|
|
27638
27645
|
description: "Absolute path to the project directory, the one holding project.godot."
|
|
@@ -27934,7 +27941,7 @@ var TOOL_SPECS = [
|
|
|
27934
27941
|
},
|
|
27935
27942
|
{
|
|
27936
27943
|
name: "project_test",
|
|
27937
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case:
|
|
27944
|
+
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. 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. Needs gdUnit4 under addons/gdUnit4.",
|
|
27938
27945
|
parameters: {
|
|
27939
27946
|
projectPath: PROJECT_PATH,
|
|
27940
27947
|
path: {
|
|
@@ -28303,35 +28310,46 @@ var TOOL_SPECS = [
|
|
|
28303
28310
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28304
28311
|
nodePath: {
|
|
28305
28312
|
type: "string",
|
|
28313
|
+
ops: ["tree", "text", "find", "rect", "property"],
|
|
28306
28314
|
description: "tree, find, text: where to start, default /root. rect: the node to place. property: the node to read."
|
|
28307
28315
|
},
|
|
28308
28316
|
property: {
|
|
28309
28317
|
type: "string",
|
|
28318
|
+
ops: ["property", "find"],
|
|
28310
28319
|
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."
|
|
28311
28320
|
},
|
|
28312
|
-
depth: { type: "number", description: "tree: levels to descend. Default 3." },
|
|
28321
|
+
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
28313
28322
|
includeProperties: {
|
|
28314
28323
|
type: "boolean",
|
|
28324
|
+
ops: ["tree"],
|
|
28315
28325
|
description: "tree: include each node's properties. Default false."
|
|
28316
28326
|
},
|
|
28317
28327
|
className: {
|
|
28318
28328
|
type: "string",
|
|
28329
|
+
ops: ["find"],
|
|
28319
28330
|
description: "find: a native class, matching its subclasses too, or a class_name."
|
|
28320
28331
|
},
|
|
28321
|
-
script: { type: "string", description: "find: the script file the node carries." },
|
|
28332
|
+
script: { type: "string", ops: ["find"], description: "find: the script file the node carries." },
|
|
28322
28333
|
namePattern: {
|
|
28323
28334
|
type: "string",
|
|
28335
|
+
ops: ["find"],
|
|
28324
28336
|
description: 'find: a case-insensitive glob on the node name, such as "Enemy*".'
|
|
28325
28337
|
},
|
|
28326
|
-
group: { type: "string", description: "find: a group the node is in." },
|
|
28327
|
-
limit: {
|
|
28338
|
+
group: { type: "string", ops: ["find"], description: "find: a group the node is in." },
|
|
28339
|
+
limit: {
|
|
28340
|
+
type: "number",
|
|
28341
|
+
ops: ["find", "text"],
|
|
28342
|
+
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with truncated saying whether there were more."
|
|
28343
|
+
},
|
|
28328
28344
|
includeHidden: {
|
|
28329
28345
|
type: "boolean",
|
|
28346
|
+
ops: ["text"],
|
|
28330
28347
|
description: "text: read hidden nodes as well, for checking that something is not showing. Default false."
|
|
28331
28348
|
},
|
|
28332
28349
|
metrics: {
|
|
28333
28350
|
type: "array",
|
|
28334
28351
|
items: { type: "string" },
|
|
28352
|
+
ops: ["metrics"],
|
|
28335
28353
|
description: "metrics: which to read. Default all."
|
|
28336
28354
|
}
|
|
28337
28355
|
},
|
|
@@ -28364,10 +28382,14 @@ var TOOL_SPECS = [
|
|
|
28364
28382
|
parameters: {
|
|
28365
28383
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28366
28384
|
nodePath: { type: "string", description: 'Absolute node path, such as "/root/Main/Player".' },
|
|
28367
|
-
property: { type: "string" },
|
|
28368
|
-
value: { description: "set: the value, fitted to the property's type." },
|
|
28369
|
-
method: { type: "string" },
|
|
28370
|
-
args: {
|
|
28385
|
+
property: { type: "string", ops: ["set"] },
|
|
28386
|
+
value: { ops: ["set"], description: "set: the value, fitted to the property's type." },
|
|
28387
|
+
method: { type: "string", ops: ["call"] },
|
|
28388
|
+
args: {
|
|
28389
|
+
type: "array",
|
|
28390
|
+
ops: ["call"],
|
|
28391
|
+
description: "call: the arguments, fitted to the method's parameter types."
|
|
28392
|
+
}
|
|
28371
28393
|
},
|
|
28372
28394
|
requires: ["nodePath"],
|
|
28373
28395
|
operations: {
|
|
@@ -28382,6 +28404,7 @@ var TOOL_SPECS = [
|
|
|
28382
28404
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28383
28405
|
viewportPath: {
|
|
28384
28406
|
type: "string",
|
|
28407
|
+
ops: ["viewport"],
|
|
28385
28408
|
description: "viewport: the Viewport node. Default the root viewport."
|
|
28386
28409
|
},
|
|
28387
28410
|
width: { type: "number", description: "Scale the image to this width." },
|
|
@@ -28401,38 +28424,69 @@ var TOOL_SPECS = [
|
|
|
28401
28424
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28402
28425
|
nodePath: {
|
|
28403
28426
|
type: "string",
|
|
28427
|
+
ops: ["click", "choose"],
|
|
28404
28428
|
description: "click: the Control to click, at its centre, or the 3D node to click, where it is drawn. choose: the PopupMenu, or the OptionButton or MenuButton in front of one."
|
|
28405
28429
|
},
|
|
28406
|
-
action: { type: "string", description: "action: the InputMap action name." },
|
|
28430
|
+
action: { type: "string", ops: ["action"], description: "action: the InputMap action name." },
|
|
28407
28431
|
pressed: {
|
|
28408
28432
|
type: "boolean",
|
|
28409
|
-
|
|
28433
|
+
ops: ["action", "key", "mouse_click"],
|
|
28434
|
+
description: "action, key: leave it out and the press is a whole one, down and up a frame apart. true holds it down, false lets go of one being held. mouse_click is one raw event, so it is down unless you say false."
|
|
28410
28435
|
},
|
|
28411
|
-
strength: { type: "number", description: "action: 0 to 1. Default 1." },
|
|
28436
|
+
strength: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
|
|
28412
28437
|
keycode: {
|
|
28413
28438
|
type: ["string", "number"],
|
|
28439
|
+
ops: ["key"],
|
|
28414
28440
|
description: 'key: the key name, such as "Space" or "A", or its Godot keycode.'
|
|
28415
28441
|
},
|
|
28416
28442
|
text: {
|
|
28417
28443
|
type: "string",
|
|
28444
|
+
ops: ["text", "choose"],
|
|
28418
28445
|
description: "text: what to type. A newline is Enter and a tab is Tab. choose: the item to take, by what it says."
|
|
28419
28446
|
},
|
|
28447
|
+
replace: {
|
|
28448
|
+
type: "boolean",
|
|
28449
|
+
ops: ["text"],
|
|
28450
|
+
description: 'text: true writes over what the field already says, which is what filling one in means. Default false types at the caret, so a field reading 2.1 typed "0.3" at reads 2.10.3. The answer says what the field holds afterwards either way.'
|
|
28451
|
+
},
|
|
28420
28452
|
index: {
|
|
28421
28453
|
type: "number",
|
|
28454
|
+
ops: ["choose"],
|
|
28422
28455
|
description: "choose: the item to take, by where it is in the list, when text will not do."
|
|
28423
28456
|
},
|
|
28424
|
-
shift: { type: "boolean" },
|
|
28425
|
-
ctrl: { type: "boolean" },
|
|
28426
|
-
alt: { type: "boolean" },
|
|
28427
|
-
x: {
|
|
28428
|
-
|
|
28457
|
+
shift: { type: "boolean", ops: ["key"] },
|
|
28458
|
+
ctrl: { type: "boolean", ops: ["key"] },
|
|
28459
|
+
alt: { type: "boolean", ops: ["key"] },
|
|
28460
|
+
x: {
|
|
28461
|
+
type: "number",
|
|
28462
|
+
ops: ["mouse_click", "mouse_motion"],
|
|
28463
|
+
description: "mouse_click, mouse_motion: window pixels."
|
|
28464
|
+
},
|
|
28465
|
+
y: {
|
|
28466
|
+
type: "number",
|
|
28467
|
+
ops: ["mouse_click", "mouse_motion"],
|
|
28468
|
+
description: "mouse_click, mouse_motion: window pixels."
|
|
28469
|
+
},
|
|
28429
28470
|
button: {
|
|
28430
28471
|
type: ["string", "number"],
|
|
28472
|
+
ops: ["click", "mouse_click"],
|
|
28431
28473
|
description: "click, mouse_click: left, right, middle, wheel_up or wheel_down, or a button number. Default left."
|
|
28432
28474
|
},
|
|
28433
|
-
doubleClick: {
|
|
28434
|
-
|
|
28435
|
-
|
|
28475
|
+
doubleClick: {
|
|
28476
|
+
type: "boolean",
|
|
28477
|
+
ops: ["click", "mouse_click"],
|
|
28478
|
+
description: "click, mouse_click: default false."
|
|
28479
|
+
},
|
|
28480
|
+
relativeX: {
|
|
28481
|
+
type: "number",
|
|
28482
|
+
ops: ["mouse_motion"],
|
|
28483
|
+
description: "mouse_motion: movement since the last event."
|
|
28484
|
+
},
|
|
28485
|
+
relativeY: {
|
|
28486
|
+
type: "number",
|
|
28487
|
+
ops: ["mouse_motion"],
|
|
28488
|
+
description: "mouse_motion: movement since the last event."
|
|
28489
|
+
}
|
|
28436
28490
|
},
|
|
28437
28491
|
requires: [],
|
|
28438
28492
|
operations: {
|
|
@@ -28461,14 +28515,16 @@ var TOOL_SPECS = [
|
|
|
28461
28515
|
projectPath: RUNNING_PROJECT_PATH,
|
|
28462
28516
|
frames: {
|
|
28463
28517
|
type: "number",
|
|
28518
|
+
ops: ["frames"],
|
|
28464
28519
|
description: "frames: how many to let pass, 1 to 600. More than that is refused."
|
|
28465
28520
|
},
|
|
28466
|
-
nodePath: { type: "string", description: "signal, until: the node." },
|
|
28467
|
-
signal: { type: "string", description: "signal: the signal name." },
|
|
28468
|
-
property: { type: "string", description: "until: the property name." },
|
|
28469
|
-
value: { description: "until: the value to wait for, fitted to the property's type." },
|
|
28521
|
+
nodePath: { type: "string", ops: ["signal", "until"], description: "signal, until: the node." },
|
|
28522
|
+
signal: { type: "string", ops: ["signal"], description: "signal: the signal name." },
|
|
28523
|
+
property: { type: "string", ops: ["until"], description: "until: the property name." },
|
|
28524
|
+
value: { ops: ["until"], description: "until: the value to wait for, fitted to the property's type." },
|
|
28470
28525
|
timeoutMs: {
|
|
28471
28526
|
type: "number",
|
|
28527
|
+
ops: ["signal", "until"],
|
|
28472
28528
|
description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
|
|
28473
28529
|
}
|
|
28474
28530
|
},
|
|
@@ -28516,6 +28572,7 @@ var TOOL_SPECS = [
|
|
|
28516
28572
|
parameters: {
|
|
28517
28573
|
frameId: {
|
|
28518
28574
|
type: "number",
|
|
28575
|
+
ops: ["variables"],
|
|
28519
28576
|
description: "variables: which frame, from a stack answer. Default the innermost."
|
|
28520
28577
|
}
|
|
28521
28578
|
},
|
|
@@ -28554,7 +28611,8 @@ function buildToolDefinitions() {
|
|
|
28554
28611
|
};
|
|
28555
28612
|
}
|
|
28556
28613
|
for (const [name, schema] of Object.entries(spec.parameters)) {
|
|
28557
|
-
|
|
28614
|
+
const { ops: _ops, ...carried } = schema;
|
|
28615
|
+
properties[name] = carried;
|
|
28558
28616
|
}
|
|
28559
28617
|
const required = [...spec.requires];
|
|
28560
28618
|
if (spec.operations && !spec.defaultOperation) {
|
|
@@ -28703,6 +28761,18 @@ function realPathOr(path) {
|
|
|
28703
28761
|
return path;
|
|
28704
28762
|
}
|
|
28705
28763
|
}
|
|
28764
|
+
var RUNNER_DIRECTORY = "addons/gdUnit4/";
|
|
28765
|
+
function aboveTheRunner(entry) {
|
|
28766
|
+
const runner = entry.detail.findIndex((line) => line.includes(RUNNER_DIRECTORY));
|
|
28767
|
+
if (runner < 0) {
|
|
28768
|
+
return entry;
|
|
28769
|
+
}
|
|
28770
|
+
const cut = entry.detail.length - runner;
|
|
28771
|
+
return {
|
|
28772
|
+
...entry,
|
|
28773
|
+
detail: [...entry.detail.slice(0, runner), `[and ${cut} frames inside ${RUNNER_DIRECTORY}]`]
|
|
28774
|
+
};
|
|
28775
|
+
}
|
|
28706
28776
|
function hasMainScene(projectFile) {
|
|
28707
28777
|
const scene = parseProjectGodot(readFileSync8(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
28708
28778
|
return typeof scene === "string" && scene !== "";
|
|
@@ -29045,6 +29115,15 @@ class GodotServer {
|
|
|
29045
29115
|
};
|
|
29046
29116
|
}
|
|
29047
29117
|
}
|
|
29118
|
+
if (op !== null) {
|
|
29119
|
+
const elsewhere = Object.keys(args).filter((key) => key !== "op" && !opTakes(spec, op, key));
|
|
29120
|
+
if (elsewhere.length > 0) {
|
|
29121
|
+
return {
|
|
29122
|
+
ok: false,
|
|
29123
|
+
response: this.createErrorResponse(`${spec.name} ${op} does not take ${elsewhere.join(", ")}. ${op} takes: ${argumentsOf(spec, op).join(", ")}.`)
|
|
29124
|
+
};
|
|
29125
|
+
}
|
|
29126
|
+
}
|
|
29048
29127
|
const required = [...spec.requires, ...op !== null ? spec.operations?.[op]?.requires ?? [] : []];
|
|
29049
29128
|
const missing = required.filter((field) => {
|
|
29050
29129
|
const value = Object.hasOwn(args, field) ? args[field] : undefined;
|
|
@@ -29169,7 +29248,8 @@ class GodotServer {
|
|
|
29169
29248
|
return await this.handleRuntimeCommand("read_text", {
|
|
29170
29249
|
projectPath: args["projectPath"],
|
|
29171
29250
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
29172
|
-
include_hidden: readBoolean(args, "includeHidden") ?? false
|
|
29251
|
+
include_hidden: readBoolean(args, "includeHidden") ?? false,
|
|
29252
|
+
limit: readPositiveNumber(args, "limit") ?? 500
|
|
29173
29253
|
});
|
|
29174
29254
|
case "rect":
|
|
29175
29255
|
return await this.handleRuntimeCommand("get_rect", {
|
|
@@ -29595,7 +29675,7 @@ class GodotServer {
|
|
|
29595
29675
|
rmSync4(reportsDir, { recursive: true, force: true });
|
|
29596
29676
|
rmSync4(userData, { recursive: true, force: true });
|
|
29597
29677
|
}
|
|
29598
|
-
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
|
|
29678
|
+
const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
|
|
29599
29679
|
const verdicts = {
|
|
29600
29680
|
0: "passed",
|
|
29601
29681
|
100: "failures",
|
|
@@ -29627,6 +29707,7 @@ class GodotServer {
|
|
|
29627
29707
|
};
|
|
29628
29708
|
}
|
|
29629
29709
|
const failed = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ ...entry, path: suite.path })));
|
|
29710
|
+
const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0);
|
|
29630
29711
|
return this.jsonTextResponse({
|
|
29631
29712
|
passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
|
|
29632
29713
|
verdict,
|
|
@@ -29637,7 +29718,7 @@ class GodotServer {
|
|
|
29637
29718
|
skipped: report.skipped,
|
|
29638
29719
|
time: report.time,
|
|
29639
29720
|
failed,
|
|
29640
|
-
suites:
|
|
29721
|
+
suites: unclean.map((suite) => ({
|
|
29641
29722
|
name: suite.name,
|
|
29642
29723
|
path: suite.path,
|
|
29643
29724
|
tests: suite.tests,
|
|
@@ -29646,6 +29727,7 @@ class GodotServer {
|
|
|
29646
29727
|
skipped: suite.skipped,
|
|
29647
29728
|
time: suite.time
|
|
29648
29729
|
})),
|
|
29730
|
+
suitesPassed: report.suites.length - unclean.length,
|
|
29649
29731
|
engineErrors: run.log.count("error"),
|
|
29650
29732
|
engineWarnings: run.log.count("warning"),
|
|
29651
29733
|
engineEntries,
|
|
@@ -30211,31 +30293,26 @@ class GodotServer {
|
|
|
30211
30293
|
});
|
|
30212
30294
|
}
|
|
30213
30295
|
async handleRuntimeWait(op, args) {
|
|
30296
|
+
if (op === "frames") {
|
|
30297
|
+
const frames = readPositiveNumber(args, "frames") ?? 1;
|
|
30298
|
+
const waited = patienceForFrames(frames, this.runtimeTimeoutMs());
|
|
30299
|
+
return await this.handleRuntimeCommand("wait_frames", { projectPath: args["projectPath"], frames }, waited);
|
|
30300
|
+
}
|
|
30214
30301
|
const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 5000;
|
|
30215
30302
|
const nodePath = readNonEmptyString(args, "nodePath") ?? "";
|
|
30216
30303
|
const patience = Math.max(this.runtimeTimeoutMs(), timeoutMs + 5000);
|
|
30217
|
-
|
|
30218
|
-
|
|
30219
|
-
|
|
30220
|
-
|
|
30221
|
-
|
|
30222
|
-
|
|
30223
|
-
|
|
30224
|
-
|
|
30225
|
-
|
|
30226
|
-
|
|
30227
|
-
|
|
30228
|
-
|
|
30229
|
-
}, patience);
|
|
30230
|
-
default:
|
|
30231
|
-
return await this.handleRuntimeCommand("wait_until", {
|
|
30232
|
-
projectPath: args["projectPath"],
|
|
30233
|
-
path: nodePath,
|
|
30234
|
-
property: readString(args, "property") ?? "",
|
|
30235
|
-
value: args["value"],
|
|
30236
|
-
timeout_ms: timeoutMs
|
|
30237
|
-
}, patience);
|
|
30238
|
-
}
|
|
30304
|
+
return op === "signal" ? await this.handleRuntimeCommand("wait_signal", {
|
|
30305
|
+
projectPath: args["projectPath"],
|
|
30306
|
+
path: nodePath,
|
|
30307
|
+
signal: readString(args, "signal") ?? "",
|
|
30308
|
+
timeout_ms: timeoutMs
|
|
30309
|
+
}, patience) : await this.handleRuntimeCommand("wait_until", {
|
|
30310
|
+
projectPath: args["projectPath"],
|
|
30311
|
+
path: nodePath,
|
|
30312
|
+
property: readString(args, "property") ?? "",
|
|
30313
|
+
value: args["value"],
|
|
30314
|
+
timeout_ms: timeoutMs
|
|
30315
|
+
}, patience);
|
|
30239
30316
|
}
|
|
30240
30317
|
}
|
|
30241
30318
|
async function runGodotServer() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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",
|