//! Current Outfit Folder state, policy composition, and transactional mutations. use crate::appearance::ICurrentOutfitPolicy; use crate::client_core::ClientWeakHandle; use crate::{ GridClient, Inventory, InventoryBase, InventoryFolder, InventoryItem, InventoryObject, InventoryObjectClass, InventoryWearable, Primitive, }; use libremetaverse_types::compat::{CancellationToken, Object}; use libremetaverse_types::{AssetType, AttachmentPoint, Error, FolderType, UUID, WearableType}; use std::collections::{HashMap, HashSet}; use std::future::Future; use std::pin::Pin; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; pub(crate) const MAX_CLOTHING_LAYERS: i32 = 60; const MAX_ANCESTRY_DEPTH: usize = 256; type BackendFuture<'a, T> = Pin> + 'a>>; 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) } pub(crate) fn default_policy_decision(item: Option>) -> Result { item.ok_or(Error::ArgumentNull).map(|_| true) } pub(crate) fn default_policy_report( added: Option>>, removed: Option>>, cancellation_token: Option, ) -> Pin> + Send>> { Box::pin(async move { let _ = (added, removed); check_cancelled(cancellation_token.as_ref()) }) } /// Thread-safe AND-composition of independent, pure outfit policies. pub struct CompositeCurrentOutfitPolicy { policies: RwLock>>, } impl CompositeCurrentOutfitPolicy { #[allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible. pub(crate) fn native_new() -> Result { Ok(Self { policies: RwLock::new(Vec::new()), }) } pub(crate) fn native_add_policy( &self, policy: Option>, ) -> Result<&Self, Error> { let policy = policy.ok_or(Error::ArgumentNull)?; let mut policies = write(&self.policies); if !policies .iter() .any(|candidate| Arc::ptr_eq(candidate, &policy)) { policies.push(policy); } Ok(self) } #[allow(clippy::unnecessary_wraps)] // The mapped removal method is fallible. pub(crate) fn native_remove_policy( &self, policy: Option>, ) -> Result<(), Error> { let Some(policy) = policy else { return Ok(()); }; write(&self.policies).retain(|candidate| !Arc::ptr_eq(candidate, &policy)); Ok(()) } fn snapshot(&self) -> Vec> { read(&self.policies).clone() } pub(crate) fn native_can_attach( &self, item: Option>, ) -> Result { let item = item.ok_or(Error::ArgumentNull)?; let policies = self.snapshot(); let mut allowed = true; for policy in policies { // Consult every policy even after a denial. Policies may record an // audit decision, and the C# composite preserves that behavior. allowed &= policy.can_attach(Some(Arc::clone(&item)))?; } Ok(allowed) } pub(crate) fn native_can_detach( &self, item: Option>, ) -> Result { let item = item.ok_or(Error::ArgumentNull)?; let policies = self.snapshot(); let mut allowed = true; for policy in policies { allowed &= policy.can_detach(Some(Arc::clone(&item)))?; } Ok(allowed) } pub(crate) async fn native_report_item_change( &self, added: Option>>, removed: Option>>, cancellation_token: Option, ) -> Result<(), Error> { check_cancelled(cancellation_token.as_ref())?; let policies = self.snapshot(); for policy in policies { check_cancelled(cancellation_token.as_ref())?; policy .report_item_change( added.as_ref().map(Arc::clone), removed.as_ref().map(Arc::clone), cancellation_token.clone(), ) .await?; } Ok(()) } } impl ICurrentOutfitPolicy for CompositeCurrentOutfitPolicy { fn can_attach(&self, item: Option>) -> Result { self.native_can_attach(item) } fn can_detach(&self, item: Option>) -> Result { self.native_can_detach(item) } fn report_item_change( &self, added: Option>>, removed: Option>>, cancellation_token: Option, ) -> Pin> + Send + '_>> { Box::pin(self.native_report_item_change(added, removed, cancellation_token)) } } #[derive(Clone, Debug)] struct LinkRequest { item: InventoryItem, description: String, } trait OutfitBackend: Send + Sync { fn store(&self) -> Option; fn attachments_at(&self, point: AttachmentPoint) -> Vec; fn attachment_point(&self, item: UUID) -> Option; fn create_links( &self, parent: UUID, links: Vec, ) -> BackendFuture<'_, Vec>; fn remove_links(&self, links: Vec) -> BackendFuture<'_, ()>; fn attach( &self, item: InventoryItem, point: AttachmentPoint, replace: bool, ) -> BackendFuture<'_, ()>; fn detach(&self, item: UUID) -> BackendFuture<'_, ()>; } struct ClientOutfitBackend { client: ClientWeakHandle, } impl ClientOutfitBackend { fn client(&self) -> Result { self.client.upgrade().ok_or(Error::InvalidOperation) } } impl OutfitBackend for ClientOutfitBackend { fn store(&self) -> Option { self.client .upgrade() .and_then(|client| client.native_inventory().ok()) .and_then(|manager| manager.native_store()) } fn attachments_at(&self, point: AttachmentPoint) -> Vec { self.client .upgrade() .and_then(|client| client.native_appearance().ok()) .map_or_else(Vec::new, |manager| manager.native_attachments_at(point)) } fn attachment_point(&self, item: UUID) -> Option { self.client .upgrade() .and_then(|client| client.native_appearance().ok()) .and_then(|manager| manager.native_attachment_point(item)) } fn create_links( &self, parent: UUID, links: Vec, ) -> BackendFuture<'_, Vec> { Box::pin(async move { if links.is_empty() { return Ok(Vec::new()); } let client = self.client()?; let manager = client.native_inventory()?; let requested: Vec<_> = links .iter() .map(|request| (request.item.clone(), request.description.clone())) .collect(); let success = Arc::new(AtomicBool::new(false)); let callback_success = Arc::clone(&success); manager .native_create_links( parent, Box::new(requested.into_iter()), Some(Box::new(move |value| { callback_success.store(value, Ordering::Release); })), None, ) .await?; if !success.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let Some(store) = manager.native_store() else { return Ok(Vec::new()); }; let requested_ids: HashSet<_> = links .iter() .map(|request| request.item.base.uuid()) .collect(); let mut created = Vec::new(); for target in requested_ids { for node in store.find_all_links(target)? { let Some(value) = node.data() else { continue; }; let Some(item) = inventory_item(value.as_ref()) else { continue; }; if item.base.parent_uuid() == parent { created.push(item); } } } Ok(created) }) } fn remove_links(&self, links: Vec) -> BackendFuture<'_, ()> { Box::pin(async move { if links.is_empty() { return Ok(()); } self.client()? .native_inventory()? .native_remove_inventory_objects(links, Vec::new(), None) .await }) } fn attach( &self, item: InventoryItem, point: AttachmentPoint, replace: bool, ) -> BackendFuture<'_, ()> { Box::pin(async move { self.client()? .native_appearance()? .native_attach(item, point, replace) }) } fn detach(&self, item: UUID) -> BackendFuture<'_, ()> { Box::pin(async move { self.client()?.native_appearance()?.native_detach(item) }) } } #[derive(Default)] struct CurrentOutfitState { cof: Option, shadow_links: HashMap, shadow_targets: HashMap, } /// Stateful COF service. Mutations are serialized, while policy callbacks and /// inventory reads are always performed from snapshots outside synchronous locks. #[derive(Clone)] pub struct CurrentOutfitFolder { backend: Arc, policies: Arc, state: Arc>, operation: Arc, disposed: Arc, } impl CurrentOutfitFolder { pub(crate) fn native_new(client: Option>) -> Result { let client = client.ok_or(Error::ArgumentNull)?; let backend: Arc = Arc::new(ClientOutfitBackend { client: client.native_weak_handle(), }); Self::with_backend(backend) } fn with_backend(backend: Arc) -> Result { Ok(Self { backend, policies: Arc::new(CompositeCurrentOutfitPolicy::native_new()?), state: Arc::new(RwLock::new(CurrentOutfitState::default())), operation: Arc::new(tokio::sync::Semaphore::new(1)), disposed: Arc::new(AtomicBool::new(false)), }) } fn ensure_active(&self) -> Result<(), Error> { if self.disposed.load(Ordering::Acquire) { Err(Error::InvalidOperation) } else { Ok(()) } } async fn acquire_operation<'a>( &'a self, cancellation_token: Option<&CancellationToken>, ) -> Result, Error> { if let Some(token) = cancellation_token { tokio::select! { biased; () = token.cancelled() => Err(Error::Cancelled), permit = self.operation.acquire() => permit.map_err(|_| Error::InvalidOperation), } } else { self.operation .acquire() .await .map_err(|_| Error::InvalidOperation) } } #[allow(clippy::unnecessary_wraps)] // The mapped Dispose API is fallible. pub(crate) fn native_dispose(&self) -> Result<(), Error> { self.disposed.store(true, Ordering::Release); Ok(()) } pub(crate) fn native_add_policy( &self, policy: Arc, ) -> Result, Error> { self.ensure_active()?; self.policies.native_add_policy(Some(Arc::clone(&policy)))?; Ok(policy) } pub(crate) fn native_remove_policy( &self, policy: Arc, ) -> Result<(), Error> { self.policies.native_remove_policy(Some(policy)) } pub(crate) fn native_cof(&self) -> Option { read(&self.state).cof.clone().or_else(|| { let store = self.backend.store()?; let id = store.find_folder_for_type(FolderType::CurrentOutfit)?; store .get_value_or_default_with_uuid_a7c63fbe::(id) .ok() .flatten() }) } pub(crate) fn native_set_cof(&mut self, value: Option) { write(&self.state).cof = value; } pub(crate) fn native_resolve_inventory_link( &self, item: Arc, ) -> Result>, Error> { if !item.is_link()? { return Ok(Some(item)); } if let Some(target) = read(&self.state) .shadow_targets .get(&item.asset_uuid()) .cloned() { return Ok(Some(Arc::new(target))); } let Some(store) = self.backend.store() else { return Ok(None); }; Ok(store.native_item_value(item.asset_uuid()).map(Arc::new)) } pub(crate) fn native_get_attachment_item_id(prim: Primitive) -> Result { let Some(values) = prim.name_values else { return Ok(UUID::zero()); }; for value in values { if value.name != "AttachItemID" { continue; } return match value.value { Some(Object::UUID(id)) => Ok(id), Some(Object::String(text)) => UUID::parse(text).or(Ok(UUID::zero())), _ => Ok(UUID::zero()), }; } Ok(UUID::zero()) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_fetch_parent( &self, item: InventoryBase, cancellation_token: Option, ) -> Result, Error> { check_cancelled(cancellation_token.as_ref())?; if item.parent_uuid() == UUID::zero() { return Ok(None); } let Some(store) = self.backend.store() else { return Ok(None); }; store.get_value_or_default_with_uuid(item.parent_uuid()) } pub(crate) async fn native_is_object_descendent_of( &self, item: InventoryBase, parent_id: UUID, cancellation_token: Option, ) -> Result { if parent_id == UUID::zero() { return Ok(false); } let mut current = item; let mut visited = HashSet::new(); for _ in 0..MAX_ANCESTRY_DEPTH { check_cancelled(cancellation_token.as_ref())?; let parent = current.parent_uuid(); if parent == parent_id { return Ok(true); } if parent == UUID::zero() || !visited.insert(parent) { return Ok(false); } let Some(next) = self .native_fetch_parent(current, cancellation_token.clone()) .await? else { return Ok(false); }; current = next; } Ok(false) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_get_current_outfit_links( &self, cancellation_token: Option, ) -> Result, Error> { check_cancelled(cancellation_token.as_ref())?; let Some(cof) = self.native_cof() else { return Ok(Vec::new()); }; let mut links = HashMap::new(); if let Some(store) = self.backend.store() && let Ok(contents) = store.get_contents_with_uuid(cof.base.uuid()) { for value in contents { if let Some(item) = inventory_item(value.as_ref()) && item.is_link()? { links.insert(item.base.uuid(), item); } } } for (id, item) in &read(&self.state).shadow_links { if item.base.parent_uuid() == cof.base.uuid() { links.insert(*id, item.clone()); } } let mut links: Vec<_> = links.into_values().collect(); links.sort_unstable_by_key(|item| item.base.uuid()); Ok(links) } pub(crate) async fn native_get_worn_at( &self, wearable_type: WearableType, cancellation_token: Option, ) -> Result, Error> { let links = self .native_get_current_outfit_links(cancellation_token.clone()) .await?; let mut worn = Vec::new(); for link in links { check_cancelled(cancellation_token.as_ref())?; let Some(item) = self.native_resolve_inventory_link(Arc::new(link))? else { continue; }; if item_wearable_type(&item) == Some(wearable_type) { worn.push((*item).clone()); } } Ok(worn) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_can_attach_item( &self, item: InventoryItem, cancellation_token: Option, ) -> Result { self.ensure_active()?; check_cancelled(cancellation_token.as_ref())?; self.policies.native_can_attach(Some(Arc::new(item))) } #[allow(clippy::unused_async)] // Preserves the mapped asynchronous API boundary. pub(crate) async fn native_can_detach_item( &self, item: InventoryItem, cancellation_token: Option, ) -> Result { self.ensure_active()?; check_cancelled(cancellation_token.as_ref())?; self.policies.native_can_detach(Some(Arc::new(item))) } pub(crate) async fn native_add_to_outfit( &self, requested: Vec, replace: bool, cancellation_token: Option, ) -> Result<(), Error> { self.ensure_active()?; check_cancelled(cancellation_token.as_ref())?; let operation = self.acquire_operation(cancellation_token.as_ref()).await?; check_cancelled(cancellation_token.as_ref())?; let changes = self .plan_add(requested, replace, cancellation_token.clone()) .await?; let (added, removed) = self.commit_changes(changes).await?; drop(operation); self.report_changes(added, removed, cancellation_token) .await } pub(crate) async fn native_remove_from_outfit( &self, requested: Vec, cancellation_token: Option, ) -> Result<(), Error> { self.ensure_active()?; check_cancelled(cancellation_token.as_ref())?; let operation = self.acquire_operation(cancellation_token.as_ref()).await?; check_cancelled(cancellation_token.as_ref())?; let links = self .native_get_current_outfit_links(cancellation_token.clone()) .await?; let requested_ids: HashSet<_> = requested .iter() .flat_map(|item| [item.base.uuid(), item.resolved_item_id()]) .collect(); let remove: Vec<_> = links .into_iter() .filter(|link| { requested_ids.contains(&link.base.uuid()) || requested_ids.contains(&link.asset_uuid()) }) .collect(); self.backend .remove_links(remove.iter().map(|link| link.base.uuid()).collect()) .await?; self.remove_shadow(&remove); drop(operation); self.report_changes(Vec::new(), requested, cancellation_token) .await } pub(crate) async fn native_attach( &self, item: InventoryItem, point: AttachmentPoint, replace: bool, cancellation_token: Option, ) -> Result<(), Error> { if !self .native_can_attach_item(item.clone(), cancellation_token.clone()) .await? { return Err(Error::InvalidOperation); } let operation = self.acquire_operation(cancellation_token.as_ref()).await?; check_cancelled(cancellation_token.as_ref())?; let Some(cof) = self.native_cof() else { return Err(Error::InvalidOperation); }; let displaced = if replace && point != AttachmentPoint::Default { self.backend.attachments_at(point) } else { Vec::new() }; let displaced_ids: HashSet<_> = displaced.iter().map(|item| item.base.uuid()).collect(); let displaced_links: Vec<_> = self .native_get_current_outfit_links(cancellation_token.clone()) .await? .into_iter() .filter(|link| displaced_ids.contains(&link.asset_uuid())) .collect(); let created = self .backend .create_links( cof.base.uuid(), vec![LinkRequest { item: item.clone(), description: String::new(), }], ) .await?; if let Err(error) = self.backend.attach(item.clone(), point, replace).await { let _ = self .backend .remove_links(created.iter().map(|link| link.base.uuid()).collect()) .await; return Err(error); } let remove_result = if displaced_links.is_empty() { Ok(()) } else { self.backend .remove_links( displaced_links .iter() .map(|link| link.base.uuid()) .collect(), ) .await }; if let Err(error) = remove_result { let _ = self .backend .remove_links(created.iter().map(|link| link.base.uuid()).collect()) .await; let _ = self.backend.detach(item.base.uuid()).await; for old in displaced { let _ = self.backend.attach(old, point, false).await; } return Err(error); } self.add_shadow( &created, &[LinkRequest { item: item.clone(), description: String::new(), }], ); self.remove_shadow(&displaced_links); drop(operation); self.report_changes(vec![item], displaced, cancellation_token) .await } pub(crate) async fn native_detach( &self, item: InventoryItem, cancellation_token: Option, ) -> Result<(), Error> { if !self .native_can_detach_item(item.clone(), cancellation_token.clone()) .await? { return Err(Error::InvalidOperation); } let operation = self.acquire_operation(cancellation_token.as_ref()).await?; check_cancelled(cancellation_token.as_ref())?; let attachment_point = self.backend.attachment_point(item.base.uuid()); let links = self .native_get_current_outfit_links(cancellation_token.clone()) .await?; let remove: Vec<_> = links .into_iter() .filter(|link| link.asset_uuid() == item.base.uuid()) .collect(); self.backend.detach(item.base.uuid()).await?; if let Err(error) = self .backend .remove_links(remove.iter().map(|link| link.base.uuid()).collect()) .await { if let Some(point) = attachment_point { let _ = self.backend.attach(item, point, false).await; } return Err(error); } self.remove_shadow(&remove); drop(operation); self.report_changes(Vec::new(), vec![item], cancellation_token) .await } pub(crate) async fn native_replace_outfit( &self, folder_id: UUID, cancellation_token: Option, ) -> Result { self.ensure_active()?; check_cancelled(cancellation_token.as_ref())?; let Some(store) = self.backend.store() else { return Ok(false); }; let Ok(contents) = store.get_contents_with_uuid(folder_id) else { return Ok(false); }; let mut requested = Vec::new(); for value in contents { let Some(item) = inventory_item(value.as_ref()) else { continue; }; if item.is_link()? { if let Some(resolved) = self.native_resolve_inventory_link(Arc::new(item))? { requested.push((*resolved).clone()); } } else { requested.push(item); } } let requested_ids: HashSet<_> = requested.iter().map(|item| item.base.uuid()).collect(); let operation = self.acquire_operation(cancellation_token.as_ref()).await?; check_cancelled(cancellation_token.as_ref())?; let mut changes = self .plan_add(requested, true, cancellation_token.clone()) .await?; for link in self .native_get_current_outfit_links(cancellation_token.clone()) .await? { if !requested_ids.contains(&link.asset_uuid()) { changes.remove.push(link); } } changes.remove.sort_unstable_by_key(|item| item.base.uuid()); changes.remove.dedup_by_key(|item| item.base.uuid()); let (added, removed) = self.commit_changes(changes).await?; drop(operation); self.report_changes(added, removed, cancellation_token) .await?; Ok(true) } async fn plan_add( &self, requested: Vec, replace: bool, cancellation_token: Option, ) -> Result { let Some(cof) = self.native_cof() else { return Err(Error::InvalidOperation); }; let links = self .native_get_current_outfit_links(cancellation_token.clone()) .await?; let mut current = Vec::new(); for link in &links { if let Some(item) = self.native_resolve_inventory_link(Arc::new(link.clone()))? { current.push((link.clone(), (*item).clone())); } } let mut seen = HashSet::new(); let mut add: Vec = Vec::new(); let mut remove = Vec::new(); let mut planned_layers = HashMap::>::new(); for (link, worn) in ¤t { if let Some(wearable_type) = item_wearable_type(worn) && let Some(layer) = parse_layer(&link.description(), wearable_type) { planned_layers .entry(wearable_type) .or_default() .insert(layer); } } let mut replaced_types = HashSet::new(); for item in requested { check_cancelled(cancellation_token.as_ref())?; if !seen.insert(item.base.uuid()) || current .iter() .any(|(_, worn)| worn.base.uuid() == item.base.uuid()) { continue; } let description = if let Some(wearable_type) = item_wearable_type(&item) { let same_type: Vec<_> = current .iter() .filter(|(_, worn)| item_wearable_type(worn) == Some(wearable_type)) .collect(); let bodypart = item.asset_type() == AssetType::Bodypart; if (replace && replaced_types.insert(wearable_type)) || bodypart { remove.extend(same_type.iter().map(|(link, _)| link.clone())); } if bodypart { add.retain(|request| item_wearable_type(&request.item) != Some(wearable_type)); String::new() } else { if replace && replaced_types.contains(&wearable_type) && !add .iter() .any(|request| item_wearable_type(&request.item) == Some(wearable_type)) { planned_layers.entry(wearable_type).or_default().clear(); } let used = planned_layers.entry(wearable_type).or_default(); let layer = (0..MAX_CLOTHING_LAYERS) .find(|layer| !used.contains(layer)) .ok_or(Error::InvalidOperation)?; used.insert(layer); format!("{}{:02}", wearable_type as u8, layer) } } else if is_attachment(&item) { String::new() } else { return Err(Error::Argument); }; add.push(LinkRequest { item, description }); } remove.sort_unstable_by_key(|item| item.base.uuid()); remove.dedup_by_key(|item| item.base.uuid()); Ok(OutfitChanges { cof: cof.base.uuid(), add, remove, }) } async fn commit_changes( &self, changes: OutfitChanges, ) -> Result<(Vec, Vec), Error> { let added_items: Vec<_> = changes.add.iter().map(|entry| entry.item.clone()).collect(); let removed_items: Vec<_> = changes .remove .iter() .filter_map(|link| { self.native_resolve_inventory_link(Arc::new(link.clone())) .ok() .flatten() .map(|item| (*item).clone()) }) .collect(); let created = self .backend .create_links(changes.cof, changes.add.clone()) .await?; if let Err(error) = self .backend .remove_links(changes.remove.iter().map(|link| link.base.uuid()).collect()) .await { let _ = self .backend .remove_links(created.iter().map(|link| link.base.uuid()).collect()) .await; let restore: Vec<_> = changes .remove .iter() .filter_map(|link| { self.native_resolve_inventory_link(Arc::new(link.clone())) .ok() .flatten() .map(|item| LinkRequest { item: (*item).clone(), description: link.description(), }) }) .collect(); let _ = self.backend.create_links(changes.cof, restore).await; return Err(error); } self.add_shadow(&created, &changes.add); self.remove_shadow(&changes.remove); Ok((added_items, removed_items)) } async fn report_changes( &self, added: Vec, removed: Vec, cancellation_token: Option, ) -> Result<(), Error> { // A committed mutation is never rolled back merely because cancellation // races with notification. Policies receive the committed change exactly once. let notification_token = cancellation_token.filter(|token| !token.is_cancellation_requested()); self.policies .native_report_item_change( Some(Arc::new(added)), Some(Arc::new(removed)), notification_token, ) .await } fn add_shadow(&self, links: &[InventoryItem], requests: &[LinkRequest]) { let mut state = write(&self.state); for link in links { state.shadow_links.insert(link.base.uuid(), link.clone()); } for request in requests { state .shadow_targets .insert(request.item.base.uuid(), request.item.clone()); } } fn remove_shadow(&self, links: &[InventoryItem]) { let mut state = write(&self.state); for link in links { state.shadow_links.remove(&link.base.uuid()); } let retained: HashSet<_> = state .shadow_links .values() .map(InventoryItem::asset_uuid) .collect(); state .shadow_targets .retain(|target, _| retained.contains(target)); } #[allow(dead_code, clippy::unnecessary_wraps, clippy::unused_self)] // The compatibility overload is fallible and instance-based. pub(crate) fn get_cof_link_description( &self, item: &T, ) -> Result { if item.inventory_item().asset_type() == AssetType::Bodypart { return Ok(String::new()); } Ok(item .wearable_type() .map_or_else(String::new, |value| format!("{}00", value as u8))) } } struct OutfitChanges { cof: UUID, add: Vec, remove: Vec, } #[allow(dead_code)] // Internal C# overload adapter used by compatibility fixtures. pub(crate) trait CofLinkItem { fn inventory_item(&self) -> &InventoryItem; fn wearable_type(&self) -> Option; } impl CofLinkItem for InventoryWearable { fn inventory_item(&self) -> &InventoryItem { &self.base } fn wearable_type(&self) -> Option { Some(self.wearable_type()) } } impl CofLinkItem for InventoryObject { fn inventory_item(&self) -> &InventoryItem { &self.base } fn wearable_type(&self) -> Option { None } } 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 } fn item_wearable_type(item: &InventoryItem) -> Option { matches!(item.asset_type(), AssetType::Bodypart | AssetType::Clothing).then(|| { let wearable = InventoryWearable { base: item.clone() }; wearable.wearable_type() }) } fn is_attachment(item: &InventoryItem) -> bool { matches!(item.asset_type(), AssetType::Object | AssetType::Mesh) } fn parse_layer(description: &str, wearable_type: WearableType) -> Option { let prefix = (wearable_type as u8).to_string(); description .strip_prefix(&prefix)? .parse::() .ok() .filter(|layer| (0..MAX_CLOTHING_LAYERS).contains(layer)) } #[allow(clippy::too_many_lines)] // Mirrors every field in the mapped constructor. pub(crate) fn primitive_defaults() -> Primitive { Primitive { acceleration: libremetaverse_types::Vector3::zero(), active_clients: 0, angular_velocity: libremetaverse_types::Vector3::zero(), crc: 0, child_count: 0, click_action: libremetaverse_types::ClickAction::Touch, collision_plane: libremetaverse_types::Vector4::zero(), extended_mesh_flags: 0, face_media: Vec::new(), flags: libremetaverse_types::PrimFlags::NONE, flexible: None, group_id: UUID::zero(), id: UUID::zero(), is_attachment: false, joint: libremetaverse_types::JointType::Invalid, joint_axis_or_anchor: libremetaverse_types::Vector3::zero(), joint_pivot: libremetaverse_types::Vector3::zero(), light: None, light_map: None, local_id: 0, media_url: String::new(), media_version: String::new(), name_values: None, owner_id: UUID::zero(), parent_id: 0, particle_sys: crate::PrimitiveParticleSystem { angular_velocity: libremetaverse_types::Vector3::zero(), blend_func_dest: crate::PrimitiveParticleSystemBlendFunc::OneMinusSourceAlpha as u8, blend_func_source: crate::PrimitiveParticleSystemBlendFunc::SourceAlpha as u8, burst_part_count: 0, burst_radius: 0.0, burst_rate: 0.0, burst_speed_max: 0.0, burst_speed_min: 0.0, crc: 0, inner_angle: 0.0, max_age: 0.0, outer_angle: 0.0, part_acceleration: libremetaverse_types::Vector3::zero(), part_data_flags: crate::PrimitiveParticleSystemParticleDataFlags::NONE, part_end_color: libremetaverse_types::Color4::white(), part_end_glow: 0.0, part_end_scale_x: 1.0, part_end_scale_y: 1.0, part_flags: 0, part_max_age: 0.0, part_start_color: libremetaverse_types::Color4::white(), part_start_glow: 0.0, part_start_scale_x: 1.0, part_start_scale_y: 1.0, pattern: crate::PrimitiveParticleSystemSourcePattern::NONE, start_age: 0.0, target: UUID::zero(), texture: UUID::zero(), }, physics_props: None, position: libremetaverse_types::Vector3::zero(), prim_data: crate::PrimitiveConstructionData { material: libremetaverse_types::Material::Stone, p_code: libremetaverse_types::PCode::Prim, path_begin: 0.0, path_curve: libremetaverse_types::PathCurve::Line, path_end: 1.0, path_radius_offset: 0.0, path_revolutions: 1.0, path_scale_x: 1.0, path_scale_y: 1.0, path_shear_x: 0.0, path_shear_y: 0.0, path_skew: 0.0, path_taper_x: 0.0, path_taper_y: 0.0, path_twist: 0.0, path_twist_begin: 0.0, profile_begin: 0.0, profile_end: 1.0, profile_hollow: 0.0, state: 0, profile_curve_with_field: 0, }, properties: None, region_handle: 0, rotation: libremetaverse_types::Quaternion::identity(), scale: libremetaverse_types::Vector3::one(), scratch_pad: Vec::new(), sculpt: None, signaled_animations: None, sound: UUID::zero(), sound_flags: libremetaverse_types::SoundFlags::NONE, sound_gain: 0.0, sound_radius: 0.0, tag: None, text: String::new(), text_color: libremetaverse_types::Color4::black(), texture_anim: crate::PrimitiveTextureAnimation { face: 0, flags: crate::PrimitiveTextureAnimMode::ANIM_OFF, length: 0.0, rate: 0.0, size_x: 0, size_y: 0, start: 0.0, }, textures: None, tree_species: crate::Tree::Pine1, unknown_extra_params: Vec::new(), velocity: libremetaverse_types::Vector3::zero(), } } #[cfg(test)] mod tests { use super::*; use libremetaverse_types::InventoryType; use libremetaverse_types::compat::CancellationTokenSource; use std::sync::Mutex; use std::sync::atomic::AtomicU64; fn mutex(lock: &Mutex) -> std::sync::MutexGuard<'_, T> { lock.lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } #[derive(Default)] struct RecordingBackend { events: Mutex>, next: AtomicU64, fail_remove: AtomicBool, attachments: RwLock>, } impl RecordingBackend { fn events(&self) -> Vec { mutex(&self.events).clone() } } impl OutfitBackend for RecordingBackend { fn store(&self) -> Option { None } fn attachments_at(&self, point: AttachmentPoint) -> Vec { read(&self.attachments) .iter() .filter(|(_, candidate)| *candidate == point) .map(|(item, _)| item.clone()) .collect() } fn attachment_point(&self, item: UUID) -> Option { read(&self.attachments) .iter() .find(|(candidate, _)| candidate.base.uuid() == item) .map(|(_, point)| *point) } fn create_links( &self, parent: UUID, links: Vec, ) -> BackendFuture<'_, Vec> { Box::pin(async move { let mut created = Vec::new(); for request in links { mutex(&self.events).push(format!( "create:{}:{}", request.item.base.name(), request.description )); let ordinal = self.next.fetch_add(1, Ordering::AcqRel) + 1; let mut link = InventoryItem::new_with_uuid(UUID::new_with_u_int64(ordinal)?)?; link.base.set_parent_uuid(parent); link.base.set_name(request.item.base.name()); link.set_asset_type(AssetType::Link); link.set_inventory_type(request.item.inventory_type()); link.set_asset_uuid(request.item.base.uuid()); link.set_description(request.description); created.push(link); } Ok(created) }) } fn remove_links(&self, links: Vec) -> BackendFuture<'_, ()> { Box::pin(async move { mutex(&self.events).push(format!("remove:{}", links.len())); if self.fail_remove.load(Ordering::Acquire) && !links.is_empty() { Err(Error::InvalidOperation) } else { Ok(()) } }) } fn attach( &self, item: InventoryItem, point: AttachmentPoint, replace: bool, ) -> BackendFuture<'_, ()> { Box::pin(async move { mutex(&self.events).push(format!( "attach:{}:{}:{replace}", item.base.name(), point as u8 )); let mut attachments = write(&self.attachments); if replace && point != AttachmentPoint::Default { attachments.retain(|(_, candidate)| *candidate != point); } attachments.retain(|(existing, _)| existing.base.uuid() != item.base.uuid()); attachments.push((item, point)); Ok(()) }) } fn detach(&self, item: UUID) -> BackendFuture<'_, ()> { Box::pin(async move { mutex(&self.events).push(format!("detach:{item}")); write(&self.attachments).retain(|(existing, _)| existing.base.uuid() != item); Ok(()) }) } } fn block_on(future: F) -> F::Output { use std::task::{Context, Poll, Wake, Waker}; struct ThreadWake(std::thread::Thread); impl Wake for ThreadWake { fn wake(self: Arc) { self.0.unpark(); } } let waker = Waker::from(Arc::new(ThreadWake(std::thread::current()))); let mut context = Context::from_waker(&waker); let mut future = std::pin::pin!(future); loop { match future.as_mut().poll(&mut context) { Poll::Ready(output) => return output, Poll::Pending => std::thread::park(), } } } fn service(backend: Arc) -> CurrentOutfitFolder { let backend_dyn: Arc = backend; let service = CurrentOutfitFolder::with_backend(backend_dyn).unwrap(); let mut folder = InventoryFolder::new(UUID::new_with_u_int64(900).unwrap()).unwrap(); folder.base.set_name("Current Outfit".into()); write(&service.state).cof = Some(folder); service } fn wearable(name: &str, id: u64, wearable_type: WearableType) -> InventoryItem { let mut item = InventoryWearable::new(UUID::new_with_u_int64(id).unwrap()).unwrap(); item.base.base.set_name(name.into()); item.base .set_asset_uuid(UUID::new_with_u_int64(id + 100).unwrap()); item.base.set_asset_type(AssetType::Clothing); item.base.set_inventory_type(InventoryType::WEARABLE); item.set_wearable_type(wearable_type); item.base } fn attachment(name: &str, id: u64) -> InventoryItem { let mut item = InventoryObject::new(UUID::new_with_u_int64(id).unwrap()).unwrap(); item.base.base.set_name(name.into()); item.base.set_asset_type(AssetType::Object); item.base.set_inventory_type(InventoryType::OBJECT); item.base } #[test] fn fake_grid_add_preserves_layers_and_worn_state() { let backend = Arc::new(RecordingBackend::default()); let service = service(Arc::clone(&backend)); block_on(service.native_add_to_outfit( vec![ wearable("first", 10, WearableType::Shirt), wearable("second", 11, WearableType::Shirt), ], false, None, )) .unwrap(); assert_eq!( backend.events(), ["create:first:400", "create:second:401", "remove:0"] ); let worn = block_on(service.native_get_worn_at(WearableType::Shirt, None)).unwrap(); assert_eq!(worn.len(), 2); } #[test] fn failed_replace_keeps_old_shadow_state_and_records_compensation() { let backend = Arc::new(RecordingBackend::default()); let service = service(Arc::clone(&backend)); let old = wearable("old", 20, WearableType::Shirt); let request = LinkRequest { item: old.clone(), description: "400".into(), }; let created = block_on( backend.create_links(UUID::new_with_u_int64(900).unwrap(), vec![request.clone()]), ) .unwrap(); service.add_shadow(&created, &[request]); mutex(&backend.events).clear(); backend.fail_remove.store(true, Ordering::Release); assert_eq!( block_on(service.native_add_to_outfit( vec![wearable("new", 21, WearableType::Shirt)], true, None, )), Err(Error::InvalidOperation) ); assert_eq!( backend.events(), ["create:new:400", "remove:1", "remove:1", "create:old:400"] ); let worn = block_on(service.native_get_worn_at(WearableType::Shirt, None)).unwrap(); assert_eq!(worn, vec![old]); } #[test] fn attach_creates_cof_link_before_network_execution() { let backend = Arc::new(RecordingBackend::default()); let service = service(Arc::clone(&backend)); block_on(service.native_attach(attachment("hat", 30), AttachmentPoint::Skull, true, None)) .unwrap(); assert_eq!(backend.events(), ["create:hat:", "attach:hat:2:true"]); assert_eq!( block_on(service.native_get_current_outfit_links(None)) .unwrap() .len(), 1 ); } #[test] fn attachment_replace_removes_displaced_link_after_new_attachment() { let backend = Arc::new(RecordingBackend::default()); let service = service(Arc::clone(&backend)); let old = attachment("old-hat", 31); block_on(backend.attach(old.clone(), AttachmentPoint::Skull, false)).unwrap(); let request = LinkRequest { item: old, description: String::new(), }; let created = block_on( backend.create_links(UUID::new_with_u_int64(900).unwrap(), vec![request.clone()]), ) .unwrap(); service.add_shadow(&created, &[request]); mutex(&backend.events).clear(); block_on(service.native_attach( attachment("new-hat", 32), AttachmentPoint::Skull, true, None, )) .unwrap(); assert_eq!( backend.events(), ["create:new-hat:", "attach:new-hat:2:true", "remove:1"] ); let attachments = backend.attachments_at(AttachmentPoint::Skull); assert_eq!(attachments.len(), 1); assert_eq!(attachments[0].base.name(), "new-hat"); let links = block_on(service.native_get_current_outfit_links(None)).unwrap(); assert_eq!(links.len(), 1); assert_eq!(links[0].asset_uuid(), attachments[0].base.uuid()); } #[test] fn cancelled_operation_does_not_reach_backend() { let backend = Arc::new(RecordingBackend::default()); let service = service(Arc::clone(&backend)); let source = CancellationTokenSource::new(); source.cancel(); assert_eq!( block_on(service.native_add_to_outfit( vec![wearable("shirt", 40, WearableType::Shirt)], false, Some(source.token()), )), Err(Error::Cancelled) ); assert!(backend.events().is_empty()); } }