Files
MetaCrate/crates/libremetaverse/src/agent_manager.rs
Chili Palmer 0791e0a69e
All checks were successful
Native code generation / deterministic (push) Successful in 12m6s
Imaging and meshing gate / native (push) Successful in 4m6s
Native Rust workspace compile / compile (push) Successful in 4m10s
Implement native agent movement and teleportation (#60)
2026-08-09 23:07:30 +00:00

4750 lines
173 KiB
Rust

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