gdharness 1.3.40 → 1.3.42

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.
Files changed (32) hide show
  1. package/build/cli.js +343 -142
  2. package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
  3. package/build/godot/addons/gdharness_editor/file_reads.gd +36 -1
  4. package/build/godot/addons/gdharness_editor/file_walk.gd +33 -1
  5. package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
  6. package/build/godot/addons/gdharness_editor/property_values.gd +6 -9
  7. package/build/godot/addons/gdharness_editor/scene_file.gd +27 -0
  8. package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -2
  9. package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -2
  10. package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +15 -0
  11. package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +7 -2
  12. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +59 -0
  13. package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
  14. package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +75 -11
  15. package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +77 -22
  16. package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -1
  17. package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +4 -3
  18. package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
  19. package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
  20. package/build/godot/addons/gdharness_runtime/runtime_typing.gd +15 -8
  21. package/build/godot/operations/animation_reads.gd +31 -4
  22. package/build/godot/operations/dependencies.gd +5 -2
  23. package/build/godot/operations/file_reads.gd +36 -1
  24. package/build/godot/operations/file_walk.gd +33 -1
  25. package/build/godot/operations/gdscript_analysis.gd +19 -1
  26. package/build/godot/operations/gdscript_authoring.gd +65 -4
  27. package/build/godot/operations/import_pipeline.gd +1 -1
  28. package/build/godot/operations/input_actions.gd +1 -0
  29. package/build/godot/operations/plugins.gd +39 -1
  30. package/build/godot/operations/project_config.gd +104 -1
  31. package/build/godot/operations/property_values.gd +6 -9
  32. package/package.json +1 -1
@@ -35,8 +35,24 @@ static func missing_reason(path: String) -> String:
35
35
 
36
36
  ## [param said], the refusal for [param path], a res:// path nothing is at, with the files of its
37
37
  ## extension whose names are nearest its own, or every one when there are few and none is near, so
38
- ## the caller has a path to send instead.
38
+ ## the caller has a path to send instead. Files of the same name beside it come first, since a
39
+ ## misspelt extension has no file of its own extension to be near.
39
40
  func with_nearest_files(said: String, path: String) -> String:
41
+ # One the refusal names is the file it refuses, as a script given for a scene is. Word by word,
42
+ # since res://brain.gd is inside res://brain.gdd.
43
+ var named: Array[String] = []
44
+ for word: String in said.replace("\n", " ").replace(",", " ").replace(";", " ").split(" ", false):
45
+ named.append(word.rstrip(".:"))
46
+ var namesakes: Array[String] = []
47
+ for file: String in _namesakes(path):
48
+ if not file in named:
49
+ namesakes.append(file)
50
+ if not namesakes.is_empty():
51
+ var alone: bool = namesakes.size() == 1
52
+ return (
53
+ "%s. The %s of that name beside it %s %s."
54
+ % [said, "file" if alone else "files", "is" if alone else "are", ", ".join(namesakes)]
55
+ )
40
56
  var extension: String = path.get_extension()
41
57
  if extension.is_empty():
42
58
  return said
@@ -65,6 +81,22 @@ func with_nearest_files(said: String, path: String) -> String:
65
81
  return "%s. None of the project's %d .%s files is named like it." % [said, files.size(), extension]
66
82
 
67
83
 
84
+ # The files in [param path]'s folder named as it is with another extension, leaving out the import
85
+ # and uid files the engine keeps beside a file, which are named after the whole file name.
86
+ static func _namesakes(path: String) -> Array[String]:
87
+ var found: Array[String] = []
88
+ var folder: String = path.get_base_dir()
89
+ if not DirAccess.dir_exists_absolute(folder):
90
+ return found
91
+ var stem: String = path.get_file().get_basename()
92
+ var files: PackedStringArray = DirAccess.get_files_at(folder)
93
+ files.sort()
94
+ for file: String in files:
95
+ if file.get_basename() == stem and file != path.get_file():
96
+ found.append(folder.path_join(file))
97
+ return found
98
+
99
+
68
100
  # A directory the engine steps over, marker file and all.
69
101
  func is_stepped_over(path: String) -> bool:
