//! 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, pub look_at: Option>, } #[derive(Clone, Debug, Default, PartialEq)] pub struct ActionLog { pub adjusted_heights: Vec<(f32, f32, f32)>, pub attachments: Vec>, pub detachments: Vec>, pub removed_outfits: Vec>, pub instant_messages: Vec<(Guid, String)>, pub replies: Vec<(i32, String)>, pub camera_fovs: Vec, pub debug_settings: Vec<(String, String)>, pub environment_settings: Vec<(String, String)>, pub group_ids: Vec<(Guid, Option)>, pub group_names: Vec<(String, Option)>, pub rotations: Vec, pub sits: Vec, pub sit_ground_count: usize, pub teleports: Vec, pub unsit_count: usize, } #[derive(Clone, Default)] pub struct RecordingActions(pub Arc>); type ActionFuture<'a> = Pin> + 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, _: 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, _: CancellationToken) -> ActionFuture<'_> { self.0.lock().unwrap().detachments.push(item_ids); Box::pin(async { Ok(()) }) } fn rem_outfit(&self, item_ids: Vec, _: 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, _: 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, _: 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, look_at: Option>, _: 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, pub active_group: Option, pub camera_settings: Option<[f32; 6]>, pub debug_settings: BTreeMap, pub environment_settings: BTreeMap, pub inventory_map: Option, pub sit_id: Option, } #[derive(Clone, Default)] pub struct RecordingQueries(pub Arc>); type QueryFuture<'a, T> = Pin> + 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)> { 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)> { 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>, pub queries: Arc>, } 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 { 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 { 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 { 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 { 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, }) } }