gdharness 1.2.39 → 1.2.41
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 +1435 -1410
- package/build/godot/addons/gdharness_editor/animation_reads.gd +11 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
- package/build/godot/addons/gdharness_editor/headless_check.gd +22 -11
- package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +9 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +12 -12
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +175 -0
- package/build/godot/addons/gdharness_runtime/runtime_items.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +43 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +61 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +48 -18
- package/build/godot/operations/animation_reads.gd +11 -2
- package/build/godot/operations/editor_tools.gd +9 -0
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/property_values.gd +1 -1
- package/build/godot/operations/reading.gd +15 -0
- package/build/index.js +2789 -2765
- package/package.json +1 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Where a control drawing a list draws one of its items, for a click by words: a tab, a list entry,
|
|
4
|
+
## a tree row or a menu bar title. Each is drawn by the control rather than held as a node, so a click
|
|
5
|
+
## on the control's path presses its middle, which is whichever item happens to sit there.
|
|
6
|
+
|
|
7
|
+
const Says = preload("runtime_says.gd")
|
|
8
|
+
const Values = preload("runtime_values.gd")
|
|
9
|
+
const Words = preload("runtime_words.gd")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Where [param control] draws [param item], one of [method Words.items_of], in the control's own
|
|
13
|
+
## space, or an empty rectangle while it draws it nowhere a pointer reaches: a tab scrolled off its
|
|
14
|
+
## bar, an entry or a row scrolled out of its list, a menu bar the operating system draws. A tree
|
|
15
|
+
## row is the cell saying [param wanted] when one does, and the whole row otherwise.
|
|
16
|
+
##
|
|
17
|
+
## Each one checked against the control's own answer to which item is under a point, where it has
|
|
18
|
+
## one, since an entry's rectangle is kept in the list's unscrolled space and a row's is computed
|
|
19
|
+
## from the scroll as of the last draw.
|
|
20
|
+
static func item_rect(control: Control, item: Variant, wanted: String) -> Rect2:
|
|
21
|
+
var bounds: Rect2 = Rect2(Vector2.ZERO, control.size)
|
|
22
|
+
var bar: TabBar = control as TabBar
|
|
23
|
+
if bar != null:
|
|
24
|
+
var tab: int = item
|
|
25
|
+
var placed: Rect2 = bar.get_tab_rect(tab)
|
|
26
|
+
return placed if bar.get_tab_idx_at_point(placed.get_center()) == tab else Rect2()
|
|
27
|
+
var list: ItemList = control as ItemList
|
|
28
|
+
if list != null:
|
|
29
|
+
var entry: int = item
|
|
30
|
+
var scrolled: Vector2 = Vector2(list.get_h_scroll_bar().value, list.get_v_scroll_bar().value)
|
|
31
|
+
var placed: Rect2 = list.get_item_rect(entry, false)
|
|
32
|
+
placed.position -= scrolled
|
|
33
|
+
var reached: bool = bounds.has_point(placed.get_center())
|
|
34
|
+
return (
|
|
35
|
+
placed if reached and list.get_item_at_position(placed.get_center(), true) == entry else Rect2()
|
|
36
|
+
)
|
|
37
|
+
var tree: Tree = control as Tree
|
|
38
|
+
if tree != null:
|
|
39
|
+
var row: TreeItem = item
|
|
40
|
+
var column: int = tree_column(tree, row, wanted)
|
|
41
|
+
var placed: Rect2 = tree.get_item_area_rect(row, column)
|
|
42
|
+
var centre: Vector2 = placed.get_center()
|
|
43
|
+
if not bounds.has_point(centre) or tree.get_item_at_position(centre) != row:
|
|
44
|
+
return Rect2()
|
|
45
|
+
return placed if column == -1 or tree.get_column_at_position(centre) == column else Rect2()
|
|
46
|
+
var menus: MenuBar = control as MenuBar
|
|
47
|
+
if menus != null and not menus.is_native_menu():
|
|
48
|
+
var menu: int = item
|
|
49
|
+
return _menu_title_rect(menus, menu)
|
|
50
|
+
return Rect2()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## The column of [param row] whose cell says [param wanted], or -1 when only the row as a whole
|
|
54
|
+
## does, its cells read together.
|
|
55
|
+
static func tree_column(tree: Tree, row: TreeItem, wanted: String) -> int:
|
|
56
|
+
for column: int in tree.columns:
|
|
57
|
+
if Says.matches(Words.cell_says(tree, row, column), wanted):
|
|
58
|
+
return column
|
|
59
|
+
return -1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## [param item] of [param control] as an answer names it: what it is, its number where the control
|
|
63
|
+
## numbers its items, and what it says.
|
|
64
|
+
static func item_named(control: Control, item: Variant, says: String) -> Dictionary:
|
|
65
|
+
if control is Tree:
|
|
66
|
+
return {"kind": "row", "says": says}
|
|
67
|
+
var kind: String = "tab" if control is TabBar else ("menu" if control is MenuBar else "item")
|
|
68
|
+
return {"kind": kind, "index": item, "says": says}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Where in [param control] to press for [param item], the one of its items a click by
|
|
72
|
+
## [param wanted] matched, as {"point", "scrolled"}, or a refusal saying why there is nowhere.
|
|
73
|
+
##
|
|
74
|
+
## Scrolled into the control's own view first when it is drawn nowhere yet, the way a player brings
|
|
75
|
+
## a tab or an entry into view before pressing it. A disabled item is refused as a disabled button
|
|
76
|
+
## is. A tree row is pressed on the cell saying the words, whose column goes into [param found].
|
|
77
|
+
static func in_view(
|
|
78
|
+
scene: SceneTree, control: Control, node_path: String, item: Variant, wanted: String, found: Dictionary
|
|
79
|
+
) -> Dictionary:
|
|
80
|
+
var named: Dictionary = found.get("item", {})
|
|
81
|
+
var naming: String = 'the %s "%s" of %s' % [named.get("kind", "item"), named.get("says", ""), node_path]
|
|
82
|
+
var index: int = item if item is int else -1
|
|
83
|
+
var bar: TabBar = control as TabBar
|
|
84
|
+
var list: ItemList = control as ItemList
|
|
85
|
+
var menus: MenuBar = control as MenuBar
|
|
86
|
+
if (
|
|
87
|
+
(bar != null and bar.is_tab_disabled(index))
|
|
88
|
+
or (list != null and list.is_item_disabled(index))
|
|
89
|
+
or (menus != null and menus.is_menu_disabled(index))
|
|
90
|
+
):
|
|
91
|
+
return {"type": "error", "message": "%s is disabled, so clicking it presses nothing" % naming}
|
|
92
|
+
var placed: Rect2 = item_rect(control, item, wanted)
|
|
93
|
+
var scrolled: bool = false
|
|
94
|
+
if placed.size == Vector2.ZERO:
|
|
95
|
+
scrolled = _scroll_to(control, item)
|
|
96
|
+
if scrolled:
|
|
97
|
+
# A tree places its rows by the scroll it last drew with.
|
|
98
|
+
await scene.process_frame
|
|
99
|
+
var left: String = Values.afterwards(control)
|
|
100
|
+
if left != "in_tree" or (typeof(item) == TYPE_OBJECT and not is_instance_valid(item)):
|
|
101
|
+
return {
|
|
102
|
+
"type": "error",
|
|
103
|
+
"message":
|
|
104
|
+
"%s went while it was being scrolled into view, so nothing was clicked" % naming
|
|
105
|
+
}
|
|
106
|
+
placed = item_rect(control, item, wanted)
|
|
107
|
+
if placed.size == Vector2.ZERO:
|
|
108
|
+
var tried: String = " even scrolled to" if scrolled else ""
|
|
109
|
+
return {
|
|
110
|
+
"type": "error",
|
|
111
|
+
"message": "%s is drawn nowhere a pointer reaches%s, so nothing was clicked" % [naming, tried]
|
|
112
|
+
}
|
|
113
|
+
var tree: Tree = control as Tree
|
|
114
|
+
if tree != null:
|
|
115
|
+
var row: TreeItem = item
|
|
116
|
+
var column: int = tree_column(tree, row, wanted)
|
|
117
|
+
if column != -1:
|
|
118
|
+
named["column"] = column
|
|
119
|
+
return {"point": placed.get_center(), "scrolled": scrolled}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## Where [param menus] draws the title of [param menu], worked out as the engine works it out,
|
|
123
|
+
## since the bar keeps it to itself: each title shaped in the bar's font, padded by its normal style
|
|
124
|
+
## and spaced by its separation, counting only the menus it shows.
|
|
125
|
+
static func _menu_title_rect(menus: MenuBar, menu: int) -> Rect2:
|
|
126
|
+
var style: StyleBox = menus.get_theme_stylebox("normal")
|
|
127
|
+
var separation: int = menus.get_theme_constant("h_separation")
|
|
128
|
+
var offset: int = 0
|
|
129
|
+
for before: int in menu:
|
|
130
|
+
if not menus.is_menu_hidden(before):
|
|
131
|
+
offset += int(_menu_title_size(menus, before, style).x + separation)
|
|
132
|
+
var size: Vector2 = _menu_title_size(menus, menu, style)
|
|
133
|
+
var x: float = menus.size.x - offset - size.x if menus.is_layout_rtl() else float(offset)
|
|
134
|
+
return Rect2(Vector2(x, 0), size)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
static func _menu_title_size(menus: MenuBar, menu: int, style: StyleBox) -> Vector2:
|
|
138
|
+
var line: TextLine = TextLine.new()
|
|
139
|
+
var _shaped: bool = line.add_string(
|
|
140
|
+
menus.atr(menus.get_menu_title(menu)),
|
|
141
|
+
menus.get_theme_font("font"),
|
|
142
|
+
menus.get_theme_font_size("font_size"),
|
|
143
|
+
menus.language
|
|
144
|
+
)
|
|
145
|
+
return line.get_size() + style.get_minimum_size()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## Scrolls [param control] until it draws [param item], and answers whether it asked anything to
|
|
149
|
+
## move. A tab bar and a tree have a call for it; a list is scrolled by its own bar, by as little as
|
|
150
|
+
## brings the whole entry in.
|
|
151
|
+
static func _scroll_to(control: Control, item: Variant) -> bool:
|
|
152
|
+
var index: int = item if item is int else -1
|
|
153
|
+
var bar: TabBar = control as TabBar
|
|
154
|
+
if bar != null:
|
|
155
|
+
var before: int = bar.get_tab_offset()
|
|
156
|
+
bar.ensure_tab_visible(index)
|
|
157
|
+
return bar.get_tab_offset() != before
|
|
158
|
+
var tree: Tree = control as Tree
|
|
159
|
+
if tree != null:
|
|
160
|
+
var row: TreeItem = item
|
|
161
|
+
var was: float = tree.get_scroll().y
|
|
162
|
+
tree.scroll_to_item(row)
|
|
163
|
+
return tree.get_scroll().y != was
|
|
164
|
+
var list: ItemList = control as ItemList
|
|
165
|
+
if list == null:
|
|
166
|
+
return false
|
|
167
|
+
var rows: VScrollBar = list.get_v_scroll_bar()
|
|
168
|
+
var entry: Rect2 = list.get_item_rect(index, false)
|
|
169
|
+
var top: float = entry.position.y - list.get_theme_stylebox("panel").get_offset().y
|
|
170
|
+
var was_at: float = rows.value
|
|
171
|
+
if top < rows.value:
|
|
172
|
+
rows.value = top
|
|
173
|
+
elif top + entry.size.y > rows.value + rows.page:
|
|
174
|
+
rows.value = top + entry.size.y - rows.page
|
|
175
|
+
return rows.value != was_at
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://mynpfgtjyipe
|
|
@@ -254,12 +254,17 @@ static func can_read(holder: Variant, named: String) -> bool:
|
|
|
254
254
|
## reports an engine error for one of another type, so a step into a map typed by an enum read the
|
|
255
255
|
## right entry and left four errors in the game's log for each of the two walks, and the run was
|
|
256
256
|
## counted as unclean for a read.
|
|
257
|
+
##
|
|
258
|
+
## A step written in double quotes is the key inside them, so a key holding a colon is one step:
|
|
259
|
+
## Alchemill keys its stock by namespaced ids, "stock:\"base:dew\"", and split at that colon the
|
|
260
|
+
## step named a key "base" the map had never had.
|
|
257
261
|
static func _key_in(map: Dictionary, named: String) -> Array:
|
|
258
|
-
var
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
var word: String = key_written(named)
|
|
263
|
+
var spellings: Array = [word, StringName(word)]
|
|
264
|
+
if word.is_valid_int():
|
|
265
|
+
spellings.append(int(word))
|
|
266
|
+
if word.is_valid_float():
|
|
267
|
+
spellings.append(float(word))
|
|
263
268
|
var typed: int = map.get_typed_key_builtin() if map.is_typed_key() else TYPE_NIL
|
|
264
269
|
for key: Variant in spellings:
|
|
265
270
|
if typed != TYPE_NIL and typeof(key) != typed:
|
|
@@ -269,9 +274,35 @@ static func _key_in(map: Dictionary, named: String) -> Array:
|
|
|
269
274
|
return []
|
|
270
275
|
|
|
271
276
|
|
|
277
|
+
## The key a step names: the words inside the quotes of one written in double quotes, read as JSON
|
|
278
|
+
## reads a string so a quote inside is written \", and the step itself otherwise.
|
|
279
|
+
static func key_written(named: String) -> String:
|
|
280
|
+
if named.length() < 2 or not named.begins_with('"') or not named.ends_with('"'):
|
|
281
|
+
return named
|
|
282
|
+
var reader: JSON = JSON.new()
|
|
283
|
+
if reader.parse(named) != OK or not reader.data is String:
|
|
284
|
+
return named
|
|
285
|
+
return reader.data
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
## [param key] written as a step that names it: in double quotes when it holds what a bare step
|
|
289
|
+
## cannot, a colon a path splits at, a bracket that reads as a call, a quote, or nothing at all.
|
|
290
|
+
static func step_naming(key: Variant) -> String:
|
|
291
|
+
var text: String = str(key)
|
|
292
|
+
if not (key is String or key is StringName):
|
|
293
|
+
return text
|
|
294
|
+
if text.is_empty():
|
|
295
|
+
return JSON.stringify(text)
|
|
296
|
+
for character: String in [":", "(", ")", '"']:
|
|
297
|
+
if text.contains(character):
|
|
298
|
+
return JSON.stringify(text)
|
|
299
|
+
return text
|
|
300
|
+
|
|
301
|
+
|
|
272
302
|
## [param reaching] split into its steps at each colon, except one inside a call's brackets or a
|
|
273
|
-
## quoted argument: `get_node("/root/Main"):name` is two steps, and
|
|
274
|
-
## argument holding one became a step that names
|
|
303
|
+
## quoted argument or key: `get_node("/root/Main"):name` is two steps, and so is
|
|
304
|
+
## `stock:"base:dew"`; split at every colon a quoted argument holding one became a step that names
|
|
305
|
+
## nothing.
|
|
275
306
|
static func steps_of(reaching: String) -> PackedStringArray:
|
|
276
307
|
var steps: PackedStringArray = []
|
|
277
308
|
var step: String = ""
|
|
@@ -569,17 +600,19 @@ static func nothing_there(holder: Variant, named: String, called_as: String) ->
|
|
|
569
600
|
var keys: Array = map.keys()
|
|
570
601
|
# A key reading as the step and still not reached is one of a type a step cannot spell, and
|
|
571
602
|
# saying the map has no key 3 while listing 3 among its keys contradicts itself.
|
|
603
|
+
var word: String = key_written(named)
|
|
572
604
|
for key: Variant in keys:
|
|
573
|
-
if str(key) ==
|
|
605
|
+
if str(key) == word:
|
|
574
606
|
return (
|
|
575
607
|
"%s has a key that reads %s, but it is %s, which a path step cannot name"
|
|
576
608
|
% [called_as, named, type_string(typeof(key))]
|
|
577
609
|
)
|
|
578
610
|
# Eight of them, because a map keyed by something unexpected is told by the first few and a
|
|
579
|
-
# map of two hundred would bury the sentence saying which key was missing.
|
|
611
|
+
# map of two hundred would bury the sentence saying which key was missing. Each written as a
|
|
612
|
+
# step reaching it, so the list says how to name a key holding a colon.
|
|
580
613
|
var some: Array[String] = []
|
|
581
614
|
for key: Variant in keys.slice(0, 8):
|
|
582
|
-
some.append(
|
|
615
|
+
some.append(step_naming(key))
|
|
583
616
|
var rest: String = ""
|
|
584
617
|
if keys.size() > 8:
|
|
585
618
|
rest = " and %d more" % [keys.size() - 8]
|
|
@@ -2,6 +2,7 @@ extends RefCounted
|
|
|
2
2
|
|
|
3
3
|
## Which control a click means when it names the control by the words on it rather than by path.
|
|
4
4
|
|
|
5
|
+
const Items = preload("runtime_items.gd")
|
|
5
6
|
const Queries = preload("runtime_queries.gd")
|
|
6
7
|
const Read = preload("reading.gd")
|
|
7
8
|
const Says = preload("runtime_says.gd")
|
|
@@ -24,7 +25,8 @@ const WHOLE: int = 1
|
|
|
24
25
|
|
|
25
26
|
## The control a click by [param wanted] words presses, found under [param root_path] in the tree
|
|
26
27
|
## [param root] heads, or the refusal saying why there is none or which of several:
|
|
27
|
-
## {"path", "found"} or {"type", "message"}
|
|
28
|
+
## {"path", "found"} or {"type", "message"}, with `item` as well when the match is an item of a
|
|
29
|
+
## control drawing a list, which [method Items.item_rect] places.
|
|
28
30
|
##
|
|
29
31
|
## Matched the way a find matches `says`, against a control's own words, and only among controls
|
|
30
32
|
## the player can see. A match inside a button presses the button, because the label is what says
|
|
@@ -57,6 +59,10 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
57
59
|
# that only says them as part of more stands in for.
|
|
58
60
|
var exact_out_of_reach: Array[Node] = []
|
|
59
61
|
var out_of_reach_as: Array[String] = []
|
|
62
|
+
# The item of a control drawing a list that matched, null for a control matched as a whole, and
|
|
63
|
+
# the point in the control a pointer would go to for it.
|
|
64
|
+
var items: Array = []
|
|
65
|
+
var aims: Array[Vector2] = []
|
|
60
66
|
var pending: Array[Node] = [reached["node"]]
|
|
61
67
|
while not pending.is_empty():
|
|
62
68
|
var node: Node = pending.pop_front()
|
|
@@ -66,6 +72,26 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
66
72
|
if _says_exactly(Words.said_by(node), wanted):
|
|
67
73
|
exact_out_of_reach.append(node)
|
|
68
74
|
out_of_reach_as.append("hidden")
|
|
75
|
+
elif Words.draws_a_list(node):
|
|
76
|
+
# Each item a match of its own, so the words of one tab are matched as a whole and the
|
|
77
|
+
# index picks between two items saying them as it does between two buttons.
|
|
78
|
+
var drawing: Control = node
|
|
79
|
+
for entry: Dictionary in Words.items_of(drawing):
|
|
80
|
+
var words: String = entry["says"]
|
|
81
|
+
if not Says.matches(words, wanted):
|
|
82
|
+
continue
|
|
83
|
+
var whole: bool = _says_exactly(words, wanted)
|
|
84
|
+
var drawn_at_now: Rect2 = Items.item_rect(drawing, entry["item"], wanted)
|
|
85
|
+
matched.append(drawing)
|
|
86
|
+
said.append(words)
|
|
87
|
+
exact.append(whole)
|
|
88
|
+
inside.append(not whole and _inside_a_word(words, wanted))
|
|
89
|
+
items.append(entry["item"])
|
|
90
|
+
# An item drawn nowhere yet, scrolled out of its list, is judged at the list's middle,
|
|
91
|
+
# which is where the click scrolls it towards.
|
|
92
|
+
aims.append(
|
|
93
|
+
drawing.size * 0.5 if drawn_at_now.size == Vector2.ZERO else drawn_at_now.get_center()
|
|
94
|
+
)
|
|
69
95
|
else:
|
|
70
96
|
var control: Control = node
|
|
71
97
|
var target: Control = _pressed_through(control)
|
|
@@ -78,6 +104,8 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
78
104
|
said.append(words)
|
|
79
105
|
exact.append(whole)
|
|
80
106
|
inside.append(buried)
|
|
107
|
+
items.append(null)
|
|
108
|
+
aims.append(target.size * 0.5)
|
|
81
109
|
elif (whole and not exact[at]) or (inside[at] and not buried):
|
|
82
110
|
said[at] = words
|
|
83
111
|
exact[at] = whole
|
|
@@ -93,13 +121,13 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
93
121
|
var outside: int = 0
|
|
94
122
|
var open: Array[int] = []
|
|
95
123
|
for index: int in matched.size():
|
|
96
|
-
if _off_screen(matched[index]):
|
|
124
|
+
if _off_screen(matched[index], aims[index]):
|
|
97
125
|
outside += 1
|
|
98
126
|
if exact[index]:
|
|
99
127
|
exact_out_of_reach.append(matched[index])
|
|
100
128
|
out_of_reach_as.append("outside what the screen shows")
|
|
101
129
|
continue
|
|
102
|
-
var over: Node = _cover_of(matched[index], drawn, areas)
|
|
130
|
+
var over: Node = _cover_of(matched[index], drawn, areas, aims[index])
|
|
103
131
|
if over == null:
|
|
104
132
|
open.append(index)
|
|
105
133
|
else:
|
|
@@ -125,12 +153,14 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
125
153
|
var lines: Array[String] = []
|
|
126
154
|
var wholes: Array[bool] = []
|
|
127
155
|
var insides: Array[bool] = []
|
|
156
|
+
var picked_items: Array = []
|
|
128
157
|
for index: int in open:
|
|
129
158
|
targets.append(matched[index])
|
|
130
159
|
ranks.append(_rank(matched[index], exact[index]))
|
|
131
160
|
lines.append(said[index])
|
|
132
161
|
wholes.append(exact[index])
|
|
133
162
|
insides.append(inside[index])
|
|
163
|
+
picked_items.append(items[index])
|
|
134
164
|
|
|
135
165
|
var under: String = "" if root_path == "/root" else " under %s" % root_path
|
|
136
166
|
if targets.is_empty():
|
|
@@ -165,14 +195,19 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
165
195
|
}
|
|
166
196
|
var index: int = 0 if which == null else Read.as_int(which, -1)
|
|
167
197
|
if index < 0 or index >= targets.size():
|
|
168
|
-
|
|
198
|
+
# A number arrives from JSON as a float, and 5 was quoted back as 5.0.
|
|
199
|
+
var given: String = str(index) if index >= 0 else str(which)
|
|
200
|
+
var among: String = (
|
|
201
|
+
'the only match on screen%s saying "%s" is index 0' % [under, wanted]
|
|
202
|
+
if targets.size() == 1
|
|
203
|
+
else (
|
|
204
|
+
'the %d matches on screen%s saying "%s" are 0 to %d'
|
|
205
|
+
% [targets.size(), under, wanted, targets.size() - 1]
|
|
206
|
+
)
|
|
207
|
+
)
|
|
169
208
|
return {
|
|
170
209
|
"type": "error",
|
|
171
|
-
"message":
|
|
172
|
-
(
|
|
173
|
-
'index %s is not one of the %d control%s on screen%s saying "%s": %s'
|
|
174
|
-
% [str(which), targets.size(), plural, under, wanted, _candidates(targets, lines)]
|
|
175
|
-
)
|
|
210
|
+
"message": "index %s picks nothing: %s: %s" % [given, among, _candidates(targets, lines)]
|
|
176
211
|
}
|
|
177
212
|
# A match saying the words as part of more, while another control says exactly them, is a stand-in
|
|
178
213
|
# for that one, not the control meant: a tab reading "Out" sat in a collapsed drawer and a dispatch
|
|
@@ -257,7 +292,11 @@ static func control_saying(root: Node, root_path: String, wanted: String, which:
|
|
|
257
292
|
found["picked"] = "%s; %s" % [_described(1, ranks[0], wanted, under), others]
|
|
258
293
|
if covered > 0:
|
|
259
294
|
found["covered"] = covered
|
|
260
|
-
|
|
295
|
+
var answer: Dictionary = {"path": str(targets[index].get_path()), "found": found}
|
|
296
|
+
if picked_items[index] != null:
|
|
297
|
+
answer["item"] = picked_items[index]
|
|
298
|
+
found["item"] = Items.item_named(targets[index], picked_items[index], lines[index])
|
|
299
|
+
return answer
|
|
261
300
|
|
|
262
301
|
|
|
263
302
|
## How good a match [param target] is for a click: a control the pointer can reach over one it
|
|
@@ -451,7 +490,7 @@ static func _pressed_through(control: Control) -> Control:
|
|
|
451
490
|
## What a pointer at [param target]'s centre would reach instead of it, drawn over it, or null.
|
|
452
491
|
static func drawn_over(target: Control) -> Node:
|
|
453
492
|
var drawn: Array[Node] = _pointer_takers(target.get_tree().root)
|
|
454
|
-
return _cover_of(target, drawn, _areas_of(drawn))
|
|
493
|
+
return _cover_of(target, drawn, _areas_of(drawn), target.size * 0.5)
|
|
455
494
|
|
|
456
495
|
|
|
457
496
|
## Why [param node], which is not visible in the tree, is not, as a parenthesis to follow its path:
|
|
@@ -631,7 +670,8 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
631
670
|
return areas
|
|
632
671
|
|
|
633
672
|
|
|
634
|
-
## What a pointer at [param target]
|
|
673
|
+
## What a pointer at [param aim] in [param target], its centre or the middle of the item a click is
|
|
674
|
+
## for, reaches instead of it, or null when nothing does.
|
|
635
675
|
##
|
|
636
676
|
## The engine's own order for which control takes the pointer: an embedded window over everything
|
|
637
677
|
## in the viewport that holds it, then the higher canvas layer, then the later in the tree. A
|
|
@@ -650,10 +690,10 @@ static func _areas_of(drawn: Array[Node]) -> Array[Rect2]:
|
|
|
650
690
|
## point is passed over before anything costlier is asked of it. A window's box is in the space of
|
|
651
691
|
## the viewport it is embedded in, which is not the target's when the target is in another window,
|
|
652
692
|
## so a window is judged by [method _window_over] instead.
|
|
653
|
-
static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2]) -> Node:
|
|
693
|
+
static func _cover_of(target: Control, drawn: Array[Node], areas: Array[Rect2], aim: Vector2) -> Node:
|
|
654
694
|
var viewport: Viewport = target.get_viewport()
|
|
655
695
|
var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
|
|
656
|
-
var point: Vector2 =
|
|
696
|
+
var point: Vector2 = target.get_global_transform_with_canvas() * aim + _moved_by(target, moves)
|
|
657
697
|
var layer: int = _layer_of(target)
|
|
658
698
|
for at: int in drawn.size():
|
|
659
699
|
var other: Node = drawn[at]
|
|
@@ -770,13 +810,14 @@ static func _where_it_lands(target: Control, point: Vector2) -> Dictionary[Scrol
|
|
|
770
810
|
return moves
|
|
771
811
|
|
|
772
812
|
|
|
773
|
-
## Whether a click could not reach [param target] however it scrolled:
|
|
774
|
-
## would bring it, outside its viewport or the one showing that on the screen, or
|
|
775
|
-
## container the click does not scroll. A drawer parked off the side of the
|
|
776
|
-
## outside the viewport of a game with no window, was listed among the controls
|
|
777
|
-
|
|
813
|
+
## Whether a click could not reach [param aim] in [param target] however it scrolled: the point,
|
|
814
|
+
## where the click would bring it, outside its viewport or the one showing that on the screen, or
|
|
815
|
+
## clipped away by a container the click does not scroll. A drawer parked off the side of the
|
|
816
|
+
## screen, or anything outside the viewport of a game with no window, was listed among the controls
|
|
817
|
+
## on screen.
|
|
818
|
+
static func _off_screen(target: Control, aim: Vector2) -> bool:
|
|
778
819
|
var moves: Dictionary[ScrollContainer, Vector2] = _scrolls_for(target)
|
|
779
|
-
var point: Vector2 =
|
|
820
|
+
var point: Vector2 = target.get_global_transform_with_canvas() * aim + _moved_by(target, moves)
|
|
780
821
|
if _clipped_away(target, point, moves) or not target.get_viewport().get_visible_rect().has_point(point):
|
|
781
822
|
return true
|
|
782
823
|
var reached: Dictionary = Screen.reach(target.get_viewport(), point)
|
|
@@ -477,5 +477,7 @@ func _element(item: Variant, builtin: int, declared: String, script: Script, res
|
|
|
477
477
|
var wanted: String = type_string(builtin)
|
|
478
478
|
if builtin == TYPE_OBJECT:
|
|
479
479
|
wanted = declared if script == null else script_named(script)
|
|
480
|
-
return {
|
|
480
|
+
return {
|
|
481
|
+
"message": "is %s, which cannot become %s" % [JSON.stringify(serialize(item), "", false), wanted]
|
|
482
|
+
}
|
|
481
483
|
return {"value": fitted_item}
|
|
@@ -23,7 +23,7 @@ static func said_by(node: Node) -> String:
|
|
|
23
23
|
var label: Label = node as Label
|
|
24
24
|
if label != null:
|
|
25
25
|
return _label_says(label)
|
|
26
|
-
if
|
|
26
|
+
if draws_a_list(node):
|
|
27
27
|
return "\n".join(lines_said_by(node))
|
|
28
28
|
var own: String = _text_of(node)
|
|
29
29
|
# A control showing a picture rather than words is named for a player who cannot see it, and that
|
|
@@ -41,37 +41,54 @@ static func said_by(node: Node) -> String:
|
|
|
41
41
|
## every label on it and none of those, and a wait for a tab's title or a list item was never met.
|
|
42
42
|
static func lines_said_by(node: Node) -> Array[String]:
|
|
43
43
|
var lines: Array[String] = []
|
|
44
|
+
if not draws_a_list(node):
|
|
45
|
+
_keep(_text_of(node), lines)
|
|
46
|
+
return lines
|
|
47
|
+
var tree: Tree = node as Tree
|
|
48
|
+
if tree != null and tree.column_titles_visible:
|
|
49
|
+
var titles: Array[String] = []
|
|
50
|
+
for column: int in tree.columns:
|
|
51
|
+
_keep(node.atr(tree.get_column_title(column)), titles)
|
|
52
|
+
_keep(" ".join(titles), lines)
|
|
53
|
+
for entry: Dictionary in items_of(node):
|
|
54
|
+
lines.append(str(entry["says"]))
|
|
55
|
+
return lines
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Each item [param node] draws as one of a list, as {"says", "item"}: a tab, a menu title, a list
|
|
59
|
+
## entry or a menu item by its index, and a tree row by its TreeItem. Empty for a node drawing none.
|
|
60
|
+
## Held apart rather than joined, so a click on a tab's title presses that tab: the bar's middle is
|
|
61
|
+
## whichever tab happens to sit there.
|
|
62
|
+
static func items_of(node: Node) -> Array[Dictionary]:
|
|
63
|
+
var items: Array[Dictionary] = []
|
|
44
64
|
if node is TabBar:
|
|
45
65
|
var bar: TabBar = node
|
|
46
66
|
for tab: int in bar.tab_count:
|
|
47
67
|
if not bar.is_tab_hidden(tab):
|
|
48
|
-
|
|
68
|
+
_keep_item(node.atr(bar.get_tab_title(tab)), tab, items)
|
|
49
69
|
elif node is MenuBar:
|
|
50
70
|
var menus: MenuBar = node
|
|
51
71
|
for menu: int in menus.get_menu_count():
|
|
52
72
|
if not menus.is_menu_hidden(menu):
|
|
53
|
-
|
|
73
|
+
_keep_item(node.atr(menus.get_menu_title(menu)), menu, items)
|
|
54
74
|
elif node is ItemList:
|
|
55
75
|
var list: ItemList = node
|
|
56
76
|
for item: int in list.item_count:
|
|
57
|
-
|
|
77
|
+
_keep_item(
|
|
78
|
+
_as_drawn(node, list.get_item_text(item), list.get_item_auto_translate_mode(item)),
|
|
79
|
+
item,
|
|
80
|
+
items
|
|
81
|
+
)
|
|
58
82
|
elif node is PopupMenu:
|
|
59
83
|
# A separator too, by its title: the popup draws a titled one as a heading over the items
|
|
60
84
|
# below it, and one without a title says nothing and is dropped as an empty line.
|
|
61
85
|
var menu: PopupMenu = node
|
|
62
86
|
for item: int in menu.item_count:
|
|
63
|
-
|
|
87
|
+
_keep_item(item_says(menu, item), item, items)
|
|
64
88
|
elif node is Tree:
|
|
65
89
|
var tree: Tree = node
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for column: int in tree.columns:
|
|
69
|
-
_keep(node.atr(tree.get_column_title(column)), titles)
|
|
70
|
-
_keep(" ".join(titles), lines)
|
|
71
|
-
_tree_rows(tree, tree.get_root(), tree.hide_root, lines)
|
|
72
|
-
else:
|
|
73
|
-
_keep(_text_of(node), lines)
|
|
74
|
-
return lines
|
|
90
|
+
_tree_rows(tree, tree.get_root(), tree.hide_root, items)
|
|
91
|
+
return items
|
|
75
92
|
|
|
76
93
|
|
|
77
94
|
## What item [param index] of [param menu] shows: the translation where the item has one. A menu is
|
|
@@ -86,7 +103,9 @@ static func masked(field: LineEdit) -> String:
|
|
|
86
103
|
return field.secret_character.repeat(field.text.length())
|
|
87
104
|
|
|
88
105
|
|
|
89
|
-
|
|
106
|
+
## Whether [param node] draws a list of items rather than holding words in a `text`; see
|
|
107
|
+
## [method items_of].
|
|
108
|
+
static func draws_a_list(node: Node) -> bool:
|
|
90
109
|
return node is TabBar or node is MenuBar or node is ItemList or node is PopupMenu or node is Tree
|
|
91
110
|
|
|
92
111
|
|
|
@@ -96,6 +115,12 @@ static func _keep(line: String, into: Array[String]) -> void:
|
|
|
96
115
|
into.append(trimmed)
|
|
97
116
|
|
|
98
117
|
|
|
118
|
+
static func _keep_item(line: String, item: Variant, into: Array[Dictionary]) -> void:
|
|
119
|
+
var trimmed: String = line.strip_edges()
|
|
120
|
+
if not trimmed.is_empty():
|
|
121
|
+
into.append({"says": trimmed, "item": item})
|
|
122
|
+
|
|
123
|
+
|
|
99
124
|
## [param words] as [param node] draws them, which for a game with translations is the translation
|
|
100
125
|
## rather than the key it holds: a control set to `UI_HIRE` shows "Hire", and a wait for "Hire" was
|
|
101
126
|
## never met. [param mode] is an item's own setting, which the engine reads before the node's.
|
|
@@ -113,14 +138,14 @@ static func _as_drawn(
|
|
|
113
138
|
## The rows of a tree from [param item] down, a row being its columns' text side by side, and
|
|
114
139
|
## nothing under a collapsed row or a hidden one, since neither is drawn. A hidden root is only the
|
|
115
140
|
## place its children hang from.
|
|
116
|
-
static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[
|
|
141
|
+
static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[Dictionary]) -> void:
|
|
117
142
|
if item == null or not item.visible:
|
|
118
143
|
return
|
|
119
144
|
if not skip:
|
|
120
145
|
var cells: Array[String] = []
|
|
121
146
|
for column: int in tree.columns:
|
|
122
|
-
_keep(
|
|
123
|
-
|
|
147
|
+
_keep(cell_says(tree, item, column), cells)
|
|
148
|
+
_keep_item(" ".join(cells), item, into)
|
|
124
149
|
if item.collapsed:
|
|
125
150
|
return
|
|
126
151
|
var child: TreeItem = item.get_first_child()
|
|
@@ -129,6 +154,11 @@ static func _tree_rows(tree: Tree, item: TreeItem, skip: bool, into: Array[Strin
|
|
|
129
154
|
child = child.get_next()
|
|
130
155
|
|
|
131
156
|
|
|
157
|
+
## What [param column] of the row [param item] of [param tree] shows.
|
|
158
|
+
static func cell_says(tree: Tree, item: TreeItem, column: int) -> String:
|
|
159
|
+
return _as_drawn(tree, item.get_text(column), item.get_auto_translate_mode(column))
|
|
160
|
+
|
|
161
|
+
|
|
132
162
|
## What a node with a `text` says, as it is drawn.
|
|
133
163
|
static func _text_of(node: Node) -> String:
|
|
134
164
|
var label: Label = node as Label
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## stores, which runs no script of the scene's, so a read answers the same either way.
|
|
6
6
|
|
|
7
7
|
const FileReads = preload("file_reads.gd")
|
|
8
|
+
const Read = preload("reading.gd")
|
|
8
9
|
const Serialisation = preload("serialisation.gd")
|
|
9
10
|
|
|
10
11
|
const LOOP_MODE_NAMES: Dictionary[int, String] = {
|
|
@@ -60,7 +61,12 @@ static func _bare(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
60
61
|
return [null, read]
|
|
61
62
|
var type: String = str(read.get("type", ""))
|
|
62
63
|
if not ClassDB.class_exists(type) or not ClassDB.is_parent_class(type, kind):
|
|
63
|
-
|
|
64
|
+
var said: String = "%s not found at: %s" % [kind, node_path]
|
|
65
|
+
if read.is_empty() and FileReads.scene_node(scene_path, node_path.get_slice("/", 0)).is_empty():
|
|
66
|
+
said = Read.from_the_root(
|
|
67
|
+
said, node_path, str(FileReads.scene_node(scene_path, ".").get("name", ""))
|
|
68
|
+
)
|
|
69
|
+
return [null, {"ok": false, "error": said}]
|
|
64
70
|
var bare: Node = ClassDB.instantiate(type)
|
|
65
71
|
bare.name = str(read["name"])
|
|
66
72
|
var given: Dictionary = read["properties"]
|
|
@@ -164,7 +170,10 @@ static func state_machine(tree: AnimationTree, state_machine_path: String) -> An
|
|
|
164
170
|
## the path that failed, and what is there instead.
|
|
165
171
|
static func no_machine(tree: AnimationTree, state_machine_path: String) -> String:
|
|
166
172
|
if tree == null or tree.tree_root == null:
|
|
167
|
-
return
|
|
173
|
+
return (
|
|
174
|
+
"The AnimationTree has no tree_root. Make one with resource_edit create of an "
|
|
175
|
+
+ "AnimationNodeStateMachine, then give its res:// path to tree_root with scene_node set."
|
|
176
|
+
)
|
|
168
177
|
var current: AnimationNodeStateMachine = tree.tree_root as AnimationNodeStateMachine
|
|
169
178
|
if current == null:
|
|
170
179
|
return (
|
|
@@ -7,6 +7,7 @@ extends RefCounted
|
|
|
7
7
|
## made here passes the same checks and answers in the same shape. What the engine does that an
|
|
8
8
|
## editor does not, running every script, is put back in scene_file.gd and headless_check.gd.
|
|
9
9
|
|
|
10
|
+
const AnimationTools = preload("../addons/gdharness_editor/tools/animation_tools.gd")
|
|
10
11
|
const ResourceTools = preload("../addons/gdharness_editor/tools/resource_tools.gd")
|
|
11
12
|
const SceneTools = preload("../addons/gdharness_editor/tools/scene_tools.gd")
|
|
12
13
|
|
|
@@ -32,6 +33,12 @@ const RESOURCE_COMMANDS: Array[String] = [
|
|
|
32
33
|
"set_theme_color",
|
|
33
34
|
"set_theme_font_size",
|
|
34
35
|
]
|
|
36
|
+
const ANIMATION_COMMANDS: Array[String] = [
|
|
37
|
+
"create_animation",
|
|
38
|
+
"add_animation_track",
|
|
39
|
+
"add_animation_state",
|
|
40
|
+
"connect_animation_states",
|
|
41
|
+
]
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
## Runs the tool [code]command[/code] names with [code]arguments[/code], answering what it answers.
|
|
@@ -46,6 +53,8 @@ static func run(params: Dictionary) -> Dictionary:
|
|
|
46
53
|
tools = SceneTools.new()
|
|
47
54
|
elif command in RESOURCE_COMMANDS:
|
|
48
55
|
tools = ResourceTools.new()
|
|
56
|
+
elif command in ANIMATION_COMMANDS:
|
|
57
|
+
tools = AnimationTools.new()
|
|
49
58
|
else:
|
|
50
59
|
return {"ok": false, "error": "No editor tool runs headless as " + command}
|
|
51
60
|
var answer: Variant = tools.call(command, arguments)
|
|
@@ -129,7 +129,8 @@ func _answer(operation: String, params: Dictionary, answer_path: String) -> void
|
|
|
129
129
|
)
|
|
130
130
|
quit(1)
|
|
131
131
|
return
|
|
132
|
-
|
|
132
|
+
# Keys in the order they were put in, which for a map read is the order the resource keeps.
|
|
133
|
+
var written: bool = answer.store_string(JSON.stringify(payload, "", false))
|
|
133
134
|
answer.close()
|
|
134
135
|
if not written:
|
|
135
136
|
_log.error("Could not write the answer to " + answer_path)
|