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 +3 -3
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +12 -0
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +72 -22
- package/build/godot/operations/classdb_queries.gd +27 -2
- package/build/godot/operations/import_pipeline.gd +10 -1
- package/build/godot/operations/project_config.gd +7 -1
- package/build/godot/operations/project_diagnostics.gd +12 -28
- package/build/index.js +3 -3
- package/package.json +1 -1
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
|
|
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:
|
|
252
|
-
##
|
|
253
|
-
##
|
|
254
|
-
## the same words and the click was
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
323
|
-
##
|
|
324
|
-
##
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
var
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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",
|