gdharness 1.3.41 → 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 +69 -38
- package/build/godot/addons/gdharness_editor/animation_reads.gd +31 -4
- package/build/godot/addons/gdharness_editor/file_reads.gd +34 -0
- package/build/godot/addons/gdharness_editor/headless_check.gd +29 -0
- 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 +64 -1
- 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/operations/animation_reads.gd +31 -4
- package/build/godot/operations/file_reads.gd +34 -0
- package/build/godot/operations/gdscript_analysis.gd +19 -1
- package/build/godot/operations/gdscript_authoring.gd +55 -3
- package/build/godot/operations/input_actions.gd +1 -0
- package/build/godot/operations/project_config.gd +93 -0
- package/package.json +1 -1
|
@@ -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:
|
|
@@ -640,9 +662,24 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
640
662
|
|
|
641
663
|
# The body's statements relative to the function, whether or not the caller indented them for
|
|
642
664
|
# it: a body written with its tab already on came out two levels in. A blank line stays empty,
|
|
643
|
-
# since an indent alone on it is trailing whitespace to every formatter.
|
|
644
|
-
|
|
645
|
-
|
|
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)
|
|
646
683
|
|
|
647
684
|
var below: int = _blank_run(lines, insert_line, 1)
|
|
648
685
|
if insert_line + below < lines.size():
|
|
@@ -654,6 +691,21 @@ func _add_function(lines: Array[String], mod: Dictionary, placed: Array[Dictiona
|
|
|
654
691
|
return declared_at if _insert(lines, insert_line, func_lines, placed) else -1
|
|
655
692
|
|
|
656
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
|
+
|
|
657
709
|
# How many blank lines run from [param from] on in the direction of [param step], 1 or -1.
|
|
658
710
|
static func _blank_run(lines: Array[String], from: int, step: int) -> int:
|
|
659
711
|
var count: int = 0
|
|
@@ -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] = []
|
|
@@ -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
|
|
|
@@ -321,6 +384,10 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
321
384
|
)
|
|
322
385
|
)
|
|
323
386
|
|
|
387
|
+
var clash: String = _autoload_name_refusal(name)
|
|
388
|
+
if not clash.is_empty():
|
|
389
|
+
return _log.failure(clash)
|
|
390
|
+
|
|
324
391
|
if not path.begins_with("res://"):
|
|
325
392
|
path = "res://" + path
|
|
326
393
|
|
|
@@ -364,6 +431,32 @@ func add_autoload(params: Dictionary) -> Dictionary:
|
|
|
364
431
|
}
|
|
365
432
|
|
|
366
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
|
+
|
|
367
460
|
# Remove an autoload singleton
|
|
368
461
|
func remove_autoload(params: Dictionary) -> Dictionary:
|
|
369
462
|
var name: String = str(params.get("name", ""))
|
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",
|