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

@@ -17,9 +17,12 @@ on:
- "tools/check_milestone_09_issue_60.py" - "tools/check_milestone_09_issue_60.py"
- "tools/check_milestone_09_issue_61.py" - "tools/check_milestone_09_issue_61.py"
- "tools/check_milestone_09_issue_62.py" - "tools/check_milestone_09_issue_62.py"
- "tools/check_milestone_09_issue_63.py"
- "crates/libremetaverse/src/agent_movement.rs" - "crates/libremetaverse/src/agent_movement.rs"
- "crates/libremetaverse/src/inventory.rs" - "crates/libremetaverse/src/inventory.rs"
- "crates/libremetaverse/src/inventory_manager.rs" - "crates/libremetaverse/src/inventory_manager.rs"
- "crates/libremetaverse/src/inventory_ais.rs"
- "crates/libremetaverse/src/inventory_ais_internal_semantics.rs"
- "docs/agent-movement.md" - "docs/agent-movement.md"
- "docs/inventory.md" - "docs/inventory.md"
- "crates/libremetaverse/Cargo.toml" - "crates/libremetaverse/Cargo.toml"
@@ -35,6 +38,7 @@ on:
- "tests/compat/tests/appearance_visual_semantics.rs" - "tests/compat/tests/appearance_visual_semantics.rs"
- "tests/compat/tests/inventory_store_semantics.rs" - "tests/compat/tests/inventory_store_semantics.rs"
- "tests/compat/tests/inventory_manager_semantics.rs" - "tests/compat/tests/inventory_manager_semantics.rs"
- "tests/compat/tests/inventory_ais_semantics.rs"
- "tests/compat/tests/task_inventory_semantics.rs" - "tests/compat/tests/task_inventory_semantics.rs"
- "tests/compat/tests/misclassified_link_semantics.rs" - "tests/compat/tests/misclassified_link_semantics.rs"
- "tests/upstream-tests.json" - "tests/upstream-tests.json"
@@ -54,9 +58,12 @@ on:
- "tools/check_milestone_09_issue_60.py" - "tools/check_milestone_09_issue_60.py"
- "tools/check_milestone_09_issue_61.py" - "tools/check_milestone_09_issue_61.py"
- "tools/check_milestone_09_issue_62.py" - "tools/check_milestone_09_issue_62.py"
- "tools/check_milestone_09_issue_63.py"
- "crates/libremetaverse/src/agent_movement.rs" - "crates/libremetaverse/src/agent_movement.rs"
- "crates/libremetaverse/src/inventory.rs" - "crates/libremetaverse/src/inventory.rs"
- "crates/libremetaverse/src/inventory_manager.rs" - "crates/libremetaverse/src/inventory_manager.rs"
- "crates/libremetaverse/src/inventory_ais.rs"
- "crates/libremetaverse/src/inventory_ais_internal_semantics.rs"
- "docs/agent-movement.md" - "docs/agent-movement.md"
- "docs/inventory.md" - "docs/inventory.md"
- "crates/libremetaverse/Cargo.toml" - "crates/libremetaverse/Cargo.toml"
@@ -72,6 +79,7 @@ on:
- "tests/compat/tests/appearance_visual_semantics.rs" - "tests/compat/tests/appearance_visual_semantics.rs"
- "tests/compat/tests/inventory_store_semantics.rs" - "tests/compat/tests/inventory_store_semantics.rs"
- "tests/compat/tests/inventory_manager_semantics.rs" - "tests/compat/tests/inventory_manager_semantics.rs"
- "tests/compat/tests/inventory_ais_semantics.rs"
- "tests/compat/tests/task_inventory_semantics.rs" - "tests/compat/tests/task_inventory_semantics.rs"
- "tests/compat/tests/misclassified_link_semantics.rs" - "tests/compat/tests/misclassified_link_semantics.rs"
- "tests/upstream-tests.json" - "tests/upstream-tests.json"
@@ -99,6 +107,7 @@ jobs:
python3 tools/check_milestone_09_issue_60.py python3 tools/check_milestone_09_issue_60.py
python3 tools/check_milestone_09_issue_61.py python3 tools/check_milestone_09_issue_61.py
python3 tools/check_milestone_09_issue_62.py python3 tools/check_milestone_09_issue_62.py
python3 tools/check_milestone_09_issue_63.py
cargo check --manifest-path tests/api-compile/Cargo.toml --locked cargo check --manifest-path tests/api-compile/Cargo.toml --locked
- name: Test generated wire codecs and translated parity cases - name: Test generated wire codecs and translated parity cases
run: | run: |
@@ -108,6 +117,8 @@ jobs:
cargo test -p libremetaverse-compat-tests --test wire_semantics cargo test -p libremetaverse-compat-tests --test wire_semantics
cargo test -p libremetaverse-compat-tests --test inventory_store_semantics cargo test -p libremetaverse-compat-tests --test inventory_store_semantics
cargo test -p libremetaverse-compat-tests --test inventory_manager_semantics cargo test -p libremetaverse-compat-tests --test inventory_manager_semantics
cargo test -p libremetaverse-compat-tests --test inventory_ais_semantics
cargo test -p libremetaverse --lib inventory_ais_internal_semantics
cargo test -p libremetaverse-compat-tests --test task_inventory_semantics cargo test -p libremetaverse-compat-tests --test task_inventory_semantics
cargo test -p libremetaverse-compat-tests --test misclassified_link_semantics cargo test -p libremetaverse-compat-tests --test misclassified_link_semantics
- name: Test generated visual and foliage catalogs - name: Test generated visual and foliage catalogs

