Implement native AIS inventory reconciliation (#63)
All checks were successful
Native code generation / deterministic (push) Successful in 13m18s
Imaging and meshing gate / native (push) Successful in 4m12s
Native Rust workspace compile / compile (push) Successful in 4m12s

This commit is contained in:
2026-08-10 06:41:29 +00:00
parent 61d6721287
commit d5c318d280
16 changed files with 1896 additions and 161 deletions

View File

@@ -699,6 +699,31 @@ impl HttpCapsClient {
.await
}
/// Sends a capability request whose verb or headers are not covered by the
/// fixed public compatibility overloads (`AISv3` uses `COPY` and
/// `Destination`). The request still passes through all normal limits,
/// cancellation, rate limiting, and injected-handler recording.
pub(crate) async fn send_custom(
&self,
method: &str,
uri: Uri,
headers: std::collections::BTreeMap<String, String>,
content_type: Option<String>,
payload: Vec<u8>,
cancellation_token: CancellationToken,
) -> Result<(HttpResponse, Vec<u8>), Error> {
self.send_with_headers(
method,
uri,
headers,
content_type,
payload,
cancellation_token,
None,
)
.await
}
pub async fn get_request(
&self,
uri: Uri,
@@ -1026,6 +1051,28 @@ impl HttpCapsClient {
payload: Vec<u8>,
cancellation_token: CancellationToken,
progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
) -> Result<(HttpResponse, Vec<u8>), Error> {
self.send_with_headers(
method,
uri,
std::collections::BTreeMap::new(),
content_type,
payload,
cancellation_token,
progress,
)
.await
}
async fn send_with_headers(
&self,
method: &str,
uri: Uri,
headers: std::collections::BTreeMap<String, String>,
content_type: Option<String>,
payload: Vec<u8>,
cancellation_token: CancellationToken,
progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>,
) -> Result<(HttpResponse, Vec<u8>), Error> {
if self.0.disposed.load(Ordering::Acquire) {
return Err(Error::InvalidOperation);
@@ -1057,6 +1104,7 @@ impl HttpCapsClient {
handler,
method,
uri,
headers,
content_type,
payload,
cancellation_token,
@@ -1069,6 +1117,7 @@ impl HttpCapsClient {
client,
method,
parsed,
headers,
content_type,
payload,
cancellation_token,
@@ -1086,6 +1135,7 @@ impl HttpCapsClient {
handler: &HttpMessageHandler,
method: &str,
uri: Uri,
headers: std::collections::BTreeMap<String, String>,
content_type: Option<String>,
payload: Vec<u8>,
cancellation_token: CancellationToken,
@@ -1095,7 +1145,7 @@ impl HttpCapsClient {
let request = HttpRequest {
method: method.to_owned(),
uri,
headers: std::collections::BTreeMap::new(),
headers,
content_type,
body: payload,
};
@@ -1112,6 +1162,7 @@ impl HttpCapsClient {
client: &reqwest::Client,
method: &str,
uri: reqwest::Url,
headers: std::collections::BTreeMap<String, String>,
content_type: Option<String>,
payload: Vec<u8>,
cancellation_token: CancellationToken,
@@ -1119,6 +1170,9 @@ impl HttpCapsClient {
) -> Result<(HttpResponse, Vec<u8>), Error> {
let method = reqwest::Method::from_bytes(method.as_bytes()).map_err(|_| Error::Argument)?;
let mut request = client.request(method, uri);
for (name, value) in headers {
request = request.header(name, value);
}
if let Some(content_type) = content_type {
request = request.header(reqwest::header::CONTENT_TYPE, content_type);
}

View File

@@ -119,6 +119,7 @@ struct ClientRuntime {
network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
shutdown_complete: Condvar,
shutdown_wait: Mutex<()>,
}
@@ -148,6 +149,7 @@ impl ClientRuntime {
network_manager: Mutex::new(std::sync::Weak::new()),
agent_manager: Mutex::new(std::sync::Weak::new()),
inventory_manager: Mutex::new(None),
inventory_ais_client: Mutex::new(None),
shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()),
}
@@ -224,6 +226,10 @@ impl ClientRuntime {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.http_caps_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -441,6 +447,30 @@ impl GridClient {
Ok(manager)
}
pub(crate) fn native_ais_client(&self) -> Result<crate::InventoryAISClient, crate::Error> {
let mut cached = self
.runtime
.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(client) = cached.as_ref() {
return Ok(client.clone());
}
let client =
crate::inventory_ais::InventoryAISClient::native_new(Some(Arc::new(self.clone())))?;
*cached = Some(client.clone());
Ok(client)
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn native_set_ais_client(&mut self, value: crate::InventoryAISClient) {
*self
.runtime
.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(value);
}
#[allow(clippy::needless_pass_by_value)] // The mapped C# property setter owns its value.
pub(crate) fn native_set_inventory(&mut self, value: crate::InventoryManager) {
*self

View File

@@ -6,52 +6,14 @@
#![allow(non_snake_case)]
/// C# type: `T:LibreMetaverse.AISResponseMeta`.
pub struct AISResponseMeta;
impl AISResponseMeta {
/// C# member: `F:LibreMetaverse.AISResponseMeta.Empty`.
pub fn empty() -> libremetaverse::AISResponseMeta {
libremetaverse_types::unimplemented_api!("F:LibreMetaverse.AISResponseMeta.Empty")
}
/// C# member: `M:LibreMetaverse.AISResponseMeta.#ctor(System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyDictionary{LibreMetaverse.UUID,System.Int32})`.
pub fn new(
broken_links: Vec<libremetaverse_types::UUID>,
items: Vec<libremetaverse_types::UUID>,
categories: Vec<libremetaverse_types::UUID>,
versions: std::collections::HashMap<libremetaverse_types::UUID, i32>,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.AISResponseMeta.#ctor(System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyDictionary{LibreMetaverse.UUID,System.Int32})",
)
}
/// C# member: `P:LibreMetaverse.AISResponseMeta.BrokenLinksRemoved`.
pub fn broken_links_removed(&self) -> Vec<libremetaverse_types::UUID> {
libremetaverse_types::unimplemented_api!(
"P:LibreMetaverse.AISResponseMeta.BrokenLinksRemoved"
)
}
/// C# member: `P:LibreMetaverse.AISResponseMeta.CategoriesRemoved`.
pub fn categories_removed(&self) -> Vec<libremetaverse_types::UUID> {
libremetaverse_types::unimplemented_api!(
"P:LibreMetaverse.AISResponseMeta.CategoriesRemoved"
)
}
/// C# member: `P:LibreMetaverse.AISResponseMeta.CategoryVersionUpdates`.
pub fn category_version_updates(
&self,
) -> std::collections::HashMap<libremetaverse_types::UUID, i32> {
libremetaverse_types::unimplemented_api!(
"P:LibreMetaverse.AISResponseMeta.CategoryVersionUpdates"
)
}
/// C# member: `P:LibreMetaverse.AISResponseMeta.HasAnyData`.
pub fn has_any_data(&self) -> bool {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AISResponseMeta.HasAnyData")
}
/// C# member: `P:LibreMetaverse.AISResponseMeta.ItemsRemoved`.
pub fn items_removed(&self) -> Vec<libremetaverse_types::UUID> {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AISResponseMeta.ItemsRemoved")
}
}
/// C# member: `F:LibreMetaverse.AISResponseMeta.Empty`.
/// C# member: `M:LibreMetaverse.AISResponseMeta.#ctor(System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyList{LibreMetaverse.UUID},System.Collections.Generic.IReadOnlyDictionary{LibreMetaverse.UUID,System.Int32})`.
/// C# member: `P:LibreMetaverse.AISResponseMeta.BrokenLinksRemoved`.
/// C# member: `P:LibreMetaverse.AISResponseMeta.CategoriesRemoved`.
/// C# member: `P:LibreMetaverse.AISResponseMeta.CategoryVersionUpdates`.
/// C# member: `P:LibreMetaverse.AISResponseMeta.HasAnyData`.
/// C# member: `P:LibreMetaverse.AISResponseMeta.ItemsRemoved`.
pub use crate::inventory_ais::AISResponseMeta;
/// C# type: `T:LibreMetaverse.AccessDeniedReason`.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -9265,11 +9227,14 @@ impl GridClient {
}
/// C# member: `P:LibreMetaverse.GridClient.AisClient`.
pub fn ais_client(&self) -> libremetaverse::InventoryAISClient {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.AisClient")
/* native client-core implementation */
self.native_ais_client()
.unwrap_or_else(|_| panic!("failed to construct InventoryAISClient"))
}
/// Setter for C# member: `P:LibreMetaverse.GridClient.AisClient`.
pub fn set_ais_client(&mut self, value: libremetaverse::InventoryAISClient) {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.AisClient")
/* native client-core implementation */
self.native_set_ais_client(value)
}
/// C# member: `P:LibreMetaverse.GridClient.Animesh`.
pub fn animesh(&self) -> libremetaverse::animesh::AnimeshManager {
@@ -11794,7 +11759,7 @@ impl InterestListManager {
pub use crate::inventory::Inventory;
/// C# type: `T:LibreMetaverse.InventoryAISClient`.
pub struct InventoryAISClient;
pub use crate::inventory_ais::InventoryAISClient;
impl InventoryAISClient {
/// C# member: `F:LibreMetaverse.InventoryAISClient.INVENTORY_CAP_NAME`.
pub const INVENTORY_CAP_NAME: &'static str = "InventoryAPIv3";
@@ -11804,9 +11769,7 @@ impl InventoryAISClient {
pub fn new(
client: Option<std::sync::Arc<libremetaverse::GridClient>>,
) -> Result<Self, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.#ctor(LibreMetaverse.GridClient)",
)
crate::inventory_ais::InventoryAISClient::native_new(client)
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.CopyCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Boolean,System.Threading.CancellationToken)`.
pub async fn copy_category(
@@ -11816,9 +11779,8 @@ impl InventoryAISClient {
simulate: Option<bool>,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CopyCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Boolean,System.Threading.CancellationToken)",
)
self.native_copy_category(source_uuid, dest_uuid, simulate, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.CopyCategoryChildrenAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn copy_category_children(
@@ -11827,9 +11789,8 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CopyCategoryChildrenAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_copy_category_children(category, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.CopyCategoryLinksAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn copy_category_links(
@@ -11838,9 +11799,8 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CopyCategoryLinksAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_copy_category_links(category, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.CopyItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn copy_item(
@@ -11849,9 +11809,8 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CopyItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_copy_item(item_uuid, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.CopyLibraryCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Boolean,System.Threading.CancellationToken)`.
pub async fn copy_library_category(
@@ -11861,9 +11820,13 @@ impl InventoryAISClient {
copy_subfolders: bool,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CopyLibraryCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Boolean,System.Threading.CancellationToken)",
self.native_copy_library_category(
source_uuid,
dest_uuid,
copy_subfolders,
cancellation_token,
)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.CreateInventoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Boolean,System.Threading.CancellationToken)`.
pub async fn create_inventory(
@@ -11873,9 +11836,8 @@ impl InventoryAISClient {
create_link: bool,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<(bool, Option<libremetaverse::InventoryItem>), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CreateInventoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Boolean,System.Threading.CancellationToken)",
)
self.native_create_inventory(parent_uuid, new_inventory, create_link, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.DeleteCategoryChildrenAsync(System.String,System.Threading.CancellationToken)`.
pub async fn delete_category_children(
@@ -11883,9 +11845,8 @@ impl InventoryAISClient {
category: String,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.DeleteCategoryChildrenAsync(System.String,System.Threading.CancellationToken)",
)
self.native_delete_category_children(category, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.DeleteCategoryLinksAsync(System.String,System.Threading.CancellationToken)`.
pub async fn delete_category_links(
@@ -11893,27 +11854,22 @@ impl InventoryAISClient {
category: String,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.DeleteCategoryLinksAsync(System.String,System.Threading.CancellationToken)",
)
self.native_delete_category_links(category, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.EmptyTrashAsync(System.Threading.CancellationToken)`.
pub async fn empty_trash(
&self,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.EmptyTrashAsync(System.Threading.CancellationToken)",
)
self.native_empty_trash(cancellation_token).await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.FetchCOFAsync(System.Threading.CancellationToken)`.
pub async fn fetch_cof(
&self,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.FetchCOFAsync(System.Threading.CancellationToken)",
)
self.native_fetch_cof(cancellation_token).await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.FetchCategoryCategoriesAsync(LibreMetaverse.UUID,System.Boolean,System.Boolean,System.Int32,System.Threading.CancellationToken)`.
pub async fn fetch_category_categories(
@@ -11932,9 +11888,14 @@ impl InventoryAISClient {
),
crate::Error,
> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.FetchCategoryCategoriesAsync(LibreMetaverse.UUID,System.Boolean,System.Boolean,System.Int32,System.Threading.CancellationToken)",
self.native_fetch_category_categories(
cat_id,
use_inventory_cap,
recursive,
depth,
cancellation_token,
)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.FetchCategorySubsetAsync(LibreMetaverse.UUID,System.Collections.Generic.IEnumerable{LibreMetaverse.UUID},System.Boolean,System.Boolean,System.Int32,System.Threading.CancellationToken)`.
pub async fn fetch_category_subset(
@@ -11954,9 +11915,15 @@ impl InventoryAISClient {
),
crate::Error,
> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.FetchCategorySubsetAsync(LibreMetaverse.UUID,System.Collections.Generic.IEnumerable{LibreMetaverse.UUID},System.Boolean,System.Boolean,System.Int32,System.Threading.CancellationToken)",
self.native_fetch_category_subset(
cat_id,
specific_children,
use_inventory_cap,
recursive,
depth,
cancellation_token,
)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.FetchItemAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn fetch_item(
@@ -11972,18 +11939,14 @@ impl InventoryAISClient {
),
crate::Error,
> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.FetchItemAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_fetch_item(item_uuid, cancellation_token).await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.FetchOrphansAsync(System.Threading.CancellationToken)`.
pub async fn fetch_orphans(
&self,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.FetchOrphansAsync(System.Threading.CancellationToken)",
)
self.native_fetch_orphans(cancellation_token).await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.GetCategoryAsync(System.String,System.Threading.CancellationToken)`.
pub async fn get_category(
@@ -11999,9 +11962,7 @@ impl InventoryAISClient {
),
crate::Error,
> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.GetCategoryAsync(System.String,System.Threading.CancellationToken)",
)
self.native_get_category(category, cancellation_token).await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.GetCategoryChildrenAsync(System.String,System.Int32,System.Boolean,System.Threading.CancellationToken)`.
pub async fn get_category_children(
@@ -12019,9 +11980,8 @@ impl InventoryAISClient {
),
crate::Error,
> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.GetCategoryChildrenAsync(System.String,System.Int32,System.Boolean,System.Threading.CancellationToken)",
)
self.native_get_category_children(category, depth, recursive, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.GetCategoryLinksAsync(System.String,System.Threading.CancellationToken)`.
pub async fn get_category_links(
@@ -12037,9 +11997,8 @@ impl InventoryAISClient {
),
crate::Error,
> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.GetCategoryLinksAsync(System.String,System.Threading.CancellationToken)",
)
self.native_get_category_links(category, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.MoveCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn move_category(
@@ -12048,9 +12007,8 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.MoveCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_move_category(source_uuid, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.MoveCategoryChildrenAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn move_category_children(
@@ -12059,9 +12017,8 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.MoveCategoryChildrenAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_move_category_children(category, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.MoveCategoryLinksAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn move_category_links(
@@ -12070,9 +12027,8 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.MoveCategoryLinksAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_move_category_links(category, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.MoveItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn move_item(
@@ -12081,17 +12037,14 @@ impl InventoryAISClient {
dest_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.MoveItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_move_item(item_uuid, dest_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.ParseAISResponseMeta(LibreMetaverse.StructuredData.OSDMap)`.
pub fn parse_ais_response_meta(
response: Option<libremetaverse_structured_data::OSDMap>,
) -> Result<libremetaverse::AISResponseMeta, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.ParseAISResponseMeta(LibreMetaverse.StructuredData.OSDMap)",
)
crate::inventory_ais::InventoryAISClient::native_parse_ais_response_meta(response)
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.ParseEmbedded(LibreMetaverse.StructuredData.OSDMap,System.Collections.Generic.List{LibreMetaverse.InventoryFolder}@,System.Collections.Generic.List{LibreMetaverse.InventoryItem}@,System.Collections.Generic.List{LibreMetaverse.InventoryItem}@)`.
pub fn parse_embedded(
@@ -12101,36 +12054,28 @@ impl InventoryAISClient {
items: &mut Option<Vec<libremetaverse::InventoryItem>>,
links: &mut Option<Vec<libremetaverse::InventoryItem>>,
) -> Result<(), crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.ParseEmbedded(LibreMetaverse.StructuredData.OSDMap,System.Collections.Generic.List{LibreMetaverse.InventoryFolder}@,System.Collections.Generic.List{LibreMetaverse.InventoryItem}@,System.Collections.Generic.List{LibreMetaverse.InventoryItem}@)",
)
self.native_parse_embedded(response, folders, items, links)
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.ParseFoldersFromEmbedded(LibreMetaverse.StructuredData.OSDMap)`.
pub fn parse_folders_from_embedded(
&self,
response: libremetaverse_structured_data::OSDMap,
) -> Result<Vec<libremetaverse::InventoryFolder>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.ParseFoldersFromEmbedded(LibreMetaverse.StructuredData.OSDMap)",
)
self.native_parse_folders_from_embedded(response)
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.ParseItemsFromEmbedded(LibreMetaverse.StructuredData.OSDMap)`.
pub fn parse_items_from_embedded(
&self,
response: Option<libremetaverse_structured_data::OSD>,
) -> Result<Vec<libremetaverse::InventoryItem>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.ParseItemsFromEmbedded(LibreMetaverse.StructuredData.OSDMap)",
)
self.native_parse_items_from_embedded(response)
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.ParseLinksFromEmbedded(LibreMetaverse.StructuredData.OSDMap)`.
pub fn parse_links_from_embedded(
&self,
response: Option<libremetaverse_structured_data::OSD>,
) -> Result<Vec<libremetaverse::InventoryItem>, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.ParseLinksFromEmbedded(LibreMetaverse.StructuredData.OSDMap)",
)
self.native_parse_links_from_embedded(response)
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.PurgeDescendentsAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn purge_descendents(
@@ -12138,9 +12083,8 @@ impl InventoryAISClient {
category_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.PurgeDescendentsAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_purge_descendents(category_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.PutCategoryChildrenAsync(System.String,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)`.
pub async fn put_category_children(
@@ -12149,9 +12093,8 @@ impl InventoryAISClient {
children_payload: libremetaverse_structured_data::OSD,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.PutCategoryChildrenAsync(System.String,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)",
)
self.native_put_category_children(category, children_payload, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.PutCategoryLinksAsync(System.String,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)`.
pub async fn put_category_links(
@@ -12160,9 +12103,8 @@ impl InventoryAISClient {
links_payload: libremetaverse_structured_data::OSD,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.PutCategoryLinksAsync(System.String,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)",
)
self.native_put_category_links(category, links_payload, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.RemoveCategoryAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn remove_category(
@@ -12170,9 +12112,8 @@ impl InventoryAISClient {
category_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.RemoveCategoryAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_remove_category(category_uuid, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.RemoveItemAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)`.
pub async fn remove_item(
@@ -12180,9 +12121,7 @@ impl InventoryAISClient {
item_uuid: libremetaverse_types::UUID,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.RemoveItemAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)",
)
self.native_remove_item(item_uuid, cancellation_token).await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.SlamFolderAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)`.
pub async fn slam_folder(
@@ -12191,9 +12130,8 @@ impl InventoryAISClient {
new_inventory: libremetaverse_structured_data::OSD,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.SlamFolderAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)",
)
self.native_slam_folder(folder_uuid, new_inventory, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.UpdateCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)`.
pub async fn update_category(
@@ -12202,9 +12140,8 @@ impl InventoryAISClient {
updates: libremetaverse_structured_data::OSD,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.UpdateCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)",
)
self.native_update_category(category_uuid, updates, cancellation_token)
.await
}
/// C# member: `M:LibreMetaverse.InventoryAISClient.UpdateItemAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)`.
pub async fn update_item(
@@ -12213,13 +12150,12 @@ impl InventoryAISClient {
updates: libremetaverse_structured_data::OSD,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<bool, crate::Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.UpdateItemAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)",
)
self.native_update_item(item_uuid, updates, cancellation_token)
.await
}
/// C# member: `P:LibreMetaverse.InventoryAISClient.IsAvailable`.
pub fn is_available(&self) -> bool {
libremetaverse_types::unimplemented_api!("P:LibreMetaverse.InventoryAISClient.IsAvailable")
self.native_is_available()
}
}

View File

@@ -855,6 +855,35 @@ enum InventoryValue {
}
impl InventoryValue {
fn from_item_typed(item: InventoryItem) -> Self {
macro_rules! typed {
($kind:ty, $variant:ident) => {{
let mut value = <$kind>::new(item.base.uuid)
.expect("known inventory item constructor cannot fail");
value.base = item;
Self::$variant(value)
}};
}
match item.inventory_type {
InventoryType::TEXTURE => typed!(InventoryTexture, Texture),
InventoryType::SOUND => typed!(InventorySound, Sound),
InventoryType::CALLING_CARD => typed!(InventoryCallingCard, CallingCard),
InventoryType::LANDMARK => typed!(InventoryLandmark, Landmark),
InventoryType::OBJECT => typed!(InventoryObject, Object),
InventoryType::NOTECARD => typed!(InventoryNotecard, Notecard),
InventoryType::CATEGORY => typed!(InventoryCategory, Category),
InventoryType::LSL => typed!(InventoryLSL, Lsl),
InventoryType::SNAPSHOT => typed!(InventorySnapshot, Snapshot),
InventoryType::ATTACHMENT => typed!(InventoryAttachment, Attachment),
InventoryType::WEARABLE => typed!(InventoryWearable, Wearable),
InventoryType::ANIMATION => typed!(InventoryAnimation, Animation),
InventoryType::GESTURE => typed!(InventoryGesture, Gesture),
InventoryType::SETTINGS => typed!(InventorySettings, Settings),
InventoryType::MATERIAL => typed!(InventoryMaterial, Material),
_ => Self::Item(item),
}
}
fn from_class(value: &dyn InventoryObjectClass) -> Option<Self> {
macro_rules! downcast {
($type:ty, $variant:ident) => {
@@ -1472,6 +1501,161 @@ impl Inventory {
.and_then(|value| value.item().cloned())
}
/// Atomically applies one fully parsed AIS response. A detached candidate
/// graph is built first, so validation or rebuilding failures cannot expose
/// a partially reconciled inventory to readers.
#[allow(clippy::too_many_lines)] // One lock-free candidate build followed by one state swap.
pub(crate) fn native_apply_ais_batch(
&self,
folders: Vec<InventoryFolder>,
items: Vec<InventoryItem>,
removed_items: &[UUID],
removed_categories: &[UUID],
broken_links: &[UUID],
versions: &HashMap<UUID, i32>,
) -> Result<(), Error> {
let mut incoming = HashSet::new();
for uuid in folders
.iter()
.map(|folder| folder.base.uuid)
.chain(items.iter().map(|item| item.base.uuid))
{
if uuid == UUID::zero() || !incoming.insert(uuid) {
return Err(Error::Argument);
}
}
if incoming.len() > MAX_CACHE_RECORDS || versions.keys().any(|uuid| *uuid == UUID::zero()) {
return Err(Error::Argument);
}
let mut touched = incoming.clone();
touched.extend(versions.keys().copied());
let (mut candidate, old_values) = {
let state = read(&self.inner.state);
let old_values: HashMap<_, _> = state
.items
.iter()
.filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value)))
.collect();
let mut candidate = InventoryState {
root: state.root,
library_root: state.library_root,
..InventoryState::default()
};
for (uuid, value) in &old_values {
candidate
.items
.insert(*uuid, InventoryNode::from_value(value.clone()));
}
(candidate, old_values)
};
for folder in folders {
candidate.items.insert(
folder.base.uuid,
InventoryNode::from_value(InventoryValue::Folder(folder)),
);
}
for item in items {
let uuid = item.base.uuid;
let value = InventoryValue::from_item_typed(item);
candidate
.items
.insert(uuid, InventoryNode::from_value(value));
}
for (uuid, version) in versions {
if let Some(node) = candidate.items.get(uuid) {
let Some(mut value) = node.value() else {
return Err(Error::InvalidOperation);
};
let Some(folder) = value.folder_mut() else {
return Err(Error::InvalidOperation);
};
folder.version = *version;
node.set_value(value);
}
}
let direct_removals: HashSet<_> = removed_items
.iter()
.chain(broken_links)
.copied()
.filter(|uuid| *uuid != UUID::zero())
.collect();
for uuid in direct_removals {
candidate.items.remove(&uuid);
}
rebuild_indexes_and_counts(&mut candidate);
for category in removed_categories
.iter()
.copied()
.filter(|uuid| *uuid != UUID::zero())
{
let mut pending = vec![category];
let mut descendants = HashSet::new();
while let Some(uuid) = pending.pop() {
if descendants.len() >= MAX_CACHE_RECORDS || !descendants.insert(uuid) {
continue;
}
if let Some(node) = candidate.items.get(&uuid) {
pending.extend(read(&node.inner).children.keys().copied());
}
}
for uuid in descendants {
candidate.items.remove(&uuid);
}
if candidate.root == Some(category) {
candidate.root = None;
}
if candidate.library_root == Some(category) {
candidate.library_root = None;
}
}
if candidate.items.len() > MAX_CACHE_RECORDS {
return Err(Error::InvalidOperation);
}
rebuild_indexes_and_counts(&mut candidate);
candidate.root_node = candidate
.root
.and_then(|uuid| candidate.items.get(&uuid).cloned());
candidate.library_root_node = candidate
.library_root
.and_then(|uuid| candidate.items.get(&uuid).cloned());
let new_values: HashMap<_, _> = candidate
.items
.iter()
.filter_map(|(uuid, node)| node.value().map(|value| (*uuid, value)))
.collect();
*write(&self.inner.state) = candidate;
for (uuid, old) in &old_values {
match new_values.get(uuid) {
None => self
.inner
.removed
.emit(InventoryObjectRemovedEventArgs::from_value(old.clone())),
Some(new) if touched.contains(uuid) => {
self.inner
.updated
.emit(InventoryObjectUpdatedEventArgs::from_values(
old.clone(),
new.clone(),
));
}
Some(_) => {}
}
}
for (uuid, new) in new_values {
if !old_values.contains_key(&uuid) {
self.inner
.added
.emit(InventoryObjectAddedEventArgs::from_value(new));
}
}
Ok(())
}
pub fn try_get_value_with_uuid_inventory_base(
&self,
uuid: UUID,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,215 @@
use super::*;
use libremetaverse_structured_data::{OSD, OSDParser};
use libremetaverse_types::compat::{
CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use libremetaverse_types::{InventoryType, UUID};
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex};
const CAP: &str = "http://inventory.test/ais";
const LIBRARY_CAP: &str = "http://library.test/ais";
const SEED: &str = "http://inventory.test/seed";
fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, Wake, Waker};
struct ThreadWake(std::thread::Thread);
impl Wake for ThreadWake {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
}
let waker = Waker::from(Arc::new(ThreadWake(std::thread::current())));
let mut context = Context::from_waker(&waker);
let mut future = std::pin::pin!(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(output) => return output,
Poll::Pending => std::thread::park(),
}
}
}
fn client_with_response(body: Vec<u8>) -> (GridClient, Arc<Mutex<Vec<HttpRequest>>>) {
let requests = Arc::new(Mutex::new(Vec::new()));
let recorded = Arc::clone(&requests);
let seed = format!(
"<llsd><map><key>InventoryAPIv3</key><uri>{CAP}</uri><key>LibraryAPIv3</key><uri>{LIBRARY_CAP}</uri></map></llsd>"
);
let handler = HttpMessageHandler::new(move |request, _| {
let response = if request.uri.0 == SEED {
HttpResponse {
status_code: 200,
headers: BTreeMap::new(),
content_type: Some("application/llsd+xml".into()),
body: seed.as_bytes().to_vec(),
}
} else {
recorded.lock().unwrap().push(request);
HttpResponse {
status_code: 200,
headers: BTreeMap::new(),
content_type: Some("application/llsd+xml".into()),
body: body.clone(),
}
};
async move { response }
});
let mut client = GridClient::new().unwrap();
client.set_http_caps_client(HttpCapsClient::new(handler.clone()).unwrap());
let mut simulator_client = GridClient::new().unwrap();
simulator_client.set_http_caps_client(HttpCapsClient::new(handler).unwrap());
let simulator = Simulator::new(
simulator_client,
"127.0.0.1:0".parse().unwrap(),
0,
None,
None,
)
.unwrap();
simulator
.set_seed_caps(Some(Uri(SEED.into())), Some(true))
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while simulator
.native_caps()
.and_then(|caps| caps.capability_uri("InventoryAPIv3".into()).ok().flatten())
.is_none()
&& std::time::Instant::now() < deadline
{
std::thread::sleep(std::time::Duration::from_millis(5));
}
let mut network = client.network();
network.set_current_sim(Some(simulator));
client.set_network(network);
(client, requests)
}
#[test]
fn records_exact_ais_verbs_paths_headers_and_llsd_payloads() {
let (client, requests) = client_with_response(Vec::new());
let ais = client.ais_client();
assert!(ais.is_available(), "seed capability should be visible");
let item = UUID::random().unwrap();
let destination = UUID::random().unwrap();
let payload = OSD::Map(HashMap::from([(
"name".into(),
OSD::String("renamed".into()),
)]));
assert!(block_on(ais.update_item(item, payload.clone(), None)).unwrap());
assert!(block_on(ais.copy_item(item, destination, None)).unwrap());
assert!(
block_on(ais.get_category_children("current".into(), 0, true, None))
.unwrap()
.0
);
let requests = requests.lock().unwrap();
assert_eq!(requests.len(), 3);
assert_eq!(requests[0].method, "PATCH");
assert_eq!(requests[0].uri.0, format!("{CAP}/item/{item}"));
assert_eq!(
requests[0].content_type.as_deref(),
Some("application/llsd+xml")
);
assert_eq!(
OSDParser::deserialize_with_bytes(requests[0].body.clone()).unwrap(),
payload
);
assert_eq!(requests[1].method, "COPY");
assert!(
requests[1]
.uri
.0
.starts_with(&format!("{CAP}/item/{item}?tid="))
);
assert_eq!(
requests[1].headers.get("Destination"),
Some(&destination.to_string())
);
assert!(requests[1].body.is_empty());
assert_eq!(requests[2].method, "GET");
assert_eq!(
requests[2].uri.0,
format!("{CAP}/category/current/children?depth=50")
);
}
#[test]
fn malformed_and_cancelled_responses_leave_store_unchanged() {
let (client, requests) = client_with_response(b"not llsd".to_vec());
let owner = UUID::random().unwrap();
let item_id = UUID::random().unwrap();
let store = Inventory::new_with_grid_client_uuid(Arc::new(client.clone()), owner).unwrap();
let item = InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id).unwrap();
store.update_node_for(&item).unwrap();
client.inventory().native_set_store_for_tests(store.clone());
let result = block_on(client.ais_client().fetch_item(item_id, None)).unwrap();
assert!(!result.0);
assert!(store.contains_with_uuid(item_id).unwrap());
assert_eq!(store.count(), 1);
let cancellation = CancellationTokenSource::new();
cancellation.cancel();
let result = block_on(
client
.ais_client()
.fetch_item(item_id, Some(cancellation.token())),
);
assert!(matches!(result, Err(Error::Cancelled)));
assert_eq!(requests.lock().unwrap().len(), 1);
assert_eq!(store.count(), 1);
}
#[test]
fn partially_valid_response_is_rejected_before_any_reconciliation() {
let existing = UUID::random().unwrap();
let new_folder = UUID::random().unwrap();
let malformed_item = UUID::random().unwrap();
let zero = UUID::zero();
let body = format!(
"<llsd><map><key>_removed_items</key><array><uuid>{existing}</uuid></array><key>_embedded</key><map><key>categories</key><map><key>{new_folder}</key><map><key>category_id</key><uuid>{new_folder}</uuid><key>parent_id</key><uuid>{zero}</uuid><key>name</key><string>valid folder</string></map></map><key>items</key><map><key>{malformed_item}</key><map><key>item_id</key><string>not-a-uuid</string></map></map></map></map></llsd>"
);
let (client, _) = client_with_response(body.into_bytes());
let store =
Inventory::new_with_grid_client_uuid(Arc::new(client.clone()), UUID::random().unwrap())
.unwrap();
let item =
InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, existing).unwrap();
store.update_node_for(&item).unwrap();
client.inventory().native_set_store_for_tests(store.clone());
let result = block_on(client.ais_client().get_category("current".into(), None)).unwrap();
assert!(!result.0);
assert!(store.contains_with_uuid(existing).unwrap());
assert!(!store.contains_with_uuid(new_folder).unwrap());
assert_eq!(store.count(), 1);
}
#[test]
fn response_metadata_reconciles_removals_and_folder_versions_together() {
let folder_id = UUID::random().unwrap();
let item_id = UUID::random().unwrap();
let body = format!(
"<llsd><map><key>_removed_items</key><array><uuid>{item_id}</uuid></array><key>_updated_category_versions</key><map><key>{folder_id}</key><integer>17</integer></map></map></llsd>"
);
let (client, _) = client_with_response(body.into_bytes());
let owner = UUID::random().unwrap();
let store = Inventory::new_with_grid_client_uuid(Arc::new(client.clone()), owner).unwrap();
let folder = InventoryFolder::new(folder_id).unwrap();
let mut item =
InventoryItem::new_with_inventory_type_uuid(InventoryType::OBJECT, item_id).unwrap();
item.base.set_parent_uuid(folder_id);
store.update_node_for(&folder).unwrap();
store.update_node_for(&item).unwrap();
client.inventory().native_set_store_for_tests(store.clone());
assert!(block_on(client.ais_client().remove_item(item_id, None)).unwrap());
assert!(!store.contains_with_uuid(item_id).unwrap());
let updated: Option<InventoryFolder> = store
.get_value_or_default_with_uuid_a7c63fbe(folder_id)
.unwrap();
assert_eq!(updated.unwrap().version(), 17);
}

View File

@@ -1215,6 +1215,30 @@ impl InventoryManager {
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);

View File

@@ -15,6 +15,7 @@ mod download_manager;
mod event_queue;
mod gesture;
mod inventory;
mod inventory_ais;
mod inventory_manager;
#[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator.
mod foliage_catalog;
@@ -47,6 +48,8 @@ mod avatar_animesh_semantics;
#[cfg(test)]
mod batch_link_internal_semantics;
#[cfg(test)]
mod inventory_ais_internal_semantics;
#[cfg(test)]
mod world_internal_semantics;
type Terrain = Vec<Vec<f32>>;
@@ -146,17 +149,16 @@ impl appearance::CurrentOutfitFolder {
}
}
#[allow(clippy::unused_async, dead_code)]
#[allow(dead_code)] // Internal C# surface consumed by InventoryManager workflows.
impl InventoryAISClient {
async fn create_inventory_links(
&self,
_parent_uuid: libremetaverse_types::UUID,
_new_inventory: libremetaverse_structured_data::OSD,
_cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
parent_uuid: libremetaverse_types::UUID,
new_inventory: libremetaverse_structured_data::OSD,
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> Result<Vec<InventoryItem>, Error> {
libremetaverse_types::not_implemented(
"M:LibreMetaverse.InventoryAISClient.CreateInventoryLinksAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)",
)
self.native_create_inventory_links(parent_uuid, new_inventory, cancellation_token)
.await
}
}