2194 lines
70 KiB
Rust
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);
|
|
}
|
|
}
|