View File

@@ -31,5 +31,6 @@ jobs:
python3 tools/check_milestone_09_issue_60.py python3 tools/check_milestone_09_issue_60.py
python3 tools/check_milestone_09_issue_61.py python3 tools/check_milestone_09_issue_61.py
python3 tools/check_milestone_09_issue_62.py python3 tools/check_milestone_09_issue_62.py
python3 tools/check_milestone_09_issue_63.py
- name: Compile every workspace target with bounded memory - name: Compile every workspace target with bounded memory
run: cargo check --workspace --all-targets --locked -j 1 run: cargo check --workspace --all-targets --locked -j 1

View File

@@ -367,3 +367,11 @@ before store reconciliation, and legacy task inventory uses a 16 MiB-bounded
`ReplyTaskInventory`/`RequestXfer` state machine. Focused translated and native `ReplyTaskInventory`/`RequestXfer` state machine. Focused translated and native
fake-packet tests cover correlation, stale replies, offers, bulk completion, fake-packet tests cover correlation, stale replies, offers, bulk completion,
capability task inventory, and malformed transfer sizes. capability task inventory, and malformed transfer sizes.
The client-owned `InventoryAISClient` implements Inventory/Library API v3
category, child, link, item, outfit, orphan, and trash resources with exact
LLSD XML request shapes and `COPY` destination headers. Parsed objects,
side-effect removals, and folder versions are validated and reconciled through
one atomic store transaction; cancellation and malformed responses cannot
partially mutate the cache. Focused compatibility and injected-HTTP tests cover
link correction, endpoint recording, error isolation, and metadata updates.

View File

