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.
@@ -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:
@@ -4,6 +4,7 @@ extends RefCounted
4
4
  ## happened, or when the time ran out, so the caller never sleeps for a guessed length.
5
5
 
6
6
  const GdhChanges = preload("runtime_changes.gd")
7
+ const GdhDeclared = preload("declared_values.gd")
7
8
  const GdhPaths = preload("runtime_paths.gd")
8
9
  const GdhQueries = preload("runtime_queries.gd")
9
10
  const GdhRead = preload("reading.gd")
@@ -51,6 +52,13 @@ static func _out_of_range(named: String, asked: int, least: int, most: int) -> D
51
52
  }
52
53
 
53
54
 
55
+ ## The refusal for [param signal_name], which [param node] lacks, naming the nearest signals it has,
56
+ ## or every one when none is near: a name to send instead.
57
+ static func _no_signal(node: Node, node_path: String, signal_name: String) -> Dictionary:
58
+ var said: String = "%s has no signal %s" % [node_path, signal_name]
59
+ return {"type": "error", "message": GdhRead.offered(said, signal_name, GdhRead.signals_of(node)) + "."}
60
+
61
+
54
62
  func wait_frames(params: Dictionary) -> Dictionary:
55
63
  var frames: int = GdhRead.as_int(params.get("frames", 1), 1)
56
64
  if frames < 1 or frames > MOST_FRAMES:
@@ -79,7 +87,7 @@ func wait_signal(params: Dictionary) -> Dictionary:
79
87
  return standing
80
88
  var node: Node = standing["node"]
81
89
  if not node.has_signal(signal_name):
82
- return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
90
+ return _no_signal(node, node_path, signal_name)
83
91
 
84
92
  var catcher: GdhSignalCatcher = GdhSignalCatcher.new()
85
93
  var callable: Callable = catcher._on_fired
@@ -124,7 +132,7 @@ func watch_signal(params: Dictionary) -> Dictionary:
124
132
  return standing
125
133
  var node: Node = standing["node"]
126
134
  if not node.has_signal(signal_name):
127
- return {"type": "error", "message": "%s has no signal %s" % [node_path, signal_name]}
135
+ return _no_signal(node, node_path, signal_name)
128
136
  var watch: GdhWatch = GdhWatch.new(node, node_path, signal_name)
129
137
  var listening: Error = node.connect(signal_name, watch.catcher._on_fired)
130
138
  if listening != OK:
@@ -240,7 +248,9 @@ func wait_until(params: Dictionary) -> Dictionary:
240
248
  var current: Variant = watched["value"]
241
249
  if compare in [">=", "<=", ">", "<"]:
242
250
  return await _wait_in_order(node, params, current, timeout_ms)
243
- var wanted: Variant = _values.fitted(params["value"], typeof(current))
251
+ var by_name: Array = _enum_by_name(watched, params["value"], node_path)
252
+ var sent: Variant = by_name[1] if not by_name.is_empty() and by_name[0] else params["value"]
253
+ var wanted: Variant = _values.fitted(sent, typeof(current))
244
254
  # Read against what the property is declared as, not only against what it holds this frame: an
245
255
  # object property holding null holds nothing typed, so any value was taken as it came and compared
246
256
  # with null until the time ran out. #772 waited a minute for "null" that way and answered met
@@ -252,10 +262,13 @@ func wait_until(params: Dictionary) -> Dictionary:
252
262
  var holds_objects: bool = declared == TYPE_OBJECT or untyped_object
253
263
  if holds_objects and GdhChanges.names_no_object(wanted):
254
264
  wanted = null
255
- var refused: String = _not_comparable(current, wanted, node_path, property)
265
+ # A word no value of an enum is called, refused naming the values before it is compared as text.
266
+ var refused: String = "" if by_name.is_empty() or by_name[0] else str(by_name[1])
256
267
  if refused.is_empty():
