gdharness 1.2.40 → 1.2.42
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 +103 -38
- package/build/godot/addons/gdharness_editor/animation_reads.gd +14 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
- package/build/godot/addons/gdharness_editor/file_reads.gd +134 -10
- package/build/godot/addons/gdharness_editor/headless_check.gd +56 -15
- package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
- package/build/godot/addons/gdharness_editor/reading.gd +40 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +215 -12
- package/build/godot/addons/gdharness_editor/serialisation.gd +5 -1
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +346 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +438 -0
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +102 -60
- package/build/godot/addons/gdharness_runtime/reading.gd +40 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +11 -6
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
- package/build/godot/addons/gdharness_runtime/serialisation.gd +5 -1
- package/build/godot/operations/animation_reads.gd +14 -2
- package/build/godot/operations/editor_tools.gd +28 -1
- package/build/godot/operations/file_reads.gd +134 -10
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/property_values.gd +1 -1
- package/build/godot/operations/reading.gd +40 -0
- package/build/godot/operations/serialisation.gd +5 -1
- package/build/index.js +103 -38
- package/package.json +1 -1
|
@@ -30,13 +30,27 @@ const HeadlessCheck = preload("headless_check.gd")
|
|
|
30
30
|
## How many times what scripts changed is put back before a write is refused for it.
|
|
31
31
|
const PUTTING_BACK_PASSES: int = 4
|
|
32
32
|
const PropertyValues = preload("property_values.gd")
|
|
33
|
+
const Read = preload("reading.gd")
|
|
34
|
+
## The names Object.ConnectFlags gives each bit. The number alone left a reader guessing that 2 is
|
|
35
|
+
## the persistence every connection in a saved scene carries.
|
|
36
|
+
const CONNECT_FLAG_NAMES: Dictionary[int, String] = {
|
|
37
|
+
Object.CONNECT_DEFERRED: "deferred",
|
|
38
|
+
Object.CONNECT_PERSIST: "persist",
|
|
39
|
+
Object.CONNECT_ONE_SHOT: "one_shot",
|
|
40
|
+
Object.CONNECT_REFERENCE_COUNTED: "reference_counted",
|
|
41
|
+
Object.CONNECT_APPEND_SOURCE_OBJECT: "append_source_object",
|
|
42
|
+
}
|
|
33
43
|
|
|
34
44
|
## Whether a scan asked for through [method scan_later] is still to start, which a caller waiting
|
|
35
45
|
## for the editor's files to settle has to wait through as well.
|
|
36
46
|
static var scan_due: bool = false
|
|
47
|
+
## The files a headless save named by path alone because the uid cache holds no uid for them,
|
|
48
|
+
## gathered for the answer to the call that saved, the one call its engine runs.
|
|
49
|
+
static var by_path: PackedStringArray = []
|
|
37
50
|
|
|
38
51
|
var root: Node = null
|
|
39
52
|
var path: String = ""
|
|
53
|
+
## The path of each node a scene this one inherits gives, with the file that makes it.
|
|
40
54
|
var _base_paths: Dictionary = {}
|
|
41
55
|
var _state: SceneState = null
|
|
42
56
|
## What the file stores on each node it was opened with, by the node's instance id, so a node keeps
|
|
@@ -77,9 +91,16 @@ func open(scene_path: String, for_writing: bool) -> Dictionary:
|
|
|
77
91
|
return {"ok": false, "error": "Failed to instantiate " + path}
|
|
78
92
|
_state = packed.get_state()
|
|
79
93
|
var base: SceneState = _state.get_base_scene_state()
|
|
94
|
+
# Each node a base gives, with the file that makes it: the deepest base listing it as more than
|
|
95
|
+
# values set on a node from further down.
|
|
80
96
|
while base != null:
|
|
81
97
|
for index: int in base.get_node_count():
|
|
82
|
-
|
|
98
|
+
var given: String = _relative(str(base.get_node_path(index)))
|
|
99
|
+
var made: bool = (
|
|
100
|
+
base.get_node_instance(index) != null or not str(base.get_node_type(index)).is_empty()
|
|
101
|
+
)
|
|
102
|
+
if made or not _base_paths.has(given):
|
|
103
|
+
_base_paths[given] = base.get_path()
|
|
83
104
|
base = base.get_base_scene_state()
|
|
84
105
|
# On a read as well, which would otherwise answer what the scripts set as what the file holds.
|
|
85
106
|
if not Engine.is_editor_hint():
|
|
@@ -122,6 +143,13 @@ func asked(node: Node, names: Array) -> void:
|
|
|
122
143
|
_reached.merge(HeadlessCheck.reached(node, property))
|
|
123
144
|
|
|
124
145
|
|
|
146
|
+
## Takes [param resource], built into the scene, as one the tool changed, which a write saves as it is.
|
|
147
|
+
func reached_into(resource: Resource) -> void:
|
|
148
|
+
var id: String = resource.get_scene_unique_id()
|
|
149
|
+
if not id.is_empty():
|
|
150
|
+
_reached[id] = true
|
|
151
|
+
|
|
152
|
+
|
|
125
153
|
## Takes each copy in [param copies], keyed by the node it was made from, as holding what that node's
|
|
126
154
|
## file entry holds.
|
|
127
155
|
func copied(copies: Dictionary) -> void:
|
|
@@ -150,12 +178,74 @@ func refuse(error: String) -> Dictionary:
|
|
|
150
178
|
return {"ok": false, "error": error}
|
|
151
179
|
|
|
152
180
|
|
|
181
|
+
## Each bit set in [param flags], a connection's, by name, and whatever bits are left unnamed as one
|
|
182
|
+
## number.
|
|
183
|
+
static func flag_names(flags: int) -> Array[String]:
|
|
184
|
+
var names: Array[String] = []
|
|
185
|
+
var rest: int = flags
|
|
186
|
+
for bit: int in CONNECT_FLAG_NAMES:
|
|
187
|
+
if flags & bit:
|
|
188
|
+
names.append(CONNECT_FLAG_NAMES[bit])
|
|
189
|
+
rest &= ~bit
|
|
190
|
+
if rest != 0:
|
|
191
|
+
names.append(str(rest))
|
|
192
|
+
return names
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## [param said], the refusal for [param node_path] found nowhere in this scene, with how a path is
|
|
196
|
+
## read when it starts with the root's own name, and otherwise with the last node of the path the
|
|
197
|
+
## scene has and what is under it, as after a node was moved or renamed.
|
|
198
|
+
func not_found(said: String, node_path: String) -> String:
|
|
199
|
+
if root == null:
|
|
200
|
+
return said
|
|
201
|
+
if not root.has_node(NodePath(node_path.get_slice("/", 0))):
|
|
202
|
+
var hinted: String = Read.from_the_root(said, node_path, str(root.name))
|
|
203
|
+
if hinted != said:
|
|
204
|
+
return hinted
|
|
205
|
+
var parts: PackedStringArray = node_path.trim_prefix("./").split("/", false)
|
|
206
|
+
var at: Node = root
|
|
207
|
+
var reached: PackedStringArray = []
|
|
208
|
+
for part: String in parts:
|
|
209
|
+
var below: Node = at.get_node_or_null(NodePath(part))
|
|
210
|
+
if below == null:
|
|
211
|
+
break
|
|
212
|
+
at = below
|
|
213
|
+
var _walked: bool = reached.append(part)
|
|
214
|
+
if reached.size() >= parts.size():
|
|
215
|
+
return said
|
|
216
|
+
var names: PackedStringArray = []
|
|
217
|
+
for child: Node in at.get_children():
|
|
218
|
+
var _named: bool = names.append(str(child.name))
|
|
219
|
+
return Read.missing_below(said, str(root.name), reached, parts[reached.size()], names)
|
|
220
|
+
|
|
221
|
+
|
|
153
222
|
func close() -> void:
|
|
154
223
|
if root != null:
|
|
155
224
|
root.free()
|
|
156
225
|
root = null
|
|
157
226
|
|
|
158
227
|
|
|
228
|
+
## What the file holds under the instance at [param at] that goes once its children are not
|
|
229
|
+
## editable, as {values, nodes}: the paths of the instance's nodes it sets values on, which stay with
|
|
230
|
+
## the instance's own values, and of the nodes the scene added below one of them, which are gone,
|
|
231
|
+
## since the packer skips everything below a node of an instance that is not editable. A node added
|
|
232
|
+
## straight under [param at] is the scene's own and stays.
|
|
233
|
+
func held_under(at: String) -> Dictionary:
|
|
234
|
+
var values: Array[String] = []
|
|
235
|
+
var nodes: Array[String] = []
|
|
236
|
+
for index: int in _state.get_node_count():
|
|
237
|
+
var entry: String = _relative(str(_state.get_node_path(index)))
|
|
238
|
+
var parent: String = _relative(str(_state.get_node_path(index, true)))
|
|
239
|
+
var values_only: bool = (
|
|
240
|
+
_state.get_node_instance(index) == null and str(_state.get_node_type(index)).is_empty()
|
|
241
|
+
)
|
|
242
|
+
if values_only and entry.begins_with(at + "/"):
|
|
243
|
+
values.append(entry)
|
|
244
|
+
elif not values_only and parent.begins_with(at + "/"):
|
|
245
|
+
nodes.append(entry)
|
|
246
|
+
return {"values": values, "nodes": nodes}
|
|
247
|
+
|
|
248
|
+
|
|
159
249
|
## The path of [param node] as a tool names it: "." for the root.
|
|
160
250
|
func path_of(node: Node) -> String:
|
|
161
251
|
return str(root.get_path_to(node))
|
|
@@ -166,16 +256,46 @@ func path_of(node: Node) -> String:
|
|
|
166
256
|
func foreign(node: Node) -> String:
|
|
167
257
|
if node == root or node.owner == root:
|
|
168
258
|
return ""
|
|
169
|
-
if node.owner
|
|
259
|
+
if node.owner == null:
|
|
260
|
+
return (
|
|
261
|
+
"%s is made by a script as the scene is instanced, and no scene file saves anything about it."
|
|
262
|
+
% path_of(node)
|
|
263
|
+
)
|
|
264
|
+
if root.is_editable_instance(node.owner):
|
|
170
265
|
return ""
|
|
171
|
-
var
|
|
172
|
-
return (
|
|
266
|
+
var said: String = (
|
|
173
267
|
(
|
|
174
268
|
"%s belongs to the scene instanced at %s (%s), and this scene saves nothing about it. Edit "
|
|
175
|
-
+ "that scene instead
|
|
269
|
+
+ "that scene instead, which changes every instance of it"
|
|
176
270
|
)
|
|
177
|
-
% [path_of(node),
|
|
271
|
+
% [path_of(node), path_of(node.owner), node.owner.scene_file_path]
|
|
178
272
|
)
|
|
273
|
+
var steps: PackedStringArray = []
|
|
274
|
+
for closed: String in _closed_around(node):
|
|
275
|
+
var _added: bool = steps.append("nodePath %s editableChildren true" % closed)
|
|
276
|
+
if steps.is_empty():
|
|
277
|
+
return said + "."
|
|
278
|
+
return (
|
|
279
|
+
said
|
|
280
|
+
+ ", or have this scene keep its own values for this instance with scene_node set "
|
|
281
|
+
+ ", then ".join(steps)
|
|
282
|
+
+ "."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
## The instances around [param node] whose children are not editable, outermost first: each must be
|
|
287
|
+
## made editable, in that order, before this scene keeps values of its own for [param node], since
|
|
288
|
+
## the packer skips every node under an instance that is not. Empty when nothing would let it.
|
|
289
|
+
func _closed_around(node: Node) -> PackedStringArray:
|
|
290
|
+
var closed: PackedStringArray = []
|
|
291
|
+
var instance: Node = node.owner
|
|
292
|
+
while instance != root:
|
|
293
|
+
if instance == null:
|
|
294
|
+
return []
|
|
295
|
+
if not root.is_editable_instance(instance):
|
|
296
|
+
var _at: int = closed.insert(0, path_of(instance))
|
|
297
|
+
instance = instance.owner
|
|
298
|
+
return closed
|
|
179
299
|
|
|
180
300
|
|
|
181
301
|
## Why [param node] cannot be moved, renamed, copied or deleted, or "": the scene may only
|
|
@@ -192,10 +312,10 @@ func fixed(node: Node) -> String:
|
|
|
192
312
|
if _base_paths.has(at):
|
|
193
313
|
return (
|
|
194
314
|
(
|
|
195
|
-
"%s
|
|
315
|
+
"%s is made by %s, a scene %s inherits, which creates it again on load. Edit that "
|
|
196
316
|
+ "scene instead."
|
|
197
317
|
)
|
|
198
|
-
% [at, path]
|
|
318
|
+
% [at, _base_paths[at], path]
|
|
199
319
|
)
|
|
200
320
|
return ""
|
|
201
321
|
|
|
@@ -323,10 +443,25 @@ func unusable_name(parent: Node, child_name: String, moving: Node = null) -> Str
|
|
|
323
443
|
|
|
324
444
|
## The names of the properties this scene's own file sets on [param node], whatever their values.
|
|
325
445
|
func set_here(node: Node) -> Dictionary:
|
|
446
|
+
return set_at(path_of(node))
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
## Where the values of [param node], at [param at] in the file, come from when the file does not set
|
|
450
|
+
## them: "" for a node this scene makes, which holds them all, or the scene that makes the node, the
|
|
451
|
+
## one it is instanced from or one this scene inherits.
|
|
452
|
+
func values_from(node: Node, at: String) -> String:
|
|
453
|
+
if node != root and not node.scene_file_path.is_empty():
|
|
454
|
+
return node.scene_file_path
|
|
455
|
+
if node != root and node.owner != root:
|
|
456
|
+
return node.owner.scene_file_path if node.owner != null else ""
|
|
457
|
+
return str(_base_paths.get(at, ""))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
## The names of the properties this scene's own file sets on the node at [param at].
|
|
461
|
+
func set_at(at: String) -> Dictionary:
|
|
326
462
|
var names: Dictionary = {}
|
|
327
463
|
if _state == null:
|
|
328
464
|
return names
|
|
329
|
-
var at: String = path_of(node)
|
|
330
465
|
for index: int in _state.get_node_count():
|
|
331
466
|
if _relative(str(_state.get_node_path(index))) != at:
|
|
332
467
|
continue
|
|
@@ -438,17 +573,85 @@ static func save_packed(packed: PackedScene, scene_path: String) -> Dictionary:
|
|
|
438
573
|
static func save_keeping_uid(resource: Resource, file_path: String) -> Error:
|
|
439
574
|
if Engine.is_editor_hint():
|
|
440
575
|
return ResourceSaver.save(resource, file_path)
|
|
441
|
-
var uid: int =
|
|
576
|
+
var uid: int = uid_in(file_path)
|
|
442
577
|
var saving: Error = ResourceSaver.save(resource, file_path)
|
|
443
578
|
if saving != OK:
|
|
444
579
|
return saving
|
|
445
580
|
if uid == ResourceUID.INVALID_ID:
|
|
446
581
|
uid = ResourceUID.create_id()
|
|
447
|
-
|
|
582
|
+
var marked: Error = ResourceSaver.set_uid(file_path, uid)
|
|
583
|
+
if marked != OK:
|
|
584
|
+
return marked
|
|
585
|
+
return _with_external_uids(file_path)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
## Puts back the uid on each external resource the text file at [param file_path] names. Only the
|
|
589
|
+
## editor's file system tells the saver a path's uid, so a headless save writes every reference by
|
|
590
|
+
## path alone, and each breaks when the file it names is moved.
|
|
591
|
+
static func _with_external_uids(file_path: String) -> Error:
|
|
592
|
+
if not (file_path.ends_with(".tscn") or file_path.ends_with(".tres")):
|
|
593
|
+
return OK
|
|
594
|
+
var lines: PackedStringArray = FileAccess.get_file_as_string(file_path).split("\n")
|
|
595
|
+
var pattern: RegEx = RegEx.create_from_string(' path="([^"]+)"')
|
|
596
|
+
var changed: bool = false
|
|
597
|
+
for at: int in lines.size():
|
|
598
|
+
var line: String = lines[at]
|
|
599
|
+
if not line.begins_with("[ext_resource ") or line.contains(' uid="'):
|
|
600
|
+
continue
|
|
601
|
+
var named: RegExMatch = pattern.search(line)
|
|
602
|
+
if named == null:
|
|
603
|
+
continue
|
|
604
|
+
var uid: String = reference_uid(named.get_string(1))
|
|
605
|
+
if not uid.is_empty():
|
|
606
|
+
lines[at] = line.replace(' path="', uid + ' path="')
|
|
607
|
+
changed = true
|
|
608
|
+
if not changed:
|
|
609
|
+
return OK
|
|
610
|
+
var file: FileAccess = FileAccess.open(file_path, FileAccess.WRITE)
|
|
611
|
+
if file == null:
|
|
612
|
+
return FileAccess.get_open_error()
|
|
613
|
+
if not file.store_string("\n".join(lines)):
|
|
614
|
+
return ERR_FILE_CANT_WRITE
|
|
615
|
+
file.close()
|
|
616
|
+
return OK
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
## [method uid_attribute] for a reference about to be written, noting [param named] under
|
|
620
|
+
## [member by_path] when it is left to be named by path.
|
|
621
|
+
static func reference_uid(named: String) -> String:
|
|
622
|
+
var uid: String = uid_attribute(named)
|
|
623
|
+
if uid.is_empty():
|
|
624
|
+
note_by_path(named)
|
|
625
|
+
return uid
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
## Notes [param named], a reference just written by path alone, under [member by_path] when the uid
|
|
629
|
+
## cache holds nothing for it: a file made since the project was last scanned, whose uid the cache
|
|
630
|
+
## learns from the next scan. Not a file whose uid the cache gives to another, which a scan leaves.
|
|
631
|
+
static func note_by_path(named: String) -> void:
|
|
632
|
+
if Engine.is_editor_hint() or not FileAccess.file_exists(named) or by_path.has(named):
|
|
633
|
+
return
|
|
634
|
+
var own: int = uid_in(named)
|
|
635
|
+
if own == ResourceUID.INVALID_ID and FileAccess.file_exists(named + ".uid"):
|
|
636
|
+
own = ResourceUID.text_to_id(FileAccess.get_file_as_string(named + ".uid").strip_edges())
|
|
637
|
+
if own == ResourceUID.INVALID_ID or not ResourceUID.has_id(own):
|
|
638
|
+
var _noted: bool = by_path.append(named)
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
## The uid attribute a reference to [param named] is written with, as ' uid="uid://..."', or "".
|
|
642
|
+
##
|
|
643
|
+
## Only a uid the uid cache gives that path, which is what the loader resolves it by. A uid the cache
|
|
644
|
+
## does not know (a file made with no editor since the last scan) is answered with a warning on every
|
|
645
|
+
## load, and one it gives another file (a scene copied whole keeps its original's) loads that file.
|
|
646
|
+
static func uid_attribute(named: String) -> String:
|
|
647
|
+
var id: int = ResourceLoader.get_resource_uid(named)
|
|
648
|
+
if id == ResourceUID.INVALID_ID or not ResourceUID.has_id(id) or ResourceUID.get_id_path(id) != named:
|
|
649
|
+
return ""
|
|
650
|
+
return ' uid="%s"' % ResourceUID.id_to_text(id)
|
|
448
651
|
|
|
449
652
|
|
|
450
653
|
## The uid the file at [param file_path] declares, or [constant ResourceUID.INVALID_ID].
|
|
451
|
-
static func
|
|
654
|
+
static func uid_in(file_path: String) -> int:
|
|
452
655
|
if not FileAccess.file_exists(file_path):
|
|
453
656
|
return ResourceUID.INVALID_ID
|
|
454
657
|
var file: FileAccess = FileAccess.open(file_path, FileAccess.READ)
|
|
@@ -664,8 +664,12 @@ func _serialize_array(value: Array) -> Array:
|
|
|
664
664
|
|
|
665
665
|
# Base64 rather than a list of numbers. A byte array is a texture, a sound or a save file often
|
|
666
666
|
# enough that the list would be the whole answer, megabytes of it, and unreadable either way.
|
|
667
|
+
#
|
|
668
|
+
# The engine's encoder raises an error on an empty array, and an empty tile_map_data is every
|
|
669
|
+
# TileMapLayer with nothing painted yet, so a read of one carried an engine error in its answer.
|
|
667
670
|
func _serialize_bytes(value: PackedByteArray) -> Dictionary:
|
|
668
|
-
|
|
671
|
+
var text: String = "" if value.is_empty() else Marshalls.raw_to_base64(value)
|
|
672
|
+
return {"base64": text, "size": value.size(), "_type": "PackedByteArray"}
|
|
669
673
|
|
|
670
674
|
|
|
671
675
|
# The packed vector and colour arrays, each element serialised the way one of them on its own is.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends Node
|
|
3
3
|
|
|
4
|
-
## Animations and animation trees, edited in the open editor
|
|
4
|
+
## Animations and animation trees, edited in the open editor or, for a project no editor serves, by a
|
|
5
|
+
## headless engine.
|
|
5
6
|
|
|
6
7
|
const AnimationReads = preload("../animation_reads.gd")
|
|
8
|
+
const HeadlessCheck = preload("../headless_check.gd")
|
|
7
9
|
const PropertyValues = preload("../property_values.gd")
|
|
8
10
|
const Read = preload("../reading.gd")
|
|
9
11
|
const SceneFile = preload("../scene_file.gd")
|
|
@@ -46,7 +48,9 @@ func _open_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
46
48
|
return [scene, null, refused]
|
|
47
49
|
var node: Node = _find_node(scene.root, node_path)
|
|
48
50
|
if node == null or not node.is_class(kind):
|
|
49
|
-
return [
|
|
51
|
+
return [
|
|
52
|
+
scene, null, scene.refuse(scene.not_found("%s not found at: %s" % [kind, node_path], node_path))
|
|
53
|
+
]
|
|
50
54
|
var foreign: String = scene.foreign(node)
|
|
51
55
|
if not foreign.is_empty():
|
|
52
56
|
return [scene, null, scene.refuse(foreign)]
|
|
@@ -62,25 +66,55 @@ func _read_at(scene_path: String, node_path: String, kind: String) -> Array:
|
|
|
62
66
|
return [scene, null, refused]
|
|
63
67
|
var node: Node = _find_node(scene.root, node_path)
|
|
64
68
|
if node == null or not node.is_class(kind):
|
|
65
|
-
return [
|
|
69
|
+
return [
|
|
70
|
+
scene, null, scene.refuse(scene.not_found("%s not found at: %s" % [kind, node_path], node_path))
|
|
71
|
+
]
|
|
66
72
|
return [scene, node, {}]
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
## Writes [param scene] once [param check] passes, then the files of [param homes], answering
|
|
70
76
|
## [param answer] or a refusal.
|
|
71
77
|
func _finish(scene: SceneFile, check: Callable, homes: Dictionary, answer: Dictionary) -> Dictionary:
|
|
78
|
+
if not Engine.is_editor_hint():
|
|
79
|
+
for file: Variant in homes:
|
|
80
|
+
var home: Resource = homes[file]
|
|
81
|
+
if _scripted(home):
|
|
82
|
+
return (
|
|
83
|
+
scene
|
|
84
|
+
. refuse(
|
|
85
|
+
(
|
|
86
|
+
(
|
|
87
|
+
(
|
|
88
|
+
"Nothing was written: %s holds a resource with a script that is not a tool, and what "
|
|
89
|
+
+ "that script changed when a headless engine loaded it cannot be told apart from the "
|
|
90
|
+
+ "edit. "
|
|
91
|
+
)
|
|
92
|
+
% file
|
|
93
|
+
)
|
|
94
|
+
+ HeadlessCheck.REMEDY
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
)
|
|
72
98
|
var written: Dictionary = scene.write(check)
|
|
73
99
|
if not written.is_empty():
|
|
74
100
|
return written
|
|
75
101
|
for file: Variant in homes:
|
|
76
102
|
var resource: Resource = homes[file]
|
|
77
|
-
var saving: Error =
|
|
103
|
+
var saving: Error = SceneFile.save_keeping_uid(resource, str(file))
|
|
78
104
|
if saving != OK:
|
|
79
105
|
return {"ok": false, "error": "Failed to save %s: %s" % [file, error_string(saving)]}
|
|
80
|
-
|
|
106
|
+
if Engine.is_editor_hint():
|
|
107
|
+
EditorInterface.get_resource_filesystem().update_file(str(file))
|
|
81
108
|
return answer
|
|
82
109
|
|
|
83
110
|
|
|
111
|
+
## Whether [param resource], or one it embeds, has a script that is not a tool, which a headless
|
|
112
|
+
## engine ran as it loaded the file.
|
|
113
|
+
func _scripted(resource: Resource) -> bool:
|
|
114
|
+
var script: Script = resource.get_script()
|
|
115
|
+
return (script != null and not script.is_tool()) or HeadlessCheck.holds_scripted_embedded(resource, {})
|
|
116
|
+
|
|
117
|
+
|
|
84
118
|
func _parse_json_maybe(value: Variant) -> Variant:
|
|
85
119
|
if typeof(value) != TYPE_STRING:
|
|
86
120
|
return value
|
|
@@ -399,6 +399,19 @@ func _atlas(entry: Variant, index: int) -> Array:
|
|
|
399
399
|
if texture_path.split("/").has(".."):
|
|
400
400
|
return [false, "sources[%d].texture leaves the project: %s" % [index, texture_path]]
|
|
401
401
|
if not ResourceLoader.exists(texture_path):
|
|
402
|
+
# An image the engine has not imported loads as nothing, where "no texture" would send a caller
|
|
403
|
+
# looking for a file that is there.
|
|
404
|
+
if FileAccess.file_exists(texture_path) and not FileAccess.file_exists(texture_path + ".import"):
|
|
405
|
+
return [
|
|
406
|
+
false,
|
|
407
|
+
(
|
|
408
|
+
(
|
|
409
|
+
"sources[%d].texture: %s has not been imported, so the engine has no texture for it "
|
|
410
|
+
+ "yet. project_import reimport with resourcePath %s imports it."
|
|
411
|
+
)
|
|
412
|
+
% [index, texture_path, texture_path]
|
|
413
|
+
)
|
|
414
|
+
]
|
|
402
415
|
return [false, "sources[%d].texture: no texture at %s" % [index, texture_path]]
|
|
403
416
|
var texture: Texture2D = load(texture_path) as Texture2D
|
|
404
417
|
if texture == null:
|
|
@@ -720,13 +733,15 @@ func set_theme_color(args: Dictionary) -> Dictionary:
|
|
|
720
733
|
return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, color_name]}
|
|
721
734
|
|
|
722
735
|
var stored: Color = saved.get_color(color_name, control_type)
|
|
723
|
-
return
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
736
|
+
return _noting_variation(
|
|
737
|
+
{
|
|
738
|
+
"ok": true,
|
|
739
|
+
"themePath": theme_path,
|
|
740
|
+
"controlType": control_type,
|
|
741
|
+
"colorName": color_name,
|
|
742
|
+
"color": {"r": stored.r, "g": stored.g, "b": stored.b, "a": stored.a}
|
|
743
|
+
}
|
|
744
|
+
)
|
|
730
745
|
|
|
731
746
|
|
|
732
747
|
func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
@@ -743,7 +758,23 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
743
758
|
if theme == null:
|
|
744
759
|
return {"ok": false, "error": "No Theme at %s. Create one there first." % theme_path}
|
|
745
760
|
|
|
746
|
-
|
|
761
|
+
# Godot reads a size of 0 or less as unset and answers the theme's default, so -3 would be read
|
|
762
|
+
# back as 16.
|
|
763
|
+
var pixels: float = Read.as_float(args.get("size"), 0.0)
|
|
764
|
+
if not is_finite(pixels) or pixels < 1 or pixels != floorf(pixels):
|
|
765
|
+
var shown: String = str(int(pixels)) if pixels == floorf(pixels) else str(pixels)
|
|
766
|
+
return {
|
|
767
|
+
"ok": false,
|
|
768
|
+
"error":
|
|
769
|
+
(
|
|
770
|
+
(
|
|
771
|
+
"size must be a whole number of pixels, 1 or more, not %s: Godot reads 0 or less as no size "
|
|
772
|
+
% shown
|
|
773
|
+
)
|
|
774
|
+
+ "and uses the theme's default instead."
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
theme.set_font_size(font_size_name, control_type, int(pixels))
|
|
747
778
|
|
|
748
779
|
var unsaved: String = _save_theme(theme, theme_path, "%s/font_sizes/%s" % [control_type, font_size_name])
|
|
749
780
|
if not unsaved.is_empty():
|
|
@@ -753,10 +784,32 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
753
784
|
if saved == null or not saved.has_font_size(font_size_name, control_type):
|
|
754
785
|
return {"ok": false, "error": "The theme saved without %s/%s" % [control_type, font_size_name]}
|
|
755
786
|
|
|
756
|
-
return
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
787
|
+
return _noting_variation(
|
|
788
|
+
{
|
|
789
|
+
"ok": true,
|
|
790
|
+
"themePath": theme_path,
|
|
791
|
+
"controlType": control_type,
|
|
792
|
+
"fontSizeName": font_size_name,
|
|
793
|
+
"size": saved.get_font_size(font_size_name, control_type)
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
## [param answer] with a note when its controlType names no class a theme styles. Any name is a
|
|
799
|
+
## theme type, so one that is no class is a type variation, which styles only a control whose
|
|
800
|
+
## theme_type_variation names it, and a misspelt "Buton" would otherwise read as Button set.
|
|
801
|
+
func _noting_variation(answer: Dictionary) -> Dictionary:
|
|
802
|
+
var type: String = str(answer.get("controlType", ""))
|
|
803
|
+
if (
|
|
804
|
+
ClassDB.class_exists(type)
|
|
805
|
+
and (ClassDB.is_parent_class(type, "Control") or ClassDB.is_parent_class(type, "Window"))
|
|
806
|
+
):
|
|
807
|
+
return answer
|
|
808
|
+
answer["note"] = (
|
|
809
|
+
(
|
|
810
|
+
"%s is not a Control or Window class, so this is a type variation: it styles only a control "
|
|
811
|
+
+ "whose theme_type_variation is %s."
|
|
812
|
+
)
|
|
813
|
+
% [type, type]
|
|
814
|
+
)
|
|
815
|
+
return answer
|