gdharness 1.1.21 → 1.1.23
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 +1470 -726
- package/build/godot/addons/gdharness_editor/bridge_client.gd +12 -2
- package/build/godot/addons/gdharness_editor/property_values.gd +484 -65
- package/build/godot/addons/gdharness_editor/reading.gd +17 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +229 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +116 -7
- package/build/godot/addons/gdharness_editor/tool_executor.gd +10 -3
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +374 -242
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +47 -9
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd +206 -0
- package/build/godot/addons/gdharness_editor/tools/filesystem_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/play_tools.gd +90 -15
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +373 -140
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +566 -472
- package/build/godot/addons/gdharness_runtime/call_errors.gd +42 -0
- package/build/godot/addons/gdharness_runtime/call_errors.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +15 -5
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +51 -5
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +440 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +184 -338
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +89 -16
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +107 -343
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +13 -1
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +90 -24
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd +351 -0
- package/build/godot/addons/gdharness_runtime/runtime_typing.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +92 -33
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +61 -28
- package/build/godot/addons/gdharness_runtime/serialisation.gd +116 -7
- package/build/godot/operations/audio_buses.gd +47 -11
- package/build/godot/operations/class_cache.gd +18 -2
- package/build/godot/operations/classdb_queries.gd +28 -17
- package/build/godot/operations/dependencies.gd +128 -17
- package/build/godot/operations/file_walk.gd +20 -0
- package/build/godot/operations/gdscript_analysis.gd +59 -20
- package/build/godot/operations/gdscript_authoring.gd +127 -60
- package/build/godot/operations/godot_operations.gd +24 -5
- package/build/godot/operations/import_pipeline.gd +179 -19
- package/build/godot/operations/logger.gd +2 -2
- package/build/godot/operations/plugins.gd +88 -70
- package/build/godot/operations/project_config.gd +41 -6
- package/build/godot/operations/project_diagnostics.gd +27 -11
- package/build/godot/operations/reading.gd +17 -0
- package/build/godot/operations/resource_files.gd +60 -17
- package/build/godot/operations/serialisation.gd +116 -7
- package/build/index.js +1385 -553
- package/build/keeper.js +4 -0
- package/package.json +1 -1
|
@@ -52,6 +52,23 @@ static func as_float(value: Variant, fallback: float = 0.0) -> float:
|
|
|
52
52
|
if value is String:
|
|
53
53
|
var text: String = value
|
|
54
54
|
return text.to_float()
|
|
55
|
+
if value is Dictionary:
|
|
56
|
+
var tagged: Dictionary = value
|
|
57
|
+
if str(tagged.get("_type", "")) == "float":
|
|
58
|
+
return non_finite(str(tagged.get("value", "")), fallback)
|
|
59
|
+
return fallback
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
## The float a non-finite tag names, "inf", "-inf" or "nan", or [param fallback] for anything else.
|
|
63
|
+
## JSON has no spelling for these, so the serialiser tags them rather than let them arrive as null.
|
|
64
|
+
static func non_finite(word: String, fallback: float = 0.0) -> float:
|
|
65
|
+
match word:
|
|
66
|
+
"inf":
|
|
67
|
+
return INF
|
|
68
|
+
"-inf":
|
|
69
|
+
return -INF
|
|
70
|
+
"nan":
|
|
71
|
+
return NAN
|
|
55
72
|
return fallback
|
|
56
73
|
|
|
57
74
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends RefCounted
|
|
3
|
+
|
|
4
|
+
## One scene file opened for a tool, read past the resource cache and written back only once what
|
|
5
|
+
## would be saved has been checked.
|
|
6
|
+
##
|
|
7
|
+
## Every scene tool edits a copy of the file rather than the editor's open scene, so three things
|
|
8
|
+
## the editor does for its own copy have to be done here. Measured on 4.7.2:
|
|
9
|
+
##
|
|
10
|
+
## - [code]load[/code] answers from the resource cache, and a write saves a new PackedScene without
|
|
11
|
+
## touching the cached one. While anything held the scene (a preload, an exported PackedScene), a
|
|
12
|
+
## second write started from the cached copy and saved over the first.
|
|
13
|
+
## - [code]instantiate()[/code] without an edit state keeps no record of which nodes came from
|
|
14
|
+
## another scene, so the pack wrote an inherited scene out as a standalone copy of its base and
|
|
15
|
+
## froze the root properties of every instanced scene into the instancing one.
|
|
16
|
+
## - [code]pack[/code] keeps only what the scene itself owns, so a property set on a node inside an
|
|
17
|
+
## instance, a node deleted from one and a node moved out of one were answered as done and were
|
|
18
|
+
## not in the file.
|
|
19
|
+
|
|
20
|
+
const PropertyValues = preload("property_values.gd")
|
|
21
|
+
|
|
22
|
+
var root: Node = null
|
|
23
|
+
var path: String = ""
|
|
24
|
+
var _base_paths: Dictionary = {}
|
|
25
|
+
var _state: SceneState = null
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Opens [param scene_path], answering a refusal, or {} with [member root] set.
|
|
29
|
+
##
|
|
30
|
+
## A write is refused while the editor holds unsaved changes to the scene: the file is reloaded
|
|
31
|
+
## into the editor after a write, which discards them without a prompt, and the editor saving its
|
|
32
|
+
## copy later would discard the write instead.
|
|
33
|
+
func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
34
|
+
path = scene_path
|
|
35
|
+
if not FileAccess.file_exists(path):
|
|
36
|
+
return {"ok": false, "error": "Scene not found: " + path}
|
|
37
|
+
if for_writing:
|
|
38
|
+
var held_back: String = unsaved_in_editor(path)
|
|
39
|
+
if not held_back.is_empty():
|
|
40
|
+
return {"ok": false, "error": held_back}
|
|
41
|
+
var packed: PackedScene = (
|
|
42
|
+
ResourceLoader.load(path, "PackedScene", ResourceLoader.CACHE_MODE_IGNORE) as PackedScene
|
|
43
|
+
)
|
|
44
|
+
if packed == null:
|
|
45
|
+
return {"ok": false, "error": "Failed to load %s as a scene" % path}
|
|
46
|
+
root = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
47
|
+
if root == null:
|
|
48
|
+
return {"ok": false, "error": "Failed to instantiate " + path}
|
|
49
|
+
_state = packed.get_state()
|
|
50
|
+
var base: SceneState = _state.get_base_scene_state()
|
|
51
|
+
while base != null:
|
|
52
|
+
for index: int in base.get_node_count():
|
|
53
|
+
_base_paths[_relative(str(base.get_node_path(index)))] = true
|
|
54
|
+
base = base.get_base_scene_state()
|
|
55
|
+
return {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
## Why [param scene_path] cannot be written while the editor has it open, or "".
|
|
59
|
+
static func unsaved_in_editor(scene_path: String) -> String:
|
|
60
|
+
if not EditorInterface.get_unsaved_scenes().has(scene_path):
|
|
61
|
+
return ""
|
|
62
|
+
return (
|
|
63
|
+
(
|
|
64
|
+
"%s has unsaved changes in the editor, and writing the file would discard them when the "
|
|
65
|
+
+ "editor reloads it. Save it first (scene_create op=save) or close it without saving."
|
|
66
|
+
)
|
|
67
|
+
% scene_path
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## Frees [member root] and answers [param error] as a refusal.
|
|
72
|
+
func refuse(error: String) -> Dictionary:
|
|
73
|
+
close()
|
|
74
|
+
return {"ok": false, "error": error}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
func close() -> void:
|
|
78
|
+
if root != null:
|
|
79
|
+
root.free()
|
|
80
|
+
root = null
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## The path of [param node] as a tool names it: "." for the root.
|
|
84
|
+
func path_of(node: Node) -> String:
|
|
85
|
+
return str(root.get_path_to(node))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Why [param node]'s own properties and connections cannot be written, or "". A node inside an
|
|
89
|
+
## instanced scene belongs to that scene's file unless its children were made editable here.
|
|
90
|
+
func foreign(node: Node) -> String:
|
|
91
|
+
if node == root or node.owner == root:
|
|
92
|
+
return ""
|
|
93
|
+
if node.owner != null and root.is_editable_instance(node.owner):
|
|
94
|
+
return ""
|
|
95
|
+
var owner_path: String = path_of(node.owner) if node.owner != null else "?"
|
|
96
|
+
return (
|
|
97
|
+
(
|
|
98
|
+
"%s belongs to the scene instanced at %s (%s), and this scene saves nothing about it. Edit "
|
|
99
|
+
+ "that scene instead."
|
|
100
|
+
)
|
|
101
|
+
% [path_of(node), owner_path, node.owner.scene_file_path if node.owner != null else "?"]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## Why [param node] cannot be moved, renamed, copied or deleted, or "": the scene may only
|
|
106
|
+
## restructure nodes it declares itself, not ones its base scene or an instance creates.
|
|
107
|
+
func fixed(node: Node) -> String:
|
|
108
|
+
if node == root:
|
|
109
|
+
return "The root of %s cannot be moved, renamed, copied or deleted." % path
|
|
110
|
+
if node.owner != root:
|
|
111
|
+
var why: String = foreign(node)
|
|
112
|
+
if why.is_empty():
|
|
113
|
+
why = "%s is inside an instanced scene, which creates it again on load." % path_of(node)
|
|
114
|
+
return why
|
|
115
|
+
var at: String = path_of(node)
|
|
116
|
+
if _base_paths.has(at):
|
|
117
|
+
return (
|
|
118
|
+
(
|
|
119
|
+
"%s comes from the scene %s inherits from, which creates it again on load. Edit that "
|
|
120
|
+
+ "scene instead."
|
|
121
|
+
)
|
|
122
|
+
% [at, path]
|
|
123
|
+
)
|
|
124
|
+
return ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
## Why [param child_name] cannot be given to a child of [param parent], or "". Godot renames a node
|
|
128
|
+
## whose name is taken or holds a character a path cannot, and the next call naming it would then
|
|
129
|
+
## reach another node.
|
|
130
|
+
func unusable_name(parent: Node, child_name: String, moving: Node = null) -> String:
|
|
131
|
+
if child_name.is_empty():
|
|
132
|
+
return "A node needs a name."
|
|
133
|
+
var valid: String = child_name.validate_node_name()
|
|
134
|
+
if valid != child_name:
|
|
135
|
+
return (
|
|
136
|
+
(
|
|
137
|
+
"%s is not a name a node can have; Godot would save it as %s. The characters "
|
|
138
|
+
+ '. : @ / " %% cannot be in a node name.'
|
|
139
|
+
)
|
|
140
|
+
% [child_name, valid]
|
|
141
|
+
)
|
|
142
|
+
var taken: Node = parent.get_node_or_null(NodePath(child_name))
|
|
143
|
+
if taken != null and taken != moving and taken.get_parent() == parent:
|
|
144
|
+
return "%s already has a child named %s." % [path_of(parent), child_name]
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
## The names of the properties this scene's own file sets on [param node], whatever their values.
|
|
149
|
+
func set_here(node: Node) -> Dictionary:
|
|
150
|
+
var names: Dictionary = {}
|
|
151
|
+
if _state == null:
|
|
152
|
+
return names
|
|
153
|
+
var at: String = path_of(node)
|
|
154
|
+
for index: int in _state.get_node_count():
|
|
155
|
+
if _relative(str(_state.get_node_path(index))) != at:
|
|
156
|
+
continue
|
|
157
|
+
for property: int in _state.get_node_property_count(index):
|
|
158
|
+
names[str(_state.get_node_property_name(index, property))] = true
|
|
159
|
+
return names
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## Whether [param loaded], read off [param saved] (a copy instantiated from the pack), is
|
|
163
|
+
## [param given], held by the node in [member root]. A node is compared by where it sits in each.
|
|
164
|
+
func kept(saved: Node, loaded: Variant, given: Variant) -> bool:
|
|
165
|
+
if loaded is Node and given is Node:
|
|
166
|
+
var loaded_node: Node = loaded
|
|
167
|
+
var given_node: Node = given
|
|
168
|
+
return str(saved.get_path_to(loaded_node)) == str(root.get_path_to(given_node))
|
|
169
|
+
return PropertyValues.same(loaded, given)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
## Packs [member root], has [param check] read the result as the file will hold it, and writes the
|
|
173
|
+
## file only if the check answers "". Frees [member root] either way.
|
|
174
|
+
##
|
|
175
|
+
## The check reads a scene instantiated from the pack, which is what loading the file gives, so
|
|
176
|
+
## anything the pack leaves out (a property with no storage, a node the scene does not own) is seen
|
|
177
|
+
## as missing before the file is touched.
|
|
178
|
+
func write(check: Callable = Callable()) -> Dictionary:
|
|
179
|
+
var packed: PackedScene = PackedScene.new()
|
|
180
|
+
var packing: Error = packed.pack(root)
|
|
181
|
+
if packing != OK:
|
|
182
|
+
return refuse("Failed to pack %s: %s" % [path, error_string(packing)])
|
|
183
|
+
if check.is_valid():
|
|
184
|
+
var saved: Node = packed.instantiate(PackedScene.GEN_EDIT_STATE_MAIN)
|
|
185
|
+
if saved == null:
|
|
186
|
+
return refuse("The packed copy of %s did not instantiate, so nothing was written." % path)
|
|
187
|
+
var missing: String = str(check.call(saved))
|
|
188
|
+
saved.free()
|
|
189
|
+
if not missing.is_empty():
|
|
190
|
+
return refuse("Nothing was written: " + missing)
|
|
191
|
+
close()
|
|
192
|
+
return save_packed(packed, path)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## Writes [param packed] to [param scene_path], making its directory, and brings the editor up to
|
|
196
|
+
## date with it. Answers a refusal, or {}.
|
|
197
|
+
static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
|
|
198
|
+
var directory: String = scene_path.get_base_dir()
|
|
199
|
+
if not DirAccess.dir_exists_absolute(directory):
|
|
200
|
+
var made: Error = DirAccess.make_dir_recursive_absolute(directory)
|
|
201
|
+
if made != OK:
|
|
202
|
+
return {"ok": false, "error": "Could not create %s: %s" % [directory, error_string(made)]}
|
|
203
|
+
var saving: Error = ResourceSaver.save(packed, scene_path)
|
|
204
|
+
if saving != OK:
|
|
205
|
+
return {"ok": false, "error": "Failed to save %s: %s" % [scene_path, error_string(saving)]}
|
|
206
|
+
refresh(scene_path)
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
## Brings the editor up to date with a scene file written under it: the cached copy anything holds,
|
|
211
|
+
## the editor's file list, and the tab showing it, whether current or not.
|
|
212
|
+
static func refresh(scene_path: String) -> void:
|
|
213
|
+
if ResourceLoader.has_cached(scene_path):
|
|
214
|
+
var _replaced: Resource = ResourceLoader.load(scene_path, "", ResourceLoader.CACHE_MODE_REPLACE)
|
|
215
|
+
EditorInterface.get_resource_filesystem().scan()
|
|
216
|
+
if EditorInterface.get_open_scenes().has(scene_path):
|
|
217
|
+
EditorInterface.reload_scene_from_path(scene_path)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## Whether the editor holds unsaved changes to [param scene_path], for a read to say that the file
|
|
221
|
+
## it answers from is not what the editor shows.
|
|
222
|
+
static func unsaved(scene_path: String) -> bool:
|
|
223
|
+
return EditorInterface.get_unsaved_scenes().has(scene_path)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
func _relative(node_path: String) -> String:
|
|
227
|
+
if node_path == ".":
|
|
228
|
+
return node_path
|
|
229
|
+
return node_path.trim_prefix("./")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://cdpx0m075k6d8
|
|
@@ -53,6 +53,9 @@ const LIST_TYPES: Array[int] = [
|
|
|
53
53
|
# type it wanted.
|
|
54
54
|
const SERIALISERS: Dictionary = {
|
|
55
55
|
TYPE_NIL: "_serialize_nil",
|
|
56
|
+
TYPE_FLOAT: "_serialize_float",
|
|
57
|
+
TYPE_PACKED_FLOAT32_ARRAY: "_serialize_floats",
|
|
58
|
+
TYPE_PACKED_FLOAT64_ARRAY: "_serialize_floats",
|
|
56
59
|
TYPE_VECTOR2: "_serialize_vector2",
|
|
57
60
|
TYPE_VECTOR2I: "_serialize_vector2i",
|
|
58
61
|
TYPE_VECTOR3: "_serialize_vector3",
|
|
@@ -105,6 +108,8 @@ const BUILDERS: Dictionary = {
|
|
|
105
108
|
"Color": "_build_color",
|
|
106
109
|
"NodePath": "_build_node_path",
|
|
107
110
|
"PackedByteArray": "_build_bytes",
|
|
111
|
+
"float": "_build_float",
|
|
112
|
+
"Dictionary": "_build_dictionary",
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
# What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
|
|
@@ -130,6 +135,8 @@ const REQUIRED: Dictionary = {
|
|
|
130
135
|
"Color": [["r", "g", "b"]],
|
|
131
136
|
"NodePath": [["path"]],
|
|
132
137
|
"PackedByteArray": [["base64"]],
|
|
138
|
+
"float": [["value"]],
|
|
139
|
+
"Dictionary": [["entries"]],
|
|
133
140
|
}
|
|
134
141
|
|
|
135
142
|
|
|
@@ -145,7 +152,58 @@ func serialize_value(value: Variant) -> Variant:
|
|
|
145
152
|
if typeof(value) == TYPE_OBJECT and not is_instance_valid(value):
|
|
146
153
|
return null
|
|
147
154
|
var serialiser: String = SERIALISERS.get(typeof(value), "")
|
|
148
|
-
|
|
155
|
+
if serialiser.is_empty():
|
|
156
|
+
return value
|
|
157
|
+
var carried: Variant = call(serialiser, value)
|
|
158
|
+
# The fixed shapes carry their components as bare floats, so an infinite one in a Vector2 is
|
|
159
|
+
# tagged here; containers reach their members through this function already.
|
|
160
|
+
return carried if typeof(value) in [TYPE_ARRAY, TYPE_DICTIONARY] else _finite(carried)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# [param carried] with every non-finite float in it tagged. JSON has no spelling for infinity or
|
|
164
|
+
# NaN: the engine writes 1e99999 and null, and the server, reading 1e99999 as Infinity, writes it
|
|
165
|
+
# back out as null, so a property holding INF was answered as holding nothing.
|
|
166
|
+
func _finite(carried: Variant) -> Variant:
|
|
167
|
+
if carried is float:
|
|
168
|
+
var number: float = carried
|
|
169
|
+
return _serialize_float(number)
|
|
170
|
+
if carried is Dictionary:
|
|
171
|
+
var fields: Dictionary = carried
|
|
172
|
+
var tagged: Dictionary = {}
|
|
173
|
+
for key: Variant in fields:
|
|
174
|
+
tagged[key] = _finite(fields[key])
|
|
175
|
+
return tagged
|
|
176
|
+
if carried is Array:
|
|
177
|
+
var items: Array = carried
|
|
178
|
+
var tagged_items: Array = []
|
|
179
|
+
for item: Variant in items:
|
|
180
|
+
tagged_items.append(_finite(item))
|
|
181
|
+
return tagged_items
|
|
182
|
+
return carried
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
func _serialize_float(value: float) -> Variant:
|
|
186
|
+
if is_finite(value):
|
|
187
|
+
return value
|
|
188
|
+
return {"_type": "float", "value": "nan" if is_nan(value) else ("inf" if value > 0 else "-inf")}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# A packed float array stays the JSON list it is unless something in it has no JSON spelling.
|
|
192
|
+
func _serialize_floats(value: Variant) -> Variant:
|
|
193
|
+
var numbers: Array = []
|
|
194
|
+
if value is PackedFloat32Array:
|
|
195
|
+
var single: PackedFloat32Array = value
|
|
196
|
+
numbers = Array(single)
|
|
197
|
+
else:
|
|
198
|
+
var double: PackedFloat64Array = value
|
|
199
|
+
numbers = Array(double)
|
|
200
|
+
for number: float in numbers:
|
|
201
|
+
if not is_finite(number):
|
|
202
|
+
var items: Array = []
|
|
203
|
+
for each: float in numbers:
|
|
204
|
+
items.append(_serialize_float(each))
|
|
205
|
+
return items
|
|
206
|
+
return value
|
|
149
207
|
|
|
150
208
|
|
|
151
209
|
# Rebuilds a Godot value from the shape serialize_value gave it.
|
|
@@ -181,9 +239,27 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
181
239
|
var needed: Array = REQUIRED.get(tag, [])
|
|
182
240
|
if builder.is_empty() or not _carries(fields, needed):
|
|
183
241
|
return fields
|
|
242
|
+
if tag == "float" and str(fields["value"]) not in ["inf", "-inf", "nan"]:
|
|
243
|
+
return fields
|
|
244
|
+
if tag == "Dictionary" and not pairs(fields["entries"]):
|
|
245
|
+
return fields
|
|
184
246
|
return call(builder, fields)
|
|
185
247
|
|
|
186
248
|
|
|
249
|
+
## Whether [param entries] is a list of [key, value] pairs, as a Dictionary tag carries them.
|
|
250
|
+
static func pairs(entries: Variant) -> bool:
|
|
251
|
+
if not entries is Array:
|
|
252
|
+
return false
|
|
253
|
+
var listed: Array = entries
|
|
254
|
+
for entry: Variant in listed:
|
|
255
|
+
if not entry is Array:
|
|
256
|
+
return false
|
|
257
|
+
var pair: Array = entry
|
|
258
|
+
if pair.size() != 2:
|
|
259
|
+
return false
|
|
260
|
+
return true
|
|
261
|
+
|
|
262
|
+
|
|
187
263
|
## Whether two values can be compared without the comparison itself failing.
|
|
188
264
|
##
|
|
189
265
|
## GDScript's `==` is not total. An Object against a String is a hard error rather than false, and
|
|
@@ -193,16 +269,19 @@ func deserialize_value(value: Variant) -> Variant:
|
|
|
193
269
|
## nothing anywhere naming the argument that did it. A wrong argument costs a refusal, never the
|
|
194
270
|
## session's game.
|
|
195
271
|
##
|
|
196
|
-
##
|
|
197
|
-
##
|
|
198
|
-
##
|
|
272
|
+
## Measured on 4.7.2 over every pair of built-in types: two values of different types compare only
|
|
273
|
+
## as an int against a float, a String against a StringName, or anything against null. Every other
|
|
274
|
+
## pair is that error, a word against a number as much as an object against a word, and so is a bool
|
|
275
|
+
## against an int and a NodePath against a String. An int against a float is kept because it is the
|
|
276
|
+
## same number to a caller waiting for 1 on a property holding 1.0.
|
|
199
277
|
static func comparable(one: Variant, other: Variant) -> bool:
|
|
200
278
|
var left: int = typeof(one)
|
|
201
279
|
var right: int = typeof(other)
|
|
202
|
-
if left
|
|
280
|
+
if left == right or left == TYPE_NIL or right == TYPE_NIL:
|
|
203
281
|
return true
|
|
204
|
-
|
|
205
|
-
|
|
282
|
+
var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
|
|
283
|
+
var texts: Array[int] = [TYPE_STRING, TYPE_STRING_NAME]
|
|
284
|
+
return (left in numbers and right in numbers) or (left in texts and right in texts)
|
|
206
285
|
|
|
207
286
|
|
|
208
287
|
## Whether [param value] can be given to something declared as [param type] without damage.
|
|
@@ -400,7 +479,16 @@ func _serialize_signal(value: Signal) -> Dictionary:
|
|
|
400
479
|
return described
|
|
401
480
|
|
|
402
481
|
|
|
482
|
+
## A dictionary keyed by text as itself, and any other as its entries. JSON keys are text, so an
|
|
483
|
+
## int or Vector2 key was answered as its text, and a dictionary read and written back had its keys
|
|
484
|
+
## replaced by words: `{1: "a"}` came back as `{"1": "a"}`.
|
|
403
485
|
func _serialize_dictionary(value: Dictionary) -> Dictionary:
|
|
486
|
+
var texts: bool = value.keys().all(func(key: Variant) -> bool: return key is String or key is StringName)
|
|
487
|
+
if not texts:
|
|
488
|
+
var entries: Array = []
|
|
489
|
+
for key: Variant in value:
|
|
490
|
+
entries.append([serialize_value(key), serialize_value(value[key])])
|
|
491
|
+
return {"_type": "Dictionary", "entries": entries}
|
|
404
492
|
var serialised: Dictionary = {}
|
|
405
493
|
for key: Variant in value:
|
|
406
494
|
serialised[str(key)] = serialize_value(value[key])
|
|
@@ -621,6 +709,27 @@ func _build_bytes(fields: Dictionary) -> PackedByteArray:
|
|
|
621
709
|
return Marshalls.base64_to_raw(str(fields.get("base64", "")))
|
|
622
710
|
|
|
623
711
|
|
|
712
|
+
func _build_float(fields: Dictionary) -> float:
|
|
713
|
+
return Read.as_float(fields)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
## The entries of a dictionary whose keys are not all text. JSON carries every number as a float,
|
|
717
|
+
## and a dictionary keys 1.0 apart from 1, so a whole-number key is built as an int: the key a
|
|
718
|
+
## game uses far more often, and one JSON cannot tell from the float.
|
|
719
|
+
func _build_dictionary(fields: Dictionary) -> Dictionary:
|
|
720
|
+
var built: Dictionary = {}
|
|
721
|
+
var entries: Array = fields["entries"]
|
|
722
|
+
for entry: Variant in entries:
|
|
723
|
+
var pair: Array = entry
|
|
724
|
+
var key: Variant = deserialize_value(pair[0])
|
|
725
|
+
if key is float:
|
|
726
|
+
var number: float = key
|
|
727
|
+
if is_finite(number) and number == floorf(number):
|
|
728
|
+
key = int(number)
|
|
729
|
+
built[key] = deserialize_value(pair[1])
|
|
730
|
+
return built
|
|
731
|
+
|
|
732
|
+
|
|
624
733
|
## One component of a compound value, as the type its constructor wants.
|
|
625
734
|
##
|
|
626
735
|
## Their own functions because a constructor refuses a Variant under `unsafe_call_argument`, and
|
|
@@ -11,6 +11,7 @@ const AnimationTools = preload("tools/animation_tools.gd")
|
|
|
11
11
|
const PlayTools = preload("tools/play_tools.gd")
|
|
12
12
|
const ClassTools = preload("tools/class_tools.gd")
|
|
13
13
|
const SettingsTools = preload("tools/settings_tools.gd")
|
|
14
|
+
const FilesystemTools = preload("tools/filesystem_tools.gd")
|
|
14
15
|
|
|
15
16
|
var _editor_plugin: EditorPlugin = null
|
|
16
17
|
|
|
@@ -20,6 +21,7 @@ var _animation_tools: AnimationTools = null
|
|
|
20
21
|
var _play_tools: PlayTools = null
|
|
21
22
|
var _class_tools: ClassTools = null
|
|
22
23
|
var _settings_tools: SettingsTools = null
|
|
24
|
+
var _filesystem_tools: FilesystemTools = null
|
|
23
25
|
|
|
24
26
|
var _tool_map: Dictionary = {}
|
|
25
27
|
var _initialized: bool = false
|
|
@@ -33,6 +35,7 @@ func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
|
33
35
|
_animation_tools.set_editor_plugin(plugin)
|
|
34
36
|
_play_tools.set_editor_plugin(plugin)
|
|
35
37
|
_class_tools.set_editor_plugin(plugin)
|
|
38
|
+
_filesystem_tools.set_editor_plugin(plugin)
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
func _init_tools() -> void:
|
|
@@ -64,6 +67,10 @@ func _init_tools() -> void:
|
|
|
64
67
|
_settings_tools.name = "SettingsTools"
|
|
65
68
|
add_child(_settings_tools)
|
|
66
69
|
|
|
70
|
+
_filesystem_tools = FilesystemTools.new()
|
|
71
|
+
_filesystem_tools.name = "FilesystemTools"
|
|
72
|
+
add_child(_filesystem_tools)
|
|
73
|
+
|
|
67
74
|
_tool_map = {
|
|
68
75
|
# Scene tools
|
|
69
76
|
"create_scene": [_scene_tools, "create_scene"],
|
|
@@ -78,9 +85,9 @@ func _init_tools() -> void:
|
|
|
78
85
|
"connect_signal": [_scene_tools, "connect_signal"],
|
|
79
86
|
"disconnect_signal": [_scene_tools, "disconnect_signal"],
|
|
80
87
|
"list_connections": [_scene_tools, "list_connections"],
|
|
81
|
-
"rescan_filesystem": [
|
|
82
|
-
"scan_status": [
|
|
83
|
-
"reimport_files": [
|
|
88
|
+
"rescan_filesystem": [_filesystem_tools, "rescan_filesystem"],
|
|
89
|
+
"scan_status": [_filesystem_tools, "scan_status"],
|
|
90
|
+
"reimport_files": [_filesystem_tools, "reimport_files"],
|
|
84
91
|
"global_classes": [_class_tools, "global_classes"],
|
|
85
92
|
"reload_script": [_class_tools, "reload_script"],
|
|
86
93
|
# Settings, for a caller that asked the editor rather than the file on disk
|