492 lines
15 KiB
Rust
492 lines
15 KiB
Rust
//! Recording callbacks shared by the RLV translations.
|
|
|
|
use libremetaverse_rlv::{
|
|
AttachmentRequest, CameraSettings, IRlvActionCallbacks, IRlvQueryCallbacks, InventoryMap,
|
|
RlvAttachmentPoint, RlvInventoryItem, RlvService, RlvSharedFolder,
|
|
};
|
|
use libremetaverse_types::Error;
|
|
use libremetaverse_types::compat::{CancellationToken, Guid};
|
|
use std::collections::{BTreeMap, HashMap};
|
|
use std::future::Future;
|
|
use std::pin::Pin;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
pub const SENDER_NAME: &str = "Sender 1";
|
|
pub const SENDER_ID: &str = "ffffffff-ffff-4fff-8fff-ffffffffffff";
|
|
|
|
/// Parses a UUID literal used by the pinned RLV fixtures.
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics when `value` is not a 32-digit hexadecimal UUID, ignoring hyphens.
|
|
#[must_use]
|
|
pub fn guid(value: &str) -> Guid {
|
|
let digits: Vec<_> = value.bytes().filter(|byte| *byte != b'-').collect();
|
|
assert_eq!(digits.len(), 32, "valid RLV test UUID");
|
|
let mut bytes = [0; 16];
|
|
for (index, pair) in digits.chunks_exact(2).enumerate() {
|
|
let digit = |byte| match byte {
|
|
b'0'..=b'9' => byte - b'0',
|
|
b'a'..=b'f' => byte - b'a' + 10,
|
|
b'A'..=b'F' => byte - b'A' + 10,
|
|
_ => panic!("valid RLV test UUID"),
|
|
};
|
|
bytes[index] = digit(pair[0]) << 4 | digit(pair[1]);
|
|
}
|
|
Guid(bytes)
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct AttachmentCall {
|
|
pub item_id: Guid,
|
|
pub point: RlvAttachmentPoint,
|
|
pub replace: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct TeleportCall {
|
|
pub x: f32,
|
|
pub y: f32,
|
|
pub z: f32,
|
|
pub region: Option<String>,
|
|
pub look_at: Option<Option<f32>>,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct ActionLog {
|
|
pub adjusted_heights: Vec<(f32, f32, f32)>,
|
|
pub attachments: Vec<Vec<AttachmentCall>>,
|
|
pub detachments: Vec<Vec<Guid>>,
|
|
pub removed_outfits: Vec<Vec<Guid>>,
|
|
pub instant_messages: Vec<(Guid, String)>,
|
|
pub replies: Vec<(i32, String)>,
|
|
pub camera_fovs: Vec<f32>,
|
|
pub debug_settings: Vec<(String, String)>,
|
|
pub environment_settings: Vec<(String, String)>,
|
|
pub group_ids: Vec<(Guid, Option<String>)>,
|
|
pub group_names: Vec<(String, Option<String>)>,
|
|
pub rotations: Vec<f32>,
|
|
pub sits: Vec<Guid>,
|
|
pub sit_ground_count: usize,
|
|
pub teleports: Vec<TeleportCall>,
|
|
pub unsit_count: usize,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct RecordingActions(pub Arc<Mutex<ActionLog>>);
|
|
|
|
type ActionFuture<'a> = Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
|
|
|
|
impl IRlvActionCallbacks for RecordingActions {
|
|
fn adjust_height(
|
|
&self,
|
|
distance: f32,
|
|
factor: f32,
|
|
delta: f32,
|
|
_: CancellationToken,
|
|
) -> ActionFuture<'_> {
|
|
self.0
|
|
.lock()
|
|
.unwrap()
|
|
.adjusted_heights
|
|
.push((distance, factor, delta));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn attach(&self, requests: Vec<AttachmentRequest>, _: CancellationToken) -> ActionFuture<'_> {
|
|
let calls = requests
|
|
.iter()
|
|
.map(|request| AttachmentCall {
|
|
item_id: request.item_id(),
|
|
point: request.attachment_point(),
|
|
replace: request.replace_existing_attachments(),
|
|
})
|
|
.collect();
|
|
self.0.lock().unwrap().attachments.push(calls);
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn detach(&self, item_ids: Vec<Guid>, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().detachments.push(item_ids);
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn rem_outfit(&self, item_ids: Vec<Guid>, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().removed_outfits.push(item_ids);
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn send_instant_message(
|
|
&self,
|
|
target: Guid,
|
|
message: String,
|
|
_: CancellationToken,
|
|
) -> ActionFuture<'_> {
|
|
self.0
|
|
.lock()
|
|
.unwrap()
|
|
.instant_messages
|
|
.push((target, message));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn send_reply(&self, channel: i32, message: String, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().replies.push((channel, message));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn set_cam_fov(&self, value: f32, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().camera_fovs.push(value);
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn set_debug(&self, name: String, value: String, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().debug_settings.push((name, value));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn set_env(&self, name: String, value: String, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0
|
|
.lock()
|
|
.unwrap()
|
|
.environment_settings
|
|
.push((name, value));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn set_group_with_guid_string_cancellation_token(
|
|
&self,
|
|
id: Guid,
|
|
role: Option<String>,
|
|
_: CancellationToken,
|
|
) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().group_ids.push((id, role));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn set_group_with_string_string_cancellation_token(
|
|
&self,
|
|
name: String,
|
|
role: Option<String>,
|
|
_: CancellationToken,
|
|
) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().group_names.push((name, role));
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn set_rot(&self, angle: f32, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().rotations.push(angle);
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn sit(&self, target: Guid, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().sits.push(target);
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn sit_ground(&self, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().sit_ground_count += 1;
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn tp_to(
|
|
&self,
|
|
x: f32,
|
|
y: f32,
|
|
z: f32,
|
|
region: Option<String>,
|
|
look_at: Option<Option<f32>>,
|
|
_: CancellationToken,
|
|
) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().teleports.push(TeleportCall {
|
|
x,
|
|
y,
|
|
z,
|
|
region,
|
|
look_at,
|
|
});
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
|
|
fn unsit(&self, _: CancellationToken) -> ActionFuture<'_> {
|
|
self.0.lock().unwrap().unsit_count += 1;
|
|
Box::pin(async { Ok(()) })
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct QueryState {
|
|
pub is_sitting: bool,
|
|
pub object_exists: HashMap<Guid, bool>,
|
|
pub active_group: Option<String>,
|
|
pub camera_settings: Option<[f32; 6]>,
|
|
pub debug_settings: BTreeMap<String, String>,
|
|
pub environment_settings: BTreeMap<String, String>,
|
|
pub inventory_map: Option<InventoryMap>,
|
|
pub sit_id: Option<Guid>,
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct RecordingQueries(pub Arc<Mutex<QueryState>>);
|
|
|
|
type QueryFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>;
|
|
|
|
impl IRlvQueryCallbacks for RecordingQueries {
|
|
fn is_sitting(&self, _: CancellationToken) -> QueryFuture<'_, bool> {
|
|
let value = self.0.lock().unwrap().is_sitting;
|
|
Box::pin(async move { Ok(value) })
|
|
}
|
|
|
|
fn object_exists(&self, id: Guid, _: CancellationToken) -> QueryFuture<'_, bool> {
|
|
let value = self
|
|
.0
|
|
.lock()
|
|
.unwrap()
|
|
.object_exists
|
|
.get(&id)
|
|
.copied()
|
|
.unwrap_or(false);
|
|
Box::pin(async move { Ok(value) })
|
|
}
|
|
|
|
fn try_get_active_group_name(&self, _: CancellationToken) -> QueryFuture<'_, (bool, String)> {
|
|
let value = self.0.lock().unwrap().active_group.clone();
|
|
Box::pin(async move { Ok(value.map_or((false, String::new()), |name| (true, name))) })
|
|
}
|
|
|
|
fn try_get_camera_settings(
|
|
&self,
|
|
_: CancellationToken,
|
|
) -> QueryFuture<'_, (bool, Option<CameraSettings>)> {
|
|
let values = self.0.lock().unwrap().camera_settings;
|
|
Box::pin(async move {
|
|
let Some([av_min, av_max, fov_min, fov_max, zoom_min, current_fov]) = values else {
|
|
return Ok((false, None));
|
|
};
|
|
Ok((
|
|
true,
|
|
Some(CameraSettings::new(
|
|
av_min,
|
|
av_max,
|
|
fov_min,
|
|
fov_max,
|
|
zoom_min,
|
|
current_fov,
|
|
)?),
|
|
))
|
|
})
|
|
}
|
|
|
|
fn try_get_debug_setting_value(
|
|
&self,
|
|
name: String,
|
|
_: CancellationToken,
|
|
) -> QueryFuture<'_, (bool, String)> {
|
|
let value = self.0.lock().unwrap().debug_settings.get(&name).cloned();
|
|
Box::pin(async move { Ok(value.map_or((false, String::new()), |value| (true, value))) })
|
|
}
|
|
|
|
fn try_get_environment_setting_value(
|
|
&self,
|
|
name: String,
|
|
_: CancellationToken,
|
|
) -> QueryFuture<'_, (bool, String)> {
|
|
let value = self
|
|
.0
|
|
.lock()
|
|
.unwrap()
|
|
.environment_settings
|
|
.get(&name)
|
|
.cloned();
|
|
Box::pin(async move { Ok(value.map_or((false, String::new()), |value| (true, value))) })
|
|
}
|
|
|
|
fn try_get_inventory_map(
|
|
&self,
|
|
_: CancellationToken,
|
|
) -> QueryFuture<'_, (bool, Option<InventoryMap>)> {
|
|
let value = self.0.lock().unwrap().inventory_map.take();
|
|
Box::pin(async move { Ok((value.is_some(), value)) })
|
|
}
|
|
|
|
fn try_get_sit_id(&self, _: CancellationToken) -> QueryFuture<'_, (bool, Guid)> {
|
|
let value = self.0.lock().unwrap().sit_id;
|
|
Box::pin(async move { Ok(value.map_or((false, Guid([0; 16])), |id| (true, id))) })
|
|
}
|
|
}
|
|
|
|
pub struct RlvHarness {
|
|
pub service: RlvService,
|
|
pub actions: Arc<Mutex<ActionLog>>,
|
|
pub queries: Arc<Mutex<QueryState>>,
|
|
}
|
|
|
|
impl RlvHarness {
|
|
/// Builds the same enabled service and two narrow callback fakes as the C# base fixture.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns the standardized constructor error while the public RLV service is a shim.
|
|
pub fn new() -> Result<Self, Error> {
|
|
let action_callbacks = RecordingActions::default();
|
|
let query_callbacks = RecordingQueries::default();
|
|
let actions = Arc::clone(&action_callbacks.0);
|
|
let queries = Arc::clone(&query_callbacks.0);
|
|
let service = RlvService::new(Box::new(query_callbacks), Box::new(action_callbacks), true)?;
|
|
Ok(Self {
|
|
service,
|
|
actions,
|
|
queries,
|
|
})
|
|
}
|
|
|
|
/// Processes a command using the base fixture sender identity.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns the public RLV service error for the command.
|
|
pub async fn process(&self, command: &str) -> Result<bool, Error> {
|
|
self.service
|
|
.process_message(command.into(), guid(SENDER_ID), SENDER_NAME.into(), None)
|
|
.await
|
|
}
|
|
|
|
/// Processes a command using an explicit sender identity.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns the public RLV service error for the command.
|
|
pub async fn process_from(
|
|
&self,
|
|
command: &str,
|
|
sender_id: &str,
|
|
sender_name: &str,
|
|
) -> Result<bool, Error> {
|
|
self.service
|
|
.process_message(command.into(), guid(sender_id), sender_name.into(), None)
|
|
.await
|
|
}
|
|
}
|
|
|
|
pub struct SampleInventoryTree {
|
|
pub root: RlvSharedFolder,
|
|
pub clothing: RlvSharedFolder,
|
|
pub hats: RlvSharedFolder,
|
|
pub sub_hats: RlvSharedFolder,
|
|
pub accessories: RlvSharedFolder,
|
|
pub fancy_hat: RlvInventoryItem,
|
|
pub party_hat: RlvInventoryItem,
|
|
pub business_pants: RlvInventoryItem,
|
|
pub retro_pants: RlvInventoryItem,
|
|
pub happy_shirt: RlvInventoryItem,
|
|
pub glasses: RlvInventoryItem,
|
|
pub watch: RlvInventoryItem,
|
|
}
|
|
|
|
impl SampleInventoryTree {
|
|
/// Builds the exact `#RLV` hierarchy shared by the upstream fixtures.
|
|
///
|
|
/// # Errors
|
|
///
|
|
/// Returns a mapped constructor or collection error.
|
|
pub fn build() -> Result<Self, Error> {
|
|
let root =
|
|
RlvSharedFolder::new(guid("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), "#RLV".into())?;
|
|
let clothing = root.add_child(
|
|
guid("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
|
"Clothing".into(),
|
|
)?;
|
|
let hats =
|
|
clothing.add_child(guid("dddddddd-dddd-4ddd-8ddd-dddddddddddd"), "Hats".into())?;
|
|
let sub_hats = hats.add_child(
|
|
guid("ffffffff-0000-4000-8000-000000000000"),
|
|
"Sub Hats".into(),
|
|
)?;
|
|
root.add_child(
|
|
guid("eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"),
|
|
".private".into(),
|
|
)?;
|
|
let accessories = root.add_child(
|
|
guid("cccccccc-cccc-4ccc-8ccc-cccccccccccc"),
|
|
"Accessories".into(),
|
|
)?;
|
|
let watch = accessories.add_item(
|
|
guid("c0000000-cccc-4ccc-8ccc-cccccccccccc"),
|
|
"Watch".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
let glasses = accessories.add_item(
|
|
guid("c1111111-cccc-4ccc-8ccc-cccccccccccc"),
|
|
"Glasses".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
let business_pants = clothing.add_item(
|
|
guid("b0000000-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
|
"Business Pants (Pelvis)".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
let happy_shirt = clothing.add_item(
|
|
guid("b1111111-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
|
"Happy Shirt".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
let retro_pants = clothing.add_item(
|
|
guid("b2222222-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
|
"Retro Pants".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
let party_hat = hats.add_item(
|
|
guid("d0000000-dddd-4ddd-8ddd-dddddddddddd"),
|
|
"Party Hat (Spine)".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
let fancy_hat = hats.add_item(
|
|
guid("d1111111-dddd-4ddd-8ddd-dddddddddddd"),
|
|
"Fancy Hat (chin)".into(),
|
|
false,
|
|
None,
|
|
None,
|
|
None,
|
|
None,
|
|
)?;
|
|
Ok(Self {
|
|
root,
|
|
clothing,
|
|
hats,
|
|
sub_hats,
|
|
accessories,
|
|
fancy_hat,
|
|
party_hat,
|
|
business_pants,
|
|
retro_pants,
|
|
happy_shirt,
|
|
glasses,
|
|
watch,
|
|
})
|
|
}
|
|
}
|