257
- refused = _cut_to_fit(params["value"], typeof(current), node_path, property)
258
- var rounded: String = GdhValues.inexact(params["value"], typeof(current))
268
+ refused = _not_comparable(current, wanted, node_path, property)
269
+ if refused.is_empty():
270
+ refused = _cut_to_fit(sent, typeof(current), node_path, property)
271
+ var rounded: String = GdhValues.inexact(sent, typeof(current))
259
272
  if refused.is_empty() and not rounded.is_empty():
260
273
  refused = "%s.%s holds int: %s." % [node_path, property, rounded]
261
274
  var never_held: bool = typeof(wanted) != TYPE_NIL and typeof(wanted) != TYPE_OBJECT
@@ -469,6 +482,20 @@ static func _not_comparable(current: Variant, wanted: Variant, node_path: String
469
482
  return ""
470
483
 
471
484
 
485
+ ## [param sent] as the number of the value an enum property names by it, as
486
+ ## [method GdhDeclared.enum_by_name] answers, for a word waited for on what [param watched] read; []
487
+ ## when the property is no enum or the value no word.
488
+ static func _enum_by_name(watched: Dictionary, sent: Variant, node_path: String) -> Array:
489
+ var declaration: Dictionary = watched["declaration"]
490
+ var enum_hint: bool = GdhRead.as_int(declaration.get("hint", PROPERTY_HINT_NONE)) == PROPERTY_HINT_ENUM
491
+ if watched["declared"] != TYPE_INT or not enum_hint or not sent is String:
492
+ return []
493
+ if not watched["holder"] is Object:
494
+ return []
495
+ var owner: Object = watched["holder"]
496
+ return GdhDeclared.enum_by_name(owner, declaration, str(sent), node_path)
497
+
498
+
472
499
  ## What [param property] reads as on [param node] right now, under `value`, or a `message` saying
473
500
  ## why it cannot be read.
474
501
  ##
@@ -486,18 +513,24 @@ static func _watched(node: Node, node_path: String, property: String) -> Diction
486
513
  var missing: String = GdhPaths.nothing_under(holder, named, str(reached["called"]))
487
514
  if not missing.is_empty():
488
515
  return {"type": "error", "message": missing}
489
- return {"value": GdhPaths.read_under(holder, named), "declared": _declared_type(holder, named)}
516
+ var slot: Dictionary = _declared_slot(holder, named)
517
+ return {
518
+ "value": GdhPaths.read_under(holder, named),
519
+ "declared": slot["type"],
520
+ "declaration": slot["declared"],
521
+ "holder": holder,
522
+ }
490
523
 
491
524
 
492
- ## The type [param named] is declared with on [param holder], read the way a set reads it, so the two
493
- ## refuse the same values: a property's declaration, or a typed list's or map's element type. Read
494
- ## off properties alone, an element of an Array[Node] holding null declares nothing, and a wait for 5
495
- ## on it runs out the timeout while a set of 5 on it is refused at once. TYPE_NIL for anything
496
- ## declaring none, and for a call, whose answer nothing declares.
497
- static func _declared_type(holder: Variant, named: String) -> int:
525
+ ## What [param named] is declared with on [param holder], as [method GdhValues.slot_declared] answers,
526
+ ## read the way a set reads it, so the two refuse the same values: a property's declaration, or a
527
+ ## typed list's or map's element type. Read off properties alone, an element of an Array[Node] holding
528
+ ## null declares nothing, and a wait for 5 on it runs out the timeout while a set of 5 on it is refused
529
+ ## at once. TYPE_NIL for anything declaring none, and for a call, whose answer nothing declares.
530
+ static func _declared_slot(holder: Variant, named: String) -> Dictionary:
498
531
  if not GdhPaths.method_of(named).is_empty():
499
- return TYPE_NIL
500
- return GdhValues.slot_declared(holder, named)["type"]
532
+ return {"type": TYPE_NIL, "class": "", "script": null, "declared": {}}
533
+ return GdhValues.slot_declared(holder, named)
501
534
 
502
535
 
503
536
  ## Waits until something under [param node_path] has [param said] written on it.
@@ -4,6 +4,9 @@ const GdhRead = preload("reading.gd")
4
4
  const GdhLog = preload("logger.gd")
5
5
  const GdhSerialisation = preload("serialisation.gd")
6
6
 
7
+ # How far below the closest name another may score and still be offered beside it.
8
+ const CLOSE_TO_THE_BEST: float = 0.2
9
+
7
10
  # The base classes each category name stands for. Kept to the list the tool's schema offers, which
8
11
  # a case holds: the schema offered physics3d and ui, which this refused, and physics answered the 3D
9
12
  # bodies alone beside physics2d and physics3d.
@@ -114,20 +117,20 @@ func query_class_info(params: Dictionary) -> Dictionary:
114
117
  )
