gdharness 1.0.26 → 1.0.28
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 +102 -29
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +19 -4
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +25 -12
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +3 -22
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +2 -1
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +163 -0
- package/build/godot/addons/gdharness_runtime/runtime_words.gd.uid +1 -0
- package/build/index.js +96 -29
- package/package.json +1 -1
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."
|
|
@@ -16491,7 +16506,12 @@ var init_tool_definitions = __esm(() => {
|
|
|
16491
16506
|
ops: ["property", "find"],
|
|
16492
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.'
|
|
16493
16508
|
},
|
|
16494
|
-
depth: {
|
|
16509
|
+
depth: {
|
|
16510
|
+
type: "integer",
|
|
16511
|
+
minimum: 0,
|
|
16512
|
+
ops: ["tree"],
|
|
16513
|
+
description: "tree: levels to descend, 0 for the node alone. Default 3."
|
|
16514
|
+
},
|
|
16495
16515
|
includeProperties: {
|
|
16496
16516
|
type: "boolean",
|
|
16497
16517
|
ops: ["tree"],
|
|
@@ -16518,10 +16538,11 @@ var init_tool_definitions = __esm(() => {
|
|
|
16518
16538
|
says: {
|
|
16519
16539
|
type: "string",
|
|
16520
16540
|
ops: ["find"],
|
|
16521
|
-
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."
|
|
16541
|
+
description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. 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
|
},
|
|
@@ -16545,7 +16567,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16545
16567
|
operations: {
|
|
16546
16568
|
tree: { summary: "the live scene tree", requires: [] },
|
|
16547
16569
|
text: {
|
|
16548
|
-
summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
16570
|
+
summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items read a line each",
|
|
16549
16571
|
requires: []
|
|
16550
16572
|
},
|
|
16551
16573
|
find: {
|
|
@@ -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"],
|
|
@@ -16678,7 +16716,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16678
16716
|
type: "string",
|
|
16679
16717
|
ops: ["text", "choose"],
|
|
16680
16718
|
blank: true,
|
|
16681
|
-
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says.'
|
|
16719
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says as drawn, which in a game with translations is the translation; the key it holds is matched after that.'
|
|
16682
16720
|
},
|
|
16683
16721
|
replace: {
|
|
16684
16722
|
type: "boolean",
|
|
@@ -16740,7 +16778,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
16740
16778
|
requires: ["keycode"]
|
|
16741
16779
|
},
|
|
16742
16780
|
text: {
|
|
16743
|
-
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
16781
|
+
summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask",
|
|
16744
16782
|
requires: ["text"]
|
|
16745
16783
|
},
|
|
16746
16784
|
mouse_click: {
|
|
@@ -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://"):
|
|
@@ -10,6 +10,7 @@ const Values = preload("runtime_values.gd")
|
|
|
10
10
|
## reports are the same place by construction rather than by agreement.
|
|
11
11
|
const Queries = preload("runtime_queries.gd")
|
|
12
12
|
const Read = preload("reading.gd")
|
|
13
|
+
const Words = preload("runtime_words.gd")
|
|
13
14
|
|
|
14
15
|
## The distance from a capital letter to its small one in Unicode. A keycode holds the capital.
|
|
15
16
|
const TO_SMALL: int = 32
|
|
@@ -239,11 +240,12 @@ static func _select_everything(focused: Control) -> bool:
|
|
|
239
240
|
## What the field says now, or null where the focus is not a field at all.
|
|
240
241
|
##
|
|
241
242
|
## A spin box rewrites its own text out of the number it parsed, so this is the only thing that
|
|
242
|
-
## says whether what was typed became what the field means.
|
|
243
|
+
## says whether what was typed became what the field means. A secret field answers with its mask,
|
|
244
|
+
## which still says how many characters it took.
|
|
243
245
|
static func _what_it_holds(focused: Control) -> Variant:
|
|
244
246
|
var field: LineEdit = focused as LineEdit
|
|
245
247
|
if field != null:
|
|
246
|
-
return field.text
|
|
248
|
+
return Words.masked(field) if field.secret else field.text
|
|
247
249
|
var box: TextEdit = focused as TextEdit
|
|
248
250
|
if box == null:
|
|
249
251
|
return null
|
|
@@ -666,7 +668,7 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
666
668
|
if menu.is_item_disabled(index):
|
|
667
669
|
return {
|
|
668
670
|
"type": "error",
|
|
669
|
-
"message": "%s item %d, %s, is disabled" % [node_path, index,
|
|
671
|
+
"message": "%s item %d, %s, is disabled" % [node_path, index, Words.item_says(menu, index)]
|
|
670
672
|
}
|
|
671
673
|
|
|
672
674
|
# Shown first, because a menu nobody has opened has no focus to move and Enter would go to
|
|
@@ -689,7 +691,7 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
689
691
|
"type": "chosen",
|
|
690
692
|
"path": node_path,
|
|
691
693
|
"index": index,
|
|
692
|
-
"text":
|
|
694
|
+
"text": Words.item_says(menu, index),
|
|
693
695
|
"id": menu.get_item_id(index),
|
|
694
696
|
"opened": opened,
|
|
695
697
|
"menu": str(menu.get_path()),
|
|
@@ -699,7 +701,7 @@ func choose(params: Dictionary) -> Dictionary:
|
|
|
699
701
|
var chooser: OptionButton = node as OptionButton
|
|
700
702
|
if chooser != null:
|
|
701
703
|
answer["selected"] = chooser.get_selected()
|
|
702
|
-
answer["shows"] = chooser
|
|
704
|
+
answer["shows"] = Words.said_by(chooser)
|
|
703
705
|
return answer
|
|
704
706
|
|
|
705
707
|
|
|
@@ -718,6 +720,11 @@ static func _menu_of(node: Node) -> PopupMenu:
|
|
|
718
720
|
|
|
719
721
|
## Which item was asked for: `text`, matched exactly and then case-insensitively, or `index`.
|
|
720
722
|
## Minus one when neither names one that is there.
|
|
723
|
+
##
|
|
724
|
+
## `text` is matched against the words the item shows before the ones it holds. In a game with
|
|
725
|
+
## translations they differ, and the shown ones are what a caller has read off the screen: an item
|
|
726
|
+
## held as `ACT_ENGAGE` and shown as "Hire" refused "Hire" and listed the keys. The held words
|
|
727
|
+
## still count after them, for a caller that has the key from the source.
|
|
721
728
|
static func _wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
722
729
|
if params.has("index"):
|
|
723
730
|
var asked: int = Read.as_int(params.get("index", -1), -1)
|
|
@@ -725,20 +732,26 @@ static func _wanted_item(menu: PopupMenu, params: Dictionary) -> int:
|
|
|
725
732
|
var wanted: String = str(params.get("text", ""))
|
|
726
733
|
if wanted.is_empty():
|
|
727
734
|
return -1
|
|
735
|
+
var shown: Array[String] = []
|
|
736
|
+
var held: Array[String] = []
|
|
728
737
|
for index: int in menu.get_item_count():
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
for
|
|
732
|
-
|
|
733
|
-
|
|
738
|
+
shown.append(Words.item_says(menu, index))
|
|
739
|
+
held.append(menu.get_item_text(index))
|
|
740
|
+
for words: Array[String] in [shown, held]:
|
|
741
|
+
for index: int in words.size():
|
|
742
|
+
if words[index] == wanted:
|
|
743
|
+
return index
|
|
744
|
+
for index: int in words.size():
|
|
745
|
+
if words[index].nocasecmp_to(wanted) == 0:
|
|
746
|
+
return index
|
|
734
747
|
return -1
|
|
735
748
|
|
|
736
749
|
|
|
737
|
-
## What the menu
|
|
750
|
+
## What the menu shows, for a refusal that names the choices rather than the miss.
|
|
738
751
|
static func _items_of(menu: PopupMenu) -> Array[String]:
|
|
739
752
|
var said: Array[String] = []
|
|
740
753
|
for index: int in menu.get_item_count():
|
|
741
|
-
said.append("%d: %s" % [index,
|
|
754
|
+
said.append("%d: %s" % [index, Words.item_says(menu, index)])
|
|
742
755
|
return said
|
|
743
756
|
|
|
744
757
|
|
|
@@ -6,6 +6,7 @@ extends RefCounted
|
|
|
6
6
|
const Paths = preload("runtime_paths.gd")
|
|
7
7
|
const Read = preload("reading.gd")
|
|
8
8
|
const Values = preload("runtime_values.gd")
|
|
9
|
+
const Words = preload("runtime_words.gd")
|
|
9
10
|
|
|
10
11
|
## The most nodes one find answers with, unless asked for fewer: enough for any real query and
|
|
11
12
|
## far short of the tree dump a query exists to avoid.
|
|
@@ -251,7 +252,7 @@ func _matches_apart_from_name(node: Node, wanted: Dictionary[String, String]) ->
|
|
|
251
252
|
# What this node says, rather than everything said underneath it. A row is then found by the
|
|
252
253
|
# label in it, and the path answered is that label's, which is where the words a caller is
|
|
253
254
|
# looking at actually are: matching every container above it would answer with the screen.
|
|
254
|
-
if not wanted["says"].is_empty() and not _says(said_by(node), wanted["says"]):
|
|
255
|
+
if not wanted["says"].is_empty() and not _says(Words.said_by(node), wanted["says"]):
|
|
255
256
|
return false
|
|
256
257
|
var script: Variant = node.get_script()
|
|
257
258
|
if not wanted["script"].is_empty():
|
|
@@ -346,8 +347,7 @@ func _read_into(node: Node, include_hidden: bool, most: int, into: Array[String]
|
|
|
346
347
|
if not include_hidden and not _drawn(node):
|
|
347
348
|
return 0
|
|
348
349
|
var left_out: int = 0
|
|
349
|
-
|
|
350
|
-
if not said.is_empty():
|
|
350
|
+
for said: String in Words.lines_said_by(node):
|
|
351
351
|
if into.size() < most:
|
|
352
352
|
into.append(said)
|
|
353
353
|
else:
|
|
@@ -373,25 +373,6 @@ static func _drawn(node: Node) -> bool:
|
|
|
373
373
|
return window == null or window.visible
|
|
374
374
|
|
|
375
375
|
|
|
376
|
-
## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
|
|
377
|
-
## is every label, button and field the interface is built out of.
|
|
378
|
-
##
|
|
379
|
-
## Public because three questions are the same question: what a screen reads as, which nodes say a
|
|
380
|
-
## given word, and whether anything has come to say it yet. Two copies of what a node says is how
|
|
381
|
-
## the three of them come to disagree about a SpinBox.
|
|
382
|
-
##
|
|
383
|
-
## Read rather than looked up in the property list, which the engine builds afresh on every call: a
|
|
384
|
-
## wait asks this of every node on the screen every frame, and on a hall of 3,500 nodes the lookup
|
|
385
|
-
## took the game it was watching from 60 frames a second to 11. A node without the property reads
|
|
386
|
-
## as null, which is not a string.
|
|
387
|
-
static func said_by(node: Node) -> String:
|
|
388
|
-
var text: Variant = node.get("text")
|
|
389
|
-
if not text is String:
|
|
390
|
-
return ""
|
|
391
|
-
var words: String = text
|
|
392
|
-
return words.strip_edges()
|
|
393
|
-
|
|
394
|
-
|
|
395
376
|
func get_rect(params: Dictionary) -> Dictionary:
|
|
396
377
|
var node_path: String = str(params.get("path", ""))
|
|
397
378
|
if node_path.is_empty():
|
|
@@ -7,6 +7,7 @@ const Paths = preload("runtime_paths.gd")
|
|
|
7
7
|
const Queries = preload("runtime_queries.gd")
|
|
8
8
|
const Read = preload("reading.gd")
|
|
9
9
|
const Values = preload("runtime_values.gd")
|
|
10
|
+
const Words = preload("runtime_words.gd")
|
|
10
11
|
|
|
11
12
|
## The longest one wait may last, whatever the request says: past this the server has long
|
|
12
13
|
## since given up on the reply.
|
|
@@ -291,7 +292,7 @@ func _anything_says(node_path: String, said: String, include_hidden: bool) -> bo
|
|
|
291
292
|
var pending: Array[Node] = [root]
|
|
292
293
|
while not pending.is_empty():
|
|
293
294
|
var node: Node = pending.pop_back()
|
|
294
|
-
if
|
|
295
|
+
if Words.said_by(node).containsn(said) and (include_hidden or Queries.shown(node)):
|
|
295
296
|
return true
|
|
296
297
|
pending.append_array(node.get_children(true))
|
|
297
298
|
return false
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## What a node says to a player, read the way it is drawn: the parsed words of rich text, as much of
|
|
4
|
+
## a line as is typed out so far, the translation rather than its key, a mask over a secret, and the
|
|
5
|
+
## items of the controls that draw a list.
|
|
6
|
+
##
|
|
7
|
+
## One module because three questions are the same question: what a screen reads as, which nodes say
|
|
8
|
+
## a given word, and whether anything has come to say it yet. Two copies of what a node says is how
|
|
9
|
+
## the three of them come to disagree about a SpinBox.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## What one node says, or "" for a node that says nothing. Anything with a `text` property, which
|
|
13
|
+
## is every label, button and field the interface is built out of.
|
|
14
|
+
##
|
|
15
|
+
## Read rather than looked up in the property list, which the engine builds afresh on every call: a
|
|
16
|
+
## wait asks this of every node on the screen every frame, and on a hall of 3,500 nodes the lookup
|
|
17
|
+
## took the game it was watching from 60 frames a second to 11. A node without the property reads
|
|
18
|
+
## as null, which is not a string.
|
|
19
|
+
##
|
|
20
|
+
## A label is asked about first and read directly, because a screen is mostly labels and this runs
|
|
21
|
+
## for each of them on every look a wait takes.
|
|
22
|
+
static func said_by(node: Node) -> String:
|
|
23
|
+
var label: Label = node as Label
|
|
24
|
+
if label != null:
|
|
25
|
+
return _label_says(label)
|
|
26
|
+
if _draws_a_list(node):
|
|
27
|
+
return "\n".join(lines_said_by(node))
|
|
28
|
+
return _text_of(node)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Every line [param node] draws for a player to read, one to an entry: a label's one line, or each
|
|
32
|
+
## item of a control that draws a list of them. Tab titles, list items, tree rows, menu titles and
|
|
33
|
+
## the items of an open menu are held by the control rather than in any `text`, so a screen read as
|
|
34
|
+
## every label on it and none of those, and a wait for a tab's title or a list item was never met.
|
|
35
|
+
static func lines_said_by(node: Node) -> Array[String]:
|
|
36
|
+
var lines: Array[String] = []
|
|
37
|
+
if node is TabBar:
|
|
38
|
+
var bar: TabBar = node
|
|
39
|
+
for tab: int in bar.tab_count:
|
|
40
|
+
if not bar.is_tab_hidden(tab):
|
|
41
|
+
_keep(node.atr(bar.get_tab_title(tab)), lines)
|
|
42
|
+
elif node is MenuBar:
|
|
43
|
+
var menus: MenuBar = node
|
|
44
|
+
for menu: int in menus.get_menu_count():
|
|
45
|
+
if not menus.is_menu_hidden(menu):
|
|
46
|
+
_keep(node.atr(menus.get_menu_title(menu)), lines)
|
|
47
|
+
elif node is ItemList:
|
|
48
|
+
var list: ItemList = node
|
|
49
|
+
for item: int in list.item_count:
|
|
50
|
+
_keep(_as_drawn(node, list.get_item_text(item), list.get_item_auto_translate_mode(item)), lines)
|
|
51
|
+
elif node is PopupMenu:
|
|
52
|
+
var menu: PopupMenu = node
|
|
53
|
+
for item: int in menu.item_count:
|
|
54
|
+
if not menu.is_item_separator(item):
|
|
55
|
+
_keep(item_says(menu, item), lines)
|
|
56
|
+
elif node is Tree:
|
|
57
|
+
var tree: Tree = node
|
|
58
|
+
if tree.column_titles_visible:
|
|
59
|
+
var titles: Array[String] = []
|
|
60
|
+
for column: int in tree.columns:
|
|
61
|
+
_keep(node.atr(tree.get_column_title(column)), titles)
|
|
62
|
+
_keep(" ".join(titles), lines)
|
|
63
|
+
_tree_rows(tree, tree.get_root(), tree.hide_root, lines)
|
|
64
|
+
else:
|
|
65
|
+
_keep(_text_of(node), lines)
|
|
66
|
+
return lines
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
## What item [param index] of [param menu] shows: the translation where the item has one. A menu is
|
|
70
|
+
## chosen from by these words, since they are the ones a caller has read off the screen.
|
|
71
|
+
static func item_says(menu: PopupMenu, index: int) -> String:
|
|
72
|
+
return _as_drawn(menu, menu.get_item_text(index), menu.get_item_auto_translate_mode(index))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## What a secret [param field] draws: one mask character for each character it holds. Reading the
|
|
76
|
+
## words back would hand a password to whoever reads the answer, which the player cannot see either.
|
|
77
|
+
static func masked(field: LineEdit) -> String:
|
|
78
|
+
return field.secret_character.repeat(field.text.length())
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
static func _draws_a_list(node: Node) -> bool:
|
|
82
|
+
return node is TabBar or node is MenuBar or node is ItemList or node is PopupMenu or node is Tree
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
static func _keep(line: String, into: Array[String]) -> void:
|
|
86
|
+
var trimmed: String = line.strip_edges()
|
|
87
|
+
if not trimmed.is_empty():
|
|
88
|
+
into.append(trimmed)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
## [param words] as [param node] draws them, which for a game with translations is the translation
|
|
92
|
+
## rather than the key it holds: a control set to `UI_HIRE` shows "Hire", and a wait for "Hire" was
|
|
93
|
+
## never met. [param mode] is an item's own setting, which the engine reads before the node's.
|
|
94
|
+
static func _as_drawn(
|
|
95
|
+
node: Node, words: String, mode: Node.AutoTranslateMode = Node.AUTO_TRANSLATE_MODE_INHERIT
|
|
96
|
+
) -> String:
|
|
97
|
+
match mode:
|
|
98
|
+
Node.AUTO_TRANSLATE_MODE_ALWAYS:
|
|
99
|
+
return node.tr(words)
|
|
100
|
+
Node.AUTO_TRANSLATE_MODE_DISABLED:
|
|
101
|
+
return words
|
|
102
|
+
return node.atr(words)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## The rows of a tree from [param item] down, a row being its columns' text side by side, and
|
|
106
|
+
## nothing under a collapsed row or a hidden one, since neither is drawn. A hidden root is only the
|
|
107
|
+
## place its children hang from.
|
|
108
|
+
static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[String]) -> void:
|
|
109
|
+
if item == null or not item.visible:
|
|
110
|
+
return
|
|
111
|
+
if not skip:
|
|
112
|
+
var cells: Array[String] = []
|
|
113
|
+
for column: int in tree.columns:
|
|
114
|
+
_keep(_as_drawn(tree, item.get_text(column), item.get_auto_translate_mode(column)), cells)
|
|
115
|
+
_keep(" ".join(cells), into)
|
|
116
|
+
if item.collapsed:
|
|
117
|
+
return
|
|
118
|
+
var child: TreeItem = item.get_first_child()
|
|
119
|
+
while child != null:
|
|
120
|
+
_tree_rows(tree, child, false, into)
|
|
121
|
+
child = child.get_next()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
## What a node with a `text` says, as it is drawn.
|
|
125
|
+
static func _text_of(node: Node) -> String:
|
|
126
|
+
var label: Label = node as Label
|
|
127
|
+
if label != null:
|
|
128
|
+
return _label_says(label)
|
|
129
|
+
# A RichTextLabel's text is its markup when it reads BBCode, and nobody reads the tags: a
|
|
130
|
+
# dossier line came back as "[b][color=#4fc2d4]Mollum Telken[/color], [/b]..." and a phrase
|
|
131
|
+
# running across a tag could not be found or waited for. The parsed text is what is drawn, and
|
|
132
|
+
# it also holds what a game added with append_text(), which the property never shows. It is
|
|
133
|
+
# parsed from the translation already.
|
|
134
|
+
if node is RichTextLabel:
|
|
135
|
+
var rich: RichTextLabel = node
|
|
136
|
+
return _shown_part(rich.get_parsed_text(), rich.visible_characters)
|
|
137
|
+
var text: Variant = node.get("text")
|
|
138
|
+
if not text is String:
|
|
139
|
+
return ""
|
|
140
|
+
var words: String = text
|
|
141
|
+
# What somebody typed is drawn as typed, never translated.
|
|
142
|
+
if node is LineEdit:
|
|
143
|
+
var field: LineEdit = node
|
|
144
|
+
return masked(field) if field.secret else words.strip_edges()
|
|
145
|
+
if node is TextEdit:
|
|
146
|
+
return words.strip_edges()
|
|
147
|
+
words = node.atr(words)
|
|
148
|
+
if node is Label3D:
|
|
149
|
+
var sign_text: Label3D = node
|
|
150
|
+
return (words.to_upper() if sign_text.uppercase else words).strip_edges()
|
|
151
|
+
return words.strip_edges()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
static func _label_says(label: Label) -> String:
|
|
155
|
+
var words: String = label.atr(label.text)
|
|
156
|
+
return _shown_part(words.to_upper() if label.uppercase else words, label.visible_characters)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## As much of [param words] as is drawn: a label typing a line out shows [param drawn] characters
|
|
160
|
+
## of it, and -1 is all of them. Read whole, a words wait was met on the first frame of a line being
|
|
161
|
+
## typed out, before the player could read any of it.
|
|
162
|
+
static func _shown_part(words: String, drawn: int) -> String:
|
|
163
|
+
return (words if drawn < 0 else words.substr(0, drawn)).strip_edges()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://u3ml7sk81frh
|
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."
|
|
@@ -30498,7 +30513,12 @@ var TOOL_SPECS = [
|
|
|
30498
30513
|
ops: ["property", "find"],
|
|
30499
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.'
|
|
30500
30515
|
},
|
|
30501
|
-
depth: {
|
|
30516
|
+
depth: {
|
|
30517
|
+
type: "integer",
|
|
30518
|
+
minimum: 0,
|
|
30519
|
+
ops: ["tree"],
|
|
30520
|
+
description: "tree: levels to descend, 0 for the node alone. Default 3."
|
|
30521
|
+
},
|
|
30502
30522
|
includeProperties: {
|
|
30503
30523
|
type: "boolean",
|
|
30504
30524
|
ops: ["tree"],
|
|
@@ -30525,10 +30545,11 @@ var TOOL_SPECS = [
|
|
|
30525
30545
|
says: {
|
|
30526
30546
|
type: "string",
|
|
30527
30547
|
ops: ["find"],
|
|
30528
|
-
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."
|
|
30548
|
+
description: "find: part of what the node has written on it as drawn, the way the text op reads it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. 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
|
},
|
|
@@ -30552,7 +30574,7 @@ var TOOL_SPECS = [
|
|
|
30552
30574
|
operations: {
|
|
30553
30575
|
tree: { summary: "the live scene tree", requires: [] },
|
|
30554
30576
|
text: {
|
|
30555
|
-
summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it",
|
|
30577
|
+
summary: "every line of text under nodePath, the values in its fields included, in the order somebody reads the screen, leaving out what is hidden and everything under it. Every line reads as drawn: rich text without its tags, text only as far as it has been typed out, a translation rather than its key, an upper-case label in capitals and a secret field as its mask. Tab titles, menu bar titles, list items, a tree's column titles and rows, and an open menu's items read a line each",
|
|
30556
30578
|
requires: []
|
|
30557
30579
|
},
|
|
30558
30580
|
find: {
|
|
@@ -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"],
|
|
@@ -30685,7 +30723,7 @@ var TOOL_SPECS = [
|
|
|
30685
30723
|
type: "string",
|
|
30686
30724
|
ops: ["text", "choose"],
|
|
30687
30725
|
blank: true,
|
|
30688
|
-
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says.'
|
|
30726
|
+
description: 'text: what to type. A newline is Enter and a tab is Tab, a space is a space, and "" with replace empties the field. choose: the item to take, by what it says as drawn, which in a game with translations is the translation; the key it holds is matched after that.'
|
|
30689
30727
|
},
|
|
30690
30728
|
replace: {
|
|
30691
30729
|
type: "boolean",
|
|
@@ -30747,7 +30785,7 @@ var TOOL_SPECS = [
|
|
|
30747
30785
|
requires: ["keycode"]
|
|
30748
30786
|
},
|
|
30749
30787
|
text: {
|
|
30750
|
-
summary: "type a string into the field being edited, a character at a time, and say what it landed in",
|
|
30788
|
+
summary: "type a string into the field being edited, a character at a time, and say what it landed in and what it holds afterwards, a secret field as its mask",
|
|
30751
30789
|
requires: ["text"]
|
|
30752
30790
|
},
|
|
30753
30791
|
mouse_click: {
|
|
@@ -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.28",
|
|
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",
|