class_name SpawnCatalog extends RefCounted ## Catalog of spawnable object templates for ScriptBehavior's ## `spawn_from_catalog` verb. ## ## Mirrors the loading convention of [AvatarCatalog] ## (assets/avatars/catalog.json / scripts/avatar/avatar_catalog.gd) -- ## a data-driven JSON manifest, loaded once, looked up by id. ## ## Crucially, a ScriptBehavior rule only ever supplies a `catalog_id` ## *key* -- never a resource path. The mapping from key to actual asset ## path lives entirely in this trusted, code-shipped JSON file. A ## malicious/malformed catalog_id in UGC data just fails to resolve ## ([method resolve] returns an empty Dictionary); there's no way for the ## data to point anywhere else. var _catalog: Dictionary = {} var _loaded: bool = false var _catalog_path: String = "res://assets/scripting/spawn_catalog.json" ## Load the catalog from disk. Call this before any lookups. func load_catalog(path: String = _catalog_path) -> Error: _catalog_path = path _catalog = {} var file := FileAccess.open(path, FileAccess.READ) if file == null: return FileAccess.get_open_error() var json_text: String = file.get_as_text() file.close() var result: Variant = JSON.parse_string(json_text) if result == null or not (result is Dictionary) or not (result as Dictionary).has("templates"): return ERR_INVALID_DATA _catalog = (result as Dictionary)["templates"] _loaded = true return OK func is_loaded() -> bool: return _loaded ## Resolve a catalog_id to its template Dictionary ({} if unknown/unloaded). func resolve(catalog_id: String) -> Dictionary: if not _loaded: return {} return _catalog.get(catalog_id, {}) func has_template(catalog_id: String) -> bool: return _loaded and _catalog.has(catalog_id)