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
4915 lines
179 KiB
Rust
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");
|
|
}
|
|
}
|