gdharness 1.2.12 → 1.2.14

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
@@ -11005,7 +11005,7 @@ var init_tool_definitions = __esm(() => {
11005
11005
  {
11006
11006
  name: "project_settings",
11007
11007
  title: "Project settings",
11008
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start.",
11008
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is.",
11009
11009
  parameters: {
11010
11010
  projectPath: PROJECT_PATH,
11011
11011
  setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
@@ -11708,7 +11708,7 @@ var init_tool_definitions = __esm(() => {
11708
11708
  },
11709
11709
  check: {
11710
11710
  effect: "additive",
11711
- summary: "boot headless, quit after a few frames, and report the verdict",
11711
+ summary: "boot headless, quit after a few frames, and report the verdict. booted is that verdict rather than whether the game came up: true only for a run that quit on its own within timeoutMs, exited 0 and printed no error, so a game that reached _ready and then raised an error answers false. hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together",
11712
11712
  requires: ["projectPath"]
11713
11713
  },
11714
11714
  wait: {
@@ -12175,7 +12175,7 @@ var init_tool_definitions = __esm(() => {
12175
12175
  operations: {
12176
12176
  click: {
12177
12177
  effect: "additive",
12178
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
12178
+ summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. So is a control whose pointer a mouse_behavior_recursive set to disabled on it or above it has turned off, naming where it was set; by says, one the pointer can reach is picked over it. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
12179
12179
  requires: []
12180
12180
  },
12181
12181
  choose: {
@@ -570,12 +570,24 @@ func click(params: Dictionary) -> Dictionary:
570
570
  ## The last two answered landed, since the pointer reaches the control, while the press did nothing,
571
571
  ## measured on 4.7.2: a disabled button keeps its mouse filter and ignores the press, and a control
572
572
  ## that does not process while the game is paused is hovered and never handed the event.
573
+ ## A control whose pointer a `mouse_behavior_recursive` above it has turned off is passed through to
574
+ ## what is underneath, which answered `landed: false` with nothing saying why.
573
575
  func _takes_no_click(control: Control, node_path: String) -> String:
574
576
  if not control.is_visible_in_tree():
575
577
  return "%s is not visible%s, so nothing can click it" % [node_path, Targets.why_hidden(control)]
576
578
  var button: BaseButton = control as BaseButton
577
579
  if button != null and button.disabled:
578
580
  return "%s is disabled, so clicking it presses nothing" % node_path
581
+ var turned_off: Control = Targets.pointer_turned_off_by(control)
582
+ if turned_off != null:
583
+ var on: String = "itself" if turned_off == control else str(turned_off.get_path())
584
+ return (
585
+ (
586
+ "%s takes no pointer input, since mouse_behavior_recursive is disabled on %s,"
587
+ + " so clicking it presses nothing"
588
+ )
589
+ % [node_path, on]
590
+ )
579
591
  if not control.can_process():
580
592
  var why: String = _why_it_does_not_process(control, node_path)
581
593
  var over: Node = Targets.drawn_over(control)
@@ -12,6 +12,11 @@ const Words = preload("runtime_words.gd")
12
12
  ## How many of the controls saying the same words a refusal names, with the index for each.
13
13
  const LISTED_CANDIDATES: int = 10
14
14
 
15
+ ## What a match's rank is made of, one bit each, the highest deciding first.
16
+ const TAKES_POINTER: int = 4
17
+ const ENABLED_BUTTON: int = 2
18
+ const WHOLE: int = 1
19
+
15
20
 
16
21
  ## The control a click by [param wanted] words presses, found under [param root_path] in the tree
17
22
  ## [param root] heads, or the refusal saying why there is none or which of several:
@@ -238,9 +243,12 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
238
243
  found["partOf"] = _shortened(lines[index])
239
244
  if which == null and targets.size() > 1:
240
245
  var others: String = (
241
- "the other one on screen %s" % _passed_over(ranks[0], true)
246
+ "the other one on screen %s" % _passed_over(ranks[0], ranks.slice(1), true)
242
247
  if targets.size() == 2
243
- else "the other %d on screen %s" % [targets.size() - 1, _passed_over(ranks[0], false)]
248
+ else (
249
+ "the other %d on screen %s"
250
+ % [targets.size() - 1, _passed_over(ranks[0], ranks.slice(1), false)]
251
+ )
244
252
  )
245
253
  found["picked"] = "%s; %s" % [_described(1, ranks[0], wanted, under), others]
246
254
  if covered > 0:
@@ -248,13 +256,43 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
248
256
  return {"path": str(targets[index].get_path()), "found": found}
249
257
 
250
258
 
251
- ## How good a match [param target] is for a click: an enabled button over anything else, and then
252
- ## the whole of its words over a part of them. A disabled button ranks with what is not a button,
253
- ## since a click on it presses nothing: counted as a button, one tied with an enabled button saying
254
- ## the same words and the click was refused as not clear.
259
+ ## How good a match [param target] is for a click: a control the pointer can reach over one it
260
+ ## cannot, then an enabled button over anything else, and then the whole of its words over a part
261
+ ## of them. A disabled button ranks with what is not a button, since a click on it presses nothing:
262
+ ## counted as a button, one tied with an enabled button saying the same words and the click was
263
+ ## refused as not clear. A card on its way out turning the pointer off for itself and everything in
264
+ ## it tied the same way, its Post button against the next card's.
255
265
  static func _rank(target: Control, whole: bool) -> int:
256
266
  var button: BaseButton = target as BaseButton
257
- return (2 if button != null and not button.disabled else 0) + (1 if whole else 0)
267
+ var reached: bool = pointer_turned_off_by(target) == null
268
+ return (
269
+ (TAKES_POINTER if reached else 0)
270
+ + (ENABLED_BUTTON if reached and button != null and not button.disabled else 0)
271
+ + (WHOLE if whole else 0)
272
+ )
273
+
274
+
275
+ ## The control whose `mouse_behavior_recursive` keeps the pointer from [param control], itself or
276
+ ## the nearest above it setting one, or null when nothing does.
277
+ ##
278
+ ## Turned off on a control, it takes the pointer from everything under it without touching their
279
+ ## `mouse_filter` or a button's `disabled`, so neither says the press will do nothing. Read from the
280
+ ## engine's own answer, the filter with that override applied, rather than by walking the setting:
281
+ ## a control below can turn it back on, and the setting stops at a parent that is not a Control.
282
+ ## A control that ignores the pointer of its own accord, as a label does, is not turned off.
283
+ static func pointer_turned_off_by(control: Control) -> Control:
284
+ if (
285
+ control.mouse_filter == Control.MOUSE_FILTER_IGNORE
286
+ or control.get_mouse_filter_with_override() != Control.MOUSE_FILTER_IGNORE
287
+ ):
288
+ return null
289
+ var deciding: Control = control
290
+ while (
291
+ deciding.mouse_behavior_recursive == Control.MOUSE_BEHAVIOR_INHERITED
292
+ and deciding.get_parent() is Control
293
+ ):
294
+ deciding = deciding.get_parent()
295
+ return deciding
258
296
 
259
297
 
260
298
  ## Whether one of [param wanted]'s alternatives is the whole of [param said], or the whole of one of
@@ -307,31 +345,43 @@ static func _in_a_word(code: int) -> bool:
307
345
 
308
346
  ## [param count] matches of [param rank], as the start of a sentence.
309
347
  static func _described(count: int, rank: int, wanted: String, under: String) -> String:
310
- var what: String = "button" if rank >= 2 else "control"
348
+ var what: String = "button" if rank & ENABLED_BUTTON else "control"
311
349
  var how: String = '"%s"' % wanted
312
350
  var plain: bool = true
313
351
  for words: String in Says.alternatives(wanted):
314
352
  plain = plain and Says.is_plain(words)
315
353
  if plain:
316
- how = ('exactly "%s"' if rank % 2 == 1 else '"%s" as part of more') % wanted
354
+ how = ('exactly "%s"' if rank & WHOLE else '"%s" as part of more') % wanted
317
355
  if count == 1:
318
356
  return "the one %s on screen%s saying %s" % [what, under, how]
319
357
  return "%d %ss on screen%s say %s" % [count, what, under, how]
320
358
 
321
359
 
322
- ## Why the matches below [param rank] were passed over, said of one of them when [param one]. As
323
- ## what the code knows, which is whether a control is an enabled button: a card taking clicks in its
324
- ## own `gui_input` is not one, and calling it something that cannot be pressed was a guess.
325
- static func _passed_over(rank: int, one: bool) -> String:
326
- var says: String = "says" if one else "say"
327
- var is_not: String = "is not an enabled button" if one else "are not enabled buttons"
328
- match rank:
329
- 3:
330
- return "%s or %s it as part of more" % [is_not, says]
331
- 2:
332
- return is_not
333
- _:
334
- return "%s it as part of more" % says
360
+ ## Why the matches ranked [param others] were passed over for the one ranked [param best], said of
361
+ ## one of them when [param one]: each way the best is better that one of them is not. As what the
362
+ ## code knows, which is whether a control is an enabled button: a card taking clicks in its own
363
+ ## `gui_input` is not one, and calling it something that cannot be pressed was a guess.
364
+ static func _passed_over(best: int, others: Array[int], one: bool) -> String:
365
+ var lacking: int = 0
366
+ for rank: int in others:
367
+ var short: int = best & ~rank
368
+ # One the pointer cannot reach is never ranked a button, so that is the whole of why.
369
+ if short & TAKES_POINTER:
370
+ short &= ~ENABLED_BUTTON
371
+ lacking |= short
372
+ var reasons: Array[String] = []
373
+ if lacking & TAKES_POINTER:
374
+ reasons.append("takes no pointer input" if one else "take no pointer input")
375
+ if lacking & ENABLED_BUTTON:
376
+ reasons.append("is not an enabled button" if one else "are not enabled buttons")
377
+ if lacking & WHOLE:
378
+ reasons.append("says it as part of more" if one else "say it as part of more")
379
+ # Ranked level with the best, a match was passed over for having the words only inside longer ones.
380
+ if reasons.is_empty():
381
+ return "says it only inside a longer word" if one else "say it only inside longer words"
382
+ if reasons.size() == 1:
383
+ return reasons[0]
384
+ return "%s or %s" % [", ".join(reasons.slice(0, -1)), reasons[-1]]
335
385
 
336
386
 
337
387
  ## What to add to a refusal that found nothing on screen, when words matched controls out of sight.
@@ -283,6 +283,7 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
283
283
  {
284
284
  "name": a.get("name", ""),
285
285
  "type": a.get("type", 0),
286
+ "type_name": _type_name(a, "Variant"),
286
287
  "class_name": a.get("class_name", ""),
287
288
  "hint_string": a.get("hint_string", "")
288
289
  }
@@ -293,7 +294,11 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
293
294
  "name": m.get("name", ""),
294
295
  "args": args,
295
296
  "return":
296
- {"type": return_info.get("type", 0), "class_name": return_info.get("class_name", "")},
297
+ {
298
+ "type": return_info.get("type", 0),
299
+ "type_name": _type_name(return_info, "void"),
300
+ "class_name": return_info.get("class_name", "")
301
+ },
297
302
  "flags": m.get("flags", 0),
298
303
  "default_args": m.get("default_args", [])
299
304
  }
@@ -302,6 +307,20 @@ func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
302
307
  return methods
303
308
 
304
309
 
310
+ # What [param info]'s type is called: its class for an object of one, and otherwise the engine's own
311
+ # name for the type, beside the number the engine reports, which reads as nothing without its enum.
312
+ # Nil is a value that can be anything when the usage says so, and [param nil_name] otherwise: void
313
+ # for what a method returns, and Variant for an argument or property with no type.
314
+ static func _type_name(info: Dictionary, nil_name: String) -> String:
315
+ var type: int = Read.as_int(info.get("type", 0))
316
+ var named: String = str(info.get("class_name", ""))
317
+ if type == TYPE_OBJECT and not named.is_empty():
318
+ return named
319
+ if type == TYPE_NIL:
320
+ return "Variant" if Read.as_int(info.get("usage", 0)) & PROPERTY_USAGE_NIL_IS_VARIANT else nil_name
321
+ return type_string(type)
322
+
323
+
305
324
  func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
306
325
  var properties: Array[Dictionary] = []
307
326
 
@@ -314,6 +333,7 @@ func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Di
314
333
  {
315
334
  "name": p.get("name", ""),
316
335
  "type": p.get("type", 0),
336
+ "type_name": _type_name(p, "Variant"),
317
337
  "class_name": p.get("class_name", ""),
318
338
  "hint": p.get("hint", 0),
319
339
  "hint_string": p.get("hint_string", ""),
@@ -332,7 +352,12 @@ func _signals_of(class_name_str: String, include_inherited: bool) -> Array[Dicti
332
352
  var declared_args: Array = s.get("args", [])
333
353
  for a: Dictionary in declared_args:
334
354
  sig_args.append(
335
- {"name": a.get("name", ""), "type": a.get("type", 0), "class_name": a.get("class_name", "")}
355
+ {
356
+ "name": a.get("name", ""),
357
+ "type": a.get("type", 0),
358
+ "type_name": _type_name(a, "Variant"),
359
+ "class_name": a.get("class_name", "")
360
+ }
336
361
  )
337
362
  signals.append({"name": s.get("name", ""), "args": sig_args})
338
363
 
@@ -103,7 +103,16 @@ func get_import_status(params: Dictionary) -> Dictionary:
103
103
 
104
104
  _tally(summary, status)
105
105
 
106
- return {"resources": resources, "summary": summary}
106
+ var answer: Dictionary = {"resources": resources, "summary": summary}
107
+ # A total of none reads as a walk that found nothing when the project is full of scripts and
108
+ # scenes, which the engine loads as they are and never imports.
109
+ if resource_path.is_empty() and summary["total"] == 0:
110
+ answer["note"] = (
111
+ "Nothing in this project is imported: the engine imports images, audio, fonts, models and"
112
+ + " the like, found by extension or by a .import sidecar beside them, and loads scripts and"
113
+ + " scenes as they are."
114
+ )
115
+ return answer
107
116
 
108
117
 
109
118
  # Get import options for a resource
@@ -94,6 +94,11 @@ func set_project_setting(params: Dictionary) -> Dictionary:
94
94
  var had_value: bool = ProjectSettings.has_setting(setting_path)
95
95
  if had_value:
96
96
  old_value = ProjectSettings.get_setting(setting_path)
97
+ # A setting the engine registers is there with its default whether or not project.godot sets it,
98
+ # so has_setting alone called the description of a project that had never set one an old value.
99
+ # At its default, the file does not set it: the engine drops a key at its default when it saves.
100
+ var default_value: Variant = ProjectSettings.property_get_revert(setting_path)
101
+ var was_default: bool = had_value and default_value != null and old_value == default_value
97
102
 
98
103
  var final_value: Variant = _values.deserialize_value(value)
99
104
  var wanted: int = _wanted_type(setting_path, old_value if had_value else null)
@@ -116,7 +121,8 @@ func set_project_setting(params: Dictionary) -> Dictionary:
116
121
  "setting_path": setting_path,
117
122
  "old_value": _values.serialize_value(old_value) if had_value else null,
118
123
  "new_value": _values.serialize_value(final_value),
119
- "was_new": not had_value,
124
+ "was_new": not had_value or was_default,
125
+ "was_default": was_default,
120
126
  "saved": true,
121
127
  }
122
128
 
@@ -30,16 +30,9 @@ func get_project_health(params: Dictionary) -> Dictionary:
30
30
  if not FileAccess.file_exists("res://project.godot"):
31
31
  details.append("Missing project.godot file")
32
32
  deductions += 30
33
-
34
- var recommended_dirs: Array[String] = ["res://scenes", "res://scripts", "res://assets"]
35
- var missing_dirs: Array[String] = []
36
- for dir: String in recommended_dirs:
37
- if not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(dir)):
38
- missing_dirs.append(dir)
39
-
40
- if missing_dirs.size() > 0:
41
- details.append("Consider organizing with directories: " + ", ".join(missing_dirs))
42
- deductions += 2 * missing_dirs.size()
33
+ # No layout is asked for. A project without res://scenes, res://scripts and res://assets
34
+ # failed this check and lost points, while Godot's own guidance is to keep a scene's files
35
+ # together by feature, so the check failed projects for following the engine's advice.
43
36
 
44
37
  checks["structure"] = {"name": "Project Structure", "passed": details.is_empty(), "details": details}
45
38
 
@@ -74,7 +67,6 @@ func get_project_health(params: Dictionary) -> Dictionary:
74
67
 
75
68
  var script_files: Array[String] = _files.find_files("res://", ".gd")
76
69
  var todo_count: int = 0
77
- var empty_functions: int = 0
78
70
 
79
71
  for script_path: String in script_files:
80
72
  var file: FileAccess = FileAccess.open(script_path, FileAccess.READ)
@@ -84,16 +76,12 @@ func get_project_health(params: Dictionary) -> Dictionary:
84
76
 
85
77
  if "# TODO" in content or "# FIXME" in content:
86
78
  todo_count += 1
87
- if "pass # " in content or content.ends_with("pass\n"):
88
- empty_functions += 1
89
79
 
80
+ # Work noted for later, which no script fails on, so said rather than failed. A count of
81
+ # "empty functions" read off a script ending in `pass` went with it: a virtual left for an
82
+ # override to fill ends that way and is exactly right.
90
83
  if todo_count > 0:
91
- details.append(str(todo_count) + " scripts have TODO/FIXME comments")
92
- deductions += todo_count
93
-
94
- if empty_functions > 0:
95
- details.append(str(empty_functions) + " scripts may have empty functions")
96
- deductions += empty_functions
84
+ recommendations.append("%d scripts have TODO or FIXME comments to look at" % todo_count)
97
85
 
98
86
  checks["scripts"] = {
99
87
  "name": "Scripts",
@@ -136,10 +124,6 @@ func get_project_health(params: Dictionary) -> Dictionary:
136
124
  details.append("No project name set")
137
125
  deductions += 2
138
126
 
139
- if not FileAccess.file_exists("res://export_presets.cfg"):
140
- details.append("No export presets configured")
141
- deductions += 3
142
-
143
127
  checks["config"] = {"name": "Configuration", "passed": details.is_empty(), "details": details}
144
128
 
145
129
  var score: int = maxi(0, 100 - deductions)
@@ -156,11 +140,11 @@ func get_project_health(params: Dictionary) -> Dictionary:
156
140
  else:
157
141
  grade = "F"
158
142
 
159
- if deductions > 0:
160
- if score < 70:
161
- recommendations.append("Address critical issues to improve project stability")
162
- if not FileAccess.file_exists("res://export_presets.cfg"):
163
- recommendations.append("Configure export presets for your target platforms")
143
+ if score < 70:
144
+ recommendations.append("Address critical issues to improve project stability")
145
+ # A project still being made has no presets yet, which is no fault in it.
146
+ if "config" in check_categories and not FileAccess.file_exists("res://export_presets.cfg"):
147
+ recommendations.append("No export presets are configured: add them before exporting")
164
148
 
165
149
  # Every detail costs points, so each is an issue; `issues` was always empty beside them, and a
166
150
  # category said passed next to the failure it had just found.
package/build/index.js CHANGED
@@ -33895,7 +33895,7 @@ var TOOL_SPECS = [
33895
33895
  {
33896
33896
  name: "project_settings",
33897
33897
  title: "Project settings",
33898
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start.",
33898
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is.",
33899
33899
  parameters: {
33900
33900
  projectPath: PROJECT_PATH2,
33901
33901
  setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
@@ -34598,7 +34598,7 @@ var TOOL_SPECS = [
34598
34598
  },
34599
34599
  check: {
34600
34600
  effect: "additive",
34601
- summary: "boot headless, quit after a few frames, and report the verdict",
34601
+ summary: "boot headless, quit after a few frames, and report the verdict. booted is that verdict rather than whether the game came up: true only for a run that quit on its own within timeoutMs, exited 0 and printed no error, so a game that reached _ready and then raised an error answers false. hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together",
34602
34602
  requires: ["projectPath"]
34603
34603
  },
34604
34604
  wait: {
@@ -35065,7 +35065,7 @@ var TOOL_SPECS = [
35065
35065
  operations: {
35066
35066
  click: {
35067
35067
  effect: "additive",
35068
- summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
35068
+ summary: `press and release on a Control, a frame apart, and answer with what was under the pointer and what became of the control: in_tree, removed or freed. The control is named by nodePath, or by says with the words on it, found under nodePath when both are given, and the answer then says under found which match it was. Words that are part of a RichTextLabel are pressed where they are drawn rather than at the label's middle, so a link among them is the one pressed: found.pressedOn says "the words", or "the middle of the label" with a note saying why they could not be placed, and on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control out of sight inside a ScrollContainer is scrolled to first, each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A disabled button, and a control that does not process (the game paused, say), are refused: the pointer reaches them and the press does nothing. So is a control whose pointer a mouse_behavior_recursive set to disabled on it or above it has turned off, naming where it was set; by says, one the pointer can reach is picked over it. A 3D node is clicked where it is drawn, and landed then says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room`,
35069
35069
  requires: []
35070
35070
  },
35071
35071
  choose: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
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",