115
118
 
116
119
  if not ClassDB.class_exists(class_name_str):
117
- return _not_found(class_name_str)
120
+ return _not_found(class_name_str, _declared_classes(params))
118
121
 
119
122
  var member: String = str(params.get("member", ""))
120
123
  if not member.is_empty():
121
124
  return _member_of(class_name_str, member)
122
125
 
123
- var methods: Array[Dictionary] = _methods_of(class_name_str, include_inherited)
124
- var properties: Array[Dictionary] = _properties_of(class_name_str, include_inherited)
125
- var signals: Array[Dictionary] = _signals_of(class_name_str, include_inherited)
126
+ var methods: Array[Dictionary] = _methods_of(class_name_str)
127
+ var properties: Array[Dictionary] = _properties_of(class_name_str)
128
+ var signals: Array[Dictionary] = _signals_of(class_name_str)
126
129
 
127
130
  var enums: Dictionary = {}
128
- for e: String in ClassDB.class_get_enum_list(class_name_str, not include_inherited):
131
+ for e: String in ClassDB.class_get_enum_list(class_name_str, true):
129
132
  var enum_values: Dictionary = {}
130
- for c: String in ClassDB.class_get_enum_constants(class_name_str, e, not include_inherited):
133
+ for c: String in ClassDB.class_get_enum_constants(class_name_str, e, true):
131
134
  enum_values[c] = ClassDB.class_get_integer_constant(class_name_str, c)
132
135
  enums[e] = enum_values
133
136
 
@@ -143,7 +146,7 @@ func query_class_info(params: Dictionary) -> Dictionary:
143
146
  )
144
147
  )
145
148
 
