Files
MetaCrate/crates/libremetaverse/src/avatar_manager.rs
Chili Palmer ed516a1a5d
Some checks failed
Native code generation / deterministic (push) Failing after 2m20s
Imaging and meshing gate / native (push) Failing after 1m25s
Native Rust workspace compile / compile (push) Failing after 56s
Implement avatar animation and skinning (#67)
2026-08-10 11:45:07 +00:00

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]);
}
}