//! Native wearable and attachment state for `AppearanceManager`. use crate::client_core::ClientWeakHandle; use crate::packet_catalog::PacketType; use crate::packets::{ AgentCachedTexturePacket, AgentCachedTexturePacketWearableDataBlock, AgentCachedTextureResponsePacket, AgentIsNowWearingPacket, AgentIsNowWearingPacketWearableDataBlock, AgentSetAppearancePacket, AgentSetAppearancePacketVisualParamBlock, AgentSetAppearancePacketWearableDataBlock, AgentWearablesRequestPacket, AgentWearablesUpdatePacket, DetachAttachmentIntoInvPacket, RebakeAvatarTexturesPacket, RezSingleAttachmentFromInvPacket, }; use crate::{ AgentCachedBakesReplyEventArgs, AgentWearablesReplyEventArgs, AppearanceManagerTextureData, AppearanceManagerWearableData, AvatarTextureIndex, BakeType, GridClient, InventoryAttachment, InventoryBase, InventoryFolder, InventoryItem, InventoryObject, InventoryObjectClass, InventorySortOrder, InventoryWearable, Permissions, PrimitiveTextureEntry, VisualParams, }; use futures_util::{StreamExt, stream::FuturesUnordered}; use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser}; use libremetaverse_types::compat::{ CancellationToken, CancellationTokenSource, EventHandler, Subscription, }; use libremetaverse_types::{ AssetType, AttachmentPoint, Color4, Error, FolderType, InventoryType, MultiValueDictionary, UUID, Vector3, WearableType, }; use std::collections::{HashMap, HashSet, VecDeque}; use std::fmt; use std::hash::{Hash, Hasher}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; pub(crate) const ATTACHMENT_ADD: u8 = 0x80; const MAX_WEARABLE_LAYERS: usize = 60; const MAX_FOLDER_TRAVERSAL: usize = 256; const MAX_COF_ENTRIES: usize = 512; fn mutex(lock: &Mutex) -> std::sync::MutexGuard<'_, T> { lock.lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn read(lock: &RwLock) -> std::sync::RwLockReadGuard<'_, T> { lock.read() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn write(lock: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { lock.write() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn check_cancelled(token: Option<&CancellationToken>) -> Result<(), Error> { token.map_or(Ok(()), CancellationToken::throw_if_cancellation_requested) } struct EventRegistry { next_id: AtomicU64, handlers: Arc>>>, } impl Default for EventRegistry { fn default() -> Self { Self { next_id: AtomicU64::new(1), handlers: Arc::new(Mutex::new(HashMap::new())), } } } impl EventRegistry { fn subscribe(&self, handler: EventHandler) -> Subscription { let id = self.next_id.fetch_add(1, Ordering::Relaxed); mutex(&self.handlers).insert(id, handler); let handlers = Arc::downgrade(&self.handlers); Subscription::new(move || { if let Some(handlers) = handlers.upgrade() { mutex(&handlers).remove(&id); } }) } fn emit_with(&self, mut event: impl FnMut() -> T) { let handlers: Vec<_> = mutex(&self.handlers).values().cloned().collect(); for handler in handlers { handler(event()); } } } #[derive(Clone, Debug, Eq, PartialEq)] struct WearableRecord { item_id: UUID, asset_id: UUID, asset_type: AssetType, wearable_type: WearableType, } impl WearableRecord { fn from_item(item: &InventoryItem) -> Option { if !matches!(item.asset_type(), AssetType::Bodypart | AssetType::Clothing) { return None; } let wearable = InventoryWearable { base: item.clone() }; Some(Self { item_id: item.base.uuid(), asset_id: item.asset_uuid(), asset_type: item.asset_type(), wearable_type: wearable.wearable_type(), }) } fn public(&self) -> AppearanceManagerWearableData { AppearanceManagerWearableData { asset: None, asset_id: self.asset_id, asset_type: self.asset_type, item_id: self.item_id, wearable_type: self.wearable_type, } } } #[derive(Clone, Debug, Eq, PartialEq)] struct AttachmentRecord { item: InventoryItem, point: AttachmentPoint, } #[derive(Default)] struct AppearanceEvents { agent_wearables_reply: EventRegistry, appearance_set: EventRegistry, cached_bakes_reply: EventRegistry, rebake_avatar_requested: EventRegistry, } pub(crate) struct AppearanceManagerInner { client: ClientWeakHandle, wearables: RwLock>, attachments: RwLock>, last_cof_version: AtomicI32, busy: AtomicBool, disposed: AtomicBool, texture_provider: RwLock>, texture_slots: RwLock>, wearable_assets: RwLock>, visual_parameters: RwLock>, server_visual_parameters: AtomicBool, cache_serial: AtomicI32, cache_reply_generation: AtomicU64, cache_notify: tokio::sync::Notify, wearables_reply_generation: AtomicU64, wearables_notify: tokio::sync::Notify, appearance_serial: AtomicU64, appearance_cancel: Mutex>, appearance_gate: Arc, events: AppearanceEvents, network_subscriptions: Mutex>, } /// Shared native manager. Public texture fields remain on the compatibility /// wrapper; wearable and attachment state is shared by every client accessor. pub struct AppearanceManager { pub my_textures: PrimitiveTextureEntry, pub my_visual_parameters: Vec, inner: Arc, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct AppearanceSetEventArgs { success: bool, } impl AppearanceSetEventArgs { pub(crate) const fn native_new(success: bool) -> Self { Self { success } } #[allow(clippy::trivially_copy_pass_by_ref)] // Generated property access uses `&self`. pub(crate) const fn native_success(&self) -> bool { self.success } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct RebakeAvatarTexturesEventArgs { texture_id: UUID, } impl RebakeAvatarTexturesEventArgs { pub(crate) const fn native_new(texture_id: UUID) -> Self { Self { texture_id } } pub(crate) const fn native_texture_id(&self) -> UUID { self.texture_id } } impl Clone for AppearanceManager { fn clone(&self) -> Self { Self::native_from_inner(Arc::clone(&self.inner)) } } impl fmt::Debug for AppearanceManager { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("AppearanceManager") .field("wearables", &read(&self.inner.wearables).len()) .field("attachments", &read(&self.inner.attachments).len()) .field("busy", &self.native_manager_busy()) .finish_non_exhaustive() } } struct BusyGuard<'a>(&'a AtomicBool); impl Drop for BusyGuard<'_> { fn drop(&mut self) { self.0.store(false, Ordering::Release); } } fn handle_appearance_packet( inner: &Arc, packet_type: PacketType, data: Vec, ) { match packet_type { PacketType::AgentWearablesUpdate => handle_wearables_update(inner, data), PacketType::AgentCachedTextureResponse => handle_cached_bakes(inner, data), PacketType::RebakeAvatarTextures => handle_rebake_request(inner, data), _ => {} } } fn handle_wearables_update(inner: &Arc, data: Vec) { let mut offset = 0; let Ok(packet) = AgentWearablesUpdatePacket::new_with_bytes_int32(data, &mut offset) else { return; }; let server_baking = inner .client .upgrade() .and_then(|client| client.native_network().ok()) .and_then(|network| network.native_current_sim()) .is_some_and(|simulator| { simulator.protocols.0 & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0 }); let mut received = Vec::new(); if !server_baking { for block in packet.wearable_data { let Some(wearable_type) = wearable_type_from_u8(block.wearable_type) else { continue; }; if wearable_type == WearableType::Invalid || block.item_id == UUID::zero() { continue; } received.push(WearableRecord { item_id: block.item_id, asset_id: block.asset_id, asset_type: wearable_type_to_asset_type(wearable_type), wearable_type, }); } *write(&inner.wearables) = received; inner .server_visual_parameters .store(false, Ordering::Release); } inner .wearables_reply_generation .fetch_add(1, Ordering::AcqRel); inner.wearables_notify.notify_one(); inner .events .agent_wearables_reply .emit_with(|| AgentWearablesReplyEventArgs); if !server_baking && !read(&inner.wearables).is_empty() && !inner.busy.load(Ordering::Acquire) { spawn_appearance_request(inner, false, "appearance-wearables"); } } fn handle_cached_bakes(inner: &Arc, data: Vec) { let mut offset = 0; let Ok(packet) = AgentCachedTextureResponsePacket::new_with_bytes_int32(data, &mut offset) else { return; }; let mut slots = write(&inner.texture_slots); for bake in packet.wearable_data { if let Some(target) = bake_index_to_texture_index() .get(usize::from(bake.texture_index)) .copied() && let Some(slot) = slots.get_mut(usize::from(target)) && bake.texture_id != UUID::zero() { slot.texture_id = bake.texture_id; slot.texture_index = avatar_texture_index_from_i32(i32::from(target)) .unwrap_or(AvatarTextureIndex::Unknown); } } drop(slots); inner.cache_reply_generation.fetch_add(1, Ordering::AcqRel); inner.cache_notify.notify_one(); inner .events .cached_bakes_reply .emit_with(|| AgentCachedBakesReplyEventArgs); } fn handle_rebake_request(inner: &Arc, data: Vec) { let mut offset = 0; let Ok(packet) = RebakeAvatarTexturesPacket::new_with_bytes_int32(data, &mut offset) else { return; }; let texture_id = packet.texture_data.texture_id; inner .events .rebake_avatar_requested .emit_with(|| RebakeAvatarTexturesEventArgs { texture_id }); spawn_appearance_request(inner, true, "appearance-rebake"); } fn spawn_appearance_request(inner: &Arc, force_rebake: bool, name: &str) { let manager = AppearanceManager::native_from_inner(Arc::clone(inner)); let _ = std::thread::Builder::new() .name(name.to_owned()) .spawn(move || { let Ok(runtime) = tokio::runtime::Builder::new_current_thread() .enable_all() .build() else { return; }; let _ = runtime.block_on(manager.native_request_set_appearance(force_rebake)); }); } impl AppearanceManager { pub(crate) fn native_new(client: Option>) -> Result { let client = client.ok_or(Error::ArgumentNull)?; let provider: Arc = Arc::new( crate::GridClientBakingTextureProvider::native_new(client.as_ref().clone())?, ); let inner = Arc::new(AppearanceManagerInner { client: client.native_weak_handle(), wearables: RwLock::new(Vec::new()), attachments: RwLock::new(Vec::new()), last_cof_version: AtomicI32::new(-1), busy: AtomicBool::new(false), disposed: AtomicBool::new(false), texture_provider: RwLock::new(provider), texture_slots: RwLock::new( (0..AvatarTextureIndex::NumberOfEntries as usize) .map(|_| texture_data_defaults().expect("texture defaults")) .collect(), ), wearable_assets: RwLock::new(HashMap::new()), visual_parameters: RwLock::new(Vec::new()), server_visual_parameters: AtomicBool::new(false), cache_serial: AtomicI32::new(0), cache_reply_generation: AtomicU64::new(0), cache_notify: tokio::sync::Notify::new(), wearables_reply_generation: AtomicU64::new(0), wearables_notify: tokio::sync::Notify::new(), appearance_serial: AtomicU64::new(0), appearance_cancel: Mutex::new(None), appearance_gate: Arc::new(tokio::sync::Semaphore::new(1)), events: AppearanceEvents::default(), network_subscriptions: Mutex::new(Vec::new()), }); let weak = Arc::downgrade(&inner); let subscription = client .native_network()? .subscribe_raw_packet(Arc::new(move |event| { let Some(inner) = weak.upgrade() else { return; }; handle_appearance_packet(&inner, event.packet_type, event.data); })); mutex(&inner.network_subscriptions).push(subscription); Ok(Self::native_from_inner(inner)) } pub(crate) fn native_from_inner(inner: Arc) -> Self { let my_visual_parameters = read(&inner.visual_parameters).clone(); Self { my_textures: PrimitiveTextureEntry { default_texture: None, face_textures: Vec::new(), }, my_visual_parameters, inner, } } pub(crate) fn native_inner(&self) -> Arc { Arc::clone(&self.inner) } fn client(&self) -> Result { if self.inner.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } self.inner.client.upgrade().ok_or(Error::InvalidOperation) } fn begin(&self) -> Result, Error> { self.client()?; if self .inner .busy .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) .is_err() { return Err(Error::InvalidOperation); } Ok(BusyGuard(&self.inner.busy)) } pub(crate) fn native_subscribe_agent_wearables_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.events.agent_wearables_reply.subscribe(handler) } pub(crate) fn native_subscribe_appearance_set( &self, handler: EventHandler, ) -> Subscription { self.inner.events.appearance_set.subscribe(handler) } pub(crate) fn native_subscribe_cached_bakes_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.events.cached_bakes_reply.subscribe(handler) } pub(crate) fn native_subscribe_rebake_avatar_requested( &self, handler: EventHandler, ) -> Subscription { self.inner.events.rebake_avatar_requested.subscribe(handler) } pub(crate) fn native_texture_provider(&self) -> Arc { Arc::clone(&read(&self.inner.texture_provider)) } pub(crate) fn native_set_texture_provider( &self, provider: Arc, ) { *write(&self.inner.texture_provider) = provider; } #[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible. pub(crate) fn native_dispose(&self) -> Result<(), Error> { self.inner.disposed.store(true, Ordering::Release); write(&self.inner.wearables).clear(); self.inner .server_visual_parameters .store(false, Ordering::Release); write(&self.inner.attachments).clear(); mutex(&self.inner.network_subscriptions).clear(); Ok(()) } pub(crate) fn native_manager_busy(&self) -> bool { self.inner.busy.load(Ordering::Acquire) } pub(crate) fn native_last_cof_version(&self) -> i32 { self.inner.last_cof_version.load(Ordering::Acquire) } #[allow(clippy::unnecessary_wraps)] // The mapped method returns Result<(), Error>. pub(crate) fn native_update_cof_version(&self, version: i32) -> Result<(), Error> { self.inner .last_cof_version .fetch_max(version, Ordering::AcqRel); Ok(()) } pub(crate) fn native_add_to_outfit( &self, items: Vec, replace: bool, ) -> Result<(), Error> { let busy = self.begin()?; let mut next = read(&self.inner.wearables).clone(); apply_wearables(&mut next, items, replace)?; self.send_wearables(&next)?; *write(&self.inner.wearables) = next; self.inner .server_visual_parameters .store(false, Ordering::Release); drop(busy); self.inner .events .appearance_set .emit_with(|| AppearanceSetEventArgs { success: true }); self.inner .events .agent_wearables_reply .emit_with(|| AgentWearablesReplyEventArgs); Ok(()) } pub(crate) fn native_remove_from_outfit(&self, items: Vec) -> Result<(), Error> { let busy = self.begin()?; let ids: HashSet<_> = items.into_iter().map(|item| item.base.uuid()).collect(); let mut next = read(&self.inner.wearables).clone(); next.retain(|wearable| !ids.contains(&wearable.item_id)); self.send_wearables(&next)?; *write(&self.inner.wearables) = next; self.inner .server_visual_parameters .store(false, Ordering::Release); drop(busy); self.inner .events .appearance_set .emit_with(|| AppearanceSetEventArgs { success: true }); self.inner .events .agent_wearables_reply .emit_with(|| AgentWearablesReplyEventArgs); Ok(()) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_replace_outfit( &self, items: Vec, safe: bool, ) -> Result<(), Error> { let busy = self.begin()?; let mut next = if safe { read(&self.inner.wearables) .iter() .filter(|wearable| wearable.asset_type == AssetType::Bodypart) .cloned() .collect() } else { Vec::new() }; apply_wearables(&mut next, items, false)?; self.send_wearables(&next)?; *write(&self.inner.wearables) = next; self.inner .server_visual_parameters .store(false, Ordering::Release); drop(busy); self.inner .events .appearance_set .emit_with(|| AppearanceSetEventArgs { success: true }); self.inner .events .agent_wearables_reply .emit_with(|| AgentWearablesReplyEventArgs); Ok(()) } pub(crate) async fn native_wear_outfit( &self, values: Vec, replace: bool, ) -> Result<(), Error> { let client = self.client()?; let store = client .native_inventory()? .native_store() .ok_or(Error::InvalidOperation)?; let items: Vec<_> = values .into_iter() .filter_map(|value| store.native_item_value(value.uuid())) .collect(); if replace { self.native_replace_outfit(items, false).await } else { self.native_add_to_outfit(items, false) } } pub(crate) fn native_get_wearables( &self, ) -> Result>, Error> { self.client()?; let values: Vec<_> = read(&self.inner.wearables) .iter() .map(|record| { let mut value = record.public(); value.asset = read(&self.inner.wearable_assets) .get(&record.item_id) .cloned(); value }) .collect(); Ok(Box::new(values.into_iter())) } pub(crate) fn native_get_wearable_assets( &self, wearable_type: WearableType, ) -> Result>, Error> { self.client()?; let values: Vec<_> = read(&self.inner.wearables) .iter() .filter(|wearable| wearable.wearable_type == wearable_type) .map(|wearable| wearable.asset_id) .collect(); Ok(Box::new(values.into_iter())) } pub(crate) fn native_get_wearables_by_type( &self, ) -> Result, Error> { self.client()?; let result = MultiValueDictionary::new_with_constructor()?; for wearable in read(&self.inner.wearables).iter() { let mut value = wearable.public(); value.asset = read(&self.inner.wearable_assets) .get(&wearable.item_id) .cloned(); result.add(wearable.wearable_type, Some(value))?; } Ok(result) } pub(crate) fn native_is_item_worn(&self, item_id: UUID) -> Result { self.client()?; Ok(read(&self.inner.wearables) .iter() .find(|wearable| wearable.item_id == item_id) .map_or(WearableType::Invalid, |wearable| wearable.wearable_type)) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_send_outfit( &self, cancellation_token: Option, ) -> Result<(), Error> { check_cancelled(cancellation_token.as_ref())?; let busy = self.begin()?; let snapshot = read(&self.inner.wearables).clone(); check_cancelled(cancellation_token.as_ref())?; self.send_wearables(&snapshot)?; drop(busy); self.inner .events .appearance_set .emit_with(|| AppearanceSetEventArgs { success: true }); Ok(()) } fn send_wearables(&self, wearables: &[WearableRecord]) -> Result<(), Error> { let client = self.client()?; let network = client.native_network()?; let mut packet = AgentIsNowWearingPacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.wearable_data = wearables .iter() .map(|wearable| { let mut block = AgentIsNowWearingPacketWearableDataBlock::new_with_constructor()?; block.item_id = wearable.item_id; block.wearable_type = wearable.wearable_type as u8; Ok(block) }) .collect::, Error>>()?; send_generated( &network, PacketType::AgentIsNowWearing, packet.to_bytes_with_method()?, )?; Ok(()) } pub(crate) fn native_attach( &self, item: InventoryItem, point: AttachmentPoint, replace: bool, ) -> Result<(), Error> { let busy = self.begin()?; let name = item.base.name(); let description = item.description(); let permissions = item.permissions(); self.send_attach_packet( item.base.uuid(), item.base.owner_id(), &name, &description, permissions, item.flags(), point, replace, )?; self.record_attachment(item, point, replace); drop(busy); Ok(()) } #[allow(clippy::too_many_arguments)] pub(crate) fn native_attach_fields( &self, item_id: UUID, owner_id: UUID, name: String, description: String, permissions: Permissions, item_flags: u32, point: AttachmentPoint, replace: bool, ) -> Result<(), Error> { let busy = self.begin()?; self.send_attach_packet( item_id, owner_id, &name, &description, permissions, item_flags, point, replace, )?; let mut item = InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id)?; item.base.set_owner_id(owner_id); item.base.set_name(name); item.set_description(description); item.set_permissions(permissions); item.set_flags(item_flags); item.set_asset_type(AssetType::Object); self.record_attachment(item, point, replace); drop(busy); Ok(()) } #[allow(clippy::too_many_arguments)] fn send_attach_packet( &self, item_id: UUID, owner_id: UUID, name: &str, description: &str, permissions: Permissions, item_flags: u32, point: AttachmentPoint, replace: bool, ) -> Result<(), Error> { let client = self.client()?; let network = client.native_network()?; send_generated( &network, PacketType::RezSingleAttachmentFromInv, attachment_packet_bytes( network.native_agent_id(), network.native_session_id(), item_id, owner_id, name, description, permissions, item_flags, point, replace, )?, ) } fn record_attachment(&self, item: InventoryItem, point: AttachmentPoint, replace: bool) { let mut attachments = write(&self.inner.attachments); if replace && point != AttachmentPoint::Default { attachments.retain(|existing| existing.point != point); } attachments.retain(|existing| existing.item.base.uuid() != item.base.uuid()); attachments.push(AttachmentRecord { item, point }); } pub(crate) fn native_add_attachments( &self, items: Vec, remove_existing_first: bool, replace: bool, ) -> Result<(), Error> { let busy = self.begin()?; let existing = read(&self.inner.attachments).clone(); if remove_existing_first { for attachment in &existing { self.send_detach_packet(attachment.item.base.uuid())?; write(&self.inner.attachments) .retain(|candidate| candidate.item.base.uuid() != attachment.item.base.uuid()); } } for item in items { let object = InventoryObject { base: item.clone() }; let point = object.attach_point(); let name = item.base.name(); let description = item.description(); self.send_attach_packet( item.base.uuid(), item.base.owner_id(), &name, &description, item.permissions(), item.flags(), point, replace, )?; self.record_attachment(item, point, replace); } drop(busy); Ok(()) } pub(crate) fn native_detach(&self, item_id: UUID) -> Result<(), Error> { let busy = self.begin()?; self.send_detach_packet(item_id)?; write(&self.inner.attachments).retain(|attachment| attachment.item.base.uuid() != item_id); drop(busy); Ok(()) } fn send_detach_packet(&self, item_id: UUID) -> Result<(), Error> { let client = self.client()?; let network = client.native_network()?; let mut packet = DetachAttachmentIntoInvPacket::new_with_constructor()?; packet.object_data.agent_id = network.native_agent_id(); packet.object_data.item_id = item_id; send_generated( &network, PacketType::DetachAttachmentIntoInv, packet.to_bytes_with_method()?, ) } pub(crate) fn native_get_attachments( &self, ) -> Result>, Error> { self.client()?; let values: Vec<_> = read(&self.inner.attachments) .iter() .map(|attachment| attachment.item.clone()) .collect(); Ok(Box::new(values.into_iter())) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_get_attachments_by_point( &self, cancellation_token: Option, ) -> Result, Error> { check_cancelled(cancellation_token.as_ref())?; self.client()?; let result = MultiValueDictionary::new_with_constructor()?; for attachment in read(&self.inner.attachments).iter() { result.add(attachment.point, Some(attachment.item.clone()))?; } Ok(result) } pub(crate) fn native_get_attachments_by_item( &self, ) -> Result, Error> { self.client()?; Ok(read(&self.inner.attachments) .iter() .map(|attachment| (attachment.item.clone(), attachment.point)) .collect()) } pub(crate) fn native_get_attachments_by_id( &self, ) -> Result, Error> { self.client()?; Ok(read(&self.inner.attachments) .iter() .map(|attachment| (attachment.item.base.uuid(), attachment.point)) .collect()) } pub(crate) fn native_is_item_attached(&self, item_id: UUID) -> bool { read(&self.inner.attachments) .iter() .any(|attachment| attachment.item.base.uuid() == item_id) } pub(crate) fn native_attachments_at(&self, point: AttachmentPoint) -> Vec { read(&self.inner.attachments) .iter() .filter(|attachment| attachment.point == point) .map(|attachment| attachment.item.clone()) .collect() } pub(crate) fn native_attachment_point(&self, item_id: UUID) -> Option { read(&self.inner.attachments) .iter() .find(|attachment| attachment.item.base.uuid() == item_id) .map(|attachment| attachment.point) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_get_current_outfit_folder( &self, cancellation_token: Option, ) -> Result, Error> { check_cancelled(cancellation_token.as_ref())?; let client = self.client()?; let Some(store) = client.native_inventory()?.native_store() else { return Ok(None); }; let Some(id) = store.find_folder_for_type(FolderType::CurrentOutfit) else { return Ok(None); }; store.get_value_or_default_with_uuid_a7c63fbe(id) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_get_folder_wearables( &self, folder: UUID, cancellation_token: Option, ) -> Result<(bool, Vec, Vec), Error> { check_cancelled(cancellation_token.as_ref())?; let client = self.client()?; let Some(store) = client.native_inventory()?.native_store() else { return Ok((false, Vec::new(), Vec::new())); }; let mut queue = VecDeque::from([folder]); let mut visited = HashSet::new(); let mut scheduled = HashSet::from([folder]); let mut wearables = Vec::new(); let mut attachments = Vec::new(); while let Some(folder_id) = queue.pop_front() { check_cancelled(cancellation_token.as_ref())?; if !visited.insert(folder_id) { continue; } let Ok(contents) = store.get_contents_with_uuid(folder_id) else { return Ok((false, wearables, attachments)); }; for value in contents { if let Some(child) = value.as_any().downcast_ref::() { let child_id = child.base.uuid(); if scheduled.insert(child_id) { if scheduled.len() > MAX_FOLDER_TRAVERSAL { return Ok((false, wearables, attachments)); } queue.push_back(child_id); } continue; } let Some(item) = inventory_item(value.as_ref()) else { continue; }; if matches!(item.asset_type(), AssetType::Bodypart | AssetType::Clothing) { wearables.push(InventoryWearable { base: item }); } else if matches!(item.asset_type(), AssetType::Object | AssetType::Mesh) { attachments.push(item); } } } Ok((true, wearables, attachments)) } pub(crate) async fn native_request_agent_worn( &self, cancellation_token: Option, ) -> Result, Error> { let client = self.client()?; let inventory = client.native_inventory()?; let store = inventory.native_store().ok_or(Error::InvalidOperation)?; let folder = self .native_get_current_outfit_folder(cancellation_token.clone()) .await?; let contents = if let Some(folder) = folder { inventory .native_folder_contents( folder.base.uuid(), folder.base.owner_id(), true, true, InventorySortOrder::BY_DATE, cancellation_token.clone(), true, ) .await? } else { Vec::new() }; if contents.len() > MAX_COF_ENTRIES { return Err(Error::InvalidOperation); } check_cancelled(cancellation_token.as_ref())?; let mut worn = Vec::new(); let mut wearables = Vec::new(); let mut attachments = Vec::new(); for value in contents { check_cancelled(cancellation_token.as_ref())?; let Some(link) = inventory_item(value.as_ref()) else { continue; }; let target_id = if link.is_link()? { link.asset_uuid() } else { link.base.uuid() }; let Some(item) = store.native_item_value(target_id) else { continue; }; worn.push(item.base.clone()); if let Some(record) = WearableRecord::from_item(&item) { wearables.push(record); } else if matches!(item.asset_type(), AssetType::Object | AssetType::Mesh) { let point = if item.inventory_type() == InventoryType::ATTACHMENT { InventoryAttachment { base: item.clone() }.attachment_point() } else { InventoryObject { base: item.clone() }.attach_point() }; attachments.push(AttachmentRecord { item, point }); } } *write(&self.inner.wearables) = wearables; self.inner .server_visual_parameters .store(false, Ordering::Release); *write(&self.inner.attachments) = attachments; self.inner .events .agent_wearables_reply .emit_with(|| AgentWearablesReplyEventArgs); Ok(worn) } #[allow(clippy::unnecessary_wraps)] // Public compatibility methods share the crate error model. pub(crate) fn native_get_current_param_values(&self) -> Result, Error> { Ok(crate::visual_catalog::decode_visual_params(&read( &self.inner.visual_parameters, ))) } pub(crate) fn native_server_baking_region(&self) -> Result { let protocols = self .client()? .native_network()? .native_current_sim() .map_or(0, |simulator| simulator.protocols.0); Ok(protocols & crate::RegionProtocols::AGENT_APPEARANCE_SERVICE.0 != 0) } fn native_server_baking_available(&self) -> Result { if !self.native_server_baking_region()? { return Ok(false); } let simulator = self .client()? .native_network()? .native_current_sim() .ok_or(Error::InvalidOperation)?; let Some(caps) = simulator.caps.as_ref() else { return Ok(false); }; Ok(caps .capability_uri("UpdateAvatarAppearance".to_owned())? .is_some()) } fn native_request_cached_bakes_sent(&self) -> Result { let client = self.client()?; let network = client.native_network()?; let mut packet = AgentCachedTexturePacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.agent_data.serial_num = self.inner.cache_serial.fetch_add(1, Ordering::AcqRel) + 1; let wearables = read(&self.inner.wearables); let map = wearable_bake_map(); let salts = baked_texture_hash(); for baked_index in 0..11_usize { if baked_index == BakeType::Skirt.0 as usize && !wearables .iter() .any(|item| item.wearable_type == WearableType::Skirt) { continue; } let mut hash = UUID::zero(); for wearable_type in &map[baked_index] { for item in wearables .iter() .filter(|item| item.wearable_type == *wearable_type) { hash = UUID::bitxor(hash, item.asset_id); } } if hash != UUID::zero() { let mut block = AgentCachedTexturePacketWearableDataBlock::new_with_constructor()?; block.id = UUID::bitxor(hash, salts[baked_index]); block.texture_index = u8::try_from(baked_index).map_err(|_| Error::Argument)?; packet.wearable_data.push(block); } } drop(wearables); if packet.wearable_data.is_empty() { return Ok(false); } send_generated( &network, PacketType::AgentCachedTexture, packet.to_bytes_with_method()?, )?; Ok(true) } pub(crate) fn native_request_cached_bakes(&self) -> Result<(), Error> { self.native_request_cached_bakes_sent().map(|_| ()) } async fn native_gather_legacy_wearables(&self, token: CancellationToken) -> Result<(), Error> { token.throw_if_cancellation_requested()?; let network = self.client()?.native_network()?; let generation = self .inner .wearables_reply_generation .load(Ordering::Acquire); let notified = self.inner.wearables_notify.notified(); let mut packet = AgentWearablesRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); send_generated( &network, PacketType::AgentWearablesRequest, packet.to_bytes_with_method()?, )?; if self .inner .wearables_reply_generation .load(Ordering::Acquire) == generation { tokio::select! { () = notified => {}, () = token.cancelled() => return Err(Error::Cancelled), () = tokio::time::sleep(std::time::Duration::from_secs(30)) => {}, } } token.throw_if_cancellation_requested()?; if read(&self.inner.wearables).is_empty() { Err(Error::InvalidOperation) } else { Ok(()) } } async fn native_refresh_worn_for_bake( &self, use_server_baking: bool, token: CancellationToken, ) -> Result<(), Error> { match self.native_request_agent_worn(Some(token.clone())).await { Ok(_) => {} Err(Error::Cancelled) => return Err(Error::Cancelled), Err(error) if use_server_baking => return Err(error), Err(_) => { write(&self.inner.wearables).clear(); write(&self.inner.attachments).clear(); } } if !use_server_baking && read(&self.inner.wearables).is_empty() { self.native_gather_legacy_wearables(token).await?; } Ok(()) } pub(crate) fn native_make_appearance_packet(&self) -> Result { let client = self.client()?; let network = client.native_network()?; let mut packet = AgentSetAppearancePacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.agent_data.serial_num = u32::try_from(self.inner.appearance_serial.fetch_add(1, Ordering::AcqRel) + 1) .unwrap_or(u32::MAX); let assets = read(&self.inner.wearable_assets); let server_visual = read(&self.inner.visual_parameters).clone(); let (bytes, param_values) = if self.inner.server_visual_parameters.load(Ordering::Acquire) && !server_visual.is_empty() { let values = crate::visual_catalog::decode_visual_params(&server_visual); (server_visual, values) } else { let params = VisualParams::params(); let wearing_physics = read(&self.inner.wearables) .iter() .any(|wearable| wearable.wearable_type == WearableType::Physics); let parameter_count = if wearing_physics { 251 } else { 218 }; let mut values = HashMap::new(); let bytes = VisualParams::group0_param_ids() .into_iter() .take(parameter_count) .filter_map(|param_id| { let param = params.0.get(¶m_id)?; let param_value = assets .values() .find_map(|asset| asset.params.get(¶m_id).copied()) .unwrap_or(param.default_value); values.insert(param_id, param_value); let range = param.max_value - param.min_value; Some(if range.abs() <= f32::EPSILON { 0 } else { quantize_visual_param((param_value - param.min_value) / range) }) }) .collect(); (bytes, values) }; drop(assets); for value in bytes.iter().copied() { let mut block = AgentSetAppearancePacketVisualParamBlock::new_with_constructor()?; block.param_value = value; packet.visual_param.push(block); } *write(&self.inner.visual_parameters) = bytes; let texture_ids: Vec<_> = read(&self.inner.texture_slots) .iter() .map(|slot| slot.texture_id) .collect(); packet.object_data.texture_entry = encode_texture_entry(&texture_ids)?; let map = wearable_bake_map(); let salts = baked_texture_hash(); let wearables = read(&self.inner.wearables); for baked_index in 0..11_usize { let mut hash = UUID::zero(); for wearable_type in &map[baked_index] { for item in wearables .iter() .filter(|item| item.wearable_type == *wearable_type) { hash = UUID::bitxor(hash, item.asset_id); } } if hash != UUID::zero() { hash = UUID::bitxor(hash, salts[baked_index]); } let mut block = AgentSetAppearancePacketWearableDataBlock::new_with_constructor()?; block.cache_id = hash; block.texture_index = bake_index_to_texture_index()[baked_index]; packet.wearable_data.push(block); } let value = |id, default| param_values.get(&id).copied().unwrap_or(default); let height = 1.706 + value(692, 0.0) * 0.1918 + value(842, 0.0) * 0.0375 + value(33, 0.0) * 0.12022 + value(682, 0.5) * 0.01117 + value(756, 0.0) * 0.038 + value(198, 0.0) * 0.08 + value(503, 0.0) * 0.07; packet.agent_data.size = Vector3::new_with_single_single_single(0.45, 0.6, height)?; Ok(packet) } pub(crate) async fn native_request_set_appearance( &self, force_rebake: bool, ) -> Result<(), Error> { let source = CancellationTokenSource::new(); if let Some(previous) = mutex(&self.inner.appearance_cancel).replace(source.clone()) { previous.cancel(); } let token = source.token(); let _permit = self .inner .appearance_gate .acquire() .await .map_err(|_| Error::InvalidOperation)?; token.throw_if_cancellation_requested()?; self.inner.busy.store(true, Ordering::Release); let _busy = BusyGuard(&self.inner.busy); let old_slots = clone_texture_slots(&read(&self.inner.texture_slots))?; let old_assets = read(&self.inner.wearable_assets).clone(); let old_visual_parameters = read(&self.inner.visual_parameters).clone(); let old_server_visual = self.inner.server_visual_parameters.load(Ordering::Acquire); let old_cof_version = self.inner.last_cof_version.load(Ordering::Acquire); let result = async { let start_simulator = self .client()? .native_network()? .native_current_sim() .ok_or(Error::InvalidOperation)?; let use_server_baking = self.native_server_baking_available()?; self.native_refresh_worn_for_bake(use_server_baking, token.clone()) .await?; if force_rebake { let mut slots = write(&self.inner.texture_slots); for target in bake_index_to_texture_index() { if let Some(slot) = slots.get_mut(usize::from(target)) { slot.texture_id = UUID::zero(); slot.texture = None; } } } if use_server_baking { self.native_server_bake(token.clone()).await?; } else { if !force_rebake && !read(&self.inner.wearables).is_empty() { let generation = self.inner.cache_reply_generation.load(Ordering::Acquire); let notified = self.inner.cache_notify.notified(); let request_sent = self.native_request_cached_bakes_sent()?; if request_sent && self.inner.cache_reply_generation.load(Ordering::Acquire) == generation { tokio::select! { () = notified => {}, () = token.cancelled() => return Err(Error::Cancelled), () = tokio::time::sleep(std::time::Duration::from_secs(30)) => {}, } } let _cache_reply_received = self.inner.cache_reply_generation.load(Ordering::Acquire) != generation; } self.native_gather_baking_inputs(token.clone()).await?; self.native_local_bake(Some(token.clone()), force_rebake) .await?; } token.throw_if_cancellation_requested()?; let packet = self.native_make_appearance_packet()?; let network = self.client()?.native_network()?; if network.native_current_sim().as_ref() != Some(&start_simulator) { return Err(Error::InvalidOperation); } send_generated( &network, PacketType::AgentSetAppearance, packet.to_bytes_with_method()?, ) } .await; if result.is_err() { *write(&self.inner.texture_slots) = old_slots; *write(&self.inner.wearable_assets) = old_assets; *write(&self.inner.visual_parameters) = old_visual_parameters; self.inner .server_visual_parameters .store(old_server_visual, Ordering::Release); self.inner .last_cof_version .store(old_cof_version, Ordering::Release); } // Never invoke callbacks while the manager's state locks are held. let success = result.is_ok(); self.inner .events .appearance_set .emit_with(|| AppearanceSetEventArgs { success }); result } async fn native_server_bake(&self, token: CancellationToken) -> Result<(), Error> { let cof = self .native_get_current_outfit_folder(Some(token.clone())) .await? .ok_or(Error::InvalidOperation)?; if cof.version() < 0 { return Err(Error::InvalidOperation); } let simulator = self .client()? .native_network()? .native_current_sim() .ok_or(Error::InvalidOperation)?; let capability = simulator .caps .as_ref() .ok_or(Error::InvalidOperation)? .capability_uri("UpdateAvatarAppearance".to_owned())? .ok_or(Error::InvalidOperation)?; for retry in 0..5_u32 { token.throw_if_cancellation_requested()?; if retry > 0 { let delay = (1_u64 << retry).saturating_sub(1); tokio::select! { () = tokio::time::sleep(std::time::Duration::from_secs(delay)) => {}, () = token.cancelled() => return Err(Error::Cancelled), } } let payload = OSD::Map(HashMap::from([( "cof_version".to_owned(), OSD::Integer(cof.version()), )])); let (_, bytes) = self .client()? .native_http_caps_client() .post_with_uri_osd_format_osd_cancellation_token_i_progress( capability.clone(), OSDFormat::Xml, payload, token.clone(), None, ) .await?; let OSD::Map(result) = OSDParser::deserialize_with_bytes(bytes)? else { continue; }; if !result .get("success") .is_some_and(|value| value.as_boolean().unwrap_or(false)) { continue; } let visual = result .get("visual_params") .map(OSD::as_binary) .transpose()? .unwrap_or_default(); let textures = match result.get("textures") { Some(OSD::Array(values)) => values .iter() .map(OSD::as_uuid) .collect::, _>>()?, _ => Vec::new(), }; let required = [8_usize, 9, 10, 11]; if required.iter().any(|&index| { textures .get(index) .is_none_or(|id| *id == UUID::zero() || *id == default_avatar_texture()) }) { continue; } let mut slots: Vec<_> = (0..AvatarTextureIndex::NumberOfEntries as usize) .map(|_| texture_data_defaults().expect("texture defaults")) .collect(); for (index, id) in textures.into_iter().take(slots.len()).enumerate() { slots[index].texture_id = id; slots[index].texture_index = i32::try_from(index) .ok() .and_then(avatar_texture_index_from_i32) .unwrap_or(AvatarTextureIndex::Unknown); } *write(&self.inner.texture_slots) = slots; *write(&self.inner.visual_parameters) = visual; self.inner .server_visual_parameters .store(true, Ordering::Release); if let Some(version) = result .get("cof_version") .and_then(|value| value.as_integer().ok()) { self.inner .last_cof_version .fetch_max(version, Ordering::AcqRel); } return Ok(()); } Err(Error::InvalidOperation) } async fn native_gather_baking_inputs(&self, token: CancellationToken) -> Result<(), Error> { let records = read(&self.inner.wearables).clone(); let assets = self.client()?.native_assets()?; let mut decoded = HashMap::new(); let mut slots: Vec<_> = (0..AvatarTextureIndex::NumberOfEntries as usize) .map(|_| texture_data_defaults().expect("texture defaults")) .collect(); { let current = read(&self.inner.texture_slots); for target in bake_index_to_texture_index() { if let (Some(source), Some(destination)) = ( current.get(usize::from(target)), slots.get_mut(usize::from(target)), ) { destination.texture_id = source.texture_id; destination.texture_index = source.texture_index; } } } for record in records { token.throw_if_cancellation_requested()?; let Some(asset) = assets .request_asset_with_uuid_asset_type_boolean_cancellation_token( record.asset_id, record.asset_type, true, Some(token.clone()), ) .await? else { return Err(Error::InvalidOperation); }; let wearable_asset = crate::assets::AssetWearable::native_from_asset(&asset, record.wearable_type)?; let public = AppearanceManagerWearableData { asset: Some(wearable_asset.clone()), asset_id: record.asset_id, asset_type: record.asset_type, item_id: record.item_id, wearable_type: record.wearable_type, }; decode_wearable_params(public, &mut slots)?; decoded.insert(record.item_id, wearable_asset); } let provider = self.native_texture_provider(); let semaphore = Arc::new(tokio::sync::Semaphore::new(5)); let mut requests = FuturesUnordered::new(); for (index, slot) in slots.iter().enumerate() { if slot.texture_id == UUID::zero() { continue; } let provider = Arc::clone(&provider); let semaphore = Arc::clone(&semaphore); let request_token = token.clone(); let texture_id = slot.texture_id; requests.push(async move { let _permit = semaphore .acquire_owned() .await .map_err(|_| Error::InvalidOperation)?; request_token.throw_if_cancellation_requested()?; let texture = provider .request_texture(texture_id, Some(request_token)) .await? .ok_or(Error::InvalidOperation)?; Ok::<_, Error>((index, texture)) }); } while let Some(result) = requests.next().await { let (index, texture) = result?; slots[index].texture = Some(texture); } token.throw_if_cancellation_requested()?; *write(&self.inner.texture_slots) = slots; *write(&self.inner.wearable_assets) = decoded; self.inner .server_visual_parameters .store(false, Ordering::Release); Ok(()) } async fn native_local_bake( &self, cancellation_token: Option, force_rebake: bool, ) -> Result<(), Error> { check_cancelled(cancellation_token.as_ref())?; // Texture decoding populates these slots. Baking an empty outfit is a // valid recovery path and simply retains the previous uploaded bakes. let indices: Vec<_> = read(&self.inner.texture_slots) .iter() .enumerate() .filter_map(|(index, slot)| slot.texture.as_ref().map(|_| index)) .collect(); if indices.is_empty() { return Ok(()); } let mut pending_uploads = Vec::new(); for bake_index in 0..11_i32 { check_cancelled(cancellation_token.as_ref())?; let bake_type = BakeType(bake_index); let target_index = bake_type_to_agent_texture_index(bake_type); if !force_rebake && read(&self.inner.texture_slots) .get(target_index as usize) .is_some_and(|slot| slot.texture_id != UUID::zero()) { continue; } let required = bake_type_to_textures(bake_type); let mut compositor = crate::appearance_baker::Baker::native_new(bake_type)?; let mut layer_count = 0; for index in required { if let Some(slot) = read(&self.inner.texture_slots).get(index as usize) && slot.texture.is_some() && let Some(copy) = clone_texture_slot(slot)? { compositor.native_add_texture(copy)?; layer_count += 1; } } if layer_count == 0 { continue; } compositor.native_bake()?; let Some(baked_texture) = compositor.native_baked_texture() else { continue; }; pending_uploads.push((bake_type_to_agent_texture_index(bake_type), baked_texture)); } let assets = self.client()?.native_assets()?; let upload_gate = Arc::new(tokio::sync::Semaphore::new(6)); let mut uploads = FuturesUnordered::new(); for (target, baked_texture) in pending_uploads { let assets = assets.clone(); let gate = Arc::clone(&upload_gate); let token = cancellation_token.clone(); uploads.push(async move { let _permit = gate .acquire_owned() .await .map_err(|_| Error::InvalidOperation)?; check_cancelled(token.as_ref())?; let uploaded = assets .request_upload_baked_texture(baked_texture.native_bytes(), token) .await?; Ok::<_, Error>((target, uploaded, baked_texture)) }); } while let Some(result) = uploads.next().await { let (target, uploaded, baked_texture) = result?; if uploaded == UUID::zero() { return Err(Error::InvalidOperation); } if let Some(slot) = write(&self.inner.texture_slots).get_mut(target as usize) { slot.texture_id = uploaded; slot.texture = Some(baked_texture); slot.texture_index = target; } } Ok(()) } } fn clone_texture_slot( slot: &AppearanceManagerTextureData, ) -> Result, Error> { Ok(Some(AppearanceManagerTextureData { alpha_masks: slot.alpha_masks.clone(), color: slot.color, texture: slot .texture .as_ref() .map(crate::assets::AssetTexture::native_deep_clone) .transpose()?, texture_id: slot.texture_id, texture_index: slot.texture_index, })) } fn clone_texture_slots( slots: &[AppearanceManagerTextureData], ) -> Result, Error> { slots .iter() .map(|slot| clone_texture_slot(slot)?.ok_or(Error::InvalidOperation)) .collect() } fn encode_texture_entry(texture_ids: &[UUID]) -> Result, Error> { let default = default_avatar_texture(); let mut output = default.get_bytes()?; for (face, id) in texture_ids.iter().copied().enumerate() { if id == UUID::zero() || id == default { continue; } let mask = 1_u64 .checked_shl(u32::try_from(face).map_err(|_| Error::Argument)?) .ok_or(Error::Argument)?; output.extend_from_slice(&texture_entry_face_bitfield(mask)); output.extend_from_slice(&id.get_bytes()?); } output.push(0); // Texture exception terminator. // Remaining default face attributes and their zero exception terminators. // BinaryWriter in the reference uses little-endian numeric values. output.extend_from_slice(&[0, 0, 0, 0, 0]); // inverted white RGBA + terminator output.extend_from_slice(&1.0_f32.to_le_bytes()); output.push(0); // RepeatU output.extend_from_slice(&1.0_f32.to_le_bytes()); output.push(0); // RepeatV for _ in 0..3 { output.extend_from_slice(&0_i16.to_le_bytes()); output.push(0); // OffsetU, OffsetV, Rotation } output.extend_from_slice(&[0, 0]); // material + terminator output.extend_from_slice(&[0, 0]); // media + terminator output.extend_from_slice(&[0, 0]); // glow + terminator output.extend_from_slice(&UUID::zero().get_bytes()?); output.push(0); // material ID terminator output.extend_from_slice(&UUID::zero().get_bytes()?); // render material ID Ok(output) } fn texture_entry_face_bitfield(mask: u64) -> Vec { let byte_count = usize::try_from((u64::BITS - mask.leading_zeros()).div_ceil(7)).unwrap_or(1); (0..byte_count) .map(|index| { let shift = 7 * (byte_count - index - 1); let byte = u8::try_from((mask >> shift) & 0x7f).unwrap_or(0); if index + 1 < byte_count { byte | 0x80 } else { byte } }) .collect() } impl PartialEq for AppearanceManagerWearableData { fn eq(&self, other: &Self) -> bool { self.asset_id == other.asset_id && self.asset_type == other.asset_type && self.item_id == other.item_id && self.wearable_type == other.wearable_type } } impl Eq for AppearanceManagerWearableData {} #[allow(clippy::unnecessary_wraps)] // The generated constructor is fallible across the API. pub(crate) fn texture_data_defaults() -> Result { Ok(AppearanceManagerTextureData { alpha_masks: HashMap::new(), color: Color4::white(), texture: None, texture_id: UUID::zero(), texture_index: AvatarTextureIndex::Unknown, }) } pub(crate) fn texture_data_to_string(value: &AppearanceManagerTextureData) -> String { format!("{:?}: {}", value.texture_index, value.texture_id) } #[allow(clippy::unnecessary_wraps)] // The generated constructor is fallible across the API. pub(crate) fn wearable_data_defaults() -> Result { Ok(AppearanceManagerWearableData { asset: None, asset_id: UUID::zero(), asset_type: AssetType::Unknown, item_id: UUID::zero(), wearable_type: WearableType::Invalid, }) } pub(crate) fn wearable_data_to_string(value: &AppearanceManagerWearableData) -> String { format!( "{:?}: item {}, asset {}", value.wearable_type, value.item_id, value.asset_id ) } impl Eq for crate::VisualAlphaParam {} impl Hash for crate::VisualAlphaParam { fn hash(&self, state: &mut H) { self.domain.to_bits().hash(state); self.multiply_blend.hash(state); self.skip_if_zero.hash(state); self.tga_file.hash(state); } } #[allow(clippy::needless_pass_by_value)] // Mirrors the by-value compatibility signature. pub(crate) fn decode_wearable_params( wearable: AppearanceManagerWearableData, textures: &mut Vec, ) -> Result<(), Error> { let Some(asset) = wearable.asset.as_ref() else { return Ok(()); }; if textures.len() < AvatarTextureIndex::NumberOfEntries as usize { textures.resize_with(AvatarTextureIndex::NumberOfEntries as usize, || { texture_data_defaults().expect("texture defaults") }); } let params = VisualParams::params(); let mut masks = HashMap::new(); let mut colors = Vec::new(); for (¶m_id, &value) in &asset.params { let Some(param) = params.0.get(¶m_id) else { continue; }; if let Some(Some(color)) = ¶m.color_params { let accepted = match wearable.wearable_type { WearableType::Tattoo => matches!(param_id, 1062..=1064), WearableType::Jacket => matches!(param_id, 809..=811), WearableType::Hair => matches!(param_id, 112..=115), WearableType::Skin => matches!(param_id, 108 | 110 | 111), _ => true, }; if accepted { colors.push(crate::AppearanceManagerColorParamInfo { value, visual_color_param: color.clone(), visual_param: param.clone(), wearable_type: wearable.wearable_type, }); } } if let Some(Some(alpha)) = ¶m.alpha_params && !alpha.tga_file.is_empty() && !param.is_bump_attribute { masks .entry(alpha.clone()) .or_insert(if value.abs() < f32::EPSILON { 0.01 } else { value }); } if let Some(drivers) = ¶m.drivers { for driver_id in drivers { if let Some(driver) = params.0.get(driver_id) && let Some(Some(alpha)) = &driver.alpha_params && !alpha.tga_file.is_empty() && !driver.is_bump_attribute { masks .entry(alpha.clone()) .or_insert(if value.abs() < f32::EPSILON { 0.01 } else { value }); } } } } let color = if colors.is_empty() { Color4::white() } else { color_from_params(colors)? }; for (&index, &texture_id) in &asset.textures { let slot_index = index as usize; let Some(slot) = textures.get_mut(slot_index) else { continue; }; slot.texture_index = index; slot.alpha_masks.clone_from(&masks); slot.color = color; let id = if texture_id == default_avatar_texture() { UUID::zero() } else { texture_id }; if slot.texture_id != id { slot.texture_id = id; slot.texture = None; } } Ok(()) } #[allow( clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss )] // Reference color ramps select adjacent entries using normalized f32 positions. pub(crate) fn color_from_params( params: Vec, ) -> Result { let mut result = Color4 { r: 0.0, g: 0.0, b: 0.0, a: 0.0, }; for param in params { let colors = ¶m.visual_color_param.colors; if colors.is_empty() { continue; } let selected = if colors.len() == 1 { colors[0] } else { let range = param.visual_param.max_value - param.visual_param.min_value; let normalized = if range.abs() <= f32::EPSILON { 0.0 } else { ((param.value - param.visual_param.min_value) / range).clamp(0.0, 1.0) }; let position = normalized * (colors.len() - 1) as f32; let lower = position.floor() as usize; let upper = (lower + 1).min(colors.len() - 1); Color4::lerp(colors[lower], colors[upper], position - lower as f32)? }; result = match param.visual_color_param.operation { crate::VisualColorOperation::Add => Color4::add(result, selected), crate::VisualColorOperation::Multiply => Color4::mul(result, selected), crate::VisualColorOperation::Blend => Color4::lerp(result, selected, param.value)?, }; } Ok(result) } pub(crate) fn bake_index_to_texture_index() -> Vec { vec![8, 9, 10, 11, 19, 20, 40, 41, 42, 43, 44] } pub(crate) fn avatar_texture_index_from_i32(value: i32) -> Option { use AvatarTextureIndex as T; let values = [ T::HeadBodypaint, T::UpperShirt, T::LowerPants, T::EyesIris, T::Hair, T::UpperBodypaint, T::LowerBodypaint, T::LowerShoes, T::HeadBaked, T::UpperBaked, T::LowerBaked, T::EyesBaked, T::LowerSocks, T::UpperJacket, T::LowerJacket, T::UpperGloves, T::UpperUndershirt, T::LowerUnderpants, T::Skirt, T::SkirtBaked, T::HairBaked, T::LowerAlpha, T::UpperAlpha, T::HeadAlpha, T::EyesAlpha, T::HairAlpha, T::HeadTattoo, T::UpperTattoo, T::LowerTattoo, T::HeadUniversalTattoo, T::UpperUniversalTattoo, T::LowerUniversalTattoo, T::SkirtTattoo, T::HairTattoo, T::EyesTattoo, T::LeftArmTattoo, T::LeftLegTattoo, T::Aux1Tattoo, T::Aux2Tattoo, T::Aux3Tattoo, T::LeftArmBaked, T::LegLegBaked, T::Aux1Baked, T::Aux2Baked, T::Aux3Baked, ]; usize::try_from(value) .ok() .and_then(|index| values.get(index).copied()) } fn uuid(value: &str) -> UUID { UUID::new_with_string(value.to_owned()).expect("fixed appearance UUID") } #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn quantize_visual_param(value: f32) -> u8 { (value.clamp(0.0, 1.0) * 255.0).round() as u8 } pub(crate) fn baked_texture_hash() -> Vec { [ "18ded8d6-bcfc-e415-8539-944c0f5ea7a6", "338c29e3-3024-4dbb-998d-7c04cf4fa88f", "91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f", "b2cf28af-b840-1071-3c6a-78085d8128b5", "ea800387-ea1a-14e0-56cb-24f2022f969a", "0af1ef7c-ad24-11dd-8790-001f5bf833e8", "9d762b57-ffe3-2e34-d897-0c44c8e07c72", "e12f6f01-8b0e-e00a-03c7-bc7e56a6cbdc", "3e2984a2-f03c-71d5-3e97-75fb8c7e1e2f", "29bbb16c-4b0c-4809-8de5-4b4df7cca8ef", "e0f8b768-e68d-a0cc-d1d8-c2e7f3b51b6e", ] .into_iter() .map(uuid) .collect() } pub(crate) fn default_avatar_texture() -> UUID { uuid("c228d1cf-4b5d-4ba8-84f4-899a0796aa97") } pub(crate) fn img_use_baked(bake: BakeType) -> UUID { let value = match bake { BakeType::Head => "5a9f4a74-30f2-821c-b88d-70499d3e7183", BakeType::UpperBody => "ae2de45c-d252-50b8-5c6e-19f39ce79317", BakeType::LowerBody => "24daea5f-0539-cfcf-047f-fbc40b2786ba", BakeType::Eyes => "52cc6bb6-2ee5-e632-d3ad-50197b1dcb8a", BakeType::Skirt => "43529ce8-7faa-ad92-165a-bc4078371687", BakeType::Hair => "09aac1fb-6bce-0bee-7d44-caac6dbb6c63", BakeType::BakedLeftArm => "ff62763f-d60a-9855-890b-0c96f8f8cd98", BakeType::BakedLeftLeg => "8e915e25-31d1-cc95-ae08-d58a47488251", BakeType::BakedAux1 => "9742065b-19b5-297c-858a-29711d539043", BakeType::BakedAux2 => "03642e83-2bd1-4eb9-34b4-4c47ed586d2d", BakeType::BakedAux3 => "edd51b77-fc10-ce7a-4b3d-011dfc349e4f", _ => return UUID::zero(), }; uuid(value) } pub(crate) fn img_use_baked_indices() -> libremetaverse_types::compat::FrozenDictionary { libremetaverse_types::compat::FrozenDictionary(HashMap::from([ (img_use_baked(BakeType::Head), AvatarTextureIndex::HeadBaked), ( img_use_baked(BakeType::UpperBody), AvatarTextureIndex::UpperBaked, ), ( img_use_baked(BakeType::LowerBody), AvatarTextureIndex::LowerBaked, ), (img_use_baked(BakeType::Eyes), AvatarTextureIndex::EyesBaked), ( img_use_baked(BakeType::Skirt), AvatarTextureIndex::SkirtBaked, ), (img_use_baked(BakeType::Hair), AvatarTextureIndex::HairBaked), ( img_use_baked(BakeType::BakedLeftArm), AvatarTextureIndex::LeftArmBaked, ), ( img_use_baked(BakeType::BakedLeftLeg), AvatarTextureIndex::LegLegBaked, ), ( img_use_baked(BakeType::BakedAux1), AvatarTextureIndex::Aux1Baked, ), ( img_use_baked(BakeType::BakedAux2), AvatarTextureIndex::Aux2Baked, ), ( img_use_baked(BakeType::BakedAux3), AvatarTextureIndex::Aux3Baked, ), ])) } pub(crate) fn wearable_bake_map() -> Vec> { use WearableType::{ Alpha, Eyes, Gloves, Hair, Invalid, Jacket, Pants, Shape, Shirt, Shoes, Skin, Skirt, Socks, Tattoo, Underpants, Undershirt, }; vec![ vec![ Shape, Skin, Tattoo, Hair, Alpha, Invalid, Invalid, Invalid, Invalid, ], vec![ Shape, Skin, Tattoo, Shirt, Jacket, Gloves, Undershirt, Alpha, Invalid, ], vec![ Shape, Skin, Tattoo, Pants, Shoes, Socks, Jacket, Underpants, Alpha, ], vec![ Eyes, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, ], vec![ Skirt, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, ], vec![ Hair, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, Invalid, ], vec![Invalid; 9], vec![Invalid; 9], vec![Invalid; 9], vec![Invalid; 9], vec![Invalid; 9], ] } pub(crate) const fn bake_type_to_agent_texture_index(bake: BakeType) -> AvatarTextureIndex { match bake { BakeType::Head => AvatarTextureIndex::HeadBaked, BakeType::UpperBody => AvatarTextureIndex::UpperBaked, BakeType::LowerBody => AvatarTextureIndex::LowerBaked, BakeType::Eyes => AvatarTextureIndex::EyesBaked, BakeType::Skirt => AvatarTextureIndex::SkirtBaked, BakeType::Hair => AvatarTextureIndex::HairBaked, BakeType::BakedLeftArm => AvatarTextureIndex::LeftArmBaked, BakeType::BakedLeftLeg => AvatarTextureIndex::LegLegBaked, BakeType::BakedAux1 => AvatarTextureIndex::Aux1Baked, BakeType::BakedAux2 => AvatarTextureIndex::Aux2Baked, BakeType::BakedAux3 => AvatarTextureIndex::Aux3Baked, _ => AvatarTextureIndex::Unknown, } } pub(crate) fn bake_type_to_textures(bake: BakeType) -> Vec { use AvatarTextureIndex as T; match bake { BakeType::Head => vec![ T::HeadBodypaint, T::HeadTattoo, T::HeadUniversalTattoo, T::Hair, T::HeadAlpha, ], BakeType::UpperBody => vec![ T::UpperBodypaint, T::UpperTattoo, T::UpperUniversalTattoo, T::UpperGloves, T::UpperUndershirt, T::UpperShirt, T::UpperJacket, T::UpperAlpha, ], BakeType::LowerBody => vec![ T::LowerBodypaint, T::LowerTattoo, T::LowerUniversalTattoo, T::LowerUnderpants, T::LowerSocks, T::LowerShoes, T::LowerPants, T::LowerJacket, T::LowerAlpha, ], BakeType::Eyes => vec![T::EyesIris, T::EyesTattoo, T::EyesAlpha], BakeType::Skirt => vec![T::Skirt, T::SkirtTattoo], BakeType::Hair => vec![T::Hair, T::HairTattoo, T::HairAlpha], BakeType::BakedLeftArm => vec![T::LeftArmTattoo], BakeType::BakedLeftLeg => vec![T::LeftLegTattoo], BakeType::BakedAux1 => vec![T::Aux1Tattoo], BakeType::BakedAux2 => vec![T::Aux2Tattoo], BakeType::BakedAux3 => vec![T::Aux3Tattoo], _ => Vec::new(), } } pub(crate) const fn morph_layer_for_bake_type(bake: BakeType) -> AvatarTextureIndex { match bake { BakeType::Head | BakeType::Hair => AvatarTextureIndex::Hair, BakeType::UpperBody => AvatarTextureIndex::UpperShirt, BakeType::LowerBody => AvatarTextureIndex::LowerPants, BakeType::Skirt => AvatarTextureIndex::Skirt, BakeType::BakedLeftArm => AvatarTextureIndex::LeftArmTattoo, BakeType::BakedLeftLeg => AvatarTextureIndex::LeftLegTattoo, BakeType::BakedAux1 => AvatarTextureIndex::Aux1Tattoo, BakeType::BakedAux2 => AvatarTextureIndex::Aux2Tattoo, BakeType::BakedAux3 => AvatarTextureIndex::Aux3Tattoo, _ => AvatarTextureIndex::Unknown, } } pub(crate) const fn wearable_type_to_asset_type(wearable_type: WearableType) -> AssetType { match wearable_type { WearableType::Shape | WearableType::Skin | WearableType::Hair | WearableType::Eyes => { AssetType::Bodypart } WearableType::Invalid => AssetType::Unknown, _ => AssetType::Clothing, } } fn wearable_type_from_u8(value: u8) -> Option { use WearableType as W; [ W::Shape, W::Skin, W::Hair, W::Eyes, W::Shirt, W::Pants, W::Shoes, W::Socks, W::Jacket, W::Gloves, W::Undershirt, W::Underpants, W::Skirt, W::Alpha, W::Tattoo, W::Physics, W::Universal, ] .get(usize::from(value)) .copied() .or((value == u8::MAX).then_some(W::Invalid)) } fn apply_wearables( current: &mut Vec, items: Vec, replace: bool, ) -> Result<(), Error> { let mut seen = HashSet::new(); for item in items { let wearable = WearableRecord::from_item(&item).ok_or(Error::Argument)?; if !seen.insert(wearable.item_id) || current .iter() .any(|existing| existing.item_id == wearable.item_id) { continue; } if replace || wearable.asset_type == AssetType::Bodypart { current.retain(|existing| existing.wearable_type != wearable.wearable_type); } else if current .iter() .filter(|existing| existing.wearable_type == wearable.wearable_type) .count() >= MAX_WEARABLE_LAYERS { return Err(Error::InvalidOperation); } current.push(wearable); } Ok(()) } #[allow(clippy::too_many_arguments)] fn attachment_packet_bytes( agent_id: UUID, session_id: UUID, item_id: UUID, owner_id: UUID, name: &str, description: &str, permissions: Permissions, item_flags: u32, point: AttachmentPoint, replace: bool, ) -> Result, Error> { let mut packet = RezSingleAttachmentFromInvPacket::new_with_constructor()?; packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = session_id; packet.object_data.item_id = item_id; packet.object_data.owner_id = owner_id; packet.object_data.name = name.as_bytes().to_vec(); packet.object_data.description = description.as_bytes().to_vec(); packet.object_data.everyone_mask = permissions.everyone_mask.0; packet.object_data.group_mask = permissions.group_mask.0; packet.object_data.next_owner_mask = permissions.next_owner_mask.0; packet.object_data.item_flags = item_flags; packet.object_data.attachment_pt = (point as u8) | if replace { 0 } else { ATTACHMENT_ADD }; packet.to_bytes_with_method() } fn send_generated( network: &crate::NetworkManager, packet_type: PacketType, bytes: Vec, ) -> Result<(), Error> { let simulator = network .native_current_sim() .ok_or(Error::InvalidOperation)?; let length = i32::try_from(bytes.len()).map_err(|_| Error::Argument)?; simulator.native_send_packet_data(bytes, length, packet_type, false) } fn inventory_item(value: &dyn InventoryObjectClass) -> Option { if let Some(item) = value.as_any().downcast_ref::() { return Some(item.clone()); } macro_rules! item { ($kind:ty) => { if let Some(item) = value.as_any().downcast_ref::<$kind>() { return Some(item.base.clone()); } }; } item!(crate::InventoryAnimation); item!(crate::InventoryAttachment); item!(crate::InventoryCallingCard); item!(crate::InventoryCategory); item!(crate::InventoryGesture); item!(crate::InventoryLSL); item!(crate::InventoryLandmark); item!(crate::InventoryMaterial); item!(crate::InventoryNotecard); item!(crate::InventoryObject); item!(crate::InventorySettings); item!(crate::InventorySnapshot); item!(crate::InventorySound); item!(crate::InventoryTexture); item!(crate::InventoryWearable); None } #[cfg(test)] mod tests { use super::*; fn wearable(id: u64, wearable_type: WearableType, asset_type: AssetType) -> InventoryItem { let mut wearable = InventoryWearable::new(UUID::new_with_u_int64(id).unwrap()).unwrap(); wearable.base.set_asset_type(asset_type); wearable.base.set_inventory_type(InventoryType::WEARABLE); wearable .base .set_asset_uuid(UUID::new_with_u_int64(id + 1_000).unwrap()); wearable.set_wearable_type(wearable_type); wearable.base } #[test] fn wearable_layers_are_bounded_and_bodyparts_replace_by_slot() { let mut clothing = Vec::new(); apply_wearables( &mut clothing, (1..=60) .map(|id| wearable(id, WearableType::Shirt, AssetType::Clothing)) .collect(), false, ) .unwrap(); assert_eq!(clothing.len(), MAX_WEARABLE_LAYERS); assert_eq!( apply_wearables( &mut clothing, vec![wearable(61, WearableType::Shirt, AssetType::Clothing)], false, ), Err(Error::InvalidOperation) ); let mut bodyparts = Vec::new(); apply_wearables( &mut bodyparts, vec![wearable(100, WearableType::Shape, AssetType::Bodypart)], false, ) .unwrap(); apply_wearables( &mut bodyparts, vec![wearable(101, WearableType::Shape, AssetType::Bodypart)], false, ) .unwrap(); assert_eq!(bodyparts.len(), 1); assert_eq!(bodyparts[0].item_id, UUID::new_with_u_int64(101).unwrap()); } #[test] fn attachment_packet_and_replacement_state_preserve_inventory_fields() { let client = GridClient::new().unwrap(); let item_id = UUID::new_with_u_int64(500).unwrap(); let owner_id = UUID::new_with_u_int64(501).unwrap(); let permissions = Permissions::new(1, 2, 3, 4, 5).unwrap(); let mut item = InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id).unwrap(); item.base.set_owner_id(owner_id); item.base.set_name("recording hat".into()); item.set_description("packet fields".into()); item.set_permissions(permissions); item.set_flags(0x1234_5678); item.set_asset_type(AssetType::Object); let bytes = attachment_packet_bytes( UUID::zero(), UUID::zero(), item_id, owner_id, "recording hat", "packet fields", permissions, 0x1234_5678, AttachmentPoint::Skull, false, ) .unwrap(); let mut position = 0; let packet = RezSingleAttachmentFromInvPacket::new_with_bytes_int32(bytes, &mut position).unwrap(); assert_eq!(packet.object_data.item_id, item_id); assert_eq!(packet.object_data.owner_id, owner_id); assert_eq!(packet.object_data.name, b"recording hat"); assert_eq!(packet.object_data.description, b"packet fields"); assert_eq!(packet.object_data.everyone_mask, 2); assert_eq!(packet.object_data.group_mask, 3); assert_eq!(packet.object_data.next_owner_mask, 4); assert_eq!(packet.object_data.item_flags, 0x1234_5678); assert_eq!( packet.object_data.attachment_pt, (AttachmentPoint::Skull as u8) | ATTACHMENT_ADD ); let manager = client.appearance(); manager.record_attachment(item, AttachmentPoint::Skull, false); let replacement_id = UUID::new_with_u_int64(502).unwrap(); let mut replacement = InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, replacement_id) .unwrap(); replacement.set_permissions(permissions); manager.record_attachment(replacement, AttachmentPoint::Skull, true); let state = manager.native_get_attachments_by_id().unwrap(); assert_eq!(state.len(), 1); assert_eq!(state.get(&replacement_id), Some(&AttachmentPoint::Skull)); } #[test] fn appearance_protocol_constants_and_event_payloads_preserve_reference_values() { let expected = [ "18ded8d6-bcfc-e415-8539-944c0f5ea7a6", "338c29e3-3024-4dbb-998d-7c04cf4fa88f", "91b4a2c7-1b1a-ba16-9a16-1f8f8dcc1c3f", "b2cf28af-b840-1071-3c6a-78085d8128b5", "ea800387-ea1a-14e0-56cb-24f2022f969a", "0af1ef7c-ad24-11dd-8790-001f5bf833e8", "9d762b57-ffe3-2e34-d897-0c44c8e07c72", "e12f6f01-8b0e-e00a-03c7-bc7e56a6cbdc", "3e2984a2-f03c-71d5-3e97-75fb8c7e1e2f", "29bbb16c-4b0c-4809-8de5-4b4df7cca8ef", "e0f8b768-e68d-a0cc-d1d8-c2e7f3b51b6e", ]; assert_eq!( baked_texture_hash(), expected .into_iter() .map(|value| UUID::new_with_string(value.to_owned()).unwrap()) .collect::>() ); assert!(AppearanceSetEventArgs::native_new(true).native_success()); assert!(!AppearanceSetEventArgs::native_new(false).native_success()); let texture_id = UUID::new_with_u_int64(700).unwrap(); assert_eq!( RebakeAvatarTexturesEventArgs::native_new(texture_id).native_texture_id(), texture_id ); assert_eq!(wearable_type_from_u8(0), Some(WearableType::Shape)); assert_eq!(wearable_type_from_u8(16), Some(WearableType::Universal)); assert_eq!(wearable_type_from_u8(u8::MAX), Some(WearableType::Invalid)); assert_eq!(wearable_type_from_u8(17), None); } #[test] fn appearance_packet_contains_all_bakes_visual_parameters_and_texture_entry() { let client = GridClient::new().unwrap(); let packet = client.appearance().native_make_appearance_packet().unwrap(); assert_eq!(packet.wearable_data.len(), 11); assert_eq!(packet.visual_param.len(), 218); assert_eq!(packet.object_data.texture_entry.len(), 80); assert_eq!( &packet.object_data.texture_entry[..16], default_avatar_texture().get_bytes().unwrap().as_slice() ); assert_eq!(packet.object_data.texture_entry[16], 0); let snapshot = client.appearance(); assert_eq!( snapshot.my_visual_parameters.len(), packet.visual_param.len() ); assert_eq!( snapshot.my_visual_parameters, packet .visual_param .iter() .map(|block| block.param_value) .collect::>() ); let physics_client = GridClient::new().unwrap(); let physics_manager = physics_client.appearance(); write(&physics_manager.inner.wearables).push(WearableRecord { item_id: UUID::new_with_u_int64(901).unwrap(), asset_id: UUID::new_with_u_int64(902).unwrap(), asset_type: AssetType::Clothing, wearable_type: WearableType::Physics, }); let physics_packet = physics_manager.native_make_appearance_packet().unwrap(); assert_eq!(physics_packet.visual_param.len(), 251); } #[test] fn texture_entry_encodes_face_bitfields_before_texture_ids() { let texture_id = UUID::new_with_u_int64(808).unwrap(); let mut textures = vec![UUID::zero(); 9]; textures[8] = texture_id; let encoded = encode_texture_entry(&textures).unwrap(); assert_eq!( &encoded[..16], default_avatar_texture().get_bytes().unwrap().as_slice() ); assert_eq!(&encoded[16..18], &[0x82, 0x00]); assert_eq!(&encoded[18..34], texture_id.get_bytes().unwrap().as_slice()); assert_eq!(encoded[34], 0); assert_eq!(encoded.len(), 98); } #[test] fn legacy_wearables_packet_preserves_valid_item_asset_and_type_fields() { let client = GridClient::new().unwrap(); let manager = client.appearance(); manager.inner.busy.store(true, Ordering::Release); let item_id = UUID::new_with_u_int64(1_001).unwrap(); let asset_id = UUID::new_with_u_int64(1_002).unwrap(); let mut packet = AgentWearablesUpdatePacket::new_with_constructor().unwrap(); let mut shirt = crate::packets::AgentWearablesUpdatePacketWearableDataBlock::new_with_constructor() .unwrap(); shirt.item_id = item_id; shirt.asset_id = asset_id; shirt.wearable_type = WearableType::Shirt as u8; packet.wearable_data.push(shirt); let mut empty = crate::packets::AgentWearablesUpdatePacketWearableDataBlock::new_with_constructor() .unwrap(); empty.wearable_type = WearableType::Pants as u8; packet.wearable_data.push(empty); handle_appearance_packet( &manager.inner, PacketType::AgentWearablesUpdate, packet.to_bytes_with_method().unwrap(), ); let wearables = read(&manager.inner.wearables); assert_eq!(wearables.len(), 1); assert_eq!(wearables[0].item_id, item_id); assert_eq!(wearables[0].asset_id, asset_id); assert_eq!(wearables[0].asset_type, AssetType::Clothing); assert_eq!(wearables[0].wearable_type, WearableType::Shirt); assert_eq!( manager .inner .wearables_reply_generation .load(Ordering::Acquire), 1 ); } #[tokio::test] async fn failed_appearance_request_emits_failure_and_empty_cache_request_does_not_wait() { let client = GridClient::new().unwrap(); let manager = client.appearance(); assert!(!manager.native_request_cached_bakes_sent().unwrap()); let observed = Arc::new(Mutex::new(Vec::new())); let callback_observed = Arc::clone(&observed); let _subscription = manager.native_subscribe_appearance_set(Arc::new(move |event| { mutex(&callback_observed).push(event.native_success()); })); assert_eq!( manager.native_request_set_appearance(false).await, Err(Error::InvalidOperation) ); assert_eq!(*mutex(&observed), vec![false]); assert!(!manager.native_manager_busy()); } }