class_name PropLibrary extends RefCounted const PROP_UTILS := preload("res://scripts/prop_utils.gd") const PROP_BLOCK := preload("res://scripts/prop_block.gd") const PROP_FILE := preload("res://scripts/prop_file.gd") const PROP_VISUALS := preload("res://scripts/prop_visuals.gd") static var _templates: Array[Dictionary] = [] static func get_entries() -> Array[Dictionary]: if _templates.is_empty(): _build_templates() return _templates static func get_ids() -> Array[String]: var ids: Array[String] = [] for t: Dictionary in get_entries(): ids.append(String(t["id"])) return ids static func get_entry(id: String) -> Dictionary: for t: Dictionary in get_entries(): if String(t["id"]) == id: return t return make_custom_entry(id) if is_custom_path(id) else {} static func scan(dir_path: String = PROP_FILE.DEFAULT_DIR) -> Array[Dictionary]: var entries: Array[Dictionary] = get_entries().duplicate(true) if not DirAccess.dir_exists_absolute(dir_path): push_warning("PropLibrary: prop directory '%s' does not exist." % dir_path) return entries var directory: DirAccess = DirAccess.open(dir_path) if directory == null: return entries var paths: Array[String] = [] directory.list_dir_begin() var file_name: String = directory.get_next() while not file_name.is_empty(): if not directory.current_is_dir() and file_name.to_lower().ends_with(PROP_FILE.FILE_EXT): paths.append(dir_path.path_join(file_name)) file_name = directory.get_next() directory.list_dir_end() paths.sort_custom(func(a: String, b: String) -> bool: return a.nocasecmp_to(b) < 0) for path: String in paths: var entry := make_custom_entry(path) if not entry.is_empty(): entries.append(entry) entries.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: if bool(a.get("is_custom", false)) != bool(b.get("is_custom", false)): return not bool(a.get("is_custom", false)) var an := String(a.get("name", "")).to_lower(); var bn := String(b.get("name", "")).to_lower() return an < bn if an != bn else String(a.get("path", "")).nocasecmp_to(String(b.get("path", ""))) < 0) return entries static func make_custom_entry(path: String) -> Dictionary: var normalized_path: String = canonical_path(path) if not is_custom_path(normalized_path): return {} var raw: Dictionary = PROP_FILE.load(normalized_path) if raw.is_empty(): push_warning("PropLibrary: could not load custom prop '%s'." % normalized_path) return {} var parsed: Dictionary = PROP_FILE.from_file_dict(raw) if parsed.is_empty(): push_warning("PropLibrary: could not load custom prop '%s'." % normalized_path) return {} var shapes: Array[Dictionary] = parsed.get("shapes", []) var hull: PackedVector2Array = PROP_VISUALS.collision_hull(shapes) if shapes.is_empty() or hull.size() < 3: push_warning("PropLibrary: custom prop '%s' has no valid closed collision geometry." % normalized_path) return {} var name := String(parsed.get("prop_name", "")).strip_edges() if name.is_empty(): name = normalized_path.get_file().get_basename() var preset: int = int(parsed.get("material_preset", PROP_BLOCK.MaterialPreset.NONE)) return {"id": normalized_path, "path": normalized_path, "name": name, "material_preset": preset, "material_label": PROP_FILE.preset_to_string(preset).capitalize(), "is_custom": true, "payload": PROP_FILE.flatten_to_payload(shapes), "shapes": shapes, "collision_hull": hull} static func is_custom_path(path: String) -> bool: return path.to_lower().ends_with(PROP_FILE.FILE_EXT) and (path.begins_with("res://") or path.begins_with("user://") or path.is_absolute_path()) static func canonical_path(path: String) -> String: return path.replace("\\", "/").simplify_path() static func get_default_id() -> String: return "crate" static func _build_templates() -> void: _templates = [ _make("crate", "Crate", PROP_BLOCK.MaterialPreset.WOOD, "Wood", PROP_UTILS.create_box()), _make("ball", "Ball", PROP_BLOCK.MaterialPreset.RUBBER, "Rubber", PROP_UTILS.create_ball()), _make("plank", "Plank", PROP_BLOCK.MaterialPreset.METAL, "Metal", PROP_UTILS.create_plank()), _make("triangle", "Triangle", PROP_BLOCK.MaterialPreset.CARDBOARD, "Cardboard", PROP_UTILS.create_triangle()), ] static func _make(id: String, name: String, preset: int, label: String, payload: Dictionary) -> Dictionary: return { "id": id, "name": name, "material_preset": preset, "material_label": label, "payload": payload, "is_custom": false, "path": "", "shapes": [], "collision_hull": PackedVector2Array(), }