70
102
  return FileAccess.file_exists(path + ".gdignore")
@@ -249,6 +249,16 @@ func _members(
249
249
  i += 1
250
250
  stripped = (stripped + " " + GdhPatterns.without_comment(lines[i])).strip_edges()
251
251
  header = GdhPatterns.without_annotations(stripped)
252
+ # A value that opens a triple-quoted string ends where the string does, and read from its
253
+ # first line alone it was the opening quotes and the text up to the line break.
254
+ if in_multiline_string and (header.begins_with("const ") or _declares_variable(header)):
255
+ while i + 1 < lines.size():
256
+ i += 1
257
+ stripped += "\n" + lines[i]
258
+ if _triple_quotes(lines[i]) % 2 == 1:
259
+ break
260
+ in_multiline_string = false
261
+ header = GdhPatterns.without_annotations(stripped)
252
262
  var declaration: RegExMatch = GdhPatterns.declared_class(header)
253
263
  var entry: Dictionary = {}
254
264
  if declaration != null:
@@ -273,7 +283,7 @@ func _members(
273
283
  elif header.begins_with("enum "):
274
284
  entry = _parse_enum(header, at + 1)
275
285
  own.enums.append(entry)
276
- elif header.begins_with("var ") or header.begins_with("static var "):
286
+ elif _declares_variable(header):
277
287
  # The whole line here, not the header: which annotations a variable carries is the
278
288
  # answer rather than noise in front of it, and `@export_range(0, 1)` is the hint.
279
289
  entry = _parse_variable(stripped, at + 1)
@@ -602,6 +612,10 @@ func _parse_variable(line: String, line_num: int) -> Dictionary:
602
612
  return {"name": "", "line": line_num}
603
613
 
604
614
  var var_text: String = line.substr(var_pos + 4).strip_edges()
615
+ # A colon closing the line opens the variable's setter and getter, and was read as the end of
616
+ # its default: `= Kind.ROUND:`.
617
+ if var_text.ends_with(":"):
618
+ var_text = var_text.left(-1).strip_edges()
605
619
  var name: String = ""
606
620
  var type_hint: String = ""
607
621
  var default_value: String = ""
@@ -847,6 +861,10 @@ func _collect_dependencies(line: String, into: Array[String]) -> void:
847
861
 
848
862
 
849
863
  # How many triple quotes of either kind [param line] holds.
864
+ static func _declares_variable(header: String) -> bool:
865
+ return header.begins_with("var ") or header.begins_with("static var ")
866
+
867
+
850
868
  static func _triple_quotes(line: String) -> int:
851
869
  return line.count('"""') + line.count("'''")
852
870
 
@@ -100,6 +100,9 @@ func create_gdscript(params: Dictionary) -> Dictionary:
100
100
  file.close()
101
101
  if not stored:
102
102
  return _log.failure("Failed to write to script file: " + full_script_path)
103
+ # The editor gives a script it makes a .uid beside it. A headless engine writes none until the
104
+ # project is imported, so a script made here was missing from every check tracking the sidecars.
105
+ var uid: String = _uid_beside(full_script_path)
103
106
 
104
107
  var written: Script = ResourceLoader.load(full_script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
105
108
  var parses: bool = _parses(written)
@@ -110,7 +113,10 @@ func create_gdscript(params: Dictionary) -> Dictionary:
110
113
  "absolute_path": ProjectSettings.globalize_path(full_script_path),
111
114
  "template_used": template if not template.is_empty() else "none",
112
115
  "parses": parses,
116
+ "uid": null,
113
117
  }
118
+ if not uid.is_empty():
119
+ answer["uid"] = uid
114
120
  # Read off the file as the engine parsed it rather than off the arguments, which content leaves
115
121
  # out. A script that does not parse tells the engine nothing, and its empty base and name were
116
122
  # answered for a file whose header says otherwise, so those are read off the text instead.
@@ -125,6 +131,22 @@ func create_gdscript(params: Dictionary) -> Dictionary:
125
131
  return answer
126
132
 
127
133
 
134
+ ## The uid in the .uid file beside [param path], written first when there is none, as the editor
135
+ ## writes it for a script it makes; empty when it could not be written. A sidecar already there is
136
+ ## the one the engine reads the script's uid from, so it is kept.
137
+ static func _uid_beside(path: String) -> String:
138
+ var sidecar: String = path + ".uid"
139
+ if FileAccess.file_exists(sidecar):
140
+ return FileAccess.get_file_as_string(sidecar).strip_edges()
141
+ var text: String = ResourceUID.id_to_text(ResourceUID.create_id_for_path(path))
142
+ var file: FileAccess = FileAccess.open(sidecar, FileAccess.WRITE)
143
+ if file == null:
144
+ return ""
145
+ var stored: bool = file.store_string(text + "\n")
146
+ file.close()
147
+ return text if stored else ""
148
+
149
+
128
150
  ## The refusal for [param path], which is no GDScript file, naming what [param doing] it instead:
129
151
  ## a shader is made with resource_edit, and once it exists its text is edited as a file's.
130
152
  static func _not_gdscript(path: String, doing: String) -> String:
@@ -404,6 +426,14 @@ static func _modification_problems(modifications: Array, declared: Dictionary) -
404
426
  )
405
427
  if mod_type == "add_constant" and str(fields.get("defaultValue", "")).is_empty():
406
428
  problems.append(which + " is a constant with no defaultValue, and a constant needs a value")
429
+ if (
430
+ not str(fields.get("exportHint", "")).is_empty()
431
+ and fields.has("isExport")
432
+ and not GdhRead.as_bool(fields["isExport"])
433
+ ):
434
+ problems.append(
435
+ which + " has an exportHint and isExport false, and a hint is written only on an export"
436
+ )
407
437
  if mod_name.is_empty():
408
438
  problems.append(which + " has no name")
409
439
  continue
@@ -527,8 +557,9 @@ func _add_variable(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
527
557
  var var_name: String = str(mod.get("name", ""))
528
558
  var var_type: String = str(mod.get("varType", ""))
529
559
  var default_value: String = str(mod.get("defaultValue", ""))
530
- var is_export: bool = GdhRead.as_bool(mod.get("isExport", false))
531
560
  var export_hint: String = str(mod.get("exportHint", ""))
561
+ # A hint can only mean an export, so a hint given alone writes one.
562
+ var is_export: bool = GdhRead.as_bool(mod.get("isExport", not export_hint.is_empty()))
532
563
  var is_onready: bool = GdhRead.as_bool(mod.get("isOnready", false))
533
564
 
534
565
  var var_line: String = ""
@@ -631,9 +662,24 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
631
662
 
632
663
  # The body's statements relative to the function, whether or not the caller indented them for
633
664
  # it: a body written with its tab already on came out two levels in. A blank line stays empty,
634
- # since an indent alone on it is trailing whitespace to every formatter.
635
- for bl: String in body.dedent().split("\n"):
636
- func_lines.append("" if bl.strip_edges().is_empty() else "\t" + bl)
665
+ # since an indent alone on it is trailing whitespace to every formatter. Each level is the
666
+ # file's own unit, because the engine refuses a file indented with spaces in one place and tabs
667
+ # in another, so a tab put into a script indented with spaces stopped it parsing.
668
+ var unit: String = _indent_unit(lines)
669
+ var body_lines: Array[String] = []
670
+ body_lines.assign(body.dedent().strip_edges(false, true).split("\n"))
671
+ var given_unit: String = _indent_unit(body_lines)
672
+ for bl: String in body_lines:
673
+ if bl.strip_edges().is_empty():
674
+ func_lines.append("")
675
+ continue
676
+ var stripped: String = bl.lstrip(" \t")
677
+ var leading: String = bl.substr(0, bl.length() - stripped.length())
678
+ var levels: int = floori(float(leading.length()) / given_unit.length())
679
+ if leading != given_unit.repeat(levels):
680
+ func_lines.append(unit + bl)
681
+ continue
682
+ func_lines.append(unit.repeat(levels + 1) + stripped)
637
683
 
638
684
  var below: int = _blank_run(lines, insert_line, 1)
639
685
  if insert_line + below < lines.size():
@@ -645,6 +691,21 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
645
691
  return declared_at if _insert(lines, insert_line, func_lines, placed) else -1
646
692
 
647
693
 
694
+ ## One level of indentation in [param lines]: what the first line indented below an unindented
695
+ ## line ending a block opens with, or a tab, the engine's default, when no line is.
696
+ static func _indent_unit(lines: Array[String]) -> String:
697
+ var opener: bool = false
698
+ for line: String in lines:
699
+ var stripped: String = line.strip_edges()
700
+ if stripped.is_empty() or stripped.begins_with("#"):
701
+ continue
702
+ var leading: String = line.substr(0, line.length() - line.lstrip(" \t").length())
703
+ if opener and not leading.is_empty():
704
+ return leading
705
+ opener = leading.is_empty() and stripped.ends_with(":")
706
+ return "\t"
707
+
708
+
648
709
  # How many blank lines run from [param from] on in the direction of [param step], 1 or -1.
649
710
  static func _blank_run(lines: Array[String], from: int, step: int) -> int:
650
711
  var count: int = 0
@@ -408,7 +408,7 @@ func _import_status_of(resource_path: String, import_file_path: String) -> Dicti
408
408
  # Why [param resource_path] has no import file, with what to do about it where anything can be done.
409
409
  func _no_sidecar(resource_path: String) -> String:
410
410
  if not FileAccess.file_exists(resource_path):
411
- return resource_path + " is not on disk"
411
+ return GdhFileWalk.new().with_nearest_files(resource_path + " is not on disk", resource_path)
412
412
  var never: String = _never_imported(resource_path)
413
413
  if not never.is_empty():
414
414
  return resource_path + " has no import options: " + never
@@ -280,6 +280,7 @@ func add_input_action(params: Dictionary) -> Dictionary:
280
280
  "deadzone": deadzone,
281
281
  "events": events_config,
282
282
  "was_new": not ProjectSettings.has_setting(setting),
283
+ "saved": true,
283
284
  }
284
285
  if not answer["was_new"]:
285
286
  var old_events: Array[Dictionary] = []
@@ -107,7 +107,45 @@ func enable_plugin(params: Dictionary) -> Dictionary:
107
107
  if err != OK:
108
108
  return _log.failure("Failed to save project.godot: " + error_string(err))
109
109
 
110
- return {"plugin_name": plugin_name, "action": "enabled", "enabled_plugins": _names_of(enabled_paths)}
110
+ var answer: Dictionary = {
111
+ "plugin_name": plugin_name, "action": "enabled", "enabled_plugins": _names_of(enabled_paths)
112
+ }
113
+ var unloadable: String = _why_not_loaded(plugin_cfg_path)
114
+ if not unloadable.is_empty():
115
+ answer["note"] = unloadable
116
+ return answer
117
+
118
+
119
+ # What the editor finds wrong with the plugin at [param plugin_cfg_path] when it loads it, the checks
120
+ # EditorNode::set_addon_plugin_enabled makes, or "" when it would load. Written all the same, since
121
+ # the script can be put right before the editor next starts.
122
+ static func _why_not_loaded(plugin_cfg_path: String) -> String:
123
+ var config: ConfigFile = ConfigFile.new()
124
+ if config.load(plugin_cfg_path) != OK:
125
+ return "The editor cannot read %s, so it will not enable the plugin." % plugin_cfg_path
126
+ if not config.has_section_key("plugin", "script"):
127
+ return "%s has no script field, so the editor will not enable the plugin." % plugin_cfg_path
128
+ var script_name: String = str(config.get_value("plugin", "script"))
129
+ if script_name.is_empty():
130
+ return ""
131
+ var script_path: String = plugin_cfg_path.get_base_dir().path_join(script_name)
132
+ if not FileAccess.file_exists(script_path):
133
+ return "%s does not exist, so the editor will not enable the plugin." % script_path
134
+ # Loaded only to learn whether the editor could; what the engine says of a script that does not
135
+ # parse is about a file the answer does not describe.
136
+ printerr(GdhLog.ASIDE)
137
+ var script: Script = ResourceLoader.load(script_path, "Script", ResourceLoader.CACHE_MODE_IGNORE)
138
+ printerr(GdhLog.RESUMED)
139
+ if script == null or script.get_instance_base_type() == &"":
140
+ return (
141
+ "%s does not compile, so the editor disables the plugin again at its next start." % script_path
142
+ + " script_diagnostics names the error."
143
+ )
144
+ if not ClassDB.is_parent_class(script.get_instance_base_type(), "EditorPlugin"):
145
+ return "%s does not extend EditorPlugin, so the editor loads nothing for it." % script_path
146
+ if not script.is_tool():
147
+ return "%s is not a @tool script, so the editor loads nothing for it." % script_path
148
+ return ""
111
149
 
112
150
 
113
151
  # Disable a plugin
@@ -7,6 +7,52 @@ const GdhProjectFile = preload("project_file.gd")
7
7
  const GdhSerialisation = preload("serialisation.gd")
8
8
  const GdhSettingReads = preload("setting_reads.gd")
9
9
  const GdhShapes = preload("value_shapes.gd")
10
+ ## The words GDScript reserves, which no script could reach an autoload by.
11
+ const GDSCRIPT_KEYWORDS: Array[String] = [
12
+ "and",
13
+ "as",
14
+ "assert",
15
+ "await",
16
+ "break",
17
+ "breakpoint",
18
+ "class",
19
+ "class_name",
20
+ "const",
21
+ "continue",
22
+ "elif",
23
+ "else",
24
+ "enum",
25
+ "extends",
26
+ "false",
27
+ "for",
28
+ "func",
29
+ "if",
30
+ "in",
31
+ "INF",
32
+ "is",
33
+ "match",
34
+ "NAN",
35
+ "namespace",
36
+ "not",
37
+ "null",
38
+ "or",
39
+ "pass",
40
+ "PI",
41
+ "preload",
42
+ "return",
43
+ "self",
44
+ "signal",
45
+ "static",
46
+ "super",
47
+ "TAU",
48
+ "trait",
49
+ "true",
50
+ "var",
51
+ "void",
52
+ "when",
53
+ "while",
54
+ "yield",
55
+ ]
10
56
 
11
57
  var _log: GdhLog
12
58
  var _values: GdhSerialisation = GdhSerialisation.new()
@@ -133,6 +179,23 @@ func set_project_setting(params: Dictionary) -> Dictionary:
133
179
  var err: Error = GdhProjectFile.save([setting_path])
134
180
  if err != OK:
135
181
  return _log.failure("Failed to save project.godot: " + error_string(err))
182
+ # An override still replaces what was written wherever its feature is, and a write of the base
183
+ # value read as if it reached every build.
184
+ var overrides: Dictionary = GdhSettingReads.overrides_of(setting_path, _values)
185
+ if not overrides.is_empty():
186
+ answer["overrides"] = overrides
187
+ var parts: Array[String] = []
188
+ for feature: Variant in overrides:
189
+ parts.append(
190
+ (
191
+ "one with the feature %s reads %s from %s.%s"
192
+ % [feature, JSON.stringify(overrides[feature]), setting_path, feature]
193
+ )
194
+ )
195
+ answer["overridesNote"] = (
196
+ "Not every build reads what was written: %s. Set or unset an override to change what its builds read."
197
+ % "; ".join(parts)
198
+ )
136
199
  return answer
137
200
 
138
201
 
@@ -194,6 +257,12 @@ func unset_project_setting(params: Dictionary) -> Dictionary:
194
257
  answer["removed"] = false
195
258
  answer["saved"] = false
196
259
  answer["note"] = "project.godot does not set %s, so there was nothing to take out." % setting_path
260
+ var near: PackedStringArray = PackedStringArray() if held else _settings_near(setting_path)
261
+ if not near.is_empty():
262
+ answer["note"] += (
263
+ " Nor does the engine declare it; the nearest %s %s."
264
+ % ["setting is" if near.size() == 1 else "settings are", ", ".join(near)]
265
+ )
197
266
  return _with_what_holds(answer, setting_path)
198
267
  answer["old_value"] = _values.serialize_value(value)
199
268
  # The default rather than null for a setting the engine declares: null takes the declaration away
@@ -221,8 +290,12 @@ func _with_what_holds(answer: Dictionary, setting_path: String) -> Dictionary:
221
290
 
222
291
 
223
292
  # The settings in the same section as [param setting_path] named nearest it, for a new one that is
224
- # likely a misspelt one: a setting nothing declares is written all the same, and is never read.
293
+ # likely a misspelt one: a setting nothing declares is written all the same, and is never read. None
294
+ # for an override of a setting the engine has, which its feature's builds read in the base's place.
225
295
  static func _settings_near(setting_path: String) -> PackedStringArray:
296
+ var base: String = GdhSettingReads.override_base(setting_path)
297
+ if not base.is_empty() and ProjectSettings.has_setting(base):
298
+ return PackedStringArray()
226
299
  var section: String = setting_path.get_slice("/", 0) + "/"
227
300
  var names: PackedStringArray = []
228
301
  for info: Dictionary in ProjectSettings.get_property_list():
@@ -311,6 +384,10 @@ func add_autoload(params: Dictionary) -> Dictionary:
311
384
  )
