gdharness 1.0.25 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -15897,7 +15897,12 @@ var init_tool_definitions = __esm(() => {
15897
15897
  scenePath: SCENE_PATH,
15898
15898
  actionName: { type: "string", description: 'Input action name, such as "jump".' },
15899
15899
  events: INPUT_EVENTS,
15900
- deadzone: { 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."
@@ -16489,9 +16504,14 @@ var init_tool_definitions = __esm(() => {
16489
16504
  property: {
16490
16505
  type: "string",
16491
16506
  ops: ["property", "find"],
16492
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
16507
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
16508
+ },
16509
+ depth: {
16510
+ type: "integer",
16511
+ minimum: 0,
16512
+ ops: ["tree"],
16513
+ description: "tree: levels to descend, 0 for the node alone. Default 3."
16493
16514
  },
16494
- depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
16495
16515
  includeProperties: {
16496
16516
  type: "boolean",
16497
16517
  ops: ["tree"],
@@ -16521,7 +16541,8 @@ var init_tool_definitions = __esm(() => {
16521
16541
  description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
16522
16542
  },
16523
16543
  limit: {
16524
- type: "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
  },
@@ -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"],
@@ -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://"):
@@ -48,6 +48,22 @@ const MAP_CALLS: Array[String] = [
48
48
  "get_typed_value_class_name",
49
49
  "get_typed_value_script",
50
50
  ]
51
+ ## The calls a path makes on a packed list, which has fewer than a list: measured on 4.7.2, it has no
52
+ ## front(), back(), max(), min() or hash().
53
+ const PACKED_CALLS: Array[String] = ["size", "is_empty", "duplicate", "to_byte_array"]
54
+ ## The packed lists, which a game keeps names and points in as often as it keeps them in an Array.
55
+ const PACKED: Array[int] = [
56
+ TYPE_PACKED_BYTE_ARRAY,
57
+ TYPE_PACKED_INT32_ARRAY,
58
+ TYPE_PACKED_INT64_ARRAY,
59
+ TYPE_PACKED_FLOAT32_ARRAY,
60
+ TYPE_PACKED_FLOAT64_ARRAY,
61
+ TYPE_PACKED_STRING_ARRAY,
62
+ TYPE_PACKED_VECTOR2_ARRAY,
63
+ TYPE_PACKED_VECTOR3_ARRAY,
64
+ TYPE_PACKED_COLOR_ARRAY,
65
+ TYPE_PACKED_VECTOR4_ARRAY,
66
+ ]
51
67
  ## The calls an empty list has nothing to answer, which the engine reports as an error when asked.
52
68
  const NEEDS_AN_ELEMENT: Array[String] = ["front", "back", "pick_random"]
53
69
 
@@ -142,7 +158,13 @@ static func object_at(root: Node, node: Node, node_path: String, given: Variant)
142
158
  ## Whether a step can be taken into [param value] at all: an object, a list or a map can be walked
143
159
  ## into, and a number or a string is where a path ends whether the caller meant it to or not.
144
160
  static func _can_hold(value: Variant) -> bool:
145
- return value is Object or value is Array or value is Dictionary
161
+ return value is Object or value is Array or value is Dictionary or packed(value)
162
+
163
+
164
+ ## Whether [param value] is a packed list. Stepped into the way a list is, by index, and a write into
165
+ ## one reaches the game's own copy, since packed lists share their storage until one is duplicated.
166
+ static func packed(value: Variant) -> bool:
167
+ return typeof(value) in PACKED
146
168
 
147
169
 
148
170
  ## What is wrong with reading [param named] off [param holder], or "" when nothing is.
@@ -168,6 +190,10 @@ static func can_read(holder: Variant, named: String) -> bool:
168
190
  if not list_call.is_empty():
169
191
  return list_call in LIST_CALLS and not (items.is_empty() and list_call in NEEDS_AN_ELEMENT)
170
192
  return _index_in(named, items.size()) != -1
193
+ if packed(holder):
194
+ if not method_of(named).is_empty():
195
+ return method_of(named) in PACKED_CALLS
196
+ return _index_in(named, len(holder)) != -1
171
197
  if holder is Dictionary:
172
198
  var map: Dictionary = holder
173
199
  if not method_of(named).is_empty():
@@ -209,6 +235,10 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
209
235
  var items: Array = holder
210
236
  items[_index_in(named, items.size())] = value
211
237
  return
238
+ if packed(holder):
239
+ var list: Variant = holder
240
+ list[_index_in(named, len(holder))] = value
241
+ return
212
242
  if holder is Dictionary:
213
243
  var map: Dictionary = holder
214
244
  # Written back under the key it was found under, since a map keyed by StringName and one
@@ -225,11 +255,14 @@ static func write(holder: Variant, named: String, value: Variant) -> void:
225
255
  ## What [param holder] holds under [param named], or answers to it when [param named] is a call.
226
256
  ## Only ever called once [method can_read] agrees.
227
257
  static func read_under(holder: Variant, named: String) -> Variant:
228
- if (holder is Array or holder is Dictionary) and not method_of(named).is_empty():
258
+ if (holder is Array or holder is Dictionary or packed(holder)) and not method_of(named).is_empty():
229
259
  return Callable.create(holder, method_of(named)).call()
230
260
  if holder is Array:
231
261
  var items: Array = holder
232
262
  return items[_index_in(named, items.size())]
263
+ if packed(holder):
264
+ var list: Variant = holder
265
+ return list[_index_in(named, len(holder))]
233
266
  if holder is Dictionary:
234
267
  var map: Dictionary = holder
235
268
  if map.has(named):
@@ -269,6 +302,10 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
269
302
  if not container_call.is_empty():
270
303
  return container_calls_refused(called_as, "a list", container_call)
271
304
  return "%s is a list of %d, so there is no %s in it" % [called_as, items.size(), named]
305
+ if packed(holder):
306
+ if not container_call.is_empty():
307
+ return container_calls_refused(called_as, "a packed list", container_call)
308
+ return "%s is a packed list of %d, so there is no %s in it" % [called_as, len(holder), named]
272
309
  if holder is Dictionary:
273
310
  if not container_call.is_empty():
274
311
  return container_calls_refused(called_as, "a map", container_call)
@@ -303,10 +340,14 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
303
340
  return "%s has no property %s" % [called_as, named]
304
341
 
305
342
 
306
- ## Why [param method] is not a call a path makes on [param sort] ("a list" or "a map"), naming
307
- ## the calls it does make.
343
+ ## Why [param method] is not a call a path makes on [param sort] ("a list", "a packed list" or "a
344
+ ## map"), naming the calls it does make.
308
345
  static func container_calls_refused(called_as: String, sort: String, method: String) -> String:
309
- var calls: Array[String] = LIST_CALLS if sort == "a list" else MAP_CALLS
346
+ var calls: Array[String] = MAP_CALLS
347
+ if sort == "a list":
348
+ calls = LIST_CALLS
349
+ elif sort == "a packed list":
350
+ calls = PACKED_CALLS
310
351
  var spelled: Array[String] = []
311
352
  for each: String in calls:
312
353
  spelled.append(each + "()")
@@ -705,7 +705,11 @@ func call_method(params: Dictionary) -> Dictionary:
705
705
  # rather than as "has no method", which reads as a misspelling of a method that was never going
706
706
  # to be there, and with the element spelled out, since a method on what it holds is the usual aim.
707
707
  if not reached["holder"] is Object:
708
- var sort: String = "a list" if reached["holder"] is Array else "a map"
708
+ var sort: String = "a list"
709
+ if reached["holder"] is Dictionary:
710
+ sort = "a map"
711
+ elif Paths.packed(reached["holder"]):
712
+ sort = "a packed list"
709
713
  var spelled: String = str(reached["name"])
710
714
  if Paths.method_of(spelled).is_empty():
711
715
  spelled += "()"
package/build/index.js CHANGED
@@ -29904,7 +29904,12 @@ var TOOL_SPECS = [
29904
29904
  scenePath: SCENE_PATH,
29905
29905
  actionName: { type: "string", description: 'Input action name, such as "jump".' },
29906
29906
  events: INPUT_EVENTS,
29907
- deadzone: { 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."
@@ -30496,9 +30511,14 @@ var TOOL_SPECS = [
30496
30511
  property: {
30497
30512
  type: "string",
30498
30513
  ops: ["property", "find"],
30499
- description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
30514
+ description: 'property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that takes no arguments and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.'
30515
+ },
30516
+ depth: {
30517
+ type: "integer",
30518
+ minimum: 0,
30519
+ ops: ["tree"],
30520
+ description: "tree: levels to descend, 0 for the node alone. Default 3."
30500
30521
  },
30501
- depth: { type: "number", ops: ["tree"], description: "tree: levels to descend. Default 3." },
30502
30522
  includeProperties: {
30503
30523
  type: "boolean",
30504
30524
  ops: ["tree"],
@@ -30528,7 +30548,8 @@ var TOOL_SPECS = [
30528
30548
  description: "find: part of what the node has written on it, case-insensitively, which is how a button is reached by the word on it rather than by a generated path. Its own text, so a row is found by the label in it, and hidden nodes match. A bare word is a contains; write a glob and it is one, matched against the whole of what the node says, the same as namePattern. A label with a line break is matched with the break in the words, and a backslash followed by n counts as one."
30529
30549
  },
30530
30550
  limit: {
30531
- type: "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
  },
@@ -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"],
@@ -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.25",
3
+ "version": "1.0.27",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",