2550 lines
92 KiB
Rust
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(¶m_id)?;
|
|
let param_value = assets
|
|
.values()
|
|
.find_map(|asset| asset.params.get(¶m_id).copied())
|
|
.unwrap_or(param.default_value);
|
|
values.insert(param_id, param_value);
|
|
let range = param.max_value - param.min_value;
|
|
Some(if range.abs() <= f32::EPSILON {
|
|
0
|
|
} else {
|
|
quantize_visual_param((param_value - param.min_value) / range)
|
|
})
|
|
})
|
|
.collect();
|
|
(bytes, values)
|
|
};
|
|
drop(assets);
|
|
for value in bytes.iter().copied() {
|
|
let mut block = AgentSetAppearancePacketVisualParamBlock::new_with_constructor()?;
|
|
block.param_value = value;
|
|
packet.visual_param.push(block);
|
|
}
|
|
*write(&self.inner.visual_parameters) = bytes;
|
|
|
|
let texture_ids: Vec<_> = read(&self.inner.texture_slots)
|
|
.iter()
|
|
.map(|slot| slot.texture_id)
|
|
.collect();
|
|
packet.object_data.texture_entry = encode_texture_entry(&texture_ids)?;
|
|
let map = wearable_bake_map();
|
|
let salts = baked_texture_hash();
|
|
let wearables = read(&self.inner.wearables);
|
|
for baked_index in 0..11_usize {
|
|
let mut hash = UUID::zero();
|
|
for wearable_type in &map[baked_index] {
|
|
for item in wearables
|
|
.iter()
|
|
.filter(|item| item.wearable_type == *wearable_type)
|
|
{
|
|
hash = UUID::bitxor(hash, item.asset_id);
|
|
}
|
|
}
|
|
if hash != UUID::zero() {
|
|
hash = UUID::bitxor(hash, salts[baked_index]);
|
|
}
|
|
let mut block = AgentSetAppearancePacketWearableDataBlock::new_with_constructor()?;
|
|
block.cache_id = hash;
|
|
block.texture_index = bake_index_to_texture_index()[baked_index];
|
|
packet.wearable_data.push(block);
|
|
}
|
|
let value = |id, default| param_values.get(&id).copied().unwrap_or(default);
|
|
let height = 1.706
|
|
+ value(692, 0.0) * 0.1918
|
|
+ value(842, 0.0) * 0.0375
|
|
+ value(33, 0.0) * 0.12022
|
|
+ value(682, 0.5) * 0.01117
|
|
+ value(756, 0.0) * 0.038
|
|
+ value(198, 0.0) * 0.08
|
|
+ value(503, 0.0) * 0.07;
|
|
packet.agent_data.size = Vector3::new_with_single_single_single(0.45, 0.6, height)?;
|
|
Ok(packet)
|
|
}
|
|
|
|
pub(crate) async fn native_request_set_appearance(
|
|
&self,
|
|
force_rebake: bool,
|
|
) -> Result<(), Error> {
|
|
let source = CancellationTokenSource::new();
|
|
if let Some(previous) = mutex(&self.inner.appearance_cancel).replace(source.clone()) {
|
|
previous.cancel();
|
|
}
|
|
let token = source.token();
|
|
let _permit = self
|
|
.inner
|
|
.appearance_gate
|
|
.acquire()
|
|
.await
|
|
.map_err(|_| Error::InvalidOperation)?;
|
|
token.throw_if_cancellation_requested()?;
|
|
self.inner.busy.store(true, Ordering::Release);
|
|
let _busy = BusyGuard(&self.inner.busy);
|
|
let old_slots = clone_texture_slots(&read(&self.inner.texture_slots))?;
|
|
let old_assets = read(&self.inner.wearable_assets).clone();
|
|
let old_visual_parameters = read(&self.inner.visual_parameters).clone();
|
|
let old_server_visual = self.inner.server_visual_parameters.load(Ordering::Acquire);
|
|
let old_cof_version = self.inner.last_cof_version.load(Ordering::Acquire);
|
|
let result = async {
|
|
let start_simulator = self
|
|
.client()?
|
|
.native_network()?
|
|
.native_current_sim()
|
|
.ok_or(Error::InvalidOperation)?;
|
|
let use_server_baking = self.native_server_baking_available()?;
|
|
self.native_refresh_worn_for_bake(use_server_baking, token.clone())
|
|
.await?;
|
|
if force_rebake {
|
|
let mut slots = write(&self.inner.texture_slots);
|
|
for target in bake_index_to_texture_index() {
|
|
if let Some(slot) = slots.get_mut(usize::from(target)) {
|
|
slot.texture_id = UUID::zero();
|
|
slot.texture = None;
|
|
}
|
|
}
|
|
}
|
|
if use_server_baking {
|
|
self.native_server_bake(token.clone()).await?;
|
|
} else {
|
|
if !force_rebake && !read(&self.inner.wearables).is_empty() {
|
|
let generation = self.inner.cache_reply_generation.load(Ordering::Acquire);
|
|
let notified = self.inner.cache_notify.notified();
|
|
let request_sent = self.native_request_cached_bakes_sent()?;
|
|
if request_sent
|
|
&& self.inner.cache_reply_generation.load(Ordering::Acquire) == generation
|
|
{
|
|
tokio::select! {
|
|
() = notified => {},
|
|
() = token.cancelled() => return Err(Error::Cancelled),
|
|
() = tokio::time::sleep(std::time::Duration::from_secs(30)) => {},
|
|
}
|
|
}
|
|
let _cache_reply_received =
|
|
self.inner.cache_reply_generation.load(Ordering::Acquire) != generation;
|
|
}
|
|
self.native_gather_baking_inputs(token.clone()).await?;
|
|
self.native_local_bake(Some(token.clone()), force_rebake)
|
|
.await?;
|
|
}
|
|
token.throw_if_cancellation_requested()?;
|
|
let packet = self.native_make_appearance_packet()?;
|
|
let network = self.client()?.native_network()?;
|
|
if network.native_current_sim().as_ref() != Some(&start_simulator) {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
send_generated(
|
|
&network,
|
|
PacketType::AgentSetAppearance,
|
|
packet.to_bytes_with_method()?,
|
|
)
|
|
}
|
|
.await;
|
|
if result.is_err() {
|
|
*write(&self.inner.texture_slots) = old_slots;
|
|
*write(&self.inner.wearable_assets) = old_assets;
|
|
*write(&self.inner.visual_parameters) = old_visual_parameters;
|
|
self.inner
|
|
.server_visual_parameters
|
|
.store(old_server_visual, Ordering::Release);
|
|
self.inner
|
|
.last_cof_version
|
|
.store(old_cof_version, Ordering::Release);
|
|
}
|
|
// Never invoke callbacks while the manager's state locks are held.
|
|
let success = result.is_ok();
|
|
self.inner
|
|
.events
|
|
.appearance_set
|
|
.emit_with(|| AppearanceSetEventArgs { success });
|
|
result
|
|
}
|
|
|
|
async fn native_server_bake(&self, token: CancellationToken) -> Result<(), Error> {
|
|
let cof = self
|
|
.native_get_current_outfit_folder(Some(token.clone()))
|
|
.await?
|
|
.ok_or(Error::InvalidOperation)?;
|
|
if cof.version() < 0 {
|
|
return Err(Error::InvalidOperation);
|
|
}
|
|
let simulator = self
|
|
.client()?
|
|
.native_network()?
|
|
.native_current_sim()
|
|
.ok_or(Error::InvalidOperation)?;
|
|
let capability = simulator
|
|
.caps
|
|
.as_ref()
|
|
.ok_or(Error::InvalidOperation)?
|
|
.capability_uri("UpdateAvatarAppearance".to_owned())?
|
|
.ok_or(Error::InvalidOperation)?;
|
|
for retry in 0..5_u32 {
|
|
token.throw_if_cancellation_requested()?;
|
|
if retry > 0 {
|
|
let delay = (1_u64 << retry).saturating_sub(1);
|
|
tokio::select! {
|
|
() = tokio::time::sleep(std::time::Duration::from_secs(delay)) => {},
|
|
() = token.cancelled() => return Err(Error::Cancelled),
|
|
}
|
|
}
|
|
let payload = OSD::Map(HashMap::from([(
|
|
"cof_version".to_owned(),
|
|
OSD::Integer(cof.version()),
|
|
)]));
|
|
let (_, bytes) = self
|
|
.client()?
|
|
.native_http_caps_client()
|
|
.post_with_uri_osd_format_osd_cancellation_token_i_progress(
|
|
capability.clone(),
|
|
OSDFormat::Xml,
|
|
payload,
|
|
token.clone(),
|
|
None,
|
|
)
|
|
.await?;
|
|
let OSD::Map(result) = OSDParser::deserialize_with_bytes(bytes)? else {
|
|
continue;
|
|
};
|
|
if !result
|
|
.get("success")
|
|
.is_some_and(|value| value.as_boolean().unwrap_or(false))
|
|
{
|
|
continue;
|
|
}
|
|
let visual = result
|
|
.get("visual_params")
|
|
.map(OSD::as_binary)
|
|
.transpose()?
|
|
.unwrap_or_default();
|
|
let textures = match result.get("textures") {
|
|
Some(OSD::Array(values)) => values
|
|
.iter()
|
|
.map(OSD::as_uuid)
|
|
.collect::<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 (¶m_id, &value) in &asset.params {
|
|
let Some(param) = params.0.get(¶m_id) else {
|
|
continue;
|
|
};
|
|
if let Some(Some(color)) = ¶m.color_params {
|
|
let accepted = match wearable.wearable_type {
|
|
WearableType::Tattoo => matches!(param_id, 1062..=1064),
|
|
WearableType::Jacket => matches!(param_id, 809..=811),
|
|
WearableType::Hair => matches!(param_id, 112..=115),
|
|
WearableType::Skin => matches!(param_id, 108 | 110 | 111),
|
|
_ => true,
|
|
};
|
|
if accepted {
|
|
colors.push(crate::AppearanceManagerColorParamInfo {
|
|
value,
|
|
visual_color_param: color.clone(),
|
|
visual_param: param.clone(),
|
|
wearable_type: wearable.wearable_type,
|
|
});
|
|
}
|
|
}
|
|
if let Some(Some(alpha)) = ¶m.alpha_params
|
|
&& !alpha.tga_file.is_empty()
|
|
&& !param.is_bump_attribute
|
|
{
|
|
masks
|
|
.entry(alpha.clone())
|
|
.or_insert(if value.abs() < f32::EPSILON {
|
|
0.01
|
|
} else {
|
|
value
|
|
});
|
|
}
|
|
if let Some(drivers) = ¶m.drivers {
|
|
for driver_id in drivers {
|
|
if let Some(driver) = params.0.get(driver_id)
|
|
&& let Some(Some(alpha)) = &driver.alpha_params
|
|
&& !alpha.tga_file.is_empty()
|
|
&& !driver.is_bump_attribute
|
|
{
|
|
masks
|
|
.entry(alpha.clone())
|
|
.or_insert(if value.abs() < f32::EPSILON {
|
|
0.01
|
|
} else {
|
|
value
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let color = if colors.is_empty() {
|
|
Color4::white()
|
|
} else {
|
|
color_from_params(colors)?
|
|
};
|
|
for (&index, &texture_id) in &asset.textures {
|
|
let slot_index = index as usize;
|
|
let Some(slot) = textures.get_mut(slot_index) else {
|
|
continue;
|
|
};
|
|
slot.texture_index = index;
|
|
slot.alpha_masks.clone_from(&masks);
|
|
slot.color = color;
|
|
let id = if texture_id == default_avatar_texture() {
|
|
UUID::zero()
|
|
} else {
|
|
texture_id
|
|
};
|
|
if slot.texture_id != id {
|
|
slot.texture_id = id;
|
|
slot.texture = None;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(
|
|
clippy::cast_possible_truncation,
|
|
clippy::cast_precision_loss,
|
|
clippy::cast_sign_loss
|
|
)] // Reference color ramps select adjacent entries using normalized f32 positions.
|
|
pub(crate) fn color_from_params(
|
|
params: Vec<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 = ¶m.visual_color_param.colors;
|
|
if colors.is_empty() {
|
|
continue;
|
|
}
|
|
let selected = if colors.len() == 1 {
|
|
colors[0]
|
|
} else {
|
|
let range = param.visual_param.max_value - param.visual_param.min_value;
|
|
let normalized = if range.abs() <= f32::EPSILON {
|
|
0.0
|
|
} else {
|
|
((param.value - param.visual_param.min_value) / range).clamp(0.0, 1.0)
|
|
};
|
|
let position = normalized * (colors.len() - 1) as f32;
|
|
let lower = position.floor() as usize;
|
|
let upper = (lower + 1).min(colors.len() - 1);
|
|
Color4::lerp(colors[lower], colors[upper], position - lower as f32)?
|
|
};
|
|
result = match param.visual_color_param.operation {
|
|
crate::VisualColorOperation::Add => Color4::add(result, selected),
|
|
crate::VisualColorOperation::Multiply => Color4::mul(result, selected),
|
|
crate::VisualColorOperation::Blend => Color4::lerp(result, selected, param.value)?,
|
|
};
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
pub(crate) fn bake_index_to_texture_index() -> Vec<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());
|
|
}
|
|
}
|