@@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand.
| Assembly | Types | Members | Status | | Assembly | Types | Members | Status |
|---|---:|---:|---| |---|---:|---:|---|
| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 164 types / 14,877 members; remaining surface is callable failure-only shims | | `LibreMetaverse` | 2,711 | 27,281 | native implementation: 166 types / 14,923 members; remaining surface is callable failure-only shims |
| `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain |
| `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain |
| `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim |

View File

@@ -699,6 +699,31 @@ impl HttpCapsClient {
.await .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( pub async fn get_request(
&self, &self,
uri: Uri, uri: Uri,
@@ -1026,6 +1051,28 @@ impl HttpCapsClient {
payload: Vec<u8>, payload: Vec<u8>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
progress: Option<Box<dyn IProgress<HttpCapsClientProgressReport>>>, 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> { ) -> Result<(HttpResponse, Vec<u8>), Error> {
if self.0.disposed.load(Ordering::Acquire) { if self.0.disposed.load(Ordering::Acquire) {
return Err(Error::InvalidOperation); return Err(Error::InvalidOperation);
@@ -1057,6 +1104,7 @@ impl HttpCapsClient {
handler, handler,
method, method,
uri, uri,
headers,
content_type, content_type,
payload, payload,
cancellation_token, cancellation_token,
@@ -1069,6 +1117,7 @@ impl HttpCapsClient {
client, client,
method, method,
parsed, parsed,
headers,
content_type, content_type,
payload, payload,
cancellation_token, cancellation_token,
@@ -1086,6 +1135,7 @@ impl HttpCapsClient {
handler: &HttpMessageHandler, handler: &HttpMessageHandler,
method: &str, method: &str,
uri: Uri, uri: Uri,
headers: std::collections::BTreeMap<String, String>,
content_type: Option<String>, content_type: Option<String>,
payload: Vec<u8>, payload: Vec<u8>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
@@ -1095,7 +1145,7 @@ impl HttpCapsClient {
let request = HttpRequest { let request = HttpRequest {
method: method.to_owned(), method: method.to_owned(),
uri, uri,
headers: std::collections::BTreeMap::new(), headers,
content_type, content_type,
body: payload, body: payload,
}; };
@@ -1112,6 +1162,7 @@ impl HttpCapsClient {
client: &reqwest::Client, client: &reqwest::Client,
method: &str, method: &str,
uri: reqwest::Url, uri: reqwest::Url,
headers: std::collections::BTreeMap<String, String>,
content_type: Option<String>, content_type: Option<String>,
payload: Vec<u8>, payload: Vec<u8>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
@@ -1119,6 +1170,9 @@ impl HttpCapsClient {
) -> Result<(HttpResponse, Vec<u8>), Error> { ) -> Result<(HttpResponse, Vec<u8>), Error> {
let method = reqwest::Method::from_bytes(method.as_bytes()).map_err(|_| Error::Argument)?; let method = reqwest::Method::from_bytes(method.as_bytes()).map_err(|_| Error::Argument)?;
let mut request = client.request(method, uri); let mut request = client.request(method, uri);
for (name, value) in headers {
request = request.header(name, value);
}
if let Some(content_type) = content_type { if let Some(content_type) = content_type {
request = request.header(reqwest::header::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>>, network_manager: Mutex<std::sync::Weak<crate::network_manager::NetworkManagerInner>>,
agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>, agent_manager: Mutex<std::sync::Weak<crate::agent_manager::AgentManagerInner>>,
inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>, inventory_manager: Mutex<Option<Arc<crate::inventory_manager::InventoryManagerInner>>>,
inventory_ais_client: Mutex<Option<crate::inventory_ais::InventoryAISClient>>,
shutdown_complete: Condvar, shutdown_complete: Condvar,
shutdown_wait: Mutex<()>, shutdown_wait: Mutex<()>,
} }
@@ -148,6 +149,7 @@ impl ClientRuntime {
network_manager: Mutex::new(std::sync::Weak::new()), network_manager: Mutex::new(std::sync::Weak::new()),
agent_manager: Mutex::new(std::sync::Weak::new()), agent_manager: Mutex::new(std::sync::Weak::new()),
inventory_manager: Mutex::new(None), inventory_manager: Mutex::new(None),
inventory_ais_client: Mutex::new(None),
shutdown_complete: Condvar::new(), shutdown_complete: Condvar::new(),
shutdown_wait: Mutex::new(()), shutdown_wait: Mutex::new(()),
} }
@@ -224,6 +226,10 @@ impl ClientRuntime {
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
.take(); .take();
self.inventory_ais_client
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
self.http_caps_client self.http_caps_client
.lock() .lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) .unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -441,6 +447,30 @@ impl GridClient {
Ok(manager) 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. #[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) { pub(crate) fn native_set_inventory(&mut self, value: crate::InventoryManager) {
*self *self

View File

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

View File

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

View File

@@ -69,3 +69,27 @@ snapshot subscribers before invoking them. Focused coverage lives in
`inventory_manager_semantics`, `task_inventory_semantics`, and the native `inventory_manager_semantics`, `task_inventory_semantics`, and the native
`inventory_manager::tests` fake-packet cases for callback isolation, bulk-copy `inventory_manager::tests` fake-packet cases for callback isolation, bulk-copy
completion, stale folder versions, offers, capability events, and bounded Xfer. completion, stale folder versions, offers, capability events, and bounded Xfer.
## Inventory API v3
`InventoryAISClient` implements the `InventoryAPIv3` and `LibraryAPIv3`
capability resources for categories, children, links, individual items, the
current outfit folder, orphans, and trash. Requests preserve the AIS verbs and
resource shapes, including `COPY` with its `Destination` header, transaction
IDs, depth/subset queries, and LLSD XML request content type. The client uses
the injected capability HTTP transport, so tests and applications can record
requests deterministically without replacing inventory logic.
Successful response bodies are fully decoded and validated before any local
state changes. Embedded folders, concrete items, and links are parsed together;
object or mesh links with the legacy texture inventory type are corrected to
attachments. Side-effect removals and category version updates are combined
with returned objects and installed atomically through a detached candidate
inventory graph. Malformed, oversized, cancelled, or timed-out responses leave
the live store unchanged, and no store lock crosses an await or callback.
AIS remains capability-optional. Missing capabilities and HTTP failures return
the fixed false/empty compatibility results, while cancellation is propagated.
The existing `InventoryManager` UDP fallback remains the contract-correct path
for operations whose capability is absent; AIS reconciliation shares that
manager's transport-independent store and observer ordering.

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""Audit issue 63's native AISv3 capability and reconciliation boundary."""
from __future__ import annotations
from pathlib import Path
import re
import generate_api_shims
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "crates" / "libremetaverse" / "src" / "inventory_ais.rs"
HTTP = ROOT / "crates" / "libremetaverse" / "src" / "caps_http.rs"
INVENTORY = ROOT / "crates" / "libremetaverse" / "src" / "inventory.rs"
NATIVE_TESTS = ROOT / "crates" / "libremetaverse" / "src" / "inventory_ais_internal_semantics.rs"
COMPAT_TESTS = ROOT / "tests" / "compat" / "tests" / "inventory_ais_semantics.rs"
DOC = ROOT / "docs" / "inventory.md"
MAPPING = ROOT / "api" / "RUST-MAPPING.tsv"
STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(")
def require_markers(path: Path, markers: tuple[str, ...]) -> None:
text = path.read_text()
missing = [marker for marker in markers if marker not in text]
if missing:
raise SystemExit(f"{path.name}: audit evidence missing: " + ", ".join(missing))
def main() -> None:
native_types = generate_api_shims.NATIVE_TYPES | generate_api_shims.NATIVE_DECLARATIONS
for type_id in ("T:LibreMetaverse.AISResponseMeta", "T:LibreMetaverse.InventoryAISClient"):
if type_id not in native_types:
raise SystemExit(f"issue 63 native type missing: {type_id}")
mapped = generate_api_shims.NATIVE_MEMBER_BODIES
required = set()
for row in MAPPING.read_text().splitlines()[1:]:
doc_id = row.split("\t", 1)[0]
if doc_id.startswith(("M:LibreMetaverse.InventoryAISClient.", "P:LibreMetaverse.InventoryAISClient.")):
required.add(doc_id)
missing = sorted(required - mapped.keys())
if missing:
raise SystemExit("issue 63 native members missing: " + ", ".join(missing))
for member in (
"P:LibreMetaverse.GridClient.AisClient",
"P:LibreMetaverse.GridClient.AisClient#set",
):
if member not in mapped:
raise SystemExit(f"issue 63 GridClient member missing: {member}")
if STUB_RE.search(SOURCE.read_text()):
raise SystemExit("issue 63 owned Rust stubs remain in inventory_ais.rs")
require_markers(
SOURCE,
(
'"COPY"',
'"Destination"',
'"application/llsd+xml"',
"MAX_FOLDER_DEPTH_REQUEST",
"MAX_AIS_OBJECTS",
"Error::Cancelled",
"parse_meta_map",
"native_reconcile_ais",
),
)
require_markers(HTTP, ("send_custom", "send_with_headers"))
require_markers(INVENTORY, ("native_apply_ais_batch", "detached candidate"))
require_markers(
NATIVE_TESTS,
(
"records_exact_ais_verbs_paths_headers_and_llsd_payloads",
"malformed_and_cancelled_responses_leave_store_unchanged",
"partially_valid_response_is_rejected_before_any_reconciliation",
"response_metadata_reconciles_removals_and_folder_versions_together",
),
)
require_markers(
COMPAT_TESTS,
(
"parse_links_object_asset_results_in_attachment_type_and_parses_fields",
"parse_embedded_combines_items_and_links",
"move_category_links_returns_completed_future",
),
)
require_markers(
DOC,
(
"Inventory API v3",
"InventoryAPIv3",
"LibraryAPIv3",
"Destination",
"atomically",
"UDP fallback",
),
)
print(
"issue 63 audit: AIS endpoints, LLSD parsing, COPY headers, bounded atomic "
"reconciliation, documentation, and focused tests are present"
)
if __name__ == "__main__":
main()

View File

@@ -38,6 +38,7 @@ TARGETS = {
# implementations. The generated module keeps catalog markers and re-exports # implementations. The generated module keeps catalog markers and re-exports
# the hand-written type so coverage remains deterministic. # the hand-written type so coverage remains deterministic.
NATIVE_TYPES = { NATIVE_TYPES = {
"T:LibreMetaverse.AISResponseMeta": "crate::inventory_ais::AISResponseMeta",
"T:LibreMetaverse.AccountLevelBenefits": "crate::login::AccountLevelBenefits", "T:LibreMetaverse.AccountLevelBenefits": "crate::login::AccountLevelBenefits",
"T:LibreMetaverse.AgentDisplayName": "crate::message_decoder::AgentDisplayName", "T:LibreMetaverse.AgentDisplayName": "crate::message_decoder::AgentDisplayName",
"T:LibreMetaverse.AgentDataReplyEventArgs": "crate::agent_messages::AgentDataReplyEventArgs", "T:LibreMetaverse.AgentDataReplyEventArgs": "crate::agent_messages::AgentDataReplyEventArgs",
@@ -262,6 +263,7 @@ NATIVE_DECLARATIONS = {
"T:LibreMetaverse.AgentManager": "crate::agent_manager::AgentManager", "T:LibreMetaverse.AgentManager": "crate::agent_manager::AgentManager",
"T:LibreMetaverse.GridClient": "crate::client_core::GridClient", "T:LibreMetaverse.GridClient": "crate::client_core::GridClient",
"T:LibreMetaverse.InventoryManager": "crate::inventory_manager::InventoryManager", "T:LibreMetaverse.InventoryManager": "crate::inventory_manager::InventoryManager",
"T:LibreMetaverse.InventoryAISClient": "crate::inventory_ais::InventoryAISClient",
"T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager", "T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager",
"T:LibreMetaverse.Settings": "crate::client_core::Settings", "T:LibreMetaverse.Settings": "crate::client_core::Settings",
"T:LibreMetaverse.Simulator": "crate::network_manager::Simulator", "T:LibreMetaverse.Simulator": "crate::network_manager::Simulator",
@@ -337,6 +339,10 @@ NATIVE_MEMBER_BODIES = {
"self.native_inventory().unwrap_or_else(|_| panic!(\"failed to construct InventoryManager\"))", "self.native_inventory().unwrap_or_else(|_| panic!(\"failed to construct InventoryManager\"))",
"P:LibreMetaverse.GridClient.Inventory#set": "P:LibreMetaverse.GridClient.Inventory#set":
"self.native_set_inventory(value)", "self.native_set_inventory(value)",
"P:LibreMetaverse.GridClient.AisClient":
"self.native_ais_client().unwrap_or_else(|_| panic!(\"failed to construct InventoryAISClient\"))",
"P:LibreMetaverse.GridClient.AisClient#set":
"self.native_set_ais_client(value)",
"P:LibreMetaverse.GridClient.Network": "P:LibreMetaverse.GridClient.Network":
"self.native_network().unwrap_or_else(|_| panic!(\"failed to construct NetworkManager\"))", "self.native_network().unwrap_or_else(|_| panic!(\"failed to construct NetworkManager\"))",
"P:LibreMetaverse.GridClient.Network#set": "P:LibreMetaverse.GridClient.Network#set":
@@ -363,6 +369,81 @@ NATIVE_MEMBER_BODIES = {
"crate::inventory_manager::InventoryManager::native_magic_id()", "crate::inventory_manager::InventoryManager::native_magic_id()",
"M:LibreMetaverse.InventoryManager.#ctor(LibreMetaverse.GridClient)": "M:LibreMetaverse.InventoryManager.#ctor(LibreMetaverse.GridClient)":
"crate::inventory_manager::InventoryManager::native_new(client)", "crate::inventory_manager::InventoryManager::native_new(client)",
"F:LibreMetaverse.InventoryAISClient.INVENTORY_CAP_NAME":
"crate::inventory_ais::InventoryAISClient::INVENTORY_CAP_NAME",
"F:LibreMetaverse.InventoryAISClient.LIBRARY_CAP_NAME":
"crate::inventory_ais::InventoryAISClient::LIBRARY_CAP_NAME",
"M:LibreMetaverse.InventoryAISClient.#ctor(LibreMetaverse.GridClient)":
"crate::inventory_ais::InventoryAISClient::native_new(client)",
"P:LibreMetaverse.InventoryAISClient.IsAvailable": "self.native_is_available()",
"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",
"M:LibreMetaverse.InventoryAISClient.SlamFolderAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)":
"self.native_slam_folder(folder_uuid, new_inventory, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.RemoveCategoryAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_remove_category(category_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.RemoveItemAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_remove_item(item_uuid, cancellation_token).await",
"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",
"M:LibreMetaverse.InventoryAISClient.PurgeDescendentsAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_purge_descendents(category_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.UpdateCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)":
"self.native_update_category(category_uuid, updates, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.UpdateItemAsync(LibreMetaverse.UUID,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)":
"self.native_update_item(item_uuid, updates, cancellation_token).await",
"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",
"M:LibreMetaverse.InventoryAISClient.MoveCategoryAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_move_category(source_uuid, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.PutCategoryChildrenAsync(System.String,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)":
"self.native_put_category_children(category, children_payload, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.CopyCategoryChildrenAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_copy_category_children(category, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.MoveCategoryChildrenAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_move_category_children(category, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.DeleteCategoryChildrenAsync(System.String,System.Threading.CancellationToken)":
"self.native_delete_category_children(category, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.PutCategoryLinksAsync(System.String,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken)":
"self.native_put_category_links(category, links_payload, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.CopyCategoryLinksAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_copy_category_links(category, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.MoveCategoryLinksAsync(System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_move_category_links(category, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.DeleteCategoryLinksAsync(System.String,System.Threading.CancellationToken)":
"self.native_delete_category_links(category, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.CopyItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_copy_item(item_uuid, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.MoveItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_move_item(item_uuid, dest_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.GetCategoryAsync(System.String,System.Threading.CancellationToken)":
"self.native_get_category(category, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.GetCategoryChildrenAsync(System.String,System.Int32,System.Boolean,System.Threading.CancellationToken)":
"self.native_get_category_children(category, depth, recursive, cancellation_token).await",
"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",
"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",
"M:LibreMetaverse.InventoryAISClient.GetCategoryLinksAsync(System.String,System.Threading.CancellationToken)":
"self.native_get_category_links(category, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.FetchItemAsync(LibreMetaverse.UUID,System.Threading.CancellationToken)":
"self.native_fetch_item(item_uuid, cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.FetchCOFAsync(System.Threading.CancellationToken)":
"self.native_fetch_cof(cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.FetchOrphansAsync(System.Threading.CancellationToken)":
"self.native_fetch_orphans(cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.EmptyTrashAsync(System.Threading.CancellationToken)":
"self.native_empty_trash(cancellation_token).await",
"M:LibreMetaverse.InventoryAISClient.ParseItemsFromEmbedded(LibreMetaverse.StructuredData.OSDMap)":
"self.native_parse_items_from_embedded(response)",
"M:LibreMetaverse.InventoryAISClient.ParseFoldersFromEmbedded(LibreMetaverse.StructuredData.OSDMap)":
"self.native_parse_folders_from_embedded(response)",
"M:LibreMetaverse.InventoryAISClient.ParseLinksFromEmbedded(LibreMetaverse.StructuredData.OSDMap)":
"self.native_parse_links_from_embedded(response)",
"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)",
"M:LibreMetaverse.InventoryAISClient.ParseAISResponseMeta(LibreMetaverse.StructuredData.OSDMap)":
"crate::inventory_ais::InventoryAISClient::native_parse_ais_response_meta(response)",
"M:LibreMetaverse.InventoryManager.CreateInventoryItem(LibreMetaverse.InventoryType,LibreMetaverse.UUID)": "M:LibreMetaverse.InventoryManager.CreateInventoryItem(LibreMetaverse.InventoryType,LibreMetaverse.UUID)":
"crate::inventory_manager::InventoryManager::native_create_inventory_item(type_, id)", "crate::inventory_manager::InventoryManager::native_create_inventory_item(type_, id)",
"M:LibreMetaverse.InventoryManager.CopyItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)": "M:LibreMetaverse.InventoryManager.CopyItemAsync(LibreMetaverse.UUID,LibreMetaverse.UUID,System.String,LibreMetaverse.UUID,System.Threading.CancellationToken)":

View File

@@ -994,6 +994,7 @@ def validate_generated_shims() -> None:
"crate::gesture::", "crate::gesture::",
"crate::json_codec::", "crate::json_codec::",
"crate::inventory::", "crate::inventory::",
"crate::inventory_ais::",
"crate::inventory_manager::", "crate::inventory_manager::",
"crate::message_codec::", "crate::message_codec::",
"crate::message_decoder::", "crate::message_decoder::",