Files
MetaCrate/crates/libremetaverse-rlv/src/service.rs
Chili Palmer 2f1c15c081
Some checks failed
Native code generation / deterministic (push) Failing after 8m21s
Imaging and meshing gate / native (push) Failing after 4m52s
Native Rust workspace compile / compile (push) Failing after 7m14s
Implement native RLV callback service integration (#80)
2026-08-11 01:00:48 +00:00

2194 lines
70 KiB
Rust

//! 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<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
type QueryFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>;
fn token(value: Option<CancellationToken>) -> 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<Self, Error> {
Ok(Self {
item_id,
attachment_point,
replace_existing_attachments,
})
}
pub fn equals(&self, obj: Option<Object>) -> bool {
obj.as_ref().and_then(Object::downcast_ref::<Self>) == 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<Self, Error> {
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<AttachmentRequest>,
token: CancellationToken,
) -> Result<(), Error> {
token.throw_if_cancellation_requested()
}
pub async fn detach(&self, _: Vec<Guid>, token: CancellationToken) -> Result<(), Error> {
token.throw_if_cancellation_requested()
}
pub async fn rem_outfit(&self, _: Vec<Guid>, 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<String>,
token: CancellationToken,
) -> Result<(), Error> {
token.throw_if_cancellation_requested()
}
pub async fn set_group_with_string_string_cancellation_token(
&self,
_: String,
_: Option<String>,
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<String>,
_: Option<Option<f32>>,
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<AttachmentRequest>, t: CancellationToken) -> ActionFuture<'_> {
Box::pin(self.attach(a, t))
}
fn detach(&self, a: Vec<Guid>, t: CancellationToken) -> ActionFuture<'_> {
Box::pin(self.detach(a, t))
}
fn rem_outfit(&self, a: Vec<Guid>, 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<String>,
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<String>,
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<String>,
look_at: Option<Option<f32>>,
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<Self, Error> {
Ok(Self)
}
pub async fn is_sitting(&self, token: CancellationToken) -> Result<bool, Error> {
token.throw_if_cancellation_requested()?;
Ok(false)
}
pub async fn object_exists(&self, _: Guid, token: CancellationToken) -> Result<bool, Error> {
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<CameraSettings>), 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<InventoryMap>), Error> {
token.throw_if_cancellation_requested()?;
Ok((false, None))
}
pub async fn try_get_shared_folder(
&self,
token: CancellationToken,
) -> Result<(bool, Option<RlvSharedFolder>), 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<CameraSettings>)> {
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<InventoryMap>)> {
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<dyn IRlvQueryCallbacks>,
actions: Arc<dyn IRlvActionCallbacks>,
}
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<dyn IRlvQueryCallbacks>,
actions: Arc<dyn IRlvActionCallbacks>,
) -> Self {
Self {
permissions,
queries,
actions,
}
}
async fn inventory(&self, token: &CancellationToken) -> Result<Option<InventoryMap>, 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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<RlvInventoryItem>,
enforce_nostrip: bool,
enforce_restrictions: bool,
) -> Result<Vec<Guid>, 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<bool, Error> {
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<RlvAttachmentPoint> {
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<Vec<AttachmentRequest>, 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<RlvInventoryItem> {
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<RlvSharedFolder>, 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<dyn IRlvQueryCallbacks>,
actions: Arc<dyn IRlvActionCallbacks>,
}
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<Option<InventoryMap>, 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<bool, Error> {
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::<Vec<_>>()
.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<String, Error> {
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::<Vec<_>>()
.join(";"),
);
}
}
Ok(response)
}
async fn outfit(
&self,
specific: Option<RlvWearableType>,
token: &CancellationToken,
) -> Result<String, Error> {
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<RlvAttachmentPoint>,
token: &CancellationToken,
) -> Result<String, Error> {
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<String, Error> {
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::<Vec<_>>()
.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<String, Error> {
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<Option<String>, 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<dyn IRlvQueryCallbacks>,
actions: Arc<dyn IRlvActionCallbacks>,
query_handler: QueryHandler,
}
/// Complete callback-driven RLV service.
#[derive(Clone)]
pub struct RlvService {
inner: Arc<ServiceInner>,
}
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<dyn IRlvQueryCallbacks>,
action_callbacks: Box<dyn IRlvActionCallbacks>,
enabled: bool,
) -> Result<Self, Error> {
let queries: Arc<dyn IRlvQueryCallbacks> = Arc::from(callbacks);
let actions: Arc<dyn IRlvActionCallbacks> = 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<CancellationToken>,
) -> Result<bool, Error> {
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<CancellationToken>,
) -> Result<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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<bool, Error> {
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::<Vec<_>>()
.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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<CancellationToken>,
) -> 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<Option<Guid>>,
cancellation_token: Option<CancellationToken>,
) -> 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<Option<Guid>>,
cancellation_token: Option<CancellationToken>,
) -> 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<T>(future: impl Future<Output = T>) -> 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);
}
}