gdharness 1.3.8 → 1.3.10
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 +4668 -3229
- package/build/godot/addons/gdharness_editor/animation_reads.gd +115 -4
- package/build/godot/addons/gdharness_editor/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_editor/file_reads.gd +115 -18
- package/build/godot/addons/gdharness_editor/headless_check.gd +49 -2
- package/build/godot/addons/gdharness_editor/property_paths.gd +89 -16
- package/build/godot/addons/gdharness_editor/property_values.gd +180 -121
- package/build/godot/addons/gdharness_editor/reading.gd +112 -6
- package/build/godot/addons/gdharness_editor/scene_file.gd +117 -174
- package/build/godot/addons/gdharness_editor/scene_uids.gd +107 -0
- package/build/godot/addons/gdharness_editor/scene_uids.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_editor/setting_reads.gd +121 -0
- package/build/godot/addons/gdharness_editor/setting_reads.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tool_executor.gd +39 -6
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd +263 -0
- package/build/godot/addons/gdharness_editor/tools/animation_states.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +49 -414
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd +464 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tracks.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/class_tools.gd +37 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd +221 -0
- package/build/godot/addons/gdharness_editor/tools/navigation_bake.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +162 -483
- package/build/godot/addons/gdharness_editor/tools/scene_groups.gd +16 -0
- package/build/godot/addons/gdharness_editor/tools/scene_kin.gd +8 -4
- package/build/godot/addons/gdharness_editor/tools/scene_paths.gd +2 -1
- package/build/godot/addons/gdharness_editor/tools/scene_placing.gd +10 -0
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +151 -8
- package/build/godot/addons/gdharness_editor/tools/settings_tools.gd +7 -30
- package/build/godot/addons/gdharness_editor/tools/signal_tools.gd +190 -38
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd +485 -0
- package/build/godot/addons/gdharness_editor/tools/tileset_tools.gd.uid +1 -0
- package/build/godot/addons/gdharness_editor/value_shapes.gd +128 -1
- package/build/godot/addons/gdharness_runtime/call_errors.gd +8 -2
- package/build/godot/addons/gdharness_runtime/declared_values.gd +272 -0
- package/build/godot/addons/gdharness_runtime/reading.gd +112 -6
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +35 -3
- package/build/godot/addons/gdharness_runtime/runtime_capture.gd +5 -0
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +188 -30
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +107 -104
- package/build/godot/addons/gdharness_runtime/runtime_mouse.gd +17 -0
- package/build/godot/addons/gdharness_runtime/runtime_pad.gd +13 -4
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +320 -27
- package/build/godot/addons/gdharness_runtime/runtime_queries.gd +165 -20
- package/build/godot/addons/gdharness_runtime/runtime_screen.gd +49 -3
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +31 -2
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +250 -10
- package/build/godot/addons/gdharness_runtime/runtime_waits.gd +279 -11
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +27 -0
- package/build/godot/addons/gdharness_runtime/serialisation.gd +19 -29
- package/build/godot/addons/gdharness_runtime/value_shapes.gd +128 -1
- package/build/godot/operations/animation_reads.gd +115 -4
- package/build/godot/operations/audio_buses.gd +93 -7
- package/build/godot/operations/classdb_queries.gd +119 -7
- package/build/godot/operations/declared_values.gd +272 -0
- package/build/godot/operations/dependencies.gd +154 -41
- package/build/godot/operations/editor_tools.gd +29 -13
- package/build/godot/operations/export_presets.gd +602 -4
- package/build/godot/operations/file_reads.gd +115 -18
- package/build/godot/operations/gdscript_analysis.gd +14 -0
- package/build/godot/operations/gdscript_authoring.gd +12 -2
- package/build/godot/operations/godot_operations.gd +18 -2
- package/build/godot/operations/import_pipeline.gd +20 -221
- package/build/godot/operations/input_actions.gd +127 -51
- package/build/godot/operations/project_config.gd +113 -68
- package/build/godot/operations/project_diagnostics.gd +4 -0
- package/build/godot/operations/project_validation.gd +266 -0
- package/build/godot/operations/property_paths.gd +89 -16
- package/build/godot/operations/property_values.gd +180 -121
- package/build/godot/operations/reading.gd +112 -6
- package/build/godot/operations/scene_connections.gd +222 -0
- package/build/godot/operations/serialisation.gd +19 -29
- package/build/godot/operations/setting_reads.gd +121 -0
- package/build/godot/operations/value_shapes.gd +128 -1
- package/build/godot/tests/listed.gd +20 -13
- package/build/keeper.js +1402 -707
- package/package.json +1 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
@tool
|
|
2
2
|
extends Node
|
|
3
3
|
|
|
4
|
-
## Resource files, shaders
|
|
4
|
+
## Resource files, shaders and themes, written through the open editor.
|
|
5
5
|
|
|
6
6
|
const GdhFileReads = preload("../file_reads.gd")
|
|
7
7
|
const GdhHeadlessCheck = preload("../headless_check.gd")
|
|
8
|
+
const GdhPropertyPaths = preload("../property_paths.gd")
|
|
8
9
|
const GdhPropertyValues = preload("../property_values.gd")
|
|
9
10
|
const GdhRead = preload("../reading.gd")
|
|
10
11
|
const GdhSame = preload("../same_values.gd")
|
|
11
12
|
const GdhSceneFile = preload("../scene_file.gd")
|
|
13
|
+
const GdhSceneUids = preload("../scene_uids.gd")
|
|
14
|
+
const GdhSerialisation = preload("../serialisation.gd")
|
|
12
15
|
|
|
13
16
|
# The minimal shader of a type: its declaration and the one function the engine calls for it.
|
|
14
17
|
const SHADER_EMPTY: String = """shader_type %s;
|
|
@@ -20,11 +23,9 @@ const SHADER_ENTRY: Dictionary[String, String] = {
|
|
|
20
23
|
"canvas_item": "fragment", "spatial": "fragment", "particles": "process", "sky": "sky", "fog": "fog"
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
## How many cells a read of a tile map lists, enough for a level's worth and not a whole world's.
|
|
24
|
-
const CELLS_SHOWN: int = 1000
|
|
25
|
-
|
|
26
26
|
var _editor_plugin: EditorPlugin = null
|
|
27
27
|
var _properties: GdhPropertyValues = GdhPropertyValues.new()
|
|
28
|
+
var _values: GdhSerialisation = GdhSerialisation.new()
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
func set_editor_plugin(plugin: EditorPlugin) -> void:
|
|
@@ -74,33 +75,6 @@ func _parse_properties_dict(raw: Variant) -> Dictionary:
|
|
|
74
75
|
return {}
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
## A pair of whole numbers given as {"x": 1, "y": 2} or [1, 2], as [true, Vector2i] or
|
|
78
|
-
## [false, why]. The server checks the arguments it is given but not what is inside them, and a
|
|
79
|
-
## pair given the other way went into a typed local and failed as "Invalid tool result".
|
|
80
|
-
func _xy(value: Variant, label: String) -> Array:
|
|
81
|
-
var x: Variant = null
|
|
82
|
-
var y: Variant = null
|
|
83
|
-
if value is Dictionary:
|
|
84
|
-
var fields: Dictionary = value
|
|
85
|
-
x = fields.get("x")
|
|
86
|
-
y = fields.get("y")
|
|
87
|
-
elif value is Array:
|
|
88
|
-
var pair: Array = value
|
|
89
|
-
if pair.size() == 2:
|
|
90
|
-
x = pair[0]
|
|
91
|
-
y = pair[1]
|
|
92
|
-
for number: Variant in [x, y]:
|
|
93
|
-
if (
|
|
94
|
-
not (number is float or number is int)
|
|
95
|
-
or GdhRead.as_float(number) != floorf(GdhRead.as_float(number))
|
|
96
|
-
):
|
|
97
|
-
return [
|
|
98
|
-
false,
|
|
99
|
-
'%s must be two whole numbers, {"x": .., "y": ..}, not %s.' % [label, JSON.stringify(value)]
|
|
100
|
-
]
|
|
101
|
-
return [true, Vector2i(GdhRead.as_int(x), GdhRead.as_int(y))]
|
|
102
|
-
|
|
103
|
-
|
|
104
78
|
## The theme on disk at this path, or null when there is not one.
|
|
105
79
|
##
|
|
106
80
|
## Read past the cache, because the caller saves what it gets back and the cached copy is the
|
|
@@ -123,10 +97,13 @@ func _load_theme(theme_path: String) -> Theme:
|
|
|
123
97
|
## Read back because a property can be set and still not be saved: one without storage, a
|
|
124
98
|
## script variable that is not exported. Both were answered as changed.
|
|
125
99
|
func _save_checked(resource: Resource, path: String, names: Array, previous: Variant) -> String:
|
|
126
|
-
var
|
|
100
|
+
var carried: Dictionary = GdhHeadlessCheck.embedded_in(GdhHeadlessCheck.stored(resource).values(), {})
|
|
101
|
+
var saving: Error = GdhSceneUids.save_keeping_uid(resource, path)
|
|
127
102
|
if saving != OK:
|
|
128
103
|
return "Failed to save %s: %s" % [path, error_string(saving)]
|
|
129
|
-
var unkept: String = _unkept_headless(
|
|
104
|
+
var unkept: String = _unkept_headless(
|
|
105
|
+
resource, path, names + _properties.moved_by_engine.keys(), previous, carried
|
|
106
|
+
)
|
|
130
107
|
if not unkept.is_empty():
|
|
131
108
|
return "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(path, previous)
|
|
132
109
|
var loaded: Resource = ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE)
|
|
@@ -154,8 +131,11 @@ func _save_checked(resource: Resource, path: String, names: Array, previous: Var
|
|
|
154
131
|
|
|
155
132
|
## Outside the editor, puts back in the file just saved at [param path] what a script that is not a
|
|
156
133
|
## tool changed and the call did not ask for, [param names] being what it asked for, and answers ""
|
|
157
|
-
## or why that did not hold.
|
|
158
|
-
|
|
134
|
+
## or why that did not hold. [param carried] is what each embedded id named before the save. See
|
|
135
|
+
## [GdhHeadlessCheck].
|
|
136
|
+
func _unkept_headless(
|
|
137
|
+
resource: Resource, path: String, names: Array, previous: Variant, carried: Dictionary
|
|
138
|
+
) -> String:
|
|
159
139
|
if Engine.is_editor_hint():
|
|
160
140
|
return ""
|
|
161
141
|
var script: Script = resource.get_script()
|
|
@@ -191,14 +171,14 @@ func _unkept_headless(resource: Resource, path: String, names: Array, previous:
|
|
|
191
171
|
resource if scripted else null,
|
|
192
172
|
asked,
|
|
193
173
|
reached,
|
|
194
|
-
|
|
174
|
+
carried,
|
|
175
|
+
func() -> Error: return GdhSceneUids.save_keeping_uid(resource, path)
|
|
195
176
|
)
|
|
196
177
|
|
|
197
178
|
|
|
198
179
|
func create_resource(args: Dictionary) -> Dictionary:
|
|
199
180
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
200
181
|
var resource_type: String = str(args.get("resourceType", "Resource"))
|
|
201
|
-
var asked_type: String = resource_type
|
|
202
182
|
if res_path == "res://":
|
|
203
183
|
return {"ok": false, "error": "resourcePath is required"}
|
|
204
184
|
var taken: String = _taken(res_path, "resource_edit op=create")
|
|
@@ -247,16 +227,27 @@ func create_resource(args: Dictionary) -> Dictionary:
|
|
|
247
227
|
var properties: Dictionary = _parse_properties_dict(args.get("properties", {}))
|
|
248
228
|
var refused: String = _properties.write_all(resource, properties)
|
|
249
229
|
if not refused.is_empty():
|
|
250
|
-
return {"ok": false, "error": refused}
|
|
230
|
+
return {"ok": false, "error": GdhHeadlessCheck.setter_ran(resource, _properties.unheld, refused)}
|
|
251
231
|
|
|
252
232
|
var folder: String = GdhSceneFile.make_folder_for(res_path)
|
|
253
233
|
if not folder.is_empty():
|
|
254
234
|
return {"ok": false, "error": folder}
|
|
255
|
-
|
|
235
|
+
# As a read names it, the engine class under resourceType and the script's class beside it, since
|
|
236
|
+
# a create given resourceType Resource and a script answered nothing of the class it had made, and
|
|
237
|
+
# one given a class_name answered it where a read of the same file answers Resource.
|
|
238
|
+
var answer: Dictionary = {"ok": true, "resourcePath": res_path, "resourceType": resource_type}
|
|
239
|
+
var scripted: Variant = resource.get_script()
|
|
240
|
+
if scripted is Script:
|
|
241
|
+
var attached: Script = scripted
|
|
242
|
+
answer["script"] = attached.resource_path
|
|
243
|
+
if not attached.get_global_name().is_empty():
|
|
244
|
+
answer["scriptClass"] = str(attached.get_global_name())
|
|
256
245
|
_properties.note_write(answer)
|
|
257
246
|
var failed: String = _save_checked(resource, res_path, properties.keys(), null)
|
|
258
247
|
if not failed.is_empty():
|
|
259
248
|
return {"ok": false, "error": failed}
|
|
249
|
+
if not properties.is_empty():
|
|
250
|
+
answer["properties"] = _held(resource, properties.keys())
|
|
260
251
|
return answer
|
|
261
252
|
|
|
262
253
|
|
|
@@ -265,7 +256,7 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
265
256
|
if res_path == "res://":
|
|
266
257
|
return {"ok": false, "error": "resourcePath is required"}
|
|
267
258
|
if not FileAccess.file_exists(res_path):
|
|
268
|
-
return {"ok": false, "error": "No resource at "
|
|
259
|
+
return {"ok": false, "error": "No resource at %s; op=create makes one there." % res_path}
|
|
269
260
|
# The editor's copy of an imported file is rebuilt from the source on every import, and a save
|
|
270
261
|
# to it fails; edited through the cache, the editor kept the changed copy after the refusal.
|
|
271
262
|
if FileAccess.file_exists(res_path + ".import"):
|
|
@@ -299,6 +290,10 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
299
290
|
|
|
300
291
|
var properties: Dictionary = _parse_properties_dict(args.get("properties", ""))
|
|
301
292
|
var refused: String = _properties.write_all(resource, properties)
|
|
293
|
+
if refused.is_empty():
|
|
294
|
+
refused = _holding_itself(resource, res_path, properties.keys())
|
|
295
|
+
else:
|
|
296
|
+
refused = GdhHeadlessCheck.setter_ran(resource, _properties.unheld, refused)
|
|
302
297
|
if not refused.is_empty():
|
|
303
298
|
return {"ok": false, "error": refused}
|
|
304
299
|
var answer: Dictionary = {"ok": true, "resourcePath": res_path}
|
|
@@ -307,9 +302,39 @@ func modify_resource(args: Dictionary) -> Dictionary:
|
|
|
307
302
|
var failed: String = _save_checked(resource, res_path, properties.keys(), previous)
|
|
308
303
|
if not failed.is_empty():
|
|
309
304
|
return {"ok": false, "error": failed}
|
|
305
|
+
answer["properties"] = _held(resource, properties.keys())
|
|
310
306
|
return answer
|
|
311
307
|
|
|
312
308
|
|
|
309
|
+
## Why [param resource], saved at [param path], cannot hold what [param names] were given, or "": a
|
|
310
|
+
## resource cannot hold the file it is saved as. Godot saves the reference as null with only a
|
|
311
|
+
## warning in its log, and the read back after the save reported the property as one the file does
|
|
312
|
+
## not keep.
|
|
313
|
+
func _holding_itself(resource: Resource, path: String, names: Array) -> String:
|
|
314
|
+
for key: Variant in names:
|
|
315
|
+
var property: String = str(key)
|
|
316
|
+
for held: Resource in GdhHeadlessCheck.resources_in(_properties.read_at(resource, property)):
|
|
317
|
+
if held == resource or held.resource_path == path:
|
|
318
|
+
return (
|
|
319
|
+
(
|
|
320
|
+
"%s.%s would hold %s itself, and a resource cannot hold the file it is saved as: "
|
|
321
|
+
+ "Godot saves the reference as null. Nothing was written."
|
|
322
|
+
)
|
|
323
|
+
% [GdhPropertyPaths.class_called(resource), property, path]
|
|
324
|
+
)
|
|
325
|
+
return ""
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
## What [param resource] holds at each property or path in [param asked], once the save has been
|
|
329
|
+
## checked against it: a write saying only that it was written left the caller a second call to
|
|
330
|
+
## learn what the engine kept.
|
|
331
|
+
func _held(resource: Resource, asked: Array) -> Dictionary:
|
|
332
|
+
var held: Dictionary = {}
|
|
333
|
+
for key: Variant in asked:
|
|
334
|
+
held[str(key)] = _values.serialize_value(_properties.read_at(resource, str(key)))
|
|
335
|
+
return held
|
|
336
|
+
|
|
337
|
+
|
|
313
338
|
## What a resource file holds, read as a headless engine reads it with no editor connected.
|
|
314
339
|
func read_resource(args: Dictionary) -> Dictionary:
|
|
315
340
|
var res_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
@@ -351,451 +376,60 @@ func create_shader(args: Dictionary) -> Dictionary:
|
|
|
351
376
|
return {"ok": false, "error": "Failed to write the shader file " + shader_path}
|
|
352
377
|
|
|
353
378
|
_written(shader_path)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if tileset_path == "res://":
|
|
360
|
-
return {"ok": false, "error": "resourcePath is required"}
|
|
361
|
-
# modify sets properties and builds no atlas, so the other path is the only way on offered.
|
|
362
|
-
var taken: String = (
|
|
363
|
-
""
|
|
364
|
-
if not FileAccess.file_exists(tileset_path)
|
|
365
|
-
else (
|
|
366
|
-
(
|
|
367
|
-
"%s already exists, and create_tileset builds a whole new TileSet, which would replace "
|
|
368
|
-
+ "everything in it, the tiles a scene paints with among them. Give resourcePath a path "
|
|
369
|
-
+ "nothing is at."
|
|
370
|
-
)
|
|
371
|
-
% tileset_path
|
|
372
|
-
)
|
|
373
|
-
)
|
|
374
|
-
if not taken.is_empty():
|
|
375
|
-
return {"ok": false, "error": taken}
|
|
376
|
-
|
|
377
|
-
var given: Variant = args.get("sources", [])
|
|
378
|
-
if not given is Array:
|
|
379
|
-
return {"ok": false, "error": "sources must list at least one atlas"}
|
|
380
|
-
var sources: Array = given
|
|
381
|
-
if sources.is_empty():
|
|
382
|
-
return {"ok": false, "error": "sources must list at least one atlas"}
|
|
383
|
-
|
|
384
|
-
var tileset: TileSet = TileSet.new()
|
|
385
|
-
var made: Array = []
|
|
386
|
-
for index: int in sources.size():
|
|
387
|
-
var built: Array = _atlas(sources[index], index)
|
|
388
|
-
if not built[0]:
|
|
389
|
-
return {"ok": false, "error": built[1]}
|
|
390
|
-
var atlas: TileSetAtlasSource = built[1]
|
|
391
|
-
# The first source sets the tile set's own tile size, which is what a TileMapLayer draws
|
|
392
|
-
# cells at; left at its default of 16 it did not match the atlas.
|
|
393
|
-
if index == 0:
|
|
394
|
-
tileset.tile_size = atlas.texture_region_size
|
|
395
|
-
var added: int = tileset.add_source(atlas)
|
|
396
|
-
if added < 0:
|
|
397
|
-
return {
|
|
398
|
-
"ok": false,
|
|
399
|
-
"error": "The TileSet did not take the atlas for %s" % atlas.texture.resource_path
|
|
400
|
-
}
|
|
401
|
-
var entry: Dictionary = {
|
|
402
|
-
"sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()
|
|
403
|
-
}
|
|
404
|
-
var blank: Array = built[2]
|
|
405
|
-
if not blank.is_empty():
|
|
406
|
-
entry["leftBlank"] = blank
|
|
407
|
-
var spare: String = built[3]
|
|
408
|
-
if not spare.is_empty():
|
|
409
|
-
entry["notInAnyTile"] = spare
|
|
410
|
-
made.append(entry)
|
|
411
|
-
|
|
412
|
-
var folder: String = GdhSceneFile.make_folder_for(tileset_path)
|
|
413
|
-
if not folder.is_empty():
|
|
414
|
-
return {"ok": false, "error": folder}
|
|
415
|
-
var saving: Error = GdhSceneFile.save_keeping_uid(tileset, tileset_path)
|
|
416
|
-
if saving != OK:
|
|
417
|
-
return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
|
|
418
|
-
|
|
419
|
-
_written(tileset_path)
|
|
420
|
-
return {"ok": true, "tilesetPath": tileset_path, "sources": made}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
## One atlas source from its description, with its tiles made, as [true, source, the grid cells left
|
|
424
|
-
## without a tile for being wholly transparent, a sentence on the pixels no tile covers or ""] or
|
|
425
|
-
## [false, why].
|
|
426
|
-
##
|
|
427
|
-
## Tiles are made in every cell of the grid that has a pixel that is not transparent, which is
|
|
428
|
-
## what the editor offers to do when an atlas gets a texture. Without them the atlas held no tiles,
|
|
429
|
-
## and every cell painted from it drew nothing. The cells passed over and the strip a tile size that
|
|
430
|
-
## does not divide the picture leaves are said, since a count alone left a caller to work out which.
|
|
431
|
-
func _atlas(entry: Variant, index: int) -> Array:
|
|
432
|
-
if not entry is Dictionary:
|
|
433
|
-
return [false, "sources[%d] must be an object with texture and tileSize" % index]
|
|
434
|
-
var source: Dictionary = entry
|
|
435
|
-
var texture_path: String = str(source.get("texture", ""))
|
|
436
|
-
if not texture_path.begins_with("res://") and not texture_path.begins_with("uid://"):
|
|
437
|
-
texture_path = _ensure_res_path(texture_path)
|
|
438
|
-
# Contained here as well as on the server: a relative path climbing out of the project became
|
|
439
|
-
# res://../../ and was loaded.
|
|
440
|
-
if texture_path.split("/").has(".."):
|
|
441
|
-
return [false, "sources[%d].texture leaves the project: %s" % [index, texture_path]]
|
|
442
|
-
if not ResourceLoader.exists(texture_path):
|
|
443
|
-
# An image the engine has not imported loads as nothing, where "no texture" would send a caller
|
|
444
|
-
# looking for a file that is there.
|
|
445
|
-
if FileAccess.file_exists(texture_path) and not FileAccess.file_exists(texture_path + ".import"):
|
|
446
|
-
return [
|
|
447
|
-
false,
|
|
448
|
-
(
|
|
449
|
-
(
|
|
450
|
-
"sources[%d].texture: %s has not been imported, so the engine has no texture for it "
|
|
451
|
-
+ "yet. project_import reimport with resourcePath %s imports it."
|
|
452
|
-
)
|
|
453
|
-
% [index, texture_path, texture_path]
|
|
454
|
-
)
|
|
455
|
-
]
|
|
456
|
-
return [false, "sources[%d].texture: no texture at %s" % [index, texture_path]]
|
|
457
|
-
var texture: Texture2D = load(texture_path) as Texture2D
|
|
458
|
-
if texture == null:
|
|
459
|
-
return [false, "sources[%d].texture: %s is not a texture" % [index, texture_path]]
|
|
460
|
-
|
|
461
|
-
var sized: Array = _xy(source.get("tileSize"), "sources[%d].tileSize" % index)
|
|
462
|
-
if not sized[0]:
|
|
463
|
-
return sized
|
|
464
|
-
var tile_size: Vector2i = sized[1]
|
|
465
|
-
if tile_size.x <= 0 or tile_size.y <= 0:
|
|
466
|
-
return [false, "sources[%d].tileSize must be above zero on both sides" % index]
|
|
467
|
-
|
|
468
|
-
var atlas: TileSetAtlasSource = TileSetAtlasSource.new()
|
|
469
|
-
atlas.texture = texture
|
|
470
|
-
atlas.texture_region_size = tile_size
|
|
471
|
-
for field: String in ["separation", "offset"]:
|
|
472
|
-
if not source.has(field):
|
|
473
|
-
continue
|
|
474
|
-
var pair: Array = _xy(source[field], "sources[%d].%s" % [index, field])
|
|
475
|
-
if not pair[0]:
|
|
476
|
-
return pair
|
|
477
|
-
if field == "separation":
|
|
478
|
-
atlas.separation = pair[1]
|
|
479
|
-
else:
|
|
480
|
-
atlas.margins = pair[1]
|
|
481
|
-
|
|
482
|
-
var grid: Vector2i = atlas.get_atlas_grid_size()
|
|
483
|
-
if grid.x <= 0 or grid.y <= 0:
|
|
484
|
-
return [
|
|
485
|
-
false,
|
|
486
|
-
(
|
|
487
|
-
"sources[%d]: a %s tile does not fit in %s, which is %s"
|
|
488
|
-
% [index, tile_size, texture_path, texture.get_size()]
|
|
489
|
-
)
|
|
490
|
-
]
|
|
491
|
-
var image: Image = texture.get_image()
|
|
492
|
-
var blank: Array = []
|
|
493
|
-
for y: int in grid.y:
|
|
494
|
-
for x: int in grid.x:
|
|
495
|
-
var cell: Vector2i = Vector2i(x, y)
|
|
496
|
-
# Worked out here: the atlas answers a region only for a tile it already has.
|
|
497
|
-
var region: Rect2i = Rect2i(
|
|
498
|
-
atlas.margins + cell * (atlas.texture_region_size + atlas.separation),
|
|
499
|
-
atlas.texture_region_size
|
|
500
|
-
)
|
|
501
|
-
if image != null and _blank(image, region):
|
|
502
|
-
blank.append({"x": x, "y": y})
|
|
503
|
-
continue
|
|
504
|
-
atlas.create_tile(cell)
|
|
505
|
-
var covered: Vector2i = atlas.margins + grid * tile_size + (grid - Vector2i.ONE) * atlas.separation
|
|
506
|
-
var left: Vector2i = Vector2i(texture.get_size()) - covered
|
|
507
|
-
var sides: PackedStringArray = []
|
|
508
|
-
if left.x > 0:
|
|
509
|
-
var _right: bool = sides.append("%d px on the right" % left.x)
|
|
510
|
-
if left.y > 0:
|
|
511
|
-
var _bottom: bool = sides.append("%d px at the bottom" % left.y)
|
|
512
|
-
var spare: String = (
|
|
513
|
-
""
|
|
514
|
-
if sides.is_empty()
|
|
515
|
-
else (
|
|
516
|
-
"%s is %d by %d, so %s %s in no %d by %d tile."
|
|
517
|
-
% [
|
|
518
|
-
texture_path,
|
|
519
|
-
texture.get_width(),
|
|
520
|
-
texture.get_height(),
|
|
521
|
-
" and ".join(sides),
|
|
522
|
-
"is" if sides.size() == 1 else "are",
|
|
523
|
-
tile_size.x,
|
|
524
|
-
tile_size.y
|
|
525
|
-
]
|
|
526
|
-
)
|
|
527
|
-
)
|
|
528
|
-
return [true, atlas, blank, spare]
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
func _blank(image: Image, region: Rect2i) -> bool:
|
|
532
|
-
if image.is_compressed():
|
|
533
|
-
return false
|
|
534
|
-
return image.get_region(region).is_invisible()
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
## The sources a tile set holds, as "source 0", "sources 0, 2" or "no sources", for saying what a
|
|
538
|
-
## cell could have named instead.
|
|
539
|
-
func _sources_of(tile_set: TileSet) -> String:
|
|
540
|
-
var ids: PackedStringArray = []
|
|
541
|
-
for index: int in tile_set.get_source_count():
|
|
542
|
-
var _listed: bool = ids.append(str(tile_set.get_source_id(index)))
|
|
543
|
-
if ids.is_empty():
|
|
544
|
-
return "no sources"
|
|
545
|
-
return ("source " if ids.size() == 1 else "sources ") + ", ".join(ids)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
549
|
-
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
550
|
-
var node_path: String = str(args.get("nodePath", ""))
|
|
551
|
-
if scene_path == "res://":
|
|
552
|
-
return {"ok": false, "error": "scenePath is required"}
|
|
553
|
-
|
|
554
|
-
var cells: Variant = args.get("cells", [])
|
|
555
|
-
# Refused rather than skipped: placing nothing and answering success is indistinguishable
|
|
556
|
-
# from placing everything, and the caller only finds out by opening the scene.
|
|
557
|
-
if typeof(cells) != TYPE_ARRAY:
|
|
558
|
-
return {"ok": false, "error": "cells must be an array of cells to place"}
|
|
559
|
-
var placed: Array = cells
|
|
560
|
-
if placed.is_empty():
|
|
561
|
-
return {"ok": false, "error": "cells is empty, so there is nothing to place"}
|
|
562
|
-
|
|
563
|
-
var scene: GdhSceneFile = GdhSceneFile.new()
|
|
564
|
-
var refused: Dictionary = scene.open(scene_path, true)
|
|
565
|
-
if not refused.is_empty():
|
|
566
|
-
return refused
|
|
567
|
-
|
|
568
|
-
var layer: int = GdhRead.as_int(args.get("layer", 0))
|
|
569
|
-
var found: Array = _tile_node(scene, node_path, layer)
|
|
570
|
-
if found[0] == null:
|
|
571
|
-
return scene.refuse(str(found[1]))
|
|
572
|
-
var node: Node = found[0]
|
|
573
|
-
var layered: TileMap = node as TileMap
|
|
574
|
-
var single: TileMapLayer = node as TileMapLayer
|
|
575
|
-
|
|
576
|
-
# set_cell stores a cell whatever it is given, and a cell naming no tile simply draws nothing, so
|
|
577
|
-
# the source, the tile and its alternative are checked here: the alternative is a call that
|
|
578
|
-
# reports placing tiles and a scene that shows none.
|
|
579
|
-
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
580
|
-
|
|
581
|
-
scene.asked(node, ["layer_%d/tile_data" % layer if layered != null else "tile_map_data"])
|
|
582
|
-
# Every cell is checked before any is placed, and every one refused is named: stopping at the first
|
|
583
|
-
# left a caller fixing one cell a call.
|
|
584
|
-
var checked: Array = []
|
|
585
|
-
var refusals: PackedStringArray = []
|
|
586
|
-
for index: int in placed.size():
|
|
587
|
-
var cell: Array = _cell(placed[index], index, tile_set)
|
|
588
|
-
if not cell[0]:
|
|
589
|
-
var _named: bool = refusals.append(str(cell[1]))
|
|
590
|
-
checked.append(cell)
|
|
591
|
-
if not refusals.is_empty():
|
|
592
|
-
return scene.refuse("%s. Nothing was placed." % "; ".join(refusals))
|
|
593
|
-
var wanted: Array = []
|
|
594
|
-
var erased: int = 0
|
|
595
|
-
for cell: Array in checked:
|
|
596
|
-
var at: Vector2i = cell[1]
|
|
597
|
-
var source_id: int = cell[2]
|
|
598
|
-
var atlas_coords: Vector2i = cell[3]
|
|
599
|
-
var alternative: int = cell[4]
|
|
600
|
-
if layered != null:
|
|
601
|
-
layered.set_cell(layer, at, source_id, atlas_coords, alternative)
|
|
602
|
-
else:
|
|
603
|
-
single.set_cell(at, source_id, atlas_coords, alternative)
|
|
604
|
-
wanted.append([at, source_id, atlas_coords, alternative])
|
|
605
|
-
if source_id == -1:
|
|
606
|
-
erased += 1
|
|
607
|
-
|
|
608
|
-
var at_path: String = scene.path_of(node)
|
|
609
|
-
var written: Dictionary = scene.write(
|
|
610
|
-
func(saved: Node) -> String:
|
|
611
|
-
var copy: Node = saved.get_node_or_null(at_path)
|
|
612
|
-
for entry: Array in wanted:
|
|
613
|
-
var at: Vector2i = entry[0]
|
|
614
|
-
var held: Array = _cell_at(copy, layer, at)
|
|
615
|
-
if held != entry.slice(1):
|
|
616
|
-
return "the cell at %s would load as %s, not %s." % [at, held, entry.slice(1)]
|
|
617
|
-
return ""
|
|
618
|
-
)
|
|
619
|
-
if not written.is_empty():
|
|
620
|
-
return written
|
|
621
|
-
|
|
622
|
-
return {
|
|
623
|
-
"ok": true,
|
|
624
|
-
"layer": layer,
|
|
625
|
-
"placed": wanted.size() - erased,
|
|
626
|
-
"erased": erased,
|
|
627
|
-
"requested": placed.size()
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
## The node at [param node_path] cells are painted on, with a TileSet, as [node, why not].
|
|
632
|
-
## TileMapLayer is the node since 4.3, and TileMap the one it replaced, which keeps its layers.
|
|
633
|
-
func _tile_node(scene: GdhSceneFile, node_path: String, layer: int) -> Array:
|
|
634
|
-
var node: Node = (
|
|
635
|
-
scene.root if node_path == "." or node_path == "" else scene.root.get_node_or_null(node_path)
|
|
636
|
-
)
|
|
637
|
-
if node == null:
|
|
638
|
-
return [null, "No node at %s in %s" % [node_path, scene.path]]
|
|
639
|
-
var layered: TileMap = node as TileMap
|
|
640
|
-
var single: TileMapLayer = node as TileMapLayer
|
|
641
|
-
if layered == null and single == null:
|
|
642
|
-
return [null, "%s is a %s, not a TileMapLayer or a TileMap" % [node_path, node.get_class()]]
|
|
643
|
-
var foreign: String = scene.foreign(node)
|
|
644
|
-
if not foreign.is_empty():
|
|
645
|
-
return [null, foreign]
|
|
646
|
-
if single != null and layer != 0:
|
|
647
|
-
return [null, "%s is a TileMapLayer, which is one layer; layer applies to a TileMap" % node_path]
|
|
648
|
-
if layered != null and (layer < 0 or layer >= layered.get_layers_count()):
|
|
649
|
-
return [null, "%s has %d layers, and no layer %d" % [node_path, layered.get_layers_count(), layer]]
|
|
650
|
-
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
651
|
-
if tile_set == null:
|
|
652
|
-
return [null, "%s has no TileSet, so no cell can name a source" % node_path]
|
|
653
|
-
return [node, ""]
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
## One cell from its description, checked against [param tile_set], as
|
|
657
|
-
## [true, coords, source id, atlas coords, alternative] or [false, why]. A source of -1 erases the
|
|
658
|
-
## cell, as set_cell takes it, and needs no atlasCoords.
|
|
659
|
-
func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
660
|
-
if not entry is Dictionary:
|
|
661
|
-
return [false, "cells[%d] must be an object with coords, sourceId and atlasCoords" % index]
|
|
662
|
-
var cell: Dictionary = entry
|
|
663
|
-
for field: String in ["coords", "sourceId"]:
|
|
664
|
-
if not cell.has(field):
|
|
665
|
-
return [false, "cells[%d] has no %s" % [index, field]]
|
|
666
|
-
var at: Array = _xy(cell["coords"], "cells[%d].coords" % index)
|
|
667
|
-
if not at[0]:
|
|
668
|
-
return at
|
|
669
|
-
var coords: Vector2i = at[1]
|
|
670
|
-
var named: String = "cells[%d] at (%d, %d)" % [index, coords.x, coords.y]
|
|
671
|
-
var source_id: int = GdhRead.as_int(cell["sourceId"], -2)
|
|
672
|
-
if source_id == -1:
|
|
673
|
-
return [true, coords, -1, Vector2i(-1, -1), -1]
|
|
674
|
-
if not cell.has("atlasCoords"):
|
|
675
|
-
return [
|
|
676
|
-
false, "%s has no atlasCoords, which every cell but an erased one (sourceId -1) needs" % named
|
|
677
|
-
]
|
|
678
|
-
var atlas_at: Array = _xy(cell["atlasCoords"], "cells[%d].atlasCoords" % index)
|
|
679
|
-
if not atlas_at[0]:
|
|
680
|
-
return atlas_at
|
|
681
|
-
if not tile_set.has_source(source_id):
|
|
682
|
-
return [
|
|
683
|
-
false,
|
|
684
|
-
(
|
|
685
|
-
"%s names source %d, and the TileSet has %s; sourceId -1 erases a cell"
|
|
686
|
-
% [named, source_id, _sources_of(tile_set)]
|
|
687
|
-
)
|
|
688
|
-
]
|
|
689
|
-
var source: TileSetSource = tile_set.get_source(source_id)
|
|
690
|
-
var atlas_coords: Vector2i = atlas_at[1]
|
|
691
|
-
var alternative: int = GdhRead.as_int(cell.get("alternativeTile", 0))
|
|
692
|
-
if not source.has_tile(atlas_coords):
|
|
693
|
-
return [
|
|
694
|
-
false,
|
|
695
|
-
"%s: source %d has no tile at %s; it has %s" % [named, source_id, atlas_coords, _tiles_of(source)]
|
|
696
|
-
]
|
|
697
|
-
if not source.has_alternative_tile(atlas_coords, alternative):
|
|
698
|
-
return [
|
|
699
|
-
false,
|
|
700
|
-
(
|
|
701
|
-
"%s: the tile at %s in source %d has no alternative %d"
|
|
702
|
-
% [named, atlas_coords, source_id, alternative]
|
|
703
|
-
)
|
|
704
|
-
]
|
|
705
|
-
return [true, coords, source_id, atlas_coords, alternative]
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
|
|
709
|
-
## has none: a refusal naming the one missing sent a caller guessing which of the others exist.
|
|
710
|
-
static func _tiles_of(source: TileSetSource) -> String:
|
|
711
|
-
var count: int = source.get_tiles_count()
|
|
712
|
-
if count == 0:
|
|
713
|
-
return "no tiles"
|
|
714
|
-
var named: Array[String] = []
|
|
715
|
-
for index: int in mini(count, 12):
|
|
716
|
-
named.append(str(source.get_tile_id(index)))
|
|
717
|
-
var more: String = "" if count <= 12 else " and %d more" % (count - 12)
|
|
718
|
-
return "tiles at " + ", ".join(PackedStringArray(named)) + more
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
## The cells a TileMapLayer or a TileMap holds, as {"cellCount", "cells"} in the form
|
|
722
|
-
## set_tilemap_cells takes, or {} for any other node. The scene keeps them as packed bytes, which a
|
|
723
|
-
## read answered with and nobody could read: a probe counted three cells by decoding the base64 by
|
|
724
|
-
## hand. At most CELLS_SHOWN are listed, the count saying how many there are.
|
|
725
|
-
static func cells_of(node: Node) -> Dictionary:
|
|
726
|
-
var listed: Array[Dictionary] = []
|
|
727
|
-
var count: int = 0
|
|
728
|
-
var single: TileMapLayer = node as TileMapLayer
|
|
729
|
-
var layered: TileMap = node as TileMap
|
|
730
|
-
if single != null:
|
|
731
|
-
var used: Array[Vector2i] = single.get_used_cells()
|
|
732
|
-
count = used.size()
|
|
733
|
-
for at: Vector2i in used.slice(0, CELLS_SHOWN):
|
|
734
|
-
listed.append(
|
|
735
|
-
_cell_entry(
|
|
736
|
-
at,
|
|
737
|
-
single.get_cell_source_id(at),
|
|
738
|
-
single.get_cell_atlas_coords(at),
|
|
739
|
-
single.get_cell_alternative_tile(at)
|
|
740
|
-
)
|
|
741
|
-
)
|
|
742
|
-
elif layered != null:
|
|
743
|
-
for layer: int in layered.get_layers_count():
|
|
744
|
-
var used: Array[Vector2i] = layered.get_used_cells(layer)
|
|
745
|
-
count += used.size()
|
|
746
|
-
for at: Vector2i in used:
|
|
747
|
-
if listed.size() >= CELLS_SHOWN:
|
|
748
|
-
break
|
|
749
|
-
var entry: Dictionary = _cell_entry(
|
|
750
|
-
at,
|
|
751
|
-
layered.get_cell_source_id(layer, at),
|
|
752
|
-
layered.get_cell_atlas_coords(layer, at),
|
|
753
|
-
layered.get_cell_alternative_tile(layer, at)
|
|
754
|
-
)
|
|
755
|
-
entry["layer"] = layer
|
|
756
|
-
listed.append(entry)
|
|
757
|
-
else:
|
|
758
|
-
return {}
|
|
759
|
-
var answer: Dictionary = {"cellCount": count, "cells": listed}
|
|
760
|
-
if count > listed.size():
|
|
761
|
-
answer["cellsNote"] = "The first %d of %d cells are listed." % [listed.size(), count]
|
|
379
|
+
var answer: Dictionary = {"ok": true, "shaderPath": shader_path, "shaderType": shader_type}
|
|
380
|
+
var errors: Array[Dictionary] = _shader_errors(shader_path)
|
|
381
|
+
answer["compiles"] = errors.is_empty()
|
|
382
|
+
if not errors.is_empty():
|
|
383
|
+
answer["compileErrors"] = errors
|
|
762
384
|
return answer
|
|
763
385
|
|
|
764
386
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
var
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
387
|
+
## The errors the engine raises compiling the shader at [param path], as {line, message} in order.
|
|
388
|
+
## A shader is compiled once something reads what it declares, and a create answering only the path
|
|
389
|
+
## left a missing semicolon to show up wherever the shader was first drawn with.
|
|
390
|
+
func _shader_errors(path: String) -> Array[Dictionary]:
|
|
391
|
+
var caught: GdhShaderErrors = GdhShaderErrors.new()
|
|
392
|
+
OS.add_logger(caught)
|
|
393
|
+
var shader: Shader = ResourceLoader.load(path, "Shader", ResourceLoader.CACHE_MODE_IGNORE) as Shader
|
|
394
|
+
if shader != null:
|
|
395
|
+
var _uniforms: Array = shader.get_shader_uniform_list()
|
|
396
|
+
OS.remove_logger(caught)
|
|
397
|
+
return caught.raised
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
## Every shader error the engine raises while it is attached, in order.
|
|
401
|
+
class GdhShaderErrors:
|
|
402
|
+
extends Logger
|
|
403
|
+
|
|
404
|
+
var raised: Array[Dictionary] = []
|
|
405
|
+
|
|
406
|
+
func _log_message(_message: String, _error: bool) -> void:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
# The engine's own signature for a Logger, eight parameters and not this project's to shape.
|
|
410
|
+
# gdlint:ignore = function-arguments-number
|
|
411
|
+
func _log_error(
|
|
412
|
+
_function: String,
|
|
413
|
+
_file: String,
|
|
414
|
+
line: int,
|
|
415
|
+
code: String,
|
|
416
|
+
rationale: String,
|
|
417
|
+
_editor_notify: bool,
|
|
418
|
+
error_type: int,
|
|
419
|
+
_script_backtraces: Array[ScriptBacktrace],
|
|
420
|
+
) -> void:
|
|
421
|
+
if error_type == Logger.ERROR_TYPE_SHADER:
|
|
422
|
+
raised.append({"line": line, "message": code if rationale.is_empty() else rationale})
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
## Saves [param theme] over [param theme_path] with [param items] changed, answering "" or why not.
|
|
426
|
+
func _save_theme(theme: Theme, theme_path: String, items: Array) -> String:
|
|
794
427
|
var previous: PackedByteArray = FileAccess.get_file_as_bytes(theme_path)
|
|
795
|
-
var
|
|
428
|
+
var carried: Dictionary = GdhHeadlessCheck.embedded_in(GdhHeadlessCheck.stored(theme).values(), {})
|
|
429
|
+
var saving: Error = GdhSceneUids.save_keeping_uid(theme, theme_path)
|
|
796
430
|
if saving != OK:
|
|
797
431
|
return "Failed to save %s: %s" % [theme_path, error_string(saving)]
|
|
798
|
-
var unkept: String = _unkept_headless(theme, theme_path,
|
|
432
|
+
var unkept: String = _unkept_headless(theme, theme_path, items, previous, carried)
|
|
799
433
|
if not unkept.is_empty():
|
|
800
434
|
return "Nothing was written: " + unkept + GdhHeadlessCheck.put_back(theme_path, previous)
|
|
801
435
|
_written(theme_path)
|
|
@@ -824,7 +458,7 @@ func set_theme_color(args: Dictionary) -> Dictionary:
|
|
|
824
458
|
var color: Color = parsed[1]
|
|
825
459
|
theme.set_color(color_name, control_type, color)
|
|
826
460
|
|
|
827
|
-
var unsaved: String = _save_theme(theme, theme_path, "%s/colors/%s" % [control_type, color_name])
|
|
461
|
+
var unsaved: String = _save_theme(theme, theme_path, ["%s/colors/%s" % [control_type, color_name]])
|
|
828
462
|
if not unsaved.is_empty():
|
|
829
463
|
return {"ok": false, "error": unsaved}
|
|
830
464
|
|
|
@@ -877,7 +511,9 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
877
511
|
}
|
|
878
512
|
theme.set_font_size(font_size_name, control_type, int(pixels))
|
|
879
513
|
|
|
880
|
-
var unsaved: String = _save_theme(
|
|
514
|
+
var unsaved: String = _save_theme(
|
|
515
|
+
theme, theme_path, ["%s/font_sizes/%s" % [control_type, font_size_name]]
|
|
516
|
+
)
|
|
881
517
|
if not unsaved.is_empty():
|
|
882
518
|
return {"ok": false, "error": unsaved}
|
|
883
519
|
|
|
@@ -896,6 +532,49 @@ func set_theme_font_size(args: Dictionary) -> Dictionary:
|
|
|
896
532
|
)
|
|
897
533
|
|
|
898
534
|
|
|
535
|
+
## Takes one item, such as "colors/font_color", or with none every item of a theme type, out of a
|
|
536
|
+
## Theme: a type is any name, so a misspelt one is written as given, and this is the way back.
|
|
537
|
+
func remove_theme_item(args: Dictionary) -> Dictionary:
|
|
538
|
+
var theme_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
539
|
+
var control_type: String = str(args.get("controlType", ""))
|
|
540
|
+
var theme: Theme = _load_theme(theme_path)
|
|
541
|
+
if theme == null:
|
|
542
|
+
return {"ok": false, "error": "No Theme at %s." % theme_path}
|
|
543
|
+
if not theme.get_type_list().has(control_type):
|
|
544
|
+
var types: PackedStringArray = theme.get_type_list()
|
|
545
|
+
var said: String = "%s holds no type %s" % [theme_path, control_type]
|
|
546
|
+
return {"ok": false, "error": GdhRead.offered(said, control_type, Array(types))}
|
|
547
|
+
var kinds: Array[String] = ["colors", "constants", "fonts", "font_sizes", "icons", "styles"]
|
|
548
|
+
var item: String = str(args.get("item", ""))
|
|
549
|
+
var removed: Array[String] = []
|
|
550
|
+
var held: Array[String] = []
|
|
551
|
+
for kind: int in kinds.size():
|
|
552
|
+
for called: String in theme.get_theme_item_list(kind as Theme.DataType, control_type):
|
|
553
|
+
var entry: String = "%s/%s" % [kinds[kind], called]
|
|
554
|
+
held.append(entry)
|
|
555
|
+
if item.is_empty() or item == entry:
|
|
556
|
+
removed.append("%s/%s" % [control_type, entry])
|
|
557
|
+
theme.clear_theme_item(kind as Theme.DataType, called, control_type)
|
|
558
|
+
var base: String = theme.get_type_variation_base(control_type)
|
|
559
|
+
if item.is_empty():
|
|
560
|
+
# A variation's base is stored apart from its items and would outlive them in the file.
|
|
561
|
+
if not base.is_empty():
|
|
562
|
+
removed.append("%s/base_type" % control_type)
|
|
563
|
+
theme.remove_type(control_type)
|
|
564
|
+
elif removed.is_empty() and held.is_empty():
|
|
565
|
+
var only: String = (
|
|
566
|
+
"%s holds no items, only the type it varies, %s; "
|
|
567
|
+
+ "without item, remove_theme_item takes it out."
|
|
568
|
+
)
|
|
569
|
+
return {"ok": false, "error": only % [control_type, base]}
|
|
570
|
+
elif removed.is_empty():
|
|
571
|
+
return {"ok": false, "error": GdhRead.offered("%s has no item %s" % [control_type, item], item, held)}
|
|
572
|
+
var unsaved: String = _save_theme(theme, theme_path, removed)
|
|
573
|
+
if not unsaved.is_empty():
|
|
574
|
+
return {"ok": false, "error": unsaved}
|
|
575
|
+
return {"ok": true, "themePath": theme_path, "controlType": control_type, "removed": removed}
|
|
576
|
+
|
|
577
|
+
|
|
899
578
|
## [param answer] with a note when its controlType names no class a theme styles. Any name is a
|
|
900
579
|
## theme type, so one that is no class is a type variation, which styles only a control whose
|
|
901
580
|
## theme_type_variation names it, and a misspelt "Buton" would otherwise read as Button set.
|
|
@@ -922,7 +601,7 @@ func _noting_variation(answer: Dictionary) -> Dictionary:
|
|
|
922
601
|
var close: PackedStringArray = GdhRead.nearest(type, themed)
|
|
923
602
|
if not close.is_empty():
|
|
924
603
|
answer["note"] += (
|
|
925
|
-
" If a class was meant, the nearest %s %s."
|
|
926
|
-
% ["is" if close.size() == 1 else "are", ", ".join(close)]
|
|
604
|
+
(" If a class was meant, the nearest %s %s, and remove_theme_item with controlType %s takes this out.")
|
|
605
|
+
% ["is" if close.size() == 1 else "are", ", ".join(close), type]
|
|
927
606
|
)
|
|
928
607
|
return answer
|