Files
MetaCrate/crates/libremetaverse/src/appearance_manager.rs
Chili Palmer b1f60db3d6
Some checks failed
Native code generation / deterministic (push) Failing after 2m18s
Imaging and meshing gate / native (push) Failing after 1m28s
Native Rust workspace compile / compile (push) Failing after 58s
Implement appearance baking pipeline (#66)
2026-08-10 10:33:29 +00:00

2550 lines
92 KiB
Rust

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