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.
- package/build/cli.js +343 -142
- package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +36 -1
- package/build/godot/addons/gdharness_editor/file_walk.gd +33 -1
- package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +6 -9
- package/build/godot/addons/gdharness_editor/scene_file.gd +27 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +5 -2
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +6 -2
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +15 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +7 -2
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd +59 -0
- package/build/godot/addons/gdharness_editor/tools/scene_sharing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +75 -11
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +77 -22
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +4 -1
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +4 -3
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd +211 -0
- package/build/godot/addons/gdharness_runtime/runtime_touch.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +15 -8
- package/build/godot/operations/animation_reads.gd +31 -4
- package/build/godot/operations/dependencies.gd +5 -2
- package/build/godot/operations/file_reads.gd +36 -1
- package/build/godot/operations/file_walk.gd +33 -1
- package/build/godot/operations/gdscript_analysis.gd +19 -1
- package/build/godot/operations/gdscript_authoring.gd +65 -4
- package/build/godot/operations/import_pipeline.gd +1 -1
- package/build/godot/operations/input_actions.gd +1 -0
- package/build/godot/operations/plugins.gd +39 -1
- package/build/godot/operations/project_config.gd +104 -1
- package/build/godot/operations/property_values.gd +6 -9
- 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
|
|
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
|
-
|
|
636
|
-
|
|
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
|
-
|
|
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]
|
|
387
|
-
##
|
|
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
|
|
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.
|
|
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",
|