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
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
@tool
|
|
2
|
+
extends "resource_tools.gd"
|
|
3
|
+
|
|
4
|
+
## TileSets and the cells of tile maps, beside the resource files the script it extends writes,
|
|
5
|
+
## whose helpers it shares.
|
|
6
|
+
|
|
7
|
+
## How many cells a read of a tile map lists, enough for a level's worth and not a whole world's.
|
|
8
|
+
const CELLS_SHOWN: int = 1000
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## A pair of whole numbers given as {"x": 1, "y": 2} or [1, 2], as [true, Vector2i] or
|
|
12
|
+
## [false, why]. The server checks the arguments it is given but not what is inside them, and a
|
|
13
|
+
## pair given the other way went into a typed local and failed as "Invalid tool result".
|
|
14
|
+
func _xy(value: Variant, label: String) -> Array:
|
|
15
|
+
var x: Variant = null
|
|
16
|
+
var y: Variant = null
|
|
17
|
+
if value is Dictionary:
|
|
18
|
+
var fields: Dictionary = value
|
|
19
|
+
x = fields.get("x")
|
|
20
|
+
y = fields.get("y")
|
|
21
|
+
elif value is Array:
|
|
22
|
+
var pair: Array = value
|
|
23
|
+
if pair.size() == 2:
|
|
24
|
+
x = pair[0]
|
|
25
|
+
y = pair[1]
|
|
26
|
+
for number: Variant in [x, y]:
|
|
27
|
+
if (
|
|
28
|
+
not (number is float or number is int)
|
|
29
|
+
or GdhRead.as_float(number) != floorf(GdhRead.as_float(number))
|
|
30
|
+
):
|
|
31
|
+
return [
|
|
32
|
+
false,
|
|
33
|
+
'%s must be two whole numbers, {"x": .., "y": ..}, not %s.' % [label, JSON.stringify(value)]
|
|
34
|
+
]
|
|
35
|
+
return [true, Vector2i(GdhRead.as_int(x), GdhRead.as_int(y))]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
func create_tileset(args: Dictionary) -> Dictionary:
|
|
39
|
+
var tileset_path: String = _ensure_res_path(str(args.get("resourcePath", "")))
|
|
40
|
+
if tileset_path == "res://":
|
|
41
|
+
return {"ok": false, "error": "resourcePath is required"}
|
|
42
|
+
# modify sets properties and builds no atlas, so the other path is the only way on offered.
|
|
43
|
+
var taken: String = (
|
|
44
|
+
""
|
|
45
|
+
if not FileAccess.file_exists(tileset_path)
|
|
46
|
+
else (
|
|
47
|
+
(
|
|
48
|
+
"%s already exists, and create_tileset builds a whole new TileSet, which would replace "
|
|
49
|
+
+ "everything in it, the tiles a scene paints with among them. Give resourcePath a path "
|
|
50
|
+
+ "nothing is at."
|
|
51
|
+
)
|
|
52
|
+
% tileset_path
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
if not taken.is_empty():
|
|
56
|
+
return {"ok": false, "error": taken}
|
|
57
|
+
|
|
58
|
+
var given: Variant = args.get("sources", [])
|
|
59
|
+
if not given is Array:
|
|
60
|
+
return {"ok": false, "error": "sources must list at least one atlas"}
|
|
61
|
+
var sources: Array = given
|
|
62
|
+
if sources.is_empty():
|
|
63
|
+
return {"ok": false, "error": "sources must list at least one atlas"}
|
|
64
|
+
|
|
65
|
+
var tileset: TileSet = TileSet.new()
|
|
66
|
+
var made: Array = []
|
|
67
|
+
for index: int in sources.size():
|
|
68
|
+
var built: Array = _atlas(sources[index], index)
|
|
69
|
+
if not built[0]:
|
|
70
|
+
return {"ok": false, "error": built[1]}
|
|
71
|
+
var atlas: TileSetAtlasSource = built[1]
|
|
72
|
+
# The first source sets the tile set's own tile size, which is what a TileMapLayer draws
|
|
73
|
+
# cells at; left at its default of 16 it did not match the atlas.
|
|
74
|
+
if index == 0:
|
|
75
|
+
tileset.tile_size = atlas.texture_region_size
|
|
76
|
+
var added: int = tileset.add_source(atlas)
|
|
77
|
+
if added < 0:
|
|
78
|
+
return {
|
|
79
|
+
"ok": false,
|
|
80
|
+
"error": "The TileSet did not take the atlas for %s" % atlas.texture.resource_path
|
|
81
|
+
}
|
|
82
|
+
var entry: Dictionary = {
|
|
83
|
+
"sourceId": added, "texture": atlas.texture.resource_path, "tiles": atlas.get_tiles_count()
|
|
84
|
+
}
|
|
85
|
+
var blank: Array = built[2]
|
|
86
|
+
if not blank.is_empty():
|
|
87
|
+
entry["leftBlank"] = blank
|
|
88
|
+
var spare: String = built[3]
|
|
89
|
+
if not spare.is_empty():
|
|
90
|
+
entry["notInAnyTile"] = spare
|
|
91
|
+
made.append(entry)
|
|
92
|
+
|
|
93
|
+
var folder: String = GdhSceneFile.make_folder_for(tileset_path)
|
|
94
|
+
if not folder.is_empty():
|
|
95
|
+
return {"ok": false, "error": folder}
|
|
96
|
+
var saving: Error = GdhSceneUids.save_keeping_uid(tileset, tileset_path)
|
|
97
|
+
if saving != OK:
|
|
98
|
+
return {"ok": false, "error": "Failed to save %s: %s" % [tileset_path, error_string(saving)]}
|
|
99
|
+
|
|
100
|
+
_written(tileset_path)
|
|
101
|
+
# The size cells are drawn at, which the file leaves out when it is the default 16 by 16.
|
|
102
|
+
return {
|
|
103
|
+
"ok": true,
|
|
104
|
+
"tilesetPath": tileset_path,
|
|
105
|
+
"tileSize": {"x": tileset.tile_size.x, "y": tileset.tile_size.y},
|
|
106
|
+
"sources": made
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
## One atlas source from its description, with its tiles made, as [true, source, the grid cells left
|
|
111
|
+
## without a tile for being wholly transparent, a sentence on the pixels no tile covers or ""] or
|
|
112
|
+
## [false, why].
|
|
113
|
+
##
|
|
114
|
+
## Tiles are made in every cell of the grid that has a pixel that is not transparent, which is
|
|
115
|
+
## what the editor offers to do when an atlas gets a texture. Without them the atlas held no tiles,
|
|
116
|
+
## and every cell painted from it drew nothing. The cells passed over and the strip a tile size that
|
|
117
|
+
## does not divide the picture leaves are said, since a count alone left a caller to work out which.
|
|
118
|
+
func _atlas(entry: Variant, index: int) -> Array:
|
|
119
|
+
if not entry is Dictionary:
|
|
120
|
+
return [false, "sources[%d] must be an object with texture and tileSize" % index]
|
|
121
|
+
var source: Dictionary = entry
|
|
122
|
+
var texture_path: String = str(source.get("texture", ""))
|
|
123
|
+
if not texture_path.begins_with("res://") and not texture_path.begins_with("uid://"):
|
|
124
|
+
texture_path = _ensure_res_path(texture_path)
|
|
125
|
+
# Contained here as well as on the server: a relative path climbing out of the project became
|
|
126
|
+
# res://../../ and was loaded.
|
|
127
|
+
if texture_path.split("/").has(".."):
|
|
128
|
+
return [false, "sources[%d].texture leaves the project: %s" % [index, texture_path]]
|
|
129
|
+
if not ResourceLoader.exists(texture_path):
|
|
130
|
+
# An image the engine has not imported loads as nothing, where "no texture" would send a caller
|
|
131
|
+
# looking for a file that is there.
|
|
132
|
+
if FileAccess.file_exists(texture_path) and not FileAccess.file_exists(texture_path + ".import"):
|
|
133
|
+
return [
|
|
134
|
+
false,
|
|
135
|
+
(
|
|
136
|
+
(
|
|
137
|
+
"sources[%d].texture: %s has not been imported, so the engine has no texture for it "
|
|
138
|
+
+ "yet. project_import reimport with resourcePath %s imports it."
|
|
139
|
+
)
|
|
140
|
+
% [index, texture_path, texture_path]
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
return [false, "sources[%d].texture: no texture at %s" % [index, texture_path]]
|
|
144
|
+
var texture: Texture2D = load(texture_path) as Texture2D
|
|
145
|
+
if texture == null:
|
|
146
|
+
return [false, "sources[%d].texture: %s is not a texture" % [index, texture_path]]
|
|
147
|
+
|
|
148
|
+
var sized: Array = _xy(source.get("tileSize"), "sources[%d].tileSize" % index)
|
|
149
|
+
if not sized[0]:
|
|
150
|
+
return sized
|
|
151
|
+
var tile_size: Vector2i = sized[1]
|
|
152
|
+
if tile_size.x <= 0 or tile_size.y <= 0:
|
|
153
|
+
return [false, "sources[%d].tileSize must be above zero on both sides" % index]
|
|
154
|
+
|
|
155
|
+
var atlas: TileSetAtlasSource = TileSetAtlasSource.new()
|
|
156
|
+
atlas.texture = texture
|
|
157
|
+
atlas.texture_region_size = tile_size
|
|
158
|
+
for field: String in ["separation", "offset"]:
|
|
159
|
+
if not source.has(field):
|
|
160
|
+
continue
|
|
161
|
+
var pair: Array = _xy(source[field], "sources[%d].%s" % [index, field])
|
|
162
|
+
if not pair[0]:
|
|
163
|
+
return pair
|
|
164
|
+
if field == "separation":
|
|
165
|
+
atlas.separation = pair[1]
|
|
166
|
+
else:
|
|
167
|
+
atlas.margins = pair[1]
|
|
168
|
+
|
|
169
|
+
var grid: Vector2i = atlas.get_atlas_grid_size()
|
|
170
|
+
if grid.x <= 0 or grid.y <= 0:
|
|
171
|
+
return [
|
|
172
|
+
false,
|
|
173
|
+
(
|
|
174
|
+
"sources[%d]: a %s tile does not fit in %s, which is %s"
|
|
175
|
+
% [index, tile_size, texture_path, texture.get_size()]
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
var image: Image = texture.get_image()
|
|
179
|
+
var blank: Array = []
|
|
180
|
+
for y: int in grid.y:
|
|
181
|
+
for x: int in grid.x:
|
|
182
|
+
var cell: Vector2i = Vector2i(x, y)
|
|
183
|
+
# Worked out here: the atlas answers a region only for a tile it already has.
|
|
184
|
+
var region: Rect2i = Rect2i(
|
|
185
|
+
atlas.margins + cell * (atlas.texture_region_size + atlas.separation),
|
|
186
|
+
atlas.texture_region_size
|
|
187
|
+
)
|
|
188
|
+
if image != null and _blank(image, region):
|
|
189
|
+
blank.append({"x": x, "y": y})
|
|
190
|
+
continue
|
|
191
|
+
atlas.create_tile(cell)
|
|
192
|
+
var covered: Vector2i = atlas.margins + grid * tile_size + (grid - Vector2i.ONE) * atlas.separation
|
|
193
|
+
var left: Vector2i = Vector2i(texture.get_size()) - covered
|
|
194
|
+
var sides: PackedStringArray = []
|
|
195
|
+
if left.x > 0:
|
|
196
|
+
var _right: bool = sides.append("%d px on the right" % left.x)
|
|
197
|
+
if left.y > 0:
|
|
198
|
+
var _bottom: bool = sides.append("%d px at the bottom" % left.y)
|
|
199
|
+
var spare: String = (
|
|
200
|
+
""
|
|
201
|
+
if sides.is_empty()
|
|
202
|
+
else (
|
|
203
|
+
"%s is %d by %d, so %s %s in no %d by %d tile."
|
|
204
|
+
% [
|
|
205
|
+
texture_path,
|
|
206
|
+
texture.get_width(),
|
|
207
|
+
texture.get_height(),
|
|
208
|
+
" and ".join(sides),
|
|
209
|
+
"is" if sides.size() == 1 else "are",
|
|
210
|
+
tile_size.x,
|
|
211
|
+
tile_size.y
|
|
212
|
+
]
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return [true, atlas, blank, spare]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
func _blank(image: Image, region: Rect2i) -> bool:
|
|
219
|
+
if image.is_compressed():
|
|
220
|
+
return false
|
|
221
|
+
return image.get_region(region).is_invisible()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## The sources a tile set holds, as "source 0", "sources 0, 2" or "no sources", for saying what a
|
|
225
|
+
## cell could have named instead.
|
|
226
|
+
func _sources_of(tile_set: TileSet) -> String:
|
|
227
|
+
var ids: PackedStringArray = []
|
|
228
|
+
for index: int in tile_set.get_source_count():
|
|
229
|
+
var _listed: bool = ids.append(str(tile_set.get_source_id(index)))
|
|
230
|
+
if ids.is_empty():
|
|
231
|
+
return "no sources"
|
|
232
|
+
return ("source " if ids.size() == 1 else "sources ") + ", ".join(ids)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
func set_tilemap_cells(args: Dictionary) -> Dictionary:
|
|
236
|
+
var scene_path: String = _ensure_res_path(str(args.get("scenePath", "")))
|
|
237
|
+
var node_path: String = str(args.get("nodePath", ""))
|
|
238
|
+
if scene_path == "res://":
|
|
239
|
+
return {"ok": false, "error": "scenePath is required"}
|
|
240
|
+
|
|
241
|
+
var cells: Variant = args.get("cells", [])
|
|
242
|
+
# Refused rather than skipped: placing nothing and answering success is indistinguishable
|
|
243
|
+
# from placing everything, and the caller only finds out by opening the scene.
|
|
244
|
+
if typeof(cells) != TYPE_ARRAY:
|
|
245
|
+
return {"ok": false, "error": "cells must be an array of cells to place"}
|
|
246
|
+
var placed: Array = cells
|
|
247
|
+
if placed.is_empty():
|
|
248
|
+
return {"ok": false, "error": "cells is empty, so there is nothing to place"}
|
|
249
|
+
|
|
250
|
+
var scene: GdhSceneFile = GdhSceneFile.new()
|
|
251
|
+
var refused: Dictionary = scene.open(scene_path, true)
|
|
252
|
+
if not refused.is_empty():
|
|
253
|
+
return refused
|
|
254
|
+
|
|
255
|
+
var layer: int = GdhRead.as_int(args.get("layer", 0))
|
|
256
|
+
var found: Array = _tile_node(scene, node_path, layer)
|
|
257
|
+
if found[0] == null:
|
|
258
|
+
return scene.refuse(str(found[1]))
|
|
259
|
+
var node: Node = found[0]
|
|
260
|
+
var layered: TileMap = node as TileMap
|
|
261
|
+
var single: TileMapLayer = node as TileMapLayer
|
|
262
|
+
|
|
263
|
+
# set_cell stores a cell whatever it is given, and a cell naming no tile simply draws nothing, so
|
|
264
|
+
# the source, the tile and its alternative are checked here: the alternative is a call that
|
|
265
|
+
# reports placing tiles and a scene that shows none.
|
|
266
|
+
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
267
|
+
|
|
268
|
+
scene.asked(node, ["layer_%d/tile_data" % layer if layered != null else "tile_map_data"])
|
|
269
|
+
# Every cell is checked before any is placed, and every one refused is named: stopping at the first
|
|
270
|
+
# left a caller fixing one cell a call.
|
|
271
|
+
var checked: Array = []
|
|
272
|
+
var refusals: PackedStringArray = []
|
|
273
|
+
for index: int in placed.size():
|
|
274
|
+
var cell: Array = _cell(placed[index], index, tile_set)
|
|
275
|
+
if not cell[0]:
|
|
276
|
+
var _named: bool = refusals.append(str(cell[1]))
|
|
277
|
+
checked.append(cell)
|
|
278
|
+
if not refusals.is_empty():
|
|
279
|
+
return scene.refuse("%s. Nothing was placed." % "; ".join(refusals))
|
|
280
|
+
var wanted: Array = []
|
|
281
|
+
var erased: int = 0
|
|
282
|
+
for cell: Array in checked:
|
|
283
|
+
var at: Vector2i = cell[1]
|
|
284
|
+
var source_id: int = cell[2]
|
|
285
|
+
var atlas_coords: Vector2i = cell[3]
|
|
286
|
+
var alternative: int = cell[4]
|
|
287
|
+
if layered != null:
|
|
288
|
+
layered.set_cell(layer, at, source_id, atlas_coords, alternative)
|
|
289
|
+
else:
|
|
290
|
+
single.set_cell(at, source_id, atlas_coords, alternative)
|
|
291
|
+
wanted.append([at, source_id, atlas_coords, alternative])
|
|
292
|
+
if source_id == -1:
|
|
293
|
+
erased += 1
|
|
294
|
+
|
|
295
|
+
var at_path: String = scene.path_of(node)
|
|
296
|
+
var written: Dictionary = scene.write(
|
|
297
|
+
func(saved: Node) -> String:
|
|
298
|
+
var copy: Node = saved.get_node_or_null(at_path)
|
|
299
|
+
for entry: Array in wanted:
|
|
300
|
+
var at: Vector2i = entry[0]
|
|
301
|
+
var held: Array = _cell_at(copy, layer, at)
|
|
302
|
+
if held != entry.slice(1):
|
|
303
|
+
return "the cell at %s would load as %s, not %s." % [at, held, entry.slice(1)]
|
|
304
|
+
return ""
|
|
305
|
+
)
|
|
306
|
+
if not written.is_empty():
|
|
307
|
+
return written
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"ok": true,
|
|
311
|
+
"layer": layer,
|
|
312
|
+
"placed": wanted.size() - erased,
|
|
313
|
+
"erased": erased,
|
|
314
|
+
"requested": placed.size()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
## The node at [param node_path] cells are painted on, with a TileSet, as [node, why not].
|
|
319
|
+
## TileMapLayer is the node since 4.3, and TileMap the one it replaced, which keeps its layers.
|
|
320
|
+
func _tile_node(scene: GdhSceneFile, node_path: String, layer: int) -> Array:
|
|
321
|
+
var node: Node = (
|
|
322
|
+
scene.root if node_path == "." or node_path == "" else scene.root.get_node_or_null(node_path)
|
|
323
|
+
)
|
|
324
|
+
if node == null:
|
|
325
|
+
return [null, "No node at %s in %s" % [node_path, scene.path]]
|
|
326
|
+
var layered: TileMap = node as TileMap
|
|
327
|
+
var single: TileMapLayer = node as TileMapLayer
|
|
328
|
+
if layered == null and single == null:
|
|
329
|
+
return [null, "%s is a %s, not a TileMapLayer or a TileMap" % [node_path, node.get_class()]]
|
|
330
|
+
var foreign: String = scene.foreign(node)
|
|
331
|
+
if not foreign.is_empty():
|
|
332
|
+
return [null, foreign]
|
|
333
|
+
if single != null and layer != 0:
|
|
334
|
+
return [null, "%s is a TileMapLayer, which is one layer; layer applies to a TileMap" % node_path]
|
|
335
|
+
if layered != null and (layer < 0 or layer >= layered.get_layers_count()):
|
|
336
|
+
return [null, "%s has %d layers, and no layer %d" % [node_path, layered.get_layers_count(), layer]]
|
|
337
|
+
var tile_set: TileSet = layered.tile_set if layered != null else single.tile_set
|
|
338
|
+
if tile_set == null:
|
|
339
|
+
return [
|
|
340
|
+
null,
|
|
341
|
+
(
|
|
342
|
+
(
|
|
343
|
+
'%s has no TileSet, so no cell can name a source. scene_node set with properties {"tile_set": '
|
|
344
|
+
+ '"res://..."} on %s gives it one, a TileSet file such as resource_edit create_tileset makes.'
|
|
345
|
+
)
|
|
346
|
+
% [node_path, node_path]
|
|
347
|
+
)
|
|
348
|
+
]
|
|
349
|
+
return [node, ""]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
## One cell from its description, checked against [param tile_set], as
|
|
353
|
+
## [true, coords, source id, atlas coords, alternative] or [false, why]. A source of -1 erases the
|
|
354
|
+
## cell, as set_cell takes it, and needs no atlasCoords.
|
|
355
|
+
func _cell(entry: Variant, index: int, tile_set: TileSet) -> Array:
|
|
356
|
+
if not entry is Dictionary:
|
|
357
|
+
return [false, "cells[%d] must be an object with coords, sourceId and atlasCoords" % index]
|
|
358
|
+
var cell: Dictionary = entry
|
|
359
|
+
for field: String in ["coords", "sourceId"]:
|
|
360
|
+
if not cell.has(field):
|
|
361
|
+
return [false, "cells[%d] has no %s" % [index, field]]
|
|
362
|
+
var at: Array = _xy(cell["coords"], "cells[%d].coords" % index)
|
|
363
|
+
if not at[0]:
|
|
364
|
+
return at
|
|
365
|
+
var coords: Vector2i = at[1]
|
|
366
|
+
var named: String = "cells[%d] at (%d, %d)" % [index, coords.x, coords.y]
|
|
367
|
+
var source_id: int = GdhRead.as_int(cell["sourceId"], -2)
|
|
368
|
+
if source_id == -1:
|
|
369
|
+
return [true, coords, -1, Vector2i(-1, -1), -1]
|
|
370
|
+
if not cell.has("atlasCoords"):
|
|
371
|
+
return [
|
|
372
|
+
false, "%s has no atlasCoords, which every cell but an erased one (sourceId -1) needs" % named
|
|
373
|
+
]
|
|
374
|
+
var atlas_at: Array = _xy(cell["atlasCoords"], "cells[%d].atlasCoords" % index)
|
|
375
|
+
if not atlas_at[0]:
|
|
376
|
+
return atlas_at
|
|
377
|
+
if not tile_set.has_source(source_id):
|
|
378
|
+
return [
|
|
379
|
+
false,
|
|
380
|
+
(
|
|
381
|
+
"%s names source %d, and the TileSet has %s; sourceId -1 erases a cell"
|
|
382
|
+
% [named, source_id, _sources_of(tile_set)]
|
|
383
|
+
)
|
|
384
|
+
]
|
|
385
|
+
var source: TileSetSource = tile_set.get_source(source_id)
|
|
386
|
+
var atlas_coords: Vector2i = atlas_at[1]
|
|
387
|
+
var alternative: int = GdhRead.as_int(cell.get("alternativeTile", 0))
|
|
388
|
+
if not source.has_tile(atlas_coords):
|
|
389
|
+
return [
|
|
390
|
+
false,
|
|
391
|
+
"%s: source %d has no tile at %s; it has %s" % [named, source_id, atlas_coords, _tiles_of(source)]
|
|
392
|
+
]
|
|
393
|
+
if not source.has_alternative_tile(atlas_coords, alternative):
|
|
394
|
+
return [
|
|
395
|
+
false,
|
|
396
|
+
(
|
|
397
|
+
"%s: the tile at %s in source %d has no alternative %d"
|
|
398
|
+
% [named, atlas_coords, source_id, alternative]
|
|
399
|
+
)
|
|
400
|
+
]
|
|
401
|
+
return [true, coords, source_id, atlas_coords, alternative]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
## The atlas coordinates [param source] has tiles at, the first twelve and how many more, or that it
|
|
405
|
+
## has none: a refusal naming the one missing sent a caller guessing which of the others exist.
|
|
406
|
+
static func _tiles_of(source: TileSetSource) -> String:
|
|
407
|
+
var count: int = source.get_tiles_count()
|
|
408
|
+
if count == 0:
|
|
409
|
+
return "no tiles"
|
|
410
|
+
var named: Array[String] = []
|
|
411
|
+
for index: int in mini(count, 12):
|
|
412
|
+
named.append(str(source.get_tile_id(index)))
|
|
413
|
+
var more: String = "" if count <= 12 else " and %d more" % (count - 12)
|
|
414
|
+
return "tiles at " + ", ".join(PackedStringArray(named)) + more
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
## The cells a TileMapLayer or a TileMap holds, as {"cellCount", "cells"} in the form
|
|
418
|
+
## set_tilemap_cells takes, or {} for any other node. The scene keeps them as packed bytes, which a
|
|
419
|
+
## read answered with and nobody could read: a probe counted three cells by decoding the base64 by
|
|
420
|
+
## hand. At most CELLS_SHOWN are listed, the count saying how many there are.
|
|
421
|
+
static func cells_of(node: Node) -> Dictionary:
|
|
422
|
+
var listed: Array[Dictionary] = []
|
|
423
|
+
var count: int = 0
|
|
424
|
+
var single: TileMapLayer = node as TileMapLayer
|
|
425
|
+
var layered: TileMap = node as TileMap
|
|
426
|
+
if single != null:
|
|
427
|
+
var used: Array[Vector2i] = single.get_used_cells()
|
|
428
|
+
count = used.size()
|
|
429
|
+
for at: Vector2i in used.slice(0, CELLS_SHOWN):
|
|
430
|
+
listed.append(
|
|
431
|
+
_cell_entry(
|
|
432
|
+
at,
|
|
433
|
+
single.get_cell_source_id(at),
|
|
434
|
+
single.get_cell_atlas_coords(at),
|
|
435
|
+
single.get_cell_alternative_tile(at)
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
elif layered != null:
|
|
439
|
+
for layer: int in layered.get_layers_count():
|
|
440
|
+
var used: Array[Vector2i] = layered.get_used_cells(layer)
|
|
441
|
+
count += used.size()
|
|
442
|
+
for at: Vector2i in used:
|
|
443
|
+
if listed.size() >= CELLS_SHOWN:
|
|
444
|
+
break
|
|
445
|
+
var entry: Dictionary = _cell_entry(
|
|
446
|
+
at,
|
|
447
|
+
layered.get_cell_source_id(layer, at),
|
|
448
|
+
layered.get_cell_atlas_coords(layer, at),
|
|
449
|
+
layered.get_cell_alternative_tile(layer, at)
|
|
450
|
+
)
|
|
451
|
+
entry["layer"] = layer
|
|
452
|
+
listed.append(entry)
|
|
453
|
+
else:
|
|
454
|
+
return {}
|
|
455
|
+
var answer: Dictionary = {"cellCount": count, "cells": listed}
|
|
456
|
+
if count > listed.size():
|
|
457
|
+
answer["cellsNote"] = "The first %d of %d cells are listed." % [listed.size(), count]
|
|
458
|
+
return answer
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
static func _cell_entry(at: Vector2i, source_id: int, atlas: Vector2i, alternative: int) -> Dictionary:
|
|
462
|
+
return {
|
|
463
|
+
"coords": {"x": at.x, "y": at.y},
|
|
464
|
+
"sourceId": source_id,
|
|
465
|
+
"atlasCoords": {"x": atlas.x, "y": atlas.y},
|
|
466
|
+
"alternativeTile": alternative,
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
func _cell_at(node: Node, layer: int, at: Vector2i) -> Array:
|
|
471
|
+
var layered: TileMap = node as TileMap
|
|
472
|
+
if layered != null:
|
|
473
|
+
return [
|
|
474
|
+
layered.get_cell_source_id(layer, at),
|
|
475
|
+
layered.get_cell_atlas_coords(layer, at),
|
|
476
|
+
layered.get_cell_alternative_tile(layer, at)
|
|
477
|
+
]
|
|
478
|
+
var single: TileMapLayer = node as TileMapLayer
|
|
479
|
+
if single != null:
|
|
480
|
+
return [
|
|
481
|
+
single.get_cell_source_id(at),
|
|
482
|
+
single.get_cell_atlas_coords(at),
|
|
483
|
+
single.get_cell_alternative_tile(at)
|
|
484
|
+
]
|
|
485
|
+
return []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uid://chrp5707sthas
|
|
@@ -5,6 +5,22 @@ extends RefCounted
|
|
|
5
5
|
##
|
|
6
6
|
## This file is the one beside the operations, and `bun run sync:gd` copies it into each addon.
|
|
7
7
|
|
|
8
|
+
const GdhRead = preload("reading.gd")
|
|
9
|
+
|
|
10
|
+
## What each packed array holds, one value of which every element is fitted to.
|
|
11
|
+
const PACKED_ELEMENTS: Dictionary = {
|
|
12
|
+
TYPE_PACKED_BYTE_ARRAY: TYPE_INT,
|
|
13
|
+
TYPE_PACKED_INT32_ARRAY: TYPE_INT,
|
|
14
|
+
TYPE_PACKED_INT64_ARRAY: TYPE_INT,
|
|
15
|
+
TYPE_PACKED_FLOAT32_ARRAY: TYPE_FLOAT,
|
|
16
|
+
TYPE_PACKED_FLOAT64_ARRAY: TYPE_FLOAT,
|
|
17
|
+
TYPE_PACKED_STRING_ARRAY: TYPE_STRING,
|
|
18
|
+
TYPE_PACKED_VECTOR2_ARRAY: TYPE_VECTOR2,
|
|
19
|
+
TYPE_PACKED_VECTOR3_ARRAY: TYPE_VECTOR3,
|
|
20
|
+
TYPE_PACKED_VECTOR4_ARRAY: TYPE_VECTOR4,
|
|
21
|
+
TYPE_PACKED_COLOR_ARRAY: TYPE_COLOR,
|
|
22
|
+
}
|
|
23
|
+
|
|
8
24
|
# What each tag has to carry before it is built. A tag naming a type whose keys are not there is a
|
|
9
25
|
# tag somebody wrote by mistake or wrote for a different shape, and building one out of defaults
|
|
10
26
|
# answers with a Transform2D of zeros where the caller meant something else entirely. Unbuilt, it
|
|
@@ -33,6 +49,21 @@ const REQUIRED: Dictionary = {
|
|
|
33
49
|
"RID": [["id"]],
|
|
34
50
|
"Dictionary": [["entries"]],
|
|
35
51
|
}
|
|
52
|
+
## The fields each type reads as numbers. A builder reads a word there as 0, so {"x": 0, "y": "far"}
|
|
53
|
+
## was written as (0, 0) and answered as written.
|
|
54
|
+
const NUMBER_FIELDS: Dictionary = {
|
|
55
|
+
"Vector2": ["x", "y"],
|
|
56
|
+
"Vector2i": ["x", "y"],
|
|
57
|
+
"Vector3": ["x", "y", "z"],
|
|
58
|
+
"Vector3i": ["x", "y", "z"],
|
|
59
|
+
"Vector4": ["x", "y", "z", "w"],
|
|
60
|
+
"Vector4i": ["x", "y", "z", "w"],
|
|
61
|
+
"Rect2": ["x", "y", "width", "height"],
|
|
62
|
+
"Rect2i": ["x", "y", "width", "height"],
|
|
63
|
+
"Plane": ["d"],
|
|
64
|
+
"Quaternion": ["x", "y", "z", "w"],
|
|
65
|
+
"Color": ["r", "g", "b", "a"],
|
|
66
|
+
}
|
|
36
67
|
## The fields a builder reads when they are there and does without when not.
|
|
37
68
|
const OPTIONAL: Dictionary = {
|
|
38
69
|
"Color": ["a"],
|
|
@@ -77,6 +108,86 @@ static func spare(fields: Dictionary, tag: String) -> Array:
|
|
|
77
108
|
return lists
|
|
78
109
|
|
|
79
110
|
|
|
111
|
+
## Whether a [param tag] built from [param fields] reads every one of them, and each it reads as a
|
|
112
|
+
## number holds one.
|
|
113
|
+
static func buildable(fields: Dictionary, tag: String) -> bool:
|
|
114
|
+
return (
|
|
115
|
+
spare(fields, tag).any(func(left: Array) -> bool: return left.is_empty())
|
|
116
|
+
and not_a_number(fields, tag).is_empty()
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
## The first field of [param fields] that a [param tag] reads as a number and that holds no number,
|
|
121
|
+
## or "" when there is none. Digits in quotes are a number, as a write takes them for a float.
|
|
122
|
+
static func not_a_number(fields: Dictionary, tag: String) -> String:
|
|
123
|
+
for key: String in NUMBER_FIELDS.get(tag, []):
|
|
124
|
+
if fields.has(key) and not _is_number(fields[key]):
|
|
125
|
+
return key
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
## Whether every item of [param items] is a number, for a list read by its places as a vector.
|
|
130
|
+
static func all_numbers(items: Array) -> bool:
|
|
131
|
+
return items.all(_is_number)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# A float JSON has no number for, infinity or NaN, crosses tagged as {"_type": "float", "value":
|
|
135
|
+
# "inf"}, and is a number.
|
|
136
|
+
static func _is_number(held: Variant) -> bool:
|
|
137
|
+
if held is int or held is float:
|
|
138
|
+
return true
|
|
139
|
+
if held is String:
|
|
140
|
+
return str(held).is_valid_float()
|
|
141
|
+
if held is Dictionary:
|
|
142
|
+
var tagged: Dictionary = held
|
|
143
|
+
return str(tagged.get("_type", "")) in ["float", "int"] and tagged.has("value")
|
|
144
|
+
return false
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
## Whether [param value] holds a number with a fractional part where an integer is wanted, in a
|
|
148
|
+
## list or a dictionary as much as bare. The conversion would cut 2.7 to 2 without a word, and a
|
|
149
|
+
## dictionary key arrives as the text "2.7", which reads as the same number.
|
|
150
|
+
static func fraction(value: Variant, type: int, elements: Array = []) -> bool:
|
|
151
|
+
if type == TYPE_INT:
|
|
152
|
+
var number: Variant = value
|
|
153
|
+
if typeof(value) == TYPE_STRING:
|
|
154
|
+
number = GdhRead.json_or_null(str(value))
|
|
155
|
+
if typeof(number) != TYPE_FLOAT:
|
|
156
|
+
return false
|
|
157
|
+
var whole: float = number
|
|
158
|
+
return is_finite(whole) and whole != floorf(whole)
|
|
159
|
+
var element: int = GdhRead.as_int(PACKED_ELEMENTS.get(type, TYPE_NIL), TYPE_NIL)
|
|
160
|
+
if type == TYPE_ARRAY and not elements.is_empty():
|
|
161
|
+
element = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
162
|
+
if value is Array and element != TYPE_NIL:
|
|
163
|
+
var items: Array = value
|
|
164
|
+
return items.any(func(item: Variant) -> bool: return fraction(item, element))
|
|
165
|
+
if type == TYPE_DICTIONARY and elements.size() == 2 and value is Dictionary:
|
|
166
|
+
var entries: Dictionary = value
|
|
167
|
+
var key_type: int = GdhRead.as_int(elements[0], TYPE_NIL)
|
|
168
|
+
var value_type: int = GdhRead.as_int(elements[1], TYPE_NIL)
|
|
169
|
+
for key: Variant in entries:
|
|
170
|
+
if fraction(key, key_type) or fraction(entries[key], value_type):
|
|
171
|
+
return true
|
|
172
|
+
return false
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## The packed array [param type] built from [param items], each fitted by [param fit] to what the
|
|
176
|
+
## array holds, or [param items] as they are when one cannot become that, for the caller to refuse.
|
|
177
|
+
## Element by element, so a point given as its fields is read as one: converted whole, a list of
|
|
178
|
+
## {"x": 3, "y": 4} became zero vectors, and the call ran on them.
|
|
179
|
+
static func packed_from(items: Array, type: int, fit: Callable) -> Variant:
|
|
180
|
+
var element: int = PACKED_ELEMENTS[type]
|
|
181
|
+
var numbers: Array[int] = [TYPE_INT, TYPE_FLOAT]
|
|
182
|
+
var built: Array = []
|
|
183
|
+
for item: Variant in items:
|
|
184
|
+
var one: Variant = fit.call(item, element)
|
|
185
|
+
if typeof(one) != element and not (numbers.has(typeof(one)) and numbers.has(element)):
|
|
186
|
+
return items
|
|
187
|
+
built.append(one)
|
|
188
|
+
return type_convert(built, type)
|
|
189
|
+
|
|
190
|
+
|
|
80
191
|
## The sentences a refusal of [param value] where a [param type] goes ends with: which of its keys
|
|
81
192
|
## kept it from being read as one, and how one is written. Empty for a type not built from fields.
|
|
82
193
|
##
|
|
@@ -92,7 +203,23 @@ static func remedy(value: Variant, type: int) -> String:
|
|
|
92
203
|
var shape: String = written_as(type)
|
|
93
204
|
if shape.is_empty():
|
|
94
205
|
return ""
|
|
95
|
-
return
|
|
206
|
+
return (
|
|
207
|
+
_unread(value, type) + _no_number(value, type) + " A %s is sent as %s." % [type_string(type), shape]
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
static func _no_number(value: Variant, type: int) -> String:
|
|
212
|
+
var tag: String = type_string(type)
|
|
213
|
+
if value is Dictionary:
|
|
214
|
+
var fields: Dictionary = value
|
|
215
|
+
var key: String = not_a_number(fields, tag)
|
|
216
|
+
if not key.is_empty():
|
|
217
|
+
return " Its %s is %s, which is no number." % [key, JSON.stringify(fields[key])]
|
|
218
|
+
if value is Array and NUMBER_FIELDS.has(tag):
|
|
219
|
+
var items: Array = value
|
|
220
|
+
if not all_numbers(items):
|
|
221
|
+
return " Read by its places, each item is a number."
|
|
222
|
+
return ""
|
|
96
223
|
|
|
97
224
|
|
|
98
225
|
static func _unread(value: Variant, type: int) -> String:
|
|
@@ -16,6 +16,10 @@ var refused: String = ""
|
|
|
16
16
|
var raised: Array[String] = []
|
|
17
17
|
## The warnings reported while it ran, a line each.
|
|
18
18
|
var warned: Array[String] = []
|
|
19
|
+
## Where a script error first ended each function it ended, as "res://path:line" by function name.
|
|
20
|
+
## A function the VM ends at a script error gives back its return type's default, 0, "" or null,
|
|
21
|
+
## and its caller goes on with that as if it had been returned.
|
|
22
|
+
var ended_at: Dictionary = {}
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
func _log_message(_message: String, _error: bool) -> void:
|
|
@@ -26,8 +30,8 @@ func _log_message(_message: String, _error: bool) -> void:
|
|
|
26
30
|
# gdlint:ignore = function-arguments-number
|
|
27
31
|
func _log_error(
|
|
28
32
|
function: String,
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
file: String,
|
|
34
|
+
line: int,
|
|
31
35
|
code: String,
|
|
32
36
|
rationale: String,
|
|
33
37
|
_editor_notify: bool,
|
|
@@ -45,3 +49,5 @@ func _log_error(
|
|
|
45
49
|
warned.append(text)
|
|
46
50
|
else:
|
|
47
51
|
raised.append(text)
|
|
52
|
+
if error_type == ERROR_TYPE_SCRIPT and not ended_at.has(function):
|
|
53
|
+
ended_at[function] = "%s:%d" % [file, line]
|