4750 lines
173 KiB
Rust
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();
|
|
}
|
|
}
|