146
- return {
149
+ var answer: Dictionary = {
147
150
  "class_name": class_name_str,
148
151
  "parent_class": ClassDB.get_parent_class(class_name_str),
149
152
  "can_instantiate": ClassDB.can_instantiate(class_name_str),
@@ -156,43 +159,99 @@ func query_class_info(params: Dictionary) -> Dictionary:
156
159
  "signals": signals,
157
160
  "enums": enums
158
161
  }
162
+ if include_inherited:
163
+ answer["inherited"] = _inherited_names(class_name_str)
164
+ return answer
165
+
166
+
167
+ # What [param class_name_str] inherits, by name under the ancestor declaring it, nearest first. In
168
+ # full, CharacterBody2D's inherited members ran to two hundred thousand characters, more than a
169
+ # client takes in one answer, and any one of them is read in full by asking for it as member.
170
+ func _inherited_names(class_name_str: String) -> Array[Dictionary]:
171
+ var inherited: Array[Dictionary] = []
172
+ var current: String = ClassDB.get_parent_class(class_name_str)
173
+ while not current.is_empty():
174
+ (
175
+ inherited
176
+ . append(
177
+ {
178
+ "class": current,
179
+ "methods": _names(_methods_of(current)),
180
+ "properties": _names(_properties_of(current)),
181
+ "signals": _names(_signals_of(current)),
182
+ "enums": Array(ClassDB.class_get_enum_list(current, true)),
183
+ }
184
+ )
185
+ )
186
+ current = ClassDB.get_parent_class(current)
187
+ return inherited
188
+
189
+
190
+ static func _names(members: Array[Dictionary]) -> Array[String]:
191
+ var names: Array[String] = []
192
+ for each: Dictionary in members:
193
+ names.append(str(each["name"]))
194
+ return names
195
+
196
+
197
+ static func _declared_classes(params: Dictionary) -> Dictionary:
198
+ var given: Variant = params.get("project_classes", {})
199
+ if given is Dictionary:
200
+ var declared: Dictionary = given
201
+ return declared
202
+ return {}
159
203
 
160
204
 
161
- # One member of a class by name, wherever in its ancestry it is declared.
162
- #
163
- # The question a member is asked by name for is "does this engine have it, and what does it take":
164
- # the whole class in answer to that ran to seventy thousand characters for Control, and the member
165
- # is as often an ancestor's as the class's own, so the ancestry is walked without being asked. Each
166
- # match says which class declares it. Nothing found names the members whose names contain the one
167
- # asked for, since the usual miss is a spelling.
168
205
  ## Why [param wanted] has no answer here. A class_name a project script declares is not in ClassDB,
169
- ## and "Class not found" for one read as though the project had no such class.
170
- func _not_found(wanted: String) -> Dictionary:
171
- for entry: Dictionary in ProjectSettings.get_global_class_list():
172
- if str(entry.get("class", "")) == wanted:
173
- return (
174
- _log
175
- . failure(
176
- (
177
- (
178
- "%s is not an engine class: it is the class_name %s declares, and editor_classes reads "
179
- + "the engine's ClassDB only. script_info structure on that script, with "
180
- + "includeInherited, answers what it declares and what it inherits."
181
- )
182
- % [wanted, str(entry.get("path", ""))]
183
- )
206
+ ## and "Class not found" for one read as though the project had no such class. [param declared] is
207
+ ## what the project's scripts declare on disk, class to path, rather than the engine's list, which is
208
+ ## the class cache: that lags the scripts, and a class written a moment ago was refused as declared
209
+ ## nowhere.
210
+ func _not_found(wanted: String, declared: Dictionary) -> Dictionary:
211
+ if declared.has(wanted):
212
+ return _log.failure(
213
+ (
214
+ (
215
+ "%s is not an engine class: it is the class_name %s declares, and editor_classes reads "
216
+ + "the engine's ClassDB only. script_info structure on that script, with "
217
+ + "includeInherited, answers what it declares and what it inherits."
184
218
  )
219
+ % [wanted, str(declared[wanted])]
185
220
  )
186
- # The usual miss is a spelling, one letter off a class the engine has.
187
- var close: PackedStringArray = GdhRead.nearest(wanted, ClassDB.get_class_list())
221
+ )
222
+ # The usual miss is a spelling, one letter off a class the engine has or one the project declares.
223
+ # Ranked as one pool and kept near the best, since a project class a letter off came behind three
224
+ # engine classes that shared only "Texture" with it.
225
+ var pool: PackedStringArray = ClassDB.get_class_list()
226
+ pool.append_array(PackedStringArray(declared.keys()))
227
+ var close: PackedStringArray = GdhRead.nearest(wanted, pool)
228
+ var best: float = 0.0 if close.is_empty() else close[0].to_lower().similarity(wanted.to_lower())
229
+ var engine: Array[String] = []
230
+ var own: Array[String] = []
231
+ for name: String in close:
232
+ if name.to_lower().similarity(wanted.to_lower()) < best - CLOSE_TO_THE_BEST:
233
+ continue
234
+ if declared.has(name):
235
+ own.append(name)
236
+ else:
237
+ engine.append(name)
188
238
  var offered: String = (
189
239
  ""
190
- if close.is_empty()
240
+ if engine.is_empty()
191
241
  else (
192
242
  " The nearest engine %s %s."
193
- % ["class is" if close.size() == 1 else "classes are", ", ".join(close)]
243
+ % ["class is" if engine.size() == 1 else "classes are", ", ".join(engine)]
194
244
  )
195
245
  )
246
+ if not own.is_empty():
247
+ offered += (
248
+ " The nearest %s a project script declares %s %s, which script_info structure reads."
249
+ % [
250
+ "class" if own.size() == 1 else "classes",
251
+ "is" if own.size() == 1 else "are",
252
+ ", ".join(own),
253
+ ]
254
+ )
196
255
  return _log.failure(
197
256
  (
198
257
  (
@@ -204,17 +263,24 @@ func _not_found(wanted: String) -> Dictionary:
204
263
  )
205
264
 
206
265
 
266
+ # One member of a class by name, wherever in its ancestry it is declared.
267
+ #
268
+ # The question a member is asked by name for is "does this engine have it, and what does it take":
269
+ # the whole class in answer to that ran to seventy thousand characters for Control, and the member
270
+ # is as often an ancestor's as the class's own, so the ancestry is walked without being asked. Each
271
+ # match says which class declares it. Nothing found names the members whose names contain the one
272
+ # asked for, since the usual miss is a spelling.
207
273
  func _member_of(class_name_str: String, member: String) -> Dictionary:
208
274
  var found: Array[Dictionary] = []
209
275
  var current: String = class_name_str
210
276
  while not current.is_empty():
211
- for m: Dictionary in _methods_of(current, false):
277
+ for m: Dictionary in _methods_of(current):
212
278
  if m["name"] == member:
213
279
  found.append({"kind": "method", "declared_in": current, "member": m})
214
- for p: Dictionary in _properties_of(current, false):
280
+ for p: Dictionary in _properties_of(current):
215
281
  if p["name"] == member:
216
282
  found.append({"kind": "property", "declared_in": current, "member": p})
217
- for s: Dictionary in _signals_of(current, false):
283
+ for s: Dictionary in _signals_of(current):
218
284
  if s["name"] == member:
219
285
  found.append({"kind": "signal", "declared_in": current, "member": s})
220
286
  for e: String in ClassDB.class_get_enum_list(current, true):
@@ -251,15 +317,15 @@ func _members_alike(class_name_str: String, member: String) -> Array[String]:
251
317
  var wanted: String = member.to_lower()
252
318
  var current: String = class_name_str
253
319
  while not current.is_empty():
254
- for m: Dictionary in _methods_of(current, false):
320
+ for m: Dictionary in _methods_of(current):
255
321
  var name: String = m["name"]
256
322
  if name.to_lower().contains(wanted) and not alike.has(name):
257
323
  alike.append(name)
258
- for p: Dictionary in _properties_of(current, false):
324
+ for p: Dictionary in _properties_of(current):
259
325
  var name: String = p["name"]
260
326
  if name.to_lower().contains(wanted) and not alike.has(name):
261
327
  alike.append(name)
262
- for s: Dictionary in _signals_of(current, false):
328
+ for s: Dictionary in _signals_of(current):
263
329
  var name: String = s["name"]
264
330
  if name.to_lower().contains(wanted) and not alike.has(name):
265
331
  alike.append(name)
@@ -275,7 +341,7 @@ func inspect_inheritance(params: Dictionary) -> Dictionary:
275
341
  _log.info("Inspecting inheritance for: " + class_name_str)
276
342
 
277
343
  if not ClassDB.class_exists(class_name_str):
278
- return _not_found(class_name_str)
344
+ return _not_found(class_name_str, _declared_classes(params))
279
345
 
280
346
  var ancestors: Array[String] = []
281
347
  var current: String = class_name_str
@@ -324,10 +390,10 @@ func inspect_inheritance(params: Dictionary) -> Dictionary:
324
390
  }
325
391
 
326
392
 
327
- func _methods_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
393
+ func _methods_of(class_name_str: String) -> Array[Dictionary]:
328
394
  var methods: Array[Dictionary] = []
329
395
 
330
- for m: Dictionary in ClassDB.class_get_method_list(class_name_str, not include_inherited):
396
+ for m: Dictionary in ClassDB.class_get_method_list(class_name_str, true):
331
397
  var args: Array[Dictionary] = []
332
398
  var declared_args: Array = m.get("args", [])
333
399
  for a: Dictionary in declared_args:
@@ -389,10 +455,10 @@ static func _type_name(info: Dictionary, nil_name: String) -> String:
389
455
  return type_string(type)
390
456
 
391
457
 
392
- func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
458
+ func _properties_of(class_name_str: String) -> Array[Dictionary]:
393
459
  var properties: Array[Dictionary] = []
394
460
 
395
- for p: Dictionary in ClassDB.class_get_property_list(class_name_str, not include_inherited):
461
+ for p: Dictionary in ClassDB.class_get_property_list(class_name_str, true):
396
462
  # A category, group or subgroup is an editor heading rather than a property.
397
463
  var usage: int = GdhRead.as_int(p.get("usage", 0))
398
464
  if usage & PROPERTY_USAGE_CATEGORY or usage & PROPERTY_USAGE_GROUP or usage & PROPERTY_USAGE_SUBGROUP:
@@ -412,10 +478,10 @@ func _properties_of(class_name_str: String, include_inherited: bool) -> Array[Di
412
478
  return properties
413
479
 
414
480
 
415
- func _signals_of(class_name_str: String, include_inherited: bool) -> Array[Dictionary]:
481
+ func _signals_of(class_name_str: String) -> Array[Dictionary]:
416
482
  var signals: Array[Dictionary] = []
417
483
 
418
- for s: Dictionary in ClassDB.class_get_signal_list(class_name_str, not include_inherited):
484
+ for s: Dictionary in ClassDB.class_get_signal_list(class_name_str, true):
419
485
  var sig_args: Array[Dictionary] = []
420
486
  var declared_args: Array = s.get("args", [])
421
487
  for a: Dictionary in declared_args:
@@ -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:
@@ -191,9 +191,13 @@ func modify_gdscript(params: Dictionary) -> Dictionary:
191
191
 
192
192
  # All of them checked before any is made, so a call is made whole or not at all: one skipped for a
193
193
  # missing name used to leave the rest written and the answer saying success over an empty list.
194
- var problems: Array[String] = _modification_problems(modifications, _declared(lines))
194
+ var declared: Dictionary = _declared(lines)
195
+ var problems: Array[String] = _modification_problems(modifications, declared)
195
196
  if not problems.is_empty():
196
- return _log.failure("Nothing was changed: " + "; ".join(problems))
197
+ var refusal: String = "Nothing was changed: " + "; ".join(problems)
198
+ if _redeclares(modifications, declared):
199
+ refusal += ". modify only adds, so a member the script declares already is changed in the file itself"
200
+ return _log.failure(refusal)
197
201
  # Each placed line as a zero-based index that moves down whenever a later insertion lands above
198
202
  # it, so the numbers answered are where the lines are in the file written, not where each went
199
203
  # at the moment it was placed.
@@ -414,6 +418,15 @@ static func _modification_problems(modifications: Array, declared: Dictionary) -
414
418
  return problems
415
419
 
416
420
 
421
+ static func _redeclares(modifications: Array, declared: Dictionary) -> bool:
422
+ for mod: Variant in modifications:
423
+ if mod is Dictionary:
424
+ var fields: Dictionary = mod
425
+ if declared.has(str(fields.get("name", ""))):
426
+ return true
427
+ return false
428
+
429
+
417
430
  # The members [param lines] declares at the top level, by name, as {"kind", "line"} counted from
418
431
  # one: variables, constants, signals, functions, enums and inner classes, which share one namespace.
419
432
  static func _declared(lines: Array[String]) -> Dictionary:
@@ -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.