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
|
@@ -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;
|
|
@@ -18,9 +18,11 @@ const SHADER_ENTRY: Dictionary[String, String] = {
|
|
|
18
18
|
"canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
## How many cells a read of a tile map lists, enough for a level's worth and not a whole world's.
|
|
22
|
+
const CELLS_SHOWN: int = 1000
|
|
23
|
+
|
|
21
24
|
var _editor_plugin: EditorPlugin = null
|
|
22
25
|
var _properties: PropertyValues = PropertyValues.new()
|
|
23
|
-
var _values: Serialisation = Serialisation.new()
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -45,7 +47,7 @@ func _written(path: String) -> void:
|
|
|
45
47
|
if ResourceLoader.has_cached(path):
|
|
46
48
|
var _replaced: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_REPLACE)
|
|
47
49
|
if _editor_plugin:
|
|
48
|
-
|
|
50
|
+
SceneFile.scan_later()
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
## Why a tool that creates [param path] must not write there, or "". Saving over a file that was
|
|
@@ -126,8 +128,8 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
|
|
|
126
128
|
else:
|
|
127
129
|
for key: Variant in names:
|
|
128
130
|
var property: String = str(key)
|
|
129
|
-
var read: Variant =
|
|
130
|
-
if not PropertyValues.same(read,
|
|
131
|
+
var read: Variant = _properties.read_at(loaded, property)
|
|
132
|
+
if not PropertyValues.same(read, _properties.read_at(resource, property)):
|
|
131
133
|
lost = (
|
|
132
134
|
(
|
|
133
135
|
"%s.%s would load from the file as %s: it is not a property the file keeps, so "
|
|
@@ -236,48 +238,12 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
236
238
|
return {"ok": true, "resourcePath": res_path}
|
|
237
239
|
|
|
238
240
|
|
|
239
|
-
## What a resource file holds,
|
|
240
|
-
## Read from the file rather than the editor's cached copy, since the file is what the other ops
|
|
241
|
-
## write and what the project loads.
|
|
241
|
+
## What a resource file holds, read as a headless engine reads it with no editor connected.
|
|
242
242
|
func read_resource(args: Dictionary) -> Dictionary:
|
|
243
243
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
244
|
-
var include_defaults: bool = Read.as_bool(args.get("includeDefaults", false))
|
|
245
244
|
if res_path == "res://":
|
|
246
245
|
return {"ok": false, "error": "resourcePath is required"}
|
|
247
|
-
|
|
248
|
-
return {"ok": false, "error": "No resource at " + res_path}
|
|
249
|
-
var resource: Resource = ResourceLoader.load(res_path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
250
|
-
if resource == null:
|
|
251
|
-
return {"ok": false, "error": "%s did not load as a resource." % res_path}
|
|
252
|
-
|
|
253
|
-
var script: Script = resource.get_script()
|
|
254
|
-
var scripted: Dictionary = {}
|
|
255
|
-
if script != null:
|
|
256
|
-
for declared: Dictionary in script.get_script_property_list():
|
|
257
|
-
scripted[str(declared.get("name", ""))] = true
|
|
258
|
-
var properties: Dictionary = {}
|
|
259
|
-
for p: Dictionary in resource.get_property_list():
|
|
260
|
-
var property: String = str(p.get("name", ""))
|
|
261
|
-
if property.is_empty() or property == "script":
|
|
262
|
-
continue
|
|
263
|
-
if not (Read.as_int(p.get("usage", 0)) & PROPERTY_USAGE_STORAGE):
|
|
264
|
-
continue
|
|
265
|
-
var value: Variant = resource.get(property)
|
|
266
|
-
if not include_defaults:
|
|
267
|
-
var default: Variant = (
|
|
268
|
-
script.get_property_default_value(property)
|
|
269
|
-
if scripted.has(property)
|
|
270
|
-
else ClassDB.class_get_property_default_value(resource.get_class(), property)
|
|
271
|
-
)
|
|
272
|
-
if PropertyValues.same(value, default):
|
|
273
|
-
continue
|
|
274
|
-
properties[property] = _values.serialize_value(value)
|
|
275
|
-
var answer: Dictionary = {
|
|
276
|
-
"ok": true, "resourcePath": res_path, "resourceType": resource.get_class(), "properties": properties
|
|
277
|
-
}
|
|
278
|
-
if script != null:
|
|
279
|
-
answer["script"] = script.resource_path
|
|
280
|
-
return answer
|
|
246
|
+
return FileReads.resource(res_path, Read.as_bool(args.get("includeDefaults", false)))
|
|
281
247
|
|
|
282
248
|
|
|
283
249
|
func create_shader(args: Dictionary) -> Dictionary:
|
|
@@ -552,7 +518,13 @@ func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
|
552
518
|
var atlas_coords: Vector2i = atlas_at[1]
|
|
553
519
|
var alternative: int = Read.as_int(cell.get("alternativeTile", 0))
|
|
554
520
|
if not source.has_tile(atlas_coords):
|
|
555
|
-
return [
|
|
521
|
+
return [
|
|
522
|
+
false,
|
|
523
|
+
(
|
|
524
|
+
"cells[%d]: source %d has no tile at %s; it has %s. Nothing was placed."
|
|
525
|
+
% [index, source_id, atlas_coords, _tiles_of(source)]
|
|
526
|
+
)
|
|
527
|
+
]
|
|
556
528
|
if not source.has_alternative_tile(atlas_coords, alternative):
|
|
557
529
|
return [
|
|
558
530
|
false,
|
|
@@ -561,6 +533,72 @@ func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
|
561
533
|
return [true, at[1], source_id, atlas_coords, alternative]
|
|
562
534
|
|
|
563
535
|
|
|
536
|
+
## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
|
|
537
|
+
## has none: a refusal naming the one missing sent a caller guessing which of the others exist.
|
|
538
|
+
static func _tiles_of(source: TileSetSource) -> String:
|
|
539
|
+
var count: int = source.get_tiles_count()
|
|
540
|
+
if count == 0:
|
|
541
|
+
return "no tiles"
|
|
542
|
+
var named: Array[String] = []
|
|
543
|
+
for index: int in mini(count, 12):
|
|
544
|
+
named.append(str(source.get_tile_id(index)))
|
|
545
|
+
var more: String = "" if count <= 12 else " and %d more" % (count - 12)
|
|
546
|
+
return "tiles at " + ", ".join(PackedStringArray(named)) + more
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
## The cells a TileMapLayer or a TileMap holds, as {"cellCount", "cells"} in the form
|
|
550
|
+
## set_tilemap_cells takes, or {} for any other node. The scene keeps them as packed bytes, which a
|
|
551
|
+
## read answered with and nobody could read: a probe counted three cells by decoding the base64 by
|
|
552
|
+
## hand. At most CELLS_SHOWN are listed, the count saying how many there are.
|
|
553
|
+
static func cells_of(node: Node) -> Dictionary:
|
|
554
|
+
var listed: Array[Dictionary] = []
|
|
555
|
+
var count: int = 0
|
|
556
|
+
var single: TileMapLayer = node as TileMapLayer
|
|
557
|
+
var layered: TileMap = node as TileMap
|
|
558
|
+
if single != null:
|
|
559
|
+
var used: Array[Vector2i] = single.get_used_cells()
|
|
560
|
+
count = used.size()
|
|
561
|
+
for at: Vector2i in used.slice(0, CELLS_SHOWN):
|
|
562
|
+
listed.append(
|
|
563
|
+
_cell_entry(
|
|
564
|
+
at,
|
|
565
|
+
single.get_cell_source_id(at),
|
|
566
|
+
single.get_cell_atlas_coords(at),
|
|
567
|
+
single.get_cell_alternative_tile(at)
|
|
568
|
+
)
|
|
569
|
+
)
|
|
570
|
+
elif layered != null:
|
|
571
|
+
for layer: int in layered.get_layers_count():
|
|
572
|
+
var used: Array[Vector2i] = layered.get_used_cells(layer)
|
|
573
|
+
count += used.size()
|
|
574
|
+
for at: Vector2i in used:
|
|
575
|
+
if listed.size() >= CELLS_SHOWN:
|
|
576
|
+
break
|
|
577
|
+
var entry: Dictionary = _cell_entry(
|
|
578
|
+
at,
|
|
579
|
+
layered.get_cell_source_id(layer, at),
|
|
580
|
+
layered.get_cell_atlas_coords(layer, at),
|
|
581
|
+
layered.get_cell_alternative_tile(layer, at)
|
|
582
|
+
)
|
|
583
|
+
entry["layer"] = layer
|
|
584
|
+
listed.append(entry)
|
|
585
|
+
else:
|
|
586
|
+
return {}
|
|
587
|
+
var answer: Dictionary = {"cellCount": count, "cells": listed}
|
|
588
|
+
if count > listed.size():
|
|
589
|
+
answer["cellsNote"] = "The first %d of %d cells are listed." % [listed.size(), count]
|
|
590
|
+
return answer
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
static func _cell_entry(at: Vector2i, source_id: int, atlas: Vector2i, alternative: int) -> Dictionary:
|
|
594
|
+
return {
|
|
595
|
+
"coords": {"x": at.x, "y": at.y},
|
|
596
|
+
"sourceId": source_id,
|
|
597
|
+
"atlasCoords": {"x": atlas.x, "y": atlas.y},
|
|
598
|
+
"alternativeTile": alternative,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
564
602
|
func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
|
|
565
603
|
var layered: TileMap = node as TileMap
|
|
566
604
|
if layered != null:
|
|
@@ -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
|