312
385
  )
313
386
 
387
+ var clash: String = _autoload_name_refusal(name)
388
+ if not clash.is_empty():
389
+ return _log.failure(clash)
390
+
314
391
  if not path.begins_with("res://"):
315
392
  path = "res://" + path
316
393
 
@@ -354,6 +431,32 @@ func add_autoload(params: Dictionary) -> Dictionary:
354
431
  }
355
432
 
356
433
 
434
+ ## Why [param name] cannot name an autoload, or empty when it can. The editor's Autoload tab refuses
435
+ ## the same names: an autoload named after an engine class or a built-in type takes that name in
436
+ ## every script, so an autoload called Input made every Input.is_action_pressed call the autoload.
437
+ static func _autoload_name_refusal(name: String) -> String:
438
+ var reason: String = ""
439
+ if not name.is_valid_ascii_identifier():
440
+ reason = "it is not an identifier, which a script could name"
441
+ elif name in GDSCRIPT_KEYWORDS:
442
+ reason = "it is a GDScript keyword"
443
+ elif ClassDB.class_exists(name):
444
+ reason = (
445
+ "it is the engine class %s, and every script naming %s would reach the autoload instead"
446
+ % [name, name]
447
+ )
448
+ else:
449
+ for type: int in TYPE_MAX:
450
+ if type_string(type) == name:
451
+ reason = "it is the built-in type %s" % name
452
+ if reason.is_empty():
453
+ return ""
454
+ return (
455
+ "%s cannot name an autoload: %s. Nothing was written; the editor's Autoload tab refuses it too."
456
+ % [JSON.stringify(name), reason]
457
+ )
458
+
459
+
357
460
  # Remove an autoload singleton
