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 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: { type: "number", description: "Input actions: analogue deadzone, 0 to 1. Default 0.5." },
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: "number",
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: "number", description: "completion, hover: zero-based line." },
16229
- character: { type: "number", description: "completion, hover: zero-based column." }
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: "number",
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: "number",
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: "number",
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: "number",
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: "number",
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: { type: "number", description: "How long to wait for the scan. Default 30000." },
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: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
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: "number",
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: "number",
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: "number",
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: "number",
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: { type: "number", description: "Scale the image to this width." },
16637
- height: { type: "number", description: "Scale the image to this height." },
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: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
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: "number",
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: "number",
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: "number", description: "One-based line." }
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 = [...wrongTypes(spec, args), ...outsideTheirLists(spec, args)];
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: readPositiveNumber(args, "depth") ?? 3,
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, readPositiveNumber(args, "before") ?? 3);
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 width: int = Read.as_int(params.get("width", 0))
65
- var height: int = Read.as_int(params.get("height", 0))
66
- if width > 0 and height > 0:
67
- image.resize(width, height)
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, menu.get_item_text(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": menu.get_item_text(index),
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.text
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
- if menu.get_item_text(index) == wanted:
730
- return index
731
- for index: int in menu.get_item_count():
732
- if menu.get_item_text(index).nocasecmp_to(wanted) == 0:
733
- return index
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 says, for a refusal that names the choices rather than the miss.
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, menu.get_item_text(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
- var said: String = said_by(node)
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 Queries.said_by(node).containsn(said) and (include_hidden or Queries.shown(node)):
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: { type: "number", description: "Input actions: analogue deadzone, 0 to 1. Default 0.5." },
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: "number",
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: "number", description: "completion, hover: zero-based line." },
30236
- character: { type: "number", description: "completion, hover: zero-based column." }
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: "number",
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: "number",
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: "number",
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: "number",
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: "number",
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: { type: "number", description: "How long to wait for the scan. Default 30000." },
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: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
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: "number",
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: "number",
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: "number",
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: "number",
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: { type: "number", description: "Scale the image to this width." },
30644
- height: { type: "number", description: "Scale the image to this height." },
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: { type: "number", ops: ["action"], description: "action: 0 to 1. Default 1." },
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: "number",
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: "number",
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: "number", description: "One-based line." }
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 = [...wrongTypes(spec, args), ...outsideTheirLists(spec, args)];
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: readPositiveNumber(args, "depth") ?? 3,
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, readPositiveNumber(args, "before") ?? 3);
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.26",
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",