Implement native RLV state and permissions (#79)
Some checks failed
Native code generation / deterministic (push) Failing after 2m1s
Imaging and meshing gate / native (push) Successful in 5m22s
JPEG 2000 feature / linux (push) Successful in 2m45s
Native Rust workspace compile / compile (push) Failing after 6m12s
Skia feature / linux (push) Successful in 30m44s
Some checks failed
Native code generation / deterministic (push) Failing after 2m1s
Imaging and meshing gate / native (push) Successful in 5m22s
JPEG 2000 feature / linux (push) Successful in 2m45s
Native Rust workspace compile / compile (push) Failing after 6m12s
Skia feature / linux (push) Successful in 30m44s
This commit is contained in:
@@ -5,7 +5,7 @@ edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Bounded native RLV protocol parsing and compatibility types for MetaCrate"
|
||||
description = "Bounded native RLV protocol, state, inventory, lock, camera, and permission layer"
|
||||
|
||||
[dependencies]
|
||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Native RLV protocol layer
|
||||
|
||||
`libremetaverse-rlv` implements the side-effect-free protocol boundary for the
|
||||
`libremetaverse-rlv` implements the protocol and pure state boundary for the
|
||||
Restrained Love Viewer support in the pinned LibreMetaverse snapshot. The
|
||||
parser converts one bounded chat message into typed commands. It does not
|
||||
mutate restrictions, move the camera, touch inventory, send replies, or invoke
|
||||
callbacks; those stateful responsibilities are separate milestone work that
|
||||
consumes this layer.
|
||||
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.
|
||||
|
||||
## Message contract
|
||||
|
||||
@@ -57,6 +59,33 @@ insertion mutations of actions, restrictions, queries, UUIDs, aliases, and
|
||||
multi-command messages. Every mutation must return a value or a positioned
|
||||
error without panicking. The limits apply before large parser allocations.
|
||||
|
||||
## State, inventory, and permission contract
|
||||
|
||||
`RlvSharedFolder` and `RlvInventoryItem` preserve the reference semantics of
|
||||
their C# counterparts. `InventoryMap` walks a bounded shared-inventory tree and
|
||||
publishes immutable dictionary/list membership snapshots. Path lookup handles
|
||||
hidden/private prefixes, exact names containing forward slashes, and the
|
||||
reference implementation's longest matching segment rule. Lookup by item,
|
||||
attached prim, attachment point, and wearable type does not call a client or
|
||||
inventory service.
|
||||
|
||||
`RlvRestrictionManager` deduplicates exact restrictions, retains deterministic
|
||||
insertion order, removes all state for selected object sources, and rebuilds
|
||||
immutable locked-folder snapshots when either restrictions or inventory
|
||||
changes. Recursive and non-recursive attach/detach locks support sender-item,
|
||||
attachment, wearable, and path targets plus their exception variants. Update
|
||||
handlers run only after manager locks are released, so callbacks can safely
|
||||
query the manager again. Poisoned synchronization primitives recover their
|
||||
owned state instead of making later reads fail.
|
||||
|
||||
`RlvPermissionsService` evaluates simple restrictions, secure and explicit
|
||||
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.
|
||||
|
||||
## Reproducible verification
|
||||
|
||||
Run the issue-owned checks with one build job:
|
||||
@@ -64,9 +93,11 @@ Run the issue-owned checks with one build job:
|
||||
```sh
|
||||
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 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
|
||||
```
|
||||
|
||||
These commands are cross-platform. The Gitea workflow runs the audit and
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,9 @@ extern crate self as libremetaverse_rlv;
|
||||
|
||||
mod generated;
|
||||
mod protocol;
|
||||
mod state;
|
||||
|
||||
pub use generated::*;
|
||||
pub use libremetaverse_types::Error;
|
||||
pub use protocol::*;
|
||||
pub use state::*;
|
||||
|
||||
2174
crates/libremetaverse-rlv/src/state.rs
Normal file
2174
crates/libremetaverse-rlv/src/state.rs
Normal file
File diff suppressed because it is too large
Load Diff
602
crates/libremetaverse-rlv/tests/state_semantics.rs
Normal file
602
crates/libremetaverse-rlv/tests/state_semantics.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
#![allow(clippy::float_cmp)] // Protocol fixture values are exactly representable.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::thread;
|
||||
|
||||
use libremetaverse_rlv::{
|
||||
CameraSettings, InventoryMap, RlvAttachmentPoint, RlvBlacklist, RlvInventoryItem,
|
||||
RlvPermissionsService, RlvPermissionsServiceHoverTextLocation,
|
||||
RlvPermissionsServiceObjectLocation, RlvPermissionsServiceTouchLocation, RlvRestriction,
|
||||
RlvRestrictionManager, RlvRestrictionType, RlvSharedFolder, RlvValue, RlvWearableType,
|
||||
};
|
||||
use libremetaverse_types::compat::Guid;
|
||||
|
||||
fn guid(byte: u8) -> Guid {
|
||||
Guid([byte; 16])
|
||||
}
|
||||
|
||||
fn restriction(behavior: RlvRestrictionType, sender: u8, values: Vec<RlvValue>) -> RlvRestriction {
|
||||
RlvRestriction::from_values(behavior, guid(sender), format!("sender-{sender}"), values)
|
||||
}
|
||||
|
||||
fn poll_ready<F: Future>(future: F) -> F::Output {
|
||||
let waker = Waker::noop();
|
||||
let mut context = Context::from_waker(waker);
|
||||
let mut future = std::pin::pin!(future);
|
||||
match future.as_mut().poll(&mut context) {
|
||||
Poll::Ready(output) => output,
|
||||
Poll::Pending => panic!("pure state future unexpectedly yielded"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_deduplicates_updates_and_dispatches_after_unlocking() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
let events = Arc::new(Mutex::new(Vec::new()));
|
||||
let captured = Arc::clone(&events);
|
||||
let callback_manager = manager.clone();
|
||||
let _subscription = manager.subscribe_restriction_updated(Some(Arc::new(move |event| {
|
||||
// A re-entrant read would deadlock if callbacks ran under the state lock.
|
||||
let count = callback_manager
|
||||
.find_restrictions(None, None)
|
||||
.unwrap()
|
||||
.len();
|
||||
captured
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((event.is_new(), event.is_deleted(), count));
|
||||
})));
|
||||
|
||||
let fly = restriction(RlvRestrictionType::Fly, 1, vec![]);
|
||||
assert!(manager.add_restriction(fly.clone()).unwrap());
|
||||
assert!(!manager.add_restriction(fly.clone()).unwrap());
|
||||
assert_eq!(
|
||||
manager.get_tracked_prim_ids().unwrap().collect::<Vec<_>>(),
|
||||
[guid(1)]
|
||||
);
|
||||
assert!(manager.remove_restriction(&fly));
|
||||
assert!(!manager.remove_restriction(&fly));
|
||||
assert_eq!(
|
||||
*events.lock().unwrap(),
|
||||
[(true, false, 1), (false, true, 0)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_handles_concurrent_independent_sources_deterministically() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
let mut workers = Vec::new();
|
||||
for sender in 1..=24 {
|
||||
let worker_manager = manager.clone();
|
||||
workers.push(thread::spawn(move || {
|
||||
worker_manager
|
||||
.add_restriction(restriction(RlvRestrictionType::Jump, sender, vec![]))
|
||||
.unwrap()
|
||||
}));
|
||||
}
|
||||
for worker in workers {
|
||||
assert!(worker.join().unwrap());
|
||||
}
|
||||
|
||||
let restrictions = manager
|
||||
.get_restrictions_by_type(RlvRestrictionType::Jump)
|
||||
.unwrap();
|
||||
assert_eq!(restrictions.len(), 24);
|
||||
let mut tracked = manager.get_tracked_prim_ids().unwrap().collect::<Vec<_>>();
|
||||
tracked.sort_by_key(|value| value.0);
|
||||
assert_eq!(tracked, (1..=24).map(guid).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_lock_rebuilds_cannot_publish_a_stale_revision() {
|
||||
let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
|
||||
let first = root.add_child(guid(2), "first".into()).unwrap();
|
||||
let second = root.add_child(guid(3), "second".into()).unwrap();
|
||||
first
|
||||
.add_item(
|
||||
guid(4),
|
||||
"first attachment".into(),
|
||||
false,
|
||||
None,
|
||||
Some(Some(guid(40))),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
second
|
||||
.add_item(
|
||||
guid(5),
|
||||
"second attachment".into(),
|
||||
false,
|
||||
None,
|
||||
Some(Some(guid(50))),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let manager = RlvRestrictionManager::new();
|
||||
manager.set_inventory_map(Some(InventoryMap::new(root, vec![]).unwrap()));
|
||||
let barrier = Arc::new(std::sync::Barrier::new(3));
|
||||
let mut workers = Vec::new();
|
||||
for sender in [40, 50] {
|
||||
let worker_manager = manager.clone();
|
||||
let worker_barrier = Arc::clone(&barrier);
|
||||
workers.push(thread::spawn(move || {
|
||||
worker_barrier.wait();
|
||||
worker_manager
|
||||
.add_restriction(restriction(RlvRestrictionType::DetachThis, sender, vec![]))
|
||||
.unwrap();
|
||||
}));
|
||||
}
|
||||
barrier.wait();
|
||||
for worker in workers {
|
||||
worker.join().unwrap();
|
||||
}
|
||||
let locked = manager.get_locked_folders().unwrap();
|
||||
assert_eq!(locked.len(), 2);
|
||||
assert!(!locked[&first.id()].can_detach());
|
||||
assert!(!locked[&second.id()].can_detach());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_removal_counts_repeated_inputs_before_deduplication() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::Fly, 1, vec![]))
|
||||
.unwrap();
|
||||
let repeated = std::iter::repeat_n(guid(1), 100_001);
|
||||
assert!(poll_ready(manager.remove_restrictions_for_objects(Box::new(repeated), None)).is_err());
|
||||
assert_eq!(manager.find_restrictions(None, None).unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blacklist_is_case_insensitive_sorted_and_thread_safe() {
|
||||
let blacklist = RlvBlacklist::new();
|
||||
let mut workers = Vec::new();
|
||||
for behavior in ["Fly", "JUMP", "sit", "CamZoomMax"] {
|
||||
let worker_blacklist = blacklist.clone();
|
||||
workers.push(thread::spawn(move || {
|
||||
worker_blacklist
|
||||
.blacklist_behavior(behavior.to_owned())
|
||||
.unwrap();
|
||||
}));
|
||||
}
|
||||
for worker in workers {
|
||||
worker.join().unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
blacklist.get_blacklist().unwrap(),
|
||||
["camzoommax", "fly", "jump", "sit"]
|
||||
);
|
||||
assert!(blacklist.is_blacklisted("fLy".into()).unwrap());
|
||||
blacklist.un_blacklist_behavior("FLY".into()).unwrap();
|
||||
assert!(!blacklist.is_blacklisted("fly".into()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_restrictions_apply_minimum_maximum_alias_and_last_value_rules() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
for (sender, value) in [(4, 0.4), (7, 0.7)] {
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::CamZoomMin,
|
||||
sender,
|
||||
vec![RlvValue::Real(value)],
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
for (sender, value) in [(20, 2.0), (12, 1.2)] {
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::CamZoomMax,
|
||||
sender,
|
||||
vec![RlvValue::Real(value)],
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::CamDistMin,
|
||||
31,
|
||||
vec![RlvValue::Real(3.0)],
|
||||
))
|
||||
.unwrap();
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::CamDrawColor,
|
||||
32,
|
||||
vec![
|
||||
RlvValue::Real(0.0),
|
||||
RlvValue::Real(0.5),
|
||||
RlvValue::Real(1.0),
|
||||
],
|
||||
))
|
||||
.unwrap();
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::CamDrawColor,
|
||||
33,
|
||||
vec![
|
||||
RlvValue::Real(1.0),
|
||||
RlvValue::Real(1.5),
|
||||
RlvValue::Real(-1.0),
|
||||
],
|
||||
))
|
||||
.unwrap();
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::CamUnlock, 34, vec![]))
|
||||
.unwrap();
|
||||
|
||||
let camera = RlvPermissionsService::new(manager)
|
||||
.get_camera_restrictions()
|
||||
.unwrap();
|
||||
assert_eq!(camera.zoom_min().flatten(), Some(0.7));
|
||||
assert_eq!(camera.zoom_max().flatten(), Some(1.2));
|
||||
assert_eq!(camera.av_dist_min().flatten(), Some(3.0));
|
||||
assert_eq!(camera.draw_color().flatten().unwrap().0, [0.5, 0.75, 0.5]);
|
||||
assert!(camera.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_settings_preserve_constructor_field_order() {
|
||||
let settings = CameraSettings::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0).unwrap();
|
||||
assert_eq!(settings.av_dist_min(), 1.0);
|
||||
assert_eq!(settings.av_dist_max(), 2.0);
|
||||
assert_eq!(settings.fov_min(), 3.0);
|
||||
assert_eq!(settings.fov_max(), 4.0);
|
||||
assert_eq!(settings.zoom_min(), 5.0);
|
||||
assert_eq!(settings.current_fov(), 6.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_exceptions_are_targeted_and_secure_rules_take_precedence() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
let permissions = RlvPermissionsService::new(manager.clone());
|
||||
let allowed_user = guid(90);
|
||||
let other_user = guid(91);
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::SendIm, 1, vec![]))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_send_im("hello".into(), Some(Some(allowed_user)), None)
|
||||
.unwrap()
|
||||
);
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::SendIm,
|
||||
1,
|
||||
vec![RlvValue::Uuid(allowed_user)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
permissions
|
||||
.can_send_im("hello".into(), Some(Some(allowed_user)), None)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!permissions
|
||||
.can_send_im("hello".into(), Some(Some(other_user)), None)
|
||||
.unwrap()
|
||||
);
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::SendImSec, 2, vec![]))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_send_im("hello".into(), Some(Some(allowed_user)), None)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_interaction_and_location_permissions_compose() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
let permissions = RlvPermissionsService::new(manager.clone());
|
||||
assert!(permissions.can_fly().unwrap());
|
||||
assert!(permissions.can_sit().unwrap());
|
||||
assert!(permissions.can_rez().unwrap());
|
||||
assert!(
|
||||
permissions
|
||||
.can_edit(RlvPermissionsServiceObjectLocation::RezzedInWorld, None)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let interact = restriction(RlvRestrictionType::Interact, 1, vec![]);
|
||||
manager.add_restriction(interact.clone()).unwrap();
|
||||
assert!(!permissions.can_sit().unwrap());
|
||||
assert!(!permissions.can_rez().unwrap());
|
||||
assert!(
|
||||
!permissions
|
||||
.can_edit(RlvPermissionsServiceObjectLocation::Hud, None)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(manager.remove_restriction(&interact));
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::EditWorld, 2, vec![]))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_edit(RlvPermissionsServiceObjectLocation::RezzedInWorld, None)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
permissions
|
||||
.can_edit(RlvPermissionsServiceObjectLocation::Attached, None)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_and_channel_outputs_apply_defaults_minima_and_precedence() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
let permissions = RlvPermissionsService::new(manager.clone());
|
||||
let mut distance = -1.0;
|
||||
assert!(!permissions.can_sit_tp(&mut distance).unwrap());
|
||||
assert_eq!(distance, 1.5);
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::SitTp,
|
||||
1,
|
||||
vec![RlvValue::Real(4.0)],
|
||||
))
|
||||
.unwrap();
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::SitTp,
|
||||
2,
|
||||
vec![RlvValue::Real(2.0)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(permissions.can_sit_tp(&mut distance).unwrap());
|
||||
assert_eq!(distance, 2.0);
|
||||
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::RedirChat,
|
||||
3,
|
||||
vec![RlvValue::Integer(7)],
|
||||
))
|
||||
.unwrap();
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::RedirChat,
|
||||
4,
|
||||
vec![RlvValue::Integer(7)],
|
||||
))
|
||||
.unwrap();
|
||||
let mut channels = Vec::new();
|
||||
assert!(permissions.try_get_redir_chat_channels(&mut channels));
|
||||
assert_eq!(channels, [7]);
|
||||
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::SendChannel, 5, vec![]))
|
||||
.unwrap();
|
||||
assert!(!permissions.can_chat(8, "private".into()).unwrap());
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::SendChannel,
|
||||
5,
|
||||
vec![RlvValue::Integer(8)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(permissions.can_chat(8, "private".into()).unwrap());
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::SendChannelExcept,
|
||||
6,
|
||||
vec![RlvValue::Integer(8)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(!permissions.can_chat(8, "private".into()).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn touch_and_hover_rules_apply_early_overrides_and_explicit_targets() {
|
||||
let manager = RlvRestrictionManager::new();
|
||||
let permissions = RlvPermissionsService::new(manager.clone());
|
||||
let prim = guid(80);
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::TouchAll, 1, vec![]))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_touch(
|
||||
RlvPermissionsServiceTouchLocation::RezzedInWorld,
|
||||
prim,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::TouchMe, 80, vec![]))
|
||||
.unwrap();
|
||||
assert!(
|
||||
permissions
|
||||
.can_touch(
|
||||
RlvPermissionsServiceTouchLocation::RezzedInWorld,
|
||||
prim,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::FarTouch,
|
||||
2,
|
||||
vec![RlvValue::Real(1.5)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_touch(
|
||||
RlvPermissionsServiceTouchLocation::RezzedInWorld,
|
||||
prim,
|
||||
None,
|
||||
Some(Some(2.0)),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::ShowHoverText,
|
||||
3,
|
||||
vec![RlvValue::Uuid(prim)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_show_hover_text(
|
||||
RlvPermissionsServiceHoverTextLocation::World,
|
||||
Some(Some(prim)),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
permissions
|
||||
.can_show_hover_text(
|
||||
RlvPermissionsServiceHoverTextLocation::World,
|
||||
Some(Some(guid(81))),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_and_detach_permissions_combine_item_type_source_and_folder_locks() {
|
||||
let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
|
||||
let folder = root.add_child(guid(2), "clothes".into()).unwrap();
|
||||
let item = folder
|
||||
.add_item(
|
||||
guid(3),
|
||||
"shirt".into(),
|
||||
false,
|
||||
Some(Some(RlvAttachmentPoint::Chest)),
|
||||
Some(Some(guid(30))),
|
||||
Some(Some(RlvWearableType::Shirt)),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let manager = RlvRestrictionManager::new();
|
||||
manager.set_inventory_map(Some(InventoryMap::new(root, vec![]).unwrap()));
|
||||
let permissions = RlvPermissionsService::new(manager.clone());
|
||||
assert!(
|
||||
permissions
|
||||
.can_attach_with_rlv_inventory_item_boolean(item.clone(), true)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
permissions
|
||||
.can_detach_with_rlv_inventory_item(item.clone())
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
manager
|
||||
.add_restriction(restriction(RlvRestrictionType::DetachThis, 30, vec![]))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_detach_with_rlv_inventory_item(item.clone())
|
||||
.unwrap()
|
||||
);
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::AttachThis,
|
||||
31,
|
||||
vec![RlvValue::String("clothes".into())],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_attach_with_rlv_inventory_item_boolean(item.clone(), true)
|
||||
.unwrap()
|
||||
);
|
||||
manager
|
||||
.add_restriction(restriction(
|
||||
RlvRestrictionType::RemOutfit,
|
||||
32,
|
||||
vec![RlvValue::WearableType(RlvWearableType::Shirt)],
|
||||
))
|
||||
.unwrap();
|
||||
assert!(
|
||||
!permissions
|
||||
.can_detach_with_rlv_inventory_item(item)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_folder_locks_and_exceptions_publish_isolated_snapshots() {
|
||||
let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
|
||||
let locked = root.add_child(guid(2), "locked".into()).unwrap();
|
||||
let nested = locked.add_child(guid(3), "nested".into()).unwrap();
|
||||
locked
|
||||
.add_item(
|
||||
guid(4),
|
||||
"worn object".into(),
|
||||
false,
|
||||
Some(Some(RlvAttachmentPoint::Chest)),
|
||||
Some(Some(guid(44))),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let inventory = InventoryMap::new(root, vec![]).unwrap();
|
||||
let manager = RlvRestrictionManager::new();
|
||||
manager.set_inventory_map(Some(inventory));
|
||||
let lock = restriction(RlvRestrictionType::DetachAllThis, 44, vec![]);
|
||||
manager.add_restriction(lock.clone()).unwrap();
|
||||
|
||||
let first_snapshot = manager.get_locked_folders().unwrap();
|
||||
assert!(!first_snapshot[&locked.id()].can_detach());
|
||||
assert!(!first_snapshot[&nested.id()].can_detach());
|
||||
let exception = restriction(
|
||||
RlvRestrictionType::DetachAllThisExcept,
|
||||
45,
|
||||
vec![RlvValue::String("locked".into())],
|
||||
);
|
||||
manager.add_restriction(exception).unwrap();
|
||||
let second_snapshot = manager.get_locked_folders().unwrap();
|
||||
assert!(second_snapshot[&locked.id()].can_detach());
|
||||
assert!(second_snapshot[&nested.id()].can_detach());
|
||||
// Previously returned snapshots remain immutable after later manager updates.
|
||||
assert!(!first_snapshot[&locked.id()].can_detach());
|
||||
assert!(manager.remove_restriction(&lock));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_inventory_dictionary_membership_is_snapshotted_with_reference_values() {
|
||||
let root = RlvSharedFolder::new(guid(1), "#RLV".into()).unwrap();
|
||||
let mut external = RlvInventoryItem::new(
|
||||
guid(2),
|
||||
"outside".into(),
|
||||
false,
|
||||
Some(Some(guid(3))),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let map = InventoryMap::new(root, vec![external.clone()]).unwrap();
|
||||
external.set_name("changed".into());
|
||||
// Items use C# reference semantics, while the dictionary's membership is a
|
||||
// fixed immutable snapshot. Mutating the referenced item is visible.
|
||||
assert_eq!(map.external_items().len(), 1);
|
||||
assert_eq!(
|
||||
map.external_items().get(&guid(2)).unwrap().name(),
|
||||
"changed"
|
||||
);
|
||||
external.set_attached_prim_id(Some(Some(guid(4))));
|
||||
assert_eq!(
|
||||
map.external_items()
|
||||
.get(&guid(2))
|
||||
.unwrap()
|
||||
.attached_prim_id(),
|
||||
Some(Some(guid(4)))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user