1459 lines
50 KiB
Rust
1459 lines
50 KiB
Rust
//! 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<Box<dyn Future<Output = Result<T, Error>> + 'a>>;
|
|
|
|
fn read<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
|
|
lock.read()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
}
|
|
|
|
fn write<T>(lock: &RwLock<T>) -> 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<Arc<InventoryItem>>) -> Result<bool, Error> {
|
|
item.ok_or(Error::ArgumentNull).map(|_| true)
|
|
}
|
|
|
|
pub(crate) fn default_policy_report(
|
|
added: Option<Arc<Vec<InventoryItem>>>,
|
|
removed: Option<Arc<Vec<InventoryItem>>>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + 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<Vec<Arc<dyn ICurrentOutfitPolicy>>>,
|
|
}
|
|
|
|
impl CompositeCurrentOutfitPolicy {
|
|
#[allow(clippy::unnecessary_wraps)] // The mapped constructor is fallible.
|
|
pub(crate) fn native_new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
policies: RwLock::new(Vec::new()),
|
|
})
|
|
}
|
|
|
|
pub(crate) fn native_add_policy(
|
|
&self,
|
|
policy: Option<Arc<dyn ICurrentOutfitPolicy>>,
|
|
) -> 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<Arc<dyn ICurrentOutfitPolicy>>,
|
|
) -> Result<(), Error> {
|
|
let Some(policy) = policy else {
|
|
return Ok(());
|
|
};
|
|
write(&self.policies).retain(|candidate| !Arc::ptr_eq(candidate, &policy));
|
|
Ok(())
|
|
}
|
|
|
|
fn snapshot(&self) -> Vec<Arc<dyn ICurrentOutfitPolicy>> {
|
|
read(&self.policies).clone()
|
|
}
|
|
|
|
pub(crate) fn native_can_attach(
|
|
&self,
|
|
item: Option<Arc<InventoryItem>>,
|
|
) -> Result<bool, Error> {
|
|
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<Arc<InventoryItem>>,
|
|
) -> Result<bool, Error> {
|
|
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<Arc<Vec<InventoryItem>>>,
|
|
removed: Option<Arc<Vec<InventoryItem>>>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> 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<Arc<InventoryItem>>) -> Result<bool, Error> {
|
|
self.native_can_attach(item)
|
|
}
|
|
|
|
fn can_detach(&self, item: Option<Arc<InventoryItem>>) -> Result<bool, Error> {
|
|
self.native_can_detach(item)
|
|
}
|
|
|
|
fn report_item_change(
|
|
&self,
|
|
added: Option<Arc<Vec<InventoryItem>>>,
|
|
removed: Option<Arc<Vec<InventoryItem>>>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Pin<Box<dyn Future<Output = Result<(), Error>> + 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<Inventory>;
|
|
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem>;
|
|
fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint>;
|
|
fn create_links(
|
|
&self,
|
|
parent: UUID,
|
|
links: Vec<LinkRequest>,
|
|
) -> BackendFuture<'_, Vec<InventoryItem>>;
|
|
fn remove_links(&self, links: Vec<UUID>) -> 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<GridClient, Error> {
|
|
self.client.upgrade().ok_or(Error::InvalidOperation)
|
|
}
|
|
}
|
|
|
|
impl OutfitBackend for ClientOutfitBackend {
|
|
fn store(&self) -> Option<Inventory> {
|
|
self.client
|
|
.upgrade()
|
|
.and_then(|client| client.native_inventory().ok())
|
|
.and_then(|manager| manager.native_store())
|
|
}
|
|
|
|
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
|
|
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<AttachmentPoint> {
|
|
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<LinkRequest>,
|
|
) -> BackendFuture<'_, Vec<InventoryItem>> {
|
|
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<UUID>) -> 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<InventoryFolder>,
|
|
shadow_links: HashMap<UUID, InventoryItem>,
|
|
shadow_targets: HashMap<UUID, InventoryItem>,
|
|
}
|
|
|
|
/// 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<dyn OutfitBackend>,
|
|
policies: Arc<CompositeCurrentOutfitPolicy>,
|
|
state: Arc<RwLock<CurrentOutfitState>>,
|
|
operation: Arc<tokio::sync::Semaphore>,
|
|
disposed: Arc<AtomicBool>,
|
|
}
|
|
|
|
impl CurrentOutfitFolder {
|
|
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
|
let client = client.ok_or(Error::ArgumentNull)?;
|
|
let backend: Arc<dyn OutfitBackend> = Arc::new(ClientOutfitBackend {
|
|
client: client.native_weak_handle(),
|
|
});
|
|
Self::with_backend(backend)
|
|
}
|
|
|
|
fn with_backend(backend: Arc<dyn OutfitBackend>) -> Result<Self, Error> {
|
|
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<tokio::sync::SemaphorePermit<'a>, 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<dyn ICurrentOutfitPolicy>,
|
|
) -> Result<Arc<dyn ICurrentOutfitPolicy>, Error> {
|
|
self.ensure_active()?;
|
|
self.policies.native_add_policy(Some(Arc::clone(&policy)))?;
|
|
Ok(policy)
|
|
}
|
|
|
|
pub(crate) fn native_remove_policy(
|
|
&self,
|
|
policy: Arc<dyn ICurrentOutfitPolicy>,
|
|
) -> Result<(), Error> {
|
|
self.policies.native_remove_policy(Some(policy))
|
|
}
|
|
|
|
pub(crate) fn native_cof(&self) -> Option<InventoryFolder> {
|
|
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::<InventoryFolder>(id)
|
|
.ok()
|
|
.flatten()
|
|
})
|
|
}
|
|
|
|
pub(crate) fn native_set_cof(&mut self, value: Option<InventoryFolder>) {
|
|
write(&self.state).cof = value;
|
|
}
|
|
|
|
pub(crate) fn native_resolve_inventory_link(
|
|
&self,
|
|
item: Arc<InventoryItem>,
|
|
) -> Result<Option<Arc<InventoryItem>>, 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<UUID, Error> {
|
|
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<CancellationToken>,
|
|
) -> Result<Option<InventoryBase>, 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<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
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<CancellationToken>,
|
|
) -> Result<Vec<InventoryItem>, 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<CancellationToken>,
|
|
) -> Result<Vec<InventoryItem>, 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<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
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<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
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<InventoryItem>,
|
|
replace: bool,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> 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<InventoryItem>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> 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<CancellationToken>,
|
|
) -> 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<CancellationToken>,
|
|
) -> 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<CancellationToken>,
|
|
) -> Result<bool, Error> {
|
|
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<InventoryItem>,
|
|
replace: bool,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<OutfitChanges, Error> {
|
|
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<LinkRequest> = Vec::new();
|
|
let mut remove = Vec::new();
|
|
let mut planned_layers = HashMap::<WearableType, HashSet<i32>>::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<InventoryItem>, Vec<InventoryItem>), 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<InventoryItem>,
|
|
removed: Vec<InventoryItem>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> 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<T: CofLinkItem>(
|
|
&self,
|
|
item: &T,
|
|
) -> Result<String, Error> {
|
|
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<LinkRequest>,
|
|
remove: Vec<InventoryItem>,
|
|
}
|
|
|
|
#[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<WearableType>;
|
|
}
|
|
|
|
impl CofLinkItem for InventoryWearable {
|
|
fn inventory_item(&self) -> &InventoryItem {
|
|
&self.base
|
|
}
|
|
|
|
fn wearable_type(&self) -> Option<WearableType> {
|
|
Some(self.wearable_type())
|
|
}
|
|
}
|
|
|
|
impl CofLinkItem for InventoryObject {
|
|
fn inventory_item(&self) -> &InventoryItem {
|
|
&self.base
|
|
}
|
|
|
|
fn wearable_type(&self) -> Option<WearableType> {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn inventory_item(value: &dyn InventoryObjectClass) -> Option<InventoryItem> {
|
|
if let Some(item) = value.as_any().downcast_ref::<InventoryItem>() {
|
|
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<WearableType> {
|
|
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<i32> {
|
|
let prefix = (wearable_type as u8).to_string();
|
|
description
|
|
.strip_prefix(&prefix)?
|
|
.parse::<i32>()
|
|
.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<T>(lock: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
|
lock.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct RecordingBackend {
|
|
events: Mutex<Vec<String>>,
|
|
next: AtomicU64,
|
|
fail_remove: AtomicBool,
|
|
attachments: RwLock<Vec<(InventoryItem, AttachmentPoint)>>,
|
|
}
|
|
|
|
impl RecordingBackend {
|
|
fn events(&self) -> Vec<String> {
|
|
mutex(&self.events).clone()
|
|
}
|
|
}
|
|
|
|
impl OutfitBackend for RecordingBackend {
|
|
fn store(&self) -> Option<Inventory> {
|
|
None
|
|
}
|
|
|
|
fn attachments_at(&self, point: AttachmentPoint) -> Vec<InventoryItem> {
|
|
read(&self.attachments)
|
|
.iter()
|
|
.filter(|(_, candidate)| *candidate == point)
|
|
.map(|(item, _)| item.clone())
|
|
.collect()
|
|
}
|
|
|
|
fn attachment_point(&self, item: UUID) -> Option<AttachmentPoint> {
|
|
read(&self.attachments)
|
|
.iter()
|
|
.find(|(candidate, _)| candidate.base.uuid() == item)
|
|
.map(|(_, point)| *point)
|
|
}
|
|
|
|
fn create_links(
|
|
&self,
|
|
parent: UUID,
|
|
links: Vec<LinkRequest>,
|
|
) -> BackendFuture<'_, Vec<InventoryItem>> {
|
|
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<UUID>) -> 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<F: Future>(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>) {
|
|
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<RecordingBackend>) -> CurrentOutfitFolder {
|
|
let backend_dyn: Arc<dyn OutfitBackend> = 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());
|
|
}
|
|
}
|