Files
MetaCrate/tests/compat/src/rlv_support.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

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.clone();
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 an error if the native RLV service cannot be initialized.
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,
})
}
}