gdharness 1.0.25 → 1.0.27
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
|
@@ -15897,7 +15897,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
15897
15897
|
scenePath: SCENE_PATH,
|
|
15898
15898
|
actionName: { type: "string", description: 'Input action name, such as "jump".' },
|
|
15899
15899
|
events: INPUT_EVENTS,
|
|
15900
|
-
deadzone: {
|
|
15900
|
+
deadzone: {
|
|
15901
|
+
type: "number",
|
|
15902
|
+
minimum: 0,
|
|
15903
|
+
maximum: 1,
|
|
15904
|
+
description: "Input actions: analogue deadzone, 0 to 1. Default 0.5."
|
|
15905
|
+
},
|
|
15901
15906
|
pluginName: { type: "string", description: "Folder name under addons/." },
|
|
15902
15907
|
busName: { type: "string", description: "Audio bus name." },
|
|
15903
15908
|
parentBusIndex: { type: "number", description: "Audio buses: the bus to send to. Default 0, Master." },
|
|
@@ -16042,7 +16047,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16042
16047
|
description: "Stop each suite at its first failing case. Default false: every case runs. With it on, the counts are of what ran, notRun says how many cases were left, and fixing what is named and running again finds the next one, which looks like a flaky tier and is not."
|
|
16043
16048
|
},
|
|
16044
16049
|
timeoutMs: {
|
|
16045
|
-
type: "
|
|
16050
|
+
type: "integer",
|
|
16051
|
+
minimum: 1,
|
|
16046
16052
|
description: "How long the run may take before it is killed. Default 600000."
|
|
16047
16053
|
}
|
|
16048
16054
|
},
|
|
@@ -16225,8 +16231,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16225
16231
|
type: "boolean",
|
|
16226
16232
|
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."
|
|
16227
16233
|
},
|
|
16228
|
-
line: { type: "
|
|
16229
|
-
character: { type: "
|
|
16234
|
+
line: { type: "integer", minimum: 0, description: "completion, hover: zero-based line." },
|
|
16235
|
+
character: { type: "integer", minimum: 0, description: "completion, hover: zero-based column." }
|
|
16230
16236
|
},
|
|
16231
16237
|
requires: ["projectPath", "scriptPath"],
|
|
16232
16238
|
operations: {
|
|
@@ -16329,17 +16335,20 @@ var init_tool_definitions = __esm(() => {
|
|
|
16329
16335
|
description: "Environment variables for this run alone, as a map of strings, over the server's own environment. Like savesIn, a run carrying any is started by this server rather than played by the editor. Names beginning with GDHARNESS_ are refused: they are this server's contract with the addon in the game, and the runtime directory is set here whatever the rest of the environment says, so a game that moves TEMP still announces where this server looks."
|
|
16330
16336
|
},
|
|
16331
16337
|
runtimeWaitMs: {
|
|
16332
|
-
type: "
|
|
16338
|
+
type: "integer",
|
|
16339
|
+
minimum: 0,
|
|
16333
16340
|
ops: ["start"],
|
|
16334
16341
|
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. 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."
|
|
16335
16342
|
},
|
|
16336
16343
|
frames: {
|
|
16337
|
-
type: "
|
|
16344
|
+
type: "integer",
|
|
16345
|
+
minimum: 1,
|
|
16338
16346
|
ops: ["check"],
|
|
16339
16347
|
description: "check: frames to run before quitting. Default 3."
|
|
16340
16348
|
},
|
|
16341
16349
|
timeoutMs: {
|
|
16342
|
-
type: "
|
|
16350
|
+
type: "integer",
|
|
16351
|
+
minimum: 1,
|
|
16343
16352
|
ops: ["check", "wait"],
|
|
16344
16353
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
16345
16354
|
},
|
|
@@ -16391,12 +16400,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
16391
16400
|
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
16392
16401
|
},
|
|
16393
16402
|
limit: {
|
|
16394
|
-
type: "
|
|
16403
|
+
type: "integer",
|
|
16404
|
+
minimum: 1,
|
|
16395
16405
|
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
16396
16406
|
},
|
|
16397
16407
|
before: {
|
|
16398
16408
|
ops: ["editor"],
|
|
16399
|
-
type: "
|
|
16409
|
+
type: "integer",
|
|
16410
|
+
minimum: 0,
|
|
16400
16411
|
description: "How many lines above a repeated group to carry with it, counting only lines that are not themselves part of a group. Default 3. The line that explains a wall of identical errors is the last ordinary line above it, so this walks past the wall rather than a fixed distance back."
|
|
16401
16412
|
}
|
|
16402
16413
|
},
|
|
@@ -16424,7 +16435,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16424
16435
|
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
16425
16436
|
parameters: {
|
|
16426
16437
|
projectPath: PROJECT_PATH,
|
|
16427
|
-
timeoutMs: {
|
|
16438
|
+
timeoutMs: {
|
|
16439
|
+
type: "integer",
|
|
16440
|
+
minimum: 1,
|
|
16441
|
+
description: "How long to wait for the scan. Default 30000."
|
|
16442
|
+
},
|
|
16428
16443
|
reloadScript: {
|
|
16429
16444
|
type: "string",
|
|
16430
16445
|
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
@@ -16489,9 +16504,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
16489
16504
|
property: {
|
|
16490
16505
|
type: "string",
|
|
16491
16506
|
ops: ["property", "find"],
|
|
16492
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
16507
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
16508
|
+
},
|
|
16509
|
+
depth: {
|
|
16510
|
+
type: "integer",
|
|
16511
|
+
minimum: 0,
|
|
16512
|
+
ops: ["tree"],
|
|
16513
|
+
description: "tree: levels to descend, 0 for the node alone. Default 3."
|
|
16493
16514
|
},
|
|
16494
|
-
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
16495
16515
|
includeProperties: {
|
|
16496
16516
|
type: "boolean",
|
|
16497
16517
|
ops: ["tree"],
|
|
@@ -16521,7 +16541,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16521
16541
|
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
16522
16542
|
},
|
|
16523
16543
|
limit: {
|
|
16524
|
-
type: "
|
|
16544
|
+
type: "integer",
|
|
16545
|
+
minimum: 1,
|
|
16525
16546
|
ops: ["find", "text"],
|
|
16526
16547
|
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
16527
16548
|
},
|
|
@@ -16537,7 +16558,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
16537
16558
|
description: "metrics: which to read. Default all."
|
|
16538
16559
|
},
|
|
16539
16560
|
timeoutMs: {
|
|
16540
|
-
type: "
|
|
16561
|
+
type: "integer",
|
|
16562
|
+
minimum: 1,
|
|
16541
16563
|
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
16542
16564
|
}
|
|
16543
16565
|
},
|
|
@@ -16596,12 +16618,14 @@ var init_tool_definitions = __esm(() => {
|
|
|
16596
16618
|
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
16597
16619
|
},
|
|
16598
16620
|
timeoutMs: {
|
|
16599
|
-
type: "
|
|
16621
|
+
type: "integer",
|
|
16622
|
+
minimum: 1,
|
|
16600
16623
|
ops: ["set", "call"],
|
|
16601
16624
|
description: "set, call: how long to wait for the answer before answering pending with a requestId, which does not stop the call. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS. Give a longer one for a call known to take a while, such as one that plays many turns in one go."
|
|
16602
16625
|
},
|
|
16603
16626
|
requestId: {
|
|
16604
|
-
type: "
|
|
16627
|
+
type: "integer",
|
|
16628
|
+
minimum: 1,
|
|
16605
16629
|
ops: ["result"],
|
|
16606
16630
|
description: "result: the requestId a pending answer gave."
|
|
16607
16631
|
}
|
|
@@ -16633,8 +16657,16 @@ var init_tool_definitions = __esm(() => {
|
|
|
16633
16657
|
ops: ["viewport"],
|
|
16634
16658
|
description: "viewport: the Viewport node. Default the root viewport."
|
|
16635
16659
|
},
|
|
16636
|
-
width: {
|
|
16637
|
-
|
|
16660
|
+
width: {
|
|
16661
|
+
type: "integer",
|
|
16662
|
+
minimum: 1,
|
|
16663
|
+
description: "Scale the image to this width. Given alone, the height keeps the picture's proportions."
|
|
16664
|
+
},
|
|
16665
|
+
height: {
|
|
16666
|
+
type: "integer",
|
|
16667
|
+
minimum: 1,
|
|
16668
|
+
description: "Scale the image to this height. Given alone, the width keeps the picture's proportions."
|
|
16669
|
+
},
|
|
16638
16670
|
outputPath: {
|
|
16639
16671
|
type: "string",
|
|
16640
16672
|
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
@@ -16668,7 +16700,13 @@ var init_tool_definitions = __esm(() => {
|
|
|
16668
16700
|
ops: ["action", "key", "mouse_click"],
|
|
16669
16701
|
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; a wheel button down is a whole step, since a wheel is never held."
|
|
16670
16702
|
},
|
|
16671
|
-
strength: {
|
|
16703
|
+
strength: {
|
|
16704
|
+
type: "number",
|
|
16705
|
+
minimum: 0,
|
|
16706
|
+
maximum: 1,
|
|
16707
|
+
ops: ["action"],
|
|
16708
|
+
description: "action: 0 to 1. Default 1."
|
|
16709
|
+
},
|
|
16672
16710
|
keycode: {
|
|
16673
16711
|
type: ["string", "number"],
|
|
16674
16712
|
ops: ["key"],
|
|
@@ -16760,7 +16798,9 @@ var init_tool_definitions = __esm(() => {
|
|
|
16760
16798
|
projectPath: RUNNING_PROJECT_PATH,
|
|
16761
16799
|
pid: RUNNING_PID,
|
|
16762
16800
|
frames: {
|
|
16763
|
-
type: "
|
|
16801
|
+
type: "integer",
|
|
16802
|
+
minimum: 1,
|
|
16803
|
+
maximum: 600,
|
|
16764
16804
|
ops: ["frames"],
|
|
16765
16805
|
description: "frames: how many to let pass, 1 to 600. More than that is refused."
|
|
16766
16806
|
},
|
|
@@ -16787,7 +16827,9 @@ var init_tool_definitions = __esm(() => {
|
|
|
16787
16827
|
description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
|
|
16788
16828
|
},
|
|
16789
16829
|
timeoutMs: {
|
|
16790
|
-
type: "
|
|
16830
|
+
type: "integer",
|
|
16831
|
+
minimum: 1,
|
|
16832
|
+
maximum: 120000,
|
|
16791
16833
|
ops: ["signal", "until"],
|
|
16792
16834
|
description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
|
|
16793
16835
|
}
|
|
@@ -16811,7 +16853,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16811
16853
|
parameters: {
|
|
16812
16854
|
projectPath: PROJECT_PATH,
|
|
16813
16855
|
scriptPath: SCRIPT_PATH,
|
|
16814
|
-
line: { type: "
|
|
16856
|
+
line: { type: "integer", minimum: 1, description: "One-based line." }
|
|
16815
16857
|
},
|
|
16816
16858
|
requires: ["projectPath", "scriptPath", "line"],
|
|
16817
16859
|
operations: {
|
|
@@ -38455,6 +38497,9 @@ function describeValue(value) {
|
|
|
38455
38497
|
if (Array.isArray(value)) {
|
|
38456
38498
|
return "a list";
|
|
38457
38499
|
}
|
|
38500
|
+
if (typeof value === "number") {
|
|
38501
|
+
return String(value);
|
|
38502
|
+
}
|
|
38458
38503
|
return VALUE_NAMES[typeof value] ?? typeof value;
|
|
38459
38504
|
}
|
|
38460
38505
|
function wrongTypes(spec, args) {
|
|
@@ -38496,6 +38541,22 @@ function isPlainObject3(value) {
|
|
|
38496
38541
|
function outsideTheirLists(spec, args) {
|
|
38497
38542
|
return Object.entries(args).flatMap(([name, value]) => unlistedValues(spec.parameters[name], value, name));
|
|
38498
38543
|
}
|
|
38544
|
+
function outsideTheirRanges(spec, args) {
|
|
38545
|
+
const complaints = [];
|
|
38546
|
+
for (const [name, value] of Object.entries(args)) {
|
|
38547
|
+
const schema = spec.parameters[name];
|
|
38548
|
+
if (typeof value !== "number" || schema === undefined) {
|
|
38549
|
+
continue;
|
|
38550
|
+
}
|
|
38551
|
+
const least = typeof schema["minimum"] === "number" ? schema["minimum"] : undefined;
|
|
38552
|
+
const most = typeof schema["maximum"] === "number" ? schema["maximum"] : undefined;
|
|
38553
|
+
if (least !== undefined && value < least || most !== undefined && value > most) {
|
|
38554
|
+
const range = least !== undefined && most !== undefined ? `from ${least} to ${most}` : least !== undefined ? `of ${least} or more` : `of ${most} or less`;
|
|
38555
|
+
complaints.push(`${name} ${range}, not ${value}`);
|
|
38556
|
+
}
|
|
38557
|
+
}
|
|
38558
|
+
return complaints;
|
|
38559
|
+
}
|
|
38499
38560
|
function escapingPropertyValue(properties) {
|
|
38500
38561
|
if (typeof properties !== "object" || properties === null) {
|
|
38501
38562
|
return;
|
|
@@ -38962,7 +39023,11 @@ class GodotServer {
|
|
|
38962
39023
|
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
38963
39024
|
};
|
|
38964
39025
|
}
|
|
38965
|
-
const wrong = [
|
|
39026
|
+
const wrong = [
|
|
39027
|
+
...wrongTypes(spec, args),
|
|
39028
|
+
...outsideTheirLists(spec, args),
|
|
39029
|
+
...outsideTheirRanges(spec, args)
|
|
39030
|
+
];
|
|
38966
39031
|
if (wrong.length > 0) {
|
|
38967
39032
|
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
38968
39033
|
}
|
|
@@ -39148,14 +39213,16 @@ class GodotServer {
|
|
|
39148
39213
|
case "runtime_inspect": {
|
|
39149
39214
|
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
39150
39215
|
switch (op) {
|
|
39151
|
-
case "tree":
|
|
39216
|
+
case "tree": {
|
|
39217
|
+
const depth = readNonNegativeNumber(args, "depth");
|
|
39152
39218
|
return await this.handleRuntimeCommand("get_tree", {
|
|
39153
39219
|
...whichGame(args),
|
|
39154
39220
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
39155
|
-
depth:
|
|
39221
|
+
depth: depth ?? 3,
|
|
39156
39222
|
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
39157
39223
|
properties: readArray(args, "properties") ?? []
|
|
39158
39224
|
}, patience);
|
|
39225
|
+
}
|
|
39159
39226
|
case "find":
|
|
39160
39227
|
return await this.handleFindRuntimeNodes(args, patience);
|
|
39161
39228
|
case "text":
|
|
@@ -41159,7 +41226,7 @@ class GodotServer {
|
|
|
41159
41226
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
41160
41227
|
});
|
|
41161
41228
|
const everything = console2.log.everything();
|
|
41162
|
-
const grouped = bursts(everything,
|
|
41229
|
+
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3);
|
|
41163
41230
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
41164
41231
|
return this.jsonTextResponse({
|
|
41165
41232
|
editorPid: editorPid ?? undefined,
|
|
@@ -43640,6 +43707,12 @@ up anyway, believe the run over the diagnostic and say so.
|
|
|
43640
43707
|
An unknown op, an argument the tool does not name, or a missing required one is refused with the
|
|
43641
43708
|
valid set spelled out, so a call that succeeded is a call that was understood.
|
|
43642
43709
|
|
|
43710
|
+
The running server decides what a tool takes, not the schema in your context. A conversation that
|
|
43711
|
+
began before gdharness was upgraded, and was resumed since, can go on holding the tool definitions
|
|
43712
|
+
it first loaded, even from a fresh search, while the server behind them is newer. When an answer
|
|
43713
|
+
names an op or an argument the schema you hold does not show, make the call as the answer says: the
|
|
43714
|
+
server accepts it or refuses it with what it does take.
|
|
43715
|
+
|
|
43643
43716
|
A tool that cannot answer says which state it is in and what changes it, rather than answering
|
|
43644
43717
|
emptily: no game running, a game with no debugger behind it, a game running rather than stopped, a
|
|
43645
43718
|
control outside a headless viewport. Read the refusal instead of retrying the call.
|
|
@@ -33,6 +33,19 @@ func capture_viewport(params: Dictionary) -> Dictionary:
|
|
|
33
33
|
|
|
34
34
|
## The server names the file, so a game cannot point it at a path of its own choosing; a call
|
|
35
35
|
## with no path is a call the server did not make.
|
|
36
|
+
## The size a capture drawn at [param drawn] is scaled to, from the width and height asked for, 0
|
|
37
|
+
## being not asked. One side alone keeps the picture's proportions, since that is what asking for a
|
|
38
|
+
## smaller picture means: it was ignored unless both came, so the full-size picture was sent back.
|
|
39
|
+
static func scaled_to(drawn: Vector2i, width: int, height: int) -> Vector2i:
|
|
40
|
+
if width > 0 and height > 0:
|
|
41
|
+
return Vector2i(width, height)
|
|
42
|
+
if width > 0 and drawn.x > 0:
|
|
43
|
+
return Vector2i(width, maxi(1, roundi(float(drawn.y) * width / drawn.x)))
|
|
44
|
+
if height > 0 and drawn.y > 0:
|
|
45
|
+
return Vector2i(maxi(1, roundi(float(drawn.x) * height / drawn.y)), height)
|
|
46
|
+
return drawn
|
|
47
|
+
|
|
48
|
+
|
|
36
49
|
func _capture(viewport: Viewport, params: Dictionary) -> Dictionary:
|
|
37
50
|
var requested_path: String = str(params.get("output_path", ""))
|
|
38
51
|
if requested_path.is_empty():
|
|
@@ -61,10 +74,12 @@ func _capture(viewport: Viewport, params: Dictionary) -> Dictionary:
|
|
|
61
74
|
if image == null:
|
|
62
75
|
return {"type": "error", "message": "Failed to capture viewport image"}
|
|
63
76
|
|
|
64
|
-
var
|
|
65
|
-
var
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
var drawn: Vector2i = Vector2i(image.get_width(), image.get_height())
|
|
78
|
+
var target: Vector2i = scaled_to(
|
|
79
|
+
drawn, Read.as_int(params.get("width", 0)), Read.as_int(params.get("height", 0))
|
|
80
|
+
)
|
|
81
|
+
if target != drawn:
|
|
82
|
+
image.resize(target.x, target.y)
|
|
68
83
|
|
|
69
84
|
var screenshot_path: String = requested_path
|
|
70
85
|
if screenshot_path.begins_with("user://") or screenshot_path.begins_with("res://"):
|
|
@@ -48,6 +48,22 @@ const MAP_CALLS: Array[String] = [
|
|
|
48
48
|
"get_typed_value_class_name",
|
|
49
49
|
"get_typed_value_script",
|
|
50
50
|
]
|
|
51
|
+
## The calls a path makes on a packed list, which has fewer than a list: measured on 4.7.2, it has no
|
|
52
|
+
## front(), back(), max(), min() or hash().
|
|
53
|
+
const PACKED_CALLS: Array[String] = ["size", "is_empty", "duplicate", "to_byte_array"]
|
|
54
|
+
## The packed lists, which a game keeps names and points in as often as it keeps them in an Array.
|
|
55
|
+
const PACKED: Array[int] = [
|
|
56
|
+
TYPE_PACKED_BYTE_ARRAY,
|
|
57
|
+
TYPE_PACKED_INT32_ARRAY,
|
|
58
|
+
TYPE_PACKED_INT64_ARRAY,
|
|
59
|
+
TYPE_PACKED_FLOAT32_ARRAY,
|
|
60
|
+
TYPE_PACKED_FLOAT64_ARRAY,
|
|
61
|
+
TYPE_PACKED_STRING_ARRAY,
|
|
62
|
+
TYPE_PACKED_VECTOR2_ARRAY,
|
|
63
|
+
TYPE_PACKED_VECTOR3_ARRAY,
|
|
64
|
+
TYPE_PACKED_COLOR_ARRAY,
|
|
65
|
+
TYPE_PACKED_VECTOR4_ARRAY,
|
|
66
|
+
]
|
|
51
67
|
## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
|
|
52
68
|
const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
|
|
53
69
|
|
|
@@ -142,7 +158,13 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
|
|
|
142
158
|
## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
|
|
143
159
|
## into, and a number or a string is where a path ends whether the caller meant it to or not.
|
|
144
160
|
static func _can_hold(value: Variant) -> bool:
|
|
145
|
-
return value is Object or value is Array or value is Dictionary
|
|
161
|
+
return value is Object or value is Array or value is Dictionary or packed(value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
|
|
165
|
+
## one reaches the game's own copy, since packed lists share their storage until one is duplicated.
|
|
166
|
+
static func packed(value: Variant) -> bool:
|
|
167
|
+
return typeof(value) in PACKED
|
|
146
168
|
|
|
147
169
|
|
|
148
170
|
## What is wrong with reading [param named] off [param holder], or "" when nothing is.
|
|
@@ -168,6 +190,10 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
168
190
|
if not list_call.is_empty():
|
|
169
191
|
return list_call in LIST_CALLS and not (items.is_empty() and list_call in NEEDS_AN_ELEMENT)
|
|
170
192
|
return _index_in(named, items.size()) != -1
|
|
193
|
+
if packed(holder):
|
|
194
|
+
if not method_of(named).is_empty():
|
|
195
|
+
return method_of(named) in PACKED_CALLS
|
|
196
|
+
return _index_in(named, len(holder)) != -1
|
|
171
197
|
if holder is Dictionary:
|
|
172
198
|
var map: Dictionary = holder
|
|
173
199
|
if not method_of(named).is_empty():
|
|
@@ -209,6 +235,10 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
209
235
|
var items: Array = holder
|
|
210
236
|
items[_index_in(named, items.size())] = value
|
|
211
237
|
return
|
|
238
|
+
if packed(holder):
|
|
239
|
+
var list: Variant = holder
|
|
240
|
+
list[_index_in(named, len(holder))] = value
|
|
241
|
+
return
|
|
212
242
|
if holder is Dictionary:
|
|
213
243
|
var map: Dictionary = holder
|
|
214
244
|
# Written back under the key it was found under, since a map keyed by StringName and one
|
|
@@ -225,11 +255,14 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
|
|
|
225
255
|
## What [param holder] holds under [param named], or answers to it when [param named] is a call.
|
|
226
256
|
## Only ever called once [method can_read] agrees.
|
|
227
257
|
static func read_under(holder: Variant, named: String) -> Variant:
|
|
228
|
-
if (holder is Array or holder is Dictionary) and not method_of(named).is_empty():
|
|
258
|
+
if (holder is Array or holder is Dictionary or packed(holder)) and not method_of(named).is_empty():
|
|
229
259
|
return Callable.create(holder, method_of(named)).call()
|
|
230
260
|
if holder is Array:
|
|
231
261
|
var items: Array = holder
|
|
232
262
|
return items[_index_in(named, items.size())]
|
|
263
|
+
if packed(holder):
|
|
264
|
+
var list: Variant = holder
|
|
265
|
+
return list[_index_in(named, len(holder))]
|
|
233
266
|
if holder is Dictionary:
|
|
234
267
|
var map: Dictionary = holder
|
|
235
268
|
if map.has(named):
|
|
@@ -269,6 +302,10 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
269
302
|
if not container_call.is_empty():
|
|
270
303
|
return container_calls_refused(called_as, "a list", container_call)
|
|
271
304
|
return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
|
|
305
|
+
if packed(holder):
|
|
306
|
+
if not container_call.is_empty():
|
|
307
|
+
return container_calls_refused(called_as, "a packed list", container_call)
|
|
308
|
+
return "%s is a packed list of %d, so there is no %s in it" % [called_as, len(holder), named]
|
|
272
309
|
if holder is Dictionary:
|
|
273
310
|
if not container_call.is_empty():
|
|
274
311
|
return container_calls_refused(called_as, "a map", container_call)
|
|
@@ -303,10 +340,14 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
303
340
|
return "%s has no property %s" % [called_as, named]
|
|
304
341
|
|
|
305
342
|
|
|
306
|
-
## Why [param method] is not a call a path makes on [param sort] ("a list"
|
|
307
|
-
## the calls it does make.
|
|
343
|
+
## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
|
|
344
|
+
## map"), naming the calls it does make.
|
|
308
345
|
static func container_calls_refused(called_as: String, sort: String, method: String) -> String:
|
|
309
|
-
var calls: Array[String] =
|
|
346
|
+
var calls: Array[String] = MAP_CALLS
|
|
347
|
+
if sort == "a list":
|
|
348
|
+
calls = LIST_CALLS
|
|
349
|
+
elif sort == "a packed list":
|
|
350
|
+
calls = PACKED_CALLS
|
|
310
351
|
var spelled: Array[String] = []
|
|
311
352
|
for each: String in calls:
|
|
312
353
|
spelled.append(each + "()")
|
|
@@ -705,7 +705,11 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
705
705
|
# rather than as "has no method", which reads as a misspelling of a method that was never going
|
|
706
706
|
# to be there, and with the element spelled out, since a method on what it holds is the usual aim.
|
|
707
707
|
if not reached["holder"] is Object:
|
|
708
|
-
var sort: String = "a list"
|
|
708
|
+
var sort: String = "a list"
|
|
709
|
+
if reached["holder"] is Dictionary:
|
|
710
|
+
sort = "a map"
|
|
711
|
+
elif Paths.packed(reached["holder"]):
|
|
712
|
+
sort = "a packed list"
|
|
709
713
|
var spelled: String = str(reached["name"])
|
|
710
714
|
if Paths.method_of(spelled).is_empty():
|
|
711
715
|
spelled += "()"
|
package/build/index.js
CHANGED
|
@@ -29904,7 +29904,12 @@ var TOOL_SPECS = [
|
|
|
29904
29904
|
scenePath: SCENE_PATH,
|
|
29905
29905
|
actionName: { type: "string", description: 'Input action name, such as "jump".' },
|
|
29906
29906
|
events: INPUT_EVENTS,
|
|
29907
|
-
deadzone: {
|
|
29907
|
+
deadzone: {
|
|
29908
|
+
type: "number",
|
|
29909
|
+
minimum: 0,
|
|
29910
|
+
maximum: 1,
|
|
29911
|
+
description: "Input actions: analogue deadzone, 0 to 1. Default 0.5."
|
|
29912
|
+
},
|
|
29908
29913
|
pluginName: { type: "string", description: "Folder name under addons/." },
|
|
29909
29914
|
busName: { type: "string", description: "Audio bus name." },
|
|
29910
29915
|
parentBusIndex: { type: "number", description: "Audio buses: the bus to send to. Default 0, Master." },
|
|
@@ -30049,7 +30054,8 @@ var TOOL_SPECS = [
|
|
|
30049
30054
|
description: "Stop each suite at its first failing case. Default false: every case runs. With it on, the counts are of what ran, notRun says how many cases were left, and fixing what is named and running again finds the next one, which looks like a flaky tier and is not."
|
|
30050
30055
|
},
|
|
30051
30056
|
timeoutMs: {
|
|
30052
|
-
type: "
|
|
30057
|
+
type: "integer",
|
|
30058
|
+
minimum: 1,
|
|
30053
30059
|
description: "How long the run may take before it is killed. Default 600000."
|
|
30054
30060
|
}
|
|
30055
30061
|
},
|
|
@@ -30232,8 +30238,8 @@ var TOOL_SPECS = [
|
|
|
30232
30238
|
type: "boolean",
|
|
30233
30239
|
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."
|
|
30234
30240
|
},
|
|
30235
|
-
line: { type: "
|
|
30236
|
-
character: { type: "
|
|
30241
|
+
line: { type: "integer", minimum: 0, description: "completion, hover: zero-based line." },
|
|
30242
|
+
character: { type: "integer", minimum: 0, description: "completion, hover: zero-based column." }
|
|
30237
30243
|
},
|
|
30238
30244
|
requires: ["projectPath", "scriptPath"],
|
|
30239
30245
|
operations: {
|
|
@@ -30336,17 +30342,20 @@ var TOOL_SPECS = [
|
|
|
30336
30342
|
description: "Environment variables for this run alone, as a map of strings, over the server's own environment. Like savesIn, a run carrying any is started by this server rather than played by the editor. Names beginning with GDHARNESS_ are refused: they are this server's contract with the addon in the game, and the runtime directory is set here whatever the rest of the environment says, so a game that moves TEMP still announces where this server looks."
|
|
30337
30343
|
},
|
|
30338
30344
|
runtimeWaitMs: {
|
|
30339
|
-
type: "
|
|
30345
|
+
type: "integer",
|
|
30346
|
+
minimum: 0,
|
|
30340
30347
|
ops: ["start"],
|
|
30341
30348
|
description: "start: how long to wait for the game to announce its runtime before answering. Default 5000, or half as long again as the last boot of this project took when that is more, up to 60000: the server notes how long each game took to announce, so a project that boots slowly is waited for from its second start on without being asked to. 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."
|
|
30342
30349
|
},
|
|
30343
30350
|
frames: {
|
|
30344
|
-
type: "
|
|
30351
|
+
type: "integer",
|
|
30352
|
+
minimum: 1,
|
|
30345
30353
|
ops: ["check"],
|
|
30346
30354
|
description: "check: frames to run before quitting. Default 3."
|
|
30347
30355
|
},
|
|
30348
30356
|
timeoutMs: {
|
|
30349
|
-
type: "
|
|
30357
|
+
type: "integer",
|
|
30358
|
+
minimum: 1,
|
|
30350
30359
|
ops: ["check", "wait"],
|
|
30351
30360
|
description: "check: how long to give the boot before it is called hung. Default 60000. wait: how long to wait for the run to end before answering anyway. Default 600000."
|
|
30352
30361
|
},
|
|
@@ -30398,12 +30407,14 @@ var TOOL_SPECS = [
|
|
|
30398
30407
|
description: "Only entries mentioning this text, matched against everything the run has printed rather than against the entries this answer would otherwise carry: a line is found however much was printed after it."
|
|
30399
30408
|
},
|
|
30400
30409
|
limit: {
|
|
30401
|
-
type: "
|
|
30410
|
+
type: "integer",
|
|
30411
|
+
minimum: 1,
|
|
30402
30412
|
description: "The most entries to answer with, newest kept. Default 200, and omitted says how many matching entries that left out. A run long enough to pass it needs this raised, not just filtered: filtering narrows what counts as matching, and the newest of those is still all one answer carries."
|
|
30403
30413
|
},
|
|
30404
30414
|
before: {
|
|
30405
30415
|
ops: ["editor"],
|
|
30406
|
-
type: "
|
|
30416
|
+
type: "integer",
|
|
30417
|
+
minimum: 0,
|
|
30407
30418
|
description: "How many lines above a repeated group to carry with it, counting only lines that are not themselves part of a group. Default 3. The line that explains a wall of identical errors is the last ordinary line above it, so this walks past the wall rather than a fixed distance back."
|
|
30408
30419
|
}
|
|
30409
30420
|
},
|
|
@@ -30431,7 +30442,11 @@ var TOOL_SPECS = [
|
|
|
30431
30442
|
description: "Makes the running editor scan the project filesystem, so files written outside it are picked up. The scan is a change-detecting walk rather than an unconditional reparse, so a file another engine has already imported reads as settled and the walk does not look inside it: its class_name then stays out of the list the editor resolves against, however many times you scan. Any class in that state is named under unseenByEditor, which is what no check on disk can see, since the declaration and the cache are both correct there and only the editor disagrees. The cure is this call on its own, with no change to the declaring script: measured against a real editor on three platforms, with idle waits of the same length ruled out so the scan is credited rather than the time it takes, and reproduced in a second project against its own reproduction in 203ms. editor_launch restart also does it and costs a window, and project_import refresh_classes does not: it rewrites the cache and does not touch what the editor is holding. A scan writes .godot/global_script_class_cache.cfg from the list the editor is holding, so a class the editor cannot resolve goes out of that file with it. That is not a reason to refuse the scan, because on a class the editor has merely not walked yet the scan is the cure; what the answer does instead is name the loss under cacheLost and rebuild the cache from the files, naming what came back under cacheRestored, so no fresh engine, CI run or clone inherits the short file. An editor that lost classes is still holding the short list, so the note says to restart it before scanning again. The other direction is guarded the same way: a class the editor still holds for a script that was renamed or deleted comes back into the cache at a path that is not there, and the next engine to read it fails on \"Could not parse global class\" in whichever correct script shares the bare name, so the cache is rebuilt without it and the answer names it under cacheDropped; the editor goes on holding it until it is restarted, and every scan until then ends in that rebuild. A script or shader written outside the editor has no .uid sidecar until something imports it, and the scan writes one beside it, which a project that commits sidecars needs before the commit. Needs the editor connected.",
|
|
30432
30443
|
parameters: {
|
|
30433
30444
|
projectPath: PROJECT_PATH,
|
|
30434
|
-
timeoutMs: {
|
|
30445
|
+
timeoutMs: {
|
|
30446
|
+
type: "integer",
|
|
30447
|
+
minimum: 1,
|
|
30448
|
+
description: "How long to wait for the scan. Default 30000."
|
|
30449
|
+
},
|
|
30435
30450
|
reloadScript: {
|
|
30436
30451
|
type: "string",
|
|
30437
30452
|
description: "A script to recompile in the editor after the scan, for the half a scan does not reach: a script the editor has loaded keeps the copy it built, that copy is refreshed when one of its dependencies changes rather than when it changes itself, and a scan leaves it alone. Measured here, a built copy comes back without a method added to it while script_diagnostics on the same script reads clean in the same window, so the copy the editor built and whatever the analyser resolves types against are not known to be one thing: reload for the copy, and read the diagnostics as a separate answer rather than as this one confirmed. This recompiles into the same object, so the holders that kept the stale copy alive see the new one. The methods it has afterwards come back under reloadedMethods, because a reload that compiled nothing and answered OK is the failure worth catching."
|
|
@@ -30496,9 +30511,14 @@ var TOOL_SPECS = [
|
|
|
30496
30511
|
property: {
|
|
30497
30512
|
type: "string",
|
|
30498
30513
|
ops: ["property", "find"],
|
|
30499
|
-
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
30514
|
+
description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
|
|
30515
|
+
},
|
|
30516
|
+
depth: {
|
|
30517
|
+
type: "integer",
|
|
30518
|
+
minimum: 0,
|
|
30519
|
+
ops: ["tree"],
|
|
30520
|
+
description: "tree: levels to descend, 0 for the node alone. Default 3."
|
|
30500
30521
|
},
|
|
30501
|
-
depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
|
|
30502
30522
|
includeProperties: {
|
|
30503
30523
|
type: "boolean",
|
|
30504
30524
|
ops: ["tree"],
|
|
@@ -30528,7 +30548,8 @@ var TOOL_SPECS = [
|
|
|
30528
30548
|
description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
|
|
30529
30549
|
},
|
|
30530
30550
|
limit: {
|
|
30531
|
-
type: "
|
|
30551
|
+
type: "integer",
|
|
30552
|
+
minimum: 1,
|
|
30532
30553
|
ops: ["find", "text"],
|
|
30533
30554
|
description: "find: the most nodes to answer with, default 100. text: the most lines, default 500, with omitted saying how many lines that left behind. A screen whose dialog sits under a long list is a screen read with omitted greater than zero, so raise this or point root at the dialog rather than reading the answer as what is on screen."
|
|
30534
30555
|
},
|
|
@@ -30544,7 +30565,8 @@ var TOOL_SPECS = [
|
|
|
30544
30565
|
description: "metrics: which to read. Default all."
|
|
30545
30566
|
},
|
|
30546
30567
|
timeoutMs: {
|
|
30547
|
-
type: "
|
|
30568
|
+
type: "integer",
|
|
30569
|
+
minimum: 1,
|
|
30548
30570
|
description: "How long to wait for the answer before answering pending with a requestId, which runtime_invoke op result collects the reply by. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS."
|
|
30549
30571
|
}
|
|
30550
30572
|
},
|
|
@@ -30603,12 +30625,14 @@ var TOOL_SPECS = [
|
|
|
30603
30625
|
description: `call: the arguments, fitted to the method's parameter types. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths.`
|
|
30604
30626
|
},
|
|
30605
30627
|
timeoutMs: {
|
|
30606
|
-
type: "
|
|
30628
|
+
type: "integer",
|
|
30629
|
+
minimum: 1,
|
|
30607
30630
|
ops: ["set", "call"],
|
|
30608
30631
|
description: "set, call: how long to wait for the answer before answering pending with a requestId, which does not stop the call. Default 10000, or GDHARNESS_RUNTIME_TIMEOUT_MS. Give a longer one for a call known to take a while, such as one that plays many turns in one go."
|
|
30609
30632
|
},
|
|
30610
30633
|
requestId: {
|
|
30611
|
-
type: "
|
|
30634
|
+
type: "integer",
|
|
30635
|
+
minimum: 1,
|
|
30612
30636
|
ops: ["result"],
|
|
30613
30637
|
description: "result: the requestId a pending answer gave."
|
|
30614
30638
|
}
|
|
@@ -30640,8 +30664,16 @@ var TOOL_SPECS = [
|
|
|
30640
30664
|
ops: ["viewport"],
|
|
30641
30665
|
description: "viewport: the Viewport node. Default the root viewport."
|
|
30642
30666
|
},
|
|
30643
|
-
width: {
|
|
30644
|
-
|
|
30667
|
+
width: {
|
|
30668
|
+
type: "integer",
|
|
30669
|
+
minimum: 1,
|
|
30670
|
+
description: "Scale the image to this width. Given alone, the height keeps the picture's proportions."
|
|
30671
|
+
},
|
|
30672
|
+
height: {
|
|
30673
|
+
type: "integer",
|
|
30674
|
+
minimum: 1,
|
|
30675
|
+
description: "Scale the image to this height. Given alone, the width keeps the picture's proportions."
|
|
30676
|
+
},
|
|
30645
30677
|
outputPath: {
|
|
30646
30678
|
type: "string",
|
|
30647
30679
|
description: "Also save the picture as a PNG at this absolute path, for somebody to open later; the answer says where. Refused unless it ends in .png, its directory exists, it is outside the project, and no file is there already."
|
|
@@ -30675,7 +30707,13 @@ var TOOL_SPECS = [
|
|
|
30675
30707
|
ops: ["action", "key", "mouse_click"],
|
|
30676
30708
|
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; a wheel button down is a whole step, since a wheel is never held."
|
|
30677
30709
|
},
|
|
30678
|
-
strength: {
|
|
30710
|
+
strength: {
|
|
30711
|
+
type: "number",
|
|
30712
|
+
minimum: 0,
|
|
30713
|
+
maximum: 1,
|
|
30714
|
+
ops: ["action"],
|
|
30715
|
+
description: "action: 0 to 1. Default 1."
|
|
30716
|
+
},
|
|
30679
30717
|
keycode: {
|
|
30680
30718
|
type: ["string", "number"],
|
|
30681
30719
|
ops: ["key"],
|
|
@@ -30767,7 +30805,9 @@ var TOOL_SPECS = [
|
|
|
30767
30805
|
projectPath: RUNNING_PROJECT_PATH,
|
|
30768
30806
|
pid: RUNNING_PID,
|
|
30769
30807
|
frames: {
|
|
30770
|
-
type: "
|
|
30808
|
+
type: "integer",
|
|
30809
|
+
minimum: 1,
|
|
30810
|
+
maximum: 600,
|
|
30771
30811
|
ops: ["frames"],
|
|
30772
30812
|
description: "frames: how many to let pass, 1 to 600. More than that is refused."
|
|
30773
30813
|
},
|
|
@@ -30794,7 +30834,9 @@ var TOOL_SPECS = [
|
|
|
30794
30834
|
description: "until, with says: count words on a hidden node too. Default false, since a wait for words is a wait for them to be shown: a button that exists hidden through a whole animation carries its words the whole time and satisfied the wait at once, and every wait on that screen fell back to counting frames."
|
|
30795
30835
|
},
|
|
30796
30836
|
timeoutMs: {
|
|
30797
|
-
type: "
|
|
30837
|
+
type: "integer",
|
|
30838
|
+
minimum: 1,
|
|
30839
|
+
maximum: 120000,
|
|
30798
30840
|
ops: ["signal", "until"],
|
|
30799
30841
|
description: "signal, until: how long to wait before answering anyway, 1 to 120000. Default 5000."
|
|
30800
30842
|
}
|
|
@@ -30818,7 +30860,7 @@ var TOOL_SPECS = [
|
|
|
30818
30860
|
parameters: {
|
|
30819
30861
|
projectPath: PROJECT_PATH,
|
|
30820
30862
|
scriptPath: SCRIPT_PATH,
|
|
30821
|
-
line: { type: "
|
|
30863
|
+
line: { type: "integer", minimum: 1, description: "One-based line." }
|
|
30822
30864
|
},
|
|
30823
30865
|
requires: ["projectPath", "scriptPath", "line"],
|
|
30824
30866
|
operations: {
|
|
@@ -31156,6 +31198,9 @@ function describeValue(value) {
|
|
|
31156
31198
|
if (Array.isArray(value)) {
|
|
31157
31199
|
return "a list";
|
|
31158
31200
|
}
|
|
31201
|
+
if (typeof value === "number") {
|
|
31202
|
+
return String(value);
|
|
31203
|
+
}
|
|
31159
31204
|
return VALUE_NAMES[typeof value] ?? typeof value;
|
|
31160
31205
|
}
|
|
31161
31206
|
function wrongTypes(spec, args) {
|
|
@@ -31197,6 +31242,22 @@ function isPlainObject3(value) {
|
|
|
31197
31242
|
function outsideTheirLists(spec, args) {
|
|
31198
31243
|
return Object.entries(args).flatMap(([name, value]) => unlistedValues(spec.parameters[name], value, name));
|
|
31199
31244
|
}
|
|
31245
|
+
function outsideTheirRanges(spec, args) {
|
|
31246
|
+
const complaints = [];
|
|
31247
|
+
for (const [name, value] of Object.entries(args)) {
|
|
31248
|
+
const schema = spec.parameters[name];
|
|
31249
|
+
if (typeof value !== "number" || schema === undefined) {
|
|
31250
|
+
continue;
|
|
31251
|
+
}
|
|
31252
|
+
const least = typeof schema["minimum"] === "number" ? schema["minimum"] : undefined;
|
|
31253
|
+
const most = typeof schema["maximum"] === "number" ? schema["maximum"] : undefined;
|
|
31254
|
+
if (least !== undefined && value < least || most !== undefined && value > most) {
|
|
31255
|
+
const range = least !== undefined && most !== undefined ? `from ${least} to ${most}` : least !== undefined ? `of ${least} or more` : `of ${most} or less`;
|
|
31256
|
+
complaints.push(`${name} ${range}, not ${value}`);
|
|
31257
|
+
}
|
|
31258
|
+
}
|
|
31259
|
+
return complaints;
|
|
31260
|
+
}
|
|
31200
31261
|
var BRIDGE_RETRY_MS = 2000;
|
|
31201
31262
|
var SUCCESSOR_CHECK_MS = 1e4;
|
|
31202
31263
|
var HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
|
|
@@ -31701,7 +31762,11 @@ class GodotServer {
|
|
|
31701
31762
|
response: this.createErrorResponse(`${where} does not take ${unknown.join(", ")}. ${takes} This is gdharness ${SERVER_VERSION}, which is what decides whether an argument exists.`)
|
|
31702
31763
|
};
|
|
31703
31764
|
}
|
|
31704
|
-
const wrong = [
|
|
31765
|
+
const wrong = [
|
|
31766
|
+
...wrongTypes(spec, args),
|
|
31767
|
+
...outsideTheirLists(spec, args),
|
|
31768
|
+
...outsideTheirRanges(spec, args)
|
|
31769
|
+
];
|
|
31705
31770
|
if (wrong.length > 0) {
|
|
31706
31771
|
return { ok: false, response: this.createErrorResponse(`${spec.name} takes ${wrong.join("; ")}.`) };
|
|
31707
31772
|
}
|
|
@@ -31887,14 +31952,16 @@ class GodotServer {
|
|
|
31887
31952
|
case "runtime_inspect": {
|
|
31888
31953
|
const patience = readPositiveNumber(args, "timeoutMs") ?? this.runtimeTimeoutMs();
|
|
31889
31954
|
switch (op) {
|
|
31890
|
-
case "tree":
|
|
31955
|
+
case "tree": {
|
|
31956
|
+
const depth = readNonNegativeNumber(args, "depth");
|
|
31891
31957
|
return await this.handleRuntimeCommand("get_tree", {
|
|
31892
31958
|
...whichGame(args),
|
|
31893
31959
|
root: readNonEmptyString(args, "nodePath") ?? "/root",
|
|
31894
|
-
depth:
|
|
31960
|
+
depth: depth ?? 3,
|
|
31895
31961
|
include_properties: readBoolean(args, "includeProperties") ?? false,
|
|
31896
31962
|
properties: readArray(args, "properties") ?? []
|
|
31897
31963
|
}, patience);
|
|
31964
|
+
}
|
|
31898
31965
|
case "find":
|
|
31899
31966
|
return await this.handleFindRuntimeNodes(args, patience);
|
|
31900
31967
|
case "text":
|
|
@@ -33898,7 +33965,7 @@ class GodotServer {
|
|
|
33898
33965
|
limit: readPositiveNumber(args, "limit") ?? 200
|
|
33899
33966
|
});
|
|
33900
33967
|
const everything = console2.log.everything();
|
|
33901
|
-
const grouped = bursts(everything,
|
|
33968
|
+
const grouped = bursts(everything, readNonNegativeNumber(args, "before") ?? 3);
|
|
33902
33969
|
const shown = grouped.slice(0, MOST_GROUPS);
|
|
33903
33970
|
return this.jsonTextResponse({
|
|
33904
33971
|
editorPid: editorPid ?? undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdharness",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
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",
|