//! Avatar profile and simulator-avatar state manager. #![allow( clippy::needless_pass_by_value, clippy::too_many_lines, clippy::unnecessary_wraps, clippy::unused_self )] // Public compatibility signatures and profile wire schemas are fixed. use crate::client_core::ClientWeakHandle; use crate::packet_catalog::PacketType; use crate::packets::{ AvatarAnimationPacket, AvatarPickerRequestPacket, AvatarPropertiesRequestPacket, ClassifiedInfoRequestPacket, GenericMessagePacket, GenericMessagePacketParamListBlock, TrackAgentPacket, UUIDNameRequestPacket, UUIDNameRequestPacketUUIDNameBlockBlock, }; use crate::{Error, GridClient}; use libremetaverse_structured_data::{OSD, OSDMap, OSDParser}; use libremetaverse_types::UUID; use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri}; use std::collections::HashMap; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; struct EventSlot { next: AtomicU64, handlers: Mutex>>, } impl Default for EventSlot { fn default() -> Self { Self { next: AtomicU64::new(1), handlers: Mutex::new(HashMap::new()), } } } impl EventSlot { fn subscribe(self: &Arc, handler: EventHandler) -> Subscription { let id = self.next.fetch_add(1, Ordering::Relaxed); self.handlers .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .insert(id, handler); let weak = Arc::downgrade(self); Subscription::new(move || { if let Some(slot) = weak.upgrade() { slot.handlers .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .remove(&id); } }) } fn clear(&self) { self.handlers .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); } } impl EventSlot { fn emit(&self, value: T) { let handlers = self .handlers .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .values() .cloned() .collect::>(); for handler in handlers { let value = value.clone(); let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(value))); } } } #[derive(Clone, Debug)] pub struct Animation { pub animation_id: UUID, pub animation_sequence: i32, pub animation_source_object_id: UUID, } #[derive(Clone, Debug)] pub struct AvatarAnimationEventArgs { animations: Vec, avatar_id: UUID, } fn handle_avatar_animation(inner: &AvatarManagerInner, data: Vec) { if inner.disposed.load(Ordering::Acquire) { return; } let mut offset = 0; let Ok(packet) = AvatarAnimationPacket::new_with_bytes_int32(data, &mut offset) else { return; }; if packet.animation_list.len() > 256 { return; } let animations = packet .animation_list .into_iter() .enumerate() .map(|(index, animation)| Animation { animation_id: animation.anim_id, animation_sequence: animation.anim_sequence_id, animation_source_object_id: packet .animation_source_list .get(index) .map_or_else(UUID::zero, |source| source.object_id), }) .collect(); inner.avatar_animation.emit(AvatarAnimationEventArgs { animations, avatar_id: packet.sender.id, }); } impl AvatarAnimationEventArgs { pub(crate) fn native_new(avatar_id: UUID, animations: Vec) -> Result { if animations.len() > 256 { return Err(Error::Argument); } Ok(Self { animations, avatar_id, }) } pub(crate) fn native_animations(&self) -> Vec { self.animations.clone() } pub(crate) fn native_avatar_id(&self) -> UUID { self.avatar_id } } pub(crate) struct AvatarManagerInner { client: ClientWeakHandle, disposed: AtomicBool, avatar_animation: Arc>, avatar_appearance: Arc>, avatar_classified_reply: Arc>, avatar_groups_reply: Arc>, avatar_interests_reply: Arc>, avatar_notes_reply: Arc>, avatar_picker_reply: Arc>, avatar_picks_reply: Arc>, avatar_properties_reply: Arc>, classified_info_reply: Arc>, display_name_update: Arc>, pick_info_reply: Arc>, uuid_name_reply: Arc>, viewer_effect: Arc>, viewer_effect_look_at: Arc>, viewer_effect_point_at: Arc>, subscriptions: Mutex>, } #[derive(Clone)] pub struct AvatarManager(Arc); impl AvatarManager { pub(crate) fn native_new(client: Option>) -> Result { let client = client.ok_or(Error::ArgumentNull)?; let inner = Arc::new(AvatarManagerInner { client: client.native_weak_handle(), disposed: AtomicBool::new(false), avatar_animation: Arc::default(), avatar_appearance: Arc::default(), avatar_classified_reply: Arc::default(), avatar_groups_reply: Arc::default(), avatar_interests_reply: Arc::default(), avatar_notes_reply: Arc::default(), avatar_picker_reply: Arc::default(), avatar_picks_reply: Arc::default(), avatar_properties_reply: Arc::default(), classified_info_reply: Arc::default(), display_name_update: Arc::default(), pick_info_reply: Arc::default(), uuid_name_reply: Arc::default(), viewer_effect: Arc::default(), viewer_effect_look_at: Arc::default(), viewer_effect_point_at: Arc::default(), subscriptions: Mutex::new(Vec::new()), }); let weak = Arc::downgrade(&inner); let subscription = client .native_network()? .subscribe_raw_packet(Arc::new(move |event| { if event.packet_type != PacketType::AvatarAnimation { return; } let Some(inner) = weak.upgrade() else { return }; handle_avatar_animation(&inner, event.data); })); inner .subscriptions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .push(subscription); Ok(Self(inner)) } pub(crate) fn native_inner(&self) -> Arc { Arc::clone(&self.0) } pub(crate) fn native_from_inner(inner: Arc) -> Self { Self(inner) } pub(crate) fn native_dispose(&self) -> Result<(), Error> { self.0.disposed.store(true, Ordering::Release); self.0 .subscriptions .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clear(); self.0.avatar_animation.clear(); self.0.avatar_appearance.clear(); self.0.avatar_classified_reply.clear(); self.0.avatar_groups_reply.clear(); self.0.avatar_interests_reply.clear(); self.0.avatar_notes_reply.clear(); self.0.avatar_picker_reply.clear(); self.0.avatar_picks_reply.clear(); self.0.avatar_properties_reply.clear(); self.0.classified_info_reply.clear(); self.0.display_name_update.clear(); self.0.pick_info_reply.clear(); self.0.uuid_name_reply.clear(); self.0.viewer_effect.clear(); self.0.viewer_effect_look_at.clear(); self.0.viewer_effect_point_at.clear(); Ok(()) } pub(crate) fn native_subscribe_avatar_animation( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_animation.subscribe(handler) } pub(crate) fn native_subscribe_avatar_appearance( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_appearance.subscribe(handler) } pub(crate) fn native_subscribe_avatar_classified_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_classified_reply.subscribe(handler) } pub(crate) fn native_subscribe_avatar_groups_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_groups_reply.subscribe(handler) } pub(crate) fn native_subscribe_avatar_interests_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_interests_reply.subscribe(handler) } pub(crate) fn native_subscribe_avatar_notes_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_notes_reply.subscribe(handler) } pub(crate) fn native_subscribe_avatar_picker_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_picker_reply.subscribe(handler) } pub(crate) fn native_subscribe_avatar_picks_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_picks_reply.subscribe(handler) } pub(crate) fn native_subscribe_avatar_properties_reply( &self, handler: EventHandler, ) -> Subscription { self.0.avatar_properties_reply.subscribe(handler) } pub(crate) fn native_subscribe_classified_info_reply( &self, handler: EventHandler, ) -> Subscription { self.0.classified_info_reply.subscribe(handler) } pub(crate) fn native_subscribe_display_name_update( &self, handler: EventHandler, ) -> Subscription { self.0.display_name_update.subscribe(handler) } pub(crate) fn native_subscribe_pick_info_reply( &self, handler: EventHandler, ) -> Subscription { self.0.pick_info_reply.subscribe(handler) } pub(crate) fn native_subscribe_uuid_name_reply( &self, handler: EventHandler, ) -> Subscription { self.0.uuid_name_reply.subscribe(handler) } pub(crate) fn native_subscribe_viewer_effect( &self, handler: EventHandler, ) -> Subscription { self.0.viewer_effect.subscribe(handler) } pub(crate) fn native_subscribe_viewer_effect_look_at( &self, handler: EventHandler, ) -> Subscription { self.0.viewer_effect_look_at.subscribe(handler) } pub(crate) fn native_subscribe_viewer_effect_point_at( &self, handler: EventHandler, ) -> Subscription { self.0.viewer_effect_point_at.subscribe(handler) } pub(crate) fn native_capability_available(&self, name: &str) -> Result { if self.0.disposed.load(Ordering::Acquire) { return Ok(false); } let Some(client) = self.0.client.upgrade() else { return Ok(false); }; let network = client.native_network()?; let Some(simulator) = network.native_current_sim() else { return Ok(false); }; let Some(caps) = simulator.native_caps() else { return Ok(false); }; Ok(caps.capability_uri(name.to_owned())?.is_some()) } pub(crate) fn native_request_own_avatar_textures(&self) -> Result<(), Error> { if self.0.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } let Some(client) = self.0.client.upgrade() else { return Ok(()); }; let network = client.native_network()?; if network.native_current_sim().is_none() { return Ok(()); } self.send_generic( &network, "avatartexturesrequest", &[network.native_agent_id()], ) } fn live_network(&self) -> Result { if self.0.disposed.load(Ordering::Acquire) { return Err(Error::InvalidOperation); } self.0 .client .upgrade() .ok_or(Error::InvalidOperation)? .native_network() } fn send_bytes( &self, network: &crate::NetworkManager, packet_type: PacketType, bytes: Vec, ) -> Result<(), Error> { let simulator = network .native_current_sim() .ok_or(Error::InvalidOperation)?; simulator.native_send_packet_data( bytes.clone(), i32::try_from(bytes.len()).map_err(|_| Error::Argument)?, packet_type, false, ) } fn send_generic( &self, network: &crate::NetworkManager, method: &str, parameters: &[UUID], ) -> Result<(), Error> { let mut packet = GenericMessagePacket::new_with_constructor()?; let agent_id = network.native_agent_id(); packet.agent_data.agent_id = agent_id; packet.agent_data.session_id = network.native_session_id(); packet.agent_data.transaction_id = UUID::zero(); packet.method_data.method = method.as_bytes().to_vec(); packet.method_data.invoice = UUID::zero(); packet.param_list = parameters .iter() .map(|value| GenericMessagePacketParamListBlock { parameter: value.to_string().into_bytes(), }) .collect(); self.send_bytes( network, PacketType::GenericMessage, packet.to_bytes_with_method()?, ) } pub(crate) fn native_request_track_agent(&self, target: UUID) -> Result<(), Error> { let network = self.live_network()?; let mut packet = TrackAgentPacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.target_data.prey_id = target; self.send_bytes( &network, PacketType::TrackAgent, packet.to_bytes_with_method()?, ) } pub(crate) fn native_request_avatar_names(&self, ids: &[UUID]) -> Result<(), Error> { const MAX_UUIDS_PER_PACKET: usize = 100; if ids.len() > 100_000 { return Err(Error::Argument); } let network = self.live_network()?; for chunk in ids.chunks(MAX_UUIDS_PER_PACKET) { let mut packet = UUIDNameRequestPacket::new_with_constructor()?; packet.uuid_name_block = chunk .iter() .copied() .map(|id| UUIDNameRequestPacketUUIDNameBlockBlock { id }) .collect(); self.send_bytes( &network, PacketType::UUIDNameRequest, packet.to_bytes_with_method()?, )?; } Ok(()) } pub(crate) fn native_request_avatar_properties(&self, avatar_id: UUID) -> Result<(), Error> { let network = self.live_network()?; let mut packet = AvatarPropertiesRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.agent_data.avatar_id = avatar_id; self.send_bytes( &network, PacketType::AvatarPropertiesRequest, packet.to_bytes_with_method()?, ) } pub(crate) fn native_request_avatar_name_search( &self, name: String, query_id: UUID, ) -> Result<(), Error> { if name.len() > 255 { return Err(Error::Argument); } let network = self.live_network()?; let mut packet = AvatarPickerRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.agent_data.query_id = query_id; packet.data.name = name.into_bytes(); self.send_bytes( &network, PacketType::AvatarPickerRequest, packet.to_bytes_with_method()?, ) } pub(crate) fn native_request_classified_info(&self, classified_id: UUID) -> Result<(), Error> { let network = self.live_network()?; let mut packet = ClassifiedInfoRequestPacket::new_with_constructor()?; packet.agent_data.agent_id = network.native_agent_id(); packet.agent_data.session_id = network.native_session_id(); packet.data.classified_id = classified_id; self.send_bytes( &network, PacketType::ClassifiedInfoRequest, packet.to_bytes_with_method()?, ) } pub(crate) fn native_request_generic( &self, method: &str, parameters: &[UUID], ) -> Result<(), Error> { let network = self.live_network()?; self.send_generic(&network, method, parameters) } pub(crate) async fn native_get_display_names( &self, ids: Vec, cancellation_token: Option, ) -> Result< ( bool, Option>, Option>, ), Error, > { if ids.is_empty() || ids.len() > 10_000 { return Ok((false, None, None)); } let Some(client) = self.0.client.upgrade() else { return Ok((false, None, None)); }; let token = cancellation_token.unwrap_or_else(|| client.cancellation_token()); token.throw_if_cancellation_requested()?; let network = client.native_network()?; let Some(simulator) = network.native_current_sim() else { return Ok((false, None, None)); }; let Some(caps) = simulator.native_caps() else { return Ok((false, None, None)); }; let Some(base) = caps.capability_uri("GetDisplayNames".to_owned())? else { return Ok((false, None, None)); }; let http = client.native_http_caps_client(); let mut names = Vec::new(); let mut bad_ids = Vec::new(); for chunk in ids.chunks(90) { token.throw_if_cancellation_requested()?; let query = chunk .iter() .map(ToString::to_string) .collect::>() .join("&ids="); let separator = if base.0.contains('?') { '&' } else { '?' }; let uri = Uri(format!("{}{separator}ids={query}", base.0)); let (response, bytes) = http.get(uri, token.clone(), None).await?; if !response.is_success_status_code() { return Ok((false, None, None)); } let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else { return Ok((false, None, None)); }; let mut message = crate::messages::linden::GetDisplayNamesMessage::new()?; message.deserialize(OSDMap::new_with_dictionary(map)?)?; names.extend(message.agents); bad_ids.extend(message.bad_i_ds); } Ok((true, Some(names), Some(bad_ids))) } pub(crate) async fn native_request_agent_profile( &self, avatar_id: UUID, cancellation_token: Option, ) -> Result<(bool, Option), Error> { let Some(client) = self.0.client.upgrade() else { return Ok((false, None)); }; let token = cancellation_token.unwrap_or_else(|| client.cancellation_token()); token.throw_if_cancellation_requested()?; let network = client.native_network()?; let Some(simulator) = network.native_current_sim() else { return Ok((false, None)); }; let Some(caps) = simulator.native_caps() else { return Ok((false, None)); }; let Some(base) = caps.capability_uri("AgentProfile".to_owned())? else { return Ok((false, None)); }; let uri = Uri(format!("{}/{}", base.0.trim_end_matches('/'), avatar_id)); let (response, bytes) = client .native_http_caps_client() .get(uri, token, None) .await?; if !response.is_success_status_code() { return Ok((false, None)); } let OSD::Map(map) = OSDParser::deserialize_with_bytes(bytes)? else { return Ok((false, None)); }; let mut profile = crate::messages::linden::AgentProfileMessage::new()?; profile.deserialize(OSDMap::new_with_dictionary(map)?)?; Ok((true, Some(profile))) } } pub(crate) fn agent_profile_group_defaults() -> crate::messages::linden::AgentProfileMessageGroupData { crate::messages::linden::AgentProfileMessageGroupData { description: String::new(), enabled: false, founder_id: UUID::zero(), id: UUID::zero(), image_id: UUID::zero(), is_mature_publish: false, is_open_enrollment: false, is_shown_in_search: false, name: String::new(), } } fn agent_profile_pick_defaults() -> crate::messages::linden::AgentProfileMessagePickData { crate::messages::linden::AgentProfileMessagePickData { description: String::new(), enabled: false, grid_x: 0.0, grid_y: 0.0, id: UUID::zero(), name: String::new(), parcel_id: UUID::zero(), parcel_name: String::new(), region_name: String::new(), region_x: 0.0, region_y: 0.0, region_z: 0.0, slurl: Uri("about:blank".to_owned()), snapshot_id: UUID::zero(), } } pub(crate) fn agent_profile_defaults() -> crate::messages::linden::AgentProfileMessage { crate::messages::linden::AgentProfileMessage { allowed_maturity: crate::SimAccess(0), avatar_id: UUID::zero(), caption_index: None, caption_text: None, customer_type: String::new(), display_name: String::new(), display_name_next_update: std::time::UNIX_EPOCH, first_life_about_text: String::new(), first_life_image_id: UUID::zero(), flags: crate::ProfileFlags(0), groups: Vec::new(), hide_age: None, home_page: String::new(), is_display_name_default: false, is_mature_profile: false, legacy_first_name: String::new(), legacy_last_name: String::new(), member_since: std::time::UNIX_EPOCH, notes: String::new(), partner_id: UUID::zero(), picks: Vec::new(), preferred_maturity: crate::SimAccess(0), second_life_about_text: String::new(), second_life_image_id: UUID::zero(), title: String::new(), username: String::new(), } } fn value(map: &OSDMap, key: &str) -> OSD { map.get(key).unwrap_or(OSD::Undefined) } pub(crate) fn deserialize_agent_profile( profile: &mut crate::messages::linden::AgentProfileMessage, map: &OSDMap, ) -> Result<(), Error> { profile.avatar_id = value(map, "id").as_uuid()?; profile.second_life_image_id = value(map, "sl_image_id").as_uuid()?; profile.first_life_image_id = value(map, "fl_image_id").as_uuid()?; profile.partner_id = value(map, "partner_id").as_uuid()?; profile.second_life_about_text = value(map, "sl_about_text").as_string()?; profile.first_life_about_text = value(map, "fl_about_text").as_string()?; profile.member_since = value(map, "member_since").as_date()?; profile.customer_type = value(map, "customer_type").as_string()?; profile.notes = value(map, "notes").as_string()?; profile.allowed_maturity = crate::SimAccess( u8::try_from(value(map, "allowed_maturity").as_integer()?).map_err(|_| Error::Argument)?, ); profile.preferred_maturity = crate::SimAccess( u8::try_from(value(map, "preferred_maturity").as_integer()?) .map_err(|_| Error::Argument)?, ); profile.display_name = value(map, "display_name").as_string()?; profile.display_name_next_update = value(map, "display_name_next_update").as_date()?; profile.is_display_name_default = value(map, "is_display_name_default").as_boolean()?; profile.username = value(map, "username").as_string()?; profile.legacy_first_name = value(map, "legacy_first_name").as_string()?; profile.legacy_last_name = value(map, "legacy_last_name").as_string()?; profile.is_mature_profile = value(map, "mature_profile").as_boolean()?; profile.title = value(map, "title").as_string()?; profile.home_page = value(map, "home_page").as_string()?; profile.hide_age = map .get("hide_age") .map(|entry| entry.as_boolean().map(Some)) .transpose()?; profile.caption_index = map .get("charter_member") .map(|entry| entry.as_integer().map(Some)) .transpose()?; profile.caption_text = map .get("caption") .map(|entry| entry.as_string()) .transpose()?; let mut flags = 0; for (key, flag) in [ ("online", crate::ProfileFlags::ONLINE.0), ("allow_publish", crate::ProfileFlags::ALLOW_PUBLISH.0), ("identified", crate::ProfileFlags::IDENTIFIED.0), ("transacted", crate::ProfileFlags::TRANSACTED.0), ] { if value(map, key).as_boolean()? { flags |= flag; } } profile.flags = crate::ProfileFlags(flags); profile.groups.clear(); if let OSD::Array(groups) = value(map, "groups") { if groups.len() > 512 { return Err(Error::Argument); } for group in groups { let OSD::Map(group) = group else { continue }; let group = OSDMap::new_with_dictionary(group)?; profile .groups .push(crate::messages::linden::AgentProfileMessageGroupData { description: value(&group, "description").as_string()?, enabled: value(&group, "enabled").as_boolean()?, founder_id: value(&group, "founder_id").as_uuid()?, id: value(&group, "id").as_uuid()?, image_id: value(&group, "image_id").as_uuid()?, is_mature_publish: value(&group, "mature_publish").as_boolean()?, is_open_enrollment: value(&group, "open_enrollment").as_boolean()?, is_shown_in_search: value(&group, "show_in_search").as_boolean()?, name: value(&group, "name").as_string()?, }); } } profile.picks.clear(); if let OSD::Array(picks) = value(map, "picks") { if picks.len() > 512 { return Err(Error::Argument); } for pick in picks { let OSD::Map(pick) = pick else { continue }; let pick = OSDMap::new_with_dictionary(pick)?; let mut result = agent_profile_pick_defaults(); result.description = value(&pick, "description").as_string()?; result.enabled = value(&pick, "enabled").as_boolean()?; result.grid_x = value(&pick, "grid_x").as_real()?; result.grid_y = value(&pick, "grid_y").as_real()?; result.id = value(&pick, "id").as_uuid()?; result.name = value(&pick, "name").as_string()?; result.parcel_id = value(&pick, "parcel_id").as_uuid()?; result.parcel_name = value(&pick, "parcel_name").as_string()?; result.region_name = value(&pick, "region_name").as_string()?; result.region_x = value(&pick, "region_x").as_real()?; result.region_y = value(&pick, "region_y").as_real()?; result.region_z = value(&pick, "region_z").as_real()?; result.slurl = value(&pick, "slurl") .as_uri()? .unwrap_or_else(|| Uri("about:blank".to_owned())); result.snapshot_id = value(&pick, "snapshot_id").as_uuid()?; profile.picks.push(result); } } Ok(()) } pub(crate) fn serialize_agent_profile( profile: &crate::messages::linden::AgentProfileMessage, ) -> Result { let mut map = std::collections::HashMap::from([ ("id".to_owned(), OSD::UUID(profile.avatar_id)), ( "sl_image_id".to_owned(), OSD::UUID(profile.second_life_image_id), ), ( "fl_image_id".to_owned(), OSD::UUID(profile.first_life_image_id), ), ("partner_id".to_owned(), OSD::UUID(profile.partner_id)), ( "sl_about_text".to_owned(), OSD::String(profile.second_life_about_text.clone()), ), ( "fl_about_text".to_owned(), OSD::String(profile.first_life_about_text.clone()), ), ("member_since".to_owned(), OSD::Date(profile.member_since)), ( "customer_type".to_owned(), OSD::String(profile.customer_type.clone()), ), ("notes".to_owned(), OSD::String(profile.notes.clone())), ( "allowed_maturity".to_owned(), OSD::Integer(i32::from(profile.allowed_maturity.0)), ), ( "preferred_maturity".to_owned(), OSD::Integer(i32::from(profile.preferred_maturity.0)), ), ( "display_name".to_owned(), OSD::String(profile.display_name.clone()), ), ( "display_name_next_update".to_owned(), OSD::Date(profile.display_name_next_update), ), ( "is_display_name_default".to_owned(), OSD::Boolean(profile.is_display_name_default), ), ("username".to_owned(), OSD::String(profile.username.clone())), ( "legacy_first_name".to_owned(), OSD::String(profile.legacy_first_name.clone()), ), ( "legacy_last_name".to_owned(), OSD::String(profile.legacy_last_name.clone()), ), ( "mature_profile".to_owned(), OSD::Boolean(profile.is_mature_profile), ), ("title".to_owned(), OSD::String(profile.title.clone())), ( "home_page".to_owned(), OSD::String(profile.home_page.clone()), ), ]); for (key, flag) in [ ("online", crate::ProfileFlags::ONLINE.0), ("allow_publish", crate::ProfileFlags::ALLOW_PUBLISH.0), ("identified", crate::ProfileFlags::IDENTIFIED.0), ("transacted", crate::ProfileFlags::TRANSACTED.0), ] { if profile.flags.0 & flag != 0 { map.insert(key.to_owned(), OSD::Boolean(true)); } } if let Some(Some(value)) = profile.hide_age { map.insert("hide_age".to_owned(), OSD::Boolean(value)); } if let Some(Some(value)) = profile.caption_index { map.insert("charter_member".to_owned(), OSD::Integer(value)); } else if let Some(value) = &profile.caption_text { map.insert("caption".to_owned(), OSD::String(value.clone())); } if profile.groups.len() > 512 || profile.picks.len() > 512 { return Err(Error::Argument); } map.insert( "groups".to_owned(), OSD::Array( profile .groups .iter() .map(|group| { OSD::Map(std::collections::HashMap::from([ ( "description".to_owned(), OSD::String(group.description.clone()), ), ("enabled".to_owned(), OSD::Boolean(group.enabled)), ("founder_id".to_owned(), OSD::UUID(group.founder_id)), ("id".to_owned(), OSD::UUID(group.id)), ("image_id".to_owned(), OSD::UUID(group.image_id)), ( "mature_publish".to_owned(), OSD::Boolean(group.is_mature_publish), ), ("name".to_owned(), OSD::String(group.name.clone())), ( "open_enrollment".to_owned(), OSD::Boolean(group.is_open_enrollment), ), ( "show_in_search".to_owned(), OSD::Boolean(group.is_shown_in_search), ), ])) }) .collect(), ), ); map.insert( "picks".to_owned(), OSD::Array( profile .picks .iter() .map(|pick| { OSD::Map(std::collections::HashMap::from([ ( "description".to_owned(), OSD::String(pick.description.clone()), ), ("enabled".to_owned(), OSD::Boolean(pick.enabled)), ("grid_x".to_owned(), OSD::Real(pick.grid_x)), ("grid_y".to_owned(), OSD::Real(pick.grid_y)), ("id".to_owned(), OSD::UUID(pick.id)), ("name".to_owned(), OSD::String(pick.name.clone())), ("parcel_id".to_owned(), OSD::UUID(pick.parcel_id)), ( "parcel_name".to_owned(), OSD::String(pick.parcel_name.clone()), ), ( "region_name".to_owned(), OSD::String(pick.region_name.clone()), ), ("region_x".to_owned(), OSD::Real(pick.region_x)), ("region_y".to_owned(), OSD::Real(pick.region_y)), ("region_z".to_owned(), OSD::Real(pick.region_z)), ("slurl".to_owned(), OSD::Uri(pick.slurl.clone())), ("snapshot_id".to_owned(), OSD::UUID(pick.snapshot_id)), ])) }) .collect(), ), ); OSDMap::new_with_dictionary(map) } #[cfg(test)] mod tests { use super::*; use crate::packets::{ AvatarAnimationPacketAnimationListBlock, AvatarAnimationPacketAnimationSourceListBlock, }; #[test] fn avatar_animation_packet_emits_complete_active_animation_list() { let client = GridClient::new().expect("client"); let manager = client.avatars(); let received = Arc::new(Mutex::new(Vec::new())); let values = Arc::clone(&received); let _subscription = manager.native_subscribe_avatar_animation(Arc::new(move |event| { values.lock().expect("event lock").push(event); })); let avatar_id = UUID::random().expect("avatar id"); let animation_id = UUID::random().expect("animation id"); let source_id = UUID::random().expect("source id"); let mut packet = AvatarAnimationPacket::new_with_constructor().expect("packet"); packet.sender.id = avatar_id; let mut animation = AvatarAnimationPacketAnimationListBlock::new_with_constructor().expect("animation"); animation.anim_id = animation_id; animation.anim_sequence_id = 42; packet.animation_list.push(animation); let mut source = AvatarAnimationPacketAnimationSourceListBlock::new_with_constructor().expect("source"); source.object_id = source_id; packet.animation_source_list.push(source); handle_avatar_animation( &manager.0, packet.to_bytes_with_method().expect("packet bytes"), ); let events = received.lock().expect("event lock"); assert_eq!(events.len(), 1); assert_eq!(events[0].native_avatar_id(), avatar_id); let animations = events[0].native_animations(); assert_eq!(animations.len(), 1); assert_eq!(animations[0].animation_id, animation_id); assert_eq!(animations[0].animation_sequence, 42); assert_eq!(animations[0].animation_source_object_id, source_id); } #[test] fn avatar_animation_empty_list_signals_stopped_animations() { let client = GridClient::new().expect("client"); let manager = client.avatars(); let received = Arc::new(Mutex::new(Vec::new())); let values = Arc::clone(&received); let _subscription = manager.native_subscribe_avatar_animation(Arc::new(move |event| { values .lock() .expect("event lock") .push(event.native_animations().len()); })); let mut packet = AvatarAnimationPacket::new_with_constructor().expect("packet"); packet.sender.id = UUID::random().expect("avatar id"); handle_avatar_animation( &manager.0, packet.to_bytes_with_method().expect("packet bytes"), ); assert_eq!(*received.lock().expect("event lock"), [0]); } }