Files
MetaCrate/crates/libremetaverse/src/inventory_manager.rs
Chili Palmer 52f62d8c39
Some checks failed
Native code generation / deterministic (push) Failing after 2m18s
Imaging and meshing gate / native (push) Failing after 1m30s
JPEG 2000 feature / linux (push) Successful in 2m40s
Native Rust workspace compile / compile (push) Failing after 57s
Skia feature / linux (push) Successful in 31m8s
Implement native asset pipeline and cache (#64)
2026-08-10 07:51:17 +00:00

4915 lines
179 KiB
Rust

//! Native inventory protocol manager.
//!
//! This module owns request correlation and wire behavior. The hierarchy in
//! [`crate::inventory`] remains a transport-free store, which lets packet and
//! capability replies be validated before they become observable local state.
//! Store snapshots and subscriber lists are cloned before asynchronous work or
//! event delivery, so callbacks are not invoked while an inventory lock is held.
#![allow(clippy::missing_errors_doc)] // Public signatures mirror the C# API.
#![allow(clippy::must_use_candidate)]
#![allow(clippy::needless_pass_by_value)]
#![allow(clippy::too_many_lines)] // Protocol parsers intentionally keep each wire state machine together.
#![allow(clippy::unnecessary_wraps)] // Mapped C# methods require Result-shaped Rust APIs.
use crate::agent_manager::{EventRegistry, InstantMessage, InstantMessageEventArgs};
use crate::client_core::ClientWeakHandle;
use crate::inventory::{
Inventory, InventoryAnimation, InventoryAttachment, InventoryBase, InventoryCallingCard,
InventoryCategory, InventoryFolder, InventoryGesture, InventoryItem, InventoryLSL,
InventoryLandmark, InventoryMaterial, InventoryNotecard, InventoryObject, InventorySettings,
InventorySnapshot, InventorySound, InventoryTexture, InventoryWearable, Permissions,
};
use crate::messages::linden::{CopyInventoryFromNotecardMessage, ScriptRunningReplyMessage};
use crate::network_manager::{CapsEventQueueCallback, RawPacketReceivedEventArgs, Simulator};
use crate::packet_catalog::GeneratedPacket;
use crate::packets::{
AbortXferPacket, BulkUpdateInventoryPacket, ConfirmXferPacketPacket,
CopyInventoryFromNotecardPacket, CopyInventoryFromNotecardPacketInventoryDataBlock,
CopyInventoryItemPacket, CopyInventoryItemPacketInventoryDataBlock,
CreateInventoryFolderPacket, CreateInventoryItemPacket, DeRezObjectPacket,
DeRezObjectPacketObjectDataBlock, FetchInventoryPacket, FetchInventoryPacketInventoryDataBlock,
FetchInventoryReplyPacket, GetScriptRunningPacket, InventoryDescendentsPacket,
LinkInventoryItemPacket, MoveInventoryFolderPacket,
MoveInventoryFolderPacketInventoryDataBlock, MoveInventoryItemPacket,
MoveInventoryItemPacketInventoryDataBlock, MoveTaskInventoryPacket, PacketType,
PurgeInventoryDescendentsPacket, RemoveInventoryObjectsPacket,
RemoveInventoryObjectsPacketFolderDataBlock, RemoveInventoryObjectsPacketItemDataBlock,
RemoveTaskInventoryPacket, ReplyTaskInventoryPacket, RequestTaskInventoryPacket,
RequestXferPacket, RezObjectPacket, RezRestoreToWorldPacket, RezScriptPacket,
SaveAssetIntoInventoryPacket, SendXferPacketPacket, SetScriptRunningPacket,
UpdateCreateInventoryItemPacket, UpdateInventoryFolderPacket,
UpdateInventoryFolderPacketFolderDataBlock, UpdateInventoryItemPacket,
UpdateInventoryItemPacketInventoryDataBlock, UpdateTaskInventoryPacket,
};
use crate::{
DeRezDestination, GridClient, InstantMessageDialog, InstantMessageOnline, InventoryItemClass,
InventoryObjectClass, InventorySortOrder, PermissionMask,
};
use crate::{LoginResponseData, NetworkManagerLoginResponseCallback};
use futures_channel::oneshot;
use futures_util::future::{Either, select};
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{
CancellationToken, EventHandler, ExternalError, Subscription, Task, Uri,
};
use libremetaverse_types::{
AssetType, EnumValueMetadata, Error, FolderType, InventoryType, Quaternion, SaleType, UUID,
Utils, Vector3, Vector3d, WearableType,
};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, RwLock};
use std::thread::JoinHandle;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
const MAGIC_ID_TEXT: &str = "3c115e51-04f4-523c-9fa6-98aff1034730";
const MAX_TASK_INVENTORY_BYTES: usize = 16 * 1024 * 1024;
const MAX_TASK_INVENTORY_ENTRIES: usize = 100_000;
const MAX_PENDING_REQUESTS: usize = 4_096;
const CALLBACK_TIMEOUT: Duration = Duration::from_mins(1);
const CALLBACK_SWEEP_INTERVAL: Duration = Duration::from_millis(100);
const MAX_INVENTORY_RECURSION_DEPTH: usize = 512;
const MAX_INVENTORY_TRAVERSAL: usize = 100_000;
macro_rules! fill_inventory_wire_block {
($block:expr, $item:expr, $transaction_id:expr, $type_value:expr) => {{
let item = $item;
let permissions = item.permissions();
$block.item_id = item.base.uuid();
$block.folder_id = item.base.parent_uuid();
$block.creator_id = item.creator_id();
$block.owner_id = item.base.owner_id();
$block.group_id = item.group_id();
$block.base_mask = permissions.base_mask.0;
$block.owner_mask = permissions.owner_mask.0;
$block.group_mask = permissions.group_mask.0;
$block.everyone_mask = permissions.everyone_mask.0;
$block.next_owner_mask = permissions.next_owner_mask.0;
$block.group_owned = item.group_owned();
$block.transaction_id = $transaction_id;
$block.type_ = $type_value;
$block.inv_type = item.inventory_type().0;
$block.flags = item.flags();
$block.sale_type = item.sale_type() as u8;
$block.sale_price = item.sale_price();
$block.name = Utils::string_to_bytes(item.base.name())?;
$block.description = Utils::string_to_bytes(item.description())?;
$block.creation_date = unix_seconds_i32(item.creation_date());
}};
}
fn mutex<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
value
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn read<T>(value: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
value
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write<T>(value: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
value
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn expire_waiters<T>(
requests: &Mutex<HashMap<UUID, Vec<T>>>,
now: Instant,
deadline: impl Fn(&T) -> Instant,
) -> Vec<T> {
let mut requests = mutex(requests);
let keys: Vec<_> = requests.keys().copied().collect();
let mut expired = Vec::new();
for key in keys {
if let Some(waiters) = requests.get_mut(&key) {
let mut index = 0;
while index < waiters.len() {
if deadline(&waiters[index]) <= now {
expired.push(waiters.swap_remove(index));
} else {
index += 1;
}
}
if waiters.is_empty() {
requests.remove(&key);
}
}
}
expired
}
fn wire_string(mut value: Vec<u8>) -> String {
while value.last() == Some(&0) {
value.pop();
}
String::from_utf8_lossy(&value).into_owned()
}
fn decode_packet<T: GeneratedPacket>(bytes: &[u8]) -> Result<T, Error> {
let mut packet = T::new_generated();
let mut position = 0_i32;
let mut packet_end = i32::try_from(bytes.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(
bytes,
&mut position,
&mut packet_end,
Some(zero_buffer.as_mut_slice()),
)?;
Ok(packet)
}
fn unix_seconds_i64(value: SystemTime) -> i64 {
match value.duration_since(UNIX_EPOCH) {
Ok(duration) => i64::try_from(duration.as_secs()).unwrap_or(i64::MAX),
Err(error) => -i64::try_from(error.duration().as_secs()).unwrap_or(i64::MAX),
}
}
fn unix_seconds_u32_wrapping(value: SystemTime) -> u32 {
let bytes = unix_seconds_i64(value).to_le_bytes();
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
fn uuid_crc(value: UUID) -> u32 {
let mut value = value;
value.crc().unwrap_or_default()
}
fn system_time_from_unix_i32(seconds: i32) -> SystemTime {
if seconds >= 0 {
UNIX_EPOCH + Duration::from_secs(seconds.unsigned_abs().into())
} else {
UNIX_EPOCH - Duration::from_secs(seconds.unsigned_abs().into())
}
}
fn unix_seconds_i32(value: SystemTime) -> i32 {
i32::try_from(unix_seconds_i64(value)).unwrap_or_else(|_| {
if value >= UNIX_EPOCH {
i32::MAX
} else {
i32::MIN
}
})
}
fn asset_type_from_i8(value: i8) -> AssetType {
match value {
-1 => AssetType::Unknown,
0 => AssetType::Texture,
1 => AssetType::Sound,
2 => AssetType::CallingCard,
3 => AssetType::Landmark,
4 => AssetType::Script,
5 => AssetType::Clothing,
6 => AssetType::Object,
7 => AssetType::Notecard,
8 => AssetType::Folder,
10 => AssetType::LSLText,
11 => AssetType::LSLBytecode,
12 => AssetType::TextureTGA,
13 => AssetType::Bodypart,
17 => AssetType::SoundWAV,
18 => AssetType::ImageTGA,
19 => AssetType::ImageJPEG,
20 => AssetType::Animation,
21 => AssetType::Gesture,
22 => AssetType::Simstate,
24 => AssetType::Link,
25 => AssetType::LinkFolder,
40 => AssetType::Widget,
45 => AssetType::Person,
49 => AssetType::Mesh,
56 => AssetType::Settings,
57 => AssetType::Material,
_ => AssetType::Unknown,
}
}
fn folder_type_from_i8(value: i8) -> FolderType {
folder_type_from_i8_checked(value).unwrap_or(FolderType::None)
}
fn folder_type_from_i8_checked(value: i8) -> Option<FolderType> {
match value {
-1 => Some(FolderType::None),
0 => Some(FolderType::Texture),
1 => Some(FolderType::Sound),
2 => Some(FolderType::CallingCard),
3 => Some(FolderType::Landmark),
5 => Some(FolderType::Clothing),
6 => Some(FolderType::Object),
7 => Some(FolderType::Notecard),
8 => Some(FolderType::Root),
9 => Some(FolderType::OldRoot),
10 => Some(FolderType::LSLText),
13 => Some(FolderType::BodyPart),
14 => Some(FolderType::Trash),
15 => Some(FolderType::Snapshot),
16 => Some(FolderType::LostAndFound),
20 => Some(FolderType::Animation),
21 => Some(FolderType::Gesture),
23 => Some(FolderType::Favorites),
26 => Some(FolderType::EnsembleStart),
45 => Some(FolderType::EnsembleEnd),
46 => Some(FolderType::CurrentOutfit),
47 => Some(FolderType::Outfit),
48 => Some(FolderType::MyOutfits),
49 => Some(FolderType::Mesh),
50 => Some(FolderType::Inbox),
51 => Some(FolderType::Outbox),
52 => Some(FolderType::BasicRoot),
53 => Some(FolderType::MarketplaceListings),
54 => Some(FolderType::MarketplaceStock),
55 => Some(FolderType::MarketplaceVersion),
56 => Some(FolderType::Settings),
57 => Some(FolderType::Material),
100 => Some(FolderType::Suitcase),
_ => None,
}
}
fn sale_type_from_u8(value: u8) -> SaleType {
match value {
1 => SaleType::Original,
2 => SaleType::Copy,
3 => SaleType::Contents,
_ => SaleType::Not,
}
}
#[derive(Clone)]
struct InventoryWireData {
asset_id: UUID,
base_mask: u32,
creation_date: i32,
creator_id: UUID,
description: Vec<u8>,
everyone_mask: u32,
flags: u32,
folder_id: UUID,
group_id: UUID,
group_mask: u32,
group_owned: bool,
inv_type: i8,
item_id: UUID,
name: Vec<u8>,
next_owner_mask: u32,
owner_id: UUID,
owner_mask: u32,
sale_price: i32,
sale_type: u8,
type_: i8,
}
fn inventory_item_from_wire(data: InventoryWireData) -> Result<InventoryItem, Error> {
let mut inventory_type = InventoryType(data.inv_type);
let asset_type = asset_type_from_i8(data.type_);
if inventory_type == InventoryType::TEXTURE
&& matches!(asset_type, AssetType::Object | AssetType::Mesh)
{
inventory_type = InventoryType::ATTACHMENT;
}
let mut item = InventoryItem::new_with_inventory_type_uuid(inventory_type, data.item_id)?;
item.set_asset_uuid(data.asset_id);
item.set_asset_type(asset_type);
item.set_creation_date(system_time_from_unix_i32(data.creation_date));
item.set_creator_id(data.creator_id);
item.set_description(wire_string(data.description));
item.set_flags(data.flags);
item.set_group_id(data.group_id);
item.set_group_owned(data.group_owned);
item.set_inventory_type(inventory_type);
item.base.set_name(wire_string(data.name));
item.base.set_owner_id(data.owner_id);
item.base.set_parent_uuid(data.folder_id);
item.set_permissions(Permissions::new(
data.base_mask,
data.everyone_mask,
data.group_mask,
data.next_owner_mask,
data.owner_mask,
)?);
item.set_sale_price(data.sale_price);
item.set_sale_type(sale_type_from_u8(data.sale_type));
Ok(item)
}
fn typed_inventory_item(item: InventoryItem) -> Box<dyn InventoryObjectClass> {
macro_rules! boxed {
($kind:ty) => {{
let mut value = <$kind>::new(item.base.uuid()).expect("known inventory constructor");
value.base = item;
Box::new(value) as Box<dyn InventoryObjectClass>
}};
}
match item.inventory_type() {
InventoryType::TEXTURE => boxed!(InventoryTexture),
InventoryType::SOUND => boxed!(InventorySound),
InventoryType::CALLING_CARD => boxed!(InventoryCallingCard),
InventoryType::LANDMARK => boxed!(InventoryLandmark),
InventoryType::OBJECT => boxed!(InventoryObject),
InventoryType::NOTECARD => boxed!(InventoryNotecard),
InventoryType::CATEGORY => boxed!(InventoryCategory),
InventoryType::LSL => boxed!(InventoryLSL),
InventoryType::SNAPSHOT => boxed!(InventorySnapshot),
InventoryType::ATTACHMENT => boxed!(InventoryAttachment),
InventoryType::WEARABLE => boxed!(InventoryWearable),
InventoryType::ANIMATION => boxed!(InventoryAnimation),
InventoryType::GESTURE => boxed!(InventoryGesture),
InventoryType::SETTINGS => boxed!(InventorySettings),
InventoryType::MATERIAL => boxed!(InventoryMaterial),
_ => Box::new(item),
}
}
/// C# `FindObjectByPathReplyEventArgs`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FindObjectByPathReplyEventArgs {
path: String,
inventory_object_id: UUID,
}
impl FindObjectByPathReplyEventArgs {
pub fn new(path: String, inventory_object_id: UUID) -> Result<Self, Error> {
Ok(Self {
path,
inventory_object_id,
})
}
pub fn inventory_object_id(&self) -> UUID {
self.inventory_object_id
}
pub fn path(&self) -> String {
self.path.clone()
}
}
/// C# `FolderUpdatedEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FolderUpdatedEventArgs {
folder_id: UUID,
success: bool,
}
impl FolderUpdatedEventArgs {
pub fn new(folder_id: UUID, success: bool) -> Result<Self, Error> {
Ok(Self { folder_id, success })
}
pub fn folder_id(&self) -> UUID {
self.folder_id
}
pub fn success(&self) -> bool {
self.success
}
}
/// C# `ItemReceivedEventArgs`.
#[derive(Clone, Debug)]
pub struct ItemReceivedEventArgs {
item: InventoryItem,
}
impl ItemReceivedEventArgs {
pub fn new(item: InventoryItem) -> Result<Self, Error> {
Ok(Self { item })
}
pub fn item(&self) -> InventoryItem {
self.item.clone()
}
}
/// C# `SaveAssetToInventoryEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SaveAssetToInventoryEventArgs {
item_id: UUID,
new_asset_id: UUID,
}
impl SaveAssetToInventoryEventArgs {
pub fn new(item_id: UUID, new_asset_id: UUID) -> Result<Self, Error> {
Ok(Self {
item_id,
new_asset_id,
})
}
pub fn item_id(&self) -> UUID {
self.item_id
}
pub fn new_asset_id(&self) -> UUID {
self.new_asset_id
}
}
/// C# `ScriptRunningReplyEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScriptRunningReplyEventArgs {
object_id: UUID,
script_id: UUID,
is_mono: bool,
is_running: bool,
}
impl ScriptRunningReplyEventArgs {
pub fn new(
object_id: UUID,
sctript_id: UUID,
is_mono: bool,
is_running: bool,
) -> Result<Self, Error> {
Ok(Self {
object_id,
script_id: sctript_id,
is_mono,
is_running,
})
}
pub fn is_mono(&self) -> bool {
self.is_mono
}
pub fn is_running(&self) -> bool {
self.is_running
}
pub fn object_id(&self) -> UUID {
self.object_id
}
pub fn script_id(&self) -> UUID {
self.script_id
}
}
/// C# `TaskInventoryReplyEventArgs`.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TaskInventoryReplyEventArgs {
item_id: UUID,
serial: i16,
asset_filename: String,
}
impl TaskInventoryReplyEventArgs {
pub fn new(item_id: UUID, serial: i16, asset_filename: String) -> Result<Self, Error> {
Ok(Self {
item_id,
serial,
asset_filename,
})
}
pub fn asset_filename(&self) -> String {
self.asset_filename.clone()
}
pub fn item_id(&self) -> UUID {
self.item_id
}
pub fn serial(&self) -> i16 {
self.serial
}
}
/// C# `TaskItemReceivedEventArgs`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TaskItemReceivedEventArgs {
item_id: UUID,
folder_id: UUID,
creator_id: UUID,
asset_id: UUID,
type_: InventoryType,
}
impl TaskItemReceivedEventArgs {
pub fn new(
item_id: UUID,
folder_id: UUID,
creator_id: UUID,
asset_id: UUID,
type_: InventoryType,
) -> Result<Self, Error> {
Ok(Self {
item_id,
folder_id,
creator_id,
asset_id,
type_,
})
}
pub fn asset_id(&self) -> UUID {
self.asset_id
}
pub fn creator_id(&self) -> UUID {
self.creator_id
}
pub fn folder_id(&self) -> UUID {
self.folder_id
}
pub fn item_id(&self) -> UUID {
self.item_id
}
pub fn type_(&self) -> InventoryType {
self.type_
}
}
/// C# `InventoryObjectOfferedEventArgs`.
#[derive(Clone, Debug)]
pub struct InventoryObjectOfferedEventArgs {
offer: InstantMessage,
asset_type: AssetType,
object_id: UUID,
from_task: bool,
decision: Arc<RwLock<InventoryOfferDecision>>,
}
#[derive(Clone, Copy, Debug)]
struct InventoryOfferDecision {
folder_id: UUID,
accept: bool,
}
impl InventoryObjectOfferedEventArgs {
pub fn new(
offer_details: InstantMessage,
type_: AssetType,
object_id: UUID,
from_task: bool,
folder_id: UUID,
) -> Result<Self, Error> {
Ok(Self {
offer: offer_details,
asset_type: type_,
object_id,
from_task,
decision: Arc::new(RwLock::new(InventoryOfferDecision {
folder_id,
accept: false,
})),
})
}
pub fn accept(&self) -> bool {
read(&self.decision).accept
}
pub fn set_accept(&mut self, value: bool) {
write(&self.decision).accept = value;
}
pub fn asset_type(&self) -> AssetType {
self.asset_type
}
pub fn folder_id(&self) -> UUID {
read(&self.decision).folder_id
}
pub fn set_folder_id(&mut self, value: UUID) {
write(&self.decision).folder_id = value;
}
pub fn from_task(&self) -> bool {
self.from_task
}
pub fn object_id(&self) -> UUID {
self.object_id
}
pub fn offer(&self) -> InstantMessage {
self.offer.clone()
}
}
/// Result returned by the C#-compatible multi-item copy APIs.
#[derive(Clone, Debug, Default)]
pub struct InventoryManagerCopyItemsResult {
copied_items: Option<Vec<InventoryBase>>,
error: Option<ExternalError>,
success: bool,
}
impl InventoryManagerCopyItemsResult {
pub fn new() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn copied_items(&self) -> Option<Vec<InventoryBase>> {
self.copied_items.clone()
}
pub fn set_copied_items(&mut self, value: Option<Vec<InventoryBase>>) {
self.copied_items = value;
}
pub fn error(&self) -> Option<ExternalError> {
self.error.clone()
}
pub fn set_error(&mut self, value: Option<ExternalError>) {
self.error = value;
}
pub fn success(&self) -> bool {
self.success
}
pub fn set_success(&mut self, value: bool) {
self.success = value;
}
}
/// Result returned by the C#-compatible folder refresh API.
#[derive(Clone, Debug, Default)]
pub struct InventoryManagerFolderUpdateResult {
contents: Option<Vec<InventoryBase>>,
folder_id: UUID,
success: bool,
}
impl InventoryManagerFolderUpdateResult {
pub fn new() -> Result<Self, Error> {
Ok(Self::default())
}
pub fn contents(&self) -> Option<Vec<InventoryBase>> {
self.contents.clone()
}
pub fn set_contents(&mut self, value: Option<Vec<InventoryBase>>) {
self.contents = value;
}
pub fn folder_id(&self) -> UUID {
self.folder_id
}
pub fn set_folder_id(&mut self, value: UUID) {
self.folder_id = value;
}
pub fn success(&self) -> bool {
self.success
}
pub fn set_success(&mut self, value: bool) {
self.success = value;
}
}
struct PendingCreated {
deadline: Instant,
sender: oneshot::Sender<Option<InventoryItem>>,
}
struct PendingCopy {
deadline: Instant,
expected: usize,
copied: Vec<InventoryBase>,
sender: oneshot::Sender<InventoryManagerCopyItemsResult>,
}
struct PendingFetch {
request_id: u64,
deadline: Instant,
sender: oneshot::Sender<Option<InventoryItem>>,
}
struct PendingTaskReply {
request_id: u64,
deadline: Instant,
sender: oneshot::Sender<String>,
}
struct PendingOffer {
request_id: u64,
deadline: Instant,
sender: oneshot::Sender<InventoryObjectOfferedEventArgs>,
}
struct TaskXfer {
deadline: Instant,
next_packet: u32,
announced_size: Option<usize>,
bytes: Vec<u8>,
sender: oneshot::Sender<Option<Vec<u8>>>,
}
struct CleanupWorker {
stop: Arc<(Mutex<bool>, Condvar)>,
handle: JoinHandle<()>,
}
#[derive(Default)]
struct InventoryManagerEvents {
find_object_by_path_reply: EventRegistry<FindObjectByPathReplyEventArgs>,
folder_updated: EventRegistry<FolderUpdatedEventArgs>,
inventory_object_offered: EventRegistry<InventoryObjectOfferedEventArgs>,
item_received: EventRegistry<ItemReceivedEventArgs>,
save_asset_to_inventory: EventRegistry<SaveAssetToInventoryEventArgs>,
script_running_reply: EventRegistry<ScriptRunningReplyEventArgs>,
task_inventory_reply: EventRegistry<TaskInventoryReplyEventArgs>,
task_item_received: EventRegistry<TaskItemReceivedEventArgs>,
}
/// Shared state for a client-owned inventory manager.
pub(crate) struct InventoryManagerInner {
client: ClientWeakHandle,
store: RwLock<Option<Inventory>>,
disposed: AtomicBool,
callback_position: AtomicU32,
request_position: AtomicU64,
pending_created: Mutex<HashMap<u32, PendingCreated>>,
pending_copied: Mutex<HashMap<u32, PendingCopy>>,
pending_fetches: Mutex<HashMap<UUID, Vec<PendingFetch>>>,
pending_task_replies: Mutex<HashMap<UUID, Vec<PendingTaskReply>>>,
pending_offers: Mutex<Vec<PendingOffer>>,
task_xfers: Mutex<HashMap<u64, TaskXfer>>,
cleanup_worker: Mutex<Option<CleanupWorker>>,
raw_packet_subscription: Mutex<Option<Subscription>>,
caps_subscription: Mutex<Option<Subscription>>,
im_subscription: Mutex<Option<Subscription>>,
login_callback: Mutex<Option<NetworkManagerLoginResponseCallback>>,
events: InventoryManagerEvents,
}
/// Stateful native replacement for the generated `InventoryManager` shell.
#[derive(Clone)]
pub struct InventoryManager {
inner: Arc<InventoryManagerInner>,
}
impl fmt::Debug for InventoryManager {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("InventoryManager")
.field("has_store", &read(&self.inner.store).is_some())
.field("disposed", &self.inner.disposed.load(Ordering::Acquire))
.finish()
}
}
impl InventoryManager {
pub(crate) fn native_from_inner(inner: Arc<InventoryManagerInner>) -> Self {
Self { inner }
}
pub(crate) fn native_inner(&self) -> Arc<InventoryManagerInner> {
Arc::clone(&self.inner)
}
pub(crate) fn native_new(client: Option<Arc<GridClient>>) -> Result<Self, Error> {
let client = client.ok_or(Error::ArgumentNull)?;
let inner = Arc::new(InventoryManagerInner {
client: client.native_weak_handle(),
store: RwLock::new(None),
disposed: AtomicBool::new(false),
callback_position: AtomicU32::new(0),
request_position: AtomicU64::new(0),
pending_created: Mutex::new(HashMap::new()),
pending_copied: Mutex::new(HashMap::new()),
pending_fetches: Mutex::new(HashMap::new()),
pending_task_replies: Mutex::new(HashMap::new()),
pending_offers: Mutex::new(Vec::new()),
task_xfers: Mutex::new(HashMap::new()),
cleanup_worker: Mutex::new(None),
raw_packet_subscription: Mutex::new(None),
caps_subscription: Mutex::new(None),
im_subscription: Mutex::new(None),
login_callback: Mutex::new(None),
events: InventoryManagerEvents::default(),
});
let manager = Self { inner };
manager.install_packet_handler()?;
manager.install_message_handlers()?;
manager.install_login_handler()?;
manager.install_cleanup_worker()?;
Ok(manager)
}
fn install_cleanup_worker(&self) -> Result<(), Error> {
let stop = Arc::new((Mutex::new(false), Condvar::new()));
let worker_stop = Arc::clone(&stop);
let weak = Arc::downgrade(&self.inner);
let handle = std::thread::Builder::new()
.name("inventory-callback-cleanup".to_owned())
.spawn(move || {
loop {
let (lock, wake) = &*worker_stop;
let stopped = mutex(lock);
let (stopped, _) = wake
.wait_timeout_while(stopped, CALLBACK_SWEEP_INTERVAL, |value| !*value)
.unwrap_or_else(std::sync::PoisonError::into_inner);
if *stopped {
break;
}
drop(stopped);
let Some(inner) = weak.upgrade() else {
break;
};
InventoryManager::native_from_inner(inner).expire_pending(Instant::now());
}
})
.map_err(|_| Error::InvalidOperation)?;
*mutex(&self.inner.cleanup_worker) = Some(CleanupWorker { stop, handle });
Ok(())
}
fn expire_pending(&self, now: Instant) {
let created = {
let mut pending = mutex(&self.inner.pending_created);
let expired: Vec<_> = pending
.iter()
.filter_map(|(id, request)| (request.deadline <= now).then_some(*id))
.collect();
expired
.into_iter()
.filter_map(|id| pending.remove(&id))
.collect::<Vec<_>>()
};
for request in created {
let _ = request.sender.send(None);
}
let copied = {
let mut pending = mutex(&self.inner.pending_copied);
let expired: Vec<_> = pending
.iter()
.filter_map(|(id, request)| (request.deadline <= now).then_some(*id))
.collect();
expired
.into_iter()
.filter_map(|id| pending.remove(&id))
.collect::<Vec<_>>()
};
for request in copied {
let success = !request.copied.is_empty();
let _ = request.sender.send(InventoryManagerCopyItemsResult {
copied_items: success.then_some(request.copied),
error: None,
success,
});
}
let fetches = expire_waiters(&self.inner.pending_fetches, now, |request| request.deadline);
for request in fetches {
let _ = request.sender.send(None);
}
let task_replies = expire_waiters(&self.inner.pending_task_replies, now, |request| {
request.deadline
});
for request in task_replies {
let _ = request.sender.send(String::new());
}
mutex(&self.inner.pending_offers).retain(|request| request.deadline > now);
let xfers = {
let mut pending = mutex(&self.inner.task_xfers);
let expired: Vec<_> = pending
.iter()
.filter_map(|(id, request)| (request.deadline <= now).then_some(*id))
.collect();
expired
.into_iter()
.filter_map(|id| pending.remove(&id))
.collect::<Vec<_>>()
};
for request in xfers {
let _ = request.sender.send(None);
}
}
fn client(&self) -> Result<GridClient, Error> {
if self.inner.disposed.load(Ordering::Acquire) {
return Err(Error::InvalidOperation);
}
self.inner.client.upgrade().ok_or(Error::InvalidOperation)
}
fn install_packet_handler(&self) -> Result<(), Error> {
let network = self.client()?.native_network()?;
let weak = Arc::downgrade(&self.inner);
let subscription = network.subscribe_raw_packet(Arc::new(move |event| {
if let Some(inner) = weak.upgrade() {
let _ = InventoryManager::native_from_inner(inner).handle_raw_packet(event);
}
}));
*mutex(&self.inner.raw_packet_subscription) = Some(subscription);
Ok(())
}
fn install_message_handlers(&self) -> Result<(), Error> {
let mut client = self.client()?;
let weak = Arc::downgrade(&self.inner);
let im_subscription = client.native_self().native_subscribe_im(Arc::new(
move |event: InstantMessageEventArgs| {
if let Some(inner) = weak.upgrade() {
let _ =
InventoryManager::native_from_inner(inner).handle_inventory_offer(event);
}
},
));
*mutex(&self.inner.im_subscription) = Some(im_subscription);
let weak = Arc::downgrade(&self.inner);
let caps_subscription = client.native_network()?.subscribe_caps(
"ScriptRunningReply".to_owned(),
CapsEventQueueCallback::from_handler(move |_name, message, _simulator| {
let Some(inner) = weak.upgrade() else {
return;
};
let Ok(mut reply) = ScriptRunningReplyMessage::new() else {
return;
};
if message
.serialize()
.and_then(|map| reply.deserialize(map))
.is_ok()
{
inner
.events
.script_running_reply
.emit(ScriptRunningReplyEventArgs {
object_id: reply.object_id,
script_id: reply.item_id,
is_mono: reply.mono,
is_running: reply.running,
});
}
}),
);
*mutex(&self.inner.caps_subscription) = Some(caps_subscription);
Ok(())
}
fn install_login_handler(&self) -> Result<(), Error> {
let weak = Arc::downgrade(&self.inner);
let callback = NetworkManagerLoginResponseCallback::from_handler(
move |success, redirect, _message, _reason, response| {
if success
&& !redirect
&& let (Some(inner), Some(response)) = (weak.upgrade(), response)
{
let _ = InventoryManager::native_from_inner(inner)
.install_login_inventory(response);
}
},
);
self.client()?
.native_network()?
.native_register_login_response_callback(
callback.clone(),
Some(vec![
"inventory-root".to_owned(),
"inventory-skeleton".to_owned(),
"inventory-lib-root".to_owned(),
"inventory-lib-owner".to_owned(),
"inventory-skel-lib".to_owned(),
]),
)?;
*mutex(&self.inner.login_callback) = Some(callback);
Ok(())
}
fn install_login_inventory(&self, response: LoginResponseData) -> Result<(), Error> {
let client = Arc::new(self.client()?);
let mut store = Inventory::new_with_grid_client_uuid(client, response.agent_id())?;
let mut root = InventoryFolder::new(response.inventory_root())?;
root.base.set_parent_uuid(UUID::zero());
store.set_root_folder(Some(root));
for folder in response.inventory_skeleton() {
store.update_node_for(&folder)?;
}
let mut library = InventoryFolder::new(response.library_root())?;
library.base.set_parent_uuid(UUID::zero());
store.set_library_folder(Some(library));
for folder in response.library_skeleton() {
store.update_node_for(&folder)?;
}
*write(&self.inner.store) = Some(store);
Ok(())
}
fn handle_raw_packet(&self, event: RawPacketReceivedEventArgs) -> Result<(), Error> {
match event.packet_type {
PacketType::UpdateCreateInventoryItem => {
self.handle_update_create(decode_packet::<UpdateCreateInventoryItemPacket>(
&event.data,
)?)?;
}
PacketType::BulkUpdateInventory => {
self.handle_bulk_update(decode_packet::<BulkUpdateInventoryPacket>(&event.data)?)?;
}
PacketType::FetchInventoryReply => {
self.handle_fetch_reply(decode_packet::<FetchInventoryReplyPacket>(&event.data)?)?;
}
PacketType::InventoryDescendents => {
self.handle_descendents(decode_packet::<InventoryDescendentsPacket>(&event.data)?)?;
}
PacketType::MoveInventoryItem => {
self.handle_move_item(decode_packet::<MoveInventoryItemPacket>(&event.data)?)?;
}
PacketType::SaveAssetIntoInventory => {
let packet = decode_packet::<SaveAssetIntoInventoryPacket>(&event.data)?;
self.inner
.events
.save_asset_to_inventory
.emit(SaveAssetToInventoryEventArgs {
item_id: packet.inventory_data.item_id,
new_asset_id: packet.inventory_data.new_asset_id,
});
}
PacketType::ReplyTaskInventory => {
let packet = decode_packet::<ReplyTaskInventoryPacket>(&event.data)?;
let filename = wire_string(packet.inventory_data.filename);
let waiters = mutex(&self.inner.pending_task_replies)
.remove(&packet.inventory_data.task_id)
.unwrap_or_default();
for waiter in waiters {
let _ = waiter.sender.send(filename.clone());
}
self.inner
.events
.task_inventory_reply
.emit(TaskInventoryReplyEventArgs {
item_id: packet.inventory_data.task_id,
serial: packet.inventory_data.serial,
asset_filename: filename,
});
}
PacketType::SendXferPacket => self.handle_task_xfer_packet(
decode_packet::<SendXferPacketPacket>(&event.data)?,
event.simulator,
)?,
_ => {}
}
Ok(())
}
fn handle_inventory_offer(&self, event: InstantMessageEventArgs) -> Result<(), Error> {
let offer = event.im();
let (asset_type, object_id, from_task) = match offer.dialog {
InstantMessageDialog::InventoryOffered if offer.binary_bucket.len() == 17 => (
asset_type_from_i8(offer.binary_bucket[0].cast_signed()),
UUID::from_bytes(offer.binary_bucket.clone(), 1)?,
false,
),
InstantMessageDialog::TaskInventoryOffered if offer.binary_bucket.len() == 1 => (
asset_type_from_i8(offer.binary_bucket[0].cast_signed()),
UUID::zero(),
true,
),
InstantMessageDialog::InventoryOffered | InstantMessageDialog::TaskInventoryOffered => {
return Ok(());
}
_ => return Ok(()),
};
let args = InventoryObjectOfferedEventArgs::new(
offer.clone(),
asset_type,
object_id,
from_task,
self.native_find_folder_for_asset_type(asset_type),
)?;
self.inner
.events
.inventory_object_offered
.emit(args.clone());
let waiters = std::mem::take(&mut *mutex(&self.inner.pending_offers));
for waiter in waiters {
let _ = waiter.sender.send(args.clone());
}
let response_dialog = match (offer.dialog, args.accept()) {
(InstantMessageDialog::InventoryOffered, true) => {
InstantMessageDialog::InventoryAccepted
}
(InstantMessageDialog::TaskInventoryOffered, true) => {
InstantMessageDialog::TaskInventoryAccepted
}
(InstantMessageDialog::InventoryOffered, false) => {
InstantMessageDialog::InventoryDeclined
}
(InstantMessageDialog::TaskInventoryOffered, false) => {
InstantMessageDialog::TaskInventoryDeclined
}
_ => return Ok(()),
};
let bucket = if args.accept() {
args.folder_id().get_bytes()?
} else {
Vec::new()
};
let mut client = self.client()?;
let agent = client.native_self();
let _ = agent.native_instant_message_full(
agent.native_name(),
offer.from_agent_id,
String::new(),
offer.im_session_id,
response_dialog,
InstantMessageOnline::Online,
Vector3::zero(),
UUID::zero(),
bucket,
);
if args.accept() && object_id != UUID::zero() {
let _ = self.native_request_fetch_inventory(
HashMap::from([(object_id, offer.to_agent_id)]),
None,
);
}
Ok(())
}
fn handle_task_xfer_packet(
&self,
packet: SendXferPacketPacket,
simulator: Simulator,
) -> Result<(), Error> {
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 complete = None;
let confirmation;
{
let mut transfers = mutex(&self.inner.task_xfers);
let Some(transfer) = transfers.get_mut(&xfer_id) else {
return Ok(());
};
if packet_number == transfer.next_packet {
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_TASK_INVENTORY_BYTES {
let transfer = transfers.remove(&xfer_id).expect("known xfer");
drop(transfers);
let _ = transfer.sender.send(None);
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 {
let transfer = transfers.remove(&xfer_id).expect("known xfer");
drop(transfers);
let _ = transfer.sender.send(None);
return Err(Error::InvalidOperation);
}
transfer.bytes.extend_from_slice(&incoming);
transfer.next_packet = transfer.next_packet.saturating_add(1);
if is_last {
let transfer = transfers.remove(&xfer_id).expect("known xfer");
if transfer.bytes.len() == size {
complete = Some((transfer.sender, Some(transfer.bytes)));
} else {
complete = Some((transfer.sender, None));
}
}
} else {
confirmation = transfer.next_packet.saturating_sub(1);
}
}
let mut confirm = ConfirmXferPacketPacket::new_with_constructor()?;
confirm.xfer_id.id = xfer_id;
confirm.xfer_id.packet = confirmation;
let confirmation_result =
self.send_generated(&simulator, PacketType::ConfirmXferPacket, &confirm);
if let Some((sender, data)) = complete {
let _ = sender.send(data);
}
confirmation_result
}
fn store_clone(&self) -> Option<Inventory> {
read(&self.inner.store).clone()
}
pub(crate) fn native_reconcile_ais(
&self,
folders: Vec<InventoryFolder>,
items: Vec<InventoryItem>,
meta: &crate::inventory_ais::AISResponseMeta,
) -> Result<(), Error> {
let Some(store) = self.store_clone() else {
return Ok(());
};
store.native_apply_ais_batch(
folders,
items,
&meta.items_removed(),
&meta.categories_removed(),
&meta.broken_links_removed(),
&meta.category_version_updates(),
)
}
#[cfg(test)]
pub(crate) fn native_set_store_for_tests(&self, store: Inventory) {
*write(&self.inner.store) = Some(store);
}
fn cache_server_item(&self, item: &InventoryItem) -> Result<(), Error> {
if item.base.uuid() == UUID::zero() || item.inventory_type() == InventoryType::FOLDER {
return Err(Error::Argument);
}
if let Some(store) = self.store_clone() {
let typed = typed_inventory_item(item.clone());
store.update_node_for(typed.as_ref())?;
}
Ok(())
}
fn complete_created_item(&self, callback_id: u32, item: InventoryItem) {
let created = mutex(&self.inner.pending_created).remove(&callback_id);
if let Some(created) = created {
let _ = created.sender.send(Some(item.clone()));
}
let mut copied = mutex(&self.inner.pending_copied);
let Some(copy) = copied.get_mut(&callback_id) else {
return;
};
copy.copied.push(item.base.clone());
if copy.copied.len() < copy.expected {
return;
}
let copy = copied
.remove(&callback_id)
.expect("copy request still present");
drop(copied);
let _ = copy.sender.send(InventoryManagerCopyItemsResult {
copied_items: Some(copy.copied),
error: None,
success: true,
});
}
fn handle_update_create(&self, packet: UpdateCreateInventoryItemPacket) -> Result<(), Error> {
for block in packet.inventory_data {
if block.item_id == UUID::zero() || block.inv_type == InventoryType::FOLDER.0 {
continue;
}
let mut item = inventory_item_from_wire(InventoryWireData {
asset_id: block.asset_id,
base_mask: block.base_mask,
creation_date: block.creation_date,
creator_id: block.creator_id,
description: block.description,
everyone_mask: block.everyone_mask,
flags: block.flags,
folder_id: block.folder_id,
group_id: block.group_id,
group_mask: block.group_mask,
group_owned: block.group_owned,
inv_type: block.inv_type,
item_id: block.item_id,
name: block.name,
next_owner_mask: block.next_owner_mask,
owner_id: block.owner_id,
owner_mask: block.owner_mask,
sale_price: block.sale_price,
sale_type: block.sale_type,
type_: block.type_,
})?;
if item.base.parent_uuid() == UUID::zero() {
item.base
.set_parent_uuid(self.native_find_folder_for_asset_type(item.asset_type()));
self.native_request_update_items(vec![item.clone()], UUID::zero())?;
}
self.cache_server_item(&item)?;
self.complete_created_item(block.callback_id, item.clone());
self.inner
.events
.task_item_received
.emit(TaskItemReceivedEventArgs {
item_id: item.base.uuid(),
folder_id: item.base.parent_uuid(),
creator_id: item.creator_id(),
asset_id: item.asset_uuid(),
type_: item.inventory_type(),
});
}
Ok(())
}
fn handle_bulk_update(&self, packet: BulkUpdateInventoryPacket) -> Result<(), Error> {
let store = self.store_clone();
if let Some(store) = &store {
for block in packet.folder_data {
if block.folder_id == UUID::zero() {
continue;
}
let mut folder = store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(block.folder_id)?
.unwrap_or(InventoryFolder::new(block.folder_id)?);
folder.base.set_name(wire_string(block.name));
folder.base.set_owner_id(packet.agent_data.agent_id);
folder.base.set_parent_uuid(block.parent_id);
folder.set_preferred_type(folder_type_from_i8(block.type_));
store.update_node_for(&folder)?;
}
}
for block in packet.item_data {
if block.item_id == UUID::zero() {
continue;
}
let item = inventory_item_from_wire(InventoryWireData {
asset_id: block.asset_id,
base_mask: block.base_mask,
creation_date: block.creation_date,
creator_id: block.creator_id,
description: block.description,
everyone_mask: block.everyone_mask,
flags: block.flags,
folder_id: block.folder_id,
group_id: block.group_id,
group_mask: block.group_mask,
group_owned: block.group_owned,
inv_type: block.inv_type,
item_id: block.item_id,
name: block.name,
next_owner_mask: block.next_owner_mask,
owner_id: block.owner_id,
owner_mask: block.owner_mask,
sale_price: block.sale_price,
sale_type: block.sale_type,
type_: block.type_,
})?;
self.cache_server_item(&item)?;
self.complete_created_item(block.callback_id, item);
}
Ok(())
}
fn handle_fetch_reply(&self, packet: FetchInventoryReplyPacket) -> Result<(), Error> {
for block in packet.inventory_data {
if block.item_id == UUID::zero() || block.inv_type == InventoryType::FOLDER.0 {
continue;
}
let item = inventory_item_from_wire(InventoryWireData {
asset_id: block.asset_id,
base_mask: block.base_mask,
creation_date: block.creation_date,
creator_id: block.creator_id,
description: block.description,
everyone_mask: block.everyone_mask,
flags: block.flags,
folder_id: block.folder_id,
group_id: block.group_id,
group_mask: block.group_mask,
group_owned: block.group_owned,
inv_type: block.inv_type,
item_id: block.item_id,
name: block.name,
next_owner_mask: block.next_owner_mask,
owner_id: block.owner_id,
owner_mask: block.owner_mask,
sale_price: block.sale_price,
sale_type: block.sale_type,
type_: block.type_,
})?;
self.cache_server_item(&item)?;
let waiters = mutex(&self.inner.pending_fetches)
.remove(&item.base.uuid())
.unwrap_or_default();
for waiter in waiters {
let _ = waiter.sender.send(Some(item.clone()));
}
self.inner
.events
.item_received
.emit(ItemReceivedEventArgs { item });
}
Ok(())
}
fn handle_descendents(&self, packet: InventoryDescendentsPacket) -> Result<(), Error> {
let folder_id = packet.agent_data.folder_id;
let owner_id = packet.agent_data.owner_id;
let Some(store) = self.store_clone() else {
return Ok(());
};
let mut parent = store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)?
.unwrap_or(InventoryFolder::new(folder_id)?);
if packet.agent_data.version < parent.version() {
return Ok(());
}
for block in packet.folder_data {
if block.folder_id == UUID::zero() {
continue;
}
let mut folder = InventoryFolder::new(block.folder_id)?;
folder.base.set_parent_uuid(block.parent_id);
folder.base.set_owner_id(owner_id);
folder.base.set_name(wire_string(block.name));
folder.set_preferred_type(folder_type_from_i8(block.type_));
store.update_node_for(&folder)?;
}
for block in packet.item_data {
if block.item_id == UUID::zero() {
continue;
}
let item = inventory_item_from_wire(InventoryWireData {
asset_id: block.asset_id,
base_mask: block.base_mask,
creation_date: block.creation_date,
creator_id: block.creator_id,
description: block.description,
everyone_mask: block.everyone_mask,
flags: block.flags,
folder_id: block.folder_id,
group_id: block.group_id,
group_mask: block.group_mask,
group_owned: block.group_owned,
inv_type: block.inv_type,
item_id: block.item_id,
name: block.name,
next_owner_mask: block.next_owner_mask,
owner_id,
owner_mask: block.owner_mask,
sale_price: block.sale_price,
sale_type: block.sale_type,
type_: block.type_,
})?;
self.cache_server_item(&item)?;
}
parent.set_version(packet.agent_data.version);
parent.set_descendent_count(packet.agent_data.descendents);
parent.base.set_owner_id(owner_id);
store.update_node_for(&parent)?;
self.inner
.events
.folder_updated
.emit(FolderUpdatedEventArgs {
folder_id,
success: true,
});
Ok(())
}
fn handle_move_item(&self, packet: MoveInventoryItemPacket) -> Result<(), Error> {
let Some(store) = self.store_clone() else {
return Ok(());
};
for block in packet.inventory_data {
let Some(mut item) = store.native_item_value(block.item_id) else {
continue;
};
item.base.set_parent_uuid(block.folder_id);
let name = wire_string(block.new_name);
if !name.is_empty() {
item.base.set_name(name);
}
store.update_node_for(&item)?;
}
Ok(())
}
pub(crate) fn native_subscribe_find_object_by_path_reply(
&self,
handler: EventHandler<FindObjectByPathReplyEventArgs>,
) -> Subscription {
self.inner
.events
.find_object_by_path_reply
.subscribe(handler)
}
pub(crate) fn native_subscribe_folder_updated(
&self,
handler: EventHandler<FolderUpdatedEventArgs>,
) -> Subscription {
self.inner.events.folder_updated.subscribe(handler)
}
pub(crate) fn native_subscribe_inventory_object_offered(
&self,
handler: EventHandler<InventoryObjectOfferedEventArgs>,
) -> Subscription {
self.inner
.events
.inventory_object_offered
.subscribe(handler)
}
pub(crate) fn native_subscribe_item_received(
&self,
handler: EventHandler<ItemReceivedEventArgs>,
) -> Subscription {
self.inner.events.item_received.subscribe(handler)
}
pub(crate) fn native_subscribe_save_asset_to_inventory(
&self,
handler: EventHandler<SaveAssetToInventoryEventArgs>,
) -> Subscription {
self.inner.events.save_asset_to_inventory.subscribe(handler)
}
pub(crate) fn native_subscribe_script_running_reply(
&self,
handler: EventHandler<ScriptRunningReplyEventArgs>,
) -> Subscription {
self.inner.events.script_running_reply.subscribe(handler)
}
pub(crate) fn native_subscribe_task_inventory_reply(
&self,
handler: EventHandler<TaskInventoryReplyEventArgs>,
) -> Subscription {
self.inner.events.task_inventory_reply.subscribe(handler)
}
pub(crate) fn native_subscribe_task_item_received(
&self,
handler: EventHandler<TaskItemReceivedEventArgs>,
) -> Subscription {
self.inner.events.task_item_received.subscribe(handler)
}
pub(crate) fn native_magic_id() -> UUID {
UUID::new_with_string(MAGIC_ID_TEXT.to_owned()).expect("static inventory magic UUID")
}
pub(crate) fn native_decrypt_shadow_id(shadow_id: UUID) -> Result<UUID, Error> {
Ok(UUID::bitxor(shadow_id, Self::native_magic_id()))
}
pub(crate) fn native_encrypt_asset_id(asset_id: UUID) -> Result<UUID, Error> {
Self::native_decrypt_shadow_id(asset_id)
}
pub(crate) fn native_item_crc(item: &InventoryItem) -> Result<u32, Error> {
let mut crc = 0_u32;
for value in [
item.asset_uuid(),
item.base.parent_uuid(),
item.base.uuid(),
item.creator_id(),
item.base.owner_id(),
item.group_id(),
] {
crc = crc.wrapping_add(uuid_crc(value));
}
let permissions = item.permissions();
for value in [
permissions.owner_mask.0,
permissions.next_owner_mask.0,
permissions.everyone_mask.0,
permissions.group_mask.0,
item.flags(),
i32::from(item.inventory_type().0).cast_unsigned(),
i32::from(item.asset_type() as i8).cast_unsigned(),
unix_seconds_u32_wrapping(item.creation_date()),
item.sale_price().cast_unsigned(),
u32::from(item.sale_type() as u8).wrapping_mul(0x0707_3096),
] {
crc = crc.wrapping_add(value);
}
Ok(crc)
}
pub(crate) fn native_create_inventory_item(
type_: InventoryType,
id: UUID,
) -> Result<Box<dyn InventoryItemClass>, Error> {
macro_rules! boxed {
($kind:ty) => {
Box::new(<$kind>::new(id)?) as Box<dyn InventoryItemClass>
};
}
Ok(match type_ {
InventoryType::TEXTURE => boxed!(InventoryTexture),
InventoryType::SOUND => boxed!(InventorySound),
InventoryType::CALLING_CARD => boxed!(InventoryCallingCard),
InventoryType::LANDMARK => boxed!(InventoryLandmark),
InventoryType::OBJECT => boxed!(InventoryObject),
InventoryType::NOTECARD => boxed!(InventoryNotecard),
InventoryType::CATEGORY => boxed!(InventoryCategory),
InventoryType::LSL => boxed!(InventoryLSL),
InventoryType::SNAPSHOT => boxed!(InventorySnapshot),
InventoryType::ATTACHMENT => boxed!(InventoryAttachment),
InventoryType::WEARABLE => boxed!(InventoryWearable),
InventoryType::ANIMATION => boxed!(InventoryAnimation),
InventoryType::GESTURE => boxed!(InventoryGesture),
InventoryType::SETTINGS => boxed!(InventorySettings),
InventoryType::MATERIAL => boxed!(InventoryMaterial),
_ => Box::new(InventoryItem::new_with_inventory_type_uuid(type_, id)?),
})
}
pub(crate) fn native_store(&self) -> Option<Inventory> {
read(&self.inner.store).clone()
}
pub(crate) fn native_no_results() -> Task<Vec<InventoryBase>> {
Task(std::marker::PhantomData)
}
pub(crate) fn native_create_or_retrieve_inventory_item(
&self,
inventory_type: InventoryType,
item_id: UUID,
) -> Result<InventoryItem, Error> {
if let Some(item) = self
.store_clone()
.and_then(|store| store.native_item_value(item_id))
{
return Ok(item);
}
InventoryItem::new_with_inventory_type_uuid(inventory_type, item_id)
}
pub(crate) fn native_create_folder(
&self,
parent_id: UUID,
mut name: String,
preferred_type: FolderType,
) -> Result<UUID, Error> {
let store = self.store_clone();
if preferred_type != FolderType::None {
let owner_id = self.ids()?.0;
if let Some(existing) = store
.as_ref()
.and_then(|store| store.find_folder_for_type(preferred_type))
.and_then(|folder_id| {
store.as_ref().and_then(|store| {
store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)
.ok()
.flatten()
})
})
.filter(|folder| folder.base.owner_id() == owner_id)
{
return Ok(existing.base.uuid());
}
}
if name.is_empty() {
preferred_type
.enum_text()
.unwrap_or("New Folder")
.clone_into(&mut name);
}
let folder_id = UUID::random()?;
let owner_id = self.ids()?.0;
let mut folder = InventoryFolder::new(folder_id)?;
folder.base.set_parent_uuid(parent_id);
folder.base.set_owner_id(owner_id);
folder.base.set_name(name.clone());
folder.set_version(1);
folder.set_descendent_count(0);
folder.set_preferred_type(preferred_type);
if let Some(store) = store {
store.update_node_for(&folder)?;
}
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = CreateInventoryFolderPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.folder_data.folder_id = folder_id;
packet.folder_data.parent_id = parent_id;
packet.folder_data.type_ = preferred_type as i8;
packet.folder_data.name = Utils::string_to_bytes(name)?;
self.send_generated(&simulator, PacketType::CreateInventoryFolder, &packet)?;
Ok(folder_id)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn native_create_item(
&self,
parent_folder: UUID,
name: String,
description: String,
asset_type: AssetType,
asset_transaction_id: UUID,
inventory_type: InventoryType,
wearable_type: WearableType,
next_owner_mask: PermissionMask,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<InventoryItem>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
self.ensure_pending_capacity()?;
let callback_id = self.next_callback_id();
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.pending_created).insert(
callback_id,
PendingCreated {
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender,
},
);
let send_result = (|| {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = CreateInventoryItemPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_block.callback_id = callback_id;
packet.inventory_block.folder_id = parent_folder;
packet.inventory_block.transaction_id = asset_transaction_id;
packet.inventory_block.next_owner_mask = next_owner_mask.0;
packet.inventory_block.type_ = asset_type as i8;
packet.inventory_block.inv_type = inventory_type.0;
packet.inventory_block.wearable_type = wearable_type as u8;
packet.inventory_block.name = Utils::string_to_bytes(name)?;
packet.inventory_block.description = Utils::string_to_bytes(description)?;
self.send_generated(&simulator, PacketType::CreateInventoryItem, &packet)
})();
if let Err(error) = send_result {
mutex(&self.inner.pending_created).remove(&callback_id);
return Err(error);
}
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(item), _)) => Ok(item),
Either::Left((Err(_), _)) => Ok(None),
Either::Right(_) => {
mutex(&self.inner.pending_created).remove(&callback_id);
Err(Error::Cancelled)
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn native_create_link(
&self,
folder_id: UUID,
item_id: UUID,
name: String,
description: String,
inventory_type: InventoryType,
transaction_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<InventoryItem>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
if let Ok(simulator) = self.current_simulator(None)
&& self
.wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
.await?
.is_some()
{
let mut created = self
.create_links_via_cap(
folder_id,
vec![(item_id, name, description, inventory_type)],
cancellation_token,
)
.await?;
return Ok(created.pop());
}
self.ensure_pending_capacity()?;
let callback_id = self.next_callback_id();
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.pending_created).insert(
callback_id,
PendingCreated {
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender,
},
);
let send_result = (|| {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = LinkInventoryItemPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_block.callback_id = callback_id;
packet.inventory_block.folder_id = folder_id;
packet.inventory_block.transaction_id = transaction_id;
packet.inventory_block.old_item_id = item_id;
packet.inventory_block.type_ = if inventory_type == InventoryType::FOLDER {
AssetType::LinkFolder as i8
} else {
AssetType::Link as i8
};
packet.inventory_block.inv_type = inventory_type.0;
packet.inventory_block.name = Utils::string_to_bytes(name)?;
packet.inventory_block.description = Utils::string_to_bytes(description)?;
self.send_generated(&simulator, PacketType::LinkInventoryItem, &packet)
})();
if let Err(error) = send_result {
mutex(&self.inner.pending_created).remove(&callback_id);
return Err(error);
}
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(item), _)) => Ok(item),
Either::Left((Err(_), _)) => Ok(None),
Either::Right(_) => {
mutex(&self.inner.pending_created).remove(&callback_id);
Err(Error::Cancelled)
}
}
}
pub(crate) async fn native_create_links(
&self,
folder_id: UUID,
links: Box<dyn Iterator<Item = (InventoryItem, String)>>,
callback: Option<Box<dyn Fn(bool) + Send + Sync>>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
let links: Vec<_> = links.collect();
if links.is_empty() {
if let Some(callback) = callback {
callback(true);
}
return Ok(());
}
let cap = if let Ok(simulator) = self.current_simulator(None) {
self.wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
.await?
} else {
None
};
if cap.is_some() {
let values = links
.into_iter()
.map(|(item, description)| {
(
item.base.uuid(),
item.base.name(),
description,
item.inventory_type(),
)
})
.collect();
let success = self
.create_links_via_cap(folder_id, values, cancellation_token)
.await
.is_ok();
if let Some(callback) = callback {
callback(success);
}
return Ok(());
}
let mut success = true;
for (item, description) in links {
cancellation_token.throw_if_cancellation_requested()?;
if self
.native_create_link(
folder_id,
item.base.uuid(),
item.base.name(),
description,
item.inventory_type(),
UUID::random()?,
Some(cancellation_token.clone()),
)
.await?
.is_none()
{
success = false;
}
}
if let Some(callback) = callback {
callback(success);
}
Ok(())
}
async fn create_links_via_cap(
&self,
folder_id: UUID,
links: Vec<(UUID, String, String, InventoryType)>,
cancellation_token: CancellationToken,
) -> Result<Vec<InventoryItem>, Error> {
let simulator = self.current_simulator(None)?;
let cap = self
.wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
.await?
.ok_or(Error::InvalidOperation)?;
let uri = Uri(format!(
"{}/category/{}?tid={}",
cap.0.trim_end_matches('/'),
folder_id,
UUID::random()?
));
let payload = OSD::Map(HashMap::from([(
"links".to_owned(),
OSD::Array(
links
.into_iter()
.map(|(linked_id, name, description, inventory_type)| {
OSD::Map(HashMap::from([
("linked_id".to_owned(), OSD::UUID(linked_id)),
("name".to_owned(), OSD::String(name)),
("desc".to_owned(), OSD::String(description)),
(
"type".to_owned(),
OSD::Integer(if inventory_type == InventoryType::FOLDER {
AssetType::LinkFolder as i32
} else {
AssetType::Link as i32
}),
),
(
"inv_type".to_owned(),
OSD::Integer(i32::from(inventory_type.0)),
),
]))
})
.collect(),
),
)]));
let bytes = OSDParser::serialize_llsd_xml_bytes(payload)?;
let (response, body) = self
.client()?
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".to_owned(),
bytes,
cancellation_token,
None,
)
.await?;
if !response.is_success_status_code() {
return Err(Error::HttpRequest);
}
let mut created = Vec::new();
if let Ok(OSD::Map(root)) = OSDParser::deserialize_with_bytes(body)
&& let Some(OSD::Map(embedded)) = root.get("_embedded")
&& let Some(OSD::Map(values)) = embedded.get("links")
{
for value in values.values() {
if let Ok(item) = InventoryItem::from_osd(value.clone()) {
self.cache_server_item(&item)?;
self.inner
.events
.item_received
.emit(ItemReceivedEventArgs { item: item.clone() });
created.push(item);
}
}
}
Ok(created)
}
pub(crate) fn native_request_fetch_inventory(
&self,
items: HashMap<UUID, UUID>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
cancellation_token
.unwrap_or_default()
.throw_if_cancellation_requested()?;
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = FetchInventoryPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_data = items
.into_iter()
.map(|(item_id, owner_id)| {
let mut block = FetchInventoryPacketInventoryDataBlock::new_with_constructor()?;
block.item_id = item_id;
block.owner_id = owner_id;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::FetchInventory, &packet)
}
pub(crate) async fn native_request_fetch_inventory_async(
&self,
items: HashMap<UUID, UUID>,
cancellation_token: Option<CancellationToken>,
callback: Option<Box<dyn Fn(Vec<InventoryItem>) + Send + Sync>>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
let capability = if let Ok(simulator) = self.current_simulator(None) {
self.wait_for_capability(&simulator, "FetchInventory2", &cancellation_token)
.await?
} else {
None
};
if let Some(capability) = capability {
let fetched = self
.fetch_inventory_via_cap(capability, items, cancellation_token)
.await?;
if let Some(callback) = callback {
callback(fetched);
}
return Ok(());
}
self.native_request_fetch_inventory(items, Some(cancellation_token))
}
async fn fetch_inventory_via_cap(
&self,
capability: Uri,
items: HashMap<UUID, UUID>,
cancellation_token: CancellationToken,
) -> Result<Vec<InventoryItem>, Error> {
let agent_id = self.ids()?.0;
let item_values = items
.into_iter()
.map(|(item_id, owner_id)| {
OSD::Map(HashMap::from([
("item_id".to_owned(), OSD::UUID(item_id)),
("owner_id".to_owned(), OSD::UUID(owner_id)),
]))
})
.collect();
let payload = OSD::Map(HashMap::from([
("agent_id".to_owned(), OSD::UUID(agent_id)),
("items".to_owned(), OSD::Array(item_values)),
]));
let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
let (response, response_body) = self
.client()?
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
capability,
"application/llsd+xml".to_owned(),
body,
cancellation_token,
None,
)
.await?;
if !response.is_success_status_code() {
return Err(Error::HttpRequest);
}
let OSD::Map(map) = OSDParser::deserialize_with_bytes(response_body)? else {
return Err(Error::Argument);
};
let Some(OSD::Array(values)) = map.get("items") else {
return Ok(Vec::new());
};
let mut fetched = Vec::with_capacity(values.len());
for value in values {
let item = InventoryItem::from_osd(value.clone())?;
if item.base.uuid() == UUID::zero() {
continue;
}
self.cache_server_item(&item)?;
let waiters = mutex(&self.inner.pending_fetches)
.remove(&item.base.uuid())
.unwrap_or_default();
for waiter in waiters {
let _ = waiter.sender.send(Some(item.clone()));
}
self.inner
.events
.item_received
.emit(ItemReceivedEventArgs { item: item.clone() });
fetched.push(item);
}
Ok(fetched)
}
pub(crate) async fn native_fetch_item(
&self,
item_id: UUID,
owner_id: UUID,
cancellation_token: Option<CancellationToken>,
force_http: bool,
) -> Result<Option<InventoryItem>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
if cancellation_token.is_cancellation_requested() {
return Ok(None);
}
self.ensure_pending_capacity()?;
let request_id = self.next_request_id();
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.pending_fetches)
.entry(item_id)
.or_default()
.push(PendingFetch {
request_id,
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender,
});
let request = HashMap::from([(item_id, owner_id)]);
let send_result = if force_http {
let capability = if let Ok(simulator) = self.current_simulator(None) {
self.wait_for_capability(&simulator, "FetchInventory2", &cancellation_token)
.await?
} else {
None
};
if let Some(capability) = capability {
self.fetch_inventory_via_cap(capability, request, cancellation_token.clone())
.await
.map(|_| ())
} else {
self.remove_pending_fetch(item_id, request_id);
return Ok(None);
}
} else {
self.native_request_fetch_inventory_async(
request,
Some(cancellation_token.clone()),
None,
)
.await
};
if let Err(error) = send_result {
self.remove_pending_fetch(item_id, request_id);
return Err(error);
}
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(item), _)) => Ok(item),
Either::Left((Err(_), _)) | Either::Right(_) => {
self.remove_pending_fetch(item_id, request_id);
Ok(None)
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn native_request_folder_contents(
&self,
folder_id: UUID,
owner_id: UUID,
fetch_folders: bool,
fetch_items: bool,
order: InventorySortOrder,
cancellation_token: Option<CancellationToken>,
) -> Result<Vec<InventoryBase>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
let cap_name = if owner_id == self.ids()?.0 {
"FetchInventoryDescendents2"
} else {
"FetchLibDescendents2"
};
let capability = if let Ok(simulator) = self.current_simulator(None) {
self.wait_for_capability(&simulator, cap_name, &cancellation_token)
.await?
} else {
None
};
let Some(capability) = capability else {
self.inner
.events
.folder_updated
.emit(FolderUpdatedEventArgs {
folder_id,
success: false,
});
return Ok(Vec::new());
};
let mut folder = InventoryFolder::new(folder_id)?;
folder.base.set_owner_id(owner_id);
self.native_request_folder_contents_batch(
vec![folder],
capability,
fetch_folders,
fetch_items,
order,
Some(cancellation_token),
)
.await
}
pub(crate) async fn native_request_folder_contents_batch(
&self,
batch: Vec<InventoryFolder>,
capability: Uri,
fetch_folders: bool,
fetch_items: bool,
order: InventorySortOrder,
cancellation_token: Option<CancellationToken>,
) -> Result<Vec<InventoryBase>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
let requested = batch
.iter()
.map(|folder| {
OSD::Map(HashMap::from([
("folder_id".to_owned(), OSD::UUID(folder.base.uuid())),
("owner_id".to_owned(), OSD::UUID(folder.base.owner_id())),
("fetch_folders".to_owned(), OSD::Boolean(fetch_folders)),
("fetch_items".to_owned(), OSD::Boolean(fetch_items)),
("sort_order".to_owned(), OSD::Integer(order.0)),
]))
})
.collect();
let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
"folders".to_owned(),
OSD::Array(requested),
)])))?;
let response = self
.client()?
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
capability,
"application/llsd+xml".to_owned(),
body,
cancellation_token,
None,
)
.await;
let (response, response_body) = match response {
Ok(response) if response.0.is_success_status_code() => response,
Ok(_) | Err(_) => {
for folder in batch {
self.inner
.events
.folder_updated
.emit(FolderUpdatedEventArgs {
folder_id: folder.base.uuid(),
success: false,
});
}
return Ok(Vec::new());
}
};
let _ = response;
let OSD::Map(root) = OSDParser::deserialize_with_bytes(response_body)? else {
return Err(Error::Argument);
};
let Some(OSD::Array(folders)) = root.get("folders") else {
return Ok(Vec::new());
};
let store = self.store_clone();
let mut result = Vec::new();
for value in folders {
let OSD::Map(map) = value else {
continue;
};
let folder_id = map.get("folder_id").ok_or(Error::Argument)?.as_uuid()?;
let owner_id = map.get("owner_id").map_or(Ok(UUID::zero()), OSD::as_uuid)?;
let mut parent = store
.as_ref()
.and_then(|store| {
store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)
.ok()
.flatten()
})
.unwrap_or(InventoryFolder::new(folder_id)?);
parent.base.set_owner_id(owner_id);
if let Some(value) = map.get("version") {
parent.set_version(value.as_integer()?);
}
if let Some(value) = map.get("descendents") {
parent.set_descendent_count(value.as_integer()?);
}
if let Some(store) = &store {
store.update_node_for(&parent)?;
}
if let Some(OSD::Array(categories)) = map.get("categories") {
for value in categories {
let folder = InventoryFolder::from_osd(value.clone())?;
if let Some(store) = &store {
store.update_node_for(&folder)?;
}
result.push(folder.base.clone());
}
}
if let Some(OSD::Array(items)) = map.get("items") {
for value in items {
let item = InventoryItem::from_osd(value.clone())?;
self.cache_server_item(&item)?;
result.push(item.base.clone());
}
}
self.inner
.events
.folder_updated
.emit(FolderUpdatedEventArgs {
folder_id,
success: true,
});
}
Ok(result)
}
pub(crate) async fn native_request_folder_update(
&self,
folder_id: UUID,
owner_id: UUID,
fetch_folders: bool,
fetch_items: bool,
order: InventorySortOrder,
cancellation_token: Option<CancellationToken>,
) -> Result<InventoryManagerFolderUpdateResult, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
let cap_name = if owner_id == self.ids()?.0 {
"FetchInventoryDescendents2"
} else {
"FetchLibDescendents2"
};
let has_capability = if let Ok(simulator) = self.current_simulator(None) {
self.wait_for_capability(&simulator, cap_name, &cancellation_token)
.await?
.is_some()
} else {
false
};
let contents = self
.native_request_folder_contents(
folder_id,
owner_id,
fetch_folders,
fetch_items,
order,
Some(cancellation_token),
)
.await?;
Ok(InventoryManagerFolderUpdateResult {
contents: Some(contents),
folder_id,
success: has_capability,
})
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn native_folder_contents(
&self,
folder_id: UUID,
owner_id: UUID,
fetch_folders: bool,
fetch_items: bool,
order: InventorySortOrder,
cancellation_token: Option<CancellationToken>,
follow_links: bool,
) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
let remote = self
.native_request_folder_contents(
folder_id,
owner_id,
fetch_folders,
fetch_items,
order,
Some(cancellation_token.clone()),
)
.await;
if matches!(remote, Err(Error::Cancelled)) {
return remote.map(|_| Vec::new());
}
let store = self.store_clone();
let mut contents = store
.as_ref()
.and_then(|store| store.get_contents_sorted_with_uuid(folder_id, order).ok())
.unwrap_or_default();
if contents.is_empty()
&& let Ok(values) = remote
{
contents = values
.into_iter()
.map(|value| Box::new(value) as Box<dyn InventoryObjectClass>)
.collect();
}
if follow_links {
for entry in &mut contents {
let Some(item) = store
.as_ref()
.and_then(|store| store.native_item_value(entry.inventory_base().uuid()))
else {
continue;
};
if !item.is_link()? {
continue;
}
if let Some(real) = store
.as_ref()
.and_then(|store| store.native_item_value(item.asset_uuid()))
{
*entry = typed_inventory_item(real);
} else if let Some(real) = self
.native_fetch_item(
item.asset_uuid(),
owner_id,
Some(cancellation_token.clone()),
false,
)
.await?
{
*entry = typed_inventory_item(real);
}
}
}
Ok(contents)
}
pub(crate) fn native_find_folder_for_type(&self, folder_type: FolderType) -> UUID {
let Some(store) = self.store_clone() else {
return UUID::zero();
};
store.find_folder_for_type(folder_type).unwrap_or_else(|| {
store
.root_folder()
.map_or_else(UUID::zero, |folder| folder.base.uuid())
})
}
pub(crate) fn native_find_folder_for_asset_type(&self, asset_type: AssetType) -> UUID {
let Some(folder_type) = folder_type_from_i8_checked(asset_type as i8) else {
return self
.store_clone()
.and_then(|store| store.root_folder())
.map_or_else(UUID::zero, |folder| folder.base.uuid());
};
self.native_find_folder_for_type(folder_type)
}
pub(crate) fn native_local_find(
&self,
base_folder: UUID,
path: Vec<String>,
level: i32,
first_only: bool,
) -> Result<Vec<InventoryBase>, Error> {
if level < 0 {
return Ok(Vec::new());
}
let Some(store) = self.store_clone() else {
return Ok(Vec::new());
};
let mut visited = HashSet::new();
local_find_in_store(
&store,
base_folder,
&path,
usize::try_from(level).map_err(|_| Error::Argument)?,
first_only,
&mut visited,
0,
)
}
pub(crate) async fn native_find_object_by_path(
&self,
base_folder: UUID,
owner_id: UUID,
path: String,
cancellation_token: Option<CancellationToken>,
) -> Result<UUID, Error> {
if path.is_empty() {
return Err(Error::Argument);
}
let cancellation_token = cancellation_token.unwrap_or_default();
let segments: Vec<_> = path.split('/').map(ToOwned::to_owned).collect();
let mut folder = base_folder;
for (index, segment) in segments.iter().enumerate() {
cancellation_token.throw_if_cancellation_requested()?;
let contents = self
.native_folder_contents(
folder,
owner_id,
true,
true,
InventorySortOrder::BY_NAME,
Some(cancellation_token.clone()),
false,
)
.await?;
let Some(found) = contents
.into_iter()
.find(|entry| entry.inventory_base().name() == *segment)
else {
self.inner
.events
.find_object_by_path_reply
.emit(FindObjectByPathReplyEventArgs {
path,
inventory_object_id: UUID::zero(),
});
return Ok(UUID::zero());
};
folder = found.inventory_base().uuid();
if index + 1 == segments.len() {
self.inner
.events
.find_object_by_path_reply
.emit(FindObjectByPathReplyEventArgs {
path,
inventory_object_id: folder,
});
return Ok(folder);
}
}
self.inner
.events
.find_object_by_path_reply
.emit(FindObjectByPathReplyEventArgs {
path,
inventory_object_id: UUID::zero(),
});
Ok(UUID::zero())
}
pub(crate) fn native_update_folder_properties(
&self,
folder_id: UUID,
parent_id: UUID,
name: String,
folder_type: FolderType,
) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = UpdateInventoryFolderPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
let mut block = UpdateInventoryFolderPacketFolderDataBlock::new_with_constructor()?;
block.folder_id = folder_id;
block.parent_id = parent_id;
block.name = Utils::string_to_bytes(name.clone())?;
block.type_ = folder_type as i8;
packet.folder_data = vec![block];
self.send_generated(&simulator, PacketType::UpdateInventoryFolder, &packet)?;
if let Some(store) = self.store_clone()
&& let Some(mut folder) =
store.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(folder_id)?
{
folder.base.set_parent_uuid(parent_id);
folder.base.set_name(name);
folder.set_preferred_type(folder_type);
store.update_node_for(&folder)?;
}
Ok(())
}
pub(crate) fn native_move_folders(
&self,
moves: HashMap<UUID, UUID>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
cancellation_token
.unwrap_or_default()
.throw_if_cancellation_requested()?;
self.apply_local_folder_moves(&moves)?;
self.send_move_folders_packet(&moves)
}
fn apply_local_folder_moves(&self, moves: &HashMap<UUID, UUID>) -> Result<(), Error> {
if let Some(store) = self.store_clone() {
for (folder_id, parent_id) in moves {
if let Some(mut folder) =
store.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(*folder_id)?
{
folder.base.set_parent_uuid(*parent_id);
store.update_node_for(&folder)?;
}
}
}
Ok(())
}
fn send_move_folders_packet(&self, moves: &HashMap<UUID, UUID>) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = MoveInventoryFolderPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.agent_data.stamp = false;
packet.inventory_data = moves
.iter()
.map(|(folder_id, parent_id)| {
let mut block =
MoveInventoryFolderPacketInventoryDataBlock::new_with_constructor()?;
block.folder_id = *folder_id;
block.parent_id = *parent_id;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::MoveInventoryFolder, &packet)
}
pub(crate) fn native_move_items(
&self,
moves: HashMap<UUID, (UUID, String)>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
cancellation_token
.unwrap_or_default()
.throw_if_cancellation_requested()?;
self.apply_local_item_moves(&moves)?;
self.send_move_items_packet(&moves)
}
fn apply_local_item_moves(&self, moves: &HashMap<UUID, (UUID, String)>) -> Result<(), Error> {
if let Some(store) = self.store_clone() {
for (item_id, (folder_id, new_name)) in moves {
if let Some(mut item) = store.native_item_value(*item_id) {
item.base.set_parent_uuid(*folder_id);
if !new_name.is_empty() {
item.base.set_name(new_name.clone());
}
store.update_node_for(&item)?;
}
}
}
Ok(())
}
fn send_move_items_packet(&self, moves: &HashMap<UUID, (UUID, String)>) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = MoveInventoryItemPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.agent_data.stamp = false;
packet.inventory_data = moves
.iter()
.map(|(item_id, (folder_id, name))| {
let mut block = MoveInventoryItemPacketInventoryDataBlock::new_with_constructor()?;
block.item_id = *item_id;
block.folder_id = *folder_id;
block.new_name = Utils::string_to_bytes(name.clone())?;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::MoveInventoryItem, &packet)
}
pub(crate) async fn native_move_folders_async(
&self,
moves: HashMap<UUID, UUID>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
self.apply_local_folder_moves(&moves)?;
if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? {
for (folder_id, parent_id) in moves {
cancellation_token.throw_if_cancellation_requested()?;
let _ = self
.ais_patch_parent(
&capability,
"category",
folder_id,
parent_id,
cancellation_token.clone(),
)
.await;
}
return Ok(());
}
self.send_move_folders_packet(&moves)
}
pub(crate) async fn native_move_items_async(
&self,
moves: HashMap<UUID, (UUID, String)>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
self.apply_local_item_moves(&moves)?;
let can_use_ais = moves.values().all(|(_, name)| name.is_empty());
if can_use_ais
&& let Some(capability) = self.inventory_api_capability(&cancellation_token).await?
{
for (item_id, (parent_id, _)) in moves {
cancellation_token.throw_if_cancellation_requested()?;
let _ = self
.ais_patch_parent(
&capability,
"item",
item_id,
parent_id,
cancellation_token.clone(),
)
.await;
}
return Ok(());
}
self.send_move_items_packet(&moves)
}
async fn inventory_api_capability(
&self,
cancellation_token: &CancellationToken,
) -> Result<Option<Uri>, Error> {
let Ok(simulator) = self.current_simulator(None) else {
return Ok(None);
};
self.wait_for_capability(&simulator, "InventoryAPIv3", cancellation_token)
.await
}
async fn ais_patch_parent(
&self,
capability: &Uri,
kind: &str,
object_id: UUID,
parent_id: UUID,
cancellation_token: CancellationToken,
) -> Result<bool, Error> {
let uri = Uri(format!(
"{}/{kind}/{object_id}",
capability.0.trim_end_matches('/')
));
let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([(
"parent_id".to_owned(),
OSD::UUID(parent_id),
)])))?;
let response = self
.client()?
.native_http_caps_client()
.patch_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".to_owned(),
body,
cancellation_token,
None,
)
.await?;
Ok(response.0.is_success_status_code())
}
pub(crate) fn native_move_object(
&self,
item: InventoryBase,
new_parent: InventoryFolder,
) -> Result<(), Error> {
if self
.store_clone()
.and_then(|store| {
store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(item.uuid())
.ok()
.flatten()
})
.is_some()
{
self.native_move_folders(HashMap::from([(item.uuid(), new_parent.base.uuid())]), None)
} else {
self.native_move_items(
HashMap::from([(item.uuid(), (new_parent.base.uuid(), String::new()))]),
None,
)
}
}
pub(crate) fn native_request_update_items(
&self,
items: Vec<InventoryItem>,
transaction_id: UUID,
) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = UpdateInventoryItemPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.agent_data.transaction_id = transaction_id;
packet.inventory_data = items
.iter()
.map(|item| {
let mut block =
UpdateInventoryItemPacketInventoryDataBlock::new_with_constructor()?;
fill_inventory_wire_block!(
block,
item,
item.transaction_id(),
item.asset_type() as i8
);
block.callback_id = 0;
block.crc = Self::native_item_crc(item)?;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::UpdateInventoryItem, &packet)
}
pub(crate) async fn native_remove_descendants(
&self,
folder_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? {
if self
.ais_delete_object(
&capability,
&format!("category/{folder_id}/children"),
cancellation_token,
)
.await
.unwrap_or(false)
{
self.remove_local_descendants(folder_id, true)?;
}
return Ok(());
}
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = PurgeInventoryDescendentsPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_data.folder_id = folder_id;
self.send_generated(&simulator, PacketType::PurgeInventoryDescendents, &packet)?;
self.remove_local_descendants(folder_id, true)
}
pub(crate) async fn native_remove_inventory_objects(
&self,
items: Vec<UUID>,
folders: Vec<UUID>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
if let Some(capability) = self.inventory_api_capability(&cancellation_token).await? {
for item in items {
cancellation_token.throw_if_cancellation_requested()?;
if self
.ais_delete_object(
&capability,
&format!("item/{item}"),
cancellation_token.clone(),
)
.await
.unwrap_or(false)
{
self.remove_local_descendants(item, true)?;
}
}
for folder in folders {
cancellation_token.throw_if_cancellation_requested()?;
if self
.ais_delete_object(
&capability,
&format!("category/{folder}"),
cancellation_token.clone(),
)
.await
.unwrap_or(false)
{
self.remove_local_descendants(folder, true)?;
}
}
return Ok(());
}
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = RemoveInventoryObjectsPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.item_data = items
.iter()
.map(|item_id| {
let mut block = RemoveInventoryObjectsPacketItemDataBlock::new_with_constructor()?;
block.item_id = *item_id;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
packet.folder_data = folders
.iter()
.map(|folder_id| {
let mut block =
RemoveInventoryObjectsPacketFolderDataBlock::new_with_constructor()?;
block.folder_id = *folder_id;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::RemoveInventoryObjects, &packet)?;
for item in items {
self.remove_local_descendants(item, true)?;
}
for folder in folders {
self.remove_local_descendants(folder, true)?;
}
Ok(())
}
async fn ais_delete_object(
&self,
capability: &Uri,
path: &str,
cancellation_token: CancellationToken,
) -> Result<bool, Error> {
let uri = Uri(format!("{}/{path}", capability.0.trim_end_matches('/')));
let response = self
.client()?
.native_http_caps_client()
.delete_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".to_owned(),
Vec::new(),
cancellation_token,
None,
)
.await?;
Ok(response.0.is_success_status_code() || response.0.status_code == 410)
}
fn remove_local_descendants(&self, root: UUID, include_root: bool) -> Result<(), Error> {
let Some(store) = self.store_clone() else {
return Ok(());
};
if include_root {
if let Some(value) = store.get_value_or_default_with_uuid(root)? {
store.remove_node_for(&value)?;
}
return Ok(());
}
let contents = store.get_contents_with_uuid(root).unwrap_or_default();
for value in contents {
store.remove_node_for(value.as_ref())?;
}
Ok(())
}
pub(crate) fn native_empty_folder(&self, folder_id: UUID) -> Result<(), Error> {
let Some(store) = self.store_clone() else {
return Ok(());
};
let contents = store.get_contents_with_uuid(folder_id).unwrap_or_default();
let mut items = Vec::new();
let mut folders = Vec::new();
for value in contents {
if value.as_any().is::<InventoryFolder>() {
folders.push(value.inventory_base().uuid());
} else {
items.push(value.inventory_base().uuid());
}
}
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = RemoveInventoryObjectsPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.item_data = items
.iter()
.map(|id| {
let mut block = RemoveInventoryObjectsPacketItemDataBlock::new_with_constructor()?;
block.item_id = *id;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
packet.folder_data = folders
.iter()
.map(|id| {
let mut block =
RemoveInventoryObjectsPacketFolderDataBlock::new_with_constructor()?;
block.folder_id = *id;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::RemoveInventoryObjects, &packet)
}
pub(crate) async fn native_empty_system_folder(
&self,
folder_type: FolderType,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
let folder_id = self.native_find_folder_for_type(folder_type);
if folder_id != UUID::zero() {
self.native_remove_descendants(folder_id, Some(cancellation_token))
.await?;
}
Ok(())
}
fn copy_from_notecard_packet(
&self,
object_id: UUID,
notecard_id: UUID,
folder_id: UUID,
item_id: UUID,
) -> Result<CopyInventoryFromNotecardPacket, Error> {
let (agent_id, session_id) = self.ids()?;
let mut packet = CopyInventoryFromNotecardPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.notecard_data.object_id = object_id;
packet.notecard_data.notecard_item_id = notecard_id;
let mut block = CopyInventoryFromNotecardPacketInventoryDataBlock::new_with_constructor()?;
block.folder_id = folder_id;
block.item_id = item_id;
packet.inventory_data = vec![block];
Ok(packet)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn native_copy_item_from_notecard(
&self,
object_id: UUID,
notecard_id: UUID,
folder_id: UUID,
item_id: UUID,
cancellation_token: Option<CancellationToken>,
progress: Option<
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
>,
) -> Result<Option<InventoryBase>, Error> {
const NOTECARD_CALLBACK_ID: u32 = 0;
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
self.ensure_pending_capacity()?;
let (sender, receiver) = oneshot::channel();
{
let mut copied = mutex(&self.inner.pending_copied);
// This protocol fixes the callback ID at zero. Reject a concurrent
// request rather than overwriting the only correlatable waiter.
if copied.contains_key(&NOTECARD_CALLBACK_ID) {
return Err(Error::InvalidOperation);
}
copied.insert(
NOTECARD_CALLBACK_ID,
PendingCopy {
deadline: Instant::now() + CALLBACK_TIMEOUT,
expected: 1,
copied: Vec::with_capacity(1),
sender,
},
);
}
let send_result = async {
let simulator = self.current_simulator(None)?;
if let Some(capability) = self
.wait_for_capability(&simulator, "CopyInventoryFromNotecard", &cancellation_token)
.await?
{
let mut message = CopyInventoryFromNotecardMessage::new()?;
message.callback_id = 0;
message.folder_id = folder_id;
message.item_id = item_id;
message.notecard_id = notecard_id;
message.object_id = object_id;
let body =
OSDParser::serialize_llsd_xml_bytes(OSD::Map(message.serialize()?.snapshot()))?;
match self
.client()?
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
capability,
"application/llsd+xml".to_owned(),
body,
cancellation_token.clone(),
progress,
)
.await
{
Ok((response, _)) if response.is_success_status_code() => return Ok(()),
Err(Error::Cancelled) => return Err(Error::Cancelled),
Ok(_) | Err(_) => {}
}
}
cancellation_token.throw_if_cancellation_requested()?;
let packet =
self.copy_from_notecard_packet(object_id, notecard_id, folder_id, item_id)?;
self.send_generated(&simulator, PacketType::CopyInventoryFromNotecard, &packet)
}
.await;
if let Err(error) = send_result {
mutex(&self.inner.pending_copied).remove(&NOTECARD_CALLBACK_ID);
return Err(error);
}
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(result), _)) => Ok(result
.copied_items
.and_then(|mut items| (!items.is_empty()).then(|| items.remove(0)))),
Either::Left((Err(_), _)) => Ok(None),
Either::Right(_) => {
mutex(&self.inner.pending_copied).remove(&NOTECARD_CALLBACK_ID);
Err(Error::Cancelled)
}
}
}
pub(crate) async fn native_copy_items(
&self,
items: Vec<UUID>,
target_folders: Vec<UUID>,
new_names: Vec<String>,
old_owner_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<InventoryManagerCopyItemsResult, Error> {
if items.len() != target_folders.len()
|| (!new_names.is_empty() && items.len() != new_names.len())
{
return Err(Error::Argument);
}
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
if items.is_empty() {
return Ok(InventoryManagerCopyItemsResult {
copied_items: Some(Vec::new()),
error: None,
success: true,
});
}
if let Ok(simulator) = self.current_simulator(None)
&& let Some(capability) = self
.wait_for_capability(&simulator, "InventoryAPIv3", &cancellation_token)
.await?
{
let operations = items
.iter()
.zip(&target_folders)
.enumerate()
.map(|(index, (item_id, folder_id))| {
let mut operation = HashMap::from([
("item_id".to_owned(), OSD::UUID(*item_id)),
("folder_id".to_owned(), OSD::UUID(*folder_id)),
]);
if let Some(name) = new_names.get(index)
&& !name.is_empty()
{
operation.insert("new_name".to_owned(), OSD::String(name.clone()));
}
OSD::Map(operation)
})
.collect();
let payload = OSD::Map(HashMap::from([
("items".to_owned(), OSD::Array(operations)),
("agent_id".to_owned(), OSD::UUID(self.ids()?.0)),
]));
let body = OSDParser::serialize_llsd_xml_bytes(payload)?;
let response = self
.client()?
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
capability,
"application/llsd+xml".to_owned(),
body,
cancellation_token.clone(),
None,
)
.await;
if response.is_ok_and(|response| response.0.is_success_status_code()) {
return Ok(InventoryManagerCopyItemsResult {
copied_items: Some(Vec::new()),
error: None,
success: true,
});
}
}
self.ensure_pending_capacity()?;
let callback_id = self.next_callback_id();
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.pending_copied).insert(
callback_id,
PendingCopy {
deadline: Instant::now() + CALLBACK_TIMEOUT,
expected: items.len(),
copied: Vec::with_capacity(items.len()),
sender,
},
);
let send_result = (|| {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = CopyInventoryItemPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_data = items
.iter()
.zip(&target_folders)
.enumerate()
.map(|(index, (item_id, folder_id))| {
let mut block =
CopyInventoryItemPacketInventoryDataBlock::new_with_constructor()?;
block.callback_id = callback_id;
block.old_item_id = *item_id;
block.old_agent_id = old_owner_id;
block.new_folder_id = *folder_id;
block.new_name =
Utils::string_to_bytes(new_names.get(index).cloned().unwrap_or_default())?;
Ok(block)
})
.collect::<Result<Vec<_>, Error>>()?;
self.send_generated(&simulator, PacketType::CopyInventoryItem, &packet)
})();
if let Err(error) = send_result {
mutex(&self.inner.pending_copied).remove(&callback_id);
return Ok(InventoryManagerCopyItemsResult {
copied_items: None,
error: Some(ExternalError(error.to_string())),
success: false,
});
}
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(result), _)) => Ok(result),
Either::Left((Err(_), _)) => Ok(InventoryManagerCopyItemsResult::default()),
Either::Right(_) => {
mutex(&self.inner.pending_copied).remove(&callback_id);
Err(Error::Cancelled)
}
}
}
pub(crate) async fn native_copy_item(
&self,
item_id: UUID,
new_parent: UUID,
new_name: String,
old_owner_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<InventoryBase>, Error> {
Ok(self
.native_copy_items(
vec![item_id],
vec![new_parent],
vec![new_name],
old_owner_id,
cancellation_token,
)
.await?
.copied_items
.and_then(|mut items| (!items.is_empty()).then(|| items.remove(0))))
}
pub(crate) async fn native_copy_item_default_owner(
&self,
item_id: UUID,
new_parent: UUID,
new_name: String,
cancellation_token: Option<CancellationToken>,
) -> Result<Option<InventoryBase>, Error> {
self.native_copy_item(
item_id,
new_parent,
new_name,
self.ids()?.0,
cancellation_token,
)
.await
}
pub(crate) fn native_dispose(&self) -> Result<(), Error> {
if self.inner.disposed.swap(true, Ordering::AcqRel) {
return Ok(());
}
if let Some(worker) = mutex(&self.inner.cleanup_worker).take() {
let (stopped, wake) = &*worker.stop;
*mutex(stopped) = true;
wake.notify_all();
if worker.handle.thread().id() != std::thread::current().id() {
let _ = worker.handle.join();
}
}
mutex(&self.inner.raw_packet_subscription).take();
mutex(&self.inner.caps_subscription).take();
mutex(&self.inner.im_subscription).take();
if let Some(callback) = mutex(&self.inner.login_callback).take()
&& let Ok(client) = self.inner.client.upgrade().ok_or(Error::InvalidOperation)
&& let Ok(network) = client.native_network()
{
let _ = network.native_unregister_login_response_callback(&callback);
}
let created = mutex(&self.inner.pending_created)
.drain()
.map(|(_, v)| v)
.collect::<Vec<_>>();
let copied = mutex(&self.inner.pending_copied)
.drain()
.map(|(_, v)| v)
.collect::<Vec<_>>();
let fetches = mutex(&self.inner.pending_fetches)
.drain()
.flat_map(|(_, v)| v)
.collect::<Vec<_>>();
let task_replies = mutex(&self.inner.pending_task_replies)
.drain()
.flat_map(|(_, v)| v)
.collect::<Vec<_>>();
let offers = std::mem::take(&mut *mutex(&self.inner.pending_offers));
let xfers = mutex(&self.inner.task_xfers)
.drain()
.map(|(_, v)| v)
.collect::<Vec<_>>();
for request in created {
let _ = request.sender.send(None);
}
for request in copied {
let _ = request
.sender
.send(InventoryManagerCopyItemsResult::default());
}
for request in fetches {
let _ = request.sender.send(None);
}
for request in task_replies {
let _ = request.sender.send(String::new());
}
drop(offers);
for request in xfers {
let _ = request.sender.send(None);
}
write(&self.inner.store).take();
Ok(())
}
fn current_simulator(&self, simulator: Option<Simulator>) -> Result<Simulator, Error> {
simulator
.or_else(|| {
self.client()
.ok()?
.native_network()
.ok()?
.native_current_sim()
})
.ok_or(Error::InvalidOperation)
}
fn ids(&self) -> Result<(UUID, UUID), Error> {
let network = self.client()?.native_network()?;
Ok((network.native_agent_id(), network.native_session_id()))
}
fn send_encoded(
simulator: &Simulator,
packet_type: PacketType,
bytes: Vec<u8>,
) -> Result<(), Error> {
simulator.native_send_packet_data(
bytes.clone(),
i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
packet_type,
false,
)
}
#[allow(clippy::unused_self)] // Kept as a manager method to centralize all outbound packet sends.
fn send_generated<P: GeneratedPacket>(
&self,
simulator: &Simulator,
packet_type: PacketType,
packet: &P,
) -> Result<(), Error> {
for bytes in packet.encode_multiple()? {
Self::send_encoded(simulator, packet_type, bytes)?;
}
Ok(())
}
fn next_callback_id(&self) -> u32 {
loop {
let value = self
.inner
.callback_position
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1);
if value != 0 {
return value;
}
}
}
fn next_request_id(&self) -> u64 {
loop {
let value = self
.inner
.request_position
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1);
if value != 0 {
return value;
}
}
}
fn ensure_pending_capacity(&self) -> Result<(), Error> {
let total = mutex(&self.inner.pending_created).len()
+ mutex(&self.inner.pending_copied).len()
+ mutex(&self.inner.pending_fetches)
.values()
.map(Vec::len)
.sum::<usize>()
+ mutex(&self.inner.pending_task_replies)
.values()
.map(Vec::len)
.sum::<usize>()
+ mutex(&self.inner.pending_offers).len()
+ mutex(&self.inner.task_xfers).len();
if total >= MAX_PENDING_REQUESTS {
Err(Error::InvalidOperation)
} else {
Ok(())
}
}
fn remove_pending_fetch(&self, item_id: UUID, request_id: u64) {
let mut pending = mutex(&self.inner.pending_fetches);
if let Some(waiters) = pending.get_mut(&item_id) {
waiters.retain(|waiter| waiter.request_id != request_id);
if waiters.is_empty() {
pending.remove(&item_id);
}
}
}
fn remove_pending_task_reply(&self, object_id: UUID, request_id: u64) {
let mut pending = mutex(&self.inner.pending_task_replies);
if let Some(waiters) = pending.get_mut(&object_id) {
waiters.retain(|waiter| waiter.request_id != request_id);
if waiters.is_empty() {
pending.remove(&object_id);
}
}
}
pub(crate) fn native_request_task_inventory(
&self,
object_local_id: u32,
simulator: Option<Simulator>,
) -> Result<(), Error> {
let simulator = self.current_simulator(simulator)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = RequestTaskInventoryPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_data.local_id = object_local_id;
Self::send_encoded(
&simulator,
PacketType::RequestTaskInventory,
packet.to_bytes_with_method()?,
)
}
pub(crate) fn native_update_task_inventory(
&self,
object_local_id: u32,
item: InventoryItem,
simulator: Option<Simulator>,
only_modify_metadata: bool,
) -> Result<UUID, Error> {
let simulator = self.current_simulator(simulator)?;
let transaction_id = if only_modify_metadata {
UUID::zero()
} else {
UUID::random()?
};
let (agent_id, session_id) = self.ids()?;
let mut packet = UpdateTaskInventoryPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.update_data.key = 0;
packet.update_data.local_id = object_local_id;
fill_inventory_wire_block!(
packet.inventory_data,
&item,
transaction_id,
item.asset_type() as i8
);
packet.inventory_data.crc = Self::native_item_crc(&item)?;
self.send_generated(&simulator, PacketType::UpdateTaskInventory, &packet)?;
Ok(transaction_id)
}
pub(crate) fn native_move_task_inventory(
&self,
object_local_id: u32,
task_item_id: UUID,
inventory_folder_id: UUID,
simulator: Simulator,
) -> Result<(), Error> {
let (agent_id, session_id) = self.ids()?;
let mut packet = MoveTaskInventoryPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.agent_data.folder_id = inventory_folder_id;
packet.inventory_data.item_id = task_item_id;
packet.inventory_data.local_id = object_local_id;
self.send_generated(&simulator, PacketType::MoveTaskInventory, &packet)
}
pub(crate) fn native_remove_task_inventory(
&self,
object_local_id: u32,
task_item_id: UUID,
simulator: Simulator,
) -> Result<(), Error> {
let (agent_id, session_id) = self.ids()?;
let mut packet = RemoveTaskInventoryPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.inventory_data.item_id = task_item_id;
packet.inventory_data.local_id = object_local_id;
self.send_generated(&simulator, PacketType::RemoveTaskInventory, &packet)
}
pub(crate) fn native_copy_script_to_task(
&self,
object_local_id: u32,
item: InventoryItem,
enable_script: bool,
simulator: Option<Simulator>,
) -> Result<UUID, Error> {
let simulator = self.current_simulator(simulator)?;
let transaction_id = UUID::random()?;
let (agent_id, session_id) = self.ids()?;
let mut packet = RezScriptPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.update_block.object_local_id = object_local_id;
packet.update_block.enabled = enable_script;
fill_inventory_wire_block!(
packet.inventory_block,
&item,
transaction_id,
item.asset_type() as i8
);
packet.inventory_block.crc = Self::native_item_crc(&item)?;
self.send_generated(&simulator, PacketType::RezScript, &packet)?;
Ok(transaction_id)
}
pub(crate) fn native_request_get_script_running(
&self,
object_id: UUID,
script_id: UUID,
) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let mut packet = GetScriptRunningPacket::new_with_constructor()?;
packet.script.object_id = object_id;
packet.script.item_id = script_id;
self.send_generated(&simulator, PacketType::GetScriptRunning, &packet)
}
pub(crate) fn native_request_set_script_running(
&self,
object_id: UUID,
script_id: UUID,
running: bool,
) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = SetScriptRunningPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.script.object_id = object_id;
packet.script.item_id = script_id;
packet.script.running = running;
self.send_generated(&simulator, PacketType::SetScriptRunning, &packet)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn native_rez_from_inventory(
&self,
simulator: Simulator,
task_id: UUID,
_rotation: Quaternion,
position: Vector3,
item: InventoryItem,
group_owner: UUID,
query_id: UUID,
rez_selected: bool,
) -> Result<UUID, Error> {
let (agent_id, session_id) = self.ids()?;
let permissions = item.permissions();
let mut packet = RezObjectPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.agent_data.group_id = group_owner;
packet.rez_data.from_task_id = task_id;
packet.rez_data.bypass_raycast = 1;
packet.rez_data.ray_start = position;
packet.rez_data.ray_end = position;
packet.rez_data.ray_target_id = UUID::zero();
packet.rez_data.ray_end_is_intersection = false;
packet.rez_data.rez_selected = rez_selected;
packet.rez_data.remove_item = false;
packet.rez_data.item_flags = item.flags();
packet.rez_data.group_mask = permissions.group_mask.0;
packet.rez_data.everyone_mask = permissions.everyone_mask.0;
packet.rez_data.next_owner_mask = permissions.next_owner_mask.0;
fill_inventory_wire_block!(
packet.inventory_data,
&item,
query_id,
item.inventory_type().0
);
self.send_generated(&simulator, PacketType::RezObject, &packet)?;
self.remove_if_no_copy(item.base.uuid())?;
Ok(query_id)
}
pub(crate) fn native_rez_from_inventory_default(
&self,
simulator: Simulator,
rotation: Quaternion,
position: Vector3,
item: InventoryItem,
group_owner: Option<UUID>,
) -> Result<UUID, Error> {
let group_owner = if let Some(group_owner) = group_owner {
group_owner
} else {
let mut client = self.client()?;
client.native_self().native_active_group()
};
self.native_rez_from_inventory(
simulator,
UUID::zero(),
rotation,
position,
item,
group_owner,
UUID::random()?,
true,
)
}
pub(crate) fn native_restore_rez_from_inventory(
&self,
simulator: Simulator,
item: InventoryItem,
query_id: UUID,
) -> Result<UUID, Error> {
let (agent_id, session_id) = self.ids()?;
let mut packet = RezRestoreToWorldPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
fill_inventory_wire_block!(
packet.inventory_data,
&item,
query_id,
item.inventory_type().0
);
self.send_generated(&simulator, PacketType::RezRestoreToWorld, &packet)?;
Ok(query_id)
}
pub(crate) fn native_de_rez_to_inventory(
&self,
object_local_id: u32,
destination: DeRezDestination,
destination_id: UUID,
transaction_id: UUID,
) -> Result<(), Error> {
let simulator = self.current_simulator(None)?;
let (agent_id, session_id) = self.ids()?;
let mut packet = DeRezObjectPacket::new_with_constructor()?;
packet.agent_data.agent_id = agent_id;
packet.agent_data.session_id = session_id;
packet.agent_block.group_id = UUID::zero();
packet.agent_block.destination = destination as u8;
packet.agent_block.destination_id = destination_id;
packet.agent_block.packet_count = 1;
packet.agent_block.packet_number = 1;
packet.agent_block.transaction_id = transaction_id;
let mut block = DeRezObjectPacketObjectDataBlock::new_with_constructor()?;
block.object_local_id = object_local_id;
packet.object_data = vec![block];
self.send_generated(&simulator, PacketType::DeRezObject, &packet)
}
pub(crate) fn native_de_rez_to_objects_folder(
&self,
object_local_id: u32,
) -> Result<(), Error> {
self.native_de_rez_to_inventory(
object_local_id,
DeRezDestination::AgentInventoryTake,
self.native_find_folder_for_type(FolderType::Object),
UUID::random()?,
)
}
pub(crate) fn native_give_item(
&self,
item_id: UUID,
item_name: String,
asset_type: AssetType,
recipient: UUID,
do_effect: bool,
) -> Result<(), Error> {
let mut bucket = Vec::with_capacity(17);
bucket.push(asset_type as u8);
bucket.extend_from_slice(&item_id.get_bytes()?);
self.send_inventory_offer(recipient, item_name, bucket, do_effect)?;
self.remove_if_no_copy(item_id)
}
pub(crate) async fn native_give_folder(
&self,
folder_id: UUID,
folder_name: String,
recipient: UUID,
do_effect: bool,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
let (folders, items) = self
.native_collect_inventory_recursive(
folder_id,
self.ids()?.0,
Some(cancellation_token.clone()),
)
.await?;
if folders.len() + items.len() > Self::MAX_GIVE_ITEMS as usize || items.is_empty() {
return Ok(());
}
let mut bucket = Vec::with_capacity(17 * (folders.len() + items.len() + 1));
bucket.push(AssetType::Folder as u8);
bucket.extend_from_slice(&folder_id.get_bytes()?);
for folder in &folders {
bucket.push(AssetType::Folder as u8);
bucket.extend_from_slice(&folder.base.uuid().get_bytes()?);
}
for item in &items {
bucket.push(item.asset_type() as u8);
bucket.extend_from_slice(&item.base.uuid().get_bytes()?);
}
self.send_inventory_offer(recipient, folder_name, bucket, do_effect)?;
for item in items {
self.remove_if_no_copy(item.base.uuid())?;
}
Ok(())
}
pub(crate) async fn native_wait_for_next_inventory_offer(
&self,
cancellation_token: Option<CancellationToken>,
) -> Result<InventoryObjectOfferedEventArgs, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
self.ensure_pending_capacity()?;
let request_id = self.next_request_id();
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.pending_offers).push(PendingOffer {
request_id,
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender,
});
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(offer), _)) => Ok(offer),
Either::Left((Err(_), _)) => Err(Error::InvalidOperation),
Either::Right(_) => {
mutex(&self.inner.pending_offers)
.retain(|request| request.request_id != request_id);
Err(Error::Cancelled)
}
}
}
fn send_inventory_offer(
&self,
recipient: UUID,
message: String,
binary_bucket: Vec<u8>,
do_effect: bool,
) -> Result<(), Error> {
let mut client = self.client()?;
let region_id = client
.native_network()?
.native_current_sim()
.map_or_else(UUID::zero, |simulator| simulator.native_id());
let color = client.settings.default_effect_color;
let agent = client.native_self();
let agent_id = agent.native_agent_id();
agent.native_instant_message_full(
agent.native_name(),
recipient,
message,
UUID::random()?,
InstantMessageDialog::InventoryOffered,
InstantMessageOnline::Online,
Vector3::zero(),
region_id,
binary_bucket,
)?;
if do_effect {
agent.native_viewer_effect(
agent_id,
recipient,
Vector3d::zero(),
color,
1.0,
UUID::random()?,
crate::EffectType::Beam,
None,
)?;
}
Ok(())
}
fn remove_if_no_copy(&self, item_id: UUID) -> Result<(), Error> {
let Some(store) = self.store_clone() else {
return Ok(());
};
let Some(item) = store.native_item_value(item_id) else {
return Ok(());
};
if item.permissions().owner_mask.0 & PermissionMask::COPY.0 == 0 {
store.remove_node_for(&item)?;
}
Ok(())
}
async fn native_collect_inventory_recursive(
&self,
root_folder: UUID,
owner_id: UUID,
cancellation_token: Option<CancellationToken>,
) -> Result<(Vec<InventoryFolder>, Vec<InventoryItem>), Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
let mut folders = Vec::new();
let mut items = Vec::new();
let mut pending = vec![(root_folder, 0_usize)];
let mut visited = HashSet::new();
let mut traversed = 0_usize;
while let Some((folder_id, depth)) = pending.pop() {
cancellation_token.throw_if_cancellation_requested()?;
if depth > MAX_INVENTORY_RECURSION_DEPTH
|| traversed >= MAX_INVENTORY_TRAVERSAL
|| !visited.insert(folder_id)
{
continue;
}
let contents = self
.native_folder_contents(
folder_id,
owner_id,
true,
true,
InventorySortOrder::BY_DATE,
Some(cancellation_token.clone()),
false,
)
.await?;
for value in contents {
traversed = traversed.saturating_add(1);
if let Some(folder) = value.as_any().downcast_ref::<InventoryFolder>() {
folders.push(folder.clone());
pending.push((folder.base.uuid(), depth + 1));
} else if let Some(store_item) = self
.store_clone()
.and_then(|store| store.native_item_value(value.inventory_base().uuid()))
{
items.push(store_item);
} else if let Some(item) = value.as_any().downcast_ref::<InventoryItem>() {
items.push(item.clone());
}
if traversed >= MAX_INVENTORY_TRAVERSAL {
break;
}
}
}
Ok((folders, items))
}
pub(crate) async fn native_get_inventory_recursive(
&self,
folder_id: UUID,
owner_id: UUID,
_folders: Vec<InventoryFolder>,
_items: Vec<InventoryItem>,
cancellation_token: Option<CancellationToken>,
) -> Result<(), Error> {
self.native_collect_inventory_recursive(folder_id, owner_id, cancellation_token)
.await
.map(|_| ())
}
async fn wait_for_capability(
&self,
simulator: &Simulator,
name: &str,
cancellation_token: &CancellationToken,
) -> Result<Option<Uri>, Error> {
let Some(caps) = simulator.native_caps() else {
return Ok(None);
};
if let Some(uri) = caps.capability_uri(name.to_owned())? {
return Ok(Some(uri));
}
if caps.seed_request_finished() {
return Ok(None);
}
let (sender, receiver) = oneshot::channel();
let sender = Arc::new(Mutex::new(Some(sender)));
let wake = Arc::clone(&sender);
let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |_| {
if let Some(sender) = mutex(&wake).take() {
let _ = sender.send(());
}
})));
if let Some(uri) = caps.capability_uri(name.to_owned())? {
return Ok(Some(uri));
}
let cancellation = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancellation);
match select(receiver, cancellation).await {
Either::Left(_) => caps.capability_uri(name.to_owned()),
Either::Right(_) => Err(Error::Cancelled),
}
}
pub(crate) async fn native_get_task_inventory(
&self,
object_id: UUID,
object_local_id: u32,
simulator: Option<Simulator>,
cancellation_token: Option<CancellationToken>,
) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
let cancellation_token = cancellation_token.unwrap_or_default();
cancellation_token.throw_if_cancellation_requested()?;
let simulator = self.current_simulator(simulator)?;
if let Some(capability) = self
.wait_for_capability(&simulator, "RequestTaskInventory", &cancellation_token)
.await?
{
return self
.get_task_inventory_via_cap(object_id, capability, cancellation_token)
.await;
}
// Legacy task inventory is a two-stage correlated operation: first the
// simulator supplies a temporary filename, then an Xfer downloads it.
self.ensure_pending_capacity()?;
let request_id = self.next_request_id();
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.pending_task_replies)
.entry(object_id)
.or_default()
.push(PendingTaskReply {
request_id,
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender,
});
// A disconnected fake simulator can reject the enqueue; keep waiting so
// cancellation still demonstrates callback teardown deterministically.
let _ = self.native_request_task_inventory(object_local_id, Some(simulator.clone()));
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
let filename = match select(receiver, cancelled).await {
Either::Left((Ok(filename), _)) if !filename.is_empty() => filename,
Either::Left(_) => return Ok(Vec::new()),
Either::Right(_) => {
self.remove_pending_task_reply(object_id, request_id);
return Ok(Vec::new());
}
};
let Some(bytes) = self
.download_task_inventory_xfer(simulator, filename, cancellation_token.clone())
.await?
else {
return Ok(Vec::new());
};
if bytes.len() > MAX_TASK_INVENTORY_BYTES {
return Ok(Vec::new());
}
let task_data = wire_string(bytes);
parse_task_inventory_values(&task_data)
}
async fn download_task_inventory_xfer(
&self,
simulator: Simulator,
filename: String,
cancellation_token: CancellationToken,
) -> Result<Option<Vec<u8>>, Error> {
cancellation_token.throw_if_cancellation_requested()?;
self.ensure_pending_capacity()?;
let mut xfer_id = UUID::random()?.get_u_long()?;
{
let transfers = mutex(&self.inner.task_xfers);
while transfers.contains_key(&xfer_id) {
xfer_id = UUID::random()?.get_u_long()?;
}
}
let (sender, receiver) = oneshot::channel();
mutex(&self.inner.task_xfers).insert(
xfer_id,
TaskXfer {
deadline: Instant::now() + CALLBACK_TIMEOUT,
next_packet: 0,
announced_size: None,
bytes: Vec::new(),
sender,
},
);
let mut request = RequestXferPacket::new_with_constructor()?;
request.xfer_id.id = xfer_id;
request.xfer_id.filename = Utils::string_to_bytes(filename)?;
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 = AssetType::Unknown as i16;
if let Err(error) = self.send_generated(&simulator, PacketType::RequestXfer, &request) {
mutex(&self.inner.task_xfers).remove(&xfer_id);
return Err(error);
}
let cancelled = cancellation_token.cancelled();
futures_util::pin_mut!(receiver, cancelled);
match select(receiver, cancelled).await {
Either::Left((Ok(data), _)) => Ok(data),
Either::Left((Err(_), _)) => Ok(None),
Either::Right(_) => {
mutex(&self.inner.task_xfers).remove(&xfer_id);
let mut abort = AbortXferPacket::new_with_constructor()?;
abort.xfer_id.id = xfer_id;
let _ = self.send_generated(&simulator, PacketType::AbortXfer, &abort);
Ok(None)
}
}
}
async fn get_task_inventory_via_cap(
&self,
object_id: UUID,
capability: Uri,
cancellation_token: CancellationToken,
) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
let mut folder = InventoryFolder::new(object_id)?;
folder.base.set_name("Contents".to_owned());
folder.base.set_parent_uuid(UUID::zero());
let mut result: Vec<Box<dyn InventoryObjectClass>> = vec![Box::new(folder)];
let separator = if capability.0.contains('?') { '&' } else { '?' };
let uri = Uri(format!("{}{separator}task_id={}", capability.0, object_id));
let response = self
.client()?
.native_http_caps_client()
.get(uri, cancellation_token, None)
.await;
let Ok((response, bytes)) = response else {
return Ok(result);
};
if !response.is_success_status_code() || bytes.len() > MAX_TASK_INVENTORY_BYTES {
return Ok(result);
}
let Ok(OSD::Map(map)) = OSDParser::deserialize_with_bytes(bytes) else {
return Ok(result);
};
let Some(OSD::Array(contents)) = map.get("contents") else {
return Ok(result);
};
for value in contents.iter().take(MAX_TASK_INVENTORY_ENTRIES) {
let OSD::Map(item_map) = value else {
continue;
};
if !item_map.contains_key("item_id") {
continue;
}
if let Ok(item) = InventoryItem::from_osd(OSD::Map(item_map.clone())) {
result.push(Box::new(item));
}
}
Ok(result)
}
pub(crate) fn native_parse_task_inventory(
task_data: String,
) -> Result<Vec<InventoryBase>, Error> {
if task_data.len() > MAX_TASK_INVENTORY_BYTES {
return Err(Error::Argument);
}
let parsed = parse_task_inventory_values(&task_data)?;
Ok(parsed
.into_iter()
.map(|value| value.inventory_base().clone())
.collect())
}
}
fn local_find_in_store(
store: &Inventory,
base_folder: UUID,
path: &[String],
level: usize,
first_only: bool,
visited: &mut HashSet<UUID>,
depth: usize,
) -> Result<Vec<InventoryBase>, Error> {
if path.is_empty()
|| level >= path.len()
|| depth > MAX_INVENTORY_RECURSION_DEPTH
|| visited.len() >= MAX_INVENTORY_TRAVERSAL
|| !visited.insert(base_folder)
{
return Ok(Vec::new());
}
let mut result = Vec::new();
for value in store
.get_contents_with_uuid(base_folder)
.unwrap_or_default()
{
if value.inventory_base().name() != path[level] {
continue;
}
if level + 1 == path.len() {
result.push(value.inventory_base().clone());
if first_only {
break;
}
} else if value.as_any().is::<InventoryFolder>() {
result.extend(local_find_in_store(
store,
value.inventory_base().uuid(),
path,
level + 1,
first_only,
visited,
depth + 1,
)?);
if first_only && !result.is_empty() {
break;
}
}
}
Ok(result)
}
fn parse_line(line: &str) -> Option<(&str, Option<&str>)> {
let line = line.trim();
if line.is_empty() {
return None;
}
let mut parts = line.splitn(2, char::is_whitespace);
let key = parts.next()?;
let value = parts
.next()
.map(str::trim)
.filter(|value| !value.is_empty());
Some((key, value))
}
fn parse_uuid(value: Option<&str>) -> UUID {
value
.and_then(|value| UUID::new_with_string(value.to_owned()).ok())
.unwrap_or_else(UUID::zero)
}
fn pipe_string(value: Option<&str>) -> String {
value
.unwrap_or_default()
.split('|')
.next()
.unwrap_or_default()
.to_owned()
}
fn parse_hex(value: Option<&str>) -> u32 {
value
.and_then(|value| value.strip_prefix("0x").or(Some(value)))
.and_then(|value| u32::from_str_radix(value, 16).ok())
.unwrap_or_default()
}
fn parse_task_inventory_values(
task_data: &str,
) -> Result<Vec<Box<dyn InventoryObjectClass>>, Error> {
let normalized = task_data.replace("\r\n", "\n");
let lines: Vec<_> = normalized.lines().collect();
let mut line = 0_usize;
let mut result: Vec<Box<dyn InventoryObjectClass>> = Vec::new();
while line < lines.len() && result.len() < MAX_TASK_INVENTORY_ENTRIES {
let Some((kind, _)) = parse_line(lines[line]) else {
line += 1;
continue;
};
line += 1;
if kind == "inv_object" {
let mut item_id = UUID::zero();
let mut parent_id = UUID::zero();
let mut name = String::new();
let mut asset_type = AssetType::Unknown;
while line < lines.len() {
let current = parse_line(lines[line]);
line += 1;
let Some((key, value)) = current else {
continue;
};
if key == "{" {
continue;
}
if key == "}" {
break;
}
match key {
"obj_id" => item_id = parse_uuid(value),
"parent_id" => parent_id = parse_uuid(value),
"type" => {
asset_type =
Utils::string_to_asset_type(value.unwrap_or_default().to_owned())
.unwrap_or(AssetType::Unknown);
}
"name" => name = pipe_string(value),
_ => {}
}
}
if asset_type == AssetType::Folder {
let mut folder = InventoryFolder::new(item_id)?;
folder.base.set_parent_uuid(parent_id);
folder.base.set_name(name);
result.push(Box::new(folder));
} else {
let mut item = InventoryItem::new_with_uuid(item_id)?;
item.base.set_parent_uuid(parent_id);
item.base.set_name(name);
item.set_asset_type(asset_type);
result.push(Box::new(item));
}
} else if kind == "inv_item" {
let mut item_id = UUID::zero();
let mut asset_id = UUID::zero();
let mut parent_id = UUID::zero();
let mut creator_id = UUID::zero();
let mut owner_id = UUID::zero();
let mut last_owner_id = UUID::zero();
let mut group_id = UUID::zero();
let mut group_owned = false;
let mut name = String::new();
let mut description = String::new();
let mut asset_type = AssetType::Unknown;
let mut inventory_type = InventoryType::UNKNOWN;
let mut creation_date = UNIX_EPOCH;
let mut flags = 0_u32;
let mut permissions = Permissions::no_permissions();
let mut sale_type = SaleType::Not;
let mut sale_price = 0_i32;
while line < lines.len() {
let current = parse_line(lines[line]);
line += 1;
let Some((key, value)) = current else {
continue;
};
if key == "{" {
continue;
}
if key == "}" {
break;
}
if key == "permissions" {
while line < lines.len() {
let current = parse_line(lines[line]);
line += 1;
let Some((key, value)) = current else {
continue;
};
if key == "{" {
continue;
}
if key == "}" {
break;
}
match key {
"creator_mask" | "base_mask" => {
permissions.base_mask.0 = parse_hex(value);
}
"owner_mask" => permissions.owner_mask.0 = parse_hex(value),
"group_mask" => permissions.group_mask.0 = parse_hex(value),
"everyone_mask" => permissions.everyone_mask.0 = parse_hex(value),
"next_owner_mask" => permissions.next_owner_mask.0 = parse_hex(value),
"creator_id" => creator_id = parse_uuid(value),
"owner_id" => owner_id = parse_uuid(value),
"last_owner_id" => last_owner_id = parse_uuid(value),
"group_id" => group_id = parse_uuid(value),
"group_owned" => {
group_owned = value
.and_then(|value| value.parse::<u32>().ok())
.is_some_and(|value| value != 0);
}
_ => {}
}
}
} else if key == "sale_info" {
while line < lines.len() {
let current = parse_line(lines[line]);
line += 1;
let Some((key, value)) = current else {
continue;
};
if key == "{" {
continue;
}
if key == "}" {
break;
}
match key {
"sale_type" => {
sale_type = Utils::string_to_sale_type(
value.unwrap_or_default().to_owned(),
)
.unwrap_or(SaleType::Not);
}
"sale_price" => {
sale_price = value
.and_then(|value| value.parse().ok())
.unwrap_or_default();
}
_ => {}
}
}
} else {
match key {
"item_id" => item_id = parse_uuid(value),
"parent_id" => parent_id = parse_uuid(value),
"shadow_id" => {
asset_id =
InventoryManager::native_decrypt_shadow_id(parse_uuid(value))?;
}
"asset_id" => asset_id = parse_uuid(value),
"type" => {
asset_type =
Utils::string_to_asset_type(value.unwrap_or_default().to_owned())
.unwrap_or(AssetType::Unknown);
}
"inv_type" => {
inventory_type = Utils::string_to_inventory_type(
value.unwrap_or_default().to_owned(),
)
.unwrap_or(InventoryType::UNKNOWN);
}
"flags" => {
flags = value
.and_then(|value| value.parse().ok())
.unwrap_or_default();
}
"name" => name = pipe_string(value),
"desc" => description = pipe_string(value),
"creation_date" => {
if let Some(seconds) = value.and_then(|value| value.parse::<u64>().ok())
{
creation_date = UNIX_EPOCH + Duration::from_secs(seconds);
}
}
_ => {}
}
}
}
let mut item = InventoryItem::new_with_inventory_type_uuid(inventory_type, item_id)?;
item.set_asset_uuid(asset_id);
item.set_asset_type(asset_type);
item.set_creation_date(creation_date);
item.set_creator_id(creator_id);
item.set_description(description);
item.set_flags(flags);
item.set_group_id(group_id);
item.set_group_owned(group_owned);
item.base.set_name(name);
item.base.set_owner_id(owner_id);
item.set_last_owner_id(last_owner_id);
item.base.set_parent_uuid(parent_id);
item.set_permissions(permissions);
item.set_sale_price(sale_price);
item.set_sale_type(sale_type);
result.push(Box::new(item));
}
}
Ok(result)
}
impl InventoryManager {
pub(crate) async fn native_update_material_inventory(
&self,
material: crate::assets::AssetMaterial,
material_item_id: UUID,
task_id: Option<UUID>,
cancellation_token: Option<CancellationToken>,
progress: Option<
Box<dyn libremetaverse_types::compat::IProgress<crate::HttpCapsClientProgressReport>>,
>,
) -> Result<(bool, String, UUID, UUID), Error> {
let client = self.client()?;
let simulator = client
.native_network()?
.current_sim()
.ok_or(Error::InvalidOperation)?;
let caps = simulator.native_caps().ok_or(Error::InvalidOperation)?;
let name = if task_id.is_some() {
"UpdateMaterialTaskInventory"
} else {
"UpdateMaterialAgentInventory"
};
let deadline = Instant::now() + Duration::from_secs(2);
let uri = loop {
if let Some(uri) = caps.capability_uri(name.to_owned())? {
break uri;
}
if caps.seed_request_finished() || Instant::now() >= deadline {
return Err(Error::InvalidOperation);
}
std::thread::sleep(Duration::from_millis(1));
};
let mut query = HashMap::from([("item_id".to_owned(), OSD::UUID(material_item_id))]);
if let Some(task_id) = task_id {
query.insert("task_id".to_owned(), OSD::UUID(task_id));
}
let payload = OSDParser::serialize_llsd_xml_bytes(OSD::Map(query))?;
let token = cancellation_token.unwrap_or_default();
let (response, metadata) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
uri,
"application/llsd+xml".into(),
payload,
token.clone(),
None,
)
.await?;
if !(200..300).contains(&response.status_code) {
return Ok((
false,
format!("HTTP {}", response.status_code),
UUID::zero(),
UUID::zero(),
));
}
let meta: serde_json::Value =
serde_json::from_slice(&metadata).map_err(|_| Error::Argument)?;
let status = meta
.get("state")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned();
let uploader = meta
.get("uploader")
.and_then(serde_json::Value::as_str)
.ok_or(Error::Argument)?
.to_owned();
let data = material.to_json()?.into_bytes();
let (upload_response, response_body) = client
.native_http_caps_client()
.post_with_uri_string_bytes_cancellation_token_i_progress(
Uri(uploader),
"application/octet-stream".into(),
data,
token,
progress,
)
.await?;
if !(200..300).contains(&upload_response.status_code) {
return Ok((
false,
format!("HTTP {}", upload_response.status_code),
material_item_id,
UUID::zero(),
));
}
let result: serde_json::Value =
serde_json::from_slice(&response_body).map_err(|_| Error::Argument)?;
let final_status = result
.get("state")
.and_then(serde_json::Value::as_str)
.unwrap_or(&status)
.to_owned();
let asset_id = result
.get("new_asset")
.or_else(|| result.get("new_asset_id"))
.and_then(serde_json::Value::as_str)
.map(|id| UUID::new_with_string(id.to_owned()))
.transpose()?
.unwrap_or_else(UUID::zero);
let item_id = result
.get("item_id")
.and_then(serde_json::Value::as_str)
.map(|id| UUID::new_with_string(id.to_owned()))
.transpose()?
.unwrap_or(material_item_id);
Ok((final_status == "complete", final_status, asset_id, item_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::packets::{
BulkUpdateInventoryPacketItemDataBlock, InventoryDescendentsPacketFolderDataBlock,
UpdateCreateInventoryItemPacketInventoryDataBlock,
};
use futures_util::FutureExt;
fn fixture() -> (Arc<GridClient>, InventoryManager, Inventory) {
let mut client = GridClient::new().expect("client");
let network = client.native_network().expect("network");
client.native_set_network(network);
let _ = client.native_self();
let client = Arc::new(client);
let manager = InventoryManager::native_new(Some(Arc::clone(&client))).expect("manager");
let store = Inventory::new_with_grid_client_uuid(
Arc::clone(&client),
UUID::random().expect("owner"),
)
.expect("store");
*write(&manager.inner.store) = Some(store.clone());
(client, manager, store)
}
fn created_block(
item_id: UUID,
folder_id: UUID,
owner_id: UUID,
callback_id: u32,
) -> UpdateCreateInventoryItemPacketInventoryDataBlock {
let mut block = UpdateCreateInventoryItemPacketInventoryDataBlock::new_with_constructor()
.expect("block");
block.item_id = item_id;
block.folder_id = folder_id;
block.owner_id = owner_id;
block.asset_id = UUID::random().expect("asset");
block.inv_type = InventoryType::NOTECARD.0;
block.type_ = AssetType::Notecard as i8;
block.callback_id = callback_id;
block.name = b"Created\0".to_vec();
block
}
#[test]
fn fake_create_reply_correlates_callback_and_caches_validated_item() {
let (_client, manager, store) = fixture();
let item_id = UUID::random().expect("item");
let folder_id = UUID::random().expect("folder");
let owner_id = UUID::random().expect("owner");
let (matched_sender, matched_receiver) = oneshot::channel();
let (other_sender, mut other_receiver) = oneshot::channel();
mutex(&manager.inner.pending_created).insert(
41,
PendingCreated {
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender: matched_sender,
},
);
mutex(&manager.inner.pending_created).insert(
42,
PendingCreated {
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender: other_sender,
},
);
let mut packet = UpdateCreateInventoryItemPacket::new_with_constructor().expect("packet");
packet.inventory_data = vec![created_block(item_id, folder_id, owner_id, 41)];
manager.handle_update_create(packet).expect("reply");
let item = matched_receiver
.now_or_never()
.expect("matched callback completed")
.expect("matched sender")
.expect("created item");
assert_eq!(item.base.uuid(), item_id);
assert_eq!(item.base.parent_uuid(), folder_id);
assert!(other_receiver.try_recv().expect("other receiver").is_none());
assert!(store.native_item_value(item_id).is_some());
assert!(mutex(&manager.inner.pending_created).contains_key(&42));
manager.native_dispose().expect("dispose");
}
#[test]
fn fake_bulk_copy_reply_waits_for_every_block() {
let (_client, manager, _store) = fixture();
let callback_id = 73;
let (sender, receiver) = oneshot::channel();
mutex(&manager.inner.pending_copied).insert(
callback_id,
PendingCopy {
deadline: Instant::now() + CALLBACK_TIMEOUT,
expected: 2,
copied: Vec::new(),
sender,
},
);
let mut packet = BulkUpdateInventoryPacket::new_with_constructor().expect("packet");
packet.item_data = (0..2)
.map(|index| {
let source = created_block(
UUID::random().expect("item"),
UUID::random().expect("folder"),
UUID::random().expect("owner"),
callback_id,
);
let mut block =
BulkUpdateInventoryPacketItemDataBlock::new_with_constructor().expect("block");
block.item_id = source.item_id;
block.folder_id = source.folder_id;
block.owner_id = source.owner_id;
block.asset_id = source.asset_id;
block.inv_type = source.inv_type;
block.type_ = source.type_;
block.callback_id = source.callback_id;
block.name = format!("copy-{index}\0").into_bytes();
block
})
.collect();
manager.handle_bulk_update(packet).expect("bulk reply");
let result = receiver
.now_or_never()
.expect("copy completed")
.expect("copy sender");
assert!(result.success());
assert_eq!(result.copied_items().expect("copies").len(), 2);
manager.native_dispose().expect("dispose");
}
#[test]
fn copy_from_notecard_udp_packet_preserves_protocol_identifiers() {
let (_client, manager, _store) = fixture();
let object_id = UUID::random().expect("object");
let notecard_id = UUID::random().expect("notecard");
let folder_id = UUID::random().expect("folder");
let item_id = UUID::random().expect("item");
let packet = manager
.copy_from_notecard_packet(object_id, notecard_id, folder_id, item_id)
.expect("packet");
let (agent_id, session_id) = manager.ids().expect("identity");
assert_eq!(packet.agent_data.agent_id, agent_id);
assert_eq!(packet.agent_data.session_id, session_id);
assert_eq!(packet.notecard_data.object_id, object_id);
assert_eq!(packet.notecard_data.notecard_item_id, notecard_id);
assert_eq!(packet.inventory_data.len(), 1);
assert_eq!(packet.inventory_data[0].folder_id, folder_id);
assert_eq!(packet.inventory_data[0].item_id, item_id);
manager.native_dispose().expect("dispose");
}
#[test]
fn cancelling_one_fetch_waiter_preserves_concurrent_correlation() {
let (_client, manager, _store) = fixture();
let item_id = UUID::random().expect("item");
let (cancelled_sender, cancelled_receiver) = oneshot::channel();
let (active_sender, active_receiver) = oneshot::channel();
mutex(&manager.inner.pending_fetches).insert(
item_id,
vec![
PendingFetch {
request_id: 1,
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender: cancelled_sender,
},
PendingFetch {
request_id: 2,
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender: active_sender,
},
],
);
manager.remove_pending_fetch(item_id, 1);
assert!(
cancelled_receiver
.now_or_never()
.expect("cancelled")
.is_err()
);
let mut reply = FetchInventoryReplyPacket::new_with_constructor().expect("packet");
let source = created_block(
item_id,
UUID::random().expect("folder"),
UUID::random().expect("owner"),
0,
);
let mut block =
crate::packets::FetchInventoryReplyPacketInventoryDataBlock::new_with_constructor()
.expect("block");
block.item_id = source.item_id;
block.folder_id = source.folder_id;
block.owner_id = source.owner_id;
block.asset_id = source.asset_id;
block.inv_type = source.inv_type;
block.type_ = source.type_;
reply.inventory_data = vec![block];
manager.handle_fetch_reply(reply).expect("fetch reply");
assert_eq!(
active_receiver
.now_or_never()
.expect("active callback completed")
.expect("active sender")
.expect("item")
.base
.uuid(),
item_id
);
manager.native_dispose().expect("dispose");
}
#[test]
fn stale_descendents_packet_cannot_mutate_store() {
let (_client, manager, store) = fixture();
let parent_id = UUID::random().expect("parent");
let child_id = UUID::random().expect("child");
let mut parent = InventoryFolder::new(parent_id).expect("parent folder");
parent.set_version(10);
store.update_node_for(&parent).expect("cache parent");
let mut packet = InventoryDescendentsPacket::new_with_constructor().expect("packet");
packet.agent_data.folder_id = parent_id;
packet.agent_data.version = 9;
let mut child =
InventoryDescendentsPacketFolderDataBlock::new_with_constructor().expect("child block");
child.folder_id = child_id;
child.parent_id = parent_id;
child.name = b"stale\0".to_vec();
packet.folder_data = vec![child];
manager.handle_descendents(packet).expect("stale reply");
assert!(!store.contains_with_uuid(child_id).expect("contains"));
assert_eq!(
store
.get_value_or_default_with_uuid_a7c63fbe::<InventoryFolder>(parent_id)
.expect("lookup")
.expect("parent")
.version(),
10
);
manager.native_dispose().expect("dispose");
}
#[test]
fn asset_type_without_folder_type_falls_back_to_inventory_root() {
let (_client, manager, mut store) = fixture();
let root_id = UUID::random().expect("root");
let ordinary_id = UUID::random().expect("ordinary folder");
let root = InventoryFolder::new(root_id).expect("root folder");
store.set_root_folder(Some(root));
let mut ordinary = InventoryFolder::new(ordinary_id).expect("ordinary folder");
ordinary.base.set_parent_uuid(root_id);
ordinary.set_preferred_type(FolderType::None);
store
.update_node_for(&ordinary)
.expect("cache ordinary folder");
assert_eq!(
manager.native_find_folder_for_asset_type(AssetType::Script),
root_id
);
manager.native_dispose().expect("dispose");
}
#[test]
fn task_xfer_rejects_announced_data_above_bound() {
let (client, manager, _store) = fixture();
let simulator = Simulator::new(
client.as_ref().clone(),
"127.0.0.1:9".parse().expect("endpoint"),
0,
None,
None,
)
.expect("simulator");
let xfer_id = 991_u64;
let (sender, receiver) = oneshot::channel();
mutex(&manager.inner.task_xfers).insert(
xfer_id,
TaskXfer {
deadline: Instant::now() + CALLBACK_TIMEOUT,
next_packet: 0,
announced_size: None,
bytes: Vec::new(),
sender,
},
);
let mut packet = SendXferPacketPacket::new_with_constructor().expect("packet");
packet.xfer_id.id = xfer_id;
packet.xfer_id.packet = 0x8000_0000;
packet.data_packet.data = u32::try_from(MAX_TASK_INVENTORY_BYTES + 1)
.expect("bounded size")
.to_le_bytes()
.to_vec();
assert_eq!(
manager.handle_task_xfer_packet(packet, simulator),
Err(Error::Argument)
);
assert!(
receiver
.now_or_never()
.expect("xfer completed")
.expect("xfer sender")
.is_none()
);
assert!(!mutex(&manager.inner.task_xfers).contains_key(&xfer_id));
manager.native_dispose().expect("dispose");
}
#[test]
fn fake_inventory_offer_is_correlated_and_shares_acceptance_decision() {
let (_client, manager, _store) = fixture();
let item_id = UUID::random().expect("item");
let destination = UUID::random().expect("destination");
let observed = Arc::new(Mutex::new(None));
let observed_by_handler = Arc::clone(&observed);
let _subscription =
manager.native_subscribe_inventory_object_offered(Arc::new(move |mut args| {
args.set_folder_id(destination);
args.set_accept(true);
*mutex(&observed_by_handler) = Some(args);
}));
let (sender, receiver) = oneshot::channel();
mutex(&manager.inner.pending_offers).push(PendingOffer {
request_id: 17,
deadline: Instant::now() + CALLBACK_TIMEOUT,
sender,
});
let mut bucket = vec![AssetType::Notecard as u8];
bucket.extend_from_slice(&item_id.get_bytes().expect("UUID bytes"));
let offer = InstantMessage {
binary_bucket: bucket,
dialog: InstantMessageDialog::InventoryOffered,
from_agent_id: UUID::random().expect("sender"),
from_agent_name: "Sender".to_owned(),
group_im: false,
im_session_id: UUID::random().expect("session"),
message: "Notecard".to_owned(),
offline: InstantMessageOnline::Online,
parent_estate_id: 0,
position: Vector3::zero(),
region_id: UUID::zero(),
timestamp: UNIX_EPOCH,
to_agent_id: UUID::random().expect("recipient"),
};
manager
.handle_inventory_offer(InstantMessageEventArgs::new(offer, None).expect("event"))
.expect("offer");
let awaited = receiver
.now_or_never()
.expect("waiter completed")
.expect("waiter sender");
assert_eq!(awaited.object_id(), item_id);
assert_eq!(awaited.asset_type(), AssetType::Notecard);
assert!(awaited.accept());
assert_eq!(awaited.folder_id(), destination);
assert!(observed.lock().expect("observed").is_some());
manager.native_dispose().expect("dispose");
}
#[test]
fn fake_caps_script_running_reply_emits_correlated_event() {
let (client, manager, _store) = fixture();
let simulator = Simulator::new(
client.as_ref().clone(),
"127.0.0.1:9".parse().expect("endpoint"),
0,
None,
None,
)
.expect("simulator");
let object_id = UUID::random().expect("object");
let script_id = UUID::random().expect("script");
let observed = Arc::new(Mutex::new(None));
let observed_by_handler = Arc::clone(&observed);
let _subscription = manager.native_subscribe_script_running_reply(Arc::new(move |event| {
*mutex(&observed_by_handler) = Some(event);
}));
let mut reply = ScriptRunningReplyMessage::new().expect("message");
reply.object_id = object_id;
reply.item_id = script_id;
reply.mono = true;
reply.running = false;
client
.native_network()
.expect("network")
.dispatch_caps_event("ScriptRunningReply", &reply, simulator);
let event = observed.lock().expect("observed").clone().expect("event");
assert_eq!(event.object_id(), object_id);
assert_eq!(event.script_id(), script_id);
assert!(event.is_mono());
assert!(!event.is_running());
manager.native_dispose().expect("dispose");
}
}