class_name ConversationSemantic extends RefCounted const MAX_ID_LENGTH := 96 const MAX_TRACE_SIZE := 32 static func is_valid_id(value: Variant) -> bool: if value is not String and value is not StringName: return false var text := String(value) if text.is_empty() or text.length() > MAX_ID_LENGTH: return false for character in text: if not _is_safe_id_character(character): return false return true static func normalize_id_array(value: Variant, allow_empty: bool = true) -> Variant: if value is not Array and value is not PackedStringArray: return null var normalized: Array[StringName] = [] var seen: Dictionary = {} for item in value: if not is_valid_id(item): return null var semantic_id := StringName(item) if seen.has(semantic_id): continue seen[semantic_id] = true normalized.append(semantic_id) if not allow_empty and normalized.is_empty(): return null return normalized static func normalize_trace(value: Variant) -> Variant: var normalized: Variant = normalize_id_array(value) if normalized == null or normalized.size() > MAX_TRACE_SIZE: return null return normalized static func duplicate_entity(entity: WorldEntityRef) -> WorldEntityRef: return entity.duplicate_ref() if entity != null else null static func _is_safe_id_character(character: String) -> bool: var code := character.unicode_at(0) return ( (code >= 48 and code <= 57) or (code >= 65 and code <= 90) or (code >= 97 and code <= 122) or character in ["_", "-", ".", ":"] )