//! Native non-movement agent services and event delivery. #![allow(clippy::inherent_to_string)] // Public names mirror C# ToString mappings. #![allow(clippy::cast_possible_truncation)] // C# double-to-float mappings truncate identically. #![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility API. #![allow(clippy::needless_pass_by_value)] // Owned parameters mirror mapped C# signatures. #![allow(clippy::too_many_arguments)] // Packet-oriented signatures mirror the reference API. #![allow(clippy::unnecessary_wraps)] // Generated callers require mapped Result signatures. #![allow(clippy::unused_self)] // Instance shape is part of the mapped callback surface. #![allow(clippy::if_not_else)] // Protocol mismatch handling is clearest as the early branch. use crate::messages::linden::AgentStateUpdateMessage; use crate::messages::linden::{ AgentPreferencesMessage, AvatarRenderInfoMessage, ExperienceInfo, ExperienceInfoMessage, ExperienceListMessage, ExperiencePreferencesMessage, RegionExperiencesMessage, ViewerBenefitsMessage, ViewerStatsMessage, }; use crate::{ AgentAccessEventArgs, AgentDataReplyEventArgs, AgentExperiencesEventArgs, AgentPreferencesEventArgs, AnimationsChangedEventArgs, AvatarRenderInfoEventArgs, BalanceEventArgs, ChatEventArgs, ChatSessionMemberAddedEventArgs, ChatSessionMemberLeftEventArgs, ExperienceInfoEventArgs, ExperiencePreferencesEventArgs, GroupChatJoinedEventArgs, MoneyBalanceReplyEventArgs, NavMeshStatusUpdateEventArgs, ProductInfoEventArgs, RegionExperiencesEventArgs, SetDisplayNameReplyEventArgs, ViewerBenefitsEventArgs, }; use crate::{ AgentManagerAgentMovement, ChatSessionMember, ChatType, EffectType, Error, GridClient, InstantMessageDialog, InstantMessageOnline, LookAtType, MoneyTransactionType, MuteEntry, PointAtType, Simulator, TransactionFlags, network_manager::CapsEventQueueCallback, }; use libremetaverse_structured_data::{OSD, OSDMap, OSDParser}; use libremetaverse_types::compat::{EventHandler, Subscription, Uri}; use libremetaverse_types::{Color4, UUID, Vector3, Vector3d}; use std::collections::{BTreeMap, HashMap}; use std::fmt; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, OnceLock, RwLock}; use std::time::{Duration, Instant, UNIX_EPOCH}; const MAX_CHAT_BYTES: usize = 1023; const MAX_DIALOG_LABEL_BYTES: usize = 254; const MAX_GESTURE_ASSET_BYTES: usize = 16 * 1024 * 1024; const MAX_MUTE_LIST_BYTES: usize = 16 * 1024 * 1024; const GESTURE_TIMEOUT: Duration = Duration::from_secs(30); struct MuteXfer { announced_size: Option, bytes: Vec, next_packet: u32, started: Instant, } impl Default for MuteXfer { fn default() -> Self { Self { announced_size: None, bytes: Vec::new(), next_packet: 0, started: Instant::now(), } } } fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { value .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn send_simulator_packet( simulator: &Simulator, packet_type: crate::packets::PacketType, data: Vec, ) -> Result<(), Error> { let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?; let zerocoded = data .first() .is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0); simulator.native_send_packet_data(data, length, packet_type, zerocoded) } fn parse_mute_list(text: &str) -> HashMap { let mut result = HashMap::new(); for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) { let mut fields = line.splitn(3, char::is_whitespace); let (Some(type_text), Some(id_text), Some(name_and_flags)) = (fields.next(), fields.next(), fields.next()) else { continue; }; let Ok(type_number) = type_text.parse::() else { continue; }; let type_ = match type_number { 0 => crate::MuteType::ByName, 1 => crate::MuteType::Resident, 2 => crate::MuteType::Object, 3 => crate::MuteType::Group, 4 => crate::MuteType::External, _ => continue, }; let Ok(id) = UUID::new_with_string(id_text.to_owned()) else { continue; }; let (name, flags) = name_and_flags .rsplit_once('|') .map_or((name_and_flags, 0), |(name, flags)| { (name, flags.trim().parse::().unwrap_or(0)) }); let name = name.trim().to_owned(); if name.is_empty() { continue; } result.insert( format!("{id}|{name}"), MuteEntry { flags: crate::MuteFlags(flags), id, name, type_, }, ); } result } pub(crate) struct EventRegistryState { next_id: AtomicU64, handlers: Mutex)>>, } pub(crate) struct EventRegistry { state: Arc>, } impl Default for EventRegistry { fn default() -> Self { Self { state: Arc::new(EventRegistryState { next_id: AtomicU64::new(1), handlers: Mutex::new(Vec::new()), }), } } } impl EventRegistry { pub(crate) fn subscribe(&self, handler: EventHandler) -> Subscription { let id = self.state.next_id.fetch_add(1, Ordering::Relaxed); mutex(&self.state.handlers).push((id, handler)); let state = Arc::downgrade(&self.state); Subscription::new(move || { if let Some(state) = state.upgrade() { mutex(&state.handlers).retain(|(candidate, _)| *candidate != id); } }) } pub(crate) fn emit(&self, value: T) { let handlers: Vec<_> = mutex(&self.state.handlers) .iter() .map(|(_, handler)| Arc::clone(handler)) .collect(); for handler in handlers { let argument = value.clone(); let _ = catch_unwind(AssertUnwindSafe(|| handler(argument))); } } } #[derive(Clone)] pub struct InstantMessage { pub binary_bucket: Vec, pub dialog: InstantMessageDialog, pub from_agent_id: UUID, pub from_agent_name: String, pub group_im: bool, pub im_session_id: UUID, pub message: String, pub offline: InstantMessageOnline, pub parent_estate_id: u32, pub position: Vector3, pub region_id: UUID, pub timestamp: std::time::SystemTime, pub to_agent_id: UUID, } impl fmt::Debug for InstantMessage { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("InstantMessage") .field("dialog", &self.dialog) .field("from_agent_id", &self.from_agent_id) .field("group_im", &self.group_im) .field("offline", &self.offline) .field("parent_estate_id", &self.parent_estate_id) .field("region_id", &self.region_id) .field("timestamp", &self.timestamp) .field("to_agent_id", &self.to_agent_id) .field("message_len", &self.message.len()) .field("binary_bucket_len", &self.binary_bucket.len()) .finish_non_exhaustive() } } impl InstantMessage { #[must_use] pub fn to_string(&self) -> String { format!("{}: {}", self.from_agent_name, self.message) } } #[derive(Clone)] pub struct InstantMessageEventArgs { im: InstantMessage, simulator: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct ProductInfoRequestMessageProductInfo { pub description: String, pub name: String, pub sku: String, } impl ProductInfoRequestMessageProductInfo { pub fn new() -> Result { Ok(Self::default()) } } #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct ProductInfoRequestMessage { pub products: Vec, } impl ProductInfoRequestMessage { pub fn new() -> Result { Ok(Self::default()) } pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { self.products.clear(); let Some(OSD::Array(products)) = map.get("products") else { return Ok(()); }; for product in products { let OSD::Map(product) = product else { continue; }; self.products.push(ProductInfoRequestMessageProductInfo { description: product .get("description") .map_or(Ok(String::new()), OSD::as_string)?, name: product .get("name") .map_or(Ok(String::new()), OSD::as_string)?, sku: product .get("sku") .map_or(Ok(String::new()), OSD::as_string)?, }); } Ok(()) } pub fn serialize(&self) -> Result { OSDMap::new_with_dictionary(HashMap::from([( "products".into(), OSD::Array( self.products .iter() .map(|product| { OSD::Map(HashMap::from([ ("sku".into(), OSD::String(product.sku.clone())), ("name".into(), OSD::String(product.name.clone())), ( "description".into(), OSD::String(product.description.clone()), ), ])) }) .collect(), ), )])) } } impl crate::interfaces::IMessage for ProductInfoRequestMessage { fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { Self::deserialize(self, map) } fn serialize(&self) -> Result { Self::serialize(self) } } impl InstantMessageEventArgs { pub fn new(im: InstantMessage, simulator: Option) -> Result { Ok(Self { im, simulator }) } #[must_use] pub fn im(&self) -> InstantMessage { self.im.clone() } #[must_use] pub fn simulator(&self) -> Option { self.simulator.clone() } } pub(crate) struct AgentManagerInner { disposed: AtomicBool, agent_id: RwLock, session_id: RwLock, first_name: RwLock, last_name: RwLock, active_group: RwLock, active_group_powers: RwLock, agent_access: RwLock, hover_height: RwLock, balance: RwLock, active_gestures: RwLock>, signaled_animations: Arc>>, mute_list: Arc>>, mute_xfers: Mutex>, instant_message: EventRegistry, agent_data_reply: EventRegistry, animations_changed: EventRegistry, agent_experiences: RwLock>, experience_preferences: RwLock>, region_experiences: RwLock>, agent_preferences: RwLock>, avatar_render_info: RwLock>, viewer_benefits: RwLock>, benefits: RwLock>, last_nav_mesh_status: RwLock>, product_info: RwLock>, group_chat_sessions: Arc>>>, network: Mutex>, raw_packet_subscription: Mutex>, caps_subscriptions: Mutex>, movement: OnceLock>, chat_from_simulator: EventRegistry, money_balance: EventRegistry, money_balance_reply: EventRegistry, agent_experiences_updated: EventRegistry, experience_preferences_updated: EventRegistry, region_experiences_updated: EventRegistry, experience_info_received: EventRegistry, agent_preferences_updated: EventRegistry, avatar_render_info_updated: EventRegistry, viewer_benefits_updated: EventRegistry, product_info_updated: EventRegistry, group_chat_joined: EventRegistry, chat_session_member_added: EventRegistry, chat_session_member_left: EventRegistry, set_display_name_reply: EventRegistry, nav_mesh_status_update: EventRegistry, mute_list_updated: EventRegistry<()>, } impl Default for AgentManagerInner { fn default() -> Self { Self { disposed: AtomicBool::new(false), agent_id: RwLock::new(UUID::zero()), session_id: RwLock::new(UUID::zero()), first_name: RwLock::new(String::new()), last_name: RwLock::new(String::new()), active_group: RwLock::new(UUID::zero()), active_group_powers: RwLock::new(crate::GroupPowers(0)), agent_access: RwLock::new(String::new()), hover_height: RwLock::new(0.0), balance: RwLock::new(0), active_gestures: RwLock::new(HashMap::new()), signaled_animations: Arc::new(RwLock::new(HashMap::new())), mute_list: Arc::new(RwLock::new(HashMap::new())), mute_xfers: Mutex::new(HashMap::new()), instant_message: EventRegistry::default(), agent_data_reply: EventRegistry::default(), animations_changed: EventRegistry::default(), agent_experiences: RwLock::new(None), experience_preferences: RwLock::new(None), region_experiences: RwLock::new(None), agent_preferences: RwLock::new(None), avatar_render_info: RwLock::new(None), viewer_benefits: RwLock::new(None), benefits: RwLock::new(None), last_nav_mesh_status: RwLock::new(None), product_info: RwLock::new(None), group_chat_sessions: Arc::new(RwLock::new(HashMap::new())), network: Mutex::new(None), raw_packet_subscription: Mutex::new(None), caps_subscriptions: Mutex::new(Vec::new()), movement: OnceLock::new(), chat_from_simulator: EventRegistry::default(), money_balance: EventRegistry::default(), money_balance_reply: EventRegistry::default(), agent_experiences_updated: EventRegistry::default(), experience_preferences_updated: EventRegistry::default(), region_experiences_updated: EventRegistry::default(), experience_info_received: EventRegistry::default(), agent_preferences_updated: EventRegistry::default(), avatar_render_info_updated: EventRegistry::default(), viewer_benefits_updated: EventRegistry::default(), product_info_updated: EventRegistry::default(), group_chat_joined: EventRegistry::default(), chat_session_member_added: EventRegistry::default(), chat_session_member_left: EventRegistry::default(), set_display_name_reply: EventRegistry::default(), nav_mesh_status_update: EventRegistry::default(), mute_list_updated: EventRegistry::default(), } } } impl AgentManagerInner { fn handle_raw_packet(&self, event: crate::network_manager::RawPacketReceivedEventArgs) { if let Some(movement) = self.movement.get() && movement .handle_raw_packet(event.packet_type, &event.data, event.simulator.clone()) .unwrap_or(false) { return; } let result = match event.packet_type { crate::packets::PacketType::ChatFromSimulator => { self.handle_chat_packet(&event.data, event.simulator) } crate::packets::PacketType::ImprovedInstantMessage => { self.handle_instant_message_packet(&event.data, event.simulator) } crate::packets::PacketType::MoneyBalanceReply => { self.handle_money_balance_packet(&event.data) } crate::packets::PacketType::AgentDataUpdate => { self.handle_agent_data_update_packet(&event.data) } crate::packets::PacketType::AvatarAnimation => { self.handle_avatar_animation_packet(&event.data) } crate::packets::PacketType::MuteListUpdate => { self.handle_mute_list_update_packet(&event.data, event.simulator) } crate::packets::PacketType::SendXferPacket => { self.handle_mute_xfer_packet(&event.data, event.simulator) } _ => Ok(()), }; // Packet callbacks are intentionally fault-isolated, matching the C# // event pipeline. Malformed wire data is rejected without panicking or // poisoning the network receive worker. let _ = result; } fn handle_mute_list_update_packet( &self, data: &[u8], simulator: Simulator, ) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let expected_agent = *self .agent_id .read() .unwrap_or_else(std::sync::PoisonError::into_inner); if expected_agent != UUID::zero() && packet.mute_data.agent_id != expected_agent { return Ok(()); } let filename = wire_string(packet.mute_data.filename); let xfer_id = UUID::random()?.get_u_long()?; { let mut transfers = mutex(&self.mute_xfers); transfers.retain(|_, transfer| transfer.started.elapsed() < Duration::from_mins(1)); transfers.insert(xfer_id, MuteXfer::default()); } let mut request = crate::packets::RequestXferPacket::new_with_constructor()?; request.xfer_id.id = xfer_id; request.xfer_id.filename = filename.into_bytes(); request.xfer_id.file_path = 4; request.xfer_id.delete_on_completion = true; request.xfer_id.use_big_packets = false; request.xfer_id.v_file_id = UUID::zero(); request.xfer_id.v_file_type = libremetaverse_types::AssetType::Unknown as i16; send_simulator_packet( &simulator, crate::packets::PacketType::RequestXfer, request.to_bytes_with_method()?, )?; Ok(()) } fn handle_mute_xfer_packet(&self, data: &[u8], simulator: Simulator) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let xfer_id = packet.xfer_id.id; let packet_number = packet.xfer_id.packet & 0x0fff_ffff; let is_last = packet.xfer_id.packet & 0x8000_0000 != 0; let mut completed = None; let confirmation; { let mut transfers = mutex(&self.mute_xfers); let Some(transfer) = transfers.get_mut(&xfer_id) else { return Ok(()); }; if packet_number != transfer.next_packet { confirmation = transfer.next_packet.saturating_sub(1); } else { confirmation = packet_number; let mut incoming = packet.data_packet.data; if packet_number == 0 { let size_bytes: [u8; 4] = incoming .get(..4) .ok_or(Error::InvalidOperation)? .try_into() .map_err(|_| Error::InvalidOperation)?; let size = usize::try_from(u32::from_le_bytes(size_bytes)) .map_err(|_| Error::Argument)?; if size > MAX_MUTE_LIST_BYTES { transfers.remove(&xfer_id); return Err(Error::Argument); } transfer.bytes = Vec::with_capacity(size); transfer.announced_size = Some(size); incoming.drain(..4); } let size = transfer.announced_size.ok_or(Error::InvalidOperation)?; if transfer.bytes.len().saturating_add(incoming.len()) > size { transfers.remove(&xfer_id); return Err(Error::InvalidOperation); } transfer.bytes.extend_from_slice(&incoming); transfer.next_packet = transfer.next_packet.saturating_add(1); if is_last { if transfer.bytes.len() != size { transfers.remove(&xfer_id); return Err(Error::InvalidOperation); } completed = Some(std::mem::take(&mut transfer.bytes)); transfers.remove(&xfer_id); } } } let mut confirm = crate::packets::ConfirmXferPacketPacket::new_with_constructor()?; confirm.xfer_id.id = xfer_id; confirm.xfer_id.packet = confirmation; send_simulator_packet( &simulator, crate::packets::PacketType::ConfirmXferPacket, confirm.to_bytes_with_method()?, )?; if let Some(bytes) = completed { let parsed = parse_mute_list(&wire_string(bytes)); *self .mute_list .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = parsed; self.mute_list_updated.emit(()); } Ok(()) } fn handle_chat_packet(&self, data: &[u8], simulator: Simulator) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let chat = packet.chat_data; self.chat_from_simulator.emit(ChatEventArgs::new( simulator, wire_string(chat.message), chat_audible_level(chat.audible)?, chat_type(chat.chat_type)?, chat_source_type(chat.source_type)?, wire_string(chat.from_name), chat.source_id, chat.owner_id, chat.position, )?); Ok(()) } fn handle_instant_message_packet( &self, data: &[u8], simulator: Simulator, ) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let block = packet.message_block; if block.from_group { self.group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .entry(block.id) .or_default(); } self.instant_message.emit(InstantMessageEventArgs::new( InstantMessage { binary_bucket: block.binary_bucket, dialog: instant_message_dialog(block.dialog)?, from_agent_id: packet.agent_data.agent_id, from_agent_name: wire_string(block.from_agent_name), group_im: block.from_group, im_session_id: block.id, message: wire_string(block.message), offline: instant_message_online(block.offline)?, parent_estate_id: block.parent_estate_id, position: block.position, region_id: block.region_id, timestamp: UNIX_EPOCH + Duration::from_secs(u64::from(block.timestamp)), to_agent_id: block.to_agent_id, }, Some(simulator), )?); Ok(()) } fn handle_money_balance_packet(&self, data: &[u8]) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let money = packet.money_data; let transaction = packet.transaction_info; *self .balance .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = money.money_balance; self.money_balance_reply .emit(MoneyBalanceReplyEventArgs::new( money.transaction_id, money.transaction_success, money.money_balance, money.square_meters_credit, money.square_meters_committed, wire_string(money.description), crate::TransactionInfo { amount: transaction.amount, dest_id: transaction.dest_id, is_dest_group: transaction.is_dest_group, is_source_group: transaction.is_source_group, item_description: wire_string(transaction.item_description), source_id: transaction.source_id, transaction_type: transaction.transaction_type, }, )?); self.money_balance .emit(BalanceEventArgs::new(money.money_balance)?); Ok(()) } fn handle_agent_data_update_packet(&self, data: &[u8]) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let update = packet.agent_data; let expected_id = self.expected_agent_id(); if expected_id != UUID::zero() && update.agent_id != expected_id { return Ok(()); } let first_name = wire_string(update.first_name); let last_name = wire_string(update.last_name); let group_title = wire_string(update.group_title); let group_name = wire_string(update.group_name); let group_powers = crate::GroupPowers(update.group_powers); self.first_name .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone_from(&first_name); self.last_name .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone_from(&last_name); *self .active_group .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = update.active_group_id; *self .active_group_powers .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = group_powers; self.agent_data_reply.emit(AgentDataReplyEventArgs::new( first_name, last_name, update.active_group_id, group_title, group_powers, group_name, )?); Ok(()) } fn handle_avatar_animation_packet(&self, data: &[u8]) -> Result<(), Error> { let packet = decode_raw_packet::(data)?; let expected_id = self.expected_agent_id(); if expected_id != UUID::zero() && packet.sender.id != expected_id { return Ok(()); } let animations = packet .animation_list .into_iter() .map(|entry| (entry.anim_id, entry.anim_sequence_id)) .collect::>(); self.signaled_animations .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone_from(&animations); self.animations_changed .emit(AnimationsChangedEventArgs::new(animations)?); Ok(()) } fn expected_agent_id(&self) -> UUID { let configured_id = *self .agent_id .read() .unwrap_or_else(std::sync::PoisonError::into_inner); if configured_id == UUID::zero() { mutex(&self.network) .as_ref() .map_or(UUID::zero(), crate::NetworkManager::native_agent_id) } else { configured_id } } pub(crate) fn native_ids(&self, network: &crate::NetworkManager) -> (UUID, UUID) { let agent_id = *self .agent_id .read() .unwrap_or_else(std::sync::PoisonError::into_inner); let session_id = *self .session_id .read() .unwrap_or_else(std::sync::PoisonError::into_inner); ( if agent_id == UUID::zero() { network.native_agent_id() } else { agent_id }, if session_id == UUID::zero() { network.native_session_id() } else { session_id }, ) } fn handle_caps_event( self: &Arc, client: Arc, name: &str, message: &dyn crate::interfaces::IMessage, simulator: Simulator, ) { if let Some(movement) = self.movement.get() && movement .handle_caps_event(name, message, simulator.clone()) .unwrap_or(false) { return; } let result = match name { "ChatterBoxInvitation" => self.handle_chat_invitation(client, message, simulator), "ChatterBoxSessionEventReply" => self.handle_chat_session_event_reply(client, message), "ChatterBoxSessionStartReply" => self.handle_chat_session_start_reply(message), "ChatterBoxSessionAgentListUpdates" => self.handle_chat_session_agent_updates(message), "SetDisplayNameReply" => self.handle_set_display_name_reply(message), "NavMeshStatusUpdate" => self.handle_nav_mesh_status_update(message, simulator), _ => Ok(()), }; // Event queue handlers are fault-isolated just like packet handlers. // A malformed event must not stop delivery of subsequent queue entries. let _ = result; } fn handle_chat_session_event_reply( &self, client: Arc, message: &dyn crate::interfaces::IMessage, ) -> Result<(), Error> { let mut reply = crate::messages::linden::ChatterboxSessionEventReplyMessage::new()?; reply.deserialize(message.serialize()?)?; if !reply.success { AgentManager::native_new(Some(client))?.native_join_group_chat(reply.session_id)?; } Ok(()) } fn handle_chat_session_start_reply( &self, message: &dyn crate::interfaces::IMessage, ) -> Result<(), Error> { let mut reply = crate::messages::linden::ChatterBoxSessionStartReplyMessage::new()?; reply.deserialize(message.serialize()?)?; if reply.success { self.group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .entry(reply.session_id) .or_default(); } self.group_chat_joined.emit(GroupChatJoinedEventArgs::new( reply.session_id, reply.session_name, reply.temp_session_id, reply.success, )?); Ok(()) } fn handle_chat_session_agent_updates( &self, message: &dyn crate::interfaces::IMessage, ) -> Result<(), Error> { let mut update = crate::messages::linden::ChatterBoxSessionAgentListUpdatesMessage::new()?; update.deserialize(message.serialize()?)?; let session_id = update.session_id; let mut added = Vec::new(); let mut left = Vec::new(); { let mut sessions = self .group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner); let members = sessions.entry(session_id).or_default(); for block in update.updates { let found = members .iter() .position(|member| member.avatar_key == block.agent_id); match block.transition.as_str() { "ENTER" if found.is_none() => added.push(block.agent_id), "LEAVE" => { if let Some(index) = found { members.remove(index); } left.push(block.agent_id); continue; } _ => {} } let member = ChatSessionMember { avatar_key: block.agent_id, can_voice_chat: block.can_voice_chat, is_moderator: block.is_moderator, mute_text: block.mute_text, mute_voice: block.mute_voice, }; if let Some(index) = found { members[index] = member; } else if block.transition == "ENTER" { members.push(member); } } } for agent_id in added { self.chat_session_member_added .emit(ChatSessionMemberAddedEventArgs::new(session_id, agent_id)?); } for agent_id in left { self.chat_session_member_left .emit(ChatSessionMemberLeftEventArgs::new(session_id, agent_id)?); } Ok(()) } fn handle_chat_invitation( self: &Arc, client: Arc, message: &dyn crate::interfaces::IMessage, simulator: Simulator, ) -> Result<(), Error> { let mut invitation = crate::messages::linden::ChatterBoxInvitationMessage::new()?; invitation.deserialize(message.serialize()?)?; // The reference deliberately ignores voice-only invitations. if invitation.voice { return Ok(()); } self.group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .entry(invitation.im_session_id) .or_default(); let args = InstantMessageEventArgs::new( InstantMessage { binary_bucket: invitation.binary_bucket, dialog: invitation.dialog, from_agent_id: invitation.from_agent_id, from_agent_name: invitation.from_agent_name, group_im: invitation.group_im, im_session_id: invitation.im_session_id, message: invitation.message, offline: invitation.offline, parent_estate_id: invitation.parent_estate_id, position: invitation.position, region_id: invitation.region_id, timestamp: invitation.timestamp, to_agent_id: invitation.to_agent_id, }, Some(simulator), )?; let session_id = args.im().im_session_id; let inner = Arc::clone(self); let fallback_args = args.clone(); let spawned = std::thread::Builder::new() .name("libremetaverse-chat-invitation".into()) .spawn(move || { if let (Ok(runtime), Ok(manager)) = ( tokio::runtime::Builder::new_current_thread() .enable_all() .build(), AgentManager::native_new(Some(client)), ) { // The C# handler catches acceptance failures and still // delivers the invitation to subscribers afterwards. let _ = runtime .block_on(manager.native_chatter_box_accept_invite(session_id, None)); } inner.instant_message.emit(args); }); if spawned.is_err() { self.instant_message.emit(fallback_args); } Ok(()) } fn handle_set_display_name_reply( &self, message: &dyn crate::interfaces::IMessage, ) -> Result<(), Error> { let mut reply = crate::messages::linden::SetDisplayNameReplyMessage::new()?; reply.deserialize(message.serialize()?)?; self.set_display_name_reply .emit(SetDisplayNameReplyEventArgs::new( reply.status, reply.reason, reply.display_name, )?); Ok(()) } fn handle_nav_mesh_status_update( &self, message: &dyn crate::interfaces::IMessage, simulator: Simulator, ) -> Result<(), Error> { let mut update = crate::messages::linden::NavMeshStatusUpdateMessage::new()?; update.deserialize(message.serialize()?)?; *self .last_nav_mesh_status .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(update.clone()); self.nav_mesh_status_update .emit(NavMeshStatusUpdateEventArgs::new(update, simulator)?); Ok(()) } } /// Native owner for the non-movement `AgentManager` surface. pub struct AgentManager { pub agent_state_status: Option, pub group_chat_sessions: Arc>>>, pub movement: AgentManagerAgentMovement, pub mute_list: Arc>>, pub signaled_animations: Arc>>, pub(crate) client: Arc, inner: Arc, } impl fmt::Debug for AgentManager { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("AgentManager") .field("disposed", &self.inner.disposed.load(Ordering::Acquire)) .field( "group_chat_session_count", &self .group_chat_sessions .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .len(), ) .finish_non_exhaustive() } } impl AgentManager { pub(crate) fn native_new(client: Option>) -> Result { let client = client.ok_or(Error::ArgumentNull)?; let inner = client.cached_agent_manager_inner().unwrap_or_else(|| { let inner = Arc::new(AgentManagerInner::default()); client.cache_agent_manager_inner(&inner); inner }); let network = { let mut network = mutex(&inner.network); network.get_or_insert_with(|| client.network()).clone() }; { let mut subscription = mutex(&inner.raw_packet_subscription); if subscription.is_none() { let weak = Arc::downgrade(&inner); *subscription = Some(network.subscribe_raw_packet(Arc::new(move |event| { if let Some(inner) = weak.upgrade() { inner.handle_raw_packet(event); } }))); } } { let mut subscriptions = mutex(&inner.caps_subscriptions); if subscriptions.is_empty() { for event_name in [ "ChatterBoxInvitation", "ChatterBoxSessionEventReply", "ChatterBoxSessionStartReply", "ChatterBoxSessionAgentListUpdates", "SetDisplayNameReply", "NavMeshStatusUpdate", "TeleportFinish", "TeleportFailed", "CrossedRegion", ] { let weak_inner = Arc::downgrade(&inner); let weak_client = Arc::downgrade(&client); subscriptions.push(network.subscribe_caps( event_name.into(), CapsEventQueueCallback::from_handler(move |name, message, simulator| { if let (Some(inner), Some(client)) = (weak_inner.upgrade(), weak_client.upgrade()) { inner.handle_caps_event(client, name, message, simulator); } }), )); } } } let movement_runtime = Arc::clone(inner.movement.get_or_init(|| { crate::agent_movement::AgentMovementRuntime::new( Arc::clone(&client), network.clone(), Arc::downgrade(&inner), ) })); Ok(Self { agent_state_status: None, group_chat_sessions: Arc::clone(&inner.group_chat_sessions), movement: AgentManagerAgentMovement::from_runtime(movement_runtime), mute_list: Arc::clone(&inner.mute_list), signaled_animations: Arc::clone(&inner.signaled_animations), client, inner, }) } pub(crate) fn native_subscribe_im( &self, handler: EventHandler, ) -> Subscription { self.inner.instant_message.subscribe(handler) } pub(crate) fn native_subscribe_agent_data_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.agent_data_reply.subscribe(handler) } pub(crate) fn native_subscribe_animations_changed( &self, handler: EventHandler, ) -> Subscription { self.inner.animations_changed.subscribe(handler) } pub(crate) fn native_dispose(&self) -> Result<(), Error> { if self.inner.disposed.swap(true, Ordering::AcqRel) { return Ok(()); } mutex(&self.inner.raw_packet_subscription).take(); mutex(&self.inner.caps_subscriptions).clear(); if let Some(movement) = self.inner.movement.get() { movement.stop(); } mutex(&self.inner.network).take(); mutex(&self.inner.mute_xfers).clear(); self.group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); self.mute_list .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); self.inner .active_gestures .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); self.signaled_animations .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); Ok(()) } pub(crate) fn native_subscribe_chat_from_simulator( &self, handler: EventHandler, ) -> Subscription { self.inner.chat_from_simulator.subscribe(handler) } pub(crate) fn native_subscribe_money_balance( &self, handler: EventHandler, ) -> Subscription { self.inner.money_balance.subscribe(handler) } pub(crate) fn native_subscribe_money_balance_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.money_balance_reply.subscribe(handler) } pub(crate) fn native_subscribe_agent_experiences_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.agent_experiences_updated.subscribe(handler) } pub(crate) fn native_subscribe_experience_preferences_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.experience_preferences_updated.subscribe(handler) } pub(crate) fn native_subscribe_region_experiences_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.region_experiences_updated.subscribe(handler) } pub(crate) fn native_subscribe_experience_info_received( &self, handler: EventHandler, ) -> Subscription { self.inner.experience_info_received.subscribe(handler) } pub(crate) fn native_subscribe_agent_preferences_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.agent_preferences_updated.subscribe(handler) } pub(crate) fn native_subscribe_avatar_render_info_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.avatar_render_info_updated.subscribe(handler) } pub(crate) fn native_subscribe_viewer_benefits_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.viewer_benefits_updated.subscribe(handler) } pub(crate) fn native_subscribe_product_info_updated( &self, handler: EventHandler, ) -> Subscription { self.inner.product_info_updated.subscribe(handler) } pub(crate) fn native_subscribe_mute_list_updated( &self, handler: EventHandler<()>, ) -> Subscription { self.inner.mute_list_updated.subscribe(handler) } pub(crate) fn native_subscribe_group_chat_joined( &self, handler: EventHandler, ) -> Subscription { self.inner.group_chat_joined.subscribe(handler) } pub(crate) fn native_subscribe_chat_session_member_added( &self, handler: EventHandler, ) -> Subscription { self.inner.chat_session_member_added.subscribe(handler) } pub(crate) fn native_subscribe_chat_session_member_left( &self, handler: EventHandler, ) -> Subscription { self.inner.chat_session_member_left.subscribe(handler) } pub(crate) fn native_subscribe_set_display_name_reply( &self, handler: EventHandler, ) -> Subscription { self.inner.set_display_name_reply.subscribe(handler) } pub(crate) fn native_subscribe_nav_mesh_status_update( &self, handler: EventHandler, ) -> Subscription { self.inner.nav_mesh_status_update.subscribe(handler) } pub(crate) fn native_last_nav_mesh_status( &self, ) -> Option { self.inner .last_nav_mesh_status .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_last_nav_mesh_status( &self, value: Option, ) { *self .inner .last_nav_mesh_status .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_agent_experience_list(&self) -> Option { self.inner .agent_experiences .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_agent_experience_list(&self, value: Option) { *self .inner .agent_experiences .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_experience_preferences(&self) -> Option { self.inner .experience_preferences .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_experience_preferences( &self, value: Option, ) { *self .inner .experience_preferences .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_last_region_experiences(&self) -> Option { self.inner .region_experiences .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_last_region_experiences( &self, value: Option, ) { *self .inner .region_experiences .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_avatar_render_info(&self) -> Option { self.inner .avatar_render_info .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_avatar_render_info(&self, value: Option) { *self .inner .avatar_render_info .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_viewer_benefits(&self) -> Option { self.inner .viewer_benefits .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_benefits(&self) -> crate::AccountLevelBenefits { self.inner .benefits .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() .or_else(|| self.client.network().native_account_level_benefits()) .unwrap_or_else(|| { crate::AccountLevelBenefits::new_with_osd_map( OSDMap::new_with_dictionary(HashMap::new()).expect("an empty OSD map is valid"), ) .expect("an empty benefit map is valid") }) } pub(crate) fn native_set_benefits(&self, value: crate::AccountLevelBenefits) { *self .inner .benefits .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value); } pub(crate) fn native_set_viewer_benefits(&self, value: Option) { *self .inner .viewer_benefits .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_product_info(&self) -> Option { self.inner .product_info .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_product_info(&self, value: Option) { *self .inner .product_info .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_first_name(&self) -> String { self.inner .first_name .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_first_name(&self, value: String) { *self .inner .first_name .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_last_name(&self) -> String { self.inner .last_name .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_active_group(&self) -> UUID { *self .inner .active_group .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } pub(crate) fn native_set_active_group(&self, value: UUID) { *self .inner .active_group .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_active_group_powers(&self) -> crate::GroupPowers { *self .inner .active_group_powers .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } pub(crate) fn native_set_active_group_powers(&self, value: crate::GroupPowers) { *self .inner .active_group_powers .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_set_last_name(&self, value: String) { *self .inner .last_name .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_agent_access(&self) -> String { self.inner .agent_access .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone() } pub(crate) fn native_set_agent_access(&self, value: String) { *self .inner .agent_access .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_hover_height(&self) -> f32 { *self .inner .hover_height .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } pub(crate) fn native_set_hover_height(&self, value: f32) { *self .inner .hover_height .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_balance(&self) -> i32 { *self .inner .balance .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } pub(crate) fn native_set_balance(&self, value: i32) { *self .inner .balance .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_agent_id(&self) -> UUID { let value = *self .inner .agent_id .read() .unwrap_or_else(std::sync::PoisonError::into_inner); if value == UUID::zero() { self.client.network().native_agent_id() } else { value } } pub(crate) fn native_set_agent_id(&self, value: UUID) { *self .inner .agent_id .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } pub(crate) fn native_session_id(&self) -> UUID { let value = *self .inner .session_id .read() .unwrap_or_else(std::sync::PoisonError::into_inner); if value == UUID::zero() { self.client.network().native_session_id() } else { value } } pub(crate) fn native_set_session_id(&self, value: UUID) { *self .inner .session_id .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = value; } fn send_packet( &self, packet_type: crate::packets::PacketType, data: Vec, ) -> Result<(), Error> { let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?; let simulator = self .client .network() .current_sim() .ok_or(Error::InvalidOperation)?; let do_zerocode = data .first() .is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0); simulator.native_send_packet_data(data, length, packet_type, do_zerocode) } pub(crate) fn native_chat( &self, message: String, channel: i32, type_: ChatType, split_large_messages: Option, ) -> Result<(), Error> { for (packet_type, data) in self.native_chat_payloads(message, channel, type_, split_large_messages)? { self.send_packet(packet_type, data)?; } Ok(()) } fn native_chat_payloads( &self, message: String, channel: i32, type_: ChatType, split_large_messages: Option, ) -> Result)>, Error> { if channel < 0 { let message = split_utf8(&message, MAX_DIALOG_LABEL_BYTES, 1) .into_iter() .next() .unwrap_or_default(); let mut packet = crate::packets::ScriptDialogReplyPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.object_id = self.native_agent_id(); packet.data.chat_channel = channel; packet.data.button_index = 0; packet.data.button_label = message.into_bytes(); return Ok(vec![( crate::packets::PacketType::ScriptDialogReply, packet.to_bytes_with_method()?, )]); } let max_parts = if split_large_messages.unwrap_or(true) { usize::MAX } else { 1 }; split_utf8(&message, MAX_CHAT_BYTES, max_parts) .into_iter() .map(|message| { let mut packet = crate::packets::ChatFromViewerPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.chat_data.channel = channel; packet.chat_data.message = message.into_bytes(); packet.chat_data.type_ = type_ as u8; Ok(( crate::packets::PacketType::ChatFromViewer, packet.to_bytes_with_method()?, )) }) .collect() } pub(crate) fn native_instant_message( &self, target: UUID, message: String, session_id: Option, ) -> Result<(), Error> { let session_id = session_id.unwrap_or_else(|| { if self.native_agent_id() == target { self.native_agent_id() } else { UUID::bitxor(target, self.native_agent_id()) } }); self.native_instant_message_full( self.native_name(), target, message, session_id, InstantMessageDialog::MessageFromAgent, InstantMessageOnline::Offline, Vector3::zero(), UUID::zero(), Vec::new(), ) } #[allow(clippy::too_many_arguments)] pub(crate) fn native_instant_message_full( &self, from_name: String, target: UUID, message: String, mut im_session_id: UUID, dialog: InstantMessageDialog, offline: InstantMessageOnline, _position: Vector3, region_id: UUID, binary_bucket: Vec, ) -> Result<(), Error> { // Match the reference's safety guard. Message text is intentionally not logged. if target == UUID::zero() { return Ok(()); } if im_session_id == UUID::zero() || im_session_id == self.native_agent_id() { im_session_id = if self.native_agent_id() == target { self.native_agent_id() } else { UUID::bitxor(target, self.native_agent_id()) }; } for message in split_utf8(&message, MAX_CHAT_BYTES, usize::MAX) { let mut packet = crate::packets::ImprovedInstantMessagePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.message_block.dialog = dialog as u8; packet.message_block.from_agent_name = from_name.as_bytes().to_vec(); packet.message_block.from_group = false; packet.message_block.id = im_session_id; packet.message_block.message = message.into_bytes(); packet.message_block.offline = offline as u8; packet.message_block.to_agent_id = target; packet .message_block .binary_bucket .clone_from(&binary_bucket); // The C# implementation ignores its position argument and sends zero. packet.message_block.position = Vector3::zero(); packet.message_block.region_id = region_id; self.send_packet( crate::packets::PacketType::ImprovedInstantMessage, packet.to_bytes_with_method()?, )?; } Ok(()) } pub(crate) fn native_instant_message_conference( &self, from_name: String, target: UUID, message: String, session_id: UUID, conference_ids: Vec, ) -> Result<(), Error> { let mut bucket = Vec::with_capacity(conference_ids.len().saturating_mul(16)); for id in conference_ids { bucket.extend_from_slice(&id.get_bytes()?); } self.native_instant_message_full( from_name, target, message, session_id, InstantMessageDialog::MessageFromAgent, InstantMessageOnline::Offline, Vector3::zero(), UUID::zero(), bucket, ) } pub(crate) fn native_instant_message_group( &self, from_name: Option, group_id: UUID, message: String, ) -> Result<(), Error> { if !self .group_chat_sessions .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .contains_key(&group_id) { return Ok(()); } let from_name = from_name.unwrap_or_else(|| self.native_name()); self.native_instant_message_full( from_name, group_id, message, group_id, InstantMessageDialog::SessionSend, InstantMessageOnline::Online, Vector3::zero(), UUID::zero(), vec![0], ) } pub(crate) fn native_is_group_message(&self, message: InstantMessage) -> bool { message.group_im || self .group_chat_sessions .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .contains_key(&message.im_session_id) } pub(crate) fn native_group_invite_respond( &self, group_id: UUID, im_session_id: UUID, accept: bool, ) -> Result<(), Error> { self.native_instant_message_full( self.native_name(), group_id, String::new(), im_session_id, if accept { InstantMessageDialog::GroupInvitationAccept } else { InstantMessageDialog::GroupInvitationDecline }, InstantMessageOnline::Offline, Vector3::zero(), UUID::zero(), Vec::new(), ) } pub(crate) fn native_join_group_chat(&self, group_id: UUID) -> Result<(), Error> { self.send_session_control(group_id, InstantMessageDialog::SessionGroupStart) } pub(crate) fn native_leave_group_chat(&self, group_id: UUID) -> Result<(), Error> { self.send_session_control(group_id, InstantMessageDialog::SessionDrop)?; self.group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .remove(&group_id); Ok(()) } fn send_session_control( &self, group_id: UUID, dialog: InstantMessageDialog, ) -> Result<(), Error> { let mut packet = crate::packets::ImprovedInstantMessagePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.message_block.dialog = dialog as u8; packet.message_block.from_agent_name = self.native_name().into_bytes(); packet.message_block.id = group_id; packet.message_block.to_agent_id = group_id; packet.message_block.position = Vector3::zero(); self.send_packet( crate::packets::PacketType::ImprovedInstantMessage, packet.to_bytes_with_method()?, ) } pub(crate) fn native_reply_to_script_dialog( &self, channel: i32, button_index: i32, button_label: String, object_id: UUID, ) -> Result<(), Error> { let mut packet = crate::packets::ScriptDialogReplyPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.chat_channel = channel; packet.data.button_index = button_index; packet.data.button_label = button_label.into_bytes(); packet.data.object_id = object_id; self.send_packet( crate::packets::PacketType::ScriptDialogReply, packet.to_bytes_with_method()?, ) } pub(crate) fn native_activate_gesture( &self, inventory_id: UUID, asset_id: UUID, ) -> Result<(), Error> { let mut packet = crate::packets::ActivateGesturesPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); let mut block = crate::packets::ActivateGesturesPacketDataBlock::new_with_constructor()?; block.item_id = inventory_id; block.asset_id = asset_id; packet.data = vec![block]; self.send_packet( crate::packets::PacketType::ActivateGestures, packet.to_bytes_with_method()?, )?; self.inner .active_gestures .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .insert(inventory_id, asset_id); Ok(()) } pub(crate) fn native_deactivate_gesture(&self, inventory_id: UUID) -> Result<(), Error> { let mut packet = crate::packets::DeactivateGesturesPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); let mut block = crate::packets::DeactivateGesturesPacketDataBlock::new_with_constructor()?; block.item_id = inventory_id; packet.data = vec![block]; self.send_packet( crate::packets::PacketType::DeactivateGestures, packet.to_bytes_with_method()?, )?; self.inner .active_gestures .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .remove(&inventory_id); Ok(()) } pub(crate) async fn native_play_gesture( &self, gesture_id: UUID, cancellation_token: Option, ) -> Result<(), Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; tokio::select! { result = self.play_gesture_inner(gesture_id, cancellation.clone()) => result, () = cancellation.cancelled() => Err(Error::Cancelled), () = tokio::time::sleep(GESTURE_TIMEOUT) => Err(Error::Cancelled), } } async fn play_gesture_inner( &self, gesture_id: UUID, cancellation: libremetaverse_types::compat::CancellationToken, ) -> Result<(), Error> { let Some(data) = self .fetch_gesture_asset(gesture_id, cancellation.clone()) .await? else { return Ok(()); }; let mut gesture = crate::assets::AssetGesture::new_with_uuid_bytes(gesture_id, data)?; if !gesture.decode()? { return Ok(()); } for step in gesture.sequence { cancellation.throw_if_cancellation_requested()?; match step { crate::assets::GestureStep::Chat(step) => { let (channel, text) = gesture_chat_channel(&step.text); self.native_chat(text, channel, ChatType::Normal, None)?; } crate::assets::GestureStep::Animation(step) => { if step.animation_start { if self .signaled_animations .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .contains_key(&step.id) { self.native_animation_stop(step.id, true)?; } self.native_animation_start(step.id, true)?; } else { self.native_animation_stop(step.id, true)?; } } crate::assets::GestureStep::Sound(step) => { self.native_play_gesture_sound(step.id)?; } crate::assets::GestureStep::Wait(step) => { if step.wait_for_time { let seconds = f64::from(step.wait_time.max(0.0)); tokio::select! { () = tokio::time::sleep(Duration::from_secs_f64(seconds)) => {}, () = cancellation.cancelled() => return Err(Error::Cancelled), } } // Preserve the reference behavior: WaitForAnimation is // recognized but intentionally has no blocking behavior. } crate::assets::GestureStep::EOF(_) => break, } } Ok(()) } async fn fetch_gesture_asset( &self, gesture_id: UUID, cancellation: libremetaverse_types::compat::CancellationToken, ) -> Result>, Error> { let simulator = self .client .network() .current_sim() .ok_or(Error::InvalidOperation)?; if let Some(viewer_asset) = simulator .native_caps() .and_then(|caps| caps.capability_uri("ViewerAsset".into()).ok().flatten()) { let separator = if viewer_asset.0.contains('?') { '&' } else { '?' }; let uri = Uri(format!( "{}{separator}gesture_id={gesture_id}", viewer_asset.0 )); let response = self .client .http_caps_client() .get(uri, cancellation, None) .await; return match response { Ok((response, data)) if response.is_success_status_code() => Ok(Some(data)), Err(Error::Cancelled) => Err(Error::Cancelled), Ok(_) | Err(_) => Ok(None), }; } self.fetch_gesture_asset_udp(simulator, gesture_id, cancellation) .await } async fn fetch_gesture_asset_udp( &self, simulator: Simulator, gesture_id: UUID, cancellation: libremetaverse_types::compat::CancellationToken, ) -> Result>, Error> { let transfer_id = UUID::random()?; let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); let _subscription = self .client .network() .subscribe_raw_packet(Arc::new(move |event| { if matches!( event.packet_type, crate::packets::PacketType::TransferInfo | crate::packets::PacketType::TransferPacket ) { let _ = sender.send(event); } })); let mut request = crate::packets::TransferRequestPacket::new_with_constructor()?; request.transfer_info.channel_type = crate::ChannelType::Asset as i32; request.transfer_info.priority = 101.0; request.transfer_info.source_type = crate::SourceType::Asset as i32; request.transfer_info.transfer_id = transfer_id; request.transfer_info.params = gesture_id.get_bytes()?; request .transfer_info .params .extend_from_slice(&(libremetaverse_types::AssetType::Gesture as i32).to_le_bytes()); let data = request.to_bytes_with_method()?; let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?; simulator.native_send_packet_data( data, length, crate::packets::PacketType::TransferRequest, false, )?; let mut abort = UdpAssetAbortGuard::new(simulator, transfer_id); let mut size = None; let mut chunks = BTreeMap::>::new(); loop { let event = tokio::select! { event = receiver.recv() => event.ok_or(Error::InvalidOperation)?, () = cancellation.cancelled() => return Err(Error::Cancelled), }; match event.packet_type { crate::packets::PacketType::TransferInfo => { let packet = decode_raw_packet::(&event.data)?; if packet.transfer_info.transfer_id != transfer_id { continue; } if packet.transfer_info.status != crate::StatusCode::OK as i32 { abort.complete(); return Ok(None); } let announced = usize::try_from(packet.transfer_info.size).map_err(|_| Error::Argument)?; if announced > MAX_GESTURE_ASSET_BYTES { return Err(Error::Argument); } size = Some(announced); } crate::packets::PacketType::TransferPacket => { let packet = decode_raw_packet::(&event.data)?; if packet.transfer_data.transfer_id != transfer_id || packet.transfer_data.packet < 0 { continue; } chunks .entry(packet.transfer_data.packet) .or_insert(packet.transfer_data.data); } _ => continue, } let Some(expected_size) = size else { continue; }; let mut assembled = Vec::with_capacity(expected_size); for packet_number in 0_i32.. { let Some(chunk) = chunks.get(&packet_number) else { break; }; let remaining = expected_size.saturating_sub(assembled.len()); assembled.extend_from_slice(&chunk[..chunk.len().min(remaining)]); if assembled.len() == expected_size { abort.complete(); return Ok(Some(assembled)); } } if expected_size == 0 { abort.complete(); return Ok(Some(Vec::new())); } } } fn native_play_gesture_sound(&self, sound_id: UUID) -> Result<(), Error> { if sound_id == UUID::zero() { return Ok(()); } let simulator = self .client .network() .current_sim() .ok_or(Error::InvalidOperation)?; let mut packet = crate::packets::SoundTriggerPacket::new_with_constructor()?; packet.sound_data.sound_id = sound_id; packet.sound_data.handle = simulator.handle; packet.sound_data.position = Vector3::zero(); packet.sound_data.gain = 1.0; self.send_packet( crate::packets::PacketType::SoundTrigger, packet.to_bytes_with_method()?, ) } pub(crate) fn native_active_gestures(&self) -> RwLock> { RwLock::new( self.inner .active_gestures .read() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(), ) } pub(crate) fn native_request_balance(&self) -> Result<(), Error> { let mut packet = crate::packets::MoneyBalanceRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.money_data.transaction_id = UUID::zero(); self.send_packet( crate::packets::PacketType::MoneyBalanceRequest, packet.to_bytes_with_method()?, ) } pub(crate) fn native_animation_start( &self, animation: UUID, reliable: bool, ) -> Result<(), Error> { self.native_animate(HashMap::from([(animation, true)]), reliable) } pub(crate) fn native_animation_stop( &self, animation: UUID, reliable: bool, ) -> Result<(), Error> { self.native_animate(HashMap::from([(animation, false)]), reliable) } pub(crate) fn native_animate( &self, animations: HashMap, reliable: bool, ) -> Result<(), Error> { let mut packet = crate::packets::AgentAnimationPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.animation_list = animations .iter() .map(|(animation, start)| { let mut block = crate::packets::AgentAnimationPacketAnimationListBlock::new_with_constructor()?; block.anim_id = *animation; block.start_anim = *start; Ok(block) }) .collect::, Error>>()?; packet.physical_avatar_event_list = Vec::new(); let mut data = packet.to_bytes_with_method()?; let flags = data.first_mut().ok_or(Error::Argument)?; if reliable { *flags |= crate::Helpers::MSG_RELIABLE; } else { *flags &= !crate::Helpers::MSG_RELIABLE; } self.send_packet(crate::packets::PacketType::AgentAnimation, data)?; let mut signaled = self .signaled_animations .write() .unwrap_or_else(std::sync::PoisonError::into_inner); for (animation, start) in animations { if start { signaled.insert(animation, 1); } else { signaled.remove(&animation); } } Ok(()) } pub(crate) fn native_request_mute_list(&self) -> Result<(), Error> { let mut packet = crate::packets::MuteListRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.mute_data.mute_crc = 0; self.send_packet( crate::packets::PacketType::MuteListRequest, packet.to_bytes_with_method()?, ) } pub(crate) fn native_update_mute_list_entry( &self, type_: crate::MuteType, id: UUID, name: String, flags: crate::MuteFlags, ) -> Result<(), Error> { let mut packet = crate::packets::UpdateMuteListEntryPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.mute_data.mute_type = type_ as i32; packet.mute_data.mute_id = id; packet.mute_data.mute_name = name.as_bytes().to_vec(); packet.mute_data.mute_flags = u32::try_from(flags.0).map_err(|_| Error::Argument)?; self.send_packet( crate::packets::PacketType::UpdateMuteListEntry, packet.to_bytes_with_method()?, )?; self.mute_list .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .insert( format!("{id}|{name}"), MuteEntry { flags, id, name, type_, }, ); self.inner.mute_list_updated.emit(()); Ok(()) } pub(crate) fn native_remove_mute_list_entry( &self, id: UUID, name: String, ) -> Result<(), Error> { let mut packet = crate::packets::RemoveMuteListEntryPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.mute_data.mute_id = id; packet.mute_data.mute_name = name.as_bytes().to_vec(); self.send_packet( crate::packets::PacketType::RemoveMuteListEntry, packet.to_bytes_with_method()?, )?; if self .mute_list .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .remove(&format!("{id}|{name}")) .is_some() { self.inner.mute_list_updated.emit(()); } Ok(()) } pub(crate) fn native_give_money( &self, target: UUID, amount: i32, description: String, type_: MoneyTransactionType, flags: TransactionFlags, ) -> Result<(), Error> { let mut packet = crate::packets::MoneyTransferRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.money_data.description = description.into_bytes(); packet.money_data.dest_id = target; packet.money_data.source_id = self.native_agent_id(); packet.money_data.transaction_type = type_ as i32; packet.money_data.flags = flags.0; packet.money_data.amount = amount; self.send_packet( crate::packets::PacketType::MoneyTransferRequest, packet.to_bytes_with_method()?, ) } pub(crate) fn native_give_avatar_money( &self, target: UUID, amount: i32, description: String, ) -> Result<(), Error> { self.native_give_money( target, amount, description, MoneyTransactionType::Gift, TransactionFlags::NONE, ) } pub(crate) fn native_give_group_money( &self, target: UUID, amount: i32, description: String, ) -> Result<(), Error> { self.native_give_money( target, amount, description, MoneyTransactionType::Gift, TransactionFlags::DEST_GROUP, ) } pub(crate) fn native_give_object_money( &self, target: UUID, amount: i32, object_name: String, ) -> Result<(), Error> { self.native_give_money( target, amount, object_name, MoneyTransactionType::PayObject, TransactionFlags::NONE, ) } pub(crate) fn native_pay_upload_fee( &self, amount: Option, description: String, ) -> Result<(), Error> { let benefit_cost = self.native_viewer_benefits().map_or(0, |benefits| { benefits.account_level_benefits.texture_upload_cost }); self.native_give_money( UUID::zero(), amount.unwrap_or_else(|| { if benefit_cost > 0 { benefit_cost } else { self.client.settings_ref().upload_cost } }), description, MoneyTransactionType::UploadCharge, TransactionFlags::NONE, ) } pub(crate) fn native_viewer_effect( &self, source_avatar: UUID, target_object: UUID, global_offset: Vector3d, color: Color4, duration: f32, effect_id: UUID, effect_type: EffectType, subtype: Option, ) -> Result<(), Error> { let mut type_data = Vec::with_capacity(if subtype.is_some() { 57 } else { 56 }); type_data.extend_from_slice(&source_avatar.get_bytes()?); type_data.extend_from_slice(&target_object.get_bytes()?); type_data.extend_from_slice(&global_offset.get_bytes()?); if let Some(subtype) = subtype { type_data.push(subtype); } let mut packet = crate::packets::ViewerEffectPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); let mut effect = crate::packets::ViewerEffectPacketEffectBlock::new_with_constructor()?; effect.agent_id = self.native_agent_id(); effect.color = color.get_bytes_with_method()?; effect.duration = duration; effect.id = effect_id; effect.type_ = effect_type as u8; effect.type_data = type_data; packet.effect = vec![effect]; self.send_packet( crate::packets::PacketType::ViewerEffect, packet.to_bytes_with_method()?, ) } pub(crate) fn native_point_at_effect( &self, source_avatar: UUID, target_object: UUID, global_offset: Vector3d, type_: PointAtType, effect_id: UUID, ) -> Result<(), Error> { let duration = if type_ == PointAtType::Clear { 0.0 } else { f32::MAX / 4.0 }; self.native_viewer_effect( source_avatar, target_object, global_offset, Color4::default(), duration, effect_id, EffectType::PointAt, Some(type_ as u8), ) } pub(crate) fn native_look_at_effect( &self, source_avatar: UUID, target_object: UUID, global_offset: Vector3d, type_: LookAtType, effect_id: UUID, ) -> Result<(), Error> { let duration = match type_ { LookAtType::Clear | LookAtType::FreeLook => 2.0, LookAtType::Hover => 1.0, LookAtType::Idle => 3.0, LookAtType::AutoListen | LookAtType::Respond => 4.0, LookAtType::None | LookAtType::Select | LookAtType::Focus | LookAtType::Mouselook => { f32::MAX / 2.0 } LookAtType::Conversation => 0.0, }; self.native_viewer_effect( source_avatar, target_object, global_offset, Color4::default(), duration, effect_id, EffectType::LookAt, Some(type_ as u8), ) } pub(crate) fn native_name(&self) -> String { let first = self.native_first_name(); let last = self.native_last_name(); match (first.is_empty(), last.is_empty()) { (false, false) => format!("{first} {last}"), (false, true) => first, (true, false) => last, (true, true) => String::new(), } } pub(crate) fn native_update_profile_udp( &self, profile: crate::AvatarAvatarProperties, ) -> Result<(), Error> { let mut packet = crate::packets::AvatarPropertiesUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.properties_data.about_text = profile.about_text.into_bytes(); packet.properties_data.allow_publish = profile.flags.0 & crate::ProfileFlags::ALLOW_PUBLISH.0 != 0; packet.properties_data.fl_about_text = profile.first_life_text.into_bytes(); packet.properties_data.fl_image_id = profile.first_life_image; packet.properties_data.image_id = profile.profile_image; packet.properties_data.mature_publish = profile.flags.0 & crate::ProfileFlags::MATURE_PUBLISH.0 != 0; packet.properties_data.profile_url = profile.profile_url.into_bytes(); self.send_packet( crate::packets::PacketType::AvatarPropertiesUpdate, packet.to_bytes_with_method()?, ) } fn profile_payload(profile: &crate::AvatarAvatarProperties) -> Result { OSDMap::new_with_dictionary(HashMap::from([ ( "sl_about_text".into(), OSD::String(profile.about_text.clone()), ), ( "fl_about_text".into(), OSD::String(profile.first_life_text.clone()), ), ( "profile_url".into(), OSD::String(profile.profile_url.clone()), ), ( "allow_publish".into(), OSD::Boolean(profile.flags.0 & crate::ProfileFlags::ALLOW_PUBLISH.0 != 0), ), ("sl_image_id".into(), OSD::UUID(profile.profile_image)), ("fl_image_id".into(), OSD::UUID(profile.first_life_image)), ])) } async fn put_agent_profile( &self, target: UUID, payload: OSDMap, cancellation_token: Option, ) -> Result<(), Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let Some(capability) = self.native_capability("AgentProfile", &cancellation)? else { return Ok(()); }; let uri = Uri(format!("{}/{}", capability.0.trim_end_matches('/'), target)); let (response, _) = self .client .http_caps_client() .put_with_uri_osd_format_osd_cancellation_token_i_progress( uri, libremetaverse_structured_data::OSDFormat::Xml, OSD::Map(payload.snapshot()), cancellation, None, ) .await?; if response.is_success_status_code() { Ok(()) } else { Err(Error::InvalidOperation) } } pub(crate) async fn native_update_profile_async( &self, profile: crate::AvatarAvatarProperties, cancellation_token: Option, ) -> Result<(), Error> { self.put_agent_profile( self.native_agent_id(), Self::profile_payload(&profile)?, cancellation_token, ) .await } pub(crate) fn native_update_profile( &self, profile: crate::AvatarAvatarProperties, ) -> Result<(), Error> { let capability_available = self .client .network() .current_sim() .and_then(|simulator| simulator.native_caps()) .and_then(|caps| caps.capability_uri("AgentProfile".into()).ok().flatten()) .is_some(); if !capability_available { return self.native_update_profile_udp(profile); } let client = Arc::clone(&self.client); let target = self.native_agent_id(); let payload = Self::profile_payload(&profile)?; std::thread::Builder::new() .name("libremetaverse-profile-update".into()) .spawn(move || { let Ok(runtime) = tokio::runtime::Builder::new_current_thread() .enable_all() .build() else { return; }; let Ok(manager) = AgentManager::native_new(Some(client)) else { return; }; let _ = runtime.block_on(manager.put_agent_profile(target, payload, None)); }) .map(|_| ()) .map_err(|_| Error::InvalidOperation) } fn native_update_profile_notes_udp(&self, target: UUID, notes: String) -> Result<(), Error> { let mut packet = crate::packets::AvatarNotesUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.target_id = target; packet.data.notes = notes.into_bytes(); self.send_packet( crate::packets::PacketType::AvatarNotesUpdate, packet.to_bytes_with_method()?, ) } pub(crate) async fn native_update_profile_notes_async( &self, target: UUID, notes: String, cancellation_token: Option, ) -> Result<(), Error> { self.put_agent_profile( target, OSDMap::new_with_dictionary(HashMap::from([("notes".into(), OSD::String(notes))]))?, cancellation_token, ) .await } pub(crate) fn native_update_profile_notes( &self, target: UUID, notes: String, ) -> Result<(), Error> { let capability_available = self .client .network() .current_sim() .and_then(|simulator| simulator.native_caps()) .and_then(|caps| caps.capability_uri("AgentProfile".into()).ok().flatten()) .is_some(); if !capability_available { return self.native_update_profile_notes_udp(target, notes); } let client = Arc::clone(&self.client); let payload = OSDMap::new_with_dictionary(HashMap::from([("notes".into(), OSD::String(notes))]))?; std::thread::Builder::new() .name("libremetaverse-profile-notes-update".into()) .spawn(move || { let Ok(runtime) = tokio::runtime::Builder::new_current_thread() .enable_all() .build() else { return; }; let Ok(manager) = AgentManager::native_new(Some(client)) else { return; }; let _ = runtime.block_on(manager.put_agent_profile(target, payload, None)); }) .map(|_| ()) .map_err(|_| Error::InvalidOperation) } pub(crate) fn native_update_interests( &self, interests: crate::AvatarInterests, ) -> Result<(), Error> { let mut packet = crate::packets::AvatarInterestsUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.properties_data.languages_text = interests.languages_text.into_bytes(); packet.properties_data.skills_mask = interests.skills_mask; packet.properties_data.skills_text = interests.skills_text.into_bytes(); packet.properties_data.want_to_mask = interests.want_to_mask; packet.properties_data.want_to_text = interests.want_to_text.into_bytes(); self.send_packet( crate::packets::PacketType::AvatarInterestsUpdate, packet.to_bytes_with_method()?, ) } #[allow(clippy::too_many_arguments)] pub(crate) fn native_pick_info_update( &self, pick_id: UUID, top_pick: bool, parcel_id: UUID, name: String, global_position: Vector3d, texture_id: UUID, description: String, ) -> Result<(), Error> { let mut packet = crate::packets::PickInfoUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.pick_id = pick_id; packet.data.desc = description.into_bytes(); packet.data.creator_id = self.native_agent_id(); packet.data.top_pick = top_pick; packet.data.parcel_id = parcel_id; packet.data.name = name.into_bytes(); packet.data.snapshot_id = texture_id; packet.data.pos_global = global_position; packet.data.sort_order = 0; packet.data.enabled = false; self.send_packet( crate::packets::PacketType::PickInfoUpdate, packet.to_bytes_with_method()?, ) } pub(crate) fn native_pick_delete(&self, pick_id: UUID) -> Result<(), Error> { let mut packet = crate::packets::PickDeletePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.pick_id = pick_id; self.send_packet( crate::packets::PacketType::PickDelete, packet.to_bytes_with_method()?, ) } #[allow(clippy::too_many_arguments)] pub(crate) fn native_update_classified_info( &self, classified_id: UUID, category: crate::DirectoryManagerClassifiedCategories, snapshot_id: UUID, price: i32, position: Vector3d, name: String, description: String, auto_renew: bool, ) -> Result<(), Error> { let mut packet = crate::packets::ClassifiedInfoUpdatePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.classified_id = classified_id; packet.data.category = category as u32; packet.data.parcel_id = UUID::zero(); packet.data.parent_estate = 0; packet.data.snapshot_id = snapshot_id; packet.data.pos_global = position; packet.data.classified_flags = if auto_renew { 32 } else { 0 }; packet.data.price_for_listing = price; packet.data.name = name.into_bytes(); packet.data.desc = description.into_bytes(); self.send_packet( crate::packets::PacketType::ClassifiedInfoUpdate, packet.to_bytes_with_method()?, ) } pub(crate) fn native_delete_classified(&self, classified_id: UUID) -> Result<(), Error> { let mut packet = crate::packets::ClassifiedDeletePacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); packet.data.classified_id = classified_id; self.send_packet( crate::packets::PacketType::ClassifiedDelete, packet.to_bytes_with_method()?, ) } async fn get_map( &self, uri: Uri, cancellation: libremetaverse_types::compat::CancellationToken, ) -> Result, Error> { cancellation.throw_if_cancellation_requested()?; let response = self .client .http_caps_client() .get(uri, cancellation, None) .await; let (response, data) = match response { Ok(value) => value, Err(Error::Cancelled) => return Err(Error::Cancelled), Err(_) => return Ok(None), }; if !response.is_success_status_code() { return Ok(None); } match OSDParser::deserialize_with_bytes(data) { Ok(OSD::Map(map)) => OSDMap::new_with_dictionary(map).map(Some), _ => Ok(None), } } async fn get_capability_map( &self, capability: &str, suffix: &str, cancellation_token: Option, ) -> Result, Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); let Some(uri) = self.native_capability(capability, &cancellation)? else { return Ok(None); }; self.get_map(Uri(format!("{}{suffix}", uri.0)), cancellation) .await } async fn post_capability_map( &self, capability: &str, payload: OSDMap, cancellation_token: Option, ) -> Result, Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); let Some(uri) = self.native_capability(capability, &cancellation)? else { return Ok(None); }; let response = self .client .http_caps_client() .post_with_uri_osd_format_osd_cancellation_token_i_progress( uri, libremetaverse_structured_data::OSDFormat::Xml, OSD::Map(payload.snapshot()), cancellation, None, ) .await; let (response, data) = match response { Ok(value) => value, Err(Error::Cancelled) => return Err(Error::Cancelled), Err(_) => return Ok(None), }; if !response.is_success_status_code() { return Ok(None); } if data.is_empty() { return OSDMap::new_with_dictionary(HashMap::new()).map(Some); } match OSDParser::deserialize_with_bytes(data) { Ok(OSD::Map(map)) => OSDMap::new_with_dictionary(map).map(Some), _ => Ok(None), } } async fn get_experience_list( &self, capability: &str, suffix: &str, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map(capability, suffix, cancellation_token) .await? else { return Ok(None); }; let mut result = ExperienceListMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } Ok(Some(result)) } pub(crate) async fn native_get_agent_experiences( &self, cancellation_token: Option, ) -> Result, Error> { let result = self .get_experience_list("AgentExperiences", "", cancellation_token) .await?; if let Some(message) = result.clone() { self.native_set_agent_experience_list(Some(message.clone())); self.inner .agent_experiences_updated .emit(AgentExperiencesEventArgs::new(message)?); } Ok(result) } pub(crate) async fn native_get_admin_experiences( &self, cancellation_token: Option, ) -> Result, Error> { self.get_experience_list("GetAdminExperiences", "", cancellation_token) .await } pub(crate) async fn native_get_creator_experiences( &self, cancellation_token: Option, ) -> Result, Error> { self.get_experience_list("GetCreatorExperiences", "", cancellation_token) .await } pub(crate) async fn native_get_group_experiences( &self, group_id: UUID, cancellation_token: Option, ) -> Result, Error> { self.get_experience_list( "GroupExperiences", &format!("?{group_id}"), cancellation_token, ) .await } async fn get_experience_preferences_for_capability( &self, capability: &str, suffix: &str, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map(capability, suffix, cancellation_token) .await? else { return Ok(None); }; let mut result = ExperiencePreferencesMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } self.native_set_experience_preferences(Some(result.clone())); self.inner .experience_preferences_updated .emit(ExperiencePreferencesEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_get_agent_experience_permissions( &self, cancellation_token: Option, ) -> Result, Error> { self.get_experience_preferences_for_capability("GetExperiences", "", cancellation_token) .await } pub(crate) async fn native_get_experience_preferences( &self, cancellation_token: Option, ) -> Result, Error> { self.get_experience_preferences_for_capability( "ExperiencePreferences", "", cancellation_token, ) .await } pub(crate) async fn native_set_experience_preferences_async( &self, preferences: ExperiencePreferencesMessage, cancellation_token: Option, ) -> Result<(), Error> { let payload = preferences.serialize()?; let _ = self .post_capability_map("ExperiencePreferences", payload, cancellation_token) .await?; Ok(()) } pub(crate) async fn native_get_experience_permission( &self, experience_id: UUID, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map( "ExperiencePreferences", &format!("?{experience_id}"), cancellation_token, ) .await? else { return Ok(None); }; Self::extract_experience_permission(map, experience_id).map(Some) } pub(crate) async fn native_set_experience_permission( &self, experience_id: UUID, permission: String, cancellation_token: Option, ) -> Result { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let Some(uri) = self.native_capability("ExperiencePreferences", &cancellation)? else { return Ok(false); }; let payload = OSD::Map(HashMap::from([( experience_id.to_string(), OSD::Map(HashMap::from([( "permission".into(), OSD::String(permission), )])), )])); let response = self .client .http_caps_client() .put_with_uri_osd_format_osd_cancellation_token_i_progress( uri, libremetaverse_structured_data::OSDFormat::Xml, payload, cancellation, None, ) .await; match response { Ok((response, _)) => Ok(response.is_success_status_code()), Err(Error::Cancelled) => Err(Error::Cancelled), Err(_) => Ok(false), } } pub(crate) async fn native_forget_experience_permission( &self, experience_id: UUID, cancellation_token: Option, ) -> Result { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let Some(uri) = self.native_capability("ExperiencePreferences", &cancellation)? else { return Ok(false); }; let response = self .client .http_caps_client() .delete_with_uri_string_bytes_cancellation_token_i_progress( Uri(format!("{}?{experience_id}", uri.0)), String::new(), Vec::new(), cancellation, None, ) .await; match response { Ok((response, _)) => Ok(response.is_success_status_code()), Err(Error::Cancelled) => Err(Error::Cancelled), Err(_) => Ok(false), } } pub(crate) async fn native_get_experience_info( &self, experience_ids: Box>, cancellation_token: Option, ) -> Result, Error> { let ids: Vec<_> = experience_ids.collect(); if ids.is_empty() { return Ok(Some(ExperienceInfoMessage::new()?)); } let mut suffix = format!("/id/?page_size={}", ids.len()); for id in ids { use std::fmt::Write as _; let _ = write!(suffix, "&public_id={id}"); } self.native_get_experience_info_by_uri("GetExperienceInfo", &suffix, cancellation_token) .await } pub(crate) async fn native_find_experience_by_name( &self, query: String, page: Option, page_size: Option, cancellation_token: Option, ) -> Result, Error> { let suffix = format!( "?page={}&page_size={}&query={}", page.unwrap_or(0), page_size.unwrap_or(30), percent_encode(&query) ); self.native_get_experience_info_by_uri("FindExperienceByName", &suffix, cancellation_token) .await } async fn native_get_experience_info_by_uri( &self, capability: &str, suffix: &str, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map(capability, suffix, cancellation_token) .await? else { return Ok(None); }; let mut result = ExperienceInfoMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } self.inner .experience_info_received .emit(ExperienceInfoEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_update_experience( &self, info: ExperienceInfo, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .post_capability_map("UpdateExperience", info.serialize()?, cancellation_token) .await? else { return Ok(None); }; let mut result = ExperienceInfoMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } Ok(result.experiences.into_iter().next()) } pub(crate) async fn native_is_experience_role( &self, capability: &str, experience_id: UUID, cancellation_token: Option, ) -> Result { let Some(map) = self .get_capability_map( capability, &format!("?experience_id={experience_id}"), cancellation_token, ) .await? else { return Ok(false); }; map.get("status") .map_or(Ok(false), |value| value.as_boolean()) .or(Ok(false)) } pub(crate) async fn native_get_region_experiences( &self, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map("RegionExperiences", "", cancellation_token) .await? else { return Ok(None); }; let mut result = RegionExperiencesMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } self.native_set_last_region_experiences(Some(result.clone())); self.inner .region_experiences_updated .emit(RegionExperiencesEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_chatter_box_accept_invite( &self, session_id: UUID, cancellation_token: Option, ) -> Result<(), Error> { let mut message = crate::messages::linden::ChatSessionAcceptInvitation::new()?; message.session_id = session_id; if self .post_capability_map( "ChatSessionRequest", message.serialize()?, cancellation_token, ) .await? .is_none() { return Err(Error::InvalidOperation); } Ok(()) } pub(crate) async fn native_start_im_conference( &self, participants: Vec, session_id: UUID, cancellation_token: Option, ) -> Result<(), Error> { let mut message = crate::messages::linden::ChatSessionRequestStartConference::new()?; message.agents_block = participants; message.session_id = session_id; if self .post_capability_map( "ChatSessionRequest", message.serialize()?, cancellation_token, ) .await? .is_none() { return Err(Error::InvalidOperation); } Ok(()) } pub(crate) fn native_moderate_chat_sessions( &self, session_id: UUID, member_id: UUID, key: String, moderate: bool, cancellation_token: Option, ) -> Result<(), Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let capability_available = self .client .network() .current_sim() .and_then(|simulator| simulator.native_caps()) .and_then(|caps| { caps.capability_uri("ChatSessionRequest".into()) .ok() .flatten() }) .is_some(); if !capability_available { return Err(Error::InvalidOperation); } let mut message = crate::messages::linden::ChatSessionRequestMuteUpdate::new()?; message.agent_id = member_id; message.request_key = key; message.request_value = moderate; message.session_id = session_id; let payload = message.serialize()?; let client = Arc::clone(&self.client); std::thread::Builder::new() .name("libremetaverse-chat-moderation".into()) .spawn(move || { let Ok(runtime) = tokio::runtime::Builder::new_current_thread() .enable_all() .build() else { return; }; let Ok(manager) = AgentManager::native_new(Some(client)) else { return; }; let _ = runtime.block_on(manager.post_capability_map( "ChatSessionRequest", payload, Some(cancellation), )); }) .map(|_| ()) .map_err(|_| Error::InvalidOperation) } pub(crate) async fn native_set_display_name( &self, old_name: String, new_name: String, cancellation_token: Option, ) -> Result<(), Error> { let mut message = crate::messages::linden::SetDisplayNameMessage::new()?; message.old_display_name = old_name; message.new_display_name = new_name; let _ = self .post_capability_map("SetDisplayName", message.serialize()?, cancellation_token) .await?; Ok(()) } pub(crate) async fn native_update_agent_language( &self, language: String, is_public: bool, cancellation_token: Option, ) -> Result<(), Error> { let mut message = crate::messages::linden::UpdateAgentLanguageMessage::new()?; message.language = language; message.language_public = is_public; let _ = self .post_capability_map( "UpdateAgentLanguage", message.serialize()?, cancellation_token, ) .await?; Ok(()) } pub(crate) async fn native_set_agent_access_async( &self, access: String, cancellation_token: Option, ) -> Result { let payload = OSDMap::new_with_dictionary(HashMap::from([( "access_prefs".into(), OSD::Map(HashMap::from([("max".into(), OSD::String(access))])), )]))?; let Some(result) = self .post_capability_map("UpdateAgentInformation", payload, cancellation_token) .await? else { return AgentAccessEventArgs::new(false, self.native_agent_access()); }; let new_level = match result.get("access_prefs") { Some(OSD::Map(values)) => values .get("max") .map_or_else(|| Ok(self.native_agent_access()), OSD::as_string)?, _ => self.native_agent_access(), }; self.native_set_agent_access(new_level.clone()); AgentAccessEventArgs::new(true, new_level) } pub(crate) async fn native_set_hover_height_async( &self, hover_height: f64, cancellation_token: Option, ) -> Result<(), Error> { let payload = OSDMap::new_with_dictionary(HashMap::from([( "hover_height".into(), OSD::Real(hover_height), )]))?; if let Some(result) = self .post_capability_map("AgentPreferences", payload, cancellation_token) .await? && let Some(value) = result.get("hover_height") && let Ok(value) = value.as_real() { self.native_set_hover_height(value as f32); } Ok(()) } pub(crate) async fn native_get_agent_preferences( &self, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map("AgentPreferences", "", cancellation_token) .await? else { return Ok(None); }; let mut result = AgentPreferencesMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } self.native_set_hover_height(result.hover_height); *self .inner .agent_preferences .write() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(result.clone()); self.inner .agent_preferences_updated .emit(AgentPreferencesEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_get_avatar_render_info( &self, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map("AvatarRenderInfo", "", cancellation_token) .await? else { return Ok(None); }; let mut result = AvatarRenderInfoMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } self.native_set_avatar_render_info(Some(result.clone())); self.inner .avatar_render_info_updated .emit(AvatarRenderInfoEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_post_avatar_render_info( &self, agents: HashMap, cancellation_token: Option, ) -> Result<(), Error> { if agents.is_empty() { return Ok(()); } let message = AvatarRenderInfoMessage { agents: agents .into_iter() .map(|(id, (weight, too_complex))| { ( id, crate::messages::linden::AvatarRenderInfoMessageAvatarInfo { too_complex, weight, }, ) }) .collect(), over_limit: 0, reporting_limit: 0, }; let _ = self .post_capability_map("AvatarRenderInfo", message.serialize()?, cancellation_token) .await?; Ok(()) } pub(crate) async fn native_post_local_avatar_render_info( &self, weight: i32, too_complex: bool, cancellation_token: Option, ) -> Result<(), Error> { self.native_post_avatar_render_info( HashMap::from([(self.native_agent_id(), (weight, too_complex))]), cancellation_token, ) .await } pub(crate) async fn native_get_viewer_benefits( &self, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map("ViewerBenefits", "", cancellation_token) .await? else { return Ok(None); }; let account_level_benefits = map.get("account_level_benefits").and_then(|value| { let OSD::Map(values) = value else { return None; }; crate::AccountLevelBenefits::new_with_osd_map( OSDMap::new_with_dictionary(values.clone()).ok()?, ) .ok() }); let mut result = ViewerBenefitsMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } if let Some(benefits) = account_level_benefits { self.native_set_benefits(benefits); } self.native_set_viewer_benefits(Some(result.clone())); self.inner .viewer_benefits_updated .emit(ViewerBenefitsEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_send_viewer_stats( &self, stats: ViewerStatsMessage, cancellation_token: Option, ) -> Result<(), Error> { let _ = self .post_capability_map("ViewerStats", stats.serialize()?, cancellation_token) .await?; Ok(()) } pub(crate) async fn native_request_nav_mesh_generation_status( &self, cancellation_token: Option, ) -> Result, Error> { let Some(map) = self .get_capability_map("NavMeshGenerationStatus", "", cancellation_token) .await? else { return Ok(None); }; let mut result = crate::messages::linden::NavMeshStatusUpdateMessage::new()?; if result.deserialize(map).is_err() { return Ok(None); } self.native_set_last_nav_mesh_status(Some(result.clone())); Ok(Some(result)) } pub(crate) async fn native_request_nav_mesh_rebake( &self, cancellation_token: Option, ) -> Result<(), Error> { let empty = OSDMap::new_with_dictionary(HashMap::new())?; let _ = self .post_capability_map("NavMeshGenerationStatus", empty, cancellation_token) .await?; Ok(()) } pub(crate) async fn native_get_product_info( &self, cancellation_token: Option, ) -> Result, Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); let Some(uri) = self.native_capability("ProductInfoRequest", &cancellation)? else { return Ok(None); }; let response = self .client .http_caps_client() .get(uri, cancellation, None) .await; let (response, data) = match response { Ok(value) => value, Err(Error::Cancelled) => return Err(Error::Cancelled), Err(_) => return Ok(None), }; if !response.is_success_status_code() { return Ok(None); } let map = match OSDParser::deserialize_with_bytes(data) { Ok(OSD::Map(map)) => map, Ok(OSD::Array(products)) => HashMap::from([("products".into(), OSD::Array(products))]), _ => return Ok(None), }; let mut result = ProductInfoRequestMessage::new()?; if result .deserialize(OSDMap::new_with_dictionary(map)?) .is_err() { return Ok(None); } self.native_set_product_info(Some(result.clone())); self.inner .product_info_updated .emit(ProductInfoEventArgs::new(result.clone())?); Ok(Some(result)) } pub(crate) async fn native_get_attachment_resources( &self, cancellation_token: Option, ) -> Result< ( bool, Option, ), Error, > { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let Some(capability) = self.native_capability("AttachmentResources", &cancellation)? else { return Ok((false, None)); }; let (response, data) = self .client .http_caps_client() .get(capability, cancellation, None) .await?; if !response.is_success_status_code() { return Ok((false, None)); } let Ok(parsed) = OSDParser::deserialize_with_bytes(data) else { return Ok((false, None)); }; match crate::messages::linden::AttachmentResourcesMessage::from_osd(parsed) { Ok(info) => Ok((true, Some(info))), Err(_) => Ok((false, None)), } } pub(crate) async fn native_retrieve_instant_messages( &self, cancellation_token: Option, ) -> Result<(), Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); let Some(offline) = self.native_capability("ReadOfflineMsgs", &cancellation)? else { return self.native_retrieve_instant_messages_legacy(); }; if self .native_capability("AcceptFriendship", &cancellation)? .is_none() || self .native_capability("AcceptGroupInvite", &cancellation)? .is_none() { return self.native_retrieve_instant_messages_legacy(); } let (_, data) = self .client .http_caps_client() .get(offline, cancellation, None) .await?; self.deliver_offline_messages(&data) } fn native_capability( &self, name: &str, cancellation: &libremetaverse_types::compat::CancellationToken, ) -> Result, Error> { let Some(caps) = self .client .network() .current_sim() .and_then(|simulator| simulator.native_caps()) else { return Ok(None); }; let deadline = Instant::now() + Duration::from_secs(2); loop { if let Some(uri) = caps.capability_uri(name.to_owned())? { return Ok(Some(uri)); } if caps.seed_request_finished() || Instant::now() >= deadline { return Ok(None); } cancellation.throw_if_cancellation_requested()?; std::thread::sleep(Duration::from_millis(1)); } } pub(crate) async fn native_query_experiences_on_parcel( &self, parcel_local_id: i32, experience_ids: Box>, cancellation_token: Option, ) -> Result>, Error> { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let Some(capability) = self.native_capability("ExperienceQuery", &cancellation)? else { return Ok(None); }; let ids: Vec<_> = experience_ids.collect(); let mut uri = format!("{}?parcelid={parcel_local_id}", capability.0); if !ids.is_empty() { uri.push_str("&experiences="); uri.push_str( &ids.iter() .map(ToString::to_string) .collect::>() .join(","), ); } let response = self .client .http_caps_client() .get(Uri(uri), cancellation.clone(), None) .await; let (response, data) = match response { Ok(result) => result, Err(Error::Cancelled) => return Err(Error::Cancelled), Err(_) => return Ok(None), }; if !response.is_success_status_code() { return Ok(None); } let OSD::Map(mut map) = OSDParser::deserialize_with_bytes(data)? else { return Ok(None); }; let Some(OSD::Map(experiences)) = map.remove("experiences") else { return Ok(None); }; let mut result = HashMap::with_capacity(experiences.len()); for (id, allowed) in experiences { let id = UUID::new_with_string(id)?; result.insert(id, allowed.as_boolean()?); } Ok(Some(result)) } #[allow(clippy::too_many_arguments)] pub(crate) async fn native_send_postcard( &self, jpeg_image_data: Vec, to_email: String, from_name: String, subject: String, message: String, global_position: Vector3, cancellation_token: Option, ) -> Result { let cancellation = cancellation_token.unwrap_or_else(|| self.client.cancellation_token()); cancellation.throw_if_cancellation_requested()?; let Some(capability) = self.native_capability("SendPostcard", &cancellation)? else { return Ok(false); }; let mut postcard = crate::messages::linden::SendPostcardMessage::new()?; postcard.from_name = from_name; postcard.to_email = to_email; postcard.subject = subject; postcard.message = message; postcard.global_position = global_position; let payload = OSD::Map(postcard.serialize()?.snapshot()); let first = self .client .http_caps_client() .post_with_uri_osd_format_osd_cancellation_token_i_progress( capability, libremetaverse_structured_data::OSDFormat::Xml, payload, cancellation.clone(), None, ) .await; let (response, data) = match first { Ok(value) => value, Err(Error::Cancelled) => return Err(Error::Cancelled), Err(_) => return Ok(false), }; if !response.is_success_status_code() { return Ok(false); } let Ok(OSD::Map(mut response)) = OSDParser::deserialize_with_bytes(data) else { return Ok(false); }; let Some(uploader) = response.remove("uploader") else { return Ok(false); }; let uploader = uploader.as_string()?; if uploader.is_empty() { return Ok(false); } let upload = self .client .http_caps_client() .post_with_uri_string_bytes_cancellation_token_i_progress( Uri(uploader), "application/octet-stream".into(), jpeg_image_data, cancellation, None, ) .await; let (response, data) = match upload { Ok(value) => value, Err(Error::Cancelled) => return Err(Error::Cancelled), Err(_) => return Ok(false), }; if !response.is_success_status_code() { return Ok(false); } let Ok(OSD::Map(mut response)) = OSDParser::deserialize_with_bytes(data) else { return Ok(false); }; Ok(response .remove("state") .is_some_and(|state| state.as_string().as_deref() == Ok("complete"))) } fn native_retrieve_instant_messages_legacy(&self) -> Result<(), Error> { let mut packet = crate::packets::RetrieveInstantMessagesPacket::new_with_constructor()?; packet.agent_data.agent_id = self.native_agent_id(); packet.agent_data.session_id = self.native_session_id(); let data = packet.to_bytes_with_method()?; let length = i32::try_from(data.len()).map_err(|_| Error::Argument)?; let simulator = self .client .network() .current_sim() .ok_or(Error::InvalidOperation)?; simulator.native_send_packet_data( data, length, crate::packets::PacketType::RetrieveInstantMessages, false, ) } fn deliver_offline_messages(&self, data: &[u8]) -> Result<(), Error> { let root = OSDParser::deserialize_with_bytes(data.to_vec())?; let messages = match root { OSD::Array(values) => values.into_iter().next().and_then(|value| match value { OSD::Array(messages) => Some(messages), _ => None, }), OSD::Map(mut map) => map.remove("messages").and_then(|value| match value { OSD::Array(messages) => Some(messages), _ => None, }), _ => None, } .ok_or(Error::Argument)?; for value in messages { let OSD::Map(map) = value else { return Err(Error::Argument); }; let message = offline_message(&map)?; if message.group_im { self.group_chat_sessions .write() .unwrap_or_else(std::sync::PoisonError::into_inner) .entry(message.im_session_id) .or_default(); } self.inner.instant_message.emit(InstantMessageEventArgs { im: message, simulator: None, }); } Ok(()) } pub(crate) fn extract_experience_permission( map: OSDMap, experience_id: UUID, ) -> Result { if osd_uuid_array_contains(&map, "experiences", experience_id)? { return Ok("Allow".into()); } if osd_uuid_array_contains(&map, "blocked", experience_id)? { return Ok("Block".into()); } Ok("Forget".into()) } } struct UdpAssetAbortGuard { simulator: Simulator, transfer_id: UUID, completed: bool, } impl UdpAssetAbortGuard { fn new(simulator: Simulator, transfer_id: UUID) -> Self { Self { simulator, transfer_id, completed: false, } } fn complete(&mut self) { self.completed = true; } } impl Drop for UdpAssetAbortGuard { fn drop(&mut self) { if self.completed { return; } let Ok(mut packet) = crate::packets::TransferAbortPacket::new_with_constructor() else { return; }; packet.transfer_info.channel_type = crate::ChannelType::Asset as i32; packet.transfer_info.transfer_id = self.transfer_id; let Ok(data) = packet.to_bytes_with_method() else { return; }; let Ok(length) = i32::try_from(data.len()) else { return; }; let do_zerocode = data .first() .is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0); let _ = self.simulator.native_send_packet_data( data, length, crate::packets::PacketType::TransferAbort, do_zerocode, ); } } fn required<'a>(map: &'a HashMap, key: &str) -> Result<&'a OSD, Error> { map.get(key).ok_or(Error::Argument) } fn gesture_chat_channel(text: &str) -> (i32, String) { let Some(rest) = text.strip_prefix('/') else { return (0, text.to_owned()); }; let bytes = rest.as_bytes(); let mut end = usize::from(bytes.first() == Some(&b'-')); let digit_start = end; while bytes.get(end).is_some_and(u8::is_ascii_digit) { end += 1; } if end == digit_start { return (0, text.to_owned()); } let Ok(channel) = rest[..end].parse::() else { return (0, text.to_owned()); }; let message = rest[end..] .trim_start_matches(char::is_whitespace) .to_owned(); (channel, message) } fn decode_raw_packet(data: &[u8]) -> Result { let mut packet = T::new_generated(); let mut position = 0_i32; let mut packet_end = i32::try_from(data.len()).map_err(|_| Error::Argument)? - 1; let mut zero_buffer = vec![0_u8; crate::udp_transport::UdpTransportConfig::default().max_decoded_packet_size]; packet.decode_from_bytes( data, &mut position, &mut packet_end, Some(zero_buffer.as_mut_slice()), )?; Ok(packet) } fn wire_string(mut bytes: Vec) -> String { while bytes.last() == Some(&0) { bytes.pop(); } String::from_utf8_lossy(&bytes).into_owned() } fn chat_audible_level(value: u8) -> Result { match value { 0 => Ok(crate::ChatAudibleLevel::Barely), 1 => Ok(crate::ChatAudibleLevel::Fully), u8::MAX => Ok(crate::ChatAudibleLevel::Not), _ => Err(Error::Argument), } } fn chat_source_type(value: u8) -> Result { match value { 0 => Ok(crate::ChatSourceType::System), 1 => Ok(crate::ChatSourceType::Agent), 2 => Ok(crate::ChatSourceType::Object), _ => Err(Error::Argument), } } fn chat_type(value: u8) -> Result { match value { 0 => Ok(crate::ChatType::Whisper), 1 => Ok(crate::ChatType::Normal), 2 => Ok(crate::ChatType::Shout), 4 => Ok(crate::ChatType::StartTyping), 5 => Ok(crate::ChatType::StopTyping), 6 => Ok(crate::ChatType::Debug), 8 => Ok(crate::ChatType::OwnerSay), 9 => Ok(crate::ChatType::RegionSayTo), u8::MAX => Ok(crate::ChatType::RegionSay), _ => Err(Error::Argument), } } fn instant_message_online(value: u8) -> Result { match value { 0 => Ok(InstantMessageOnline::Online), 1 => Ok(InstantMessageOnline::Offline), _ => Err(Error::Argument), } } fn instant_message_dialog(value: u8) -> Result { use InstantMessageDialog as Dialog; match value { 0 => Ok(Dialog::MessageFromAgent), 1 => Ok(Dialog::MessageBox), 3 => Ok(Dialog::GroupInvitation), 4 => Ok(Dialog::InventoryOffered), 5 => Ok(Dialog::InventoryAccepted), 6 => Ok(Dialog::InventoryDeclined), 7 => Ok(Dialog::GroupVote), 9 => Ok(Dialog::TaskInventoryOffered), 10 => Ok(Dialog::TaskInventoryAccepted), 11 => Ok(Dialog::TaskInventoryDeclined), 12 => Ok(Dialog::NewUserDefault), 13 => Ok(Dialog::SessionAdd), 14 => Ok(Dialog::SessionOfflineAdd), 15 => Ok(Dialog::SessionGroupStart), 16 => Ok(Dialog::SessionCardlessStart), 17 => Ok(Dialog::SessionSend), 18 => Ok(Dialog::SessionDrop), 19 => Ok(Dialog::MessageFromObject), 20 => Ok(Dialog::BusyAutoResponse), 21 => Ok(Dialog::ConsoleAndChatHistory), 22 => Ok(Dialog::RequestTeleport), 23 => Ok(Dialog::AcceptTeleport), 24 => Ok(Dialog::DenyTeleport), 25 => Ok(Dialog::GodLikeRequestTeleport), 26 => Ok(Dialog::RequestLure), 28 => Ok(Dialog::GotoUrl), 29 => Ok(Dialog::Session911Start), 30 => Ok(Dialog::Lure911), 31 => Ok(Dialog::FromTaskAsAlert), 32 => Ok(Dialog::GroupNotice), 33 => Ok(Dialog::GroupNoticeInventoryAccepted), 34 => Ok(Dialog::GroupNoticeInventoryDeclined), 35 => Ok(Dialog::GroupInvitationAccept), 36 => Ok(Dialog::GroupInvitationDecline), 37 => Ok(Dialog::GroupNoticeRequested), 38 => Ok(Dialog::FriendshipOffered), 39 => Ok(Dialog::FriendshipAccepted), 40 => Ok(Dialog::FriendshipDeclined), 41 => Ok(Dialog::StartTyping), 42 => Ok(Dialog::StopTyping), _ => Err(Error::Argument), } } fn split_utf8(message: &str, max_bytes: usize, max_parts: usize) -> Vec { assert!(max_bytes > 0, "split size must be at least one byte"); if message.is_empty() { return vec![String::new()]; } let mut parts = Vec::new(); let mut start = 0; while start < message.len() && parts.len() < max_parts { let proposed = start.saturating_add(max_bytes).min(message.len()); let mut end = proposed; while end > start && !message.is_char_boundary(end) { end -= 1; } let end = if end == start { message[start..] .char_indices() .nth(1) .map_or(message.len(), |(offset, _)| start + offset) } else { end }; parts.push(message[start..end].to_owned()); start = end; } parts } fn percent_encode(value: &str) -> String { let mut encoded = String::with_capacity(value.len()); for byte in value.bytes() { if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') { encoded.push(char::from(byte)); } else { use std::fmt::Write as _; let _ = write!(encoded, "%{byte:02X}"); } } encoded } fn optional_uuid(map: &HashMap, key: &str) -> Result { map.get(key).map_or(Ok(UUID::zero()), OSD::as_uuid) } fn optional_i32(map: &HashMap, key: &str, fallback: i32) -> Result { map.get(key).map_or(Ok(fallback), OSD::as_integer) } fn optional_u32(map: &HashMap, key: &str, fallback: u32) -> Result { map.get(key).map_or(Ok(fallback), OSD::as_u_integer) } fn offline_message(map: &HashMap) -> Result { let dialog = match required(map, "dialog")?.as_integer()? { 0 => InstantMessageDialog::MessageFromAgent, 17 => InstantMessageDialog::SessionSend, _ => return Err(Error::Argument), }; let offline = match optional_i32(map, "offline", 1)? { 0 => InstantMessageOnline::Online, 1 => InstantMessageOnline::Offline, _ => return Err(Error::Argument), }; let position = if let Some(value) = map.get("position") { value.as_vector3()? } else { Vector3 { #[allow(clippy::cast_possible_truncation)] x: required(map, "local_x")?.as_real()? as f32, #[allow(clippy::cast_possible_truncation)] y: required(map, "local_y")?.as_real()? as f32, #[allow(clippy::cast_possible_truncation)] z: required(map, "local_z")?.as_real()? as f32, } }; let ticks = optional_i32(map, "timestamp", 0)?.max(0); Ok(InstantMessage { binary_bucket: map .get("binary_bucket") .map_or_else(|| Ok(vec![0]), OSD::as_binary)?, dialog, from_agent_id: required(map, "from_agent_id")?.as_uuid()?, from_agent_name: required(map, "from_agent_name")?.as_string()?, group_im: map .get("from_group") .is_some_and(|value| value.as_boolean() == Ok(true)), im_session_id: required(map, "transaction-id")?.as_uuid()?, message: required(map, "message")?.as_string()?, offline, parent_estate_id: optional_u32(map, "parent_estate_id", 1)?, position, region_id: optional_uuid(map, "region_id")?, timestamp: UNIX_EPOCH + Duration::from_secs(u64::try_from(ticks).unwrap_or_default()), to_agent_id: required(map, "to_agent_id")?.as_uuid()?, }) } fn osd_uuid_array_contains(map: &OSDMap, key: &str, id: UUID) -> Result { let Some(OSD::Array(values)) = map.get(key) else { return Ok(false); }; for value in values { if value.as_uuid()? == id { return Ok(true); } } Ok(false) } #[cfg(test)] mod tests { use super::*; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; fn loopback(port: u16) -> SocketAddr { SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port) } fn simulator(client: &GridClient, endpoint: SocketAddr) -> Simulator { Simulator::native_new(client.clone(), endpoint, 0x1000, Some(256), Some(256)) .expect("simulator") } fn transport_wire(data: Vec) -> Vec { if data .first() .is_none_or(|flags| flags & crate::Helpers::MSG_ZEROCODED == 0) { return data; } let mut encoded = vec![0_u8; data.len().saturating_mul(2).saturating_add(2)]; let length = crate::packet_wire::zero_encode( Some(&data), i32::try_from(data.len()).unwrap(), Some(&mut encoded), ) .unwrap(); encoded.truncate(usize::try_from(length).unwrap()); encoded } #[test] fn chat_uses_utf8_safe_protocol_chunks_and_exact_identity() { let client = Arc::new(GridClient::new().expect("client")); let manager = AgentManager::native_new(Some(client)).unwrap(); manager.native_set_agent_id( UUID::new_with_string("11111111-1111-1111-1111-111111111111".into()).unwrap(), ); manager.native_set_session_id( UUID::new_with_string("22222222-2222-2222-2222-222222222222".into()).unwrap(), ); let message = "é".repeat(600); let packets = manager .native_chat_payloads(message.clone(), 7, ChatType::Normal, Some(true)) .unwrap(); let mut reconstructed = String::new(); assert_eq!(packets.len(), 2); for (_, data) in packets { let data = transport_wire(data); let packet = decode_raw_packet::(&data).unwrap(); assert_eq!(packet.agent_data.agent_id, manager.native_agent_id()); assert_eq!(packet.agent_data.session_id, manager.native_session_id()); assert_eq!(packet.chat_data.channel, 7); assert_eq!(packet.chat_data.type_, ChatType::Normal as u8); assert!(packet.chat_data.message.len() <= MAX_CHAT_BYTES); reconstructed.push_str(&wire_string(packet.chat_data.message)); } assert_eq!(reconstructed, message); } #[test] fn gesture_chat_channel_matches_reference_regex_behavior() { assert_eq!(gesture_chat_channel("hello"), (0, "hello".into())); assert_eq!(gesture_chat_channel("/5 hello"), (5, "hello".into())); assert_eq!(gesture_chat_channel("/-42answer"), (-42, "answer".into())); assert_eq!(gesture_chat_channel("/x nope"), (0, "/x nope".into())); assert_eq!( gesture_chat_channel("/999999999999999999 text"), (0, "/999999999999999999 text".into()) ); } #[test] fn instant_message_debug_redacts_message_and_binary_payload() { let message = InstantMessage { binary_bucket: b"private-bucket".to_vec(), dialog: InstantMessageDialog::MessageFromAgent, from_agent_id: UUID::random().unwrap(), from_agent_name: "Sender Resident".into(), group_im: false, im_session_id: UUID::random().unwrap(), message: "private-message".into(), offline: InstantMessageOnline::Online, parent_estate_id: 1, position: Vector3::zero(), region_id: UUID::random().unwrap(), timestamp: UNIX_EPOCH, to_agent_id: UUID::random().unwrap(), }; let diagnostic = format!("{message:?}"); assert!(!diagnostic.contains("private-message")); assert!(!diagnostic.contains("private-bucket")); assert!(!diagnostic.contains("Sender Resident")); assert!(diagnostic.contains("message_len")); } #[test] fn raw_agent_packets_update_state_and_raise_callbacks_outside_locks() { let client = GridClient::new().unwrap(); let sim = simulator(&client, loopback(13001)); let inner = AgentManagerInner::default(); let chats = Arc::new(Mutex::new(Vec::new())); let chat_values = Arc::clone(&chats); let _chat_subscription = inner.chat_from_simulator.subscribe(Arc::new(move |event| { chat_values.lock().unwrap().push(event.message()); })); let mut chat = crate::packets::ChatFromSimulatorPacket::new_with_constructor().unwrap(); chat.chat_data.message = b"hello\0".to_vec(); chat.chat_data.from_name = b"Grid Resident\0".to_vec(); chat.chat_data.audible = 1; chat.chat_data.chat_type = ChatType::Normal as u8; chat.chat_data.source_type = crate::ChatSourceType::Agent as u8; inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs { packet_type: crate::packets::PacketType::ChatFromSimulator, data: transport_wire(chat.to_bytes_with_method().unwrap()), simulator: sim.clone(), }); assert_eq!(*chats.lock().unwrap(), ["hello"]); let ims = Arc::new(Mutex::new(Vec::new())); let im_values = Arc::clone(&ims); let _im_subscription = inner.instant_message.subscribe(Arc::new(move |event| { im_values.lock().unwrap().push(event.im().message); })); let mut im = crate::packets::ImprovedInstantMessagePacket::new_with_constructor().unwrap(); im.message_block.dialog = InstantMessageDialog::MessageFromAgent as u8; im.message_block.offline = InstantMessageOnline::Online as u8; im.message_block.message = b"private\0".to_vec(); inner .handle_instant_message_packet(&transport_wire(im.to_bytes_with_method().unwrap()), sim) .unwrap(); assert_eq!(*ims.lock().unwrap(), ["private"]); let balances = Arc::new(Mutex::new(Vec::new())); let balance_values = Arc::clone(&balances); let _balance_subscription = inner.money_balance.subscribe(Arc::new(move |event| { balance_values.lock().unwrap().push(event.balance()); })); let mut money = crate::packets::MoneyBalanceReplyPacket::new_with_constructor().unwrap(); money.money_data.money_balance = 1234; money.money_data.description = b"payment\0".to_vec(); money.transaction_info.item_description = b"item\0".to_vec(); inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs { packet_type: crate::packets::PacketType::MoneyBalanceReply, data: transport_wire(money.to_bytes_with_method().unwrap()), simulator: simulator(&client, loopback(13002)), }); assert_eq!(*balances.lock().unwrap(), [1234]); assert_eq!(*inner.balance.read().unwrap(), 1234); let identity = Arc::new(Mutex::new(Vec::new())); let identity_values = Arc::clone(&identity); let _identity_subscription = inner.agent_data_reply.subscribe(Arc::new(move |event| { identity_values.lock().unwrap().push(( event.first_name(), event.last_name(), event.active_group_id(), event.group_title(), event.group_powers(), event.group_name(), )); })); let mut identity_packet = crate::packets::AgentDataUpdatePacket::new_with_constructor().unwrap(); identity_packet.agent_data.agent_id = UUID::random().unwrap(); identity_packet.agent_data.first_name = b"Test\0".to_vec(); identity_packet.agent_data.last_name = b"Resident\0".to_vec(); identity_packet.agent_data.active_group_id = UUID::random().unwrap(); identity_packet.agent_data.group_title = b"Builder\0".to_vec(); identity_packet.agent_data.group_name = b"Builders\0".to_vec(); identity_packet.agent_data.group_powers = 0x1234; inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs { packet_type: crate::packets::PacketType::AgentDataUpdate, data: transport_wire(identity_packet.to_bytes_with_method().unwrap()), simulator: simulator(&client, loopback(13005)), }); assert_eq!( *identity.lock().unwrap(), [( "Test".into(), "Resident".into(), identity_packet.agent_data.active_group_id, "Builder".into(), crate::GroupPowers(0x1234), "Builders".into(), )] ); assert_eq!(*inner.first_name.read().unwrap(), "Test"); assert_eq!(*inner.last_name.read().unwrap(), "Resident"); let animation_id = UUID::random().unwrap(); let changed = Arc::new(Mutex::new(Vec::new())); let changed_values = Arc::clone(&changed); let _animation_subscription = inner.animations_changed.subscribe(Arc::new(move |event| { changed_values.lock().unwrap().push(event.animations()); })); let mut animations = crate::packets::AvatarAnimationPacket::new_with_constructor().unwrap(); animations.sender.id = UUID::random().unwrap(); let mut animation = crate::packets::AvatarAnimationPacketAnimationListBlock::new_with_constructor() .unwrap(); animation.anim_id = animation_id; animation.anim_sequence_id = 17; animations.animation_list.push(animation); inner.handle_raw_packet(crate::network_manager::RawPacketReceivedEventArgs { packet_type: crate::packets::PacketType::AvatarAnimation, data: transport_wire(animations.to_bytes_with_method().unwrap()), simulator: simulator(&client, loopback(13006)), }); assert_eq!( *changed.lock().unwrap(), [HashMap::from([(animation_id, 17)])] ); assert_eq!( *inner.signaled_animations.read().unwrap(), HashMap::from([(animation_id, 17)]) ); } #[test] fn group_chat_caps_events_track_members_and_raise_compatible_events() { let client = Arc::new(GridClient::new().unwrap()); let manager = Arc::new(AgentManager::native_new(Some(Arc::clone(&client))).unwrap()); let sim = simulator(&client, loopback(13003)); let session_id = UUID::new_with_string("33333333-3333-3333-3333-333333333333".into()).unwrap(); let agent_id = UUID::new_with_string("44444444-4444-4444-4444-444444444444".into()).unwrap(); let joined = Arc::new(Mutex::new(Vec::new())); let joined_values = Arc::clone(&joined); let _joined = manager.native_subscribe_group_chat_joined(Arc::new(move |event| { joined_values.lock().unwrap().push(( event.session_id(), event.session_name(), event.tmp_session_id(), event.success(), )); })); let manager_for_added = Arc::clone(&manager); let added = Arc::new(Mutex::new(Vec::new())); let added_values = Arc::clone(&added); let _added = manager.native_subscribe_chat_session_member_added(Arc::new(move |event| { // Taking the collection read lock proves callbacks are not invoked // while the membership write lock is held. assert!( manager_for_added .group_chat_sessions .read() .unwrap() .contains_key(&event.session_id()) ); added_values.lock().unwrap().push(event.agent_id()); })); let left = Arc::new(Mutex::new(Vec::new())); let left_values = Arc::clone(&left); let _left = manager.native_subscribe_chat_session_member_left(Arc::new(move |event| { left_values.lock().unwrap().push(event.agent_id()); })); let mut started = crate::messages::linden::ChatterBoxSessionStartReplyMessage::new().unwrap(); started.session_id = session_id; started.session_name = "Group Chat".into(); started.temp_session_id = UUID::random().unwrap(); started.success = true; let mut started_roundtrip = crate::messages::linden::ChatterBoxSessionStartReplyMessage::new().unwrap(); started_roundtrip .deserialize(started.serialize().unwrap()) .unwrap(); assert_eq!(started_roundtrip.session_id, session_id); client .network() .dispatch_caps_event("ChatterBoxSessionStartReply", &started, sim.clone()); assert_eq!( *joined.lock().unwrap(), [( session_id, "Group Chat".into(), started.temp_session_id, true )] ); assert!( manager .group_chat_sessions .read() .unwrap() .contains_key(&session_id) ); let mut entered = crate::messages::linden::ChatterBoxSessionAgentListUpdatesMessage::new().unwrap(); entered.session_id = session_id; let mut member = crate::messages::linden:: ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock::new() .unwrap(); member.agent_id = agent_id; member.transition = "ENTER".into(); member.can_voice_chat = true; member.is_moderator = true; member.mute_text = true; entered.updates.push(member); client.network().dispatch_caps_event( "ChatterBoxSessionAgentListUpdates", &entered, sim.clone(), ); assert_eq!(*added.lock().unwrap(), [agent_id]); assert_eq!( manager.group_chat_sessions.read().unwrap()[&session_id], [ChatSessionMember { avatar_key: agent_id, can_voice_chat: true, is_moderator: true, mute_text: true, mute_voice: false, }] ); let mut departed = crate::messages::linden::ChatterBoxSessionAgentListUpdatesMessage::new().unwrap(); departed.session_id = session_id; let mut member = crate::messages::linden:: ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock::new() .unwrap(); member.agent_id = agent_id; member.transition = "LEAVE".into(); departed.updates.push(member); client .network() .dispatch_caps_event("ChatterBoxSessionAgentListUpdates", &departed, sim); assert_eq!(*left.lock().unwrap(), [agent_id]); assert!(manager.group_chat_sessions.read().unwrap()[&session_id].is_empty()); } #[test] fn caps_invitation_and_display_name_reply_preserve_event_payloads() { let client = Arc::new(GridClient::new().unwrap()); let manager = AgentManager::native_new(Some(Arc::clone(&client))).unwrap(); let sim = simulator(&client, loopback(13004)); let (sender, receiver) = std::sync::mpsc::channel(); let _im = manager.native_subscribe_im(Arc::new(move |event| { let _ = sender.send(event.im()); })); let mut invitation = crate::messages::linden::ChatterBoxInvitationMessage::new().unwrap(); invitation.from_agent_id = UUID::random().unwrap(); invitation.from_agent_name = "Inviter Resident".into(); invitation.to_agent_id = UUID::random().unwrap(); invitation.im_session_id = UUID::random().unwrap(); invitation.message = "conference invitation".into(); invitation.dialog = InstantMessageDialog::SessionSend; invitation.offline = InstantMessageOnline::Online; invitation.timestamp = UNIX_EPOCH + Duration::from_secs(42); let mut invitation_roundtrip = crate::messages::linden::ChatterBoxInvitationMessage::new().unwrap(); invitation_roundtrip .deserialize(invitation.serialize().unwrap()) .unwrap(); assert_eq!(invitation_roundtrip.im_session_id, invitation.im_session_id); client .network() .dispatch_caps_event("ChatterBoxInvitation", &invitation, sim.clone()); let delivered = receiver.recv_timeout(Duration::from_secs(2)).unwrap(); assert_eq!(delivered.from_agent_id, invitation.from_agent_id); assert_eq!(delivered.im_session_id, invitation.im_session_id); assert_eq!(delivered.message, invitation.message); assert!( manager .group_chat_sessions .read() .unwrap() .contains_key(&invitation.im_session_id) ); let display_events = Arc::new(Mutex::new(Vec::new())); let display_values = Arc::clone(&display_events); let _display = manager.native_subscribe_set_display_name_reply(Arc::new(move |event| { display_values.lock().unwrap().push(( event.status(), event.reason(), event.display_name().display_name(), )); })); let mut reply = crate::messages::linden::SetDisplayNameReplyMessage::new().unwrap(); reply.status = 200; reply.reason = "ok".into(); reply.display_name.set_id(invitation.to_agent_id); reply .display_name .set_display_name(Some("New Display Name".into())); client .network() .dispatch_caps_event("SetDisplayNameReply", &reply, sim); assert_eq!( *display_events.lock().unwrap(), [(200, "ok".into(), Some("New Display Name".into()))] ); } #[test] fn profile_capability_payload_matches_reference_keys() { let profile = crate::AvatarAvatarProperties { about_text: "about".into(), born_on: String::new(), charter_member: String::new(), first_life_image: UUID::new_with_string("11111111-1111-1111-1111-111111111111".into()) .unwrap(), first_life_text: "first".into(), flags: crate::ProfileFlags( crate::ProfileFlags::ALLOW_PUBLISH.0 | crate::ProfileFlags::MATURE_PUBLISH.0, ), partner: UUID::zero(), profile_image: UUID::new_with_string("22222222-2222-2222-2222-222222222222".into()) .unwrap(), profile_url: "https://example.invalid/profile".into(), }; let payload = AgentManager::profile_payload(&profile).unwrap(); assert_eq!( payload.get("sl_about_text").unwrap().as_string().unwrap(), "about" ); assert_eq!( payload.get("fl_about_text").unwrap().as_string().unwrap(), "first" ); assert!(payload.get("allow_publish").unwrap().as_boolean().unwrap()); assert_eq!( payload.get("sl_image_id").unwrap().as_uuid().unwrap(), profile.profile_image ); assert_eq!( payload.get("fl_image_id").unwrap().as_uuid().unwrap(), profile.first_life_image ); } #[test] fn mute_list_parser_preserves_types_names_and_flags() { let resident = UUID::new_with_string("11111111-1111-1111-1111-111111111111".into()).unwrap(); let group = UUID::new_with_string("22222222-2222-2222-2222-222222222222".into()).unwrap(); let parsed = parse_mute_list(&format!( "1 {resident} Example Resident|3\n3 {group} Example Group|8\ninvalid line\n" )); assert_eq!(parsed.len(), 2); assert_eq!( parsed[&format!("{resident}|Example Resident")].type_, crate::MuteType::Resident ); assert_eq!( parsed[&format!("{resident}|Example Resident")].flags, crate::MuteFlags(3) ); assert_eq!( parsed[&format!("{group}|Example Group")].type_, crate::MuteType::Group ); assert_eq!( parsed[&format!("{group}|Example Group")].flags, crate::MuteFlags(8) ); } #[test] fn nav_mesh_caps_event_updates_last_status_before_callback() { let client = Arc::new(GridClient::new().unwrap()); let manager = Arc::new(AgentManager::native_new(Some(Arc::clone(&client))).unwrap()); let sim = simulator(&client, loopback(13007)); let region_id = UUID::random().unwrap(); let observed = Arc::new(Mutex::new(Vec::new())); let observed_values = Arc::clone(&observed); let manager_for_callback = Arc::clone(&manager); let _subscription = manager.native_subscribe_nav_mesh_status_update(Arc::new(move |event| { let stored = manager_for_callback .native_last_nav_mesh_status() .expect("status is stored before the event"); observed_values.lock().unwrap().push(( event.message().status, event.simulator().ip_end_point(), stored.version, )); })); let mut status = crate::messages::linden::NavMeshStatusUpdateMessage::new().unwrap(); status.region_id = region_id; status.status = "building".into(); status.version = 9; client .network() .dispatch_caps_event("NavMeshStatusUpdate", &status, sim.clone()); let stored = manager.native_last_nav_mesh_status().unwrap(); assert_eq!(stored.region_id, region_id); assert_eq!(stored.status, "building"); assert_eq!(stored.version, 9); assert_eq!( *observed.lock().unwrap(), [("building".into(), sim.ip_end_point(), 9)] ); } #[tokio::test(flavor = "current_thread")] async fn fake_grid_serves_gesture_capability_and_observes_udp_actions() { use crate::packets::{ Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType, RegionHandshakePacket, }; use libremetaverse_types::compat::{HttpMessageHandler, HttpResponse, Uri}; use std::collections::BTreeMap; use std::net::UdpSocket; use std::sync::mpsc; fn packet_type(bytes: &[u8]) -> Option { let mut packet_end = i32::try_from(bytes.len()).ok()?.checked_sub(1)?; Packet::build_packet_with_bytes_int32_bytes( bytes.to_vec(), &mut packet_end, vec![0; 8192], ) .ok() .map(|packet| packet.type_) } fn ack(sequence: u32) -> Vec { let mut packet = PacketAckPacket::new_with_constructor().unwrap(); packet.packets = vec![PacketAckPacketPacketsBlock { id: sequence }]; let mut bytes = packet.to_bytes_with_method().unwrap(); bytes[0] &= !(crate::Helpers::MSG_RELIABLE | crate::Helpers::MSG_ZEROCODED); bytes } let animation_id = UUID::random().unwrap(); let sound_id = UUID::random().unwrap(); let gesture_id = UUID::random().unwrap(); let mut gesture = crate::assets::AssetGesture::new_with_constructor().unwrap(); gesture.sequence = vec![ crate::assets::GestureStep::Chat(crate::assets::GestureStepChat { text: "/7 hello grid".into(), }), crate::assets::GestureStep::Animation(crate::assets::GestureStepAnimation { animation_start: true, id: animation_id, name: "Wave".into(), }), crate::assets::GestureStep::Sound(crate::assets::GestureStepSound { id: sound_id, name: "Chime".into(), }), crate::assets::GestureStep::EOF(crate::assets::GestureStepEOF), ]; gesture.encode().unwrap(); let gesture_bytes = gesture.asset_data().to_vec(); let socket = UdpSocket::bind(loopback(0)).expect("fake grid UDP socket"); socket .set_read_timeout(Some(Duration::from_secs(10))) .unwrap(); let endpoint = socket.local_addr().unwrap(); let (udp_sender, udp_receiver) = mpsc::channel(); let server = std::thread::spawn(move || { let mut buffer = [0_u8; 8192]; let (length, client_endpoint) = socket.recv_from(&mut buffer).expect("UseCircuitCode"); assert_eq!( packet_type(&buffer[..length]), Some(PacketType::UseCircuitCode) ); let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); socket.send_to(&ack(sequence), client_endpoint).unwrap(); let mut handshake = RegionHandshakePacket::new_with_constructor().unwrap(); handshake.region_info.sim_name = b"Agent Test Region\0".to_vec(); let mut handshake_bytes = handshake.to_bytes_with_method().unwrap(); handshake_bytes[0] &= !(crate::Helpers::MSG_RELIABLE | crate::Helpers::MSG_ZEROCODED); socket.send_to(&handshake_bytes, client_endpoint).unwrap(); let mut observed = Vec::new(); while observed.len() < 3 { let (length, _) = socket.recv_from(&mut buffer).expect("agent UDP packet"); if buffer[0] & crate::Helpers::MSG_RELIABLE != 0 { let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); socket.send_to(&ack(sequence), client_endpoint).unwrap(); } let Some(kind) = packet_type(&buffer[..length]) else { continue; }; if matches!( kind, PacketType::ChatFromViewer | PacketType::AgentAnimation | PacketType::SoundTrigger ) && !observed.contains(&kind) { observed.push(kind); } } udp_sender.send(observed).unwrap(); }); let requests = Arc::new(Mutex::new(Vec::new())); let recorded = Arc::clone(&requests); let handler = HttpMessageHandler::new(move |request, _| { let recorded = Arc::clone(&recorded); let gesture_bytes = gesture_bytes.clone(); async move { let is_seed = request.uri.0.contains("/seed"); recorded.lock().unwrap().push(request); let body = if is_seed { OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([( "ViewerAsset".into(), OSD::Uri(Uri("https://caps.test/viewer-asset".into())), )]))) .unwrap() } else { gesture_bytes }; HttpResponse { status_code: 200, headers: BTreeMap::new(), content_type: None, body, } } }); let mut client = GridClient::new().unwrap(); client.native_set_http_caps_client(crate::HttpCapsClient::new(handler).unwrap()); client.settings().timing().login_timeout = 2_000; let client = Arc::new(client); let mut network = client.network(); network.set_circuit_code(0x1234_5678); let simulator = network .native_connect( endpoint, 0x1000, true, Some(Uri("https://caps.test/seed".into())), 256, 256, ) .unwrap() .unwrap(); let caps = simulator.clone().caps.expect("seed capabilities"); let deadline = Instant::now() + Duration::from_secs(2); while caps.capability_uri("ViewerAsset".into()).unwrap().is_none() && Instant::now() < deadline { std::thread::sleep(Duration::from_millis(2)); } assert_eq!( caps.capability_uri("ViewerAsset".into()).unwrap(), Some(Uri("https://caps.test/viewer-asset".into())), "seed requests: {:?}", requests.lock().unwrap() ); let manager = AgentManager::native_new(Some(Arc::clone(&client))).unwrap(); manager.native_play_gesture(gesture_id, None).await.unwrap(); let observed = udp_receiver.recv_timeout(Duration::from_secs(3)).unwrap(); assert_eq!( observed, [ PacketType::ChatFromViewer, PacketType::AgentAnimation, PacketType::SoundTrigger, ] ); let requests = requests.lock().unwrap(); assert!( requests .iter() .any(|request| request.uri.0 == "https://caps.test/seed") ); assert!(requests.iter().any(|request| { request.uri.0 == format!("https://caps.test/viewer-asset?gesture_id={gesture_id}") })); drop(requests); simulator.native_disconnect(false).unwrap(); server.join().unwrap(); } }