1022 lines
38 KiB
Rust
1022 lines
38 KiB
Rust
//! 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<T> {
|
|
next: AtomicU64,
|
|
handlers: Mutex<HashMap<u64, EventHandler<T>>>,
|
|
}
|
|
|
|
impl<T> Default for EventSlot<T> {
|
|
fn default() -> Self {
|
|
Self {
|
|
next: AtomicU64::new(1),
|
|
handlers: Mutex::new(HashMap::new()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: 'static> EventSlot<T> {
|
|
fn subscribe(self: &Arc<Self>, handler: EventHandler<T>) -> 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<T: Clone + 'static> EventSlot<T> {
|
|
fn emit(&self, value: T) {
|
|
let handlers = self
|
|
.handlers
|
|
.lock()
|
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
.values()
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
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<Animation>,
|
|
avatar_id: UUID,
|
|
}
|
|
|
|
fn handle_avatar_animation(inner: &AvatarManagerInner, data: Vec<u8>) {
|
|
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<Animation>) -> Result<Self, Error> {
|
|
if animations.len() > 256 {
|
|
return Err(Error::Argument);
|
|
}
|
|
Ok(Self {
|
|
animations,
|
|
avatar_id,
|
|
})
|
|
}
|
|
|
|
pub(crate) fn native_animations(&self) -> Vec<Animation> {
|
|
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<EventSlot<crate::AvatarAnimationEventArgs>>,
|
|
avatar_appearance: Arc<EventSlot<crate::AvatarAppearanceEventArgs>>,
|
|
avatar_classified_reply: Arc<EventSlot<crate::AvatarClassifiedReplyEventArgs>>,
|
|
avatar_groups_reply: Arc<EventSlot<crate::AvatarGroupsReplyEventArgs>>,
|
|
avatar_interests_reply: Arc<EventSlot<crate::AvatarInterestsReplyEventArgs>>,
|
|
avatar_notes_reply: Arc<EventSlot<crate::AvatarNotesReplyEventArgs>>,
|
|
avatar_picker_reply: Arc<EventSlot<crate::AvatarPickerReplyEventArgs>>,
|
|
avatar_picks_reply: Arc<EventSlot<crate::AvatarPicksReplyEventArgs>>,
|
|
avatar_properties_reply: Arc<EventSlot<crate::AvatarPropertiesReplyEventArgs>>,
|
|
classified_info_reply: Arc<EventSlot<crate::ClassifiedInfoReplyEventArgs>>,
|
|
display_name_update: Arc<EventSlot<crate::DisplayNameUpdateEventArgs>>,
|
|
pick_info_reply: Arc<EventSlot<crate::PickInfoReplyEventArgs>>,
|
|
uuid_name_reply: Arc<EventSlot<crate::UUIDNameReplyEventArgs>>,
|
|
viewer_effect: Arc<EventSlot<crate::ViewerEffectEventArgs>>,
|
|
viewer_effect_look_at: Arc<EventSlot<crate::ViewerEffectLookAtEventArgs>>,
|
|
viewer_effect_point_at: Arc<EventSlot<crate::ViewerEffectPointAtEventArgs>>,
|
|
subscriptions: Mutex<Vec<Subscription>>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct AvatarManager(Arc<AvatarManagerInner>);
|
|
|
|
impl AvatarManager {
|
|
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
|
|
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<AvatarManagerInner> {
|
|
Arc::clone(&self.0)
|
|
}
|
|
|
|
pub(crate) fn native_from_inner(inner: Arc<AvatarManagerInner>) -> 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<crate::AvatarAnimationEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_animation.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_appearance(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarAppearanceEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_appearance.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_classified_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarClassifiedReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_classified_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_groups_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarGroupsReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_groups_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_interests_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarInterestsReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_interests_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_notes_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarNotesReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_notes_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_picker_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarPickerReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_picker_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_picks_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarPicksReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_picks_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_avatar_properties_reply(
|
|
&self,
|
|
handler: EventHandler<crate::AvatarPropertiesReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.avatar_properties_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_classified_info_reply(
|
|
&self,
|
|
handler: EventHandler<crate::ClassifiedInfoReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.classified_info_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_display_name_update(
|
|
&self,
|
|
handler: EventHandler<crate::DisplayNameUpdateEventArgs>,
|
|
) -> Subscription {
|
|
self.0.display_name_update.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_pick_info_reply(
|
|
&self,
|
|
handler: EventHandler<crate::PickInfoReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.pick_info_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_uuid_name_reply(
|
|
&self,
|
|
handler: EventHandler<crate::UUIDNameReplyEventArgs>,
|
|
) -> Subscription {
|
|
self.0.uuid_name_reply.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_viewer_effect(
|
|
&self,
|
|
handler: EventHandler<crate::ViewerEffectEventArgs>,
|
|
) -> Subscription {
|
|
self.0.viewer_effect.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_viewer_effect_look_at(
|
|
&self,
|
|
handler: EventHandler<crate::ViewerEffectLookAtEventArgs>,
|
|
) -> Subscription {
|
|
self.0.viewer_effect_look_at.subscribe(handler)
|
|
}
|
|
pub(crate) fn native_subscribe_viewer_effect_point_at(
|
|
&self,
|
|
handler: EventHandler<crate::ViewerEffectPointAtEventArgs>,
|
|
) -> Subscription {
|
|
self.0.viewer_effect_point_at.subscribe(handler)
|
|
}
|
|
|
|
pub(crate) fn native_capability_available(&self, name: &str) -> Result<bool, Error> {
|
|
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<crate::NetworkManager, Error> {
|
|
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<u8>,
|
|
) -> 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<UUID>,
|
|
cancellation_token: Option<CancellationToken>,
|
|
) -> Result<
|
|
(
|
|
bool,
|
|
Option<Vec<crate::AgentDisplayName>>,
|
|
Option<Vec<UUID>>,
|
|
),
|
|
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::<Vec<_>>()
|
|
.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<CancellationToken>,
|
|
) -> Result<(bool, Option<crate::messages::linden::AgentProfileMessage>), 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<OSDMap, Error> {
|
|
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]);
|
|
}
|
|
}
|