gdharness 1.2.30 → 1.2.32
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 +7197 -6159
- package/build/godot/addons/gdharness_editor/bridge_client.gd +14 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +237 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/plugin.gd +27 -0
- package/build/godot/addons/gdharness_editor/property_values.gd +80 -54
- package/build/godot/addons/gdharness_editor/scene_file.gd +38 -1
- package/build/godot/addons/gdharness_editor/serialisation.gd +46 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +22 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +64 -1
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +82 -44
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +178 -0
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +73 -124
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +39 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd +285 -0
- package/build/godot/addons/gdharness_runtime/runtime_hover.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +151 -106
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +112 -0
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +5 -4
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +1 -47
- package/build/godot/addons/gdharness_runtime/serialisation.gd +46 -0
- package/build/godot/operations/audio_buses.gd +14 -1
- package/build/godot/operations/class_cache.gd +146 -35
- package/build/godot/operations/classdb_queries.gd +19 -12
- package/build/godot/operations/file_reads.gd +237 -0
- package/build/godot/operations/gdscript_authoring.gd +136 -19
- package/build/godot/operations/godot_operations.gd +24 -0
- package/build/godot/operations/project_config.gd +16 -6
- package/build/godot/operations/property_values.gd +767 -0
- package/build/godot/operations/rename_checks.gd +5 -2
- package/build/godot/operations/serialisation.gd +46 -0
- package/build/index.js +4896 -3872
- package/build/keeper.js +184 -71
- package/package.json +1 -1
|
@@ -517,6 +517,20 @@ func _handle_message(json_string: String) -> void:
|
|
|
517
517
|
pass
|
|
518
518
|
|
|
519
519
|
|
|
520
|
+
## Says [param tool_name] was not run because the editor is scanning or importing, for the server to
|
|
521
|
+
## ask again once that is over. An older server reads it as an ordinary refusal.
|
|
522
|
+
func send_busy(request_id: String, tool_name: String) -> void:
|
|
523
|
+
_send_message(
|
|
524
|
+
{
|
|
525
|
+
"type": "tool_result",
|
|
526
|
+
"id": request_id,
|
|
527
|
+
"success": false,
|
|
528
|
+
"busy": true,
|
|
529
|
+
"error": "The editor is scanning or importing its files, so %s was not run." % tool_name,
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
|
|
520
534
|
func send_tool_result(request_id: String, success: bool, result: Variant = null, error: String = "") -> void:
|
|
521
535
|
var response: Dictionary = {"type": "tool_result", "id": request_id, "success": success}
|
|
522
536
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
extends RefCounted
|
|
2
|
+
|
|
3
|
+
## Scenes and resources read from their files, by the editor's tools and by a headless engine with
|
|
4
|
+
## none connected, so a read answers the same either way.
|
|
5
|
+
##
|
|
6
|
+
## A scene is read off its SceneState rather than instantiated. Instantiating outside the editor
|
|
7
|
+
## runs every script's member initialisers and _init (measured on 4.7.2, with or without -e), so a
|
|
8
|
+
## read with no editor would have run the game's code on every node; the editor gets placeholders
|
|
9
|
+
## and runs none. The state holds everything the file says, and the scenes it instances theirs,
|
|
10
|
+
## without making a node. Loading the file still does what loading does in the game: static
|
|
11
|
+
## initialisers run, and a resource with a script is made with it.
|
|
12
|
+
|
|
13
|
+
const PropertyValues = preload("property_values.gd")
|
|
14
|
+
const Read = preload("reading.gd")
|
|
15
|
+
const Serialisation = preload("serialisation.gd")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## The nodes of [param scene_path] as {name, type, path, children}, with instanceOf on a node that
|
|
19
|
+
## instances another scene and every stored property when [param include_properties] is set, the
|
|
20
|
+
## file's value or the default where it sets none. [param depth] -1 reads every level.
|
|
21
|
+
static func scene_tree(scene_path: String, depth: int, include_properties: bool) -> Dictionary:
|
|
22
|
+
if not FileAccess.file_exists(scene_path):
|
|
23
|
+
return {"ok": false, "error": "Scene not found: " + scene_path}
|
|
24
|
+
# Loaded without naming the type, so a resource handed in by mistake loads as what it is and the
|
|
25
|
+
# refusal can say so, where asking for a PackedScene printed the loader's error and a backtrace.
|
|
26
|
+
var loaded: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
27
|
+
if loaded == null:
|
|
28
|
+
return {"ok": false, "error": "Failed to load %s as a scene" % scene_path}
|
|
29
|
+
if not loaded is PackedScene:
|
|
30
|
+
return {
|
|
31
|
+
"ok": false,
|
|
32
|
+
"error":
|
|
33
|
+
(
|
|
34
|
+
"%s is a %s, not a scene: resource_edit read reads its properties."
|
|
35
|
+
% [scene_path, loaded.get_class()]
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
var packed: PackedScene = loaded
|
|
39
|
+
var nodes: Dictionary = {}
|
|
40
|
+
var refused: String = _lay(packed.get_state(), ".", nodes, [scene_path])
|
|
41
|
+
if not refused.is_empty():
|
|
42
|
+
return {"ok": false, "error": refused}
|
|
43
|
+
if not nodes.has("."):
|
|
44
|
+
return {"ok": false, "error": "%s holds no nodes" % scene_path}
|
|
45
|
+
return {"ok": true, "tree": _entry(nodes, ".", include_properties, depth, 0)}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Lays the nodes of [param state] into [param nodes] under [param prefix], keyed by their path
|
|
49
|
+
## from the outermost root, each instanced scene laid where it is instanced and the instancing
|
|
50
|
+
## file's own entries for it applied over it. Answers a refusal, or "".
|
|
51
|
+
##
|
|
52
|
+
## [param within] is the files being laid, outermost first, so a scene that instances one of them
|
|
53
|
+
## is refused rather than laid forever; the engine refuses to load such a scene, so this is only
|
|
54
|
+
## reached through a state built some other way.
|
|
55
|
+
static func _lay(state: SceneState, prefix: String, nodes: Dictionary, within: Array) -> String:
|
|
56
|
+
var base: SceneState = state.get_base_scene_state()
|
|
57
|
+
if base != null and not nodes.has(prefix):
|
|
58
|
+
var refused: String = _lay(base, prefix, nodes, within)
|
|
59
|
+
if not refused.is_empty():
|
|
60
|
+
return refused
|
|
61
|
+
for index: int in state.get_node_count():
|
|
62
|
+
var path: String = _joined(prefix, str(state.get_node_path(index)))
|
|
63
|
+
var fresh: bool = not nodes.has(path)
|
|
64
|
+
if fresh:
|
|
65
|
+
var instance: PackedScene = state.get_node_instance(index)
|
|
66
|
+
if instance != null:
|
|
67
|
+
if within.has(instance.resource_path):
|
|
68
|
+
return "%s instances %s, which holds it" % [within.back(), instance.resource_path]
|
|
69
|
+
var refused: String = _lay(
|
|
70
|
+
instance.get_state(), path, nodes, within + [instance.resource_path]
|
|
71
|
+
)
|
|
72
|
+
if not refused.is_empty():
|
|
73
|
+
return refused
|
|
74
|
+
if not nodes.has(path):
|
|
75
|
+
return "%s holds no nodes" % instance.resource_path
|
|
76
|
+
nodes[path]["instanceOf"] = instance.resource_path
|
|
77
|
+
elif state.is_node_instance_placeholder(index):
|
|
78
|
+
nodes[path] = _made("InstancePlaceholder")
|
|
79
|
+
nodes[path]["instanceOf"] = state.get_node_instance_placeholder(index)
|
|
80
|
+
elif str(state.get_node_type(index)).is_empty():
|
|
81
|
+
# An entry for a node of an instance that no longer has it, which the engine skips too.
|
|
82
|
+
continue
|
|
83
|
+
else:
|
|
84
|
+
nodes[path] = _made(str(state.get_node_type(index)))
|
|
85
|
+
# The name is the state's even for an entry laid already, since an inherited scene names its
|
|
86
|
+
# root itself; a root instanced inside another scene is renamed by that scene once laid.
|
|
87
|
+
nodes[path]["name"] = str(state.get_node_name(index))
|
|
88
|
+
if fresh:
|
|
89
|
+
# The root of what is laid here is placed by whoever instanced it.
|
|
90
|
+
if path != prefix:
|
|
91
|
+
_place(
|
|
92
|
+
nodes,
|
|
93
|
+
_joined(prefix, str(state.get_node_path(index, true))),
|
|
94
|
+
path,
|
|
95
|
+
state.get_node_index(index)
|
|
96
|
+
)
|
|
97
|
+
var held: Dictionary = nodes[path]["properties"]
|
|
98
|
+
for property: int in state.get_node_property_count(index):
|
|
99
|
+
held[str(state.get_node_property_name(index, property))] = state.get_node_property_value(
|
|
100
|
+
index, property
|
|
101
|
+
)
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
static func _made(type: String) -> Dictionary:
|
|
106
|
+
var children: Array[String] = []
|
|
107
|
+
return {"name": "", "type": type, "properties": {}, "children": children}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## Puts [param path] among [param parent]'s children, at [param at] when the file says where (a node
|
|
111
|
+
## added inside an instance) and last when it does not.
|
|
112
|
+
static func _place(nodes: Dictionary, parent: String, path: String, at: int) -> void:
|
|
113
|
+
if not nodes.has(parent):
|
|
114
|
+
return
|
|
115
|
+
var children: Array[String] = nodes[parent]["children"]
|
|
116
|
+
if at >= 0 and at < children.size():
|
|
117
|
+
var _placed: int = children.insert(at, path)
|
|
118
|
+
else:
|
|
119
|
+
children.append(path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
## [param given], a path as a SceneState gives it ("." or "./Body/Tail"), under [param prefix].
|
|
123
|
+
static func _joined(prefix: String, given: String) -> String:
|
|
124
|
+
var relative: String = given.trim_prefix("./")
|
|
125
|
+
if relative == "." or relative.is_empty():
|
|
126
|
+
return prefix
|
|
127
|
+
if prefix == ".":
|
|
128
|
+
return relative
|
|
129
|
+
return prefix + "/" + relative
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
static func _entry(
|
|
133
|
+
nodes: Dictionary, path: String, include_properties: bool, depth: int, current_depth: int
|
|
134
|
+
) -> Dictionary:
|
|
135
|
+
var node: Dictionary = nodes[path]
|
|
136
|
+
var children: Array[Dictionary] = []
|
|
137
|
+
var data: Dictionary = {"name": node["name"], "type": node["type"], "path": path, "children": children}
|
|
138
|
+
# The root's file is the scene being read, so only another node's names a second scene.
|
|
139
|
+
if path != "." and node.has("instanceOf"):
|
|
140
|
+
data["instanceOf"] = node["instanceOf"]
|
|
141
|
+
if include_properties:
|
|
142
|
+
var given: Dictionary = node["properties"]
|
|
143
|
+
data["properties"] = _stored(str(node["type"]), given)
|
|
144
|
+
if depth >= 0 and current_depth >= depth:
|
|
145
|
+
return data
|
|
146
|
+
for child: String in node["children"]:
|
|
147
|
+
children.append(_entry(nodes, child, include_properties, depth, current_depth + 1))
|
|
148
|
+
return data
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Every property a node of [param type] stores, with the value [param given] sets or the default,
|
|
152
|
+
## read as a node holding those values would answer them.
|
|
153
|
+
##
|
|
154
|
+
## The engine's half is read off a bare node of the class with the file's values set on it, which
|
|
155
|
+
## runs no script: what a node lists depends on its values (a thread group's settings appear only
|
|
156
|
+
## once it has one) and some setters set others (a Control's anchors preset sets how it grows), so
|
|
157
|
+
## the class's defaults and the file's entries alone answered neither. The script's half is its
|
|
158
|
+
## declared defaults under the file's values.
|
|
159
|
+
static func _stored(type: String, given: Dictionary) -> Dictionary:
|
|
160
|
+
var attached: Variant = given.get("script")
|
|
161
|
+
var script: Script = attached if attached is Script else null
|
|
162
|
+
var scripted: Dictionary = {}
|
|
163
|
+
if script != null:
|
|
164
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
165
|
+
var property: String = str(declared.get("name", ""))
|
|
166
|
+
if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
167
|
+
scripted[property] = script.get_property_default_value(property)
|
|
168
|
+
var values: Dictionary = {}
|
|
169
|
+
if ClassDB.class_exists(type) and ClassDB.can_instantiate(type):
|
|
170
|
+
var bare: Object = ClassDB.instantiate(type)
|
|
171
|
+
for property: Variant in given:
|
|
172
|
+
if property != "script" and not scripted.has(property):
|
|
173
|
+
bare.set(str(property), given[property])
|
|
174
|
+
for declared: Dictionary in bare.get_property_list():
|
|
175
|
+
var property: String = str(declared.get("name", ""))
|
|
176
|
+
if not property.is_empty() and Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE:
|
|
177
|
+
values[property] = bare.get(property)
|
|
178
|
+
if bare is Node:
|
|
179
|
+
bare.free()
|
|
180
|
+
for property: String in scripted:
|
|
181
|
+
values[property] = scripted[property]
|
|
182
|
+
# Given last, which keeps what a bare node cannot hold: the script itself, and a path to a node
|
|
183
|
+
# for a property that holds the node once it is in a tree.
|
|
184
|
+
values.merge(given, true)
|
|
185
|
+
var serialiser: Serialisation = Serialisation.new()
|
|
186
|
+
var answered: Dictionary = {}
|
|
187
|
+
for property: Variant in values:
|
|
188
|
+
answered[property] = serialiser.serialize_value(values[property])
|
|
189
|
+
return answered
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## What a resource file holds, in the form modify takes, so a value read here can be written back.
|
|
193
|
+
## Read from the file rather than a cached copy, since the file is what the writes change and what
|
|
194
|
+
## the project loads.
|
|
195
|
+
static func resource(res_path: String, include_defaults: bool) -> Dictionary:
|
|
196
|
+
if not FileAccess.file_exists(res_path):
|
|
197
|
+
return {"ok": false, "error": "No resource at " + res_path}
|
|
198
|
+
var loaded: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
199
|
+
if loaded == null:
|
|
200
|
+
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
201
|
+
# A scene's own properties are its packed bundle, which says nothing a caller can use.
|
|
202
|
+
if loaded is PackedScene:
|
|
203
|
+
return {
|
|
204
|
+
"ok": false,
|
|
205
|
+
"error":
|
|
206
|
+
"%s is a scene: scene_tree reads its nodes, and scene_node get one node's properties." % res_path
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var script: Script = loaded.get_script()
|
|
210
|
+
var scripted: Dictionary = {}
|
|
211
|
+
if script != null:
|
|
212
|
+
for declared: Dictionary in script.get_script_property_list():
|
|
213
|
+
scripted[str(declared.get("name", ""))] = true
|
|
214
|
+
var serialiser: Serialisation = Serialisation.new()
|
|
215
|
+
var properties: Dictionary = {}
|
|
216
|
+
for p: Dictionary in loaded.get_property_list():
|
|
217
|
+
var property: String = str(p.get("name", ""))
|
|
218
|
+
if property.is_empty() or property == "script":
|
|
219
|
+
continue
|
|
220
|
+
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
221
|
+
continue
|
|
222
|
+
var value: Variant = loaded.get(property)
|
|
223
|
+
if not include_defaults:
|
|
224
|
+
var default: Variant = (
|
|
225
|
+
script.get_property_default_value(property)
|
|
226
|
+
if scripted.has(property)
|
|
227
|
+
else ClassDB.class_get_property_default_value(loaded.get_class(), property)
|
|
228
|
+
)
|
|
229
|
+
if PropertyValues.same(value, default):
|
|
230
|
+
continue
|
|
231
|
+
properties[property] = serialiser.serialize_value(value)
|
|
232
|
+
var answer: Dictionary = {
|
|
233
|
+
"ok": true, "resourcePath": res_path, "resourceType": loaded.get_class(), "properties": properties
|
|
234
|
+
}
|
|
235
|
+
if script != null:
|
|
236
|
+
answer["script"] = script.resource_path
|
|
237
|
+
return answer
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://c1lf7gjr8v1ux
|
|
@@ -8,6 +8,17 @@ extends EditorPlugin
|
|
|
8
8
|
const BridgeClient = preload("bridge_client.gd")
|
|
9
9
|
const ToolExecutor = preload("tool_executor.gd")
|
|
10
10
|
|
|
11
|
+
## The tools answered while the editor scans or imports, because none of them saves a file: the
|
|
12
|
+
## ones a caller waits on a scan with, and the ones that only read or stop a played game.
|
|
13
|
+
const ANSWERED_WHILE_SCANNING: Array[String] = [
|
|
14
|
+
"scan_status",
|
|
15
|
+
"rescan_filesystem",
|
|
16
|
+
"reimport_files",
|
|
17
|
+
"playing_status",
|
|
18
|
+
"stop_playing",
|
|
19
|
+
"profile_read",
|
|
20
|
+
]
|
|
21
|
+
|
|
11
22
|
var _client: BridgeClient
|
|
12
23
|
var _tool_executor: ToolExecutor
|
|
13
24
|
var _status_label: Label
|
|
@@ -85,10 +96,26 @@ func _on_disconnected() -> void:
|
|
|
85
96
|
_status_label.add_theme_color_override("font_color", Color.RED)
|
|
86
97
|
|
|
87
98
|
|
|
99
|
+
## Whether [param tool_name] waits for the editor to finish scanning or importing. A save during a
|
|
100
|
+
## scan updates the file list the scan's thread is writing out (see SceneFile.scan_later), and an
|
|
101
|
+
## import can run frames of the editor from inside itself, the one handling this call among them.
|
|
102
|
+
func turned_away(tool_name: String) -> bool:
|
|
103
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
104
|
+
return (
|
|
105
|
+
not ANSWERED_WHILE_SCANNING.has(tool_name) and (filesystem.is_scanning() or filesystem.is_importing())
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
88
109
|
func _on_tool_requested(request_id: String, tool_name: String, args: Dictionary) -> void:
|
|
89
110
|
if _tool_executor == null or _client == null:
|
|
90
111
|
return
|
|
91
112
|
|
|
113
|
+
# Turned away rather than held, so the server asks again: a call held here could run after the
|
|
114
|
+
# server had given up on it.
|
|
115
|
+
if turned_away(tool_name):
|
|
116
|
+
_client.send_busy(request_id, tool_name)
|
|
117
|
+
return
|
|
118
|
+
|
|
92
119
|
var result: Dictionary = _tool_executor.execute_tool(tool_name, args)
|
|
93
120
|
var success: bool = result.get("ok", false)
|
|
94
121
|
|
|
@@ -162,6 +162,9 @@ func cannot_hold(
|
|
|
162
162
|
## max_value) is set once more after the rest, so the order a caller wrote them in does not decide
|
|
163
163
|
## what is saved.
|
|
164
164
|
##
|
|
165
|
+
## A name may be a colon path into a property's components, "position:x" or "region_rect:size:y",
|
|
166
|
+
## spelled as the runtime tools spell it, so one axis can be written without restating the others.
|
|
167
|
+
##
|
|
165
168
|
## Nothing here saves, so a refusal partway leaves the change in memory and the file as it was. The
|
|
166
169
|
## caller returns the refusal instead of saving.
|
|
167
170
|
func write_all(target: Object, properties: Dictionary) -> String:
|
|
@@ -169,19 +172,23 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
169
172
|
for key: Variant in properties:
|
|
170
173
|
var property: String = str(key)
|
|
171
174
|
var declared: Dictionary = declaration(target, property)
|
|
175
|
+
if declared.is_empty() and ":" in property:
|
|
176
|
+
declared = _component_slot(target, property)
|
|
177
|
+
if declared.has("refused"):
|
|
178
|
+
return str(declared["refused"])
|
|
172
179
|
if declared.is_empty():
|
|
173
180
|
return "%s has no property %s" % [target.get_class(), property]
|
|
174
181
|
var value: Array = value_for(target, declared, properties[key])
|
|
175
182
|
if not value[0]:
|
|
176
183
|
return str(value[1])
|
|
177
|
-
target
|
|
184
|
+
_write(target, property, value[1])
|
|
178
185
|
given[property] = value[1]
|
|
179
186
|
|
|
180
187
|
for property: String in given:
|
|
181
|
-
if not same(target
|
|
182
|
-
target
|
|
188
|
+
if not same(read_at(target, property), given[property]):
|
|
189
|
+
_write(target, property, given[property])
|
|
183
190
|
for property: String in given:
|
|
184
|
-
var held: Variant = target
|
|
191
|
+
var held: Variant = read_at(target, property)
|
|
185
192
|
if not same(held, given[property]):
|
|
186
193
|
return (
|
|
187
194
|
(
|
|
@@ -193,6 +200,58 @@ func write_all(target: Object, properties: Dictionary) -> String:
|
|
|
193
200
|
return ""
|
|
194
201
|
|
|
195
202
|
|
|
203
|
+
## What [param target] holds under [param key], a property or a colon path into one's components.
|
|
204
|
+
func read_at(target: Object, key: String) -> Variant:
|
|
205
|
+
if ":" in key and declaration(target, key).is_empty():
|
|
206
|
+
return target.get_indexed(NodePath(key))
|
|
207
|
+
return target.get(key)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## Writes [param value] under [param key], putting a changed component back into the property it
|
|
211
|
+
## was read out of, and that one into the one above it.
|
|
212
|
+
func _write(target: Object, key: String, value: Variant) -> void:
|
|
213
|
+
if ":" in key and declaration(target, key).is_empty():
|
|
214
|
+
target.set_indexed(NodePath(key), value)
|
|
215
|
+
else:
|
|
216
|
+
target.set(key, value)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
## The declaration the last step of [param path] would have, typed as the component it reaches, so
|
|
220
|
+
## a fraction for an integer vector's axis is refused as one for an int property is; or
|
|
221
|
+
## {"refused": why} for the first step that is not there.
|
|
222
|
+
func _component_slot(target: Object, path: String) -> Dictionary:
|
|
223
|
+
var steps: PackedStringArray = path.split(":")
|
|
224
|
+
var owner: String = target.get_class()
|
|
225
|
+
if declaration(target, steps[0]).is_empty():
|
|
226
|
+
return {"refused": "%s has no property %s" % [owner, steps[0]]}
|
|
227
|
+
var value: Variant = target.get(steps[0])
|
|
228
|
+
var walked: String = steps[0]
|
|
229
|
+
for index: int in range(1, steps.size()):
|
|
230
|
+
var step: String = steps[index]
|
|
231
|
+
var has: PackedStringArray = Serialisation.components_of(value)
|
|
232
|
+
var kind: String = type_string(typeof(value))
|
|
233
|
+
var sort: String = ("an " if kind.left(1).to_upper() in ["A", "E", "I", "O", "U"] else "a ") + kind
|
|
234
|
+
if has.is_empty():
|
|
235
|
+
return {
|
|
236
|
+
"refused":
|
|
237
|
+
(
|
|
238
|
+
(
|
|
239
|
+
"%s.%s is %s, which a path does not step into: a path reaches the components of a "
|
|
240
|
+
+ "vector, a colour, a rectangle or a transform."
|
|
241
|
+
)
|
|
242
|
+
% [owner, walked, sort]
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
if not step in has:
|
|
246
|
+
return {
|
|
247
|
+
"refused":
|
|
248
|
+
"%s.%s is %s, so it has no %s; it has %s" % [owner, walked, sort, step, ", ".join(has)]
|
|
249
|
+
}
|
|
250
|
+
value = value[step]
|
|
251
|
+
walked += ":" + step
|
|
252
|
+
return {"name": path, "type": typeof(value), "usage": PROPERTY_USAGE_DEFAULT}
|
|
253
|
+
|
|
254
|
+
|
|
196
255
|
## [param raw] as the value [param declared] takes, as [ok, value] or [false, why not].
|
|
197
256
|
func value_for(target: Object, declared: Dictionary, raw: Variant) -> Array:
|
|
198
257
|
var property: String = str(declared.get("name", ""))
|
|
@@ -607,57 +666,24 @@ func _parse_new_resource(type_tag: String, value: Dictionary) -> Array:
|
|
|
607
666
|
return [true, built]
|
|
608
667
|
|
|
609
668
|
|
|
610
|
-
## A dictionary with no tag, read against the type the property declares
|
|
611
|
-
##
|
|
612
|
-
##
|
|
613
|
-
## a
|
|
669
|
+
## A dictionary with no tag, read against the type the property declares, by the fields the tagged
|
|
670
|
+
## form of that type carries: the shape a read answers with, untagged, is a value too. A rectangle
|
|
671
|
+
## written as the position and size a read gave was refused while the four numbers were taken, and
|
|
672
|
+
## a Rect2i, an AABB or a transform had no untagged form at all. Anything else is a dictionary the
|
|
673
|
+
## property genuinely wants, and its values are read as values in their own right: kept as the tags
|
|
674
|
+
## they arrived as, a Vector2 read off one node and written to another was saved as a dictionary
|
|
675
|
+
## holding the word "Vector2".
|
|
614
676
|
func _parse_shaped_dictionary(value: Dictionary, expected_type: int, elements: Array) -> Variant:
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
Read.as_float(value["x"]), Read.as_float(value["y"]), Read.as_float(value["z"])
|
|
626
|
-
)
|
|
627
|
-
TYPE_VECTOR3I:
|
|
628
|
-
if value.has("x") and value.has("y") and value.has("z"):
|
|
629
|
-
return Vector3i(Read.as_int(value["x"]), Read.as_int(value["y"]), Read.as_int(value["z"]))
|
|
630
|
-
TYPE_VECTOR4:
|
|
631
|
-
if value.has("x") and value.has("y") and value.has("z") and value.has("w"):
|
|
632
|
-
return Vector4(
|
|
633
|
-
Read.as_float(value["x"]),
|
|
634
|
-
Read.as_float(value["y"]),
|
|
635
|
-
Read.as_float(value["z"]),
|
|
636
|
-
Read.as_float(value["w"])
|
|
637
|
-
)
|
|
638
|
-
TYPE_COLOR:
|
|
639
|
-
if value.has("r") and value.has("g") and value.has("b"):
|
|
640
|
-
return Color(
|
|
641
|
-
Read.as_float(value["r"]),
|
|
642
|
-
Read.as_float(value["g"]),
|
|
643
|
-
Read.as_float(value["b"]),
|
|
644
|
-
Read.as_float(value.get("a", 1), 1.0)
|
|
645
|
-
)
|
|
646
|
-
TYPE_RECT2:
|
|
647
|
-
if value.has("x") and value.has("y") and value.has("width") and value.has("height"):
|
|
648
|
-
return Rect2(
|
|
649
|
-
Read.as_float(value["x"]),
|
|
650
|
-
Read.as_float(value["y"]),
|
|
651
|
-
Read.as_float(value["width"]),
|
|
652
|
-
Read.as_float(value["height"])
|
|
653
|
-
)
|
|
654
|
-
TYPE_NODE_PATH:
|
|
655
|
-
if value.has("path"):
|
|
656
|
-
return NodePath(str(value["path"]))
|
|
657
|
-
if expected_type != TYPE_NIL and expected_type != TYPE_DICTIONARY:
|
|
658
|
-
return value
|
|
659
|
-
|
|
660
|
-
return _parse_entries(value, elements)
|
|
677
|
+
if expected_type == TYPE_NIL or expected_type == TYPE_DICTIONARY:
|
|
678
|
+
return _parse_entries(value, elements)
|
|
679
|
+
var tag: String = type_string(expected_type)
|
|
680
|
+
if Serialisation.REQUIRED.has(tag):
|
|
681
|
+
var tagged: Dictionary = value.duplicate()
|
|
682
|
+
tagged["_type"] = tag
|
|
683
|
+
var built: Variant = _values.deserialize_value(tagged)
|
|
684
|
+
if typeof(built) == expected_type:
|
|
685
|
+
return built
|
|
686
|
+
return value
|
|
661
687
|
|
|
662
688
|
|
|
663
689
|
## The entries of a dictionary a property holds, each value read as a value in its own right, and
|
|
@@ -19,6 +19,10 @@ extends RefCounted
|
|
|
19
19
|
|
|
20
20
|
const PropertyValues = preload("property_values.gd")
|
|
21
21
|
|
|
22
|
+
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
23
|
+
## for the editor's files to settle has to wait through as well.
|
|
24
|
+
static var scan_due: bool = false
|
|
25
|
+
|
|
22
26
|
var root: Node = null
|
|
23
27
|
var path: String = ""
|
|
24
28
|
var _base_paths: Dictionary = {}
|
|
@@ -249,11 +253,44 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
|
|
|
249
253
|
static func refresh(scene_path: String) -> void:
|
|
250
254
|
if ResourceLoader.has_cached(scene_path):
|
|
251
255
|
var _replaced: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_REPLACE)
|
|
252
|
-
|
|
256
|
+
scan_later()
|
|
253
257
|
if EditorInterface.get_open_scenes().has(scene_path):
|
|
254
258
|
EditorInterface.reload_scene_from_path(scene_path)
|
|
255
259
|
|
|
256
260
|
|
|
261
|
+
## Has the editor scan its files once the call in hand has answered, rather than now.
|
|
262
|
+
##
|
|
263
|
+
## A scan's thread ends by writing the editor's file list to its cache, walking the list as it goes,
|
|
264
|
+
## while each save made in the editor updates an entry of that list on the main thread and frees
|
|
265
|
+
## what the entry held (4.7.2's [code]_save_filesystem_cache[/code] and
|
|
266
|
+
## [code]EditorNode::_resource_saved[/code]). A scene saved 250 ms after a resource's scan began was
|
|
267
|
+
## followed by garbage decoded as text and a dropped connection. Started at the end of the frame,
|
|
268
|
+
## the scan begins after every save the call makes, and the plugin turns away calls that could save
|
|
269
|
+
## until it is over.
|
|
270
|
+
static func scan_later() -> void:
|
|
271
|
+
if scan_due:
|
|
272
|
+
return
|
|
273
|
+
scan_due = true
|
|
274
|
+
_start_scan.call_deferred()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
## [method EditorFileSystem.scan] does nothing, and says nothing, while the thread of the scan
|
|
278
|
+
## before it is still to be joined, which is a frame or two after that scan stops reporting itself,
|
|
279
|
+
## so a scan that leaves the editor not scanning is asked for once more once that is over. Asked of
|
|
280
|
+
## a scan that ended within the call, the second costs one more scan.
|
|
281
|
+
static func _start_scan() -> void:
|
|
282
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
283
|
+
var tree: SceneTree = Engine.get_main_loop()
|
|
284
|
+
while filesystem.is_scanning() or filesystem.is_importing():
|
|
285
|
+
await tree.process_frame
|
|
286
|
+
filesystem.scan()
|
|
287
|
+
if not filesystem.is_scanning():
|
|
288
|
+
await tree.process_frame
|
|
289
|
+
await tree.process_frame
|
|
290
|
+
filesystem.scan()
|
|
291
|
+
scan_due = false
|
|
292
|
+
|
|
293
|
+
|
|
257
294
|
## Whether the editor holds unsaved changes to [param scene_path], for a read to say that the file
|
|
258
295
|
## it answers from is not what the editor shows.
|
|
259
296
|
static func unsaved(scene_path: String) -> bool:
|
|
@@ -246,6 +246,52 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
246
246
|
return call(builder, fields)
|
|
247
247
|
|
|
248
248
|
|
|
249
|
+
## The components a path reads off [param value], by the names GDScript gives them, or none for a
|
|
250
|
+
## value that has none: a vector's axes, a colour's channels, a rectangle's corners and size, a
|
|
251
|
+
## transform's basis and origin. A value of one of these is a copy wherever it is read, so a write
|
|
252
|
+
## into a component is a write into the copy until it is put back where it was read from.
|
|
253
|
+
static func components_of(value: Variant) -> PackedStringArray:
|
|
254
|
+
match typeof(value):
|
|
255
|
+
TYPE_VECTOR2, TYPE_VECTOR2I:
|
|
256
|
+
return PackedStringArray(["x", "y"])
|
|
257
|
+
TYPE_VECTOR3, TYPE_VECTOR3I:
|
|
258
|
+
return PackedStringArray(["x", "y", "z"])
|
|
259
|
+
TYPE_VECTOR4, TYPE_VECTOR4I, TYPE_QUATERNION:
|
|
260
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
261
|
+
TYPE_COLOR:
|
|
262
|
+
return PackedStringArray(
|
|
263
|
+
[
|
|
264
|
+
"r",
|
|
265
|
+
"g",
|
|
266
|
+
"b",
|
|
267
|
+
"a",
|
|
268
|
+
"r8",
|
|
269
|
+
"g8",
|
|
270
|
+
"b8",
|
|
271
|
+
"a8",
|
|
272
|
+
"h",
|
|
273
|
+
"s",
|
|
274
|
+
"v",
|
|
275
|
+
"ok_hsl_h",
|
|
276
|
+
"ok_hsl_s",
|
|
277
|
+
"ok_hsl_l"
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
TYPE_RECT2, TYPE_RECT2I, TYPE_AABB:
|
|
281
|
+
return PackedStringArray(["position", "size", "end"])
|
|
282
|
+
TYPE_PLANE:
|
|
283
|
+
return PackedStringArray(["normal", "x", "y", "z", "d"])
|
|
284
|
+
TYPE_BASIS:
|
|
285
|
+
return PackedStringArray(["x", "y", "z"])
|
|
286
|
+
TYPE_TRANSFORM2D:
|
|
287
|
+
return PackedStringArray(["x", "y", "origin"])
|
|
288
|
+
TYPE_TRANSFORM3D:
|
|
289
|
+
return PackedStringArray(["basis", "origin"])
|
|
290
|
+
TYPE_PROJECTION:
|
|
291
|
+
return PackedStringArray(["x", "y", "z", "w"])
|
|
292
|
+
return PackedStringArray()
|
|
293
|
+
|
|
294
|
+
|
|
249
295
|
## Whether [param entries] is a list of [key, value] pairs, as a Dictionary tag carries them.
|
|
250
296
|
static func pairs(entries: Variant) -> bool:
|
|
251
297
|
if not entries is Array:
|
|
@@ -95,6 +95,7 @@ func _init_tools() -> void:
|
|
|
95
95
|
"rescan_filesystem": [_filesystem_tools, "rescan_filesystem"],
|
|
96
96
|
"scan_status": [_filesystem_tools, "scan_status"],
|
|
97
97
|
"reimport_files": [_filesystem_tools, "reimport_files"],
|
|
98
|
+
"files_moved": [_filesystem_tools, "files_moved"],
|
|
98
99
|
"global_classes": [_class_tools, "global_classes"],
|
|
99
100
|
"reload_script": [_class_tools, "reload_script"],
|
|
100
101
|
# Settings, for a caller that asked the editor rather than the file on disk
|
|
@@ -117,9 +117,31 @@ func reload_script(args: Dictionary) -> Dictionary:
|
|
|
117
117
|
answer["restoredAs"] = error_string(restored)
|
|
118
118
|
answer["methods"] = _method_names(script)
|
|
119
119
|
answer["constants"] = _constant_names(script)
|
|
120
|
+
_reread_open_tabs(path, answer)
|
|
120
121
|
return answer
|
|
121
122
|
|
|
122
123
|
|
|
124
|
+
## Has the script editor's tabs reread what changed on disk, as the editor does when its window takes
|
|
125
|
+
## the focus, which a hidden editor's never does. A tab with unsaved edits is left alone and named:
|
|
126
|
+
## saving it writes its text over the file, and rereading would ask a question nobody can answer in a
|
|
127
|
+
## hidden editor's dialog.
|
|
128
|
+
func _reread_open_tabs(path: String, answer: Dictionary) -> void:
|
|
129
|
+
# The engine fixtures reload from a script run outside any editor, where there are no tabs and
|
|
130
|
+
# EditorInterface has none of its methods.
|
|
131
|
+
if not Engine.is_editor_hint():
|
|
132
|
+
return
|
|
133
|
+
var scripts: ScriptEditor = EditorInterface.get_script_editor()
|
|
134
|
+
var unsaved: PackedStringArray = scripts.get_unsaved_files()
|
|
135
|
+
if path in unsaved:
|
|
136
|
+
answer["unsavedInEditor"] = true
|
|
137
|
+
return
|
|
138
|
+
var rereads: Variant = EditorInterface.get_editor_settings().get_setting(
|
|
139
|
+
"text_editor/behavior/files/auto_reload_scripts_on_external_change"
|
|
140
|
+
)
|
|
141
|
+
if unsaved.is_empty() and rereads is bool and rereads:
|
|
142
|
+
scripts.reload_open_files()
|
|
143
|
+
|
|
144
|
+
|
|
123
145
|
## [param path] as the files on disk spell it, a step at a time, or as given where a step is not
|
|
124
146
|
## there. On a filesystem that ignores case, `res://Core/Rules.gd` opens the file at
|
|
125
147
|
## `res://core/rules.gd`, but the editor holds its copy under the spelling on disk, and a load by
|