Implement appearance and current outfit services (#65)
This commit is contained in:
964
crates/libremetaverse/src/appearance_manager.rs
Normal file
964
crates/libremetaverse/src/appearance_manager.rs
Normal file
@@ -0,0 +1,964 @@
|
||||
//! Native wearable and attachment state for `AppearanceManager`.
|
||||
|
||||
use crate::client_core::ClientWeakHandle;
|
||||
use crate::packet_catalog::PacketType;
|
||||
use crate::packets::{
|
||||
AgentIsNowWearingPacket, AgentIsNowWearingPacketWearableDataBlock,
|
||||
DetachAttachmentIntoInvPacket, RezSingleAttachmentFromInvPacket,
|
||||
};
|
||||
use crate::{
|
||||
AgentWearablesReplyEventArgs, AppearanceManagerWearableData, AppearanceSetEventArgs,
|
||||
GridClient, InventoryBase, InventoryFolder, InventoryItem, InventoryObject,
|
||||
InventoryObjectClass, InventoryWearable, Permissions, PrimitiveTextureEntry,
|
||||
appearance::CurrentOutfitFolder,
|
||||
};
|
||||
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription};
|
||||
use libremetaverse_types::{
|
||||
AssetType, AttachmentPoint, Error, FolderType, InventoryType, MultiValueDictionary, UUID,
|
||||
WearableType,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
const ATTACHMENT_ADD: u8 = 0x80;
|
||||
const MAX_WEARABLE_LAYERS: usize = 60;
|
||||
const MAX_FOLDER_TRAVERSAL: usize = 256;
|
||||
|
||||
fn mutex<T>(lock: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
lock.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
struct EventRegistry<T> {
|
||||
next_id: AtomicU64,
|
||||
handlers: Arc<Mutex<HashMap<u64, EventHandler<T>>>>,
|
||||
}
|
||||
|
||||
impl<T> Default for EventRegistry<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
next_id: AtomicU64::new(1),
|
||||
handlers: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> EventRegistry<T> {
|
||||
fn subscribe(&self, handler: EventHandler<T>) -> 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<Self> {
|
||||
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<AgentWearablesReplyEventArgs>,
|
||||
appearance_set: EventRegistry<AppearanceSetEventArgs>,
|
||||
}
|
||||
|
||||
pub(crate) struct AppearanceManagerInner {
|
||||
client: ClientWeakHandle,
|
||||
wearables: RwLock<Vec<WearableRecord>>,
|
||||
attachments: RwLock<Vec<AttachmentRecord>>,
|
||||
last_cof_version: AtomicI32,
|
||||
busy: AtomicBool,
|
||||
disposed: AtomicBool,
|
||||
events: AppearanceEvents,
|
||||
}
|
||||
|
||||
/// 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<u8>,
|
||||
inner: Arc<AppearanceManagerInner>,
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
impl AppearanceManager {
|
||||
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
||||
let client = client.ok_or(Error::ArgumentNull)?;
|
||||
Ok(Self::native_from_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),
|
||||
events: AppearanceEvents::default(),
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn native_from_inner(inner: Arc<AppearanceManagerInner>) -> Self {
|
||||
Self {
|
||||
my_textures: PrimitiveTextureEntry {
|
||||
default_texture: None,
|
||||
face_textures: Vec::new(),
|
||||
},
|
||||
my_visual_parameters: Vec::new(),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn native_inner(&self) -> Arc<AppearanceManagerInner> {
|
||||
Arc::clone(&self.inner)
|
||||
}
|
||||
|
||||
fn client(&self) -> Result<GridClient, Error> {
|
||||
if self.inner.disposed.load(Ordering::Acquire) {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
self.inner.client.upgrade().ok_or(Error::InvalidOperation)
|
||||
}
|
||||
|
||||
fn begin(&self) -> Result<BusyGuard<'_>, 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<AgentWearablesReplyEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.events.agent_wearables_reply.subscribe(handler)
|
||||
}
|
||||
|
||||
pub(crate) fn native_subscribe_appearance_set(
|
||||
&self,
|
||||
handler: EventHandler<AppearanceSetEventArgs>,
|
||||
) -> Subscription {
|
||||
self.inner.events.appearance_set.subscribe(handler)
|
||||
}
|
||||
|
||||
#[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();
|
||||
write(&self.inner.attachments).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<InventoryItem>,
|
||||
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;
|
||||
drop(busy);
|
||||
self.inner
|
||||
.events
|
||||
.appearance_set
|
||||
.emit_with(|| AppearanceSetEventArgs);
|
||||
self.inner
|
||||
.events
|
||||
.agent_wearables_reply
|
||||
.emit_with(|| AgentWearablesReplyEventArgs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn native_remove_from_outfit(&self, items: Vec<InventoryItem>) -> 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;
|
||||
drop(busy);
|
||||
self.inner
|
||||
.events
|
||||
.appearance_set
|
||||
.emit_with(|| AppearanceSetEventArgs);
|
||||
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<InventoryItem>,
|
||||
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;
|
||||
drop(busy);
|
||||
self.inner
|
||||
.events
|
||||
.appearance_set
|
||||
.emit_with(|| AppearanceSetEventArgs);
|
||||
self.inner
|
||||
.events
|
||||
.agent_wearables_reply
|
||||
.emit_with(|| AgentWearablesReplyEventArgs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn native_wear_outfit(
|
||||
&self,
|
||||
values: Vec<InventoryBase>,
|
||||
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<Box<dyn Iterator<Item = AppearanceManagerWearableData>>, Error> {
|
||||
self.client()?;
|
||||
let values: Vec<_> = read(&self.inner.wearables)
|
||||
.iter()
|
||||
.map(WearableRecord::public)
|
||||
.collect();
|
||||
Ok(Box::new(values.into_iter()))
|
||||
}
|
||||
|
||||
pub(crate) fn native_get_wearable_assets(
|
||||
&self,
|
||||
wearable_type: WearableType,
|
||||
) -> Result<Box<dyn Iterator<Item = UUID>>, 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<MultiValueDictionary<WearableType, AppearanceManagerWearableData>, Error> {
|
||||
self.client()?;
|
||||
let result = MultiValueDictionary::new_with_constructor()?;
|
||||
for wearable in read(&self.inner.wearables).iter() {
|
||||
result.add(wearable.wearable_type, Some(wearable.public()))?;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn native_is_item_worn(&self, item_id: UUID) -> Result<WearableType, Error> {
|
||||
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<CancellationToken>,
|
||||
) -> 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);
|
||||
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::<Result<Vec<_>, 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<InventoryItem>,
|
||||
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<Box<dyn Iterator<Item = InventoryItem>>, 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<CancellationToken>,
|
||||
) -> Result<MultiValueDictionary<AttachmentPoint, InventoryItem>, 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<HashMap<InventoryItem, AttachmentPoint>, 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<HashMap<UUID, AttachmentPoint>, 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<InventoryItem> {
|
||||
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<AttachmentPoint> {
|
||||
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<CancellationToken>,
|
||||
) -> Result<Option<InventoryFolder>, 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<CancellationToken>,
|
||||
) -> Result<(bool, Vec<InventoryWearable>, Vec<InventoryItem>), 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::<InventoryFolder>() {
|
||||
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<CancellationToken>,
|
||||
) -> Result<Vec<InventoryBase>, Error> {
|
||||
let client = self.client()?;
|
||||
let cof = CurrentOutfitFolder::new(Some(Arc::new(client)))?;
|
||||
let links = cof
|
||||
.get_current_outfit_links(cancellation_token.clone())
|
||||
.await?;
|
||||
check_cancelled(cancellation_token.as_ref())?;
|
||||
Ok(links.into_iter().map(|item| item.base).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 {}
|
||||
|
||||
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 apply_wearables(
|
||||
current: &mut Vec<WearableRecord>,
|
||||
items: Vec<InventoryItem>,
|
||||
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<Vec<u8>, 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<u8>,
|
||||
) -> 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<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
|
||||
}
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user