358
461
  func remove_autoload(params: Dictionary) -> Dictionary:
359
462
  var name: String = str(params.get("name", ""))
@@ -383,19 +383,16 @@ func _readable(value: Variant, relative_to: Node) -> Variant:
383
383
  return _values.serialize_value(value)
384
384
 
385
385
 
386
- ## What [param target] stores, by name, but its script and the properties in [param leave]. A list
387
- ## or a map is copied, since a setter can change the one it holds in place.
386
+ ## What [param target] stores, by name, but its script and the properties in [param leave], in the
387
+ ## names a read answers with: a Curve's points as point_0/position and the rest rather than the _data
388
+ ## list every point write changes. A list or a map is copied, since a setter can change the one it
389
+ ## holds in place.
388
390
  func _stored(target: Object, leave: Dictionary) -> Dictionary:
389
391
  var held: Dictionary = {}
390
- for entry: Dictionary in target.get_property_list():
392
+ for entry: Dictionary in GdhShapes.read_properties(target):
391
393
  var name: String = str(entry["name"])
392
394
  var usage: int = GdhRead.as_int(entry["usage"])
393
- if (
394
- usage & PROPERTY_USAGE_STORAGE == 0
395
- or usage & HEADINGS != 0
396
- or name == "script"
397
- or leave.has(name)
398
- ):
395
+ if usage & HEADINGS != 0 or name == "script" or leave.has(name):
399
396
  continue
400
397
  var value: Variant = target.get(name)
401
398
  if value is Array:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "1.3.40",
3
+ "version": "1.3.42",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",