diff --git a/.gitea/workflows/rust-workspace.yml b/.gitea/workflows/rust-workspace.yml index 2967de2..40c31c3 100644 --- a/.gitea/workflows/rust-workspace.yml +++ b/.gitea/workflows/rust-workspace.yml @@ -11,6 +11,7 @@ on: - "tools/check_milestone_10_issue_77.py" - "tools/check_milestone_10_issue_78.py" - "tools/check_milestone_10_issue_79.py" + - "tools/check_milestone_10_issue_80.py" - "**/*.rs" - "**/Cargo.toml" - "Cargo.lock" @@ -24,6 +25,7 @@ on: - "tools/check_milestone_10_issue_77.py" - "tools/check_milestone_10_issue_78.py" - "tools/check_milestone_10_issue_79.py" + - "tools/check_milestone_10_issue_80.py" - "**/*.rs" - "**/Cargo.toml" - "Cargo.lock" @@ -63,6 +65,7 @@ jobs: python3 tools/check_milestone_10_issue_77.py python3 tools/check_milestone_10_issue_78.py python3 tools/check_milestone_10_issue_79.py + python3 tools/check_milestone_10_issue_80.py - name: Test the complete native world milestone run: python3 tools/test_milestone_09.py - name: Compile every workspace target with bounded memory diff --git a/README.md b/README.md index 3f8a40d..0547ba7 100644 --- a/README.md +++ b/README.md @@ -466,14 +466,19 @@ format, ownership, error, and tangent-output contracts are documented in the The native RLV layer parses bounded chat messages into typed clear, action, restriction, and query directives, then provides independent thread-safe restriction, inventory-map, folder-lock, camera, blacklist, and permission -providers without network I/O. It +providers without network I/O. Its callback-driven service dispatches typed +actions and queries through `Send + Sync` host adapters, integrates completed +inventory and agent services through their public boundaries, and reports +inventory, attachment, outfit, chat, sit, and object-source lifecycle events. +It preserves source casing, folder paths, separators, numeric channels, UUIDs, and attachment/wearable aliases while exposing canonical behavior names and source-located typed errors. All 119 restriction names from the pinned LibreMetaverse snapshot are covered, and deterministic malformed-input mutations exercise the parser without an external fuzzing runtime. Immutable snapshots and post-lock update events make concurrent policy reads -deterministic; callback execution and transport effects remain explicit -consumers of the layer. The grammar, state precedence, normalization rules, -limits, and ownership boundary are documented in the +deterministic; cancellation and resource limits are checked before callbacks, +and no callback runs under an internal lock. The grammar, state precedence, +normalization rules, service orchestration, limits, and ownership boundary are +documented in the [`RLV protocol guide`](crates/libremetaverse-rlv/README.md). diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index fa89c46..a33fa5e 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -9,7 +9,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | | `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims | -| `LibreMetaverse.RLV` | 28 | 499 | native implementation: 12 types / 147 members; remaining surface is callable failure-only shims | +| `LibreMetaverse.RLV` | 28 | 499 | native implementation: 17 types / 200 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | native implementation: 1 type / 14 members; no generated shims remain | | `LibreMetaverse.Rendering.Simple` | 1 | 6 | native implementation: 1 type / 6 members; no generated shims remain | | `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 16 types / 295 members; no generated shims remain | diff --git a/crates/libremetaverse-rlv/README.md b/crates/libremetaverse-rlv/README.md index c9d6e02..2fdd83d 100644 --- a/crates/libremetaverse-rlv/README.md +++ b/crates/libremetaverse-rlv/README.md @@ -3,11 +3,10 @@ `libremetaverse-rlv` implements the protocol and pure state boundary for the Restrained Love Viewer support in the pinned LibreMetaverse snapshot. The side-effect-free parser converts one bounded chat message into typed commands. -Independent, thread-safe managers then store restrictions and evaluate -inventory, folder locks, camera limits, blacklists, and permissions without -performing network I/O. Command execution, viewer callbacks, replies, and -`GridClient` integration remain separate milestone work that consumes this -layer. +Independent, thread-safe managers store restrictions and evaluate inventory, +folder locks, camera limits, blacklists, and permissions without performing +network I/O. The callback-driven service then executes those typed decisions +through explicit host adapters. ## Message contract @@ -83,8 +82,39 @@ target rules, permissive exception precedence, IM/chat/channel behavior, teleport limits, edit/touch/hover decisions, shared and unshared wear, and folder attachment locks. Camera aggregation applies the reference min/max, clamping, averaging, alias, texture, and lock rules. The case-insensitive -blacklist returns a sorted snapshot. These providers are deterministic, -thread-safe, and contain no network or callback dependencies. +blacklist returns a sorted snapshot. These providers are deterministic and +thread-safe with no network access. Object-source cleanup snapshots the +affected restrictions and notification listeners, then invokes the injected +action callback after releasing every manager lock. + +## Callback and service contract + +`RlvService` owns bounded chat and instant-message dispatch, typed action and +get-request routing, restriction lifecycle notifications, and inventory, +attachment, outfit, public-message, sit, and unsit reports. A chat message is +rejected before dispatch when it exceeds the parser's 64 KiB or 128-command +limits. Multi-command processing retains source order and continues after a +well-formed command returns `false`, matching the reference aggregate result. +Blacklist replies, query channels, version strings, restriction messages, and +case-sensitive general notification filters preserve the pinned behavior. + +Hosts implement the `IRlvQueryCallbacks` and `IRlvActionCallbacks` `Send + Sync` +traits. Query adapters expose snapshots from the completed inventory and agent +services; action adapters perform attachment, outfit, camera, group, movement, +teleport, chat, and reply operations through those services instead of +reaching into their state. The safe default adapters reproduce the reference +query values and no-op actions while still observing cancellation. Every host +future is awaited only after service and restriction-manager locks have been +released, so recording callbacks may synchronously inspect service state. + +The command processor refreshes `InventoryMap` through the query boundary and +publishes the same snapshot to `RlvPermissionsService` before folder or item +decisions. Attachment requests are value objects with a stable cross-platform +hash. Folder walks preserve deterministic order, skip private and hidden +content where required, deduplicate linked items, honor `nostrip`, body-part, +link-exception, and recursive-lock rules, and never manufacture an action for +an invalid target. Cancellation is checked before processing, reports, +inventory refreshes, each multi-command element, and every callback. ## Reproducible verification @@ -94,11 +124,16 @@ Run the issue-owned checks with one build job: CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-rlv --locked CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_common_semantics --locked CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_inventory_map_semantics --locked +CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_query_basics_semantics --locked +CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_attach_commands_semantics --locked +CARGO_BUILD_JOBS=1 cargo test -p libremetaverse-compat-tests --test rlv_notify_semantics --locked CARGO_BUILD_JOBS=1 cargo clippy -p libremetaverse-rlv --all-targets --locked -- -D warnings RUSTDOCFLAGS='-D warnings' CARGO_BUILD_JOBS=1 cargo doc -p libremetaverse-rlv --no-deps --locked python3 tools/check_milestone_10_issue_78.py python3 tools/check_milestone_10_issue_79.py +python3 tools/check_milestone_10_issue_80.py ``` -These commands are cross-platform. The Gitea workflow runs the audit and -workspace compile on `ubuntu-latest`. +The complete issue-owned compatibility slice contains 616 translated RLV +cases across 24 test binaries. These commands are cross-platform. The Gitea +workflow runs the audit and workspace compile on `ubuntu-latest`. diff --git a/crates/libremetaverse-rlv/src/generated.rs b/crates/libremetaverse-rlv/src/generated.rs index bda600c..d50c595 100644 --- a/crates/libremetaverse-rlv/src/generated.rs +++ b/crates/libremetaverse-rlv/src/generated.rs @@ -6,47 +6,13 @@ #![allow(non_snake_case)] /// C# type: `T:LibreMetaverse.RLV.AttachmentRequest`. -pub struct AttachmentRequest; -impl AttachmentRequest { - /// C# member: `M:LibreMetaverse.RLV.AttachmentRequest.#ctor(System.Guid,LibreMetaverse.RLV.RlvAttachmentPoint,System.Boolean)`. - pub fn new( - item_id: libremetaverse_types::compat::Guid, - attachment_point: libremetaverse_rlv::RlvAttachmentPoint, - replace_existing_attachments: bool, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.AttachmentRequest.#ctor(System.Guid,LibreMetaverse.RLV.RlvAttachmentPoint,System.Boolean)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.AttachmentRequest.Equals(System.Object)`. - pub fn equals(&self, obj: Option) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.AttachmentRequest.Equals(System.Object)" - ) - } - /// C# member: `M:LibreMetaverse.RLV.AttachmentRequest.GetHashCode`. - pub fn get_hash_code(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.RLV.AttachmentRequest.GetHashCode" - ) - } - /// C# member: `P:LibreMetaverse.RLV.AttachmentRequest.AttachmentPoint`. - pub fn attachment_point(&self) -> libremetaverse_rlv::RlvAttachmentPoint { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.AttachmentRequest.AttachmentPoint" - ) - } - /// C# member: `P:LibreMetaverse.RLV.AttachmentRequest.ItemId`. - pub fn item_id(&self) -> libremetaverse_types::compat::Guid { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.AttachmentRequest.ItemId") - } - /// C# member: `P:LibreMetaverse.RLV.AttachmentRequest.ReplaceExistingAttachments`. - pub fn replace_existing_attachments(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.AttachmentRequest.ReplaceExistingAttachments" - ) - } -} +/// C# member: `M:LibreMetaverse.RLV.AttachmentRequest.#ctor(System.Guid,LibreMetaverse.RLV.RlvAttachmentPoint,System.Boolean)`. +/// C# member: `M:LibreMetaverse.RLV.AttachmentRequest.Equals(System.Object)`. +/// C# member: `M:LibreMetaverse.RLV.AttachmentRequest.GetHashCode`. +/// C# member: `P:LibreMetaverse.RLV.AttachmentRequest.AttachmentPoint`. +/// C# member: `P:LibreMetaverse.RLV.AttachmentRequest.ItemId`. +/// C# member: `P:LibreMetaverse.RLV.AttachmentRequest.ReplaceExistingAttachments`. +pub use crate::service::AttachmentRequest; /// C# type: `T:LibreMetaverse.RLV.CameraRestrictions`. pub use crate::state::CameraRestrictions; @@ -157,7 +123,7 @@ impl CameraSettings { } /// C# type: `T:LibreMetaverse.RLV.IRlvActionCallbacks`. -pub trait IRlvActionCallbacks { +pub trait IRlvActionCallbacks: Send + Sync { /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.AdjustHeightAsync(System.Single,System.Single,System.Single,System.Threading.CancellationToken)`. fn adjust_height( &self, @@ -379,7 +345,7 @@ pub trait IRlvActionCallbacks { } /// C# type: `T:LibreMetaverse.RLV.IRlvQueryCallbacks`. -pub trait IRlvQueryCallbacks { +pub trait IRlvQueryCallbacks: Send + Sync { /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.IsSittingAsync(System.Threading.CancellationToken)`. fn is_sitting( &self, @@ -654,186 +620,24 @@ impl LockedFolderPublic { } /// C# type: `T:LibreMetaverse.RLV.RlvActionCallbacksDefault`. -pub struct RlvActionCallbacksDefault; -impl RlvActionCallbacksDefault { - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.#ctor", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.AdjustHeightAsync(System.Single,System.Single,System.Single,System.Threading.CancellationToken)`. - pub async fn adjust_height( - &self, - distance: f32, - factor: f32, - delta: f32, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.AdjustHeightAsync(System.Single,System.Single,System.Single,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.AttachAsync(System.Collections.Generic.IReadOnlyList{LibreMetaverse.RLV.AttachmentRequest},System.Threading.CancellationToken)`. - pub async fn attach( - &self, - items_to_attach: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.AttachAsync(System.Collections.Generic.IReadOnlyList{LibreMetaverse.RLV.AttachmentRequest},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.DetachAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)`. - pub async fn detach( - &self, - item_ids: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.DetachAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.RemOutfitAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)`. - pub async fn rem_outfit( - &self, - item_ids: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.RemOutfitAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SendInstantMessageAsync(System.Guid,System.String,System.Threading.CancellationToken)`. - pub async fn send_instant_message( - &self, - target_user: libremetaverse_types::compat::Guid, - message: String, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SendInstantMessageAsync(System.Guid,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SendReplyAsync(System.Int32,System.String,System.Threading.CancellationToken)`. - pub async fn send_reply( - &self, - channel: i32, - message: String, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SendReplyAsync(System.Int32,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetCamFOVAsync(System.Single,System.Threading.CancellationToken)`. - pub async fn set_cam_fov( - &self, - fov_in_radians: f32, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetCamFOVAsync(System.Single,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetDebugAsync(System.String,System.String,System.Threading.CancellationToken)`. - pub async fn set_debug( - &self, - setting_name: String, - setting_value: String, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetDebugAsync(System.String,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetEnvAsync(System.String,System.String,System.Threading.CancellationToken)`. - pub async fn set_env( - &self, - setting_name: String, - setting_value: String, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetEnvAsync(System.String,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetGroupAsync(System.Guid,System.String,System.Threading.CancellationToken)`. - pub async fn set_group_with_guid_string_cancellation_token( - &self, - group_id: libremetaverse_types::compat::Guid, - role_name: Option, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetGroupAsync(System.Guid,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetGroupAsync(System.String,System.String,System.Threading.CancellationToken)`. - pub async fn set_group_with_string_string_cancellation_token( - &self, - group_name: String, - role_name: Option, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetGroupAsync(System.String,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetRotAsync(System.Single,System.Threading.CancellationToken)`. - pub async fn set_rot( - &self, - angle_in_radians: f32, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetRotAsync(System.Single,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SitAsync(System.Guid,System.Threading.CancellationToken)`. - pub async fn sit( - &self, - target: libremetaverse_types::compat::Guid, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SitAsync(System.Guid,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SitGroundAsync(System.Threading.CancellationToken)`. - pub async fn sit_ground( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SitGroundAsync(System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.TpToAsync(System.Single,System.Single,System.Single,System.String,System.Nullable{System.Single},System.Threading.CancellationToken)`. - pub async fn tp_to( - &self, - x: f32, - y: f32, - z: f32, - region_name: Option, - lookat: Option>, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.TpToAsync(System.Single,System.Single,System.Single,System.String,System.Nullable{System.Single},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.UnsitAsync(System.Threading.CancellationToken)`. - pub async fn unsit( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.UnsitAsync(System.Threading.CancellationToken)", - ) - } -} -impl libremetaverse_rlv::IRlvActionCallbacks for RlvActionCallbacksDefault {} +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.#ctor`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.AdjustHeightAsync(System.Single,System.Single,System.Single,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.AttachAsync(System.Collections.Generic.IReadOnlyList{LibreMetaverse.RLV.AttachmentRequest},System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.DetachAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.RemOutfitAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SendInstantMessageAsync(System.Guid,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SendReplyAsync(System.Int32,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetCamFOVAsync(System.Single,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetDebugAsync(System.String,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetEnvAsync(System.String,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetGroupAsync(System.Guid,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetGroupAsync(System.String,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SetRotAsync(System.Single,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SitAsync(System.Guid,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.SitGroundAsync(System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.TpToAsync(System.Single,System.Single,System.Single,System.String,System.Nullable{System.Single},System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvActionCallbacksDefault.UnsitAsync(System.Threading.CancellationToken)`. +pub use crate::service::RlvActionCallbacksDefault; /// C# type: `T:LibreMetaverse.RLV.RlvAttachmentPoint`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -975,102 +779,20 @@ impl RlvBlacklist { } /// C# type: `T:LibreMetaverse.RLV.RlvCallbacksDefault`. -pub struct RlvCallbacksDefault; -impl RlvCallbacksDefault { - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.RLV.RlvCallbacksDefault.#ctor") - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.IsSittingAsync(System.Threading.CancellationToken)`. - pub async fn is_sitting( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.IsSittingAsync(System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.ObjectExistsAsync(System.Guid,System.Threading.CancellationToken)`. - pub async fn object_exists( - &self, - object_id: libremetaverse_types::compat::Guid, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.ObjectExistsAsync(System.Guid,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetActiveGroupNameAsync(System.Threading.CancellationToken)`. - pub async fn try_get_active_group_name( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, String), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetActiveGroupNameAsync(System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetCameraSettingsAsync(System.Threading.CancellationToken)`. - pub async fn try_get_camera_settings( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, Option), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetCameraSettingsAsync(System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetDebugSettingValueAsync(System.String,System.Threading.CancellationToken)`. - pub async fn try_get_debug_setting_value( - &self, - setting_name: String, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, String), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetDebugSettingValueAsync(System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetEnvironmentSettingValueAsync(System.String,System.Threading.CancellationToken)`. - pub async fn try_get_environment_setting_value( - &self, - setting_name: String, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, String), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetEnvironmentSettingValueAsync(System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetInventoryMapAsync(System.Threading.CancellationToken)`. - pub async fn try_get_inventory_map( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, Option), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetInventoryMapAsync(System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetSharedFolderAsync(System.Threading.CancellationToken)`. - pub async fn try_get_shared_folder( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, Option), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetSharedFolderAsync(System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetSitIdAsync(System.Threading.CancellationToken)`. - pub async fn try_get_sit_id( - &self, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(bool, libremetaverse_types::compat::Guid), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetSitIdAsync(System.Threading.CancellationToken)", - ) - } -} -impl libremetaverse_rlv::IRlvQueryCallbacks for RlvCallbacksDefault {} +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.#ctor`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.IsSittingAsync(System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.ObjectExistsAsync(System.Guid,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetActiveGroupNameAsync(System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetCameraSettingsAsync(System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetDebugSettingValueAsync(System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetEnvironmentSettingValueAsync(System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetInventoryMapAsync(System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetSharedFolderAsync(System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvCallbacksDefault.TryGetSitIdAsync(System.Threading.CancellationToken)`. +pub use crate::service::RlvCallbacksDefault; /// C# type: `T:LibreMetaverse.RLV.RlvCommandProcessor`. -pub struct RlvCommandProcessor; -impl RlvCommandProcessor {} +pub use crate::service::RlvCommandProcessor; /// C# type: `T:LibreMetaverse.RLV.RlvCommon`. pub use crate::protocol::RlvCommon; @@ -2066,183 +1788,27 @@ pub enum RlvRestrictionType { } /// C# type: `T:LibreMetaverse.RLV.RlvService`. -pub struct RlvService; -impl RlvService { - /// C# member: `F:LibreMetaverse.RLV.RlvService.RLVVersion`. - pub const RLV_VERSION: &'static str = "RestrainedLove viewer v3.4.3 (RLVa 2.4.2)"; - /// C# member: `F:LibreMetaverse.RLV.RlvService.RLVVersionNum`. - pub const RLV_VERSION_NUM: &'static str = "2040213"; - /// C# member: `M:LibreMetaverse.RLV.RlvService.#ctor(LibreMetaverse.RLV.IRlvQueryCallbacks,LibreMetaverse.RLV.IRlvActionCallbacks,System.Boolean)`. - pub fn new( - callbacks: Box, - action_callbacks: Box, - enabled: bool, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.#ctor(LibreMetaverse.RLV.IRlvQueryCallbacks,LibreMetaverse.RLV.IRlvActionCallbacks,System.Boolean)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ProcessInstantMessageAsync(System.String,System.Guid,System.Threading.CancellationToken)`. - pub async fn process_instant_message( - &self, - message: String, - sender_id: libremetaverse_types::compat::Guid, - cancellation_token: Option, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ProcessInstantMessageAsync(System.String,System.Guid,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ProcessMessageAsync(System.String,System.Guid,System.String,System.Threading.CancellationToken)`. - pub async fn process_message( - &self, - message: String, - sender_id: libremetaverse_types::compat::Guid, - sender_name: String, - cancellation_token: Option, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ProcessMessageAsync(System.String,System.Guid,System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportInventoryOfferAcceptedAsync(System.String,System.Threading.CancellationToken)`. - pub async fn report_inventory_offer_accepted( - &self, - folder_path: String, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportInventoryOfferAcceptedAsync(System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportInventoryOfferDeclinedAsync(System.String,System.Threading.CancellationToken)`. - pub async fn report_inventory_offer_declined( - &self, - folder_path: String, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportInventoryOfferDeclinedAsync(System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemAttachedAsync(System.Guid,System.Boolean,LibreMetaverse.RLV.RlvAttachmentPoint,System.Threading.CancellationToken)`. - pub async fn report_item_attached( - &self, - object_folder_id: libremetaverse_types::compat::Guid, - is_shared: bool, - attachment_point: libremetaverse_rlv::RlvAttachmentPoint, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportItemAttachedAsync(System.Guid,System.Boolean,LibreMetaverse.RLV.RlvAttachmentPoint,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemDetachedAsync(System.Guid,System.Guid,System.Guid,System.Boolean,LibreMetaverse.RLV.RlvAttachmentPoint,System.Threading.CancellationToken)`. - pub async fn report_item_detached( - &self, - item_id: libremetaverse_types::compat::Guid, - prim_id: libremetaverse_types::compat::Guid, - object_folder_id: libremetaverse_types::compat::Guid, - is_shared: bool, - attachment_point: libremetaverse_rlv::RlvAttachmentPoint, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportItemDetachedAsync(System.Guid,System.Guid,System.Guid,System.Boolean,LibreMetaverse.RLV.RlvAttachmentPoint,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemUnwornAsync(System.Guid,System.Guid,System.Boolean,LibreMetaverse.RLV.RlvWearableType,System.Threading.CancellationToken)`. - pub async fn report_item_unworn( - &self, - item_id: libremetaverse_types::compat::Guid, - object_folder_id: libremetaverse_types::compat::Guid, - is_shared: bool, - wearable_type: libremetaverse_rlv::RlvWearableType, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportItemUnwornAsync(System.Guid,System.Guid,System.Boolean,LibreMetaverse.RLV.RlvWearableType,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemWornAsync(System.Guid,System.Boolean,LibreMetaverse.RLV.RlvWearableType,System.Threading.CancellationToken)`. - pub async fn report_item_worn( - &self, - object_folder_id: libremetaverse_types::compat::Guid, - is_shared: bool, - wearable_type: libremetaverse_rlv::RlvWearableType, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportItemWornAsync(System.Guid,System.Boolean,LibreMetaverse.RLV.RlvWearableType,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportSendPublicMessageAsync(System.String,System.Threading.CancellationToken)`. - pub async fn report_send_public_message( - &self, - message: String, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportSendPublicMessageAsync(System.String,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportSitAsync(System.Nullable{System.Guid},System.Threading.CancellationToken)`. - pub async fn report_sit( - &self, - object_id: Option>, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportSitAsync(System.Nullable{System.Guid},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.RLV.RlvService.ReportUnsitAsync(System.Nullable{System.Guid},System.Threading.CancellationToken)`. - pub async fn report_unsit( - &self, - object_id: Option>, - cancellation_token: Option, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.RlvService.ReportUnsitAsync(System.Nullable{System.Guid},System.Threading.CancellationToken)", - ) - } - /// C# member: `P:LibreMetaverse.RLV.RlvService.Blacklist`. - pub fn blacklist(&self) -> libremetaverse_rlv::RlvBlacklist { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvService.Blacklist") - } - /// C# member: `P:LibreMetaverse.RLV.RlvService.Commands`. - pub fn commands(&self) -> libremetaverse_rlv::RlvCommandProcessor { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvService.Commands") - } - /// C# member: `P:LibreMetaverse.RLV.RlvService.EnableInstantMessageProcessing`. - pub fn enable_instant_message_processing(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.RlvService.EnableInstantMessageProcessing" - ) - } - /// Setter for C# member: `P:LibreMetaverse.RLV.RlvService.EnableInstantMessageProcessing`. - pub fn set_enable_instant_message_processing(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.RLV.RlvService.EnableInstantMessageProcessing" - ) - } - /// C# member: `P:LibreMetaverse.RLV.RlvService.Enabled`. - pub fn enabled(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvService.Enabled") - } - /// Setter for C# member: `P:LibreMetaverse.RLV.RlvService.Enabled`. - pub fn set_enabled(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvService.Enabled") - } - /// C# member: `P:LibreMetaverse.RLV.RlvService.Permissions`. - pub fn permissions(&self) -> libremetaverse_rlv::RlvPermissionsService { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvService.Permissions") - } - /// C# member: `P:LibreMetaverse.RLV.RlvService.Restrictions`. - pub fn restrictions(&self) -> libremetaverse_rlv::RlvRestrictionManager { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.RLV.RlvService.Restrictions") - } -} +/// C# member: `F:LibreMetaverse.RLV.RlvService.RLVVersion`. +/// C# member: `F:LibreMetaverse.RLV.RlvService.RLVVersionNum`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.#ctor(LibreMetaverse.RLV.IRlvQueryCallbacks,LibreMetaverse.RLV.IRlvActionCallbacks,System.Boolean)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ProcessInstantMessageAsync(System.String,System.Guid,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ProcessMessageAsync(System.String,System.Guid,System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportInventoryOfferAcceptedAsync(System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportInventoryOfferDeclinedAsync(System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemAttachedAsync(System.Guid,System.Boolean,LibreMetaverse.RLV.RlvAttachmentPoint,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemDetachedAsync(System.Guid,System.Guid,System.Guid,System.Boolean,LibreMetaverse.RLV.RlvAttachmentPoint,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemUnwornAsync(System.Guid,System.Guid,System.Boolean,LibreMetaverse.RLV.RlvWearableType,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportItemWornAsync(System.Guid,System.Boolean,LibreMetaverse.RLV.RlvWearableType,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportSendPublicMessageAsync(System.String,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportSitAsync(System.Nullable{System.Guid},System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.RLV.RlvService.ReportUnsitAsync(System.Nullable{System.Guid},System.Threading.CancellationToken)`. +/// C# member: `P:LibreMetaverse.RLV.RlvService.Blacklist`. +/// C# member: `P:LibreMetaverse.RLV.RlvService.Commands`. +/// C# member: `P:LibreMetaverse.RLV.RlvService.EnableInstantMessageProcessing`. +/// C# member: `P:LibreMetaverse.RLV.RlvService.Enabled`. +/// C# member: `P:LibreMetaverse.RLV.RlvService.Permissions`. +/// C# member: `P:LibreMetaverse.RLV.RlvService.Restrictions`. +pub use crate::service::RlvService; /// C# type: `T:LibreMetaverse.RLV.RlvSharedFolder`. pub use crate::state::RlvSharedFolder; diff --git a/crates/libremetaverse-rlv/src/lib.rs b/crates/libremetaverse-rlv/src/lib.rs index 4d7ae65..1f1c61a 100644 --- a/crates/libremetaverse-rlv/src/lib.rs +++ b/crates/libremetaverse-rlv/src/lib.rs @@ -4,6 +4,7 @@ extern crate self as libremetaverse_rlv; mod generated; mod protocol; +mod service; mod state; pub use generated::*; diff --git a/crates/libremetaverse-rlv/src/protocol.rs b/crates/libremetaverse-rlv/src/protocol.rs index 7077e62..35c6ff1 100644 --- a/crates/libremetaverse-rlv/src/protocol.rs +++ b/crates/libremetaverse-rlv/src/protocol.rs @@ -1325,15 +1325,48 @@ fn parse_i32(value: &str) -> Option { } fn parse_guid(value: &str) -> Result { - UUID::parse(value.to_owned()) - .map(|value| value.guid()) - .map_err(|_| RlvParseErrorKind::InvalidUuid) + let digits = value + .bytes() + .filter(|byte| *byte != b'-') + .collect::>(); + if digits.len() != 32 { + return Err(RlvParseErrorKind::InvalidUuid); + } + let mut bytes = [0u8; 16]; + for (index, pair) in digits.chunks_exact(2).enumerate() { + let digit = |byte| match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + }; + let high = digit(pair[0]).ok_or(RlvParseErrorKind::InvalidUuid)?; + let low = digit(pair[1]).ok_or(RlvParseErrorKind::InvalidUuid)?; + bytes[index] = high << 4 | low; + } + Ok(Guid(bytes)) } fn guid_to_string(value: Guid) -> String { - UUID::new_with_guid(value).map_or_else( - |_| "00000000-0000-0000-0000-000000000000".to_owned(), - |value| value.to_string(), + let bytes = value.0; + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15], ) } @@ -1341,7 +1374,9 @@ fn value_to_object(value: RlvValue) -> Object { match value { RlvValue::Integer(value) => Object::Integer(value), RlvValue::Real(value) => Object::Real(f64::from(value)), - RlvValue::Uuid(value) => UUID::new_with_guid(value).map_or(Object::Undefined, Object::UUID), + RlvValue::Uuid(value) => { + UUID::parse(guid_to_string(value)).map_or(Object::Undefined, Object::UUID) + } RlvValue::String(value) => Object::String(value), RlvValue::AttachmentPoint(value) => Object::Integer(value as i32), RlvValue::WearableType(value) => Object::Integer(value as i32), diff --git a/crates/libremetaverse-rlv/src/service.rs b/crates/libremetaverse-rlv/src/service.rs new file mode 100644 index 0000000..822be86 --- /dev/null +++ b/crates/libremetaverse-rlv/src/service.rs @@ -0,0 +1,2193 @@ +//! Callback-driven RLV command execution and service orchestration. + +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::needless_pass_by_value)] +#![allow(clippy::option_option)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::too_many_lines)] +// These public methods mirror Task-returning C# callbacks and intentionally +// retain an async Rust surface even when the conservative default is ready. +#![allow(clippy::unused_async)] + +use std::collections::{HashMap, HashSet}; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use libremetaverse_types::Error; +use libremetaverse_types::compat::{CancellationToken, Guid, Object}; + +use crate::{ + CameraSettings, IRlvActionCallbacks, IRlvQueryCallbacks, InventoryMap, MAX_RLV_COMMANDS, + MAX_RLV_MESSAGE_BYTES, RlvAction, RlvAttachmentPoint, RlvBlacklist, RlvCameraQuery, + RlvDirective, RlvGestureState, RlvGroupTarget, RlvInventoryItem, RlvParser, + RlvPermissionsService, RlvQuery, RlvRestriction, RlvRestrictionManager, + RlvRestrictionOperation, RlvRestrictionType, RlvSharedFolder, RlvTarget, RlvValue, + RlvWearableType, attachment_from_name, restriction_name, +}; + +type ActionFuture<'a> = Pin> + Send + 'a>>; +type QueryFuture<'a, T> = Pin> + Send + 'a>>; + +fn token(value: Option) -> CancellationToken { + value.unwrap_or_default() +} + +fn guid_text(value: Guid) -> String { + let bytes = value.0; + format!( + "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + bytes[0], + bytes[1], + bytes[2], + bytes[3], + bytes[4], + bytes[5], + bytes[6], + bytes[7], + bytes[8], + bytes[9], + bytes[10], + bytes[11], + bytes[12], + bytes[13], + bytes[14], + bytes[15], + ) +} + +/// Immutable request passed to the host attachment service. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct AttachmentRequest { + item_id: Guid, + attachment_point: RlvAttachmentPoint, + replace_existing_attachments: bool, +} + +impl AttachmentRequest { + pub fn new( + item_id: Guid, + attachment_point: RlvAttachmentPoint, + replace_existing_attachments: bool, + ) -> Result { + Ok(Self { + item_id, + attachment_point, + replace_existing_attachments, + }) + } + + pub fn equals(&self, obj: Option) -> bool { + obj.as_ref().and_then(Object::downcast_ref::) == Some(self) + } + + pub fn get_hash_code(&self) -> i32 { + let bytes = self + .item_id + .0 + .iter() + .copied() + .chain(attachment_name(self.attachment_point).bytes()) + .chain([u8::from(self.replace_existing_attachments)]); + bytes + .fold(2_166_136_261_u32, |hash, byte| { + (hash ^ u32::from(byte)).wrapping_mul(16_777_619) + }) + .cast_signed() + } + + pub const fn attachment_point(&self) -> RlvAttachmentPoint { + self.attachment_point + } + + pub const fn item_id(&self) -> Guid { + self.item_id + } + + pub const fn replace_existing_attachments(&self) -> bool { + self.replace_existing_attachments + } +} + +/// Safe no-op action adapter for hosts that only consume RLV state. +#[derive(Clone, Copy, Debug, Default)] +pub struct RlvActionCallbacksDefault; + +impl RlvActionCallbacksDefault { + pub const fn new() -> Result { + Ok(Self) + } + + pub async fn adjust_height( + &self, + _: f32, + _: f32, + _: f32, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn attach( + &self, + _: Vec, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn detach(&self, _: Vec, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn rem_outfit(&self, _: Vec, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn send_instant_message( + &self, + _: Guid, + _: String, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn send_reply( + &self, + _: i32, + _: String, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn set_cam_fov(&self, _: f32, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn set_debug( + &self, + _: String, + _: String, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn set_env( + &self, + _: String, + _: String, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn set_group_with_guid_string_cancellation_token( + &self, + _: Guid, + _: Option, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn set_group_with_string_string_cancellation_token( + &self, + _: String, + _: Option, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn set_rot(&self, _: f32, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn sit(&self, _: Guid, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn sit_ground(&self, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn tp_to( + &self, + _: f32, + _: f32, + _: f32, + _: Option, + _: Option>, + token: CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } + pub async fn unsit(&self, token: CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested() + } +} + +impl IRlvActionCallbacks for RlvActionCallbacksDefault { + fn adjust_height(&self, a: f32, b: f32, c: f32, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.adjust_height(a, b, c, t)) + } + fn attach(&self, a: Vec, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.attach(a, t)) + } + fn detach(&self, a: Vec, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.detach(a, t)) + } + fn rem_outfit(&self, a: Vec, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.rem_outfit(a, t)) + } + fn send_instant_message(&self, a: Guid, b: String, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.send_instant_message(a, b, t)) + } + fn send_reply(&self, a: i32, b: String, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.send_reply(a, b, t)) + } + fn set_cam_fov(&self, a: f32, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.set_cam_fov(a, t)) + } + fn set_debug(&self, a: String, b: String, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.set_debug(a, b, t)) + } + fn set_env(&self, a: String, b: String, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.set_env(a, b, t)) + } + fn set_group_with_guid_string_cancellation_token( + &self, + a: Guid, + b: Option, + t: CancellationToken, + ) -> ActionFuture<'_> { + Box::pin(self.set_group_with_guid_string_cancellation_token(a, b, t)) + } + fn set_group_with_string_string_cancellation_token( + &self, + a: String, + b: Option, + t: CancellationToken, + ) -> ActionFuture<'_> { + Box::pin(self.set_group_with_string_string_cancellation_token(a, b, t)) + } + fn set_rot(&self, a: f32, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.set_rot(a, t)) + } + fn sit(&self, a: Guid, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.sit(a, t)) + } + fn sit_ground(&self, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.sit_ground(t)) + } + fn tp_to( + &self, + x: f32, + y: f32, + z: f32, + region: Option, + look_at: Option>, + token: CancellationToken, + ) -> ActionFuture<'_> { + Box::pin(self.tp_to(x, y, z, region, look_at, token)) + } + fn unsit(&self, t: CancellationToken) -> ActionFuture<'_> { + Box::pin(self.unsit(t)) + } +} + +/// Conservative query adapter matching the pinned reference defaults. +#[derive(Clone, Copy, Debug, Default)] +pub struct RlvCallbacksDefault; + +impl RlvCallbacksDefault { + pub const fn new() -> Result { + Ok(Self) + } + + pub async fn is_sitting(&self, token: CancellationToken) -> Result { + token.throw_if_cancellation_requested()?; + Ok(false) + } + pub async fn object_exists(&self, _: Guid, token: CancellationToken) -> Result { + token.throw_if_cancellation_requested()?; + Ok(false) + } + pub async fn try_get_active_group_name( + &self, + token: CancellationToken, + ) -> Result<(bool, String), Error> { + token.throw_if_cancellation_requested()?; + Ok((false, "None".to_owned())) + } + pub async fn try_get_camera_settings( + &self, + token: CancellationToken, + ) -> Result<(bool, Option), Error> { + token.throw_if_cancellation_requested()?; + Ok((false, None)) + } + pub async fn try_get_debug_setting_value( + &self, + name: String, + token: CancellationToken, + ) -> Result<(bool, String), Error> { + token.throw_if_cancellation_requested()?; + let value = match name.to_ascii_lowercase().as_str() { + "avatarsex" | "restrainedloveforbidgivetorlv" | "windlightuseatmosshaders" => Some("0"), + "renderresolutiondivisor" | "restrainedlovenosetenv" => Some("1"), + _ => None, + }; + Ok(value.map_or_else(|| (false, String::new()), |v| (true, v.to_owned()))) + } + pub async fn try_get_environment_setting_value( + &self, + name: String, + token: CancellationToken, + ) -> Result<(bool, String), Error> { + token.throw_if_cancellation_requested()?; + let name = name.to_ascii_lowercase(); + let value = if matches!( + name.as_str(), + "ambient" + | "bluedensity" + | "bluehorizon" + | "cloudcolor" + | "cloud" + | "clouddetail" + | "sunmooncolor" + ) { + Some("0;0;0") + } else if name == "cloudscroll" { + Some("0;0") + } else if matches!(name.as_str(), "preset" | "asset") { + Some("") + } else if matches!(name.as_str(), "moonimage" | "sunimage" | "cloudimage") { + Some("00000000-0000-0000-0000-000000000000") + } else if name == "sunglowsize" { + Some("1") + } else if ENV_ZERO_SETTINGS.contains(&name.as_str()) { + Some("0") + } else { + None + }; + Ok(value.map_or_else(|| (false, String::new()), |v| (true, v.to_owned()))) + } + pub async fn try_get_inventory_map( + &self, + token: CancellationToken, + ) -> Result<(bool, Option), Error> { + token.throw_if_cancellation_requested()?; + Ok((false, None)) + } + pub async fn try_get_shared_folder( + &self, + token: CancellationToken, + ) -> Result<(bool, Option), Error> { + token.throw_if_cancellation_requested()?; + Ok((false, None)) + } + pub async fn try_get_sit_id(&self, token: CancellationToken) -> Result<(bool, Guid), Error> { + token.throw_if_cancellation_requested()?; + Ok((false, Guid([0; 16]))) + } +} + +const ENV_ZERO_SETTINGS: &[&str] = &[ + "daytime", + "ambientr", + "ambientg", + "ambientb", + "ambienti", + "bluedensityr", + "bluedensityg", + "bluedensityb", + "bluedensityi", + "bluehorizonr", + "bluehorizong", + "bluehorizonb", + "bluehorizoni", + "cloudcolorr", + "cloudcolorg", + "cloudcolorb", + "cloudcolori", + "cloudcoverage", + "cloudx", + "cloudy", + "cloudd", + "clouddetailx", + "clouddetaily", + "clouddetaild", + "cloudscale", + "cloudscrollx", + "cloudscrolly", + "cloudvariance", + "densitymultiplier", + "distancemultiplier", + "dropletradius", + "eastangle", + "icelevel", + "hazedensity", + "hazehorizon", + "maxaltitude", + "moisturelevel", + "moonazim", + "moonnbrightness", + "moonelev", + "moonscale", + "scenegamma", + "starbrightness", + "sunglowfocus", + "sunazim", + "sunelev", + "sunscale", + "sunmoonposition", + "sunmooncolorr", + "sunmooncolorg", + "sunmooncolorb", + "sunmooncolori", +]; + +impl IRlvQueryCallbacks for RlvCallbacksDefault { + fn is_sitting(&self, t: CancellationToken) -> QueryFuture<'_, bool> { + Box::pin(self.is_sitting(t)) + } + fn object_exists(&self, a: Guid, t: CancellationToken) -> QueryFuture<'_, bool> { + Box::pin(self.object_exists(a, t)) + } + fn try_get_active_group_name(&self, t: CancellationToken) -> QueryFuture<'_, (bool, String)> { + Box::pin(self.try_get_active_group_name(t)) + } + fn try_get_camera_settings( + &self, + t: CancellationToken, + ) -> QueryFuture<'_, (bool, Option)> { + Box::pin(self.try_get_camera_settings(t)) + } + fn try_get_debug_setting_value( + &self, + a: String, + t: CancellationToken, + ) -> QueryFuture<'_, (bool, String)> { + Box::pin(self.try_get_debug_setting_value(a, t)) + } + fn try_get_environment_setting_value( + &self, + a: String, + t: CancellationToken, + ) -> QueryFuture<'_, (bool, String)> { + Box::pin(self.try_get_environment_setting_value(a, t)) + } + fn try_get_inventory_map( + &self, + t: CancellationToken, + ) -> QueryFuture<'_, (bool, Option)> { + Box::pin(self.try_get_inventory_map(t)) + } + fn try_get_sit_id(&self, t: CancellationToken) -> QueryFuture<'_, (bool, Guid)> { + Box::pin(self.try_get_sit_id(t)) + } +} + +#[derive(Clone)] +pub struct RlvCommandProcessor { + permissions: RlvPermissionsService, + queries: Arc, + actions: Arc, +} + +impl std::fmt::Debug for RlvCommandProcessor { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("RlvCommandProcessor") + .finish_non_exhaustive() + } +} + +impl RlvCommandProcessor { + fn new( + permissions: RlvPermissionsService, + queries: Arc, + actions: Arc, + ) -> Self { + Self { + permissions, + queries, + actions, + } + } + + async fn inventory(&self, token: &CancellationToken) -> Result, Error> { + token.throw_if_cancellation_requested()?; + let (available, inventory) = self.queries.try_get_inventory_map(token.clone()).await?; + if available && let Some(inventory) = inventory { + self.permissions + .restriction_manager() + .set_inventory_map(Some(inventory.clone())); + return Ok(Some(inventory)); + } + Ok(None) + } + + async fn process( + &self, + action: &RlvAction, + sender: Guid, + token: &CancellationToken, + ) -> Result { + token.throw_if_cancellation_requested()?; + match action { + RlvAction::SetRotation(value) => { + self.actions.set_rot(*value, token.clone()).await?; + } + RlvAction::AdjustHeight { + distance, + factor, + delta, + } => { + self.actions + .adjust_height(*distance, *factor, *delta, token.clone()) + .await?; + } + RlvAction::SetCameraFov(value) => { + if self.permissions.get_camera_restrictions()?.is_locked() { + return Ok(false); + } + self.actions.set_cam_fov(*value, token.clone()).await?; + } + RlvAction::Teleport { + x, + y, + z, + region, + look_at, + } => { + if !self.permissions.can_tp_loc()? || !self.permissions.can_unsit()? { + return Ok(false); + } + self.actions + .tp_to(*x, *y, *z, region.clone(), look_at.map(Some), token.clone()) + .await?; + } + RlvAction::Sit(target) => { + if !self.permissions.can_sit()? + || !self.queries.object_exists(*target, token.clone()).await? + { + return Ok(false); + } + if self.queries.is_sitting(token.clone()).await? + && (!self.permissions.can_unsit()? || !self.permissions.can_stand_tp()?) + { + return Ok(false); + } + self.actions.sit(*target, token.clone()).await?; + } + RlvAction::Unsit => { + if !self.permissions.can_unsit()? { + return Ok(false); + } + self.actions.unsit(token.clone()).await?; + } + RlvAction::SitGround => { + if !self.permissions.can_sit()? { + return Ok(false); + } + self.actions.sit_ground(token.clone()).await?; + } + RlvAction::RemoveOutfit(target) => { + return self.remove_outfit(target, token).await; + } + RlvAction::DetachMe => { + return self.detach_me(sender, token).await; + } + RlvAction::RemoveAttachment(target) => { + return self.remove_attachment(target, token).await; + } + RlvAction::DetachAll { folder_path } => { + return self.detach_all(folder_path, token).await; + } + RlvAction::DetachThis { target, recursive } => { + return self.detach_this(target, sender, *recursive, token).await; + } + RlvAction::SetGroup { target, role } => match target { + RlvGroupTarget::Uuid(id) => { + self.actions + .set_group_with_guid_string_cancellation_token( + *id, + role.clone(), + token.clone(), + ) + .await?; + } + RlvGroupTarget::Name(name) => { + self.actions + .set_group_with_string_string_cancellation_token( + name.clone(), + role.clone(), + token.clone(), + ) + .await?; + } + }, + RlvAction::SetDebug { name, value } => { + self.actions + .set_debug(name.clone(), value.clone(), token.clone()) + .await?; + } + RlvAction::SetEnvironment { name, value } => { + self.actions + .set_env(name.clone(), value.clone(), token.clone()) + .await?; + } + RlvAction::Attach { + folder_path, + replace, + recursive, + } => { + return self + .attach_folder(folder_path, *replace, *recursive, token) + .await; + } + RlvAction::AttachThis { + target, + replace, + recursive, + } => { + return self + .attach_this(target, sender, *replace, *recursive, token) + .await; + } + } + Ok(true) + } + + async fn attach_folder( + &self, + path: &str, + replace: bool, + recursive: bool, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let mut folder = None; + if !inventory.try_get_folder_from_path(path.to_owned(), false, &mut folder) { + self.actions.attach(Vec::new(), token.clone()).await?; + return Ok(false); + } + let requests = collect_attach( + folder.expect("successful folder lookup"), + replace, + recursive, + false, + )?; + self.actions.attach(requests, token.clone()).await?; + Ok(true) + } + + async fn attach_this( + &self, + target: &RlvTarget, + sender: Guid, + replace: bool, + recursive: bool, + token: &CancellationToken, + ) -> Result { + if matches!(target, RlvTarget::FolderPath(_)) { + return Ok(false); + } + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let (folders, skip_hidden) = folders_for_target(&inventory, target, sender)?; + let mut seen = HashSet::new(); + let mut requests = Vec::new(); + for folder in folders { + for request in collect_attach(folder, replace, recursive, skip_hidden)? { + if seen.insert(request.item_id()) { + requests.push(request); + } + } + } + self.actions.attach(requests, token.clone()).await?; + Ok(true) + } + + async fn remove_attachment( + &self, + target: &RlvTarget, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let items = match target { + RlvTarget::Sender => inventory + .current_outfit() + .into_iter() + .filter(|item| item.attached_to().flatten().is_some()) + .collect(), + RlvTarget::Uuid(id) => inventory.get_items_by_prim_id(*id)?, + RlvTarget::AttachmentPoint(point) => inventory.get_items_by_attachment_point(*point)?, + RlvTarget::FolderPath(path) => { + let mut folder = None; + if !inventory.try_get_folder_from_path(path.clone(), false, &mut folder) { + return Ok(false); + } + folder.expect("successful folder lookup").items() + } + RlvTarget::WearableType(_) => return Ok(false), + }; + let ids = self.detachable_ids(items, true, true)?; + self.actions.detach(ids, token.clone()).await?; + Ok(true) + } + + async fn detach_me(&self, sender: Guid, token: &CancellationToken) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let items = inventory.get_items_by_prim_id(sender)?; + if items.is_empty() { + return Ok(false); + } + let ids = self.detachable_ids(items, false, true)?; + self.actions.detach(ids, token.clone()).await?; + Ok(true) + } + + async fn detach_all(&self, path: &str, token: &CancellationToken) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let mut folder = None; + if !inventory.try_get_folder_from_path(path.to_owned(), false, &mut folder) { + return Ok(false); + } + let items = collect_folder_items(folder.expect("successful folder lookup"), true, false); + let ids = self.detachable_ids(items, true, true)?; + self.actions.detach(ids, token.clone()).await?; + Ok(true) + } + + async fn detach_this( + &self, + target: &RlvTarget, + sender: Guid, + recursive: bool, + token: &CancellationToken, + ) -> Result { + if matches!(target, RlvTarget::FolderPath(_)) { + return Ok(false); + } + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let (folders, skip_hidden) = folders_for_target(&inventory, target, sender)?; + let mut items = Vec::new(); + for folder in folders { + items.extend(collect_folder_items(folder, recursive, skip_hidden)); + } + let ids = self.detachable_ids(items, true, true)?; + self.actions.detach(ids, token.clone()).await?; + Ok(true) + } + + async fn remove_outfit( + &self, + target: &RlvTarget, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(false); + }; + let items = match target { + RlvTarget::Sender => inventory + .current_outfit() + .into_iter() + .filter(|item| item.worn_on().flatten().is_some()) + .collect(), + RlvTarget::WearableType(kind) => inventory.get_items_by_wearable_type(*kind)?, + RlvTarget::FolderPath(path) => { + let mut folder = None; + if !inventory.try_get_folder_from_path(path.clone(), false, &mut folder) { + return Ok(false); + } + collect_folder_items(folder.expect("successful folder lookup"), false, false) + } + _ => return Ok(false), + }; + let ids = self.detachable_ids(items, true, true)?; + self.actions.rem_outfit(ids, token.clone()).await?; + Ok(true) + } + + fn detachable_ids( + &self, + items: Vec, + enforce_nostrip: bool, + enforce_restrictions: bool, + ) -> Result, Error> { + let mut order = Vec::new(); + let mut allowed = HashMap::new(); + for item in items { + let id = item.id(); + let can_detach = can_remove_item( + &self.permissions, + &item, + enforce_nostrip, + enforce_restrictions, + )?; + if !allowed.contains_key(&id) { + order.push(id); + } + allowed + .entry(id) + .and_modify(|current| *current &= can_detach) + .or_insert(can_detach); + } + Ok(order + .into_iter() + .filter(|id| allowed.get(id) == Some(&true)) + .collect()) + } +} + +fn can_remove_item( + permissions: &RlvPermissionsService, + item: &RlvInventoryItem, + enforce_nostrip: bool, + enforce_restrictions: bool, +) -> Result { + if item.worn_on().flatten().is_none() + && item.attached_to().flatten().is_none() + && item.gesture_state().flatten() != Some(RlvGestureState::Active) + { + return Ok(false); + } + if enforce_nostrip && item.name().to_ascii_lowercase().contains("nostrip") { + return Ok(false); + } + if enforce_nostrip + && !item.is_link() + && item + .folder() + .is_some_and(|folder| folder.name().to_ascii_lowercase().contains("nostrip")) + { + return Ok(false); + } + if matches!( + item.worn_on().flatten(), + Some( + RlvWearableType::Skin + | RlvWearableType::Shape + | RlvWearableType::Eyes + | RlvWearableType::Hair + ) + ) { + return Ok(false); + } + if enforce_restrictions && !permissions.can_detach_with_rlv_inventory_item(item.clone())? { + return Ok(false); + } + Ok(true) +} + +fn point_from_item_name(name: &str) -> Option { + let mut remaining = name; + let mut found = None; + while let Some(open) = remaining.find('(') { + let tail = &remaining[open + 1..]; + let Some(close) = tail.find(')') else { break }; + if let Some(point) = attachment_from_name(&tail[..close]) { + found = Some(point); + } + remaining = &tail[close + 1..]; + } + found +} + +fn collect_attach( + root: RlvSharedFolder, + replace: bool, + recursive: bool, + skip_hidden_root: bool, +) -> Result, Error> { + let mut stack = vec![(root, replace, skip_hidden_root)]; + let mut seen = HashSet::new(); + let mut requests = Vec::new(); + while let Some((folder, inherited_replace, skip_hidden)) = stack.pop() { + if skip_hidden && folder.name().starts_with('.') { + continue; + } + let folder_replace = if folder.name().starts_with('+') { + false + } else { + inherited_replace + }; + let folder_point = point_from_item_name(&folder.name()); + for item in folder.items() { + if item.attached_to().flatten().is_some() + || item.worn_on().flatten().is_some() + || item.gesture_state().flatten() == Some(RlvGestureState::Active) + { + continue; + } + if seen.insert(item.id()) { + requests.push(AttachmentRequest::new( + item.id(), + point_from_item_name(&item.name()) + .or(folder_point) + .unwrap_or(RlvAttachmentPoint::Default), + folder_replace, + )?); + } + } + if recursive { + let mut children = folder.children(); + children.reverse(); + stack.extend( + children + .into_iter() + .map(|child| (child, folder_replace, true)), + ); + } + } + Ok(requests) +} + +fn collect_folder_items( + root: RlvSharedFolder, + recursive: bool, + skip_hidden_root: bool, +) -> Vec { + let mut stack = vec![(root, skip_hidden_root)]; + let mut items = Vec::new(); + while let Some((folder, skip_hidden)) = stack.pop() { + if skip_hidden && folder.name().starts_with('.') { + continue; + } + items.extend(folder.items()); + if recursive { + let mut children = folder.children(); + children.reverse(); + stack.extend(children.into_iter().map(|child| (child, true))); + } + } + items +} + +fn folders_for_target( + inventory: &InventoryMap, + target: &RlvTarget, + sender: Guid, +) -> Result<(Vec, bool), Error> { + let (folders, skip_hidden) = match target { + RlvTarget::Sender => ( + inventory + .find_folders_containing(false, Some(Some(sender)), None, None)? + .collect(), + false, + ), + RlvTarget::Uuid(id) => { + let mut seen = HashSet::new(); + let folders = inventory + .get_items_by_prim_id(*id)? + .into_iter() + .filter_map(|item| item.folder()) + .filter(|folder| seen.insert(folder.id())) + .collect(); + (folders, true) + } + RlvTarget::AttachmentPoint(point) => ( + inventory + .find_folders_containing(false, None, Some(Some(*point)), None)? + .collect(), + true, + ), + RlvTarget::WearableType(kind) => ( + inventory + .find_folders_containing(false, None, None, Some(Some(*kind)))? + .collect(), + true, + ), + RlvTarget::FolderPath(path) => { + let mut folder = None; + if inventory.try_get_folder_from_path(path.clone(), false, &mut folder) { + (folder.into_iter().collect(), true) + } else { + (Vec::new(), true) + } + } + }; + Ok((folders, skip_hidden)) +} + +struct QueryHandler { + blacklist: RlvBlacklist, + restrictions: RlvRestrictionManager, + queries: Arc, + actions: Arc, +} + +const OUTFIT_ORDER: &[RlvWearableType] = &[ + RlvWearableType::Gloves, + RlvWearableType::Jacket, + RlvWearableType::Pants, + RlvWearableType::Shirt, + RlvWearableType::Shoes, + RlvWearableType::Skirt, + RlvWearableType::Socks, + RlvWearableType::Underpants, + RlvWearableType::Undershirt, + RlvWearableType::Skin, + RlvWearableType::Eyes, + RlvWearableType::Hair, + RlvWearableType::Shape, + RlvWearableType::Alpha, + RlvWearableType::Tattoo, + RlvWearableType::Physics, +]; + +impl QueryHandler { + async fn inventory(&self, token: &CancellationToken) -> Result, Error> { + token.throw_if_cancellation_requested()?; + let (available, inventory) = self.queries.try_get_inventory_map(token.clone()).await?; + if available { Ok(inventory) } else { Ok(None) } + } + + async fn process( + &self, + query: &RlvQuery, + channel: i32, + sender: Guid, + token: &CancellationToken, + ) -> Result { + token.throw_if_cancellation_requested()?; + let response = match query { + RlvQuery::Version { .. } => Some(RlvService::RLV_VERSION.to_owned()), + RlvQuery::VersionNumber { include_blacklist } => { + let mut response = RlvService::RLV_VERSION_NUM.to_owned(); + if *include_blacklist { + let blacklist = self.blacklist.get_blacklist()?; + if !blacklist.is_empty() { + response.push(','); + response.push_str(&blacklist.join(",")); + } + } + Some(response) + } + RlvQuery::Blacklist { filter } => Some( + self.blacklist + .get_blacklist()? + .into_iter() + .filter(|entry| entry.contains(filter)) + .collect::>() + .join(","), + ), + RlvQuery::Status { + all_senders, + filter, + separator, + } => Some(self.status(*all_senders, filter, separator, sender)?), + RlvQuery::Camera(kind) => { + let (available, settings) = + self.queries.try_get_camera_settings(token.clone()).await?; + if available { + settings.map(|settings| { + match kind { + RlvCameraQuery::AvatarDistanceMin => settings.av_dist_min(), + RlvCameraQuery::AvatarDistanceMax => settings.av_dist_max(), + RlvCameraQuery::FovMin => settings.fov_min(), + RlvCameraQuery::FovMax => settings.fov_max(), + RlvCameraQuery::ZoomMin => settings.zoom_min(), + RlvCameraQuery::CurrentFov => settings.current_fov(), + } + .to_string() + }) + } else { + None + } + } + RlvQuery::SitId => { + let (available, id) = self.queries.try_get_sit_id(token.clone()).await?; + Some(if available && id != Guid([0; 16]) { + guid_text(id) + } else { + "NULL_KEY".to_owned() + }) + } + RlvQuery::Outfit(kind) => Some(self.outfit(*kind, token).await?), + RlvQuery::Attachment(point) => Some(self.attachment(*point, token).await?), + RlvQuery::Inventory { path, worn } => { + Some(self.inventory_query(path, *worn, token).await?) + } + RlvQuery::FindFolders { + first_only, + terms, + separator, + } => Some( + self.find_folders(*first_only, terms, separator, token) + .await?, + ), + RlvQuery::Path { legacy, target } => self.path(*legacy, target, sender, token).await?, + RlvQuery::Group => { + let (available, group) = self + .queries + .try_get_active_group_name(token.clone()) + .await?; + Some(if available { group } else { "none".to_owned() }) + } + RlvQuery::Debug(name) => { + let (available, value) = self + .queries + .try_get_debug_setting_value(name.clone(), token.clone()) + .await?; + available.then_some(value) + } + RlvQuery::Environment(name) => { + let (available, value) = self + .queries + .try_get_environment_setting_value(name.clone(), token.clone()) + .await?; + available.then_some(value) + } + }; + let Some(response) = response else { + return Ok(false); + }; + token.throw_if_cancellation_requested()?; + self.actions + .send_reply(channel, response, token.clone()) + .await?; + Ok(true) + } + + fn status( + &self, + all_senders: bool, + filter: &str, + separator: &str, + sender: Guid, + ) -> Result { + let restrictions = self.restrictions.find_restrictions( + Some(filter.to_owned()), + (!all_senders).then_some(Some(sender)), + )?; + let mut response = String::new(); + for restriction in restrictions { + response.push_str(separator); + response.push_str(restriction_name(restriction.original_behavior())); + if !restriction.values().is_empty() { + response.push(':'); + response.push_str( + &restriction + .values() + .iter() + .map(ToString::to_string) + .collect::>() + .join(";"), + ); + } + } + Ok(response) + } + + async fn outfit( + &self, + specific: Option, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(String::new()); + }; + let worn: HashSet<_> = inventory + .current_outfit() + .into_iter() + .filter_map(|item| item.worn_on().flatten()) + .collect(); + if let Some(specific) = specific { + return Ok(if worn.contains(&specific) { "1" } else { "0" }.to_owned()); + } + Ok(OUTFIT_ORDER + .iter() + .map(|kind| if worn.contains(kind) { '1' } else { '0' }) + .collect()) + } + + async fn attachment( + &self, + specific: Option, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(String::new()); + }; + let attached: HashSet<_> = inventory + .current_outfit() + .into_iter() + .filter_map(|item| item.attached_to().flatten()) + .collect(); + if let Some(specific) = specific { + return Ok(if attached.contains(&specific) { + "1" + } else { + "0" + } + .to_owned()); + } + Ok(ATTACHMENT_POINT_ORDER + .iter() + .map(|point| if attached.contains(point) { '1' } else { '0' }) + .collect()) + } + + async fn inventory_query( + &self, + path: &str, + worn: bool, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(String::new()); + }; + let mut target = Some(inventory.root()); + if !path.is_empty() { + target = None; + if !inventory.try_get_folder_from_path(path.to_owned(), false, &mut target) { + return Ok(String::new()); + } + } + let target = target.expect("inventory root or successful lookup"); + if !worn { + return Ok(target + .children() + .into_iter() + .filter(|folder| !folder.name().starts_with('.')) + .map(|folder| folder.name()) + .collect::>() + .join(",")); + } + let mut result = vec![format!("|{}", worn_indicator(&target))]; + result.extend( + target + .children() + .into_iter() + .filter(|folder| !folder.name().starts_with('.')) + .map(|folder| format!("{}|{}", folder.name(), worn_indicator(&folder))), + ); + Ok(result.join(",")) + } + + async fn find_folders( + &self, + first_only: bool, + terms: &[String], + separator: &str, + token: &CancellationToken, + ) -> Result { + let Some(inventory) = self.inventory(token).await? else { + return Ok(String::new()); + }; + let mut stack = vec![inventory.root()]; + let mut found = Vec::new(); + while let Some(folder) = stack.pop() { + if terms.iter().all(|term| folder.name().contains(term)) { + found.push(folder.clone()); + if first_only { + break; + } + } + let mut children = folder.children(); + children.reverse(); + stack.extend( + children.into_iter().filter(|child| { + !child.name().starts_with('.') && !child.name().starts_with('~') + }), + ); + } + let mut paths = Vec::new(); + for folder in found { + let mut path = None; + if inventory.try_build_path_to_folder(folder.id(), &mut path) + && let Some(path) = path + { + paths.push(path); + } + } + Ok(paths.join(separator)) + } + + async fn path( + &self, + legacy: bool, + target: &RlvTarget, + sender: Guid, + token: &CancellationToken, + ) -> Result, Error> { + let Some(inventory) = self.inventory(token).await? else { + return Ok(Some(String::new())); + }; + let folders: Vec<_> = match target { + RlvTarget::Sender => inventory + .find_folders_containing(legacy, Some(Some(sender)), None, None)? + .collect(), + RlvTarget::Uuid(id) => inventory + .find_folders_containing(legacy, Some(Some(*id)), None, None)? + .collect(), + RlvTarget::AttachmentPoint(point) => inventory + .find_folders_containing(legacy, None, Some(Some(*point)), None)? + .collect(), + RlvTarget::WearableType(kind) => inventory + .find_folders_containing(legacy, None, None, Some(Some(*kind)))? + .collect(), + RlvTarget::FolderPath(_) => return Ok(None), + }; + let mut folders = folders; + folders.sort_by_key(RlvSharedFolder::name); + let mut paths = Vec::new(); + for folder in folders { + let mut path = None; + if inventory.try_build_path_to_folder(folder.id(), &mut path) + && let Some(path) = path + { + paths.push(path); + } + } + Ok(Some(paths.join(","))) + } +} + +fn worn_counts(root: &RlvSharedFolder, recursive: bool) -> (usize, usize) { + let mut total = 0usize; + let mut worn = 0usize; + let mut stack = vec![root.clone()]; + while let Some(folder) = stack.pop() { + let items = folder.items(); + total += items.len(); + worn += items + .into_iter() + .filter(|item| { + item.attached_to().flatten().is_some() + || item.worn_on().flatten().is_some() + || item.gesture_state().flatten() == Some(RlvGestureState::Active) + }) + .count(); + if recursive { + stack.extend(folder.children()); + } + } + (total, worn) +} + +fn worn_digit((total, worn): (usize, usize)) -> char { + if total == 0 { + '0' + } else if worn == 0 { + '1' + } else if total != worn { + '2' + } else { + '3' + } +} + +fn worn_indicator(folder: &RlvSharedFolder) -> String { + [ + worn_digit(worn_counts(folder, false)), + worn_digit(worn_counts(folder, true)), + ] + .into_iter() + .collect() +} + +const ATTACHMENT_POINT_ORDER: &[RlvAttachmentPoint] = &[ + RlvAttachmentPoint::Default, + RlvAttachmentPoint::Chest, + RlvAttachmentPoint::Skull, + RlvAttachmentPoint::LeftShoulder, + RlvAttachmentPoint::RightShoulder, + RlvAttachmentPoint::LeftHand, + RlvAttachmentPoint::RightHand, + RlvAttachmentPoint::LeftFoot, + RlvAttachmentPoint::RightFoot, + RlvAttachmentPoint::Spine, + RlvAttachmentPoint::Pelvis, + RlvAttachmentPoint::Mouth, + RlvAttachmentPoint::Chin, + RlvAttachmentPoint::LeftEar, + RlvAttachmentPoint::RightEar, + RlvAttachmentPoint::LeftEyeball, + RlvAttachmentPoint::RightEyeball, + RlvAttachmentPoint::Nose, + RlvAttachmentPoint::RightUpperArm, + RlvAttachmentPoint::RightForearm, + RlvAttachmentPoint::LeftUpperArm, + RlvAttachmentPoint::LeftForearm, + RlvAttachmentPoint::RightHip, + RlvAttachmentPoint::RightUpperLeg, + RlvAttachmentPoint::RightLowerLeg, + RlvAttachmentPoint::LeftHip, + RlvAttachmentPoint::LeftUpperLeg, + RlvAttachmentPoint::LeftLowerLeg, + RlvAttachmentPoint::Stomach, + RlvAttachmentPoint::LeftPec, + RlvAttachmentPoint::RightPec, + RlvAttachmentPoint::HUDCenter2, + RlvAttachmentPoint::HUDTopRight, + RlvAttachmentPoint::HUDTop, + RlvAttachmentPoint::HUDTopLeft, + RlvAttachmentPoint::HUDCenter, + RlvAttachmentPoint::HUDBottomLeft, + RlvAttachmentPoint::HUDBottom, + RlvAttachmentPoint::HUDBottomRight, + RlvAttachmentPoint::Neck, + RlvAttachmentPoint::AvatarCenter, + RlvAttachmentPoint::LeftHandRing, + RlvAttachmentPoint::RightHandRing, + RlvAttachmentPoint::TailBase, + RlvAttachmentPoint::TailTip, + RlvAttachmentPoint::LeftWing, + RlvAttachmentPoint::RightWing, + RlvAttachmentPoint::Jaw, + RlvAttachmentPoint::AltLeftEar, + RlvAttachmentPoint::AltRightEar, + RlvAttachmentPoint::AltLeftEye, + RlvAttachmentPoint::AltRightEye, + RlvAttachmentPoint::Tongue, + RlvAttachmentPoint::Groin, + RlvAttachmentPoint::LeftHindFoot, + RlvAttachmentPoint::RightHindFoot, +]; + +struct ServiceInner { + enabled: AtomicBool, + instant_messages: AtomicBool, + blacklist: RlvBlacklist, + restrictions: RlvRestrictionManager, + permissions: RlvPermissionsService, + commands: RlvCommandProcessor, + queries: Arc, + actions: Arc, + query_handler: QueryHandler, +} + +/// Complete callback-driven RLV service. +#[derive(Clone)] +pub struct RlvService { + inner: Arc, +} + +impl std::fmt::Debug for RlvService { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("RlvService") + .field("enabled", &self.enabled()) + .field( + "instant_messages", + &self.enable_instant_message_processing(), + ) + .finish_non_exhaustive() + } +} + +impl RlvService { + pub const RLV_VERSION: &'static str = "RestrainedLove viewer v3.4.3 (RLVa 2.4.2)"; + pub const RLV_VERSION_NUM: &'static str = "2040213"; + + pub fn new( + callbacks: Box, + action_callbacks: Box, + enabled: bool, + ) -> Result { + let queries: Arc = Arc::from(callbacks); + let actions: Arc = Arc::from(action_callbacks); + let blacklist = RlvBlacklist::new(); + let restrictions = RlvRestrictionManager::new(); + restrictions.set_action_callbacks(actions.clone()); + let permissions = RlvPermissionsService::new(restrictions.clone()); + let commands = + RlvCommandProcessor::new(permissions.clone(), queries.clone(), actions.clone()); + let query_handler = QueryHandler { + blacklist: blacklist.clone(), + restrictions: restrictions.clone(), + queries: queries.clone(), + actions: actions.clone(), + }; + Ok(Self { + inner: Arc::new(ServiceInner { + enabled: AtomicBool::new(enabled), + instant_messages: AtomicBool::new(false), + blacklist, + restrictions, + permissions, + commands, + queries, + actions, + query_handler, + }), + }) + } + + pub async fn process_instant_message( + &self, + message: String, + sender_id: Guid, + cancellation_token: Option, + ) -> Result { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + if !self.enabled() || !self.enable_instant_message_processing() || !message.starts_with('@') + { + return Ok(false); + } + let message = message.to_ascii_lowercase(); + if self.blacklist().is_blacklisted(message.clone())? { + return Ok(false); + } + let response = match message.as_str() { + "@version" => Some(Self::RLV_VERSION.to_owned()), + "@getblacklist" => Some(self.blacklist().get_blacklist()?.join(",")), + _ => None, + }; + let Some(response) = response else { + return Ok(false); + }; + token.throw_if_cancellation_requested()?; + self.inner + .actions + .send_instant_message(sender_id, response, token) + .await?; + Ok(true) + } + + pub async fn process_message( + &self, + message: String, + sender_id: Guid, + sender_name: String, + cancellation_token: Option, + ) -> Result { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + if !self.enabled() || !message.starts_with('@') || message.len() > MAX_RLV_MESSAGE_BYTES { + return Ok(false); + } + let body = &message[1..]; + if body.split(',').count() > MAX_RLV_COMMANDS { + return Ok(false); + } + let mut result = true; + for raw in body.split(',') { + token.throw_if_cancellation_requested()?; + if !self + .process_single(raw, sender_id, &sender_name, &token) + .await? + { + result = false; + } + } + Ok(result) + } + + async fn process_single( + &self, + raw: &str, + sender: Guid, + sender_name: &str, + token: &CancellationToken, + ) -> Result { + if raw.eq_ignore_ascii_case("clear") { + return self.process_clear(sender, "", token).await; + } + if let Some((behavior, filter)) = raw.split_once('=') + && behavior.eq_ignore_ascii_case("clear") + && !filter.is_empty() + { + return self + .process_clear(sender, &filter.to_ascii_lowercase(), token) + .await; + } + let Ok(parsed) = RlvParser::parse_message(&format!("@{raw}"), sender, sender_name) else { + return Ok(false); + }; + let command = &parsed.commands[0]; + if self.blacklist().is_blacklisted(command.behavior.clone())? { + if let RlvDirective::Query { channel, .. } = command.directive { + self.inner + .actions + .send_reply(channel, String::new(), token.clone()) + .await?; + } + return Ok(false); + } + match &command.directive { + RlvDirective::Clear => self.process_clear(sender, "", token).await, + RlvDirective::Action(action) => { + self.inner.commands.process(action, sender, token).await + } + RlvDirective::Restriction { + behavior, + operation, + values, + } => { + self.process_restriction(*behavior, *operation, values, sender, sender_name, token) + .await + } + RlvDirective::Query { channel, query } => { + self.inner + .query_handler + .process(query, *channel, sender, token) + .await + } + } + } + + async fn refresh_inventory(&self, token: &CancellationToken) -> Result<(), Error> { + token.throw_if_cancellation_requested()?; + let (available, inventory) = self + .inner + .queries + .try_get_inventory_map(token.clone()) + .await?; + if available && let Some(inventory) = inventory { + self.inner.restrictions.set_inventory_map(Some(inventory)); + } + Ok(()) + } + + async fn process_restriction( + &self, + behavior: RlvRestrictionType, + operation: RlvRestrictionOperation, + values: &[RlvValue], + sender: Guid, + sender_name: &str, + token: &CancellationToken, + ) -> Result { + self.refresh_inventory(token).await?; + let mut values = values.to_vec(); + if behavior == RlvRestrictionType::Notify && values.len() > 2 { + values.truncate(2); + } + let restriction = + RlvRestriction::from_values(behavior, sender, sender_name.to_owned(), values); + match operation { + RlvRestrictionOperation::Add => { + self.inner + .restrictions + .add_restriction(restriction.clone())?; + } + RlvRestrictionOperation::Remove => { + self.inner.restrictions.remove_restriction(&restriction); + } + } + self.send_restriction_notification( + &restriction, + operation == RlvRestrictionOperation::Add, + token, + ) + .await?; + Ok(true) + } + + async fn process_clear( + &self, + sender: Guid, + filter: &str, + token: &CancellationToken, + ) -> Result { + self.refresh_inventory(token).await?; + let restrictions = self + .inner + .restrictions + .find_restrictions(None, Some(Some(sender)))?; + let removed: Vec<_> = restrictions + .into_iter() + .filter(|restriction| { + restriction_name(restriction.original_behavior()) + .to_ascii_lowercase() + .contains(filter) + }) + .filter(|restriction| self.inner.restrictions.remove_restriction(restriction)) + .collect(); + for restriction in &removed { + token.throw_if_cancellation_requested()?; + self.send_restriction_notification(restriction, false, token) + .await?; + } + let message = if filter.is_empty() { + "clear".to_owned() + } else { + format!("clear:{filter}") + }; + self.send_restriction_change("clear", &message, token) + .await?; + Ok(true) + } + + async fn send_restriction_notification( + &self, + restriction: &RlvRestriction, + added: bool, + token: &CancellationToken, + ) -> Result<(), Error> { + let behavior = restriction_name(restriction.original_behavior()); + let mut message = behavior.to_owned(); + if !restriction.values().is_empty() { + message.push(':'); + message.push_str( + &restriction + .values() + .iter() + .map(ToString::to_string) + .collect::>() + .join(";"), + ); + } + message.push_str(if added { "=n" } else { "=y" }); + self.send_restriction_change(behavior, &message, token) + .await + } + + async fn send_restriction_change( + &self, + behavior: &str, + message: &str, + token: &CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested()?; + let listeners = self + .inner + .restrictions + .get_restrictions_by_type(RlvRestrictionType::Notify)?; + for listener in listeners { + token.throw_if_cancellation_requested()?; + let [RlvValue::Integer(channel), rest @ ..] = listener.values() else { + continue; + }; + let filter = match rest.first() { + Some(RlvValue::String(value)) => value.as_str(), + _ => "", + }; + if behavior + .to_ascii_lowercase() + .contains(&filter.to_ascii_lowercase()) + { + self.inner + .actions + .send_reply(*channel, format!("/{message}"), token.clone()) + .await?; + } + } + Ok(()) + } + + async fn send_notification( + &self, + message: String, + token: &CancellationToken, + ) -> Result<(), Error> { + token.throw_if_cancellation_requested()?; + let listeners = self + .inner + .restrictions + .get_restrictions_by_type(RlvRestrictionType::Notify)?; + for listener in listeners { + token.throw_if_cancellation_requested()?; + let [RlvValue::Integer(channel), rest @ ..] = listener.values() else { + continue; + }; + let filter = match rest.first() { + Some(RlvValue::String(value)) => value.as_str(), + _ => "", + }; + if message.contains(filter) { + self.inner + .actions + .send_reply(*channel, message.clone(), token.clone()) + .await?; + } + } + Ok(()) + } + + pub async fn report_inventory_offer_accepted( + &self, + mut folder_path: String, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let shared = folder_path.starts_with("#RLV/"); + if shared { + folder_path.drain(.."#RLV/".len()); + } + self.send_notification( + format!( + "/accepted_in_{} inv_offer {folder_path}", + if shared { "rlv" } else { "inv" } + ), + &token, + ) + .await + } + + pub async fn report_inventory_offer_declined( + &self, + mut folder_path: String, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + if folder_path.starts_with("#RLV/") { + folder_path.drain(.."#RLV/".len()); + } + self.send_notification(format!("/declined inv_offer {folder_path}"), &token) + .await + } + + pub async fn report_item_worn( + &self, + folder: Guid, + shared: bool, + kind: RlvWearableType, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let legal = self + .inner + .permissions + .can_attach_with_nullable_boolean_nullable_nullable( + Some(Some(folder)), + shared, + None, + Some(Some(kind)), + )?; + self.send_notification( + format!("/worn {} {}", legality(legal), wearable_name(kind)), + &token, + ) + .await + } + + pub async fn report_item_unworn( + &self, + item: Guid, + folder: Guid, + shared: bool, + kind: RlvWearableType, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let legal = self + .inner + .permissions + .can_detach_with_nullable_nullable_nullable_boolean_nullable_nullable( + Some(Some(item)), + None, + Some(Some(folder)), + shared, + None, + Some(Some(kind)), + )?; + self.send_notification( + format!("/unworn {} {}", legality(legal), wearable_name(kind)), + &token, + ) + .await + } + + pub async fn report_item_attached( + &self, + folder: Guid, + shared: bool, + point: RlvAttachmentPoint, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let legal = self + .inner + .permissions + .can_attach_with_nullable_boolean_nullable_nullable( + Some(Some(folder)), + shared, + Some(Some(point)), + None, + )?; + self.send_notification( + format!("/attached {} {}", legality(legal), attachment_name(point)), + &token, + ) + .await + } + + pub async fn report_item_detached( + &self, + item: Guid, + prim: Guid, + folder: Guid, + shared: bool, + point: RlvAttachmentPoint, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let legal = self + .inner + .permissions + .can_detach_with_nullable_nullable_nullable_boolean_nullable_nullable( + Some(Some(item)), + Some(Some(prim)), + Some(Some(folder)), + shared, + Some(Some(point)), + None, + )?; + self.send_notification( + format!("/detached {} {}", legality(legal), attachment_name(point)), + &token, + ) + .await + } + + pub async fn report_send_public_message( + &self, + message: String, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let mut channels = Vec::new(); + let redirected = if message.to_ascii_lowercase().starts_with("/me ") { + self.inner + .permissions + .try_get_redir_emote_channels(&mut channels) + } else { + self.inner + .permissions + .try_get_redir_chat_channels(&mut channels) + }; + if redirected { + for channel in channels { + token.throw_if_cancellation_requested()?; + self.inner + .actions + .send_reply(channel, message.clone(), token.clone()) + .await?; + } + } + Ok(()) + } + + pub async fn report_sit( + &self, + object: Option>, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let object = object.flatten(); + let legal = if object.is_some() { + self.inner.permissions.can_interact()? && self.inner.permissions.can_sit()? + } else { + self.inner.permissions.can_sit()? + }; + let message = object.map_or_else( + || format!("/sat ground {}", legality(legal)), + |id| format!("/sat object {} {}", legality(legal), guid_text(id)), + ); + self.send_notification(message, &token).await + } + + pub async fn report_unsit( + &self, + object: Option>, + cancellation_token: Option, + ) -> Result<(), Error> { + let token = token(cancellation_token); + token.throw_if_cancellation_requested()?; + let object = object.flatten(); + let legal = if object.is_some() { + self.inner.permissions.can_interact()? && self.inner.permissions.can_unsit()? + } else { + self.inner.permissions.can_unsit()? + }; + let message = object.map_or_else( + || format!("/unsat ground {}", legality(legal)), + |id| format!("/unsat object {} {}", legality(legal), guid_text(id)), + ); + self.send_notification(message, &token).await + } + + pub fn blacklist(&self) -> RlvBlacklist { + self.inner.blacklist.clone() + } + pub fn commands(&self) -> RlvCommandProcessor { + self.inner.commands.clone() + } + pub fn enable_instant_message_processing(&self) -> bool { + self.inner.instant_messages.load(Ordering::Acquire) + } + pub fn set_enable_instant_message_processing(&mut self, value: bool) { + self.inner.instant_messages.store(value, Ordering::Release); + } + pub fn enabled(&self) -> bool { + self.inner.enabled.load(Ordering::Acquire) + } + pub fn set_enabled(&mut self, value: bool) { + self.inner.enabled.store(value, Ordering::Release); + } + pub fn permissions(&self) -> RlvPermissionsService { + self.inner.permissions.clone() + } + pub fn restrictions(&self) -> RlvRestrictionManager { + self.inner.restrictions.clone() + } +} + +const fn legality(value: bool) -> &'static str { + if value { "legally" } else { "illegally" } +} + +fn wearable_name(value: RlvWearableType) -> String { + format!("{value:?}").to_ascii_lowercase() +} + +fn attachment_name(value: RlvAttachmentPoint) -> &'static str { + ATTACHMENT_POINT_ORDER + .iter() + .position(|candidate| *candidate == value) + .map_or("Unknown", |index| ATTACHMENT_POINT_NAMES[index]) +} + +const ATTACHMENT_POINT_NAMES: &[&str] = &[ + "None", + "Chest", + "Skull", + "Left Shoulder", + "Right Shoulder", + "Left Hand", + "Right Hand", + "Left Foot", + "Right Foot", + "Spine", + "Pelvis", + "Mouth", + "Chin", + "Left Ear", + "Right Ear", + "Left Eyeball", + "Right Eyeball", + "Nose", + "R Upper Arm", + "R Forearm", + "L Upper Arm", + "L Forearm", + "Right Hip", + "R Upper Leg", + "R Lower Leg", + "Left Hip", + "L Upper Leg", + "L Lower Leg", + "Stomach", + "Left Pec", + "Right Pec", + "Center 2", + "Top Right", + "Top", + "Top Left", + "Center", + "Bottom Left", + "Bottom", + "Bottom Right", + "Neck", + "Avatar Center", + "Left Ring Finger", + "Right Ring Finger", + "Tail Base", + "Tail Tip", + "Left Wing", + "Right Wing", + "Jaw", + "Alt Left Ear", + "Alt Right Ear", + "Alt Left Eye", + "Alt Right Eye", + "Tongue", + "Groin", + "Left Hind Foot", + "Right Hind Foot", +]; + +#[cfg(test)] +mod tests { + use std::future::Future; + use std::task::{Context, Poll, Waker}; + + use libremetaverse_types::compat::CancellationTokenSource; + + use super::*; + + fn complete(future: impl Future) -> T { + let mut future = Box::pin(future); + let mut context = Context::from_waker(Waker::noop()); + match future.as_mut().poll(&mut context) { + Poll::Ready(value) => value, + Poll::Pending => panic!("default callback unexpectedly suspended"), + } + } + + #[test] + fn default_callbacks_preserve_upstream_query_values() { + let callbacks = RlvCallbacksDefault::new().unwrap(); + + assert_eq!( + complete(callbacks.try_get_debug_setting_value( + "RenderResolutionDivisor".to_owned(), + CancellationToken::default(), + )), + Ok((true, "1".to_owned())) + ); + assert_eq!( + complete(callbacks.try_get_environment_setting_value( + "sunimage".to_owned(), + CancellationToken::default(), + )), + Ok((true, "00000000-0000-0000-0000-000000000000".to_owned())) + ); + assert_eq!( + complete(callbacks.try_get_environment_setting_value( + "not-a-setting".to_owned(), + CancellationToken::default(), + )), + Ok((false, String::new())) + ); + } + + #[test] + fn default_callbacks_observe_cancellation_without_side_effects() { + let source = CancellationTokenSource::new(); + let token = source.token(); + source.cancel(); + + assert_eq!( + complete(RlvActionCallbacksDefault.send_reply(7, "ignored".to_owned(), token.clone())), + Err(Error::Cancelled) + ); + assert_eq!( + complete(RlvCallbacksDefault.is_sitting(token)), + Err(Error::Cancelled) + ); + } + + #[test] + fn attachment_request_hash_is_stable_and_value_based() { + let request = AttachmentRequest::new( + Guid([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]), + RlvAttachmentPoint::Chest, + true, + ) + .unwrap(); + let copy = request.clone(); + + assert_eq!(request, copy); + assert_eq!(request.get_hash_code(), copy.get_hash_code()); + assert_eq!(request.get_hash_code(), 1_073_454_789); + } +} diff --git a/crates/libremetaverse-rlv/src/state.rs b/crates/libremetaverse-rlv/src/state.rs index 1c9bac9..8e3e962 100644 --- a/crates/libremetaverse-rlv/src/state.rs +++ b/crates/libremetaverse-rlv/src/state.rs @@ -20,9 +20,10 @@ use libremetaverse_types::compat::{ use libremetaverse_types::{Error, UUID}; use crate::{ - RlvAttachmentPoint, RlvGestureState, RlvPermissionsServiceHoverTextLocation, - RlvPermissionsServiceObjectLocation, RlvPermissionsServiceTouchLocation, RlvRestriction, - RlvRestrictionType, RlvValue, RlvWearableType, restriction_name, + IRlvActionCallbacks, RlvAttachmentPoint, RlvGestureState, + RlvPermissionsServiceHoverTextLocation, RlvPermissionsServiceObjectLocation, + RlvPermissionsServiceTouchLocation, RlvRestriction, RlvRestrictionType, RlvValue, + RlvWearableType, restriction_name, }; /// Maximum number of folders accepted in one immutable inventory snapshot. @@ -970,6 +971,7 @@ struct RestrictionState { struct RestrictionManagerInner { state: RwLock, + action_callbacks: RwLock>>, handlers: Arc>>>, next_handler: AtomicU64, } @@ -1003,12 +1005,17 @@ impl RlvRestrictionManager { Self { inner: Arc::new(RestrictionManagerInner { state: RwLock::new(RestrictionState::default()), + action_callbacks: RwLock::new(None), handlers: Arc::new(Mutex::new(BTreeMap::new())), next_handler: AtomicU64::new(1), }), } } + pub(crate) fn set_action_callbacks(&self, callbacks: Arc) { + *write_lock(&self.inner.action_callbacks) = Some(callbacks); + } + /// Replaces the shared-inventory snapshot used for folder-lock decisions. pub fn set_inventory_map(&self, inventory: Option) { let mut state = write_lock(&self.inner.state); @@ -1215,10 +1222,58 @@ impl RlvRestrictionManager { token.throw_if_cancellation_requested()?; } self.emit(RestrictionUpdatedEventArgs { - restriction, + restriction: restriction.clone(), is_new: false, is_deleted: true, }); + self.notify_removed_restriction(&restriction, cancellation_token.clone()) + .await?; + } + } + Ok(()) + } + + async fn notify_removed_restriction( + &self, + restriction: &RlvRestriction, + cancellation_token: Option, + ) -> Result<(), Error> { + let callbacks = read_lock(&self.inner.action_callbacks).clone(); + let Some(callbacks) = callbacks else { + return Ok(()); + }; + let listeners = self.restrictions_by_type(RlvRestrictionType::Notify); + let behavior = restriction_name(restriction.original_behavior()); + let mut message = behavior.to_owned(); + if !restriction.values().is_empty() { + message.push(':'); + message.push_str( + &restriction + .values() + .iter() + .map(ToString::to_string) + .collect::>() + .join(";"), + ); + } + message.push_str("=y"); + let token = cancellation_token.unwrap_or_default(); + for listener in listeners { + token.throw_if_cancellation_requested()?; + let [RlvValue::Integer(channel), rest @ ..] = listener.values() else { + continue; + }; + let filter = match rest.first() { + Some(RlvValue::String(value)) => value.as_str(), + _ => "", + }; + if behavior + .to_ascii_lowercase() + .contains(&filter.to_ascii_lowercase()) + { + callbacks + .send_reply(*channel, format!("/{message}"), token.clone()) + .await?; } } Ok(()) diff --git a/tests/PARITY.md b/tests/PARITY.md index 129dce0..993056d 100644 --- a/tests/PARITY.md +++ b/tests/PARITY.md @@ -602,21 +602,21 @@ Reviewed Rust tests live outside `generated_parity.rs` and carry a `parity-case` | `LibreMetaverse.Tests/RLV/Commands/DetachMeCommandTests.cs::DetachMeCommandTests.DetachMeForce_WithNoStripItemName_SpecialException::test` | `DetachMeCommandTests.DetachMeForce_WithNoStripItemName_SpecialException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachMeCommandTests.cs:79` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_me_commands_semantics.rs:67 (`detach_me_ignores_nostrip`)` | `translated` | `ee511230131312be9b788033aac8766ea3cb3c1f8d7c5b9b26a4bd6a83cd695c` | | `LibreMetaverse.Tests/RLV/Commands/DetachMeCommandTests.cs::DetachMeCommandTests.DetachMeForce_WithLockedFolderRestriction::test` | `DetachMeCommandTests.DetachMeForce_WithLockedFolderRestriction` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachMeCommandTests.cs:146` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_me_commands_semantics.rs:72 (`detach_me_honors_locked_linked_folder`)` | `translated` | `05fefae0563306af0c491006f18915ff1b250b7972c82c160f20f06e07335887` | | `LibreMetaverse.Tests/RLV/Commands/DetachMeCommandTests.cs::DetachMeCommandTests.DetachMeForce_MultipleLinksSameItem::test` | `DetachMeCommandTests.DetachMeForce_MultipleLinksSameItem` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachMeCommandTests.cs:224` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_me_commands_semantics.rs:77 (`detach_me_deduplicates_multiple_links`)` | `translated` | `1ae8a318762625819371fb109fd673e0d31a2c0354add1e65802eb54cb71e303` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_Default::test` | `DetachThisCommandTests.DetachThisForce_Default` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:15` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:101 (`detach_this_default`)` | `translated` | `ae8c0fd737f869ef3e2dbd97ae28974b35c6bbc5327151c0074d531efdaaf54e` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_NoStrip::test` | `DetachThisCommandTests.DetachThisForce_NoStrip` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:87` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:109 (`detach_this_honors_item_nostrip`)` | `translated` | `3aa17222f7539814f8329e07de548cca4bb702f4b0409e793ea551327b0c57d0` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_NoStripFolder::test` | `DetachThisCommandTests.DetachThisForce_NoStripFolder` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:159` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:117 (`detach_this_honors_folder_nostrip`)` | `translated` | `16b74c3bf8312ccb885e61f933ec9d292988d2e1fad05e0188006df5630681f8` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_NoStripFolder_LinkException::test` | `DetachThisCommandTests.DetachThisForce_NoStripFolder_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:231` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:122 (`detach_this_honors_folder_link_exception`)` | `translated` | `b86b22a0ab30d0ded0cd6fbc7d34fae77e521861f5672b44a25a77325e594ab4` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById::test` | `DetachThisCommandTests.DetachThisForce_ById` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:305` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:130 (`detach_this_by_id`)` | `translated` | `3c4a211e5fefb44b42f50f6e6e58f8f852c8cf2d732bb6c3f737c3d6a592fa22` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById_NostripItemName::test` | `DetachThisCommandTests.DetachThisForce_ById_NostripItemName` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:376` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:138 (`detach_this_by_id_honors_item_nostrip`)` | `translated` | `f32926ca6f4328afdd3ddeb9f7bec8ff2ee01e9563555921ac8c663d20005551` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById_NostripFolderName::test` | `DetachThisCommandTests.DetachThisForce_ById_NostripFolderName` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:447` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:150 (`detach_this_by_id_honors_folder_nostrip`)` | `translated` | `bec3216bf016c2543a3410b069abd786167e04a7527253887f158127644656a8` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById_NostripFolderName_LinkException::test` | `DetachThisCommandTests.DetachThisForce_ById_NostripFolderName_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:518` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:162 (`detach_this_by_id_honors_link_exception`)` | `translated` | `1b5eb3a0bdef8ea65b800533ab3153dc6b3ea21d2f4f27e4820d0a0df48891a2` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint::test` | `DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:591` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:174 (`detach_this_by_attachment_point`)` | `translated` | `7e8f3a4d8b2cbdb5cc81eea598da6ef6395834b1cb7a629dc8d7ee2bfd80fab5` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip::test` | `DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:664` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:182 (`detach_this_by_attachment_point_honors_nostrip`)` | `translated` | `f6ff482bdb8d28414e01952201e37e49316b3e2345b0ff85dd4cb059193d1816` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip_LinkException::test` | `DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:738` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:190 (`detach_this_by_attachment_point_honors_link_exception`)` | `translated` | `289f8cc48f0e6a412d58258876e4aca08d8116d549642a0011bbdaf9b2223e83` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:813` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:198 (`detach_this_by_wearable`)` | `translated` | `51607294f12279974c2b44b47ba2eadca9846180a39bcde6a28c020dc4466254` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:883` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:206 (`detach_this_by_wearable_honors_nostrip`)` | `translated` | `30dec183993b2a644c2812ffc1c863e1714df134942f7827df818d9200e47c38` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip_LinkException::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:955` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:214 (`detach_this_by_wearable_honors_link_exception`)` | `translated` | `62f59cf66f21dcac15cf3166dcec2183181c0b434e9eacba796eb7025c4f1ab3` | -| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType_PrivateFolder::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType_PrivateFolder` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:1028` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:222 (`detach_this_by_wearable_skips_private_folder`)` | `translated` | `dd40588598bd081189e7c4f284af3851a140155e2ede0ae2121dd59a156dc988` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_Default::test` | `DetachThisCommandTests.DetachThisForce_Default` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:15` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:105 (`detach_this_default`)` | `translated` | `ae8c0fd737f869ef3e2dbd97ae28974b35c6bbc5327151c0074d531efdaaf54e` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_NoStrip::test` | `DetachThisCommandTests.DetachThisForce_NoStrip` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:87` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:113 (`detach_this_honors_item_nostrip`)` | `translated` | `3aa17222f7539814f8329e07de548cca4bb702f4b0409e793ea551327b0c57d0` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_NoStripFolder::test` | `DetachThisCommandTests.DetachThisForce_NoStripFolder` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:159` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:121 (`detach_this_honors_folder_nostrip`)` | `translated` | `16b74c3bf8312ccb885e61f933ec9d292988d2e1fad05e0188006df5630681f8` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_NoStripFolder_LinkException::test` | `DetachThisCommandTests.DetachThisForce_NoStripFolder_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:231` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:126 (`detach_this_honors_folder_link_exception`)` | `translated` | `b86b22a0ab30d0ded0cd6fbc7d34fae77e521861f5672b44a25a77325e594ab4` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById::test` | `DetachThisCommandTests.DetachThisForce_ById` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:305` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:134 (`detach_this_by_id`)` | `translated` | `3c4a211e5fefb44b42f50f6e6e58f8f852c8cf2d732bb6c3f737c3d6a592fa22` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById_NostripItemName::test` | `DetachThisCommandTests.DetachThisForce_ById_NostripItemName` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:376` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:142 (`detach_this_by_id_honors_item_nostrip`)` | `translated` | `f32926ca6f4328afdd3ddeb9f7bec8ff2ee01e9563555921ac8c663d20005551` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById_NostripFolderName::test` | `DetachThisCommandTests.DetachThisForce_ById_NostripFolderName` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:447` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:154 (`detach_this_by_id_honors_folder_nostrip`)` | `translated` | `bec3216bf016c2543a3410b069abd786167e04a7527253887f158127644656a8` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ById_NostripFolderName_LinkException::test` | `DetachThisCommandTests.DetachThisForce_ById_NostripFolderName_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:518` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:166 (`detach_this_by_id_honors_link_exception`)` | `translated` | `1b5eb3a0bdef8ea65b800533ab3153dc6b3ea21d2f4f27e4820d0a0df48891a2` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint::test` | `DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:591` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:178 (`detach_this_by_attachment_point`)` | `translated` | `7e8f3a4d8b2cbdb5cc81eea598da6ef6395834b1cb7a629dc8d7ee2bfd80fab5` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip::test` | `DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:664` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:186 (`detach_this_by_attachment_point_honors_nostrip`)` | `translated` | `f6ff482bdb8d28414e01952201e37e49316b3e2345b0ff85dd4cb059193d1816` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip_LinkException::test` | `DetachThisCommandTests.DetachThisForce_ByRlvAttachmentPoint_NoStrip_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:738` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:194 (`detach_this_by_attachment_point_honors_link_exception`)` | `translated` | `289f8cc48f0e6a412d58258876e4aca08d8116d549642a0011bbdaf9b2223e83` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:813` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:202 (`detach_this_by_wearable`)` | `translated` | `51607294f12279974c2b44b47ba2eadca9846180a39bcde6a28c020dc4466254` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:883` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:210 (`detach_this_by_wearable_honors_nostrip`)` | `translated` | `30dec183993b2a644c2812ffc1c863e1714df134942f7827df818d9200e47c38` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip_LinkException::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType_NoStrip_LinkException` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:955` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:218 (`detach_this_by_wearable_honors_link_exception`)` | `translated` | `62f59cf66f21dcac15cf3166dcec2183181c0b434e9eacba796eb7025c4f1ab3` | +| `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs::DetachThisCommandTests.DetachThisForce_ByRlvWearableType_PrivateFolder::test` | `DetachThisCommandTests.DetachThisForce_ByRlvWearableType_PrivateFolder` | `` | `LibreMetaverse.Tests/RLV/Commands/DetachThisCommandTests.cs:1028` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_detach_this_commands_semantics.rs:226 (`detach_this_by_wearable_skips_private_folder`)` | `translated` | `dd40588598bd081189e7c4f284af3851a140155e2ede0ae2121dd59a156dc988` | | `LibreMetaverse.Tests/RLV/Commands/RemAttachCommandTests.cs::RemAttachCommandTests.RemAttach::case:bb1d34dc76ade75f` | `RemAttachCommandTests.RemAttach` | `"@detach=force"` | `LibreMetaverse.Tests/RLV/Commands/RemAttachCommandTests.cs:15` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_rem_attach_commands_semantics.rs:177 (`detach_all_attachments`)` | `translated` | `16420d56c60229e8d8b6a2a0d63b1f500112c23cef451d69bb0ed97fe9b63bae` | | `LibreMetaverse.Tests/RLV/Commands/RemAttachCommandTests.cs::RemAttachCommandTests.RemAttach::case:c9faa5be8fef55e8` | `RemAttachCommandTests.RemAttach` | `"@remattach=force"` | `LibreMetaverse.Tests/RLV/Commands/RemAttachCommandTests.cs:16` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_rem_attach_commands_semantics.rs:182 (`remattach_all_attachments`)` | `translated` | `16420d56c60229e8d8b6a2a0d63b1f500112c23cef451d69bb0ed97fe9b63bae` | | `LibreMetaverse.Tests/RLV/Commands/RemAttachCommandTests.cs::RemAttachCommandTests.RemAttach_NoStrip::case:bb1d34dc76ade75f` | `RemAttachCommandTests.RemAttach_NoStrip` | `"@detach=force"` | `LibreMetaverse.Tests/RLV/Commands/RemAttachCommandTests.cs:83` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_rem_attach_commands_semantics.rs:187 (`detach_honors_nostrip`)` | `translated` | `5b3f1cbe99833e16c6f10ef568618627445babbcdfc9be555b48257bfdffeea7` | @@ -847,16 +847,16 @@ Reviewed Rust tests live outside `generated_parity.rs` and carry a `parity-case` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyClear_Filtered::test` | `NotifyQueryTests.NotifyClear_Filtered` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:134` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:197 (`notify_clear_filtered`)` | `translated` | `f3e0550a53faca13679421318eee85f010c1b8668e3edfae6e2548d39dbcafd7` | | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyClear::test` | `NotifyQueryTests.NotifyClear` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:155` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:213 (`notify_clear`)` | `translated` | `61d7419956a83be04eeb13557c6a2120419b2f0f1c529149c4fb57871bb6506e` | | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyInventoryOffer::test` | `NotifyQueryTests.NotifyInventoryOffer` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:178` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:230 (`notify_inventory_offer`)` | `translated` | `2bbac63ab86ec77f6d0a4558a14ee913a7f44930364476a89e02cd5ec80191b9` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifySitStandLegal::test` | `NotifyQueryTests.NotifySitStandLegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:199` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:268 (`notify_sit_and_stand_legal`)` | `translated` | `83a07a88a0386982c854a0223b64dd5c417f1a55a038e4ceba8f487983daadaf` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifySitStandWithRestrictions::test` | `NotifyQueryTests.NotifySitStandWithRestrictions` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:224` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:292 (`notify_sit_and_stand_illegal`)` | `translated` | `166e2087aa0652bdd427e1908bc338659ec1d4dc8065c016612696d1f5dfd000` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyWear::test` | `NotifyQueryTests.NotifyWear` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:252` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:316 (`notify_wear_legal`)` | `translated` | `9529acb9f3e1c335a50f8de6d63cc5711921de962546ff868811279d05826399` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyWear_Illegal::test` | `NotifyQueryTests.NotifyWear_Illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:275` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:341 (`notify_wear_illegal`)` | `translated` | `65bfc1db698470cf56dd8c6811e85ae373d1fca95cdbf82be66b7e32da311a2f` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyUnWear::test` | `NotifyQueryTests.NotifyUnWear` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:295` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:357 (`notify_unwear_legal`)` | `translated` | `340b60b8038dcd55320dc9aedc16beb9193e731ba7b659ca3cb2951a2d19b7ac` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyUnWear_illegal::test` | `NotifyQueryTests.NotifyUnWear_illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:318` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:388 (`notify_unwear_illegal`)` | `translated` | `e7056e6e145430eb2180edb92593f59c07bcbdfa21629b1ca29ac93b2fca975e` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyAttached::test` | `NotifyQueryTests.NotifyAttached` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:340` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:407 (`notify_attached_legal`)` | `translated` | `dc5239c77b9169cf3833c5a1dc765b381efe833f15b34ac7e57173c0a640942a` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyAttached_Illegal::test` | `NotifyQueryTests.NotifyAttached_Illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:363` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:434 (`notify_attached_illegal`)` | `translated` | `b56de057dece9cb8ece795d13d6bc42e7825d62f09f967f37a4dbf262d2b4456` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyDetached::test` | `NotifyQueryTests.NotifyDetached` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:383` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:465 (`notify_detached_legal`)` | `translated` | `a741cec23a985028b3dc2b09195255276a96e4f07fc4fe5f8cca6c1e3c940d71` | -| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyDetached_Illegal::test` | `NotifyQueryTests.NotifyDetached_Illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:407` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:482 (`notify_detached_illegal`)` | `translated` | `1a4c9475a3b91a975b9861685dcaf899505b4f6727257618f9add9cecd3df54d` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifySitStandLegal::test` | `NotifyQueryTests.NotifySitStandLegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:199` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:269 (`notify_sit_and_stand_legal`)` | `translated` | `83a07a88a0386982c854a0223b64dd5c417f1a55a038e4ceba8f487983daadaf` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifySitStandWithRestrictions::test` | `NotifyQueryTests.NotifySitStandWithRestrictions` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:224` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:293 (`notify_sit_and_stand_illegal`)` | `translated` | `166e2087aa0652bdd427e1908bc338659ec1d4dc8065c016612696d1f5dfd000` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyWear::test` | `NotifyQueryTests.NotifyWear` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:252` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:317 (`notify_wear_legal`)` | `translated` | `9529acb9f3e1c335a50f8de6d63cc5711921de962546ff868811279d05826399` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyWear_Illegal::test` | `NotifyQueryTests.NotifyWear_Illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:275` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:342 (`notify_wear_illegal`)` | `translated` | `65bfc1db698470cf56dd8c6811e85ae373d1fca95cdbf82be66b7e32da311a2f` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyUnWear::test` | `NotifyQueryTests.NotifyUnWear` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:295` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:358 (`notify_unwear_legal`)` | `translated` | `340b60b8038dcd55320dc9aedc16beb9193e731ba7b659ca3cb2951a2d19b7ac` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyUnWear_illegal::test` | `NotifyQueryTests.NotifyUnWear_illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:318` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:389 (`notify_unwear_illegal`)` | `translated` | `e7056e6e145430eb2180edb92593f59c07bcbdfa21629b1ca29ac93b2fca975e` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyAttached::test` | `NotifyQueryTests.NotifyAttached` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:340` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:408 (`notify_attached_legal`)` | `translated` | `dc5239c77b9169cf3833c5a1dc765b381efe833f15b34ac7e57173c0a640942a` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyAttached_Illegal::test` | `NotifyQueryTests.NotifyAttached_Illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:363` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:435 (`notify_attached_illegal`)` | `translated` | `b56de057dece9cb8ece795d13d6bc42e7825d62f09f967f37a4dbf262d2b4456` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyDetached::test` | `NotifyQueryTests.NotifyDetached` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:383` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:466 (`notify_detached_legal`)` | `translated` | `a741cec23a985028b3dc2b09195255276a96e4f07fc4fe5f8cca6c1e3c940d71` | +| `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs::NotifyQueryTests.NotifyDetached_Illegal::test` | `NotifyQueryTests.NotifyDetached_Illegal` | `` | `LibreMetaverse.Tests/RLV/Queries/NotifyQueryTests.cs:407` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_notify_semantics.rs:483 (`notify_detached_illegal`)` | `translated` | `1a4c9475a3b91a975b9861685dcaf899505b4f6727257618f9add9cecd3df54d` | | `LibreMetaverse.Tests/RLV/Queries/VersionNumBlQueryTests.cs::VersionNumBlQueryTests.VersionNumBL::case:478968a5d14830de` | `VersionNumBlQueryTests.VersionNumBL` | `"", RlvService.RLVVersionNum` | `LibreMetaverse.Tests/RLV/Queries/VersionNumBlQueryTests.cs:13` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_query_basics_semantics.rs:253 (`version_number_without_blacklist`)` | `translated` | `0dbc89f77a6f293efcca217febb2f6b8d224f9ac5e1d6532a56a7df64a7be6e9` | | `LibreMetaverse.Tests/RLV/Queries/VersionNumBlQueryTests.cs::VersionNumBlQueryTests.VersionNumBL::case:8c124ee42e9ab944` | `VersionNumBlQueryTests.VersionNumBL` | `"sendim,recvim", RlvService.RLVVersionNum + ",recvim,sendim"` | `LibreMetaverse.Tests/RLV/Queries/VersionNumBlQueryTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_query_basics_semantics.rs:262 (`version_number_with_blacklist`)` | `translated` | `0dbc89f77a6f293efcca217febb2f6b8d224f9ac5e1d6532a56a7df64a7be6e9` | | `LibreMetaverse.Tests/RLV/Queries/VersionQueryTests.cs::VersionQueryTests.ManualVersion::test` | `VersionQueryTests.ManualVersion` | `` | `LibreMetaverse.Tests/RLV/Queries/VersionQueryTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_query_basics_semantics.rs:271 (`manual_version`)` | `translated` | `6f8493b201d69e1449d8311e434736f6f6b2bad04c9fe9c6adf94a94d9cbe597` | @@ -869,37 +869,37 @@ Reviewed Rust tests live outside `generated_parity.rs` and carry a `parity-case` | `LibreMetaverse.Tests/RLV/Restrictions/AddOutfitRestrictionTests.cs::AddOutfitRestrictionTests.AddOutfit_part::test` | `AddOutfitRestrictionTests.AddOutfit_part` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AddOutfitRestrictionTests.cs:56` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:106 (`add_outfit_restricts_one_layer`)` | `translated` | `e7e5467b36484fb54b0336abccf682905fbe5bff9e04195645981815e2ffadeb` | | `LibreMetaverse.Tests/RLV/Restrictions/AllowIdleRestrictionTests.cs::AllowIdleRestrictionTests.CanAllowIdle::test` | `AllowIdleRestrictionTests.CanAllowIdle` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AllowIdleRestrictionTests.cs:9` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_simple_restrictions_semantics.rs:22 (`allow_idle_restriction`)` | `translated` | `6278bb1a70f024f687080cb8e8a56bbc923f679fb4adb61c6c91c3397a3de7fc` | | `LibreMetaverse.Tests/RLV/Restrictions/AlwaysRunRestrictionTests.cs::AlwaysRunRestrictionTests.CanAlwaysRun::test` | `AlwaysRunRestrictionTests.CanAlwaysRun` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AlwaysRunRestrictionTests.cs:9` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_simple_restrictions_semantics.rs:31 (`always_run_restriction`)` | `translated` | `9c0a34e0960e3e2a834ccfc1c129a1f5a8f8c6cfd30b322671b4e5c4cd25a24c` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis::test` | `AttachAllThisRestrictionTests.AttachAllThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:239 (`attach_all_this`)` | `translated` | `39bc0c4581f8277f4d5c90ec715c40f4715b5ae7007a28a04fe762f2f6adfbfd` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:88` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:252 (`attach_all_this_recursive`)` | `translated` | `d051e7783e79bf91a8092cd2d29cf1aacec3b0af86ecf0c7ca590ce25bc8393c` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Path::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Path` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:168` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:265 (`attach_all_this_recursive_path`)` | `translated` | `b4db2b29ad376777b382f8c6f4f7b62643bb2dd43858de4be69ac2252f18c963` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Worn::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Worn` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:248` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:278 (`attach_all_this_recursive_worn`)` | `translated` | `6ecfc6ffe71896979a37dac6dd8149a015b8d3e7e952247954333c053da4961e` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:327` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:291 (`attach_all_this_recursive_attached`)` | `translated` | `8ac38c77c6bdb03e4cbaa96600bead9a760eb5eec098504b05cfb3a1bdfb8557` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached_AddRem::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:425` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:304 (`attach_all_this_recursive_attached_removed`)` | `translated` | `495f288e0fa86059d13a027075700ba6308e9c9d585fa56e3d1adf4a71df912b` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis::test` | `AttachThisRestrictionTests.AttachThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:13` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:317 (`attach_this`)` | `translated` | `3a8a5acc4ad5cbec76737239ea23330a34f54146a4ff9d5d25082c220527a3e4` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_NotRecursive::test` | `AttachThisRestrictionTests.AttachThis_NotRecursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:81` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:330 (`attach_this_not_recursive`)` | `translated` | `a5818b4e30d04aba7d6427b2b94e025d34e82eb3bdcb7ac80a23e55f2382ec29` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByPath::test` | `AttachThisRestrictionTests.AttachThis_ByPath` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:151` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:343 (`attach_this_by_path`)` | `translated` | `93e1721f171e24e540160e738f731b895ebd868d840bcdf7c16ba92f6eb4c49a` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByRlvAttachmentPoint::test` | `AttachThisRestrictionTests.AttachThis_ByRlvAttachmentPoint` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:216` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:356 (`attach_this_by_attachment_point`)` | `translated` | `90a562b2c120daa0ea2612befe9e09649bc2ffac59c2cd360fe4a37dedf85ae3` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByWornLayer::test` | `AttachThisRestrictionTests.AttachThis_ByWornLayer` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:294` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:369 (`attach_this_by_worn_layer`)` | `translated` | `a444d51c2091fa36f1a010e98ad896042aa0262153ea8a833a14dfcbf02de85e` | -| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByWornLayer_AddRem::test` | `AttachThisRestrictionTests.AttachThis_ByWornLayer_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:369` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:382 (`attach_this_by_worn_layer_removed`)` | `translated` | `dd3d9a0d9b56f08f6be7a6200e1f50e834ecc7613de0cdf44ee89262b3abdb30` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis::test` | `AttachAllThisRestrictionTests.AttachAllThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:246 (`attach_all_this`)` | `translated` | `39bc0c4581f8277f4d5c90ec715c40f4715b5ae7007a28a04fe762f2f6adfbfd` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:88` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:259 (`attach_all_this_recursive`)` | `translated` | `d051e7783e79bf91a8092cd2d29cf1aacec3b0af86ecf0c7ca590ce25bc8393c` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Path::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Path` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:168` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:272 (`attach_all_this_recursive_path`)` | `translated` | `b4db2b29ad376777b382f8c6f4f7b62643bb2dd43858de4be69ac2252f18c963` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Worn::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Worn` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:248` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:285 (`attach_all_this_recursive_worn`)` | `translated` | `6ecfc6ffe71896979a37dac6dd8149a015b8d3e7e952247954333c053da4961e` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:327` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:298 (`attach_all_this_recursive_attached`)` | `translated` | `8ac38c77c6bdb03e4cbaa96600bead9a760eb5eec098504b05cfb3a1bdfb8557` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs::AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached_AddRem::test` | `AttachAllThisRestrictionTests.AttachAllThis_Recursive_Attached_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachAllThisRestrictionTests.cs:425` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:311 (`attach_all_this_recursive_attached_removed`)` | `translated` | `495f288e0fa86059d13a027075700ba6308e9c9d585fa56e3d1adf4a71df912b` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis::test` | `AttachThisRestrictionTests.AttachThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:13` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:324 (`attach_this`)` | `translated` | `3a8a5acc4ad5cbec76737239ea23330a34f54146a4ff9d5d25082c220527a3e4` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_NotRecursive::test` | `AttachThisRestrictionTests.AttachThis_NotRecursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:81` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:337 (`attach_this_not_recursive`)` | `translated` | `a5818b4e30d04aba7d6427b2b94e025d34e82eb3bdcb7ac80a23e55f2382ec29` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByPath::test` | `AttachThisRestrictionTests.AttachThis_ByPath` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:151` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:350 (`attach_this_by_path`)` | `translated` | `93e1721f171e24e540160e738f731b895ebd868d840bcdf7c16ba92f6eb4c49a` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByRlvAttachmentPoint::test` | `AttachThisRestrictionTests.AttachThis_ByRlvAttachmentPoint` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:216` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:363 (`attach_this_by_attachment_point`)` | `translated` | `90a562b2c120daa0ea2612befe9e09649bc2ffac59c2cd360fe4a37dedf85ae3` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByWornLayer::test` | `AttachThisRestrictionTests.AttachThis_ByWornLayer` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:294` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:376 (`attach_this_by_worn_layer`)` | `translated` | `a444d51c2091fa36f1a010e98ad896042aa0262153ea8a833a14dfcbf02de85e` | +| `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs::AttachThisRestrictionTests.AttachThis_ByWornLayer_AddRem::test` | `AttachThisRestrictionTests.AttachThis_ByWornLayer_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/AttachThisRestrictionTests.cs:369` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:389 (`attach_this_by_worn_layer_removed`)` | `translated` | `dd3d9a0d9b56f08f6be7a6200e1f50e834ecc7613de0cdf44ee89262b3abdb30` | | `LibreMetaverse.Tests/RLV/Restrictions/ChatNormalTests.cs::ChatNormalTests.CanChatNormal::test` | `ChatNormalTests.CanChatNormal` | `` | `LibreMetaverse.Tests/RLV/Restrictions/ChatNormalTests.cs:9` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_simple_restrictions_semantics.rs:40 (`chat_normal_restriction`)` | `translated` | `9a23c49327636e769c7ff2688a0649d441f3531406e8f51e152ec7186a312287` | | `LibreMetaverse.Tests/RLV/Restrictions/ChatShoutRestrictionTests.cs::ChatShoutRestrictionTests.CanChatShout::test` | `ChatShoutRestrictionTests.CanChatShout` | `` | `LibreMetaverse.Tests/RLV/Restrictions/ChatShoutRestrictionTests.cs:9` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_simple_restrictions_semantics.rs:49 (`chat_shout_restriction`)` | `translated` | `493e1dd87afaaa7f8183e677df5c32d00f150ffcdd53f9afad8684672837aa28` | | `LibreMetaverse.Tests/RLV/Restrictions/ChatWhisperRestrictionTests.cs::ChatWhisperRestrictionTests.CanChatWhisper::test` | `ChatWhisperRestrictionTests.CanChatWhisper` | `` | `LibreMetaverse.Tests/RLV/Restrictions/ChatWhisperRestrictionTests.cs:9` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_simple_restrictions_semantics.rs:58 (`chat_whisper_restriction`)` | `translated` | `67c5d2cb8b3e9424fbcf08378c2a28368fe07aa83ec028df7cf88b91576d3e9f` | | `LibreMetaverse.Tests/RLV/Restrictions/DefaultWearRestrictionTests.cs::DefaultWearRestrictionTests.CanDefaultWear::test` | `DefaultWearRestrictionTests.CanDefaultWear` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DefaultWearRestrictionTests.cs:10` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_simple_restrictions_semantics.rs:67 (`default_wear_restriction`)` | `translated` | `64eef4c82d1c5a21bc923cfd44616ddc1a5e6403a9a1c5ad6bd2e5ee06e9704c` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis::test` | `DetachAllThisRestrictionTests.DetachAllThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:395 (`detach_all_this`)` | `translated` | `ee74f952bd602bd23cfc7b203940a902e1b30e02cff60c1abc1f74da964c1f7d` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:88` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:408 (`detach_all_this_recursive`)` | `translated` | `2b6a646ee82484a6d1901ea4425b0e32fe553827f5bea37b506ffe29d1aec192` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Path::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Path` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:168` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:421 (`detach_all_this_recursive_path`)` | `translated` | `731244b623f23a8870d011280a062add751607ce2c58be3261485bb61d488297` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Worn::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Worn` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:245` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:434 (`detach_all_this_recursive_worn`)` | `translated` | `d8424a48e64346edd683b7a68652dd5a0be615658f085b0390699ac7b4db5d2c` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:324` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:447 (`detach_all_this_recursive_attached`)` | `translated` | `f399592217e7a4638ea25d469681ab9cc58bf8a937af2bfff8895996f7bbc493` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached_AddRem::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:421` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:460 (`detach_all_this_recursive_attached_removed`)` | `translated` | `507273250b932ff93fa34f87190ad1ab289edeed275be6d2259bab05c5b055d1` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis::test` | `DetachAllThisRestrictionTests.DetachAllThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:402 (`detach_all_this`)` | `translated` | `ee74f952bd602bd23cfc7b203940a902e1b30e02cff60c1abc1f74da964c1f7d` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:88` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:415 (`detach_all_this_recursive`)` | `translated` | `2b6a646ee82484a6d1901ea4425b0e32fe553827f5bea37b506ffe29d1aec192` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Path::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Path` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:168` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:428 (`detach_all_this_recursive_path`)` | `translated` | `731244b623f23a8870d011280a062add751607ce2c58be3261485bb61d488297` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Worn::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Worn` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:245` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:441 (`detach_all_this_recursive_worn`)` | `translated` | `d8424a48e64346edd683b7a68652dd5a0be615658f085b0390699ac7b4db5d2c` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:324` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:454 (`detach_all_this_recursive_attached`)` | `translated` | `f399592217e7a4638ea25d469681ab9cc58bf8a937af2bfff8895996f7bbc493` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs::DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached_AddRem::test` | `DetachAllThisRestrictionTests.DetachAllThis_Recursive_Attached_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachAllThisRestrictionTests.cs:421` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:467 (`detach_all_this_recursive_attached_removed`)` | `translated` | `507273250b932ff93fa34f87190ad1ab289edeed275be6d2259bab05c5b055d1` | | `LibreMetaverse.Tests/RLV/Restrictions/DetachRestrictionTests.cs::DetachRestrictionTests.Detach_Default::test` | `DetachRestrictionTests.Detach_Default` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachRestrictionTests.cs:12` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:182 (`detach_default`)` | `translated` | `8c5b165329ab6b46eb3ec7c9a07383e8ea1e54edf0882b43b14166d67cc45617` | | `LibreMetaverse.Tests/RLV/Restrictions/DetachRestrictionTests.cs::DetachRestrictionTests.Detach::test` | `DetachRestrictionTests.Detach` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachRestrictionTests.cs:33` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:188 (`detach_restricts_sender_prim`)` | `translated` | `50f5419aea3ea41aa9ebedd29202a549deb4ae6089c38965300a46fd5abf94bf` | | `LibreMetaverse.Tests/RLV/Restrictions/DetachRestrictionTests.cs::DetachRestrictionTests.Detach_AttachPoint::test` | `DetachRestrictionTests.Detach_AttachPoint` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachRestrictionTests.cs:55` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:196 (`detach_restricts_attachment_point`)` | `translated` | `a75fea134b92bca704b2e140135076d699d645c0d2854022d5b20d14be372589` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis::test` | `DetachThisRestrictionTests.DetachThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:473 (`detach_this`)` | `translated` | `bf6cdb48a5271d5116db4d0cbf12dbf63fc3cc93862bf718cf849d4abf9eb7cc` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_NotRecursive::test` | `DetachThisRestrictionTests.DetachThis_NotRecursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:82` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:486 (`detach_this_not_recursive`)` | `translated` | `a8452e2b0abcf82447394bae981a1d25677147fad17828d8f6d0705264efa429` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByPath::test` | `DetachThisRestrictionTests.DetachThis_ByPath` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:153` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:499 (`detach_this_by_path`)` | `translated` | `e6378e0db310005b8426cabd25969818ed36252e1995de97c6b29d5ab155ee58` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByRlvAttachmentPoint::test` | `DetachThisRestrictionTests.DetachThis_ByRlvAttachmentPoint` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:219` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:512 (`detach_this_by_attachment_point`)` | `translated` | `278e4cbf0f9999efec04fcabade19da97439cca732e0e7e3e3c004e232c27224` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByWornLayer::test` | `DetachThisRestrictionTests.DetachThis_ByWornLayer` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:297` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:525 (`detach_this_by_worn_layer`)` | `translated` | `66b7eba54e34ed58c59d594cdcb85d874c4db9bbbd871e5438c40fd3d47ad3f6` | -| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByWornLayer_AddRem::test` | `DetachThisRestrictionTests.DetachThis_ByWornLayer_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:371` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:538 (`detach_this_by_worn_layer_removed`)` | `translated` | `7424bf38db1abd74132618ee3bcf95a79c32942da26086760dc82355a815d37e` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis::test` | `DetachThisRestrictionTests.DetachThis` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:14` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:480 (`detach_this`)` | `translated` | `bf6cdb48a5271d5116db4d0cbf12dbf63fc3cc93862bf718cf849d4abf9eb7cc` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_NotRecursive::test` | `DetachThisRestrictionTests.DetachThis_NotRecursive` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:82` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:493 (`detach_this_not_recursive`)` | `translated` | `a8452e2b0abcf82447394bae981a1d25677147fad17828d8f6d0705264efa429` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByPath::test` | `DetachThisRestrictionTests.DetachThis_ByPath` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:153` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:506 (`detach_this_by_path`)` | `translated` | `e6378e0db310005b8426cabd25969818ed36252e1995de97c6b29d5ab155ee58` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByRlvAttachmentPoint::test` | `DetachThisRestrictionTests.DetachThis_ByRlvAttachmentPoint` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:219` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:519 (`detach_this_by_attachment_point`)` | `translated` | `278e4cbf0f9999efec04fcabade19da97439cca732e0e7e3e3c004e232c27224` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByWornLayer::test` | `DetachThisRestrictionTests.DetachThis_ByWornLayer` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:297` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:532 (`detach_this_by_worn_layer`)` | `translated` | `66b7eba54e34ed58c59d594cdcb85d874c4db9bbbd871e5438c40fd3d47ad3f6` | +| `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs::DetachThisRestrictionTests.DetachThis_ByWornLayer_AddRem::test` | `DetachThisRestrictionTests.DetachThis_ByWornLayer_AddRem` | `` | `LibreMetaverse.Tests/RLV/Restrictions/DetachThisRestrictionTests.cs:371` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs, LibreMetaverse.Tests/RLV/SampleInventoryTree.cs` | `tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs:545 (`detach_this_by_worn_layer_removed`)` | `translated` | `7424bf38db1abd74132618ee3bcf95a79c32942da26086760dc82355a815d37e` | | `LibreMetaverse.Tests/RLV/Restrictions/EditAttachRestrictionTests.cs::EditAttachRestrictionTests.CanEdit_Attachment::test` | `EditAttachRestrictionTests.CanEdit_Attachment` | `` | `LibreMetaverse.Tests/RLV/Restrictions/EditAttachRestrictionTests.cs:11` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:251 (`edit_attachment_restriction`)` | `translated` | `22bc9e267e270c2c762561851249af4ef64c0b5bd288950a95b1b636b2975b6e` | | `LibreMetaverse.Tests/RLV/Restrictions/EditRestrictionTests.cs::EditRestrictionTests.CanEditFolderNameSpecifiesToAddInsteadOfReplace::test` | `EditRestrictionTests.CanEditFolderNameSpecifiesToAddInsteadOfReplace` | `` | `LibreMetaverse.Tests/RLV/Restrictions/EditRestrictionTests.cs:12` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:257 (`edit_everywhere_restriction`)` | `translated` | `6d82d0869826bbc935d89967445dfc991619bdaf3fa631550c5d3f91a13c4935` | | `LibreMetaverse.Tests/RLV/Restrictions/EditWorldRestrictionTests.cs::EditWorldRestrictionTests.CanEdit_World::test` | `EditWorldRestrictionTests.CanEdit_World` | `` | `LibreMetaverse.Tests/RLV/Restrictions/EditWorldRestrictionTests.cs:11` | `` | `LibreMetaverse.Tests/RLV/RlvTestBase.cs` | `tests/compat/tests/rlv_attachment_restrictions_semantics.rs:263 (`edit_world_restriction`)` | `translated` | `1f335a2c81735bf58294b97ceedfd85192113e69c78a38804f0db923ce7a0241` | diff --git a/tests/api-compile/Cargo.lock b/tests/api-compile/Cargo.lock index a3f27b9..531009e 100644 --- a/tests/api-compile/Cargo.lock +++ b/tests/api-compile/Cargo.lock @@ -805,9 +805,11 @@ dependencies = [ name = "libremetaverse-rendering-mesh-foundry" version = "0.0.1" dependencies = [ + "flate2", "libremetaverse", "libremetaverse-imaging", "libremetaverse-prim-mesher", + "libremetaverse-rendering-simple", "libremetaverse-structured-data", "libremetaverse-types", ] @@ -818,6 +820,7 @@ version = "0.0.1" dependencies = [ "libremetaverse", "libremetaverse-imaging", + "libremetaverse-prim-mesher", "libremetaverse-types", ] diff --git a/tests/compat/src/rlv_support.rs b/tests/compat/src/rlv_support.rs index 8ae1b3a..569ab11 100644 --- a/tests/compat/src/rlv_support.rs +++ b/tests/compat/src/rlv_support.rs @@ -305,7 +305,7 @@ impl IRlvQueryCallbacks for RecordingQueries { &self, _: CancellationToken, ) -> QueryFuture<'_, (bool, Option)> { - let value = self.0.lock().unwrap().inventory_map.take(); + let value = self.0.lock().unwrap().inventory_map.clone(); Box::pin(async move { Ok((value.is_some(), value)) }) } @@ -326,7 +326,7 @@ impl RlvHarness { /// /// # Errors /// - /// Returns the standardized constructor error while the public RLV service is a shim. + /// Returns an error if the native RLV service cannot be initialized. pub fn new() -> Result { let action_callbacks = RecordingActions::default(); let query_callbacks = RecordingQueries::default(); diff --git a/tests/compat/tests/rlv_detach_this_commands_semantics.rs b/tests/compat/tests/rlv_detach_this_commands_semantics.rs index 5e08c89..e48a964 100644 --- a/tests/compat/tests/rlv_detach_this_commands_semantics.rs +++ b/tests/compat/tests/rlv_detach_this_commands_semantics.rs @@ -59,7 +59,11 @@ fn run(command: &str, setup: Setup, sender_item: bool) -> Vec { ); } match setup { - Setup::NoStripItem | Setup::PointNoStrip | Setup::WearableNoStrip => { + Setup::NoStripItem + | Setup::PointNoStrip + | Setup::PointLink + | Setup::WearableNoStrip + | Setup::WearableLink => { tree.happy_shirt.set_name("nostrip Happy Shirt".into()); } _ => {} diff --git a/tests/compat/tests/rlv_notify_semantics.rs b/tests/compat/tests/rlv_notify_semantics.rs index d6884b8..2ea0371 100644 --- a/tests/compat/tests/rlv_notify_semantics.rs +++ b/tests/compat/tests/rlv_notify_semantics.rs @@ -259,8 +259,9 @@ fn notify_inventory_offer() { } fn report_sit_and_unsit(h: &RlvHarness) { - block_on(h.service.report_sit(Some(Some(guid(ITEM_3))), None)).unwrap(); - block_on(h.service.report_unsit(Some(Some(guid(ITEM_3))), None)).unwrap(); + let sit_target = guid("11111111-1111-4111-8111-111111111111"); + block_on(h.service.report_sit(Some(Some(sit_target)), None)).unwrap(); + block_on(h.service.report_unsit(Some(Some(sit_target)), None)).unwrap(); block_on(h.service.report_sit(None, None)).unwrap(); block_on(h.service.report_unsit(None, None)).unwrap(); } diff --git a/tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs b/tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs index 78db9b4..d532bd0 100644 --- a/tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs +++ b/tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs @@ -15,6 +15,7 @@ enum Setup { PartySpine, PartyChin, BusinessPelvis, + BusinessPelvisPrim1, BusinessChin, BusinessChest, RetroPants, @@ -65,6 +66,12 @@ fn apply_setup(tree: &mut SampleInventoryTree, setup: Setup) { tree.business_pants .set_attached_prim_id(Some(Some(guid(PRIM_3)))); } + Setup::BusinessPelvisPrim1 => { + tree.business_pants + .set_attached_to(Some(Some(RlvAttachmentPoint::Pelvis))); + tree.business_pants + .set_attached_prim_id(Some(Some(guid(PRIM_1)))); + } Setup::BusinessChin => { tree.business_pants .set_attached_to(Some(Some(RlvAttachmentPoint::Chin))); @@ -333,7 +340,7 @@ fn attach_this_not_recursive() { assert!(folder_case_matches( "@attachthis=n", Some((PRIM_1, "Business Pants (Pelvis)")), - Setup::BusinessPelvis, + Setup::BusinessPelvisPrim1, LockMode::Attach, CLOTHING_LOCKED, &[FolderKey::Clothing], diff --git a/tests/upstream-tests.json b/tests/upstream-tests.json index 08b5a8e..796aa87 100644 --- a/tests/upstream-tests.json +++ b/tests/upstream-tests.json @@ -11900,7 +11900,7 @@ "rust_body_sha256": "1148507a7d321c499350d35dc9df206d70c08568241e51598d2468cedc07a1eb", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 101, + "rust_line": 105, "rust_test": "detach_this_default", "semantic_review": "reviewed" }, @@ -11922,7 +11922,7 @@ "rust_body_sha256": "1d403149f99f166bc637a94e6715d0986591ceca5d130c268dfd37d33642f99a", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 109, + "rust_line": 113, "rust_test": "detach_this_honors_item_nostrip", "semantic_review": "reviewed" }, @@ -11944,7 +11944,7 @@ "rust_body_sha256": "7debcecc7dd1f41cf84d28147c76211484ca37793196b434ae3ce4ab2150b2c8", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 117, + "rust_line": 121, "rust_test": "detach_this_honors_folder_nostrip", "semantic_review": "reviewed" }, @@ -11966,7 +11966,7 @@ "rust_body_sha256": "29cb8259d99cd8ed894c6e93c92debcb0d39db88d8e21709317a7a4521f23955", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 122, + "rust_line": 126, "rust_test": "detach_this_honors_folder_link_exception", "semantic_review": "reviewed" }, @@ -11988,7 +11988,7 @@ "rust_body_sha256": "fcc5c16800d9404260633f92f92c8baed9ee1b641baa8a615b2372164e11933e", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 130, + "rust_line": 134, "rust_test": "detach_this_by_id", "semantic_review": "reviewed" }, @@ -12010,7 +12010,7 @@ "rust_body_sha256": "6cbd74f2fc893e4fee28eb5c4878d2d600933a2faf56c2f86001e8aa35a8e341", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 138, + "rust_line": 142, "rust_test": "detach_this_by_id_honors_item_nostrip", "semantic_review": "reviewed" }, @@ -12032,7 +12032,7 @@ "rust_body_sha256": "24f2e0b3b48b8e51c3568c634f9c3392a8fd76c2ce35a60ff9dbd61eae00af14", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 150, + "rust_line": 154, "rust_test": "detach_this_by_id_honors_folder_nostrip", "semantic_review": "reviewed" }, @@ -12054,7 +12054,7 @@ "rust_body_sha256": "126011ba868d7ddfc94801cc068ae721fae3f6571e28ae867b8b9b670bb4f6bb", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 162, + "rust_line": 166, "rust_test": "detach_this_by_id_honors_link_exception", "semantic_review": "reviewed" }, @@ -12076,7 +12076,7 @@ "rust_body_sha256": "cf4ccc60fab1762ad2a094fe236c6e807c128ed1ee63b2aa49611145697ae5a4", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 174, + "rust_line": 178, "rust_test": "detach_this_by_attachment_point", "semantic_review": "reviewed" }, @@ -12098,7 +12098,7 @@ "rust_body_sha256": "b97672d0964fbec6954a7f7925e5a1966a699b328232e592dbe829937a57324a", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 182, + "rust_line": 186, "rust_test": "detach_this_by_attachment_point_honors_nostrip", "semantic_review": "reviewed" }, @@ -12120,7 +12120,7 @@ "rust_body_sha256": "6d8cf3da9722f153760afa215fad17707633b67fcdfb86db26afef83a157aac5", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 190, + "rust_line": 194, "rust_test": "detach_this_by_attachment_point_honors_link_exception", "semantic_review": "reviewed" }, @@ -12142,7 +12142,7 @@ "rust_body_sha256": "b3925bd0472eafcf348556d35eb1086c749205e25436f44e894f76fb56f1b15a", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 198, + "rust_line": 202, "rust_test": "detach_this_by_wearable", "semantic_review": "reviewed" }, @@ -12164,7 +12164,7 @@ "rust_body_sha256": "e133c6c91d06c50368bf0862befc0e488353c0cd5ab560f08e60bfde25157866", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 206, + "rust_line": 210, "rust_test": "detach_this_by_wearable_honors_nostrip", "semantic_review": "reviewed" }, @@ -12186,7 +12186,7 @@ "rust_body_sha256": "8a57fa87c7d565531dc2edd65891daa4ae19f573a37a0a97534852312e82ea3b", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 214, + "rust_line": 218, "rust_test": "detach_this_by_wearable_honors_link_exception", "semantic_review": "reviewed" }, @@ -12208,7 +12208,7 @@ "rust_body_sha256": "dddede32d4d5f45c9efc732a2fe42fc1814b9b46d89275a7619f0cc6ec61e406", "status": "translated", "rust_file": "tests/compat/tests/rlv_detach_this_commands_semantics.rs", - "rust_line": 222, + "rust_line": 226, "rust_test": "detach_this_by_wearable_skips_private_folder", "semantic_review": "reviewed" }, @@ -17156,7 +17156,7 @@ "rust_body_sha256": "769f2aa5d98cfb4cc22c7f65c38b75cf708b7082d35527a19cde78eafeec59da", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 268, + "rust_line": 269, "rust_test": "notify_sit_and_stand_legal", "semantic_review": "reviewed" }, @@ -17177,7 +17177,7 @@ "rust_body_sha256": "36227d1e169d8a05dd85b0e1b8c517501d0e4256177076c368e031509404039d", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 292, + "rust_line": 293, "rust_test": "notify_sit_and_stand_illegal", "semantic_review": "reviewed" }, @@ -17198,7 +17198,7 @@ "rust_body_sha256": "6bd2017964066fb58e6e5178dbca0da1859f4d481480612fd1675ab197f7051a", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 316, + "rust_line": 317, "rust_test": "notify_wear_legal", "semantic_review": "reviewed" }, @@ -17219,7 +17219,7 @@ "rust_body_sha256": "bf7665ce0b959e17152a8b1087c5316579dbc2f124793108a80c225d4b2fa62c", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 341, + "rust_line": 342, "rust_test": "notify_wear_illegal", "semantic_review": "reviewed" }, @@ -17240,7 +17240,7 @@ "rust_body_sha256": "85722dfdefe66adef5b82132c847e29a815680e46742292a24ab5204f931fdab", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 357, + "rust_line": 358, "rust_test": "notify_unwear_legal", "semantic_review": "reviewed" }, @@ -17261,7 +17261,7 @@ "rust_body_sha256": "1721e0d6165378fbc78d361d79ca01d1408a9bd011ec3c0e84b58f3a28802a08", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 388, + "rust_line": 389, "rust_test": "notify_unwear_illegal", "semantic_review": "reviewed" }, @@ -17282,7 +17282,7 @@ "rust_body_sha256": "676207d20a16f1037f5dea5e8daa2767d9670c7dfb0a16b7810c577893f69718", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 407, + "rust_line": 408, "rust_test": "notify_attached_legal", "semantic_review": "reviewed" }, @@ -17303,7 +17303,7 @@ "rust_body_sha256": "7dc3a16a22ff1c97f0192b5018029ccecb20b3953eb30ec03bdf2c6bfc8709db", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 434, + "rust_line": 435, "rust_test": "notify_attached_illegal", "semantic_review": "reviewed" }, @@ -17324,7 +17324,7 @@ "rust_body_sha256": "32c6c8f21eb3c40a15f41820f925f70279959a48513069e5a348d63e72867c21", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 465, + "rust_line": 466, "rust_test": "notify_detached_legal", "semantic_review": "reviewed" }, @@ -17345,7 +17345,7 @@ "rust_body_sha256": "408a8cdba541b2a9aa12eafdcb1f8cb78dcfdac240ad17380588a8407336e40d", "status": "translated", "rust_file": "tests/compat/tests/rlv_notify_semantics.rs", - "rust_line": 482, + "rust_line": 483, "rust_test": "notify_detached_illegal", "semantic_review": "reviewed" }, @@ -17623,7 +17623,7 @@ "rust_body_sha256": "26f6bebf30b4bef1250a2d48d7d8661037d80430c7bb71e07ac8ca1f50337ef2", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 239, + "rust_line": 246, "rust_test": "attach_all_this", "semantic_review": "reviewed" }, @@ -17645,7 +17645,7 @@ "rust_body_sha256": "cb3ab0fdc028daab4cd1bca0574bab9def01d4f3b8479f69acd1bb82b3aa57d4", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 252, + "rust_line": 259, "rust_test": "attach_all_this_recursive", "semantic_review": "reviewed" }, @@ -17667,7 +17667,7 @@ "rust_body_sha256": "a2df9e716a57eb3e60f2ae858b999cefa4fcde91e8b017d2bca0c1f5eeb111b4", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 265, + "rust_line": 272, "rust_test": "attach_all_this_recursive_path", "semantic_review": "reviewed" }, @@ -17689,7 +17689,7 @@ "rust_body_sha256": "6fb5793c8111be7dc2ea5a414d4d03e62ad79530f15649c6552e19e3dc500219", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 278, + "rust_line": 285, "rust_test": "attach_all_this_recursive_worn", "semantic_review": "reviewed" }, @@ -17711,7 +17711,7 @@ "rust_body_sha256": "f34d39c35611a37ca12932375d4011aa163d14efce27f8971e01443eb714e2b1", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 291, + "rust_line": 298, "rust_test": "attach_all_this_recursive_attached", "semantic_review": "reviewed" }, @@ -17733,7 +17733,7 @@ "rust_body_sha256": "31ef6f16f11e28ddb7f71dd7db713d272e3cb87a4757309f34a943244f457854", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 304, + "rust_line": 311, "rust_test": "attach_all_this_recursive_attached_removed", "semantic_review": "reviewed" }, @@ -17755,7 +17755,7 @@ "rust_body_sha256": "2ec55164960b661dcb4e403e6972c4d0132c2af8de6ba5c42dc4a3c47392792b", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 317, + "rust_line": 324, "rust_test": "attach_this", "semantic_review": "reviewed" }, @@ -17774,10 +17774,10 @@ "LibreMetaverse.Tests/RLV/RlvTestBase.cs", "LibreMetaverse.Tests/RLV/SampleInventoryTree.cs" ], - "rust_body_sha256": "f39795213dc5919346a2eea6d63a8609679edbfad5adb34b9ce7a5423e490804", + "rust_body_sha256": "261c53f3ef86afef8a37fdf33721e64691bc105b5bc325af2e2655cec7b71ea1", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 330, + "rust_line": 337, "rust_test": "attach_this_not_recursive", "semantic_review": "reviewed" }, @@ -17799,7 +17799,7 @@ "rust_body_sha256": "60fd1fc734e58ff5f86edf47431142938e16676bacb8b486842cd120a749b1d6", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 343, + "rust_line": 350, "rust_test": "attach_this_by_path", "semantic_review": "reviewed" }, @@ -17821,7 +17821,7 @@ "rust_body_sha256": "d7d45305165efa11603b8ccd5351c5fb3a56b99514f9e19b9a918ab15cd1bec3", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 356, + "rust_line": 363, "rust_test": "attach_this_by_attachment_point", "semantic_review": "reviewed" }, @@ -17843,7 +17843,7 @@ "rust_body_sha256": "68015385d285c1c52767b9c1f80479e48a37bb173782cab56e06eb0644160586", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 369, + "rust_line": 376, "rust_test": "attach_this_by_worn_layer", "semantic_review": "reviewed" }, @@ -17865,7 +17865,7 @@ "rust_body_sha256": "19dc26531ca7ef2e3588b5af89093c0c0fff30609e60279c10d6122f600a1903", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 382, + "rust_line": 389, "rust_test": "attach_this_by_worn_layer_removed", "semantic_review": "reviewed" }, @@ -17971,7 +17971,7 @@ "rust_body_sha256": "5dc59c18fa28231427cfecc55befca995b4eb61dfa3ecc08198742ce891f2ac4", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 395, + "rust_line": 402, "rust_test": "detach_all_this", "semantic_review": "reviewed" }, @@ -17993,7 +17993,7 @@ "rust_body_sha256": "629d6db121fc2d74ca2b5236a2dba218771cfc127f1ffb9cfef19b765e3a4a18", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 408, + "rust_line": 415, "rust_test": "detach_all_this_recursive", "semantic_review": "reviewed" }, @@ -18015,7 +18015,7 @@ "rust_body_sha256": "1b8915abf9287e992b43cd8187f57a5fa6913a9c2a74f8d44fe55e5bef9465fc", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 421, + "rust_line": 428, "rust_test": "detach_all_this_recursive_path", "semantic_review": "reviewed" }, @@ -18037,7 +18037,7 @@ "rust_body_sha256": "f9c2371a52accbfc21335414e1e9fa045c5be164d736c2e42d65e0481686ed2e", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 434, + "rust_line": 441, "rust_test": "detach_all_this_recursive_worn", "semantic_review": "reviewed" }, @@ -18059,7 +18059,7 @@ "rust_body_sha256": "e6c796e8f6f2753463ed0474362f5ac67625d740a7c78516b46f643f677cfd91", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 447, + "rust_line": 454, "rust_test": "detach_all_this_recursive_attached", "semantic_review": "reviewed" }, @@ -18081,7 +18081,7 @@ "rust_body_sha256": "1fa01c5c84056ced682d1278dbabc3c949eae0eab1ff3add5abc42af4d964ae4", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 460, + "rust_line": 467, "rust_test": "detach_all_this_recursive_attached_removed", "semantic_review": "reviewed" }, @@ -18166,7 +18166,7 @@ "rust_body_sha256": "2817a30621b0ba0ab6733872f37b26a6d27ca078e4543ce5f5aa8e86c0d86b37", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 473, + "rust_line": 480, "rust_test": "detach_this", "semantic_review": "reviewed" }, @@ -18188,7 +18188,7 @@ "rust_body_sha256": "a3964bcb384e0129cd50248e7dfa99d6bdeb762676b87682e3033f05a1e549c1", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 486, + "rust_line": 493, "rust_test": "detach_this_not_recursive", "semantic_review": "reviewed" }, @@ -18210,7 +18210,7 @@ "rust_body_sha256": "40608dca7fadcf24de403fdf2d0ee788ba1df14911e031f0ecc9293f5751f6ed", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 499, + "rust_line": 506, "rust_test": "detach_this_by_path", "semantic_review": "reviewed" }, @@ -18232,7 +18232,7 @@ "rust_body_sha256": "b8b242d7645cb0533e8d96ca678cfe0ea599d80b7dca9f43022cb32088cb5a17", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 512, + "rust_line": 519, "rust_test": "detach_this_by_attachment_point", "semantic_review": "reviewed" }, @@ -18254,7 +18254,7 @@ "rust_body_sha256": "ee6d3cfd784fde497f9550b30de25fc57d0c3c92f89fa427df8752511ca117eb", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 525, + "rust_line": 532, "rust_test": "detach_this_by_worn_layer", "semantic_review": "reviewed" }, @@ -18276,7 +18276,7 @@ "rust_body_sha256": "87955836e1ae1a1fe58b072a99979c594ec72261ea7678559bb5f12a8a4ccfd2", "status": "translated", "rust_file": "tests/compat/tests/rlv_recursive_folder_restrictions_semantics.rs", - "rust_line": 538, + "rust_line": 545, "rust_test": "detach_this_by_worn_layer_removed", "semantic_review": "reviewed" }, diff --git a/tools/check_milestone_10_issue_80.py b/tools/check_milestone_10_issue_80.py new file mode 100644 index 0000000..141a8c9 --- /dev/null +++ b/tools/check_milestone_10_issue_80.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Audit issue 80's native RLV callback and service orchestration boundary.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import generate_api_shims +import generate_surface + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "crates" / "libremetaverse-rlv" / "src" / "service.rs" +STATE = ROOT / "crates" / "libremetaverse-rlv" / "src" / "state.rs" +GENERATED = ROOT / "crates" / "libremetaverse-rlv" / "src" / "generated.rs" +SUPPORT = ROOT / "tests" / "compat" / "src" / "rlv_support.rs" +QUERY_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_query_basics_semantics.rs" +ACTION_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_attach_commands_semantics.rs" +NOTIFY_TESTS = ROOT / "tests" / "compat" / "tests" / "rlv_notify_semantics.rs" +DOC = ROOT / "crates" / "libremetaverse-rlv" / "README.md" +WORKFLOW = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" +CATALOG = ROOT / "api" / "public-api.json" +STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(") + +TYPES = { + "T:LibreMetaverse.RLV.AttachmentRequest": "crate::service::AttachmentRequest", + "T:LibreMetaverse.RLV.RlvActionCallbacksDefault": + "crate::service::RlvActionCallbacksDefault", + "T:LibreMetaverse.RLV.RlvCallbacksDefault": "crate::service::RlvCallbacksDefault", + "T:LibreMetaverse.RLV.RlvCommandProcessor": "crate::service::RlvCommandProcessor", + "T:LibreMetaverse.RLV.RlvService": "crate::service::RlvService", +} + + +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 catalog_members() -> set[str]: + catalog = json.loads(CATALOG.read_text()) + assembly = next( + value for value in catalog["assemblies"] + if value["identity"]["name"] == "LibreMetaverse.RLV" + ) + return { + member["doc_id"] + for api_type in assembly["types"] + if api_type["doc_id"] in TYPES + for member in api_type["members"] + } + + +def generated_type_block(text: str, doc_id: str) -> str: + marker = f"/// C# type: `{doc_id}`." + start = text.find(marker) + if start < 0: + raise SystemExit(f"generated declaration is missing for {doc_id}") + next_type = text.find("/// C# type:", start + len(marker)) + return text[start:] if next_type < 0 else text[start:next_type] + + +def main() -> None: + for api_type, declaration in TYPES.items(): + if generate_api_shims.NATIVE_TYPES.get(api_type) != declaration: + raise SystemExit(f"issue 80 native type mapping is missing for {api_type}") + members = catalog_members() + if len(members) != 53: + raise SystemExit(f"issue 80 expected 53 mapped members, found {len(members)}") + + source = SOURCE.read_text() + if STUB_RE.search(source): + raise SystemExit("issue 80 owned Rust stubs remain in service.rs") + if re.search(r"\b(?:Mutex|RwLock|read_lock|write_lock)\b", source): + raise SystemExit("service.rs must not acquire internal locks around host callbacks") + generated = GENERATED.read_text() + for api_type in TYPES: + if STUB_RE.search(generated_type_block(generated, api_type)): + raise SystemExit(f"issue 80 owned generated stubs remain for {api_type}") + if "pub trait IRlvActionCallbacks: Send + Sync" not in generated: + raise SystemExit("action callbacks are not thread-safe") + if "pub trait IRlvQueryCallbacks: Send + Sync" not in generated: + raise SystemExit("query callbacks are not thread-safe") + + catalog = json.loads((ROOT / "tests" / "upstream-tests.json").read_text()) + rlv_cases = [ + case for case in catalog["tests"] + if case.get("rust_file", "").startswith("tests/compat/tests/rlv_") + ] + if len(rlv_cases) != 616 or len({case["rust_file"] for case in rlv_cases}) != 24: + raise SystemExit("issue 80 RLV parity catalog is not the complete 616-case slice") + markers = generate_surface.scan_parity_markers(ROOT) + evidence_keys = ("status", "rust_file", "rust_line", "rust_test", "rust_body_sha256") + for case in rlv_cases: + marker = markers.get(case["id"], []) + if len(marker) != 1 or any(marker[0][key] != case[key] for key in evidence_keys): + raise SystemExit(f"issue 80 parity metadata is stale for {case['id']}") + + require_markers(SOURCE, ( + "MAX_RLV_MESSAGE_BYTES", "MAX_RLV_COMMANDS", "pub struct AttachmentRequest", + "pub struct RlvActionCallbacksDefault", "pub struct RlvCallbacksDefault", + "pub struct RlvCommandProcessor", "pub struct RlvService", + "process_instant_message", "process_message", "process_restriction", + "send_restriction_notification", "send_notification", "refresh_inventory", + "report_inventory_offer_accepted", "report_item_attached", + "report_send_public_message", "report_sit", "throw_if_cancellation_requested", + "default_callbacks_observe_cancellation_without_side_effects", + "attachment_request_hash_is_stable_and_value_based", + )) + require_markers(STATE, ( + "set_action_callbacks", "notify_removed_restriction", + "let removed = {", "let callbacks = read_lock", ".send_reply(", + )) + require_markers(SUPPORT, ( + "struct RecordingActions", "struct RecordingQueries", "struct RlvHarness", + "inventory_map.clone()", "RlvService::new", + )) + require_markers(QUERY_TESTS, ( + "manual_blacklist_all", "manual_version", "get_environment_known_setting", + "get_status_all_senders", "get_current_sit_id", + )) + require_markers(ACTION_TESTS, ( + "attach_this_by_id", "attach_all_this_recursive", + "attach_over_or_replace_uses_position_from_folder_name", + )) + require_markers(NOTIFY_TESTS, ( + "notify_every_restriction", "notify_multiple_channels_filtered", + "notify_inventory_offer", "notify_sit_and_stand_legal", + )) + require_markers(DOC, ( + "Callback and service contract", "Send + Sync", + "after releasing every manager lock", "completed inventory and agent", + "616", "ubuntu-latest", + )) + require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_80.py",)) + print( + "issue 80 audit: native callbacks, typed actions and queries, bounded service " + "dispatch, inventory/agent adapters, cancellation, post-lock notifications, " + "recording-fake workflows, compatibility evidence, and docs are present" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index 2234a98..d8f7042 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -38,6 +38,11 @@ TARGETS = { # implementations. The generated module keeps catalog markers and re-exports # the hand-written type so coverage remains deterministic. NATIVE_TYPES = { + "T:LibreMetaverse.RLV.AttachmentRequest": "crate::service::AttachmentRequest", + "T:LibreMetaverse.RLV.RlvActionCallbacksDefault": "crate::service::RlvActionCallbacksDefault", + "T:LibreMetaverse.RLV.RlvCallbacksDefault": "crate::service::RlvCallbacksDefault", + "T:LibreMetaverse.RLV.RlvCommandProcessor": "crate::service::RlvCommandProcessor", + "T:LibreMetaverse.RLV.RlvService": "crate::service::RlvService", "T:LibreMetaverse.AISResponseMeta": "crate::inventory_ais::AISResponseMeta", "T:LibreMetaverse.AccountLevelBenefits": "crate::login::AccountLevelBenefits", "T:LibreMetaverse.AgentDisplayName": "crate::message_decoder::AgentDisplayName", @@ -2817,6 +2822,8 @@ TRAIT_SUPERTRAITS = { "T:LibreMetaverse.IBakingTextureProvider": "std::any::Any + Send + Sync", "T:LibreMetaverse.Interfaces.IMessage": "std::any::Any", "T:LibreMetaverse.Appearance.ICurrentOutfitPolicy": "Send + Sync", + "T:LibreMetaverse.RLV.IRlvActionCallbacks": "Send + Sync", + "T:LibreMetaverse.RLV.IRlvQueryCallbacks": "Send + Sync", } CORE_NAMESPACES = ( "LibreMetaverse.Http",