gdharness 1.2.31 → 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 +7212 -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 -4469
- package/build/keeper.js +184 -71
- package/package.json +1 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## What a new node in a scene is made or copied from, what the scene owns of it, and where among its
|
|
5
|
+
## siblings a node may go.
|
|
6
|
+
|
|
7
|
+
const Read = preload("../reading.gd")
|
|
8
|
+
const PropertyValues = preload("../property_values.gd")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## The scene to instance for an add, or null for a node of a class, beside why neither can be made
|
|
12
|
+
## ("" when one can), as [PackedScene, String].
|
|
13
|
+
static func what_to_add(node_type: String, instance_path: String, scene_path: String) -> Array:
|
|
14
|
+
if node_type.is_empty() == instance_path.is_empty():
|
|
15
|
+
return [
|
|
16
|
+
null,
|
|
17
|
+
"add takes nodeType for a node of a class, or instancePath for a scene, and one of them only."
|
|
18
|
+
]
|
|
19
|
+
if not instance_path.is_empty():
|
|
20
|
+
var cycle: String = _instancing_cycle(scene_path, instance_path)
|
|
21
|
+
if not cycle.is_empty():
|
|
22
|
+
return [null, cycle]
|
|
23
|
+
if not ResourceLoader.exists(instance_path, "PackedScene"):
|
|
24
|
+
return [null, "No scene at %s to instance." % instance_path]
|
|
25
|
+
var packed: PackedScene = ResourceLoader.load(instance_path, "PackedScene") as PackedScene
|
|
26
|
+
if packed == null or not packed.can_instantiate():
|
|
27
|
+
return [null, "%s does not load as a scene that can be instanced." % instance_path]
|
|
28
|
+
return [packed, ""]
|
|
29
|
+
if node_type.begins_with("res://") or node_type.get_extension() in ["tscn", "scn"]:
|
|
30
|
+
return [
|
|
31
|
+
null,
|
|
32
|
+
(
|
|
33
|
+
"nodeType must be a node class, and %s is a scene: pass it as instancePath to instance it."
|
|
34
|
+
% node_type
|
|
35
|
+
)
|
|
36
|
+
]
|
|
37
|
+
if not ClassDB.class_exists(node_type) or not ClassDB.is_parent_class(node_type, "Node"):
|
|
38
|
+
return [null, "nodeType must be a node class, and %s is not one." % node_type]
|
|
39
|
+
if not ClassDB.can_instantiate(node_type):
|
|
40
|
+
return [null, "%s cannot be instantiated." % node_type]
|
|
41
|
+
return [null, ""]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## A new node of [param node_type], or an instance of [param packed] when there is one. The instance
|
|
45
|
+
## is made in the editor's edit state, so the pack writes it as an instance of its file, and owning
|
|
46
|
+
## its root leaves its own nodes to the instanced scene.
|
|
47
|
+
static func made(packed: PackedScene, node_type: String) -> Node:
|
|
48
|
+
if packed != null:
|
|
49
|
+
return packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
|
|
50
|
+
var node: Object = ClassDB.instantiate(node_type)
|
|
51
|
+
return node as Node
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## A copy of [param source] and the nodes under it that [param root] owns, each copy recorded in
|
|
55
|
+
## [param copies] against its source, or null when an instance in it no longer loads.
|
|
56
|
+
static func copied(source: Node, root: Node, copies: Dictionary) -> Node:
|
|
57
|
+
var copy: Node = null
|
|
58
|
+
if not source.scene_file_path.is_empty():
|
|
59
|
+
# An instanced scene made again from its file, so the copy is an instance too. Node.duplicate
|
|
60
|
+
# makes a plain node of the instance's class, and the pack saved it with every property of the
|
|
61
|
+
# instance's root frozen into this scene.
|
|
62
|
+
var packed: PackedScene = ResourceLoader.load(source.scene_file_path, "PackedScene") as PackedScene
|
|
63
|
+
if packed == null:
|
|
64
|
+
return null
|
|
65
|
+
copy = packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
|
|
66
|
+
for declared: Dictionary in source.get_property_list():
|
|
67
|
+
if not (Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
68
|
+
continue
|
|
69
|
+
var property: String = str(declared.get("name", ""))
|
|
70
|
+
var value: Variant = source.get(property)
|
|
71
|
+
if not PropertyValues.same(copy.get(property), value):
|
|
72
|
+
copy.set(property, value)
|
|
73
|
+
for group: StringName in source.get_groups():
|
|
74
|
+
if not copy.is_in_group(group):
|
|
75
|
+
copy.add_to_group(group, true)
|
|
76
|
+
else:
|
|
77
|
+
copy = source.duplicate(Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)
|
|
78
|
+
for child: Node in copy.get_children():
|
|
79
|
+
copy.remove_child(child)
|
|
80
|
+
child.free()
|
|
81
|
+
copy.name = source.name
|
|
82
|
+
copies[source] = copy
|
|
83
|
+
# Only what this scene owns: an instance's own nodes come with the instance.
|
|
84
|
+
for child: Node in source.get_children():
|
|
85
|
+
if child.owner == root:
|
|
86
|
+
var under: Node = copied(child, root, copies)
|
|
87
|
+
if under == null:
|
|
88
|
+
return null
|
|
89
|
+
copy.add_child(under)
|
|
90
|
+
return copy
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
## Gives each copy in [param copies] the persistent connections its source has, to the copy of the
|
|
94
|
+
## target when the target was copied too. A connection an instance makes inside itself is skipped:
|
|
95
|
+
## the instance made again from its file already has it.
|
|
96
|
+
static func reconnect(copies: Dictionary, top: Node) -> void:
|
|
97
|
+
for key: Variant in copies:
|
|
98
|
+
var source: Node = key
|
|
99
|
+
var copy: Node = copies[key]
|
|
100
|
+
for info: Dictionary in source.get_signal_list():
|
|
101
|
+
var signal_name: String = str(info.get("name", ""))
|
|
102
|
+
for conn: Dictionary in source.get_signal_connection_list(signal_name):
|
|
103
|
+
var flags: int = Read.as_int(conn.get("flags", 0))
|
|
104
|
+
var callable: Callable = conn.get("callable", Callable())
|
|
105
|
+
var target: Object = callable.get_object()
|
|
106
|
+
if not flags & Object.CONNECT_PERSIST or target == null:
|
|
107
|
+
continue
|
|
108
|
+
var to: Object = copies.get(target, target)
|
|
109
|
+
if to == target and target is Node and (target == top or top.is_ancestor_of(target as Node)):
|
|
110
|
+
continue
|
|
111
|
+
var aimed: Callable = Callable(to, callable.get_method())
|
|
112
|
+
if (
|
|
113
|
+
not copy.is_connected(signal_name, aimed)
|
|
114
|
+
and copy.connect(signal_name, aimed, flags) != OK
|
|
115
|
+
):
|
|
116
|
+
push_error(
|
|
117
|
+
"gdharness: could not copy the connection %s of %s" % [signal_name, source.name]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
## Owns [param node] and the nodes under it to [param root], except those an instanced scene
|
|
122
|
+
## inside it owns: giving those to the root made the pack write the instance's own nodes into this
|
|
123
|
+
## scene as overrides.
|
|
124
|
+
static func own(node: Node, root: Node) -> void:
|
|
125
|
+
node.owner = root
|
|
126
|
+
for child: Node in node.get_children():
|
|
127
|
+
if child.owner == null or child.owner == root:
|
|
128
|
+
own(child, root)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
## Why [param asked] is not a position [param node] can take among [param parent]'s children, or ""
|
|
132
|
+
## when it is, or when none was asked for. [param node] is null for a node not yet added.
|
|
133
|
+
static func unplaceable(parent: Node, node: Node, asked: Variant) -> String:
|
|
134
|
+
if asked == null:
|
|
135
|
+
return ""
|
|
136
|
+
var others: int = parent.get_child_count() - (1 if node != null and node.get_parent() == parent else 0)
|
|
137
|
+
var at: int = Read.as_int(asked, -1)
|
|
138
|
+
if at < 0 or at > others:
|
|
139
|
+
return (
|
|
140
|
+
"index %s is not a place among the %d other children of %s: give 0 to %d, 0 for first."
|
|
141
|
+
% [
|
|
142
|
+
str(at) if asked is int or asked is float else JSON.stringify(asked),
|
|
143
|
+
others,
|
|
144
|
+
parent.name,
|
|
145
|
+
others
|
|
146
|
+
]
|
|
147
|
+
)
|
|
148
|
+
return ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
## Moves [param node] to [param asked] among its siblings, when a place was asked for.
|
|
152
|
+
static func place(node: Node, asked: Variant) -> void:
|
|
153
|
+
if asked != null:
|
|
154
|
+
node.get_parent().move_child(node, Read.as_int(asked))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
## Why [param instanced] cannot be instanced into [param scene_path], or "" when it can: a scene
|
|
158
|
+
## inside itself, directly or through the scenes it instances, which Godot's own dialog refuses and
|
|
159
|
+
## which never finishes loading.
|
|
160
|
+
static func _instancing_cycle(scene_path: String, instanced: String) -> String:
|
|
161
|
+
var seen: Dictionary[String, bool] = {}
|
|
162
|
+
var waiting: Array[String] = [instanced]
|
|
163
|
+
while not waiting.is_empty():
|
|
164
|
+
var at: String = waiting.pop_back()
|
|
165
|
+
if at == scene_path:
|
|
166
|
+
return (
|
|
167
|
+
"%s cannot be instanced into %s: it %s, so the scene would hold itself."
|
|
168
|
+
% [instanced, scene_path, "is that scene" if at == instanced else "instances it"]
|
|
169
|
+
)
|
|
170
|
+
if seen.has(at):
|
|
171
|
+
continue
|
|
172
|
+
seen[at] = true
|
|
173
|
+
for dependency: String in ResourceLoader.get_dependencies(at):
|
|
174
|
+
# Slices joined by "::", the UID, the type and the path, some of them empty.
|
|
175
|
+
for part: String in dependency.split("::"):
|
|
176
|
+
if part.begins_with("res://") and part.get_extension() in ["tscn", "scn"]:
|
|
177
|
+
waiting.append(part)
|
|
178
|
+
return ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://bgnjhedq2qj3t
|
|
@@ -3,7 +3,9 @@ extends Node
|
|
|
3
3
|
|
|
4
4
|
## Scenes and their nodes, edited in the open editor and read back from what it holds.
|
|
5
5
|
|
|
6
|
+
const FileReads = preload("../file_reads.gd")
|
|
6
7
|
const Read = preload("../reading.gd")
|
|
8
|
+
const Placing = preload("scene_placing.gd")
|
|
7
9
|
const PropertyValues = preload("../property_values.gd")
|
|
8
10
|
const ResourceTools = preload("resource_tools.gd")
|
|
9
11
|
const SceneFile = preload("../scene_file.gd")
|
|
@@ -114,81 +116,6 @@ func _kept(scene: SceneFile, saved: Node, node: Node, names: Array) -> String:
|
|
|
114
116
|
return ""
|
|
115
117
|
|
|
116
118
|
|
|
117
|
-
## Owns [param node] and the nodes under it to [param root], except those an instanced scene
|
|
118
|
-
## inside it owns: giving those to the root made the pack write the instance's own nodes into this
|
|
119
|
-
## scene as overrides.
|
|
120
|
-
func _copy(source: Node, root: Node, copies: Dictionary) -> Node:
|
|
121
|
-
var copy: Node = null
|
|
122
|
-
if not source.scene_file_path.is_empty():
|
|
123
|
-
# An instanced scene made again from its file, so the copy is an instance too. Node.duplicate
|
|
124
|
-
# makes a plain node of the instance's class, and the pack saved it with every property of the
|
|
125
|
-
# instance's root frozen into this scene.
|
|
126
|
-
var packed: PackedScene = ResourceLoader.load(source.scene_file_path, "PackedScene") as PackedScene
|
|
127
|
-
if packed == null:
|
|
128
|
-
return null
|
|
129
|
-
copy = packed.instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
|
|
130
|
-
for declared: Dictionary in source.get_property_list():
|
|
131
|
-
if not (Read.as_int(declared.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
132
|
-
continue
|
|
133
|
-
var property: String = str(declared.get("name", ""))
|
|
134
|
-
var value: Variant = source.get(property)
|
|
135
|
-
if not PropertyValues.same(copy.get(property), value):
|
|
136
|
-
copy.set(property, value)
|
|
137
|
-
for group: StringName in source.get_groups():
|
|
138
|
-
if not copy.is_in_group(group):
|
|
139
|
-
copy.add_to_group(group, true)
|
|
140
|
-
else:
|
|
141
|
-
copy = source.duplicate(Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS)
|
|
142
|
-
for child: Node in copy.get_children():
|
|
143
|
-
copy.remove_child(child)
|
|
144
|
-
child.free()
|
|
145
|
-
copy.name = source.name
|
|
146
|
-
copies[source] = copy
|
|
147
|
-
# Only what this scene owns: an instance's own nodes come with the instance.
|
|
148
|
-
for child: Node in source.get_children():
|
|
149
|
-
if child.owner == root:
|
|
150
|
-
var copied: Node = _copy(child, root, copies)
|
|
151
|
-
if copied == null:
|
|
152
|
-
return null
|
|
153
|
-
copy.add_child(copied)
|
|
154
|
-
return copy
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
## Gives each copy in [param copies] the persistent connections its source has, to the copy of the
|
|
158
|
-
## target when the target was copied too. A connection an instance makes inside itself is skipped:
|
|
159
|
-
## the instance made again from its file already has it.
|
|
160
|
-
func _reconnect(copies: Dictionary, top: Node) -> void:
|
|
161
|
-
for key: Variant in copies:
|
|
162
|
-
var source: Node = key
|
|
163
|
-
var copy: Node = copies[key]
|
|
164
|
-
for info: Dictionary in source.get_signal_list():
|
|
165
|
-
var signal_name: String = str(info.get("name", ""))
|
|
166
|
-
for conn: Dictionary in source.get_signal_connection_list(signal_name):
|
|
167
|
-
var flags: int = Read.as_int(conn.get("flags", 0))
|
|
168
|
-
var callable: Callable = conn.get("callable", Callable())
|
|
169
|
-
var target: Object = callable.get_object()
|
|
170
|
-
if not flags & Object.CONNECT_PERSIST or target == null:
|
|
171
|
-
continue
|
|
172
|
-
var to: Object = copies.get(target, target)
|
|
173
|
-
if to == target and target is Node and (target == top or top.is_ancestor_of(target as Node)):
|
|
174
|
-
continue
|
|
175
|
-
var copied: Callable = Callable(to, callable.get_method())
|
|
176
|
-
if (
|
|
177
|
-
not copy.is_connected(signal_name, copied)
|
|
178
|
-
and copy.connect(signal_name, copied, flags) != OK
|
|
179
|
-
):
|
|
180
|
-
push_error(
|
|
181
|
-
"gdharness: could not copy the connection %s of %s" % [signal_name, source.name]
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
func _own(node: Node, root: Node) -> void:
|
|
186
|
-
node.owner = root
|
|
187
|
-
for child: Node in node.get_children():
|
|
188
|
-
if child.owner == null or child.owner == root:
|
|
189
|
-
_own(child, root)
|
|
190
|
-
|
|
191
|
-
|
|
192
119
|
## [param node]'s stored properties as a read answers them. A property holding a node is answered
|
|
193
120
|
## as the path to it: the copy read here is never in the tree, and an object description named a
|
|
194
121
|
## node that was freed when the call ended.
|
|
@@ -241,27 +168,6 @@ func _parse_properties_arg(raw_properties: Variant) -> Dictionary:
|
|
|
241
168
|
return {}
|
|
242
169
|
|
|
243
170
|
|
|
244
|
-
func _build_node_tree(
|
|
245
|
-
node: Node, include_properties: bool, depth: int, current_depth: int, node_path: String
|
|
246
|
-
) -> Dictionary:
|
|
247
|
-
var children: Array[Dictionary] = []
|
|
248
|
-
var data: Dictionary = {
|
|
249
|
-
"name": str(node.name), "type": node.get_class(), "path": node_path, "children": children
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if include_properties:
|
|
253
|
-
data["properties"] = _properties_of(node, true, {})
|
|
254
|
-
|
|
255
|
-
if depth >= 0 and current_depth >= depth:
|
|
256
|
-
return data
|
|
257
|
-
|
|
258
|
-
for child: Node in node.get_children():
|
|
259
|
-
var child_path: String = str(child.name) if node_path == "." else node_path + "/" + str(child.name)
|
|
260
|
-
children.append(_build_node_tree(child, include_properties, depth, current_depth + 1, child_path))
|
|
261
|
-
|
|
262
|
-
return data
|
|
263
|
-
|
|
264
|
-
|
|
265
171
|
func _collect_nodes_recursive(node: Node, path: String, out_nodes: Array) -> void:
|
|
266
172
|
out_nodes.append({"path": path, "node": node})
|
|
267
173
|
for child: Node in node.get_children():
|
|
@@ -350,31 +256,29 @@ func list_scene_nodes(args: Dictionary) -> Dictionary:
|
|
|
350
256
|
var depth: int = Read.as_int(args.get("depth", -1), -1)
|
|
351
257
|
var include_properties: bool = Read.as_bool(args.get("includeProperties", false))
|
|
352
258
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
var
|
|
356
|
-
if
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
var tree: Dictionary = _build_node_tree(scene.root, include_properties, depth, 0, ".")
|
|
360
|
-
scene.close()
|
|
361
|
-
return {"ok": true, "tree": tree, "unsavedInEditor": SceneFile.unsaved(scene_path)}
|
|
259
|
+
# Read as a headless engine reads it with no editor connected, so the answer does not depend on
|
|
260
|
+
# whether one is.
|
|
261
|
+
var read: Dictionary = FileReads.scene_tree(scene_path, depth, include_properties)
|
|
262
|
+
if read.get("ok", false):
|
|
263
|
+
read["unsavedInEditor"] = SceneFile.unsaved(scene_path)
|
|
264
|
+
return read
|
|
362
265
|
|
|
363
266
|
|
|
364
267
|
func add_node(args: Dictionary) -> Dictionary:
|
|
365
268
|
var project_path: String = str(args.get("projectPath", ""))
|
|
366
269
|
var scene_path: String = _to_scene_res_path(project_path, str(args.get("scenePath", "")))
|
|
367
270
|
var node_type: String = str(args.get("nodeType", ""))
|
|
271
|
+
var instance_path: String = str(args.get("instancePath", ""))
|
|
368
272
|
var node_name: String = str(args.get("nodeName", ""))
|
|
369
273
|
var parent_node_path: String = str(args.get("parentNodePath", "."))
|
|
370
274
|
var properties: Dictionary = _parse_properties_arg(args.get("properties", {}))
|
|
371
275
|
|
|
372
|
-
if
|
|
373
|
-
return {"ok": false, "error": "Missing
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if not
|
|
377
|
-
return {"ok": false, "error":
|
|
276
|
+
if node_name.is_empty():
|
|
277
|
+
return {"ok": false, "error": "Missing nodeName"}
|
|
278
|
+
var making: Array = Placing.what_to_add(node_type, instance_path, scene_path)
|
|
279
|
+
var packed: PackedScene = making[0]
|
|
280
|
+
if not str(making[1]).is_empty():
|
|
281
|
+
return {"ok": false, "error": making[1]}
|
|
378
282
|
|
|
379
283
|
var opened: Array = _open(scene_path, true)
|
|
380
284
|
var scene: SceneFile = opened[0]
|
|
@@ -392,24 +296,39 @@ func add_node(args: Dictionary) -> Dictionary:
|
|
|
392
296
|
var unusable: String = scene.unusable_name(parent, node_name)
|
|
393
297
|
if not unusable.is_empty():
|
|
394
298
|
return scene.refuse(unusable)
|
|
299
|
+
var place: Variant = args.get("index")
|
|
300
|
+
var unplaceable: String = Placing.unplaceable(parent, null, place)
|
|
301
|
+
if not unplaceable.is_empty():
|
|
302
|
+
return scene.refuse(unplaceable)
|
|
395
303
|
|
|
396
304
|
# In the tree before its properties, so a property holding a node can find it by path.
|
|
397
|
-
var new_node: Node =
|
|
305
|
+
var new_node: Node = Placing.made(packed, node_type)
|
|
398
306
|
new_node.name = node_name
|
|
399
307
|
parent.add_child(new_node)
|
|
400
|
-
|
|
308
|
+
Placing.place(new_node, place)
|
|
309
|
+
Placing.own(new_node, root)
|
|
401
310
|
var refused_property: String = _properties.write_all(new_node, properties)
|
|
402
311
|
if not refused_property.is_empty():
|
|
403
312
|
return scene.refuse(refused_property)
|
|
404
313
|
|
|
405
314
|
var new_path: String = scene.path_of(new_node)
|
|
315
|
+
# Read before the write, which frees the scene it was added to.
|
|
316
|
+
var answer: Dictionary = {
|
|
317
|
+
"ok": true,
|
|
318
|
+
"nodeName": node_name,
|
|
319
|
+
"nodeType": new_node.get_class(),
|
|
320
|
+
"nodePath": new_path,
|
|
321
|
+
"index": new_node.get_index(),
|
|
322
|
+
}
|
|
406
323
|
var written: Dictionary = scene.write(
|
|
407
324
|
func(saved: Node) -> String: return _kept(scene, saved, new_node, properties.keys())
|
|
408
325
|
)
|
|
409
326
|
if not written.is_empty():
|
|
410
327
|
return written
|
|
411
328
|
|
|
412
|
-
|
|
329
|
+
if packed != null:
|
|
330
|
+
answer["instanceOf"] = instance_path
|
|
331
|
+
return answer
|
|
413
332
|
|
|
414
333
|
|
|
415
334
|
func delete_node(args: Dictionary) -> Dictionary:
|
|
@@ -488,14 +407,14 @@ func duplicate_node(args: Dictionary) -> Dictionary:
|
|
|
488
407
|
return scene.refuse(unusable)
|
|
489
408
|
|
|
490
409
|
var copies: Dictionary = {}
|
|
491
|
-
var duplicated_node: Node =
|
|
410
|
+
var duplicated_node: Node = Placing.copied(source, root, copies)
|
|
492
411
|
if not duplicated_node:
|
|
493
412
|
return scene.refuse("Failed to duplicate node: " + node_path)
|
|
494
413
|
|
|
495
414
|
duplicated_node.name = new_name
|
|
496
415
|
target_parent.add_child(duplicated_node)
|
|
497
|
-
|
|
498
|
-
|
|
416
|
+
Placing.own(duplicated_node, root)
|
|
417
|
+
Placing.reconnect(copies, source)
|
|
499
418
|
|
|
500
419
|
var new_path: String = scene.path_of(duplicated_node)
|
|
501
420
|
var written: Dictionary = scene.write(
|
|
@@ -549,25 +468,40 @@ func reparent_node(args: Dictionary) -> Dictionary:
|
|
|
549
468
|
var unusable: String = scene.unusable_name(new_parent, str(node.name), node)
|
|
550
469
|
if not unusable.is_empty():
|
|
551
470
|
return scene.refuse(unusable)
|
|
471
|
+
var place: Variant = args.get("index")
|
|
472
|
+
var unplaceable: String = Placing.unplaceable(new_parent, node, place)
|
|
473
|
+
if not unplaceable.is_empty():
|
|
474
|
+
return scene.refuse(unplaceable)
|
|
552
475
|
|
|
553
476
|
var old_path: String = scene.path_of(node)
|
|
554
477
|
node.get_parent().remove_child(node)
|
|
555
478
|
new_parent.add_child(node)
|
|
556
|
-
|
|
479
|
+
Placing.place(node, place)
|
|
480
|
+
Placing.own(node, root)
|
|
557
481
|
|
|
558
482
|
var new_path: String = scene.path_of(node)
|
|
483
|
+
var index: int = node.get_index()
|
|
559
484
|
var written: Dictionary = scene.write(
|
|
560
485
|
func(saved: Node) -> String:
|
|
561
|
-
|
|
486
|
+
var copy: Node = saved.get_node_or_null(new_path)
|
|
487
|
+
if copy == null:
|
|
562
488
|
return "%s would not be in the scene when it loads." % new_path
|
|
563
489
|
if old_path != new_path and saved.get_node_or_null(old_path) != null:
|
|
564
490
|
return "%s would still be in the scene as well when it loads." % old_path
|
|
491
|
+
if copy.get_index() != index:
|
|
492
|
+
return "%s would load at index %d, not %d." % [new_path, copy.get_index(), index]
|
|
565
493
|
return ""
|
|
566
494
|
)
|
|
567
495
|
if not written.is_empty():
|
|
568
496
|
return written
|
|
569
497
|
|
|
570
|
-
return {
|
|
498
|
+
return {
|
|
499
|
+
"ok": true,
|
|
500
|
+
"nodePath": old_path,
|
|
501
|
+
"newParentPath": new_parent_path,
|
|
502
|
+
"newNodePath": new_path,
|
|
503
|
+
"index": index,
|
|
504
|
+
}
|
|
571
505
|
|
|
572
506
|
|
|
573
507
|
func set_node_properties(args: Dictionary) -> Dictionary:
|
|
@@ -49,6 +49,10 @@ const PROFILER_PROBE_CALLS: int = 10000
|
|
|
49
49
|
## on every 2048th call and only counts the others.
|
|
50
50
|
const POLLS_PER_READ: int = 2048
|
|
51
51
|
|
|
52
|
+
## The most frames a close waits for its answer to leave: a client that stopped reading would
|
|
53
|
+
## otherwise hold the close for ever, and the server is past caring about the answer by then.
|
|
54
|
+
const CLOSE_AFTER_FRAMES: int = 30
|
|
55
|
+
|
|
52
56
|
## Set in a game's environment by whatever starts it to be profiled from its start: the server for
|
|
53
57
|
## a run it spawns, the editor for a play. Kept in step with `PROFILE_FROM_START` in the server.
|
|
54
58
|
const PROFILE_FROM_START: String = "GDHARNESS_PROFILE_FROM_START"
|
|
@@ -118,6 +122,8 @@ var _commands: Dictionary = {}
|
|
|
118
122
|
var _loaded_version: String = _read_marker(VERSION_MARKER)
|
|
119
123
|
var _loaded_digest: String = _read_marker(DIGEST_MARKER)
|
|
120
124
|
var _profile_received: bool = false
|
|
125
|
+
var _close_asked: bool = false
|
|
126
|
+
var _close_frames: int = 0
|
|
121
127
|
var _profiler_calibrated: bool = false
|
|
122
128
|
|
|
123
129
|
|
|
@@ -140,6 +146,7 @@ func _init() -> void:
|
|
|
140
146
|
"inject_text": _typing.inject_text,
|
|
141
147
|
"inject_mouse_click": _input.inject_mouse_click,
|
|
142
148
|
"inject_mouse_motion": _input.inject_mouse_motion,
|
|
149
|
+
"inject_mouse_drag": _input.inject_mouse_drag,
|
|
143
150
|
"inject_joypad_button": _pad.inject_joypad_button,
|
|
144
151
|
"inject_joypad_motion": _pad.inject_joypad_motion,
|
|
145
152
|
"click": _input.click,
|
|
@@ -148,6 +155,7 @@ func _init() -> void:
|
|
|
148
155
|
"wait_signal": _waits.wait_signal,
|
|
149
156
|
"wait_until": _waits.wait_until,
|
|
150
157
|
"send_profile": _send_profile_now,
|
|
158
|
+
"close_game": _close_game,
|
|
151
159
|
}
|
|
152
160
|
_wait_for_the_profiler()
|
|
153
161
|
|
|
@@ -341,6 +349,14 @@ func _process(_delta: float) -> void:
|
|
|
341
349
|
var _had_pending: bool = _pending.erase(client)
|
|
342
350
|
var _had_outgoing: bool = _outgoing.erase(client)
|
|
343
351
|
|
|
352
|
+
# After the answer to the close has gone out: the close request reaches this node too, which then
|
|
353
|
+
# drops its clients and whatever they were still owed.
|
|
354
|
+
if _close_asked:
|
|
355
|
+
_close_frames += 1
|
|
356
|
+
if _close_frames >= CLOSE_AFTER_FRAMES or _outgoing.values().all(_is_empty):
|
|
357
|
+
_close_asked = false
|
|
358
|
+
_deliver_close()
|
|
359
|
+
|
|
344
360
|
|
|
345
361
|
## Hands the socket as much of what it is owed as it will take, and says whether it is still worth
|
|
346
362
|
## talking to. Reading comes first in the frame, so a client that asked and left is noticed here
|
|
@@ -578,6 +594,29 @@ func _ping(_params: Dictionary) -> Dictionary:
|
|
|
578
594
|
return {"type": "pong", "timestamp": Time.get_unix_time_from_system()}
|
|
579
595
|
|
|
580
596
|
|
|
597
|
+
## The game asked to close, as a player's close asks it: answered at once and delivered once the
|
|
598
|
+
## answer has gone out. `acceptsQuit` says whether the tree quits on the request by itself; a game
|
|
599
|
+
## that turned that off decides for itself, and may ask the player first.
|
|
600
|
+
func _close_game(_params: Dictionary) -> Dictionary:
|
|
601
|
+
_close_asked = true
|
|
602
|
+
_close_frames = 0
|
|
603
|
+
return {"type": "closing", "acceptsQuit": get_tree().auto_accept_quit}
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
## What a player's close does: the close request reaches every node, and then the root window says
|
|
607
|
+
## it was asked to close, which the tree answers by quitting when it accepts quits. So the game's own
|
|
608
|
+
## close runs, a crash guard's included, where a stop that ended the process ran none of it.
|
|
609
|
+
func _deliver_close() -> void:
|
|
610
|
+
var root: Window = get_tree().root
|
|
611
|
+
root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
|
|
612
|
+
root.close_requested.emit()
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
static func _is_empty(owed: Variant) -> bool:
|
|
616
|
+
var bytes: PackedByteArray = owed
|
|
617
|
+
return bytes.is_empty()
|
|
618
|
+
|
|
619
|
+
|
|
581
620
|
## One JSON object and a newline, queued for [method _drain] rather than written here.
|
|
582
621
|
##
|
|
583
622
|
## Raw bytes rather than put_utf8_string, which would prefix them with a length the other side is
|