RapidGameFramework
Reusable Godot managers for data-driven small games
Loading...
Searching...
No Matches
sprite_manager.gd
Go to the documentation of this file.
1extends RefCounted
2
3
11
12var packs := {}
13var sprites := {}
14var sprite_sets := {}
15var warm_profiles := {}
16var requested_sprites := {}
17var texture_cache := {}
18var scaled_texture_cache := {}
19var animation_frame_cache := {}
20var path_texture_cache := {}
21var sheet_cell_cache := {}
22var source_image_cache := {}
23var greyscale_material_cache:ShaderMaterial = null
24
25const SpriteAnimationRect = preload("res://scripts/systems/spriteManager/sprite_animation_rect.gd")
26
27
28
29func load_pack(file_path:String) -> void:
30 var file = FileAccess.open(file_path, FileAccess.READ)
31 if file == null:
32 return
33 var data = JSON.parse_string(file.get_as_text())
34 if not data is Dictionary:
35 return
36 load_pack_data(data, file_path.get_file().get_basename())
37
38
39
40func load_pack_data(data:Dictionary, fallback_pack_id:String = "sprite_pack") -> void:
41 var pack_id = str(data.get("id", fallback_pack_id))
42 packs[pack_id] = data.duplicate(true)
43 if data.has("sprite_sets"):
44 _clear_sheet_dependent_cache()
45 for set_id in data.get("sprite_sets", {}).keys():
46 var set_definition = data["sprite_sets"][set_id].duplicate(true)
47 set_definition["id"] = str(set_id)
48 set_definition["pack_id"] = pack_id
49 sprite_sets[str(set_id)] = set_definition
50 for profile_id in data.get("warm_profiles", {}).keys():
51 var warm_profile = data["warm_profiles"][profile_id].duplicate(true)
52 warm_profile["id"] = str(profile_id)
53 warm_profile["pack_id"] = pack_id
54 warm_profiles[str(profile_id)] = warm_profile
55 for required in data.get("required_sprites", []):
56 if required is Dictionary:
57 ensure_required_sprites(required)
58 for sprite_id in data.get("sprites", {}).keys():
59 var sprite = data["sprites"][sprite_id].duplicate(true)
60 sprite["id"] = str(sprite_id)
61 sprite["pack_id"] = pack_id
62 _invalidate_sprite_cache(str(sprite_id))
63 sprites[str(sprite_id)] = sprite
64
65
66
67func manifest_with_required_sprites(data:Dictionary) -> Dictionary:
68 var next_data = data.duplicate(true)
69 var next_sprites = next_data.get("sprites", {})
70 if not next_sprites is Dictionary:
71 next_sprites = {}
72 for required in next_data.get("required_sprites", []):
73 if not required is Dictionary:
74 continue
75 for entry in required_sprite_entries(required):
76 var id = str(entry.get("id", ""))
77 if id != "" and not next_sprites.has(id):
78 next_sprites[id] = entry
79 next_data["sprites"] = next_sprites
80 return next_data
81
82
83
84func load_packs(file_paths:Array) -> void:
85 for file_path in file_paths:
86 load_pack(str(file_path))
87
88
89
90func get_sprite(sprite_id:String) -> Dictionary:
91 return sprites.get(sprite_id, {}).duplicate(true)
92
93
94
95func get_sprite_set(set_id:String) -> Dictionary:
96 return sprite_sets.get(set_id, {}).duplicate(true)
97
98
99
100func get_sprite_path(sprite_id:String, default_path:String = "") -> String:
101 var sprite = sprites.get(sprite_id, {})
102 if not sprite is Dictionary:
103 return default_path
104 return str(sprite.get("path", default_path))
105
106
107
108func get_texture(sprite_id:String):
109 if texture_cache.has(sprite_id):
110 return texture_cache[sprite_id]
111 var sprite = sprites.get(sprite_id, {})
112 if not sprite is Dictionary or sprite.is_empty():
113 return null
114 var texture = null
115 if sprite.has("animation"):
116 var frames = get_animation_frames(sprite_id)
117 texture = frames[0] if not frames.is_empty() else null
118 elif sprite.has("sheet") or sprite.has("cell"):
119 texture = _texture_from_sheet(sprite)
120 else:
121 var path = str(sprite.get("path", ""))
122 if path == "":
123 return null
124 texture = get_texture_for_path(path)
125 if texture != null:
126 texture_cache[sprite_id] = texture
127 return texture
128
129
130
131func get_scaled_texture(sprite_id:String, size:int = 32):
132 var cache_key = "%s|%d" % [sprite_id, size]
133 if scaled_texture_cache.has(cache_key):
134 return scaled_texture_cache[cache_key]
135 var texture = get_texture(sprite_id)
136 if texture == null or not texture is Texture2D:
137 return null
138 var sprite = sprites.get(sprite_id, {})
139 var path = str(sprite.get("path", "")) if sprite is Dictionary else ""
140 var image = _image_from_texture_or_path(texture, path)
141 if image == null or image.is_empty():
142 return null
143 image.resize(size, size, Image.INTERPOLATE_NEAREST)
144 var scaled = ImageTexture.create_from_image(image)
145 scaled_texture_cache[cache_key] = scaled
146 return scaled
147
148
149
150func get_texture_with_fallback(sprite_ids:Array):
151 for sprite_id in sprite_ids:
152 var texture = get_texture(str(sprite_id))
153 if texture != null:
154 return texture
155 return null
156
157
158
159func rotation_for_vector(direction:Vector2, base_direction:String = "right") -> float:
160 if direction.length_squared() <= 0.0001:
161 return 0.0
162 return direction.angle() - _angle_for_direction(base_direction)
163
164
165
166func get_texture_for_definition(definition:Dictionary, options:Dictionary = {}):
167 var sprite_key = str(options.get("sprite_key", "sprite_id"))
168 var path_key = str(options.get("path_key", "image_path"))
169 var fallback_sprite_id = str(options.get("fallback_sprite_id", ""))
170 var sprite_id = str(definition.get(sprite_key, fallback_sprite_id))
171 if sprite_id != "":
172 var texture = get_texture(sprite_id)
173 if texture != null:
174 return texture
175 var path = str(definition.get(path_key, definition.get("icon_path", definition.get("path", ""))))
176 if path != "":
177 return get_texture_for_path(path)
178 return null
179
180
181
182func get_scaled_texture_for_definition(definition:Dictionary, size:int = 32, options:Dictionary = {}):
183 var sprite_key = str(options.get("sprite_key", "sprite_id"))
184 var path_key = str(options.get("path_key", "image_path"))
185 var fallback_sprite_id = str(options.get("fallback_sprite_id", ""))
186 var sprite_id = str(definition.get(sprite_key, fallback_sprite_id))
187 if sprite_id != "":
188 var texture = get_scaled_texture(sprite_id, size)
189 if texture != null:
190 return texture
191 var path = str(definition.get(path_key, definition.get("icon_path", definition.get("path", ""))))
192 var cache_key = "definition:%s|%d" % [path, size]
193 if scaled_texture_cache.has(cache_key):
194 return scaled_texture_cache[cache_key]
195 var texture = get_texture_for_definition(definition, {"path_key": path_key})
196 if texture == null or not texture is Texture2D:
197 return null
198 var image = _image_from_texture_or_path(texture, path)
199 if image == null or image.is_empty():
200 return texture
201 image.resize(size, size, Image.INTERPOLATE_NEAREST)
202 var scaled = ImageTexture.create_from_image(image)
203 scaled_texture_cache[cache_key] = scaled
204 return scaled
205
206
207
208func get_animation_frames(sprite_id:String) -> Array:
209 if animation_frame_cache.has(sprite_id):
210 return animation_frame_cache[sprite_id].duplicate()
211 var sprite = sprites.get(sprite_id, {})
212 if not sprite is Dictionary or sprite.is_empty():
213 return []
214 if not sprite.has("animation"):
215 var texture = get_texture(sprite_id)
216 var single = [texture] if texture != null else []
217 animation_frame_cache[sprite_id] = single
218 return single.duplicate()
219 var frames := []
220 var animation = sprite.get("animation", {})
221 for cell in _animation_cells(animation):
222 var frame_definition = sprite.duplicate(true)
223 frame_definition.erase("animation")
224 frame_definition["cell"] = cell
225 var texture = _texture_from_sheet(frame_definition)
226 if texture != null:
227 frames.append(texture)
228 animation_frame_cache[sprite_id] = frames
229 return frames.duplicate()
230
231
232
233func get_animation_definition(sprite_id:String) -> Dictionary:
234 var sprite = sprites.get(sprite_id, {})
235 if not sprite is Dictionary:
236 return {}
237 var animation = sprite.get("animation", {}).duplicate(true)
238 if animation.is_empty():
239 animation["fps"] = float(sprite.get("fps", 0.0))
240 else:
241 animation["fps"] = float(animation.get("fps", sprite.get("fps", 8.0)))
242 animation["frames"] = _animation_cells(animation)
243 return animation
244
245
246
250func create_texture_node(sprite_id:String, options:Dictionary = {}) -> TextureRect:
251 var request_metadata = options.get("manifest", options)
252 record_sprite_request(sprite_id, request_metadata if request_metadata is Dictionary else {})
253 var sprite = sprites.get(sprite_id, {})
254 if not sprite is Dictionary:
255 sprite = {}
256 var frames = get_animation_frames(sprite_id)
257 if frames.is_empty():
258 return null
259 if frames.size() > 1:
260 var animation = get_animation_definition(sprite_id)
261 var node = SpriteAnimationRect.new()
262 var animation_options = options.duplicate()
263 animation_options["fps"] = float(options.get("fps", animation.get("fps", 8.0)))
264 node.configure(frames, animation_options)
265 _apply_texture_node_options(node, sprite, options)
266 return node
267 var node = TextureRect.new()
268 node.texture = frames[0]
269 node.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
270 node.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
271 node.mouse_filter = Control.MOUSE_FILTER_IGNORE
272 _apply_texture_node_options(node, sprite, options)
273 return node
274
275
276
277func ensure_required_sprites(config:Dictionary) -> Array:
278 var generated := []
279 for sprite in required_sprite_entries(config):
280 var id = str(sprite.get("id", ""))
281 if sprites.has(id):
282 continue
283 sprites[id] = sprite
284 generated.append(id)
285 return generated
286
287
288
293func record_sprite_request(sprite_id:String, metadata:Dictionary = {}) -> void:
294 sprite_id = sprite_id.strip_edges()
295 if sprite_id == "":
296 return
297 var request = requested_sprites.get(sprite_id, {})
298 request["id"] = sprite_id
299 request["required"] = bool(metadata.get("required", true))
300 request["placeholder"] = bool(metadata.get("placeholder", true))
301 for key in ["display_name", "category", "direction", "tags"]:
302 if metadata.has(key):
303 request[key] = metadata[key]
304 if not request.has("display_name"):
305 request["display_name"] = sprite_id.capitalize().replace("_", " ")
306 if not request.has("tags"):
307 request["tags"] = []
308 requested_sprites[sprite_id] = request
309
310
311
312func get_requested_sprite_entries() -> Array:
313 var entries := []
314 var ids = requested_sprites.keys()
315 ids.sort()
316 for id in ids:
317 entries.append(requested_sprites[id].duplicate(true))
318 return entries
319
320
321
322func clear_requested_sprites() -> void:
323 requested_sprites.clear()
324
325
326
327func manifest_with_requested_sprites(data:Dictionary) -> Dictionary:
328 var next_data = data.duplicate(true)
329 var next_sprites = next_data.get("sprites", {})
330 if not next_sprites is Dictionary:
331 next_sprites = {}
332 for entry in get_requested_sprite_entries():
333 var id = str(entry.get("id", ""))
334 if id == "" or next_sprites.has(id):
335 continue
336 next_sprites[id] = entry
337 next_data["sprites"] = next_sprites
338 return next_data
339
340
341
345func warm_texture_cache(sprite_ids:Array = []) -> Dictionary:
346 var ids := sprite_ids.duplicate()
347 if ids.is_empty():
348 ids = sprites.keys()
349 var warmed := {
350 "requested": ids.size(),
351 "textures": 0,
352 "animations": 0,
353 "missing": []
354 }
355 for id_value in ids:
356 var sprite_id = str(id_value)
357 var frames = get_animation_frames(sprite_id)
358 if frames.is_empty():
359 warmed["missing"].append(sprite_id)
360 continue
361 warmed["textures"] = int(warmed["textures"]) + 1
362 if frames.size() > 1:
363 warmed["animations"] = int(warmed["animations"]) + 1
364 return warmed
365
366
367func warm_texture_cache_for_profile(profile_id:String, options:Dictionary = {}) -> Dictionary:
368 var profile:Dictionary = get_warm_profile(profile_id)
369 if profile.is_empty():
370 return {
371 "id": profile_id,
372 "requested": 0,
373 "textures": 0,
374 "animations": 0,
375 "scaled_textures": 0,
376 "skipped_optional": 0,
377 "missing": [profile_id]
378 }
379 var sprites_to_warm:Array = _profile_sprite_ids(profile, options)
380 var warmed:Dictionary = warm_texture_cache(sprites_to_warm)
381 warmed["skipped_optional"] = int(profile.get("_skipped_optional", 0))
382 var scaled_count:int = 0
383 var skipped_scaled_optional:int = 0
384 for scaled_entry in _profile_scaled_entries(profile, options):
385 if _should_skip_optional_profile_entry(scaled_entry, options):
386 skipped_scaled_optional += 1
387 continue
388 var sprite_id:String = str(scaled_entry.get("id", scaled_entry.get("sprite_id", "")))
389 var size:int = max(1, int(scaled_entry.get("size", options.get("size", 32))))
390 if sprite_id.is_empty():
391 continue
392 if get_scaled_texture(sprite_id, size) != null:
393 scaled_count += 1
394 elif not warmed.get("missing", []).has(sprite_id):
395 warmed["missing"].append(sprite_id)
396 warmed["id"] = profile_id
397 warmed["scaled_textures"] = scaled_count
398 warmed["skipped_optional"] = int(warmed.get("skipped_optional", 0)) + skipped_scaled_optional
399 return warmed
400
401
402func get_warm_profile(profile_id:String) -> Dictionary:
403 return warm_profiles.get(profile_id, {}).duplicate(true)
404
405
406func get_warm_profile_ids() -> Array:
407 var ids:Array = warm_profiles.keys()
408 ids.sort()
409 return ids
410
411
412
413func clear_texture_cache() -> void:
414 texture_cache.clear()
415 scaled_texture_cache.clear()
416 animation_frame_cache.clear()
417 path_texture_cache.clear()
418 sheet_cell_cache.clear()
419 source_image_cache.clear()
420
421
422
428func clear_transient_cache(options:Dictionary = {}) -> Dictionary:
429 var preserve_source_images:bool = bool(options.get("preserve_source_images", true))
430 var preserve_path_textures:bool = bool(options.get("preserve_path_textures", false))
431 var before:Dictionary = get_cache_stats()
432 texture_cache.clear()
433 scaled_texture_cache.clear()
434 animation_frame_cache.clear()
435 sheet_cell_cache.clear()
436 if not preserve_path_textures:
437 path_texture_cache.clear()
438 if not preserve_source_images:
439 source_image_cache.clear()
440 var after:Dictionary = get_cache_stats()
441 return _cache_clear_result(before, after)
442
443
444
448func trim_texture_cache(keep_sprite_ids:Array = [], options:Dictionary = {}) -> Dictionary:
449 var preserve_source_images:bool = bool(options.get("preserve_source_images", true))
450 var preserve_path_textures:bool = bool(options.get("preserve_path_textures", false))
451 var clear_sheet_cells:bool = bool(options.get("clear_sheet_cells", true))
452 var keep_ids:Dictionary = {}
453 for id_value in keep_sprite_ids:
454 var sprite_id:String = str(id_value)
455 if sprite_id != "":
456 keep_ids[sprite_id] = true
457 var before:Dictionary = get_cache_stats()
458 for sprite_id in texture_cache.keys().duplicate():
459 if not keep_ids.has(str(sprite_id)):
460 texture_cache.erase(sprite_id)
461 for sprite_id in animation_frame_cache.keys().duplicate():
462 if not keep_ids.has(str(sprite_id)):
463 animation_frame_cache.erase(sprite_id)
464 for cache_key in scaled_texture_cache.keys().duplicate():
465 var owner_id:String = str(cache_key).split("|")[0]
466 if owner_id.begins_with("definition:") or not keep_ids.has(owner_id):
467 scaled_texture_cache.erase(cache_key)
468 if clear_sheet_cells:
469 sheet_cell_cache.clear()
470 if not preserve_path_textures:
471 path_texture_cache.clear()
472 if not preserve_source_images:
473 source_image_cache.clear()
474 var after:Dictionary = get_cache_stats()
475 return _cache_clear_result(before, after)
476
477
478
479func _invalidate_sprite_cache(sprite_id:String) -> void:
480 texture_cache.erase(sprite_id)
481 animation_frame_cache.erase(sprite_id)
482 var existing_sprite = sprites.get(sprite_id, {})
483 if existing_sprite is Dictionary:
484 var existing_path = str(existing_sprite.get("path", ""))
485 if existing_path != "":
486 source_image_cache.erase(existing_path)
487 for key in scaled_texture_cache.keys():
488 if str(key).begins_with("%s|" % sprite_id):
489 scaled_texture_cache.erase(key)
490
491
492
493func _clear_sheet_dependent_cache() -> void:
494 sheet_cell_cache.clear()
495 animation_frame_cache.clear()
496 for id in sprites.keys():
497 var sprite = sprites[id]
498 if sprite is Dictionary and (sprite.has("sheet") or sprite.has("cell") or sprite.has("animation") or sprite.has("set")):
499 texture_cache.erase(str(id))
500
501
502
503func get_cache_stats() -> Dictionary:
504 return {
505 "textures": texture_cache.size(),
506 "scaled_textures": scaled_texture_cache.size(),
507 "animations": animation_frame_cache.size(),
508 "path_textures": path_texture_cache.size(),
509 "sheet_cells": sheet_cell_cache.size(),
510 "source_images": source_image_cache.size()
511 }
512
513
514func _cache_clear_result(before:Dictionary, after:Dictionary) -> Dictionary:
515 var cleared := {}
516 for key in before.keys():
517 cleared[key] = max(0, int(before.get(key, 0)) - int(after.get(key, 0)))
518 return {
519 "before": before,
520 "after": after,
521 "cleared": cleared
522 }
523
524
525
526func required_sprite_entries(config:Dictionary) -> Array:
527 var prefix = str(config.get("prefix", config.get("thing_id", "")))
528 var states = config.get("states", [])
529 var entries := []
530 if prefix == "" or not states is Array:
531 return entries
532 var directions = config.get("directions", [])
533 var directional_states = config.get("directional_states", states)
534 var tags = config.get("tags", [])
535 var category = str(config.get("category", ""))
536 for state in states:
537 var state_id = str(state)
538 var state_directions = directions if _state_is_directional(state_id, directional_states) else []
539 if state_directions is Array and not state_directions.is_empty():
540 for direction in state_directions:
541 entries.append(_required_sprite_entry(config, prefix, state_id, str(direction), tags, category))
542 else:
543 entries.append(_required_sprite_entry(config, prefix, state_id, "", tags, category))
544 return entries
545
546
547
548func get_missing_assignments(required_ids:Array = []) -> Array:
549 var ids := required_ids.duplicate()
550 if ids.is_empty():
551 for id in sprites.keys():
552 if bool(sprites[id].get("required", false)):
553 ids.append(str(id))
554 ids.sort()
555 var missing := []
556 for id in ids:
557 var sprite = get_sprite(str(id))
558 if sprite.is_empty():
559 missing.append({"id": str(id), "reason": "missing_definition"})
560 elif bool(sprite.get("placeholder", false)):
561 missing.append({"id": str(id), "reason": "placeholder"})
562 elif not _has_texture_source(sprite):
563 missing.append({"id": str(id), "reason": "missing_source"})
564 return missing
565
566
567
568func find_by_tags(tags:Array) -> Array:
569 var matches := []
570 for sprite in sprites.values():
571 var sprite_tags = sprite.get("tags", [])
572 var has_all := true
573 for tag in tags:
574 if not sprite_tags.has(str(tag)):
575 has_all = false
576 break
577 if has_all:
578 matches.append(sprite.duplicate(true))
579 return matches
580
581
582
583func get_all_sprites() -> Array:
584 var list := []
585 for sprite in sprites.values():
586 list.append(sprite.duplicate(true))
587 return list
588
589
590
591func get_all_sprite_sets() -> Array:
592 var list := []
593 for set_definition in sprite_sets.values():
594 list.append(set_definition.duplicate(true))
595 return list
596
597
598
599func reset() -> void:
600 packs.clear()
601 sprites.clear()
602 sprite_sets.clear()
603 warm_profiles.clear()
604 requested_sprites.clear()
605 clear_texture_cache()
606
607
608func _has_texture_source(sprite:Dictionary) -> bool:
609 return str(sprite.get("path", "")) != "" or sprite.has("sheet") or sprite.has("cell") or sprite.has("animation")
610
611
612func _profile_sprite_ids(profile:Dictionary, options:Dictionary = {}) -> Array:
613 var ids:Array = []
614 var skipped_optional:int = 0
615 for key in ["sprites", "sprite_ids", "animations"]:
616 var values = profile.get(key, [])
617 if values is Array:
618 for value in values:
619 if value is Dictionary:
620 if _should_skip_optional_profile_entry(value, options):
621 skipped_optional += 1
622 continue
623 var sprite_id:String = str(value.get("id", value.get("sprite_id", "")))
624 if sprite_id != "" and not ids.has(sprite_id):
625 ids.append(sprite_id)
626 else:
627 var sprite_id:String = str(value)
628 if sprite_id != "" and not ids.has(sprite_id):
629 ids.append(sprite_id)
630 if skipped_optional > 0:
631 profile["_skipped_optional"] = skipped_optional
632 return ids
633
634
635func _should_skip_optional_profile_entry(entry:Dictionary, options:Dictionary) -> bool:
636 return bool(options.get("skip_optional", false)) and bool(entry.get("optional", false))
637
638
639func _profile_scaled_entries(profile:Dictionary, options:Dictionary = {}) -> Array:
640 var entries:Array = []
641 var scaled_values = profile.get("scaled", profile.get("scaled_textures", []))
642 if scaled_values is Array:
643 for value in scaled_values:
644 if value is Dictionary:
645 entries.append(value.duplicate(true))
646 else:
647 entries.append({"id": str(value), "size": int(options.get("size", 32))})
648 var scaled_sizes = profile.get("scaled_sizes", {})
649 if scaled_sizes is Dictionary:
650 for sprite_id in scaled_sizes.keys():
651 entries.append({"id": str(sprite_id), "size": int(scaled_sizes[sprite_id])})
652 return entries
653
654
655func _apply_texture_node_options(node:TextureRect, sprite:Dictionary, options:Dictionary) -> void:
656 node.flip_h = bool(options.get("mirror_h", sprite.get("mirror_h", sprite.get("flip_h", false))))
657 node.flip_v = bool(options.get("mirror_v", sprite.get("mirror_v", sprite.get("flip_v", false))))
658 if bool(options.get("greyscale", options.get("grayscale", sprite.get("greyscale", sprite.get("grayscale", false))))):
659 node.material = _greyscale_material()
660
661
662func _greyscale_material() -> ShaderMaterial:
663 if greyscale_material_cache != null:
664 return greyscale_material_cache
665 var shader = Shader.new()
666 shader.code = "shader_type canvas_item;\nvoid fragment() {\n\tvec4 color = texture(TEXTURE, UV) * COLOR;\n\tfloat grey = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n\tCOLOR = vec4(vec3(grey), color.a);\n}\n"
667 var material = ShaderMaterial.new()
668 material.shader = shader
669 greyscale_material_cache = material
670 return greyscale_material_cache
671
672
673func _state_is_directional(state_id:String, directional_states) -> bool:
674 if directional_states is Array:
675 return directional_states.has(state_id) or directional_states.has("*")
676 return bool(directional_states)
677
678
679func _angle_for_direction(direction:String) -> float:
680 match direction.to_lower():
681 "right":
682 return 0.0
683 "down":
684 return PI * 0.5
685 "left":
686 return PI
687 "up":
688 return -PI * 0.5
689 _:
690 return 0.0
691
692
693func _required_sprite_entry(config:Dictionary, prefix:String, state_id:String, direction:String, tags, category:String) -> Dictionary:
694 var id = "%s_%s" % [prefix, state_id]
695 if direction != "":
696 id = "%s_%s" % [id, direction]
697 var display = str(config.get("display_name_prefix", prefix.capitalize().replace("_", " ")))
698 display += " " + state_id.capitalize().replace("_", " ")
699 if direction != "":
700 display += " " + direction.capitalize()
701 var entry := {
702 "id": id,
703 "display_name": display,
704 "placeholder": true,
705 "required": true,
706 "tags": tags.duplicate(true) if tags is Array else []
707 }
708 if category != "":
709 entry["category"] = category
710 if direction != "":
711 entry["direction"] = direction
712 return entry
713
714
715func _texture_from_sheet(sprite:Dictionary):
716 var sheet = _sheet_config(sprite)
717 var path = str(sheet.get("path", ""))
718 if path == "":
719 return null
720 var cell = _cell_config(sprite)
721 if cell.is_empty():
722 return null
723 var cell_width = int(sheet.get("cell_width", sheet.get("width", 64)))
724 var cell_height = int(sheet.get("cell_height", sheet.get("height", 64)))
725 var row = max(0, int(cell.get("row", cell.get("y", 0))))
726 var column = max(0, int(cell.get("column", cell.get("x", 0))))
727 var cache_key = "%s|%d|%d|%d|%d" % [path, cell_width, cell_height, row, column]
728 if sheet_cell_cache.has(cache_key):
729 return sheet_cell_cache[cache_key]
730 var source_image = _get_source_image(path)
731 if source_image == null or source_image.is_empty():
732 var source_texture = get_texture_for_path(path)
733 if source_texture == null or not source_texture is Texture2D:
734 return null
735 source_image = _image_from_texture_or_path(source_texture, path)
736 if source_image == null or source_image.is_empty():
737 return null
738 var region = Rect2i(column * cell_width, row * cell_height, cell_width, cell_height)
739 if region.position.x + region.size.x > source_image.get_width() or region.position.y + region.size.y > source_image.get_height():
740 return null
741 var image = Image.create(cell_width, cell_height, false, source_image.get_format())
742 image.blit_rect(source_image, region, Vector2i.ZERO)
743 var texture = ImageTexture.create_from_image(image)
744 sheet_cell_cache[cache_key] = texture
745 return texture
746
747
748func get_texture_for_path(path:String):
749 if path == "":
750 return null
751 if path_texture_cache.has(path):
752 return path_texture_cache[path]
753 var texture = null
754 if _should_use_resource_loader(path):
755 texture = load(path)
756 else:
757 var image = _get_source_image(path)
758 if image != null and not image.is_empty():
759 texture = ImageTexture.create_from_image(image)
760 elif ResourceLoader.exists(path):
761 texture = load(path)
762 if texture != null:
763 path_texture_cache[path] = texture
764 return texture
765
766
767func _get_source_image(path:String):
768 if path == "":
769 return null
770 if source_image_cache.has(path):
771 return source_image_cache[path]
772 var image = _load_image(path)
773 if image != null and not image.is_empty():
774 source_image_cache[path] = image
775 return image
776
777
778func _image_from_texture_or_path(texture:Texture2D, path:String = ""):
779 if path != "" and source_image_cache.has(path):
780 return source_image_cache[path].duplicate()
781 if texture == null:
782 return null
783 var image = texture.get_image()
784 if image != null and not image.is_empty() and path != "":
785 source_image_cache[path] = image
786 return image.duplicate() if image != null else null
787
788
789func _sheet_config(sprite:Dictionary) -> Dictionary:
790 var sheet = sprite.get("sheet", {})
791 if sheet is Dictionary and not sheet.is_empty():
792 if sheet.has("set"):
793 var set_definition = get_sprite_set(str(sheet.get("set", "")))
794 for key in sheet.keys():
795 if key != "set":
796 set_definition[key] = sheet[key]
797 return set_definition
798 return sheet.duplicate(true)
799 if sprite.has("set"):
800 return get_sprite_set(str(sprite.get("set", "")))
801 return {}
802
803
804func _cell_config(sprite:Dictionary) -> Dictionary:
805 var cell = sprite.get("cell", {})
806 if cell is Dictionary:
807 return cell.duplicate(true)
808 if cell is Array and cell.size() >= 2:
809 return {"row": int(cell[0]), "column": int(cell[1])}
810 var sheet = sprite.get("sheet", {})
811 if sheet is Dictionary and (sheet.has("row") or sheet.has("column")):
812 return {"row": int(sheet.get("row", 0)), "column": int(sheet.get("column", 0))}
813 return {}
814
815
816func _animation_cells(animation:Dictionary) -> Array:
817 var cells := []
818 if animation.has("frames") and animation["frames"] is Array:
819 for frame in animation["frames"]:
820 if frame is Dictionary:
821 cells.append(frame.duplicate(true))
822 elif frame is Array and frame.size() >= 2:
823 cells.append({"row": int(frame[0]), "column": int(frame[1])})
824 if cells.is_empty() and animation.has("range"):
825 var range_definition = animation.get("range", {})
826 if range_definition is Dictionary:
827 var row = int(range_definition.get("row", 0))
828 var from_column = int(range_definition.get("from_column", range_definition.get("start_column", 0)))
829 var to_column = int(range_definition.get("to_column", range_definition.get("end_column", from_column)))
830 var step = 1 if to_column >= from_column else -1
831 var column = from_column
832 while true:
833 cells.append({"row": row, "column": column})
834 if column == to_column:
835 break
836 column += step
837 return cells
838
839
840func _load_image(path:String):
841 if path == "":
842 return null
843 if ResourceLoader.exists(path):
844 var resource = ResourceLoader.load(path)
845 if resource is Texture2D:
846 var texture_image = resource.get_image()
847 if texture_image != null and not texture_image.is_empty():
848 return texture_image
849 if not FileAccess.file_exists(path):
850 return null
851 var image = Image.load_from_file(path)
852 if image == null or image.is_empty():
853 return null
854 return image
855
856
857func _should_use_resource_loader(path:String) -> bool:
858 if path == "" or not ResourceLoader.exists(path):
859 return false
860 if not OS.has_feature("editor"):
861 return true
862 return _has_import_cache(path)
863
864
865func _has_import_cache(path:String) -> bool:
866 var import_file_path = "%s.import" % path
867 if not FileAccess.file_exists(import_file_path):
868 return false
869 var config = ConfigFile.new()
870 if config.load(import_file_path) != OK:
871 return false
872 var dest_files = config.get_value("deps", "dest_files", PackedStringArray())
873 for dest_file in dest_files:
874 if FileAccess.file_exists(str(dest_file)):
875 return true
876 return false
int
Load and return a scaled Texture2D for UI controls that need smaller icons.