gdharness 1.2.31 → 1.2.33
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 +7211 -6230
- 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/scene_file.gd +38 -1
- 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 +4 -41
- 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 +54 -120
- 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/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/property_values.gd +767 -0
- package/build/godot/operations/rename_checks.gd +5 -2
- package/build/index.js +5437 -4470
- 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
|
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends Node
|
|
3
3
|
|
|
4
|
-
## The editor's view of the files on disk: scans, the reimports asked for,
|
|
4
|
+
## The editor's view of the files on disk: scans, the reimports asked for, where each stands, and
|
|
5
|
+
## files moved under it.
|
|
5
6
|
|
|
6
7
|
const Read = preload("../reading.gd")
|
|
8
|
+
const SceneFile = preload("../scene_file.gd")
|
|
7
9
|
|
|
8
10
|
var _editor_plugin: EditorPlugin = null
|
|
9
11
|
|
|
@@ -170,6 +172,7 @@ func scan_status(_args: Dictionary) -> Dictionary:
|
|
|
170
172
|
"ok": true,
|
|
171
173
|
"scanning": filesystem.is_scanning(),
|
|
172
174
|
"importing": filesystem.is_importing(),
|
|
175
|
+
"pending": SceneFile.scan_due,
|
|
173
176
|
"sinceScanFinishedMs":
|
|
174
177
|
-1 if _scan_finished_at_msec < 0 else Time.get_ticks_msec() - _scan_finished_at_msec,
|
|
175
178
|
"reimportsCompleted": _reimports_completed,
|
|
@@ -206,6 +209,66 @@ func reimport_files(args: Dictionary) -> Dictionary:
|
|
|
206
209
|
}
|
|
207
210
|
|
|
208
211
|
|
|
212
|
+
## Brings the editor up to files moved under it, the way its FileSystem dock brings itself up after
|
|
213
|
+
## a move of its own: each cached resource and each open scene named for where it now is, so what
|
|
214
|
+
## holds them saves the new path, the editor's file list told of both ends, and each open scene that
|
|
215
|
+
## moved or that the move rewrote read again from the file. A scene with unsaved edits is left as it
|
|
216
|
+
## is and named, since reading it again would discard them; its root names the new path either way.
|
|
217
|
+
##
|
|
218
|
+
## A moved tab is read again even when its text did not change: the editor keeps each tab's path
|
|
219
|
+
## twice, on the root and in its own list, and a script can set only the first. The second is what
|
|
220
|
+
## [method EditorInterface.get_open_scenes] answers, and reading the scene again is what writes it.
|
|
221
|
+
func files_moved(args: Dictionary) -> Dictionary:
|
|
222
|
+
var renames: Dictionary[String, String] = {}
|
|
223
|
+
var given: Variant = args.get("moved", [])
|
|
224
|
+
if given is Array:
|
|
225
|
+
var pairs: Array = given
|
|
226
|
+
for pair: Variant in pairs:
|
|
227
|
+
if pair is Dictionary:
|
|
228
|
+
var named: Dictionary = pair
|
|
229
|
+
renames[str(named.get("from", ""))] = str(named.get("to", ""))
|
|
230
|
+
for from: String in renames:
|
|
231
|
+
if ResourceLoader.has_cached(from):
|
|
232
|
+
var held: Resource = ResourceLoader.get_cached_ref(from)
|
|
233
|
+
if held != null:
|
|
234
|
+
held.take_over_path(renames[from])
|
|
235
|
+
var dirty: Array[String] = []
|
|
236
|
+
for path: String in EditorInterface.get_unsaved_scenes():
|
|
237
|
+
dirty.append(renames.get(path, path))
|
|
238
|
+
var retargeted: Array[String] = []
|
|
239
|
+
var reread: Array[String] = []
|
|
240
|
+
for root: Node in EditorInterface.get_open_scene_roots():
|
|
241
|
+
var path: String = root.scene_file_path
|
|
242
|
+
if renames.has(path):
|
|
243
|
+
root.scene_file_path = renames[path]
|
|
244
|
+
retargeted.append(path)
|
|
245
|
+
reread.append(renames[path])
|
|
246
|
+
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
247
|
+
for from: String in renames:
|
|
248
|
+
filesystem.update_file(from)
|
|
249
|
+
filesystem.update_file(renames[from])
|
|
250
|
+
var open: Array[String] = []
|
|
251
|
+
for root: Node in EditorInterface.get_open_scene_roots():
|
|
252
|
+
open.append(root.scene_file_path)
|
|
253
|
+
var changed: Variant = args.get("changed", [])
|
|
254
|
+
if changed is Array:
|
|
255
|
+
var paths: Array = changed
|
|
256
|
+
for path: Variant in paths:
|
|
257
|
+
if open.has(str(path)) and not reread.has(str(path)):
|
|
258
|
+
reread.append(str(path))
|
|
259
|
+
var reloaded: Array[String] = []
|
|
260
|
+
var unsaved: Array[String] = []
|
|
261
|
+
for scene: String in reread:
|
|
262
|
+
if dirty.has(scene):
|
|
263
|
+
unsaved.append(scene)
|
|
264
|
+
else:
|
|
265
|
+
EditorInterface.reload_scene_from_path(scene)
|
|
266
|
+
reloaded.append(scene)
|
|
267
|
+
reloaded.sort()
|
|
268
|
+
unsaved.sort()
|
|
269
|
+
return {"ok": true, "retargetedTabs": retargeted, "reloadedScenes": reloaded, "unsavedScenes": unsaved}
|
|
270
|
+
|
|
271
|
+
|
|
209
272
|
func _reimport(paths: PackedStringArray) -> void:
|
|
210
273
|
var filesystem: EditorFileSystem = EditorInterface.get_resource_filesystem()
|
|
211
274
|
# A scan can begin between the answer and this frame, and its import would refuse this one.
|
|
@@ -3,10 +3,10 @@ extends Node
|
|
|
3
3
|
|
|
4
4
|
## Resource files, shaders, tilesets and themes, written through the open editor.
|
|
5
5
|
|
|
6
|
+
const FileReads = preload("../file_reads.gd")
|
|
6
7
|
const PropertyValues = preload("../property_values.gd")
|
|
7
8
|
const Read = preload("../reading.gd")
|
|
8
9
|
const SceneFile = preload("../scene_file.gd")
|
|
9
|
-
const Serialisation = preload("../serialisation.gd")
|
|
10
10
|
|
|
11
11
|
# The minimal shader of a type: its declaration and the one function the engine calls for it.
|
|
12
12
|
const SHADER_EMPTY: String = """shader_type %s;
|
|
@@ -23,7 +23,6 @@ const CELLS_SHOWN: int = 1000
|
|
|
23
23
|
|
|
24
24
|
var _editor_plugin: EditorPlugin = null
|
|
25
25
|
var _properties: PropertyValues = PropertyValues.new()
|
|
26
|
-
var _values: Serialisation = Serialisation.new()
|
|
27
26
|
|
|
28
27
|
|
|
29
28
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -48,7 +47,7 @@ func _written(path: String) -> void:
|
|
|
48
47
|
if ResourceLoader.has_cached(path):
|
|
49
48
|
var _replaced: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
|
|
50
49
|
if _editor_plugin:
|
|
51
|
-
|
|
50
|
+
SceneFile.scan_later()
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
## Why a tool that creates [param path] must not write there, or "". Saving over a file that was
|
|
@@ -239,48 +238,12 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
239
238
|
return {"ok": true, "resourcePath": res_path}
|
|
240
239
|
|
|
241
240
|
|
|
242
|
-
## What a resource file holds,
|
|
243
|
-
## Read from the file rather than the editor's cached copy, since the file is what the other ops
|
|
244
|
-
## write and what the project loads.
|
|
241
|
+
## What a resource file holds, read as a headless engine reads it with no editor connected.
|
|
245
242
|
func read_resource(args: Dictionary) -> Dictionary:
|
|
246
243
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
247
|
-
var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
|
|
248
244
|
if res_path == "res://":
|
|
249
245
|
return {"ok": false, "error": "resourcePath is required"}
|
|
250
|
-
|
|
251
|
-
return {"ok": false, "error": "No resource at " + res_path}
|
|
252
|
-
var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
253
|
-
if resource == null:
|
|
254
|
-
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
255
|
-
|
|
256
|
-
var script: Script = resource.get_script()
|
|
257
|
-
var scripted: Dictionary = {}
|
|
258
|
-
if script != null:
|
|
259
|
-
for declared: Dictionary in script.get_script_property_list():
|
|
260
|
-
scripted[str(declared.get("name", ""))] = true
|
|
261
|
-
var properties: Dictionary = {}
|
|
262
|
-
for p: Dictionary in resource.get_property_list():
|
|
263
|
-
var property: String = str(p.get("name", ""))
|
|
264
|
-
if property.is_empty() or property == "script":
|
|
265
|
-
continue
|
|
266
|
-
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
267
|
-
continue
|
|
268
|
-
var value: Variant = resource.get(property)
|
|
269
|
-
if not include_defaults:
|
|
270
|
-
var default: Variant = (
|
|
271
|
-
script.get_property_default_value(property)
|
|
272
|
-
if scripted.has(property)
|
|
273
|
-
else ClassDB.class_get_property_default_value(resource.get_class(), property)
|
|
274
|
-
)
|
|
275
|
-
if PropertyValues.same(value, default):
|
|
276
|
-
continue
|
|
277
|
-
properties[property] = _values.serialize_value(value)
|
|
278
|
-
var answer: Dictionary = {
|
|
279
|
-
"ok": true, "resourcePath": res_path, "resourceType": resource.get_class(), "properties": properties
|
|
280
|
-
}
|
|
281
|
-
if script != null:
|
|
282
|
-
answer["script"] = script.resource_path
|
|
283
|
-
return answer
|
|
246
|
+
return FileReads.resource(res_path, Read.as_bool(args.get("includeDefaults", false)))
|
|
284
247
|
|
|
285
248
|
|
|
286
249
|
func create_shader(args: Dictionary) -> Dictionary:
|