gdharness 1.3.4 → 1.3.6
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 +2520 -936
- package/build/godot/addons/gdharness_editor/declared_values.gd +24 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +5 -18
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +20 -12
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +48 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd +293 -0
- package/build/godot/addons/gdharness_runtime/declared_values.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +29 -1
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +16 -13
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +18 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +32 -5
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +48 -15
- package/build/godot/operations/classdb_queries.gd +112 -46
- package/build/godot/operations/declared_values.gd +24 -0
- package/build/godot/operations/gdscript_authoring.gd +15 -2
- package/build/godot/operations/property_values.gd +5 -18
- package/build/godot/tests/listed.gd +241 -0
- package/build/keeper.js +126 -76
- package/package.json +1 -1
|
@@ -85,6 +85,30 @@ static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
|
85
85
|
return values
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
## [param word] as the number of the value the enum property [param declared] of [param target]
|
|
89
|
+
## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
|
|
90
|
+
## target in the refusal. [false, ""] for a word that is a number, which converts as one.
|
|
91
|
+
##
|
|
92
|
+
## An enum is written by the name of a value as well as its number, since the name is what the script
|
|
93
|
+
## and the inspector show and the number is only where the value happens to sit. Tried as the script
|
|
94
|
+
## spells it, then as the hint does, then as the engine's constant.
|
|
95
|
+
static func enum_by_name(target: Object, declared: Dictionary, word: String, holder: String) -> Array:
|
|
96
|
+
var property: String = str(declared.get("name", ""))
|
|
97
|
+
var choices: Dictionary = enum_of(target, declared)
|
|
98
|
+
var chosen: Variant = enum_named(choices, word)
|
|
99
|
+
if chosen == null:
|
|
100
|
+
chosen = enum_named(enum_values(str(declared.get("hint_string", ""))), word)
|
|
101
|
+
var constants: Dictionary = {}
|
|
102
|
+
if chosen == null:
|
|
103
|
+
constants = engine_constants(target, property)
|
|
104
|
+
chosen = enum_named(constants, word)
|
|
105
|
+
if chosen != null:
|
|
106
|
+
return [true, chosen]
|
|
107
|
+
if word.strip_edges().is_valid_int():
|
|
108
|
+
return [false, ""]
|
|
109
|
+
return [false, enum_refusal(holder, property, word, choices, constants)]
|
|
110
|
+
|
|
111
|
+
|
|
88
112
|
static func enum_refusal(
|
|
89
113
|
holder: String, property: String, word: String, choices: Dictionary, constants: Dictionary = {}
|
|
90
114
|
) -> String:
|
|
@@ -420,25 +420,12 @@ func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
|
|
|
420
420
|
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
421
421
|
if expected_type == TYPE_OBJECT and hint == PROPERTY_HINT_NODE_TYPE:
|
|
422
422
|
return _node_reference(target, property, raw)
|
|
423
|
-
# An enum is written by the name of a value as well as its number, since the name is what the
|
|
424
|
-
# script and the inspector show and the number is only where the value happens to sit.
|
|
425
423
|
if expected_type == TYPE_INT and hint == PROPERTY_HINT_ENUM and typeof(raw) == TYPE_STRING:
|
|
426
|
-
var
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
var constants: Dictionary = {}
|
|
432
|
-
if chosen == null:
|
|
433
|
-
constants = GdhDeclared.engine_constants(target, property)
|
|
434
|
-
chosen = GdhDeclared.enum_named(constants, str(raw))
|
|
435
|
-
if chosen != null:
|
|
436
|
-
return [true, chosen]
|
|
437
|
-
if not str(raw).strip_edges().is_valid_int():
|
|
438
|
-
var why: String = GdhDeclared.enum_refusal(
|
|
439
|
-
GdhPropertyPaths.class_called(target), property, str(raw), choices, constants
|
|
440
|
-
)
|
|
441
|
-
return [false, why]
|
|
424
|
+
var by_name: Array = GdhDeclared.enum_by_name(
|
|
425
|
+
target, declared, str(raw), GdhPropertyPaths.class_called(target)
|
|
426
|
+
)
|
|
427
|
+
if by_name[0] or not str(by_name[1]).is_empty():
|
|
428
|
+
return by_name
|
|
442
429
|
|
|
443
430
|
# A resource-valued property takes the path of one, which is how a caller names a TileSet,
|
|
444
431
|
# a material or a theme: there is no other way to hand a tool a Resource.
|
|
@@ -120,6 +120,7 @@ func _init_tools() -> void:
|
|
|
120
120
|
"play_scene": [_play_tools, "play_scene"],
|
|
121
121
|
"restart_editor": [_play_tools, "restart_editor"],
|
|
122
122
|
"quit_editor": [_play_tools, "quit_editor"],
|
|
123
|
+
"editor_servers": [_play_tools, "editor_servers"],
|
|
123
124
|
"stop_playing": [_play_tools, "stop_playing"],
|
|
124
125
|
"playing_status": [_play_tools, "playing_status"],
|
|
125
126
|
# The script profiler on a game the editor plays
|
|
@@ -177,20 +177,28 @@ func _method_names(script: GDScript) -> Array[String]:
|
|
|
177
177
|
return names
|
|
178
178
|
|
|
179
179
|
|
|
180
|
-
## The script's
|
|
181
|
-
##
|
|
182
|
-
##
|
|
183
|
-
## map does not tell the two apart.
|
|
180
|
+
## The script's constants, those of the scripts it extends included as its methods include theirs,
|
|
181
|
+
## and the keys of each one that is a Dictionary as `Kind.SHORT`, which is how the copy holds a named
|
|
182
|
+
## enum: a stale enum member is denied as a member of the enum rather than of the class. A constant
|
|
183
|
+
## Dictionary that is not an enum is listed the same way, since the map does not tell the two apart.
|
|
184
|
+
## The engine's map holds only the script's own, so a script extending a class_name listed every
|
|
185
|
+
## method it inherited and none of the constants.
|
|
184
186
|
func _constant_names(script: GDScript) -> Array[String]:
|
|
185
187
|
var names: Array[String] = []
|
|
186
|
-
var
|
|
187
|
-
|
|
188
|
-
var
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
var holder: Script = script
|
|
189
|
+
while holder != null:
|
|
190
|
+
var constants: Dictionary = holder.get_script_constant_map()
|
|
191
|
+
for named: Variant in constants:
|
|
192
|
+
var value: Variant = constants[named]
|
|
193
|
+
if not names.has(str(named)):
|
|
194
|
+
names.append(str(named))
|
|
195
|
+
if value is Dictionary:
|
|
196
|
+
var values: Dictionary = value
|
|
197
|
+
for member: Variant in values:
|
|
198
|
+
var listed: String = "%s.%s" % [named, member]
|
|
199
|
+
if not names.has(listed):
|
|
200
|
+
names.append(listed)
|
|
201
|
+
holder = holder.get_base_script()
|
|
194
202
|
names.sort()
|
|
195
203
|
return names
|
|
196
204
|
|
|
@@ -231,6 +231,54 @@ func quit_editor(_args: Dictionary) -> Dictionary:
|
|
|
231
231
|
return {"ok": true, "quitting": true, "saved": true}
|
|
232
232
|
|
|
233
233
|
|
|
234
|
+
## What the editor said as it started each of its servers, and the debug adapter started again when
|
|
235
|
+
## [param args] names it under `rebind`.
|
|
236
|
+
##
|
|
237
|
+
## Godot binds each server once and says whether it could only in the editor's log panel, as a line
|
|
238
|
+
## like `--- Failed to start Debug adapter server on port 6006: Already in use ---`, so those lines
|
|
239
|
+
## are the one account of why a port it names has nothing on it. They are read before anything is
|
|
240
|
+
## started again, so the answer carries the first attempt's reason as well as the next one's.
|
|
241
|
+
##
|
|
242
|
+
## The debug adapter binds as its node enters the tree, which is early in the editor's start and
|
|
243
|
+
## seconds before the language server, and does not try again; taking its node out of the tree and
|
|
244
|
+
## putting it back runs that start once more. The language server starts on the editor's first
|
|
245
|
+
## frame after it is ready and only once, so re-entering the tree would stop it for good, and it is
|
|
246
|
+
## never touched here.
|
|
247
|
+
func editor_servers(args: Dictionary) -> Dictionary:
|
|
248
|
+
var said: PackedStringArray = _servers_said()
|
|
249
|
+
var rebound: bool = false
|
|
250
|
+
if GdhRead.as_bool(args.get("rebind", false)):
|
|
251
|
+
var adapter: Node = _debug_adapter()
|
|
252
|
+
if adapter != null:
|
|
253
|
+
var parent: Node = adapter.get_parent()
|
|
254
|
+
var at: int = adapter.get_index()
|
|
255
|
+
parent.remove_child(adapter)
|
|
256
|
+
parent.add_child(adapter)
|
|
257
|
+
parent.move_child(adapter, at)
|
|
258
|
+
rebound = true
|
|
259
|
+
return {"ok": true, "said": said, "rebound": rebound, "saidSince": _servers_said().slice(said.size())}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
## The lines the editor's log panel holds about its language server and debug adapter, in order.
|
|
263
|
+
static func _servers_said() -> PackedStringArray:
|
|
264
|
+
var lines: PackedStringArray = []
|
|
265
|
+
var root: Node = EditorInterface.get_base_control().get_tree().root
|
|
266
|
+
for panel: Node in root.find_children("*", "EditorLog", true, false):
|
|
267
|
+
for shown: Node in panel.find_children("*", "RichTextLabel", true, false):
|
|
268
|
+
for line: String in (shown as RichTextLabel).get_parsed_text().split("\n", false):
|
|
269
|
+
var stripped: String = line.strip_edges()
|
|
270
|
+
if stripped.begins_with("---") and stripped.contains(" server"):
|
|
271
|
+
var _kept: bool = lines.append(stripped)
|
|
272
|
+
return lines
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## The editor's debug adapter node, found by its engine class, or null in an editor without one.
|
|
276
|
+
static func _debug_adapter() -> Node:
|
|
277
|
+
var root: Node = EditorInterface.get_base_control().get_tree().root
|
|
278
|
+
var found: Array[Node] = root.find_children("*", "DebugAdapterServer", true, false)
|
|
279
|
+
return found[0] if not found.is_empty() else null
|
|
280
|
+
|
|
281
|
+
|
|
234
282
|
## Asked of the tree the editor's own window is in, because this object is not in one.
|
|
235
283
|
static func _leave() -> void:
|
|
236
284
|
var base: Control = EditorInterface.get_base_control()
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## What a property declares beyond its type: the values of its enum and the range it is exported
|
|
5
|
+
## with, for taking a value by name and for saying when a value written lies outside them.
|
|
6
|
+
|
|
7
|
+
const GdhRead = preload("reading.gd")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## The values an enum hint names, by name, from its hint string: "SWORD:0,SHIELD:1" as a script
|
|
11
|
+
## declares one, or "Add,Subtract" as an engine property does, each name then taking the number
|
|
12
|
+
## after the one before it.
|
|
13
|
+
static func enum_values(hint_string: String) -> Dictionary:
|
|
14
|
+
var values: Dictionary = {}
|
|
15
|
+
var next: int = 0
|
|
16
|
+
for entry: String in hint_string.split(",", false):
|
|
17
|
+
var name: String = entry.get_slice(":", 0).strip_edges()
|
|
18
|
+
var number: String = entry.get_slice(":", 1).strip_edges() if ":" in entry else ""
|
|
19
|
+
var value: int = number.to_int() if number.is_valid_int() else next
|
|
20
|
+
values[name] = value
|
|
21
|
+
next = value + 1
|
|
22
|
+
return values
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## The values of the enum [param declared] declares, by name, spelt as the script of [param target]
|
|
26
|
+
## spells them when it declares that enum. The engine's hint capitalises each name, "SWORD" as
|
|
27
|
+
## "Sword", and a refusal listing those named values the script never spells.
|
|
28
|
+
static func enum_of(target: Object, declared: Dictionary) -> Dictionary:
|
|
29
|
+
var hinted: Dictionary = enum_values(str(declared.get("hint_string", "")))
|
|
30
|
+
var attached: Variant = target.get_script()
|
|
31
|
+
if not attached is Script:
|
|
32
|
+
return hinted
|
|
33
|
+
var script: Script = attached
|
|
34
|
+
for constant: Variant in script.get_script_constant_map().values():
|
|
35
|
+
if not constant is Dictionary:
|
|
36
|
+
continue
|
|
37
|
+
var values: Dictionary = constant
|
|
38
|
+
if values.size() != hinted.size():
|
|
39
|
+
continue
|
|
40
|
+
var spelt: Dictionary = {}
|
|
41
|
+
for name: Variant in values:
|
|
42
|
+
if hinted.get(str(name).capitalize()) == values[name]:
|
|
43
|
+
spelt[str(name)] = values[name]
|
|
44
|
+
if spelt.size() == hinted.size():
|
|
45
|
+
return spelt
|
|
46
|
+
return hinted
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## The number [param word] names among [param choices], matched as written and then ignoring case,
|
|
50
|
+
## or null when it names none.
|
|
51
|
+
static func enum_named(choices: Dictionary, word: String) -> Variant:
|
|
52
|
+
var wanted: String = word.strip_edges()
|
|
53
|
+
if choices.has(wanted):
|
|
54
|
+
return choices[wanted]
|
|
55
|
+
for name: String in choices:
|
|
56
|
+
if name.to_lower() == wanted.to_lower():
|
|
57
|
+
return choices[name]
|
|
58
|
+
return null
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## The values of the engine enum [param property] of [param target] holds, by the constants its
|
|
62
|
+
## class declares them as: blend_mode's Add is BLEND_MODE_ADD in a script, and the hint gives only
|
|
63
|
+
## the inspector's word. Empty for an enum a script declares.
|
|
64
|
+
##
|
|
65
|
+
## The enum is named by the setter's argument, as Node.ProcessMode, since the property list gives
|
|
66
|
+
## an engine enum property no class of its own.
|
|
67
|
+
static func engine_constants(target: Object, property: String) -> Dictionary:
|
|
68
|
+
var values: Dictionary = {}
|
|
69
|
+
var engine_class: String = target.get_class()
|
|
70
|
+
var setter: String = str(ClassDB.class_get_property_setter(engine_class, property))
|
|
71
|
+
if setter.is_empty():
|
|
72
|
+
return values
|
|
73
|
+
var named: String = ""
|
|
74
|
+
for method: Dictionary in ClassDB.class_get_method_list(engine_class):
|
|
75
|
+
var arguments: Array = method.get("args", [])
|
|
76
|
+
if str(method.get("name", "")) == setter and not arguments.is_empty():
|
|
77
|
+
var value_argument: Dictionary = arguments[-1]
|
|
78
|
+
named = str(value_argument.get("class_name", ""))
|
|
79
|
+
break
|
|
80
|
+
var holder: String = named.get_slice(".", 0)
|
|
81
|
+
if named.count(".") != 1 or not ClassDB.class_exists(holder):
|
|
82
|
+
return values
|
|
83
|
+
for constant: String in ClassDB.class_get_enum_constants(holder, named.get_slice(".", 1)):
|
|
84
|
+
values[constant] = ClassDB.class_get_integer_constant(holder, constant)
|
|
85
|
+
return values
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## [param word] as the number of the value the enum property [param declared] of [param target]
|
|
89
|
+
## names by it, as [true, number], or [false, why] naming the values, with [param holder] naming the
|
|
90
|
+
## target in the refusal. [false, ""] for a word that is a number, which converts as one.
|
|
91
|
+
##
|
|
92
|
+
## An enum is written by the name of a value as well as its number, since the name is what the script
|
|
93
|
+
## and the inspector show and the number is only where the value happens to sit. Tried as the script
|
|
94
|
+
## spells it, then as the hint does, then as the engine's constant.
|
|
95
|
+
static func enum_by_name(target: Object, declared: Dictionary, word: String, holder: String) -> Array:
|
|
96
|
+
var property: String = str(declared.get("name", ""))
|
|
97
|
+
var choices: Dictionary = enum_of(target, declared)
|
|
98
|
+
var chosen: Variant = enum_named(choices, word)
|
|
99
|
+
if chosen == null:
|
|
100
|
+
chosen = enum_named(enum_values(str(declared.get("hint_string", ""))), word)
|
|
101
|
+
var constants: Dictionary = {}
|
|
102
|
+
if chosen == null:
|
|
103
|
+
constants = engine_constants(target, property)
|
|
104
|
+
chosen = enum_named(constants, word)
|
|
105
|
+
if chosen != null:
|
|
106
|
+
return [true, chosen]
|
|
107
|
+
if word.strip_edges().is_valid_int():
|
|
108
|
+
return [false, ""]
|
|
109
|
+
return [false, enum_refusal(holder, property, word, choices, constants)]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
static func enum_refusal(
|
|
113
|
+
holder: String, property: String, word: String, choices: Dictionary, constants: Dictionary = {}
|
|
114
|
+
) -> String:
|
|
115
|
+
var ways: String = "by name or by number"
|
|
116
|
+
if not constants.is_empty():
|
|
117
|
+
ways = "by name, by number or by the engine's constant, such as %s" % str(constants.keys()[0])
|
|
118
|
+
var said: String = (
|
|
119
|
+
"%s.%s is an enum, and %s is none of its values: send %s %s"
|
|
120
|
+
% [holder, property, word, _listed(choices), ways]
|
|
121
|
+
)
|
|
122
|
+
var named: PackedStringArray = PackedStringArray(choices.keys())
|
|
123
|
+
named.append_array(PackedStringArray(constants.keys()))
|
|
124
|
+
var close: PackedStringArray = GdhRead.nearest(word, named)
|
|
125
|
+
if close.is_empty():
|
|
126
|
+
return said + "."
|
|
127
|
+
return said + ". The nearest %s %s." % ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
## Why [param value], shown as [param shown], lies outside what [param declared] allows, an export
|
|
131
|
+
## range or an enum's values, or "" when it does not. A range marked or_greater or or_less is open
|
|
132
|
+
## on that side.
|
|
133
|
+
static func outside(target: Object, declared: Dictionary, value: Variant, shown: String) -> String:
|
|
134
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
135
|
+
var hint_string: String = str(declared.get("hint_string", ""))
|
|
136
|
+
var property: String = str(declared.get("name", ""))
|
|
137
|
+
if hint == PROPERTY_HINT_ENUM and (typeof(value) == TYPE_STRING or typeof(value) == TYPE_STRING_NAME):
|
|
138
|
+
var names: PackedStringArray = PackedStringArray(enum_values(hint_string).keys())
|
|
139
|
+
if names.is_empty() or names.has(str(value)):
|
|
140
|
+
return ""
|
|
141
|
+
return (
|
|
142
|
+
(
|
|
143
|
+
"%s is none of the names %s lists: %s, which the inspector holds it to; the engine "
|
|
144
|
+
+ "stores it as given."
|
|
145
|
+
)
|
|
146
|
+
% [shown, property, ", ".join(names)]
|
|
147
|
+
)
|
|
148
|
+
if typeof(value) != TYPE_INT and typeof(value) != TYPE_FLOAT:
|
|
149
|
+
return ""
|
|
150
|
+
var number: float = GdhRead.as_float(value)
|
|
151
|
+
if hint == PROPERTY_HINT_ENUM and typeof(value) == TYPE_INT:
|
|
152
|
+
var choices: Dictionary = enum_of(target, declared)
|
|
153
|
+
if choices.is_empty() or choices.values().has(value):
|
|
154
|
+
return ""
|
|
155
|
+
return "%d is none of the values %s declares: %s." % [value, property, _listed(choices)]
|
|
156
|
+
if hint != PROPERTY_HINT_RANGE:
|
|
157
|
+
return ""
|
|
158
|
+
var parts: PackedStringArray = []
|
|
159
|
+
for part: String in hint_string.split(","):
|
|
160
|
+
var _added: bool = parts.append(part.strip_edges())
|
|
161
|
+
if parts.size() < 2 or not parts[0].is_valid_float() or not parts[1].is_valid_float():
|
|
162
|
+
return ""
|
|
163
|
+
var low: float = parts[0].to_float()
|
|
164
|
+
var high: float = parts[1].to_float()
|
|
165
|
+
if (number <= high or parts.has("or_greater")) and (number >= low or parts.has("or_less")):
|
|
166
|
+
return ""
|
|
167
|
+
return (
|
|
168
|
+
(
|
|
169
|
+
"%s is outside the range %s declares, %s to %s, which the inspector holds it to; the "
|
|
170
|
+
+ "engine stores it as given."
|
|
171
|
+
)
|
|
172
|
+
% [shown, property, _bound(low), _bound(high)]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
static func _listed(choices: Dictionary) -> String:
|
|
177
|
+
var listed: PackedStringArray = []
|
|
178
|
+
for name: String in choices:
|
|
179
|
+
var _added: bool = listed.append("%s (%d)" % [name, choices[name]])
|
|
180
|
+
return ", ".join(listed)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
## A range's end as the script wrote it: the engine's hint gives @export_range(0, 10) as 0.0 and 10.0.
|
|
184
|
+
static func _bound(end: float) -> String:
|
|
185
|
+
return str(int(end)) if end == floorf(end) and absf(end) < 1e15 else str(end)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
## Puts back on [param built] what its script set as it was attached and was not among
|
|
189
|
+
## [param wanted], to the defaults it declares, outside the editor. Attaching runs the script's _init
|
|
190
|
+
## there, which the editor never runs for a script that is not a tool, so a resource_name _init chose
|
|
191
|
+
## was saved as if asked for. A property whose default the engine cannot report is left as made.
|
|
192
|
+
static func unmade(built: Resource, wanted: Dictionary) -> void:
|
|
193
|
+
var attached: Variant = built.get_script()
|
|
194
|
+
if Engine.is_editor_hint() or not attached is Script:
|
|
195
|
+
return
|
|
196
|
+
var script: Script = attached
|
|
197
|
+
var asked: Dictionary = {}
|
|
198
|
+
for key: Variant in wanted:
|
|
199
|
+
asked[str(key).get_slice(":", 0)] = true
|
|
200
|
+
for entry: Dictionary in built.get_property_list():
|
|
201
|
+
var name: String = str(entry["name"])
|
|
202
|
+
var usage: int = GdhRead.as_int(entry["usage"])
|
|
203
|
+
if usage & PROPERTY_USAGE_STORAGE == 0 or name == "script" or asked.has(name):
|
|
204
|
+
continue
|
|
205
|
+
var declared: Variant = ClassDB.class_get_property_default_value(built.get_class(), name)
|
|
206
|
+
if usage & PROPERTY_USAGE_SCRIPT_VARIABLE:
|
|
207
|
+
declared = script.get_property_default_value(name)
|
|
208
|
+
var held: Variant = built.get(name)
|
|
209
|
+
if declared != null and not (typeof(held) == typeof(declared) and held == declared):
|
|
210
|
+
built.set(name, declared)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
## The element types a typed Array or Dictionary property declares: [element] for an array,
|
|
214
|
+
## [key, value] for a dictionary, [] when nothing is declared.
|
|
215
|
+
##
|
|
216
|
+
## Read from the declaration because the value cannot say: a script's exported `Array[int]` sits
|
|
217
|
+
## behind a placeholder in the editor, and measured on 4.7.2 that placeholder stores [1.5, 2.0]
|
|
218
|
+
## as given, which the game then fails to load into the typed array.
|
|
219
|
+
static func elements_of(declared: Dictionary) -> Array:
|
|
220
|
+
var type: int = GdhRead.as_int(declared.get("type", TYPE_NIL), TYPE_NIL)
|
|
221
|
+
var hint: int = GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE))
|
|
222
|
+
var wanted: String = str(declared.get("hint_string", ""))
|
|
223
|
+
if wanted.is_empty():
|
|
224
|
+
return []
|
|
225
|
+
if hint == PROPERTY_HINT_TYPE_STRING:
|
|
226
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
227
|
+
if type == TYPE_ARRAY and parts.size() == 1:
|
|
228
|
+
return [_type_in_hint(parts[0])]
|
|
229
|
+
if type == TYPE_DICTIONARY and parts.size() == 2:
|
|
230
|
+
return [_type_in_hint(parts[0]), _type_in_hint(parts[1])]
|
|
231
|
+
return []
|
|
232
|
+
if hint == PROPERTY_HINT_ARRAY_TYPE and type == TYPE_ARRAY:
|
|
233
|
+
return [_type_named(wanted)]
|
|
234
|
+
if hint == PROPERTY_HINT_DICTIONARY_TYPE and type == TYPE_DICTIONARY:
|
|
235
|
+
var parts: PackedStringArray = wanted.split(";")
|
|
236
|
+
if parts.size() == 2:
|
|
237
|
+
return [_type_named(parts[0]), _type_named(parts[1])]
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
## The type at the front of one entry of a type-string hint, "2:" or "24/17:Texture2D".
|
|
242
|
+
static func _type_in_hint(entry: String) -> int:
|
|
243
|
+
var number: String = entry.get_slice(":", 0).get_slice("/", 0)
|
|
244
|
+
return number.to_int() if number.is_valid_int() else TYPE_NIL
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
## The Variant type [param name] names, or TYPE_OBJECT for a class.
|
|
248
|
+
static func _type_named(name: String) -> int:
|
|
249
|
+
var named: String = name.get_slice(":", 0).strip_edges()
|
|
250
|
+
for type: int in TYPE_MAX:
|
|
251
|
+
if type_string(type) == named:
|
|
252
|
+
return type
|
|
253
|
+
return TYPE_OBJECT if not named.is_empty() else TYPE_NIL
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## The class the objects of a typed list or map hold, and whether they are nodes, as
|
|
257
|
+
## [class, nodes], read from entry [param at] of [param declared]'s hint: "24/17:Item" in a type
|
|
258
|
+
## string, "Item" in an array or dictionary type.
|
|
259
|
+
static func object_kind(declared: Dictionary, at: int) -> Array:
|
|
260
|
+
var entry: String = str(declared.get("hint_string", "")).get_slice(";", at)
|
|
261
|
+
if GdhRead.as_int(declared.get("hint", PROPERTY_HINT_NONE)) != PROPERTY_HINT_TYPE_STRING:
|
|
262
|
+
return [entry.strip_edges(), false]
|
|
263
|
+
var nodes: bool = entry.get_slice(":", 0).get_slice("/", 1) == str(PROPERTY_HINT_NODE_TYPE)
|
|
264
|
+
return [entry.get_slice(":", 1), nodes]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
## The base class and script that type a list or map holding [param wanted], as [class, script],
|
|
268
|
+
## or [] when nothing in the project or the engine is called that.
|
|
269
|
+
static func object_typing(wanted: String) -> Array:
|
|
270
|
+
if ClassDB.class_exists(wanted):
|
|
271
|
+
return [StringName(wanted), null]
|
|
272
|
+
var script: Script = script_class(wanted)
|
|
273
|
+
return [] if script == null else [script.get_instance_base_type(), script]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
## The script declaring the class_name [param name], or null.
|
|
277
|
+
static func script_class(name: String) -> Script:
|
|
278
|
+
for entry: Dictionary in ProjectSettings.get_global_class_list():
|
|
279
|
+
if str(entry.get("class", "")) == name:
|
|
280
|
+
return load(str(entry.get("path", ""))) as Script
|
|
281
|
+
return null
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
## Whether [param value] is the class [param name] names, an engine class or a script's class_name.
|
|
285
|
+
static func is_a(value: Object, name: String) -> bool:
|
|
286
|
+
if value.is_class(name):
|
|
287
|
+
return true
|
|
288
|
+
var script: Script = value.get_script()
|
|
289
|
+
while script != null:
|
|
290
|
+
if str(script.get_global_name()) == name:
|
|
291
|
+
return true
|
|
292
|
+
script = script.get_base_script()
|
|
293
|
+
return false
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c2r5ain6am9gy
|
|
@@ -360,6 +360,10 @@ func _process(_delta: float) -> void:
|
|
|
360
360
|
break
|
|
361
361
|
_clients.append(client)
|
|
362
362
|
_send_welcome(client)
|
|
363
|
+
# Out before the request that came with it is read: a request can hold the game in a
|
|
364
|
+
# breakpoint before the frame's drain, and the server, never welcomed, took a game held
|
|
365
|
+
# inside its call for one that had not taken it. A client gone already is dropped below.
|
|
366
|
+
var _welcomed: bool = _drain(client)
|
|
363
367
|
|
|
364
368
|
var gone: Array[StreamPeerTCP] = []
|
|
365
369
|
for client: StreamPeerTCP in _clients:
|
|
@@ -5,6 +5,7 @@ extends RefCounted
|
|
|
5
5
|
## back what the engine did rather than trusting that it did it.
|
|
6
6
|
|
|
7
7
|
const GdhCallErrors = preload("call_errors.gd")
|
|
8
|
+
const GdhDeclared = preload("declared_values.gd")
|
|
8
9
|
const GdhPaths = preload("runtime_paths.gd")
|
|
9
10
|
const GdhRead = preload("reading.gd")
|
|
10
11
|
const GdhValues = preload("runtime_values.gd")
|
|
@@ -146,6 +147,15 @@ func set_property(params: Dictionary) -> Dictionary:
|
|
|
146
147
|
}
|
|
147
148
|
if already and unmoved:
|
|
148
149
|
answer["unchanged"] = true
|
|
150
|
+
# The engine stores a number past an export range, or one no enum value has, as given; only the
|
|
151
|
+
# inspector holds a property to them, so the game now holds a value its own script never expected.
|
|
152
|
+
if holder is Object:
|
|
153
|
+
var owner: Object = holder
|
|
154
|
+
var shown: String = JSON.stringify(_values.serialize(now), "", false)
|
|
155
|
+
var declaration: Dictionary = slot["declared"]
|
|
156
|
+
var outside: String = GdhDeclared.outside(owner, declaration, now, shown)
|
|
157
|
+
if not outside.is_empty():
|
|
158
|
+
answer["outside_declared"] = outside
|
|
149
159
|
return answer
|
|
150
160
|
|
|
151
161
|
|
|
@@ -240,6 +250,15 @@ func _value_for(
|
|
|
240
250
|
var declared: int = slot["type"]
|
|
241
251
|
var declared_script: Script = slot["script"]
|
|
242
252
|
var wanted: int = declared if declared != TYPE_NIL else typeof(old_value)
|
|
253
|
+
var declaration: Dictionary = slot["declared"]
|
|
254
|
+
var enum_hint: bool = GdhRead.as_int(declaration.get("hint", PROPERTY_HINT_NONE)) == PROPERTY_HINT_ENUM
|
|
255
|
+
if declared == TYPE_INT and enum_hint and value is String and reached["holder"] is Object:
|
|
256
|
+
var owner: Object = reached["holder"]
|
|
257
|
+
var by_name: Array = GdhDeclared.enum_by_name(owner, declaration, str(value), called)
|
|
258
|
+
if by_name[0]:
|
|
259
|
+
return {"value": by_name[1]}
|
|
260
|
+
if not str(by_name[1]).is_empty():
|
|
261
|
+
return {"type": "error", "message": by_name[1]}
|
|
243
262
|
var holds_objects: bool = (
|
|
244
263
|
declared == TYPE_OBJECT or (declared == TYPE_NIL and (old_value == null or old_value is Object))
|
|
245
264
|
)
|
|
@@ -433,8 +452,15 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
433
452
|
OS.add_logger(caught)
|
|
434
453
|
var began: int = Time.get_ticks_usec()
|
|
435
454
|
var result: Variant = holder.callv(named, deserialized_args)
|
|
436
|
-
var elapsed: int = Time.get_ticks_usec() - began
|
|
437
455
|
OS.remove_logger(caught)
|
|
456
|
+
# A method that awaits hands back its state at the first await and its value only when it ends,
|
|
457
|
+
# so the answer waits for the value; one that takes longer than the server waits is collected by
|
|
458
|
+
# its requestId like any slow call.
|
|
459
|
+
var state: Object = result if result is Object else null
|
|
460
|
+
var awaited: bool = state != null and state.get_class() == "GDScriptFunctionState"
|
|
461
|
+
if awaited:
|
|
462
|
+
result = await Signal(state, &"completed")
|
|
463
|
+
var elapsed: int = Time.get_ticks_usec() - began
|
|
438
464
|
|
|
439
465
|
if not caught.refused.is_empty():
|
|
440
466
|
var message: String = (
|
|
@@ -463,6 +489,8 @@ func call_method(params: Dictionary) -> Dictionary:
|
|
|
463
489
|
"result": _values.serialize(result),
|
|
464
490
|
"elapsed_usec": elapsed
|
|
465
491
|
}
|
|
492
|
+
if awaited:
|
|
493
|
+
answer["awaited"] = true
|
|
466
494
|
_keep_reported(answer, "errors", caught.raised)
|
|
467
495
|
_keep_reported(answer, "warnings", caught.warned)
|
|
468
496
|
var asked: Variant = params.get("properties")
|
|
@@ -190,12 +190,7 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
190
190
|
var keycode_raw: Variant = params.get("keycode", 0)
|
|
191
191
|
var held: bool = GdhRead.as_bool(params.get("pressed", true), true)
|
|
192
192
|
var whole: bool = not params.has("pressed")
|
|
193
|
-
var
|
|
194
|
-
|
|
195
|
-
if keycode_raw is String:
|
|
196
|
-
var named: String = keycode_raw
|
|
197
|
-
if not named.is_empty() and key_label.is_empty():
|
|
198
|
-
key_label = named
|
|
193
|
+
var named: String = keycode_raw if keycode_raw is String else ""
|
|
199
194
|
var keycode: int = 0 if keycode_raw is String else GdhRead.as_int(keycode_raw)
|
|
200
195
|
var holding: Variant = GdhValues.held_for(params)
|
|
201
196
|
if holding is String:
|
|
@@ -205,14 +200,17 @@ func inject_key(params: Dictionary) -> Dictionary:
|
|
|
205
200
|
var event: InputEventKey = InputEventKey.new()
|
|
206
201
|
event.pressed = whole or held
|
|
207
202
|
|
|
208
|
-
if not
|
|
209
|
-
event.keycode = OS.find_keycode_from_string(
|
|
203
|
+
if not named.is_empty():
|
|
204
|
+
event.keycode = OS.find_keycode_from_string(named)
|
|
210
205
|
if event.keycode == KEY_NONE:
|
|
211
|
-
return {"type": "error", "message":
|
|
206
|
+
return {"type": "error", "message": GdhValues.unknown_key(named)}
|
|
212
207
|
elif keycode > 0:
|
|
213
208
|
event.keycode = keycode as Key
|
|
214
209
|
else:
|
|
215
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
"type": "error",
|
|
212
|
+
"message": 'keycode is required: a key name, such as "Space" or "A", or its Godot keycode.'
|
|
213
|
+
}
|
|
216
214
|
|
|
217
215
|
# A key event from a real keyboard carries all three, and InputMap consults whichever one
|
|
218
216
|
# the bound event declares: keycode first, then physical_keycode, then key_label. An
|
|
@@ -306,7 +304,8 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
306
304
|
Input.parse_input_event(_button(position, button, false, false))
|
|
307
305
|
await _host.get_tree().process_frame
|
|
308
306
|
|
|
309
|
-
|
|
307
|
+
var at: Array[Vector2] = [position]
|
|
308
|
+
var said: Dictionary = _pointer_noted(
|
|
310
309
|
_hover_noted(
|
|
311
310
|
{
|
|
312
311
|
"type": "input_injected",
|
|
@@ -322,6 +321,7 @@ func inject_mouse_click(params: Dictionary) -> Dictionary:
|
|
|
322
321
|
supplied
|
|
323
322
|
)
|
|
324
323
|
)
|
|
324
|
+
return GdhMouse.outside_noted(said, _host.get_viewport(), at)
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
## Moves the pointer to a position, with the movement the event carries taken from where the
|
|
@@ -346,7 +346,8 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
346
346
|
var supplied: bool = _follow_in_the_window(position)
|
|
347
347
|
Input.parse_input_event(_motion(position, relative))
|
|
348
348
|
|
|
349
|
-
|
|
349
|
+
var at: Array[Vector2] = [position]
|
|
350
|
+
var said: Dictionary = _pointer_noted(
|
|
350
351
|
_hover_noted(
|
|
351
352
|
{
|
|
352
353
|
"type": "input_injected",
|
|
@@ -357,6 +358,7 @@ func inject_mouse_motion(params: Dictionary) -> Dictionary:
|
|
|
357
358
|
supplied
|
|
358
359
|
)
|
|
359
360
|
)
|
|
361
|
+
return GdhMouse.outside_noted(said, _host.get_viewport(), at)
|
|
360
362
|
|
|
361
363
|
|
|
362
364
|
## Drags with a mouse button from one point to another, through any points between, as a hand does:
|
|
@@ -406,7 +408,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
|
|
|
406
408
|
Input.parse_input_event(_button(end, button, false, false))
|
|
407
409
|
await _host.get_tree().process_frame
|
|
408
410
|
|
|
409
|
-
|
|
411
|
+
var said: Dictionary = _pointer_noted(
|
|
410
412
|
_hover_noted(
|
|
411
413
|
{
|
|
412
414
|
"type": "input_injected",
|
|
@@ -420,6 +422,7 @@ func inject_mouse_drag(params: Dictionary) -> Dictionary:
|
|
|
420
422
|
supplied
|
|
421
423
|
)
|
|
422
424
|
)
|
|
425
|
+
return GdhMouse.outside_noted(said, _host.get_viewport(), points)
|
|
423
426
|
|
|
424
427
|
|
|
425
428
|
## Whether [param centre] is somewhere a click can reach it: inside the viewport, and inside every
|
|
@@ -65,6 +65,24 @@ static func mask_of(button: int) -> int:
|
|
|
65
65
|
return 1 << (button - 1)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
+
## [param answer], saying so when one of [param points] lies outside [param viewport]: the GUI picks
|
|
69
|
+
## what is under the pointer only inside it, so such a press or motion reaches no control, and its
|
|
70
|
+
## answer read the same as one that landed.
|
|
71
|
+
static func outside_noted(answer: Dictionary, viewport: Viewport, points: Array[Vector2]) -> Dictionary:
|
|
72
|
+
var shown: Rect2 = viewport.get_visible_rect()
|
|
73
|
+
for point: Vector2 in points:
|
|
74
|
+
if not shown.has_point(point):
|
|
75
|
+
answer["outside_viewport"] = (
|
|
76
|
+
(
|
|
77
|
+
"%s is outside the window's viewport, %s, so no control there receives it; the game's"
|
|
78
|
+
% [point, shown]
|
|
79
|
+
)
|
|
80
|
+
+ " _input and _unhandled_input still do."
|
|
81
|
+
)
|
|
82
|
+
break
|
|
83
|
+
return answer
|
|
84
|
+
|
|
85
|
+
|
|
68
86
|
## What to say about a name that is not a mouse button, with the ones that are.
|
|
69
87
|
static func no_such_button(named: Variant) -> Dictionary:
|
|
70
88
|
return {
|