WIP checkpoint: Implement avatar loader and customization UI in Godot
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
## AvatarCatalog - Asset catalog management.
|
||||
##
|
||||
## Manages the mapping of asset IDs to their metadata and resource paths.
|
||||
## Loads the catalog manifest from disk and provides lookup by ID and slot.
|
||||
##
|
||||
## The catalog is loaded from `res://assets/avatars/catalog.json` which follows
|
||||
## the format specified in avatar.md §7.2.
|
||||
|
||||
class_name AvatarCatalog
|
||||
extends RefCounted
|
||||
|
||||
|
||||
## Emitted when the catalog is loaded successfully.
|
||||
signal catalog_loaded
|
||||
|
||||
## Emitted when the catalog fails to load.
|
||||
signal catalog_load_failed(error_message: String)
|
||||
|
||||
## Catalog manifest structure:
|
||||
## { "assets": { "id": { "type", "slot", "path", "version", ... } } }
|
||||
var _catalog: Dictionary = {}
|
||||
var _loaded: bool = false
|
||||
var _catalog_path: String = "res://assets/avatars/catalog.json"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## 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:
|
||||
var err = FileAccess.get_open_error()
|
||||
catalog_load_failed.emit("Cannot open catalog: %s (error %d)" % [path, err])
|
||||
return err
|
||||
|
||||
var json_text: String = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json: JSONParseResult = JSON.parse_string(json_text)
|
||||
if json == null:
|
||||
catalog_load_failed.emit("Invalid JSON in catalog: %s" % path)
|
||||
return ERR_PARSE_ERROR
|
||||
|
||||
if not json is Dictionary or not json.has("assets"):
|
||||
catalog_load_failed.emit("Catalog missing 'assets' key")
|
||||
return ERR_INVALID_DATA
|
||||
|
||||
_catalog = json["assets"]
|
||||
_loaded = true
|
||||
catalog_loaded.emit()
|
||||
return OK
|
||||
|
||||
|
||||
## Get the metadata for an asset by its ID.
|
||||
## Returns a Dictionary with keys: type, slot, path, version, etc.
|
||||
## Returns null if the asset ID is not found.
|
||||
func get_asset(id: String) -> Dictionary:
|
||||
if not _loaded:
|
||||
return {}
|
||||
return _catalog.get(id, {})
|
||||
|
||||
|
||||
## Check if an asset ID exists in the catalog.
|
||||
func has_asset(id: String) -> bool:
|
||||
if not _loaded:
|
||||
return false
|
||||
return _catalog.has(id)
|
||||
|
||||
|
||||
## Get all assets that belong to a given slot.
|
||||
## Returns an array of asset ID strings.
|
||||
func get_assets_by_slot(slot: String) -> Array[String]:
|
||||
if not _loaded:
|
||||
return []
|
||||
var result: Array[String] = []
|
||||
for id in _catalog:
|
||||
var asset: Dictionary = _catalog[id]
|
||||
if asset.get("slot") == slot:
|
||||
result.append(id)
|
||||
return result
|
||||
|
||||
|
||||
## Get all asset IDs in the catalog.
|
||||
func get_all_asset_ids() -> Array[String]:
|
||||
if not _loaded:
|
||||
return []
|
||||
return _catalog.keys()
|
||||
|
||||
|
||||
## Validate a loadout against the catalog.
|
||||
## Returns a Dictionary with "valid" (bool) and "errors" (Array[String]).
|
||||
func validate_loadout(loadout: AvatarLoadout) -> Dictionary:
|
||||
var errors: Array[String] = []
|
||||
|
||||
# Check base_body
|
||||
if loadout.base_body.is_empty():
|
||||
errors.append("Loadout missing base_body")
|
||||
elif not has_asset(loadout.base_body):
|
||||
errors.append("Unknown base_body asset: %s" % loadout.base_body)
|
||||
else:
|
||||
var base: Dictionary = get_asset(loadout.base_body)
|
||||
if base.get("type") != "base_body":
|
||||
errors.append("base_body asset '%s' is not type 'base_body'" % loadout.base_body)
|
||||
|
||||
# Check each slot
|
||||
for slot_id in loadout.slots:
|
||||
var slot_value = loadout.slots[slot_id]
|
||||
|
||||
if slot_value == null:
|
||||
continue
|
||||
|
||||
if slot_id == "accessory":
|
||||
# Accessory is an array
|
||||
if not slot_value is Array:
|
||||
errors.append("Slot '%s' must be an array" % slot_id)
|
||||
continue
|
||||
for item_id in slot_value:
|
||||
if not has_asset(item_id):
|
||||
errors.append("Unknown accessory asset: %s" % item_id)
|
||||
else:
|
||||
var asset: Dictionary = get_asset(item_id)
|
||||
if asset.get("slot") != slot_id:
|
||||
errors.append("Accessory '%s' slot '%s' does not match slot '%s'" % [
|
||||
item_id, asset.get("slot"), slot_id
|
||||
])
|
||||
else:
|
||||
# Exclusive slot - single value
|
||||
if not slot_value is String:
|
||||
errors.append("Slot '%s' must be a string or null" % slot_id)
|
||||
continue
|
||||
if not has_asset(slot_value):
|
||||
errors.append("Unknown asset in slot '%s': %s" % [slot_id, slot_value])
|
||||
else:
|
||||
var asset: Dictionary = get_asset(slot_value)
|
||||
if asset.get("slot") != slot_id:
|
||||
errors.append("Asset '%s' slot '%s' does not match slot '%s'" % [
|
||||
slot_value, asset.get("slot"), slot_id
|
||||
])
|
||||
|
||||
return {
|
||||
"valid": errors.is_empty(),
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
|
||||
## Compute a hash of the loadout for change detection.
|
||||
## Returns a uint32 hash.
|
||||
static func compute_loadout_hash(loadout: AvatarLoadout) -> uint32:
|
||||
var hasher = HashingContext.new()
|
||||
hasher.start(HashingContext.HASH_SHA256)
|
||||
|
||||
hasher.update(loadout.base_body.to_utf8_buffer())
|
||||
for slot_id in loadout.slots:
|
||||
var slot_value = loadout.slots[slot_id]
|
||||
hasher.update(slot_id.to_utf8_buffer())
|
||||
if slot_value is Array:
|
||||
for item in slot_value:
|
||||
hasher.update(item.to_utf8_buffer())
|
||||
elif slot_value is String:
|
||||
hasher.update(slot_value.to_utf8_buffer())
|
||||
|
||||
var hash_bytes = hasher.finish()
|
||||
return hash_bytes[0] | (hash_bytes[1] << 8) | (hash_bytes[2] << 16) | (hash_bytes[3] << 24)
|
||||
|
||||
|
||||
## Get the catalog path.
|
||||
func get_catalog_path() -> String:
|
||||
return _catalog_path
|
||||
Reference in New Issue
Block a user