gdharness 1.3.4 → 1.3.5

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.
@@ -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 choices: Dictionary = GdhDeclared.enum_of(target, declared)
427
- var chosen: Variant = GdhDeclared.enum_named(choices, str(raw))
428
- if chosen == null:
429
- var hinted: Dictionary = GdhDeclared.enum_values(str(declared.get("hint_string", "")))
430
- chosen = GdhDeclared.enum_named(hinted, str(raw))
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.
@@ -177,20 +177,28 @@ func _method_names(script: GDScript) -> Array[String]:
177
177
  return names
178
178
 
179
179
 
180
- ## The script's own constants, and the keys of each one that is a Dictionary as `Kind.SHORT`, which
181
- ## is how the copy holds a named enum: a stale enum member is denied as a member of the enum rather
182
- ## than of the class. A constant Dictionary that is not an enum is listed the same way, since the
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 constants: Dictionary = script.get_script_constant_map()
187
- for named: Variant in constants:
188
- var value: Variant = constants[named]
189
- names.append(str(named))
190
- if value is Dictionary:
191
- var values: Dictionary = value
192
- for member: Variant in values:
193
- names.append("%s.%s" % [named, member])
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
 
@@ -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 key_label: String = str(params.get("key_label", ""))
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 key_label.is_empty():
209
- event.keycode = OS.find_keycode_from_string(key_label)
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": "Invalid key_label: " + key_label}
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 {"type": "error", "message": "keycode or key_label required"}
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
- return _pointer_noted(
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
- return _pointer_noted(
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
- return _pointer_noted(
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 {
@@ -156,6 +156,28 @@ static func glyph_of(keycode: Key, shifted: bool) -> int:
156
156
  return keycode
157
157
 
158
158
 
159
+ ## The refusal for [param name], which no key is called, with the key names nearest it. Read from the
160
+ ## engine's own names, printable keys and the special ones above [constant KEY_SPECIAL], since the
161
+ ## game takes any name OS.find_keycode_from_string does.
162
+ static func unknown_key(name: String) -> String:
163
+ var names: PackedStringArray = []
164
+ for code: int in range(32, 127) + range(KEY_SPECIAL + 1, KEY_SPECIAL + 0x100):
165
+ var spelled: String = OS.get_keycode_string(code as Key)
166
+ if not spelled.is_empty() and OS.find_keycode_from_string(spelled) == code:
167
+ var _named: bool = names.append(spelled)
168
+ var close: PackedStringArray = GdhRead.nearest(name, names)
169
+ var said: String = "No key is called %s." % name
170
+ if close.size() == 1:
171
+ said += " The nearest key name is %s." % close[0]
172
+ elif not close.is_empty():
173
+ said += " The nearest key names are %s." % ", ".join(close)
174
+ return (
175
+ said
176
+ + ' A key is named as Godot names it, such as "Space", "Enter", "Escape", "A" or "F1",'
177
+ + " or given by its keycode."
178
+ )
179
+
180
+
159
181
  ## What [param path] names: a node under [param root], as [method node_at] reads it, or an engine
160
182
  ## singleton by its name. Answers the object under "object", and under "node" the node that objects
161
183
  ## named in a value are found from, which for a singleton is [param root].
@@ -257,9 +279,11 @@ func parameter_class(object: Object, method: String, index: int) -> String:
257
279
 
258
280
 
259
281
  ## What the slot [param named] on [param holder] declares it holds, as `{"type": int, "class":
260
- ## String, "script": Script or null}`: a property's declaration, or the element type of a typed list
261
- ## or the value type of a typed map. TYPE_NIL when nothing is declared, which is also what a
262
- ## property declared as Variant or left untyped reads as: either way it takes anything.
282
+ ## String, "script": Script or null, "declared": Dictionary}`: a property's declaration, or the
283
+ ## element type of a typed list or the value type of a typed map. TYPE_NIL when nothing is declared,
284
+ ## which is also what a property declared as Variant or left untyped reads as: either way it takes
285
+ ## anything. declared is the property's entry in the property list, which carries its enum or
286
+ ## range, and empty for anything that is not a property.
263
287
  ##
264
288
  ## Asked rather than read off the value in the slot, because an object slot can be empty: a
265
289
  ## property typed `Gear` holding null reads as TYPE_NIL, and anything written into it passed the
@@ -273,7 +297,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
273
297
  # form's an integer, a rectangle's size a vector of the same form.
274
298
  if named in GdhSerialisation.components_of(holder):
275
299
  var component: Variant = holder[named]
276
- return {"type": typeof(component), "class": "", "script": null}
300
+ return {"type": typeof(component), "class": "", "script": null, "declared": {}}
277
301
  if holder is Array:
278
302
  var items: Array = holder
279
303
  if items.is_typed():
@@ -281,6 +305,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
281
305
  "type": items.get_typed_builtin(),
282
306
  "class": _typed_class(items.get_typed_script(), items.get_typed_class_name()),
283
307
  "script": _script_or_null(items.get_typed_script()),
308
+ "declared": {},
284
309
  }
285
310
  elif holder is Dictionary:
286
311
  var map: Dictionary = holder
@@ -289,6 +314,7 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
289
314
  "type": map.get_typed_value_builtin(),
290
315
  "class": _typed_class(map.get_typed_value_script(), map.get_typed_value_class_name()),
291
316
  "script": _script_or_null(map.get_typed_value_script()),
317
+ "declared": {},
292
318
  }
293
319
  elif holder is Object:
294
320
  var object: Object = holder
@@ -298,8 +324,9 @@ static func slot_declared(holder: Variant, named: String) -> Dictionary:
298
324
  "type": GdhRead.as_int(entry.get("type", TYPE_NIL), TYPE_NIL),
299
325
  "class": str(entry.get("class_name", "")),
300
326
  "script": null,
327
+ "declared": entry,
301
328
  }
302
- return {"type": TYPE_NIL, "class": "", "script": null}
329
+ return {"type": TYPE_NIL, "class": "", "script": null, "declared": {}}
303
330
 
304
331
 
305
332
  static func _script_or_null(typed: Variant) -> Script: