Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
1269 lines
47 KiB
Rust
1269 lines
47 KiB
Rust
//! Strongly typed decoding for capability event names.
|
|
|
|
// These public methods mirror generated C# signatures, including Result-returning
|
|
// constructors and value-taking parameters that are infallible in native Rust.
|
|
#![allow(clippy::inherent_to_string)]
|
|
#![allow(clippy::missing_errors_doc)]
|
|
#![allow(clippy::must_use_candidate)]
|
|
|
|
use crate::interfaces::IMessage;
|
|
use crate::message_codec::GeneratedMessage;
|
|
use crate::messages::linden::{
|
|
AgentDropGroupMessage, AgentGroupDataUpdateMessage, AgentStateUpdateMessage,
|
|
BulkUpdateInventoryMessage, ChatSessionAcceptInvitation, ChatSessionRequestMuteUpdate,
|
|
ChatSessionRequestStartConference, ChatterBoxInvitationMessage,
|
|
ChatterBoxSessionAgentListUpdatesMessage, ChatterBoxSessionStartReplyMessage,
|
|
ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage,
|
|
DirLandReplyMessage, DisplayNameUpdateMessage, EnableSimulatorMessage,
|
|
EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage,
|
|
GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest,
|
|
LandStatReplyMessage, MapLayerReplyVariant, NavMeshStatusUpdateMessage,
|
|
ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate,
|
|
ObjectPhysicsPropertiesMessage, ObjectResourcesDetail, ParcelObjectOwnersReplyMessage,
|
|
ParcelPropertiesMessage, ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage,
|
|
PlacesReplyMessage, ProvisionVoiceAccountRequestMessage, RegionInfoMessage,
|
|
RemoteParcelRequestReply, RenderMaterialsMessage, RequiredVoiceVersionMessage,
|
|
ScriptRunningReplyMessage, SearchStatRequestReply, SearchStatRequestRequest,
|
|
SendPostcardMessage, SetDisplayNameMessage, SetDisplayNameReplyMessage,
|
|
SimConsoleResponseMessage, TeleportFailedMessage, TeleportFinishMessage,
|
|
UpdateAgentInformationMessage, UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage,
|
|
UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, UploaderRequestComplete,
|
|
UploaderRequestUpload, ViewerStatsMessage,
|
|
};
|
|
use crate::{Error, MediaControls, MediaPermission};
|
|
use libremetaverse_structured_data::{OSD, OSDMap};
|
|
use libremetaverse_types::compat::Uri;
|
|
use libremetaverse_types::{AttachmentPoint, PhysicsShapeType, UUID};
|
|
use std::collections::HashMap;
|
|
use std::net::{IpAddr, Ipv4Addr};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
/// Display-name data carried by the display-name capability messages.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub struct AgentDisplayName {
|
|
id: UUID,
|
|
user_name: Option<String>,
|
|
display_name: Option<String>,
|
|
legacy_first_name: Option<String>,
|
|
legacy_last_name: Option<String>,
|
|
is_default_display_name: bool,
|
|
next_update: SystemTime,
|
|
updated: SystemTime,
|
|
}
|
|
|
|
impl AgentDisplayName {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
id: UUID::zero(),
|
|
user_name: None,
|
|
display_name: None,
|
|
legacy_first_name: None,
|
|
legacy_last_name: None,
|
|
is_default_display_name: false,
|
|
next_update: UNIX_EPOCH,
|
|
updated: UNIX_EPOCH,
|
|
})
|
|
}
|
|
|
|
pub fn from_osd(data: OSD) -> Result<Self, Error> {
|
|
let OSD::Map(map) = data else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let mut value = Self::new()?;
|
|
value.id = map.get("id").unwrap_or(&OSD::Undefined).as_uuid()?;
|
|
value.user_name = optional_string(map.get("username"))?;
|
|
value.display_name = optional_string(map.get("display_name"))?;
|
|
value.legacy_first_name = optional_string(map.get("legacy_first_name"))?;
|
|
value.legacy_last_name = optional_string(map.get("legacy_last_name"))?;
|
|
value.is_default_display_name = map
|
|
.get("is_display_name_default")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_boolean()?;
|
|
value.next_update = map
|
|
.get("display_name_next_update")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_date()?;
|
|
value.updated = map
|
|
.get("last_updated")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_date()?;
|
|
Ok(value)
|
|
}
|
|
|
|
pub fn get_osd(&self) -> Result<OSD, Error> {
|
|
Ok(OSD::Map(HashMap::from([
|
|
("id".into(), OSD::UUID(self.id)),
|
|
(
|
|
"username".into(),
|
|
OSD::String(self.user_name.clone().unwrap_or_default()),
|
|
),
|
|
(
|
|
"display_name".into(),
|
|
OSD::String(self.display_name.clone().unwrap_or_default()),
|
|
),
|
|
(
|
|
"legacy_first_name".into(),
|
|
OSD::String(self.legacy_first_name.clone().unwrap_or_default()),
|
|
),
|
|
(
|
|
"legacy_last_name".into(),
|
|
OSD::String(self.legacy_last_name.clone().unwrap_or_default()),
|
|
),
|
|
(
|
|
"is_display_name_default".into(),
|
|
OSD::Boolean(self.is_default_display_name),
|
|
),
|
|
(
|
|
"display_name_next_update".into(),
|
|
OSD::Date(self.next_update),
|
|
),
|
|
("last_updated".into(), OSD::Date(self.updated)),
|
|
])))
|
|
}
|
|
|
|
pub fn to_string(&self) -> String {
|
|
format!(
|
|
"AgentDisplayName {{ id: {}, user_name: {:?}, display_name: {:?}, legacy_first_name: {:?}, legacy_last_name: {:?}, is_default_display_name: {}, next_update: {:?}, updated: {:?} }}",
|
|
self.id,
|
|
self.user_name,
|
|
self.display_name,
|
|
self.legacy_first_name,
|
|
self.legacy_last_name,
|
|
self.is_default_display_name,
|
|
self.next_update,
|
|
self.updated
|
|
)
|
|
}
|
|
|
|
pub fn display_name(&self) -> Option<String> {
|
|
self.display_name.clone()
|
|
}
|
|
pub fn set_display_name(&mut self, value: Option<String>) {
|
|
self.display_name = value;
|
|
}
|
|
pub fn id(&self) -> UUID {
|
|
self.id
|
|
}
|
|
pub fn set_id(&mut self, value: UUID) {
|
|
self.id = value;
|
|
}
|
|
pub fn is_default_display_name(&self) -> bool {
|
|
self.is_default_display_name
|
|
}
|
|
pub fn set_is_default_display_name(&mut self, value: bool) {
|
|
self.is_default_display_name = value;
|
|
}
|
|
pub fn legacy_first_name(&self) -> Option<String> {
|
|
self.legacy_first_name.clone()
|
|
}
|
|
pub fn set_legacy_first_name(&mut self, value: Option<String>) {
|
|
self.legacy_first_name = value;
|
|
}
|
|
pub fn legacy_full_name(&self) -> String {
|
|
format!(
|
|
"{} {}",
|
|
self.legacy_first_name.as_deref().unwrap_or_default(),
|
|
self.legacy_last_name.as_deref().unwrap_or_default()
|
|
)
|
|
}
|
|
pub fn legacy_last_name(&self) -> Option<String> {
|
|
self.legacy_last_name.clone()
|
|
}
|
|
pub fn set_legacy_last_name(&mut self, value: Option<String>) {
|
|
self.legacy_last_name = value;
|
|
}
|
|
pub fn next_update(&self) -> SystemTime {
|
|
self.next_update
|
|
}
|
|
pub fn set_next_update(&mut self, value: SystemTime) {
|
|
self.next_update = value;
|
|
}
|
|
pub fn updated(&self) -> SystemTime {
|
|
self.updated
|
|
}
|
|
pub fn set_updated(&mut self, value: SystemTime) {
|
|
self.updated = value;
|
|
}
|
|
pub fn user_name(&self) -> Option<String> {
|
|
self.user_name.clone()
|
|
}
|
|
pub fn set_user_name(&mut self, value: Option<String>) {
|
|
self.user_name = value;
|
|
}
|
|
}
|
|
|
|
fn optional_string(value: Option<&OSD>) -> Result<Option<String>, Error> {
|
|
match value {
|
|
None | Some(OSD::Undefined) => Ok(None),
|
|
Some(value) => Ok(Some(value.as_string()?)),
|
|
}
|
|
}
|
|
|
|
/// Physics attributes carried by `ObjectPhysicsProperties` events.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct PrimitivePhysicsProperties {
|
|
pub density: f32,
|
|
pub friction: f32,
|
|
pub gravity_multiplier: f32,
|
|
pub local_id: u32,
|
|
pub physics_shape_type: PhysicsShapeType,
|
|
pub restitution: f32,
|
|
}
|
|
|
|
impl PrimitivePhysicsProperties {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
density: 0.0,
|
|
friction: 0.0,
|
|
gravity_multiplier: 0.0,
|
|
local_id: 0,
|
|
physics_shape_type: PhysicsShapeType::Prim,
|
|
restitution: 0.0,
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::cast_possible_truncation)] // Matches the C# double-to-float LLSD conversion.
|
|
pub fn from_osd(osd: OSD) -> Result<Self, Error> {
|
|
let OSD::Map(map) = osd else {
|
|
return Self::new();
|
|
};
|
|
let shape = map
|
|
.get("PhysicsShapeType")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_integer()?;
|
|
Ok(Self {
|
|
local_id: map
|
|
.get("LocalID")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_u_integer()?,
|
|
density: map.get("Density").unwrap_or(&OSD::Undefined).as_real()? as f32,
|
|
friction: map.get("Friction").unwrap_or(&OSD::Undefined).as_real()? as f32,
|
|
gravity_multiplier: map
|
|
.get("GravityMultiplier")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_real()? as f32,
|
|
restitution: map
|
|
.get("Restitution")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_real()? as f32,
|
|
physics_shape_type: match shape {
|
|
1 => PhysicsShapeType::None,
|
|
2 => PhysicsShapeType::ConvexHull,
|
|
_ => PhysicsShapeType::Prim,
|
|
},
|
|
})
|
|
}
|
|
|
|
pub fn get_osd(&self) -> Result<OSD, Error> {
|
|
Ok(OSD::Map(HashMap::from([
|
|
("LocalID".into(), OSD::from_u_integer(self.local_id)?),
|
|
("Density".into(), OSD::Real(f64::from(self.density))),
|
|
("Friction".into(), OSD::Real(f64::from(self.friction))),
|
|
(
|
|
"GravityMultiplier".into(),
|
|
OSD::Real(f64::from(self.gravity_multiplier)),
|
|
),
|
|
("Restitution".into(), OSD::Real(f64::from(self.restitution))),
|
|
(
|
|
"PhysicsShapeType".into(),
|
|
OSD::Integer(self.physics_shape_type as i32),
|
|
),
|
|
])))
|
|
}
|
|
}
|
|
|
|
/// Per-face media settings used by object-media capability messages.
|
|
#[allow(clippy::struct_excessive_bools)] // The flags are independent wire fields in the C# schema.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct MediaEntry {
|
|
undefined: bool,
|
|
enable_alternative_image: bool,
|
|
pub auto_loop: bool,
|
|
pub auto_play: bool,
|
|
pub auto_scale: bool,
|
|
pub auto_zoom: bool,
|
|
pub control_permissions: MediaPermission,
|
|
pub controls: MediaControls,
|
|
pub current_url: String,
|
|
pub enable_white_list: bool,
|
|
pub height: i32,
|
|
pub home_url: String,
|
|
pub interact_on_first_click: bool,
|
|
pub interact_permissions: MediaPermission,
|
|
pub white_list: Vec<String>,
|
|
pub width: i32,
|
|
}
|
|
|
|
impl MediaEntry {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
undefined: false,
|
|
enable_alternative_image: false,
|
|
auto_loop: false,
|
|
auto_play: false,
|
|
auto_scale: false,
|
|
auto_zoom: false,
|
|
control_permissions: MediaPermission::NONE,
|
|
controls: MediaControls::Standard,
|
|
current_url: String::new(),
|
|
enable_white_list: false,
|
|
height: 0,
|
|
home_url: String::new(),
|
|
interact_on_first_click: false,
|
|
interact_permissions: MediaPermission::NONE,
|
|
white_list: Vec::new(),
|
|
width: 0,
|
|
})
|
|
}
|
|
|
|
pub fn from_osd(osd: OSD) -> Result<Self, Error> {
|
|
if matches!(osd, OSD::Undefined) {
|
|
let mut entry = Self::new()?;
|
|
entry.undefined = true;
|
|
return Ok(entry);
|
|
}
|
|
let OSD::Map(map) = osd else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let mut entry = Self::new()?;
|
|
entry.enable_alternative_image = osd_bool(&map, "alt_image_enable")?;
|
|
entry.auto_loop = osd_bool(&map, "auto_loop")?;
|
|
entry.auto_play = osd_bool(&map, "auto_play")?;
|
|
entry.auto_scale = osd_bool(&map, "auto_scale")?;
|
|
entry.auto_zoom = osd_bool(&map, "auto_zoom")?;
|
|
entry.controls = match osd_integer(&map, "controls")? {
|
|
1 => MediaControls::Mini,
|
|
_ => MediaControls::Standard,
|
|
};
|
|
entry.current_url = osd_string(&map, "current_url")?;
|
|
entry.interact_on_first_click = osd_bool(&map, "first_click_interact")?;
|
|
entry.height = osd_integer(&map, "height_pixels")?;
|
|
entry.home_url = osd_string(&map, "home_url")?;
|
|
entry.control_permissions = MediaPermission(
|
|
u8::try_from(osd_integer(&map, "perms_control")?).map_err(|_| Error::Argument)?,
|
|
);
|
|
entry.interact_permissions = MediaPermission(
|
|
u8::try_from(osd_integer(&map, "perms_interact")?).map_err(|_| Error::Argument)?,
|
|
);
|
|
if let Some(OSD::Array(values)) = map.get("whitelist") {
|
|
entry.white_list = values
|
|
.iter()
|
|
.map(OSD::as_string)
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
}
|
|
entry.enable_white_list = osd_bool(&map, "whitelist_enable")?;
|
|
entry.width = osd_integer(&map, "width_pixels")?;
|
|
Ok(entry)
|
|
}
|
|
|
|
pub fn get_osd(&self) -> Result<OSDMap, Error> {
|
|
OSDMap::new_with_dictionary(HashMap::from([
|
|
(
|
|
"alt_image_enable".into(),
|
|
OSD::Boolean(self.enable_alternative_image),
|
|
),
|
|
("auto_loop".into(), OSD::Boolean(self.auto_loop)),
|
|
("auto_play".into(), OSD::Boolean(self.auto_play)),
|
|
("auto_scale".into(), OSD::Boolean(self.auto_scale)),
|
|
("auto_zoom".into(), OSD::Boolean(self.auto_zoom)),
|
|
("controls".into(), OSD::Integer(self.controls as i32)),
|
|
("current_url".into(), OSD::String(self.current_url.clone())),
|
|
(
|
|
"first_click_interact".into(),
|
|
OSD::Boolean(self.interact_on_first_click),
|
|
),
|
|
("height_pixels".into(), OSD::Integer(self.height)),
|
|
("home_url".into(), OSD::String(self.home_url.clone())),
|
|
(
|
|
"perms_control".into(),
|
|
OSD::Integer(i32::from(self.control_permissions.0)),
|
|
),
|
|
(
|
|
"perms_interact".into(),
|
|
OSD::Integer(i32::from(self.interact_permissions.0)),
|
|
),
|
|
(
|
|
"whitelist".into(),
|
|
OSD::Array(self.white_list.iter().cloned().map(OSD::String).collect()),
|
|
),
|
|
(
|
|
"whitelist_enable".into(),
|
|
OSD::Boolean(self.enable_white_list),
|
|
),
|
|
("width_pixels".into(), OSD::Integer(self.width)),
|
|
]))
|
|
}
|
|
|
|
pub fn enable_alternative_image(&self) -> bool {
|
|
self.enable_alternative_image
|
|
}
|
|
|
|
pub fn set_enable_alternative_image(&mut self, value: bool) {
|
|
self.enable_alternative_image = value;
|
|
}
|
|
|
|
pub(crate) const fn is_undefined(&self) -> bool {
|
|
self.undefined
|
|
}
|
|
}
|
|
|
|
fn osd_bool(map: &HashMap<String, OSD>, key: &str) -> Result<bool, Error> {
|
|
map.get(key).unwrap_or(&OSD::Undefined).as_boolean()
|
|
}
|
|
|
|
fn osd_integer(map: &HashMap<String, OSD>, key: &str) -> Result<i32, Error> {
|
|
map.get(key).unwrap_or(&OSD::Undefined).as_integer()
|
|
}
|
|
|
|
fn osd_string(map: &HashMap<String, OSD>, key: &str) -> Result<String, Error> {
|
|
map.get(key).unwrap_or(&OSD::Undefined).as_string()
|
|
}
|
|
|
|
/// Resource-usage response for the agent's attachments.
|
|
pub struct AttachmentResourcesMessage {
|
|
pub attachments: HashMap<AttachmentPoint, Vec<ObjectResourcesDetail>>,
|
|
pub summary_available: HashMap<String, i32>,
|
|
pub summary_used: HashMap<String, i32>,
|
|
}
|
|
|
|
impl AttachmentResourcesMessage {
|
|
pub fn new() -> Result<Self, Error> {
|
|
Ok(Self {
|
|
attachments: HashMap::new(),
|
|
summary_available: HashMap::new(),
|
|
summary_used: HashMap::new(),
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::needless_pass_by_value)] // Preserves the generated C# API mapping.
|
|
pub fn deserialize(&mut self, osd: OSDMap) -> Result<(), Error> {
|
|
self.summary_available.clear();
|
|
self.summary_used.clear();
|
|
let summary = osd.get("summary").ok_or(Error::Argument)?;
|
|
let OSD::Map(summary) = summary else {
|
|
return Err(Error::Argument);
|
|
};
|
|
decode_resource_summary(summary.get("available"), &mut self.summary_available)?;
|
|
decode_resource_summary(summary.get("used"), &mut self.summary_used)?;
|
|
|
|
self.attachments.clear();
|
|
let attachments = osd.get("attachments").ok_or(Error::Argument)?;
|
|
let OSD::Array(attachments) = attachments else {
|
|
return Err(Error::Argument);
|
|
};
|
|
for attachment in attachments {
|
|
let OSD::Map(attachment) = attachment else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let location = attachment
|
|
.get("location")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_string()?;
|
|
let point = attachment_point_from_string(&location);
|
|
let Some(OSD::Array(objects)) = attachment.get("objects") else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let mut decoded = Vec::with_capacity(objects.len());
|
|
for object in objects {
|
|
decoded.push(decode_object_resources(object)?);
|
|
}
|
|
self.attachments.insert(point, decoded);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn from_osd(osd: OSD) -> Result<Self, Error> {
|
|
let OSD::Map(values) = osd else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let mut message = Self::new()?;
|
|
message.deserialize(OSDMap::new_with_dictionary(values)?)?;
|
|
Ok(message)
|
|
}
|
|
|
|
pub fn get_message_handler(_map: OSDMap) -> Result<Option<Box<dyn IMessage>>, Error> {
|
|
Ok(Some(Box::new(Self::new()?)))
|
|
}
|
|
}
|
|
|
|
impl IMessage for AttachmentResourcesMessage {
|
|
fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
|
Self::deserialize(self, map)
|
|
}
|
|
|
|
fn serialize(&self) -> Result<OSDMap, Error> {
|
|
let summary = HashMap::from([
|
|
(
|
|
"available".into(),
|
|
encode_resource_summary(&self.summary_available),
|
|
),
|
|
("used".into(), encode_resource_summary(&self.summary_used)),
|
|
]);
|
|
OSDMap::new_with_dictionary(HashMap::from([("summary".into(), OSD::Map(summary))]))
|
|
}
|
|
}
|
|
|
|
fn decode_resource_summary(
|
|
value: Option<&OSD>,
|
|
target: &mut HashMap<String, i32>,
|
|
) -> Result<(), Error> {
|
|
let Some(OSD::Array(values)) = value else {
|
|
return Err(Error::Argument);
|
|
};
|
|
for value in values {
|
|
let OSD::Map(value) = value else {
|
|
return Err(Error::Argument);
|
|
};
|
|
target.insert(osd_string(value, "type")?, osd_integer(value, "amount")?);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn encode_resource_summary(values: &HashMap<String, i32>) -> OSD {
|
|
let mut entries: Vec<_> = values.iter().collect();
|
|
entries.sort_unstable_by(|left, right| left.0.cmp(right.0));
|
|
OSD::Array(
|
|
entries
|
|
.into_iter()
|
|
.map(|(name, amount)| {
|
|
OSD::Map(HashMap::from([
|
|
("type".into(), OSD::String(name.clone())),
|
|
("amount".into(), OSD::Integer(*amount)),
|
|
]))
|
|
})
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
fn decode_object_resources(value: &OSD) -> Result<ObjectResourcesDetail, Error> {
|
|
let OSD::Map(map) = value else {
|
|
return Err(Error::Argument);
|
|
};
|
|
let resources = match map.get("resources") {
|
|
Some(OSD::Map(values)) => values
|
|
.iter()
|
|
.map(|(key, value)| Ok((key.clone(), value.as_integer()?)))
|
|
.collect::<Result<HashMap<_, _>, Error>>()?,
|
|
_ => return Err(Error::Argument),
|
|
};
|
|
Ok(ObjectResourcesDetail {
|
|
group_owned: osd_bool(map, "is_group_owned")?,
|
|
id: map.get("id").unwrap_or(&OSD::Undefined).as_uuid()?,
|
|
location: map
|
|
.get("location")
|
|
.unwrap_or(&OSD::Undefined)
|
|
.as_vector3d()?,
|
|
name: osd_string(map, "name")?,
|
|
owner_id: map.get("owner_id").unwrap_or(&OSD::Undefined).as_uuid()?,
|
|
resources,
|
|
})
|
|
}
|
|
|
|
fn attachment_point_from_string(value: &str) -> AttachmentPoint {
|
|
const POINTS: &[AttachmentPoint] = &[
|
|
AttachmentPoint::AltLeftEar,
|
|
AttachmentPoint::AltLeftEye,
|
|
AttachmentPoint::AltRightEar,
|
|
AttachmentPoint::AltRightEye,
|
|
AttachmentPoint::Chest,
|
|
AttachmentPoint::Chin,
|
|
AttachmentPoint::Default,
|
|
AttachmentPoint::Groin,
|
|
AttachmentPoint::HUDBottom,
|
|
AttachmentPoint::HUDBottomLeft,
|
|
AttachmentPoint::HUDBottomRight,
|
|
AttachmentPoint::HUDCenter,
|
|
AttachmentPoint::HUDCenter2,
|
|
AttachmentPoint::HUDTop,
|
|
AttachmentPoint::HUDTopLeft,
|
|
AttachmentPoint::HUDTopRight,
|
|
AttachmentPoint::Jaw,
|
|
AttachmentPoint::LeftEar,
|
|
AttachmentPoint::LeftEyeball,
|
|
AttachmentPoint::LeftFoot,
|
|
AttachmentPoint::LeftForearm,
|
|
AttachmentPoint::LeftHand,
|
|
AttachmentPoint::LeftHandRing,
|
|
AttachmentPoint::LeftHindFoot,
|
|
AttachmentPoint::LeftHip,
|
|
AttachmentPoint::LeftLowerLeg,
|
|
AttachmentPoint::LeftPec,
|
|
AttachmentPoint::LeftShoulder,
|
|
AttachmentPoint::LeftUpperArm,
|
|
AttachmentPoint::LeftUpperLeg,
|
|
AttachmentPoint::LeftWing,
|
|
AttachmentPoint::Mouth,
|
|
AttachmentPoint::Neck,
|
|
AttachmentPoint::Nose,
|
|
AttachmentPoint::Pelvis,
|
|
AttachmentPoint::RightEar,
|
|
AttachmentPoint::RightEyeball,
|
|
AttachmentPoint::RightFoot,
|
|
AttachmentPoint::RightForearm,
|
|
AttachmentPoint::RightHand,
|
|
AttachmentPoint::RightHandRing,
|
|
AttachmentPoint::RightHindFoot,
|
|
AttachmentPoint::RightHip,
|
|
AttachmentPoint::RightLowerLeg,
|
|
AttachmentPoint::RightPec,
|
|
AttachmentPoint::RightShoulder,
|
|
AttachmentPoint::RightUpperArm,
|
|
AttachmentPoint::RightUpperLeg,
|
|
AttachmentPoint::RightWing,
|
|
AttachmentPoint::Root,
|
|
AttachmentPoint::Skull,
|
|
AttachmentPoint::Spine,
|
|
AttachmentPoint::Stomach,
|
|
AttachmentPoint::TailBase,
|
|
AttachmentPoint::TailTip,
|
|
AttachmentPoint::Tongue,
|
|
];
|
|
let normalized: String = value
|
|
.chars()
|
|
.filter(char::is_ascii_alphanumeric)
|
|
.flat_map(char::to_lowercase)
|
|
.collect();
|
|
POINTS
|
|
.iter()
|
|
.copied()
|
|
.find(|point| format!("{point:?}").to_ascii_lowercase() == normalized)
|
|
.unwrap_or(AttachmentPoint::Default)
|
|
}
|
|
|
|
fn generated<T>(map: &OSDMap) -> Option<Box<dyn IMessage>>
|
|
where
|
|
T: GeneratedMessage + IMessage + 'static,
|
|
{
|
|
let mut message = T::new_generated();
|
|
T::deserialize_generated(&mut message, map).ok()?;
|
|
Some(Box::new(message))
|
|
}
|
|
|
|
macro_rules! message_variant {
|
|
($type:ty) => {
|
|
impl IMessage for $type {
|
|
fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> {
|
|
<Self as GeneratedMessage>::deserialize_generated(self, &map)
|
|
}
|
|
|
|
fn serialize(&self) -> Result<OSDMap, Error> {
|
|
<Self as GeneratedMessage>::serialize_generated(self)
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// These classes are polymorphic blocks in C#. The generated Rust surface keeps
|
|
// each concrete variant as a value type, so allowing it to cross the IMessage
|
|
// boundary preserves all decoded fields without an allocation-only wrapper.
|
|
message_variant!(ChatSessionAcceptInvitation);
|
|
message_variant!(ChatSessionRequestMuteUpdate);
|
|
message_variant!(ChatSessionRequestStartConference);
|
|
message_variant!(MapLayerReplyVariant);
|
|
message_variant!(ObjectMediaRequest);
|
|
message_variant!(ObjectMediaResponse);
|
|
message_variant!(ObjectMediaUpdate);
|
|
message_variant!(RemoteParcelRequestReply);
|
|
message_variant!(SearchStatRequestReply);
|
|
message_variant!(SearchStatRequestRequest);
|
|
message_variant!(UpdateAgentInventoryRequestMessage);
|
|
message_variant!(UpdateScriptAgentRequestMessage);
|
|
message_variant!(UpdateScriptTaskUpdateMessage);
|
|
message_variant!(UploaderRequestComplete);
|
|
message_variant!(UploaderRequestUpload);
|
|
|
|
/// Decodes the event names handled by the C# `MessageUtils.DecodeEvent`
|
|
/// switch. A malformed known message follows the reference behavior and is
|
|
/// returned as `None`, allowing the caller to try packet translation next.
|
|
#[allow(clippy::too_many_lines)] // Mirrors the reference decoder's explicit event-name switch.
|
|
pub(crate) fn decode_event(
|
|
event_name: &str,
|
|
map: &OSDMap,
|
|
) -> Result<Option<Box<dyn IMessage>>, Error> {
|
|
let decoded = match event_name {
|
|
"AgentGroupDataUpdate" | "AvatarGroupsReply" => {
|
|
generated::<AgentGroupDataUpdateMessage>(map)
|
|
}
|
|
"ParcelProperties" => generated::<ParcelPropertiesMessage>(map),
|
|
"ParcelObjectOwnersReply" => generated::<ParcelObjectOwnersReplyMessage>(map),
|
|
"TeleportFinish" => generated::<TeleportFinishMessage>(map),
|
|
"EnableSimulator" => generated::<EnableSimulatorMessage>(map),
|
|
"ParcelPropertiesUpdate" => generated::<ParcelPropertiesUpdateMessage>(map),
|
|
"EstablishAgentCommunication" => generated::<EstablishAgentCommunicationMessage>(map),
|
|
"ChatterBoxInvitation" => generated::<ChatterBoxInvitationMessage>(map),
|
|
"ChatterBoxSessionEventReply" => generated::<ChatterboxSessionEventReplyMessage>(map),
|
|
"ChatterBoxSessionStartReply" => generated::<ChatterBoxSessionStartReplyMessage>(map),
|
|
"ChatterBoxSessionAgentListUpdates" => {
|
|
generated::<ChatterBoxSessionAgentListUpdatesMessage>(map)
|
|
}
|
|
"RequiredVoiceVersion" => generated::<RequiredVoiceVersionMessage>(map),
|
|
"MapLayer" if map.get("LayerData").is_some() => generated::<MapLayerReplyVariant>(map),
|
|
"ChatSessionRequest" => match map
|
|
.get("method")
|
|
.unwrap_or_default()
|
|
.as_string()
|
|
.unwrap_or_default()
|
|
.as_str()
|
|
{
|
|
"start conference" => generated::<ChatSessionRequestStartConference>(map),
|
|
"mute update" => generated::<ChatSessionRequestMuteUpdate>(map),
|
|
"accept invitation" => generated::<ChatSessionAcceptInvitation>(map),
|
|
_ => None,
|
|
},
|
|
"CopyInventoryFromNotecard" => generated::<CopyInventoryFromNotecardMessage>(map),
|
|
"ProvisionVoiceAccountRequest" => generated::<ProvisionVoiceAccountRequestMessage>(map),
|
|
"Viewerstats" | "ViewerStats" => generated::<ViewerStatsMessage>(map),
|
|
"UpdateAgentLanguage" => generated::<UpdateAgentLanguageMessage>(map),
|
|
"RemoteParcelRequest" if map.get("parcel_id").is_some() => {
|
|
generated::<RemoteParcelRequestReply>(map)
|
|
}
|
|
"UpdateScriptTask" if map.get("task_id").is_some() => {
|
|
generated::<UpdateScriptTaskUpdateMessage>(map)
|
|
}
|
|
"UpdateScriptAgent" if map.get("item_id").is_some() => {
|
|
generated::<UpdateScriptAgentRequestMessage>(map)
|
|
}
|
|
"UpdateGestureAgentInventory" | "UpdateNotecardAgentInventory"
|
|
if map.get("item_id").is_some() =>
|
|
{
|
|
generated::<UpdateAgentInventoryRequestMessage>(map)
|
|
}
|
|
"SendPostcard" => generated::<SendPostcardMessage>(map),
|
|
"LandStatReply" => generated::<LandStatReplyMessage>(map),
|
|
"ParcelVoiceInfoRequest" => generated::<ParcelVoiceInfoRequestMessage>(map),
|
|
"EventQueueGet" => {
|
|
let mut message = crate::event_queue::EventQueueGetMessage::new()?;
|
|
match message.deserialize(map.clone()) {
|
|
Ok(()) => Some(Box::new(message) as Box<dyn IMessage>),
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
"CrossedRegion" => generated::<CrossedRegionMessage>(map),
|
|
"SimConsoleResponse" => generated::<SimConsoleResponseMessage>(map),
|
|
"TeleportFailed" => generated::<TeleportFailedMessage>(map),
|
|
"PlacesReply" => generated::<PlacesReplyMessage>(map),
|
|
"UpdateAgentInformation" => generated::<UpdateAgentInformationMessage>(map),
|
|
"DirLandReply" => generated::<DirLandReplyMessage>(map),
|
|
"ScriptRunningReply" => generated::<ScriptRunningReplyMessage>(map),
|
|
"AgentDropGroup" => generated::<AgentDropGroupMessage>(map),
|
|
"AgentStateUpdate" => generated::<AgentStateUpdateMessage>(map),
|
|
"NavMeshStatusUpdate" => generated::<NavMeshStatusUpdateMessage>(map),
|
|
"ForceCloseChatterBoxSession" => generated::<ForceCloseChatterBoxSessionMessage>(map),
|
|
"RegionInfo" => generated::<RegionInfoMessage>(map),
|
|
"ObjectMediaNavigate" => generated::<ObjectMediaNavigateMessage>(map),
|
|
"ObjectMedia" => match map
|
|
.get("verb")
|
|
.unwrap_or_default()
|
|
.as_string()
|
|
.unwrap_or_default()
|
|
.as_str()
|
|
{
|
|
"GET" => generated::<ObjectMediaRequest>(map),
|
|
"UPDATE" => generated::<ObjectMediaUpdate>(map),
|
|
_ if map.get("object_media_version").is_some() => generated::<ObjectMediaResponse>(map),
|
|
_ => None,
|
|
},
|
|
"SetDisplayName" => generated::<SetDisplayNameMessage>(map),
|
|
"SetDisplayNameReply" => generated::<SetDisplayNameReplyMessage>(map),
|
|
"DisplayNameUpdate" => generated::<DisplayNameUpdateMessage>(map),
|
|
"GetDisplayNames" => generated::<GetDisplayNamesMessage>(map),
|
|
"ObjectPhysicsProperties" => generated::<ObjectPhysicsPropertiesMessage>(map),
|
|
"RenderMaterials" => generated::<RenderMaterialsMessage>(map),
|
|
"GetObjectCost" => {
|
|
if map.get("object_ids").is_some() {
|
|
generated::<GetObjectCostRequest>(map)
|
|
} else {
|
|
generated::<GetObjectCostMessage>(map)
|
|
}
|
|
}
|
|
"LandResources" => {
|
|
if map.get("parcel_id").is_some() {
|
|
generated::<LandResourcesRequest>(map)
|
|
} else if map.get("ScriptResourceSummary").is_some() {
|
|
generated::<LandResourcesMessage>(map)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
"SearchStatRequest" => {
|
|
if map.get("map_clicks").is_some() {
|
|
generated::<SearchStatRequestReply>(map)
|
|
} else if map.get("classified_id").is_some() {
|
|
generated::<SearchStatRequestRequest>(map)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
"UploadBakedTexture" => match map
|
|
.get("state")
|
|
.unwrap_or_default()
|
|
.as_string()
|
|
.unwrap_or_default()
|
|
.as_str()
|
|
{
|
|
"upload" => generated::<UploaderRequestUpload>(map),
|
|
"complete" => generated::<UploaderRequestComplete>(map),
|
|
_ => None,
|
|
},
|
|
"AttachmentResources" => {
|
|
let mut message = AttachmentResourcesMessage::new()?;
|
|
match message.deserialize(map.clone()) {
|
|
Ok(()) => Some(Box::new(message) as Box<dyn IMessage>),
|
|
Err(_) => None,
|
|
}
|
|
}
|
|
"BulkUpdateInventory" => generated::<BulkUpdateInventoryMessage>(map),
|
|
_ => None,
|
|
};
|
|
Ok(decoded)
|
|
}
|
|
|
|
#[allow(clippy::needless_pass_by_value)] // Preserves MessageUtils.ToIP's generated signature.
|
|
pub(crate) fn to_ip(osd: OSD) -> Result<IpAddr, Error> {
|
|
let bytes = osd.as_binary()?;
|
|
Ok(<[u8; 4]>::try_from(bytes)
|
|
.map(Ipv4Addr::from)
|
|
.map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), IpAddr::V4))
|
|
}
|
|
|
|
#[allow(clippy::unnecessary_wraps)] // Preserves MessageUtils.FromIP's generated signature.
|
|
pub(crate) fn from_ip(address: IpAddr) -> Result<OSD, Error> {
|
|
match address {
|
|
IpAddr::V4(address) if !address.is_unspecified() => {
|
|
Ok(OSD::Binary(address.octets().to_vec()))
|
|
}
|
|
IpAddr::V6(address) if !address.is_unspecified() => {
|
|
Ok(OSD::Binary(address.octets().to_vec()))
|
|
}
|
|
IpAddr::V4(_) | IpAddr::V6(_) => Ok(OSD::Undefined),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn to_dictionary_string(osd: OSD) -> Result<HashMap<String, String>, Error> {
|
|
let OSD::Map(map) = osd else {
|
|
return Ok(HashMap::new());
|
|
};
|
|
map.into_iter()
|
|
.map(|(key, value)| Ok((key, value.as_string()?)))
|
|
.collect()
|
|
}
|
|
|
|
pub(crate) fn to_dictionary_uri(osd: OSD) -> Result<HashMap<Uri, Uri>, Error> {
|
|
let OSD::Map(map) = osd else {
|
|
return Ok(HashMap::new());
|
|
};
|
|
let mut dictionary = HashMap::with_capacity(map.len());
|
|
for (key, value) in map {
|
|
if let Some(value) = value.as_uri()? {
|
|
dictionary.insert(Uri(key), value);
|
|
}
|
|
}
|
|
Ok(dictionary)
|
|
}
|
|
|
|
pub(crate) fn from_dictionary_string(dict: HashMap<String, String>) -> Result<OSDMap, Error> {
|
|
OSDMap::new_with_dictionary(
|
|
dict.into_iter()
|
|
.map(|(key, value)| (key, OSD::String(value)))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
pub(crate) fn from_dictionary_uri(dict: HashMap<Uri, Uri>) -> Result<OSDMap, Error> {
|
|
OSDMap::new_with_dictionary(
|
|
dict.into_iter()
|
|
.map(|(key, value)| (key.0, OSD::Uri(value)))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::too_many_lines)] // The exhaustive decoder matrix stays in one test.
|
|
mod tests {
|
|
use super::*;
|
|
use flate2::Compression;
|
|
use flate2::write::ZlibEncoder;
|
|
use libremetaverse_structured_data::OSDParser;
|
|
use libremetaverse_types::UUID;
|
|
use std::io::Write as _;
|
|
|
|
fn map(entries: impl IntoIterator<Item = (&'static str, OSD)>) -> OSDMap {
|
|
OSDMap::new_with_dictionary(
|
|
entries
|
|
.into_iter()
|
|
.map(|(key, value)| (key.to_owned(), value))
|
|
.collect(),
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn known_event_and_alias_decode_to_typed_messages_with_working_trait_codec() {
|
|
let language = map([
|
|
("language", OSD::String("en-US".to_owned())),
|
|
("language_is_public", OSD::Boolean(true)),
|
|
]);
|
|
let decoded = decode_event("UpdateAgentLanguage", &language)
|
|
.unwrap()
|
|
.expect("known message");
|
|
let typed = (decoded.as_ref() as &dyn std::any::Any)
|
|
.downcast_ref::<UpdateAgentLanguageMessage>()
|
|
.expect("typed language message");
|
|
assert_eq!(typed.language, "en-US");
|
|
assert!(typed.language_public);
|
|
assert_eq!(
|
|
decoded.serialize().unwrap().get("language"),
|
|
language.get("language")
|
|
);
|
|
|
|
let group = map([
|
|
(
|
|
"AgentData",
|
|
OSD::Array(vec![OSD::Map(HashMap::from([(
|
|
"AgentID".to_owned(),
|
|
OSD::UUID(UUID::zero()),
|
|
)]))]),
|
|
),
|
|
("GroupData", OSD::Array(Vec::new())),
|
|
("NewGroupData", OSD::Array(Vec::new())),
|
|
]);
|
|
let alias = decode_event("AvatarGroupsReply", &group).unwrap();
|
|
assert!(alias.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn dynamic_reference_messages_select_the_matching_concrete_variant() {
|
|
let parcel_id = UUID::random().unwrap();
|
|
let remote = map([("parcel_id", OSD::UUID(parcel_id))]);
|
|
let decoded = decode_event("RemoteParcelRequest", &remote)
|
|
.unwrap()
|
|
.expect("remote reply");
|
|
let typed = (decoded.as_ref() as &dyn std::any::Any)
|
|
.downcast_ref::<RemoteParcelRequestReply>()
|
|
.expect("reply variant");
|
|
assert_eq!(typed.parcel_id, parcel_id);
|
|
|
|
let chat = map([
|
|
("method", OSD::String("accept invitation".to_owned())),
|
|
("session-id", OSD::UUID(UUID::random().unwrap())),
|
|
]);
|
|
let decoded = decode_event("ChatSessionRequest", &chat)
|
|
.unwrap()
|
|
.expect("chat variant");
|
|
assert!(
|
|
(decoded.as_ref() as &dyn std::any::Any)
|
|
.downcast_ref::<ChatSessionAcceptInvitation>()
|
|
.is_some()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_or_malformed_schema_returns_none_for_packet_fallback() {
|
|
let empty = OSDMap::new_with_constructor().unwrap();
|
|
assert!(decode_event("FutureGridEvent", &empty).unwrap().is_none());
|
|
assert!(decode_event("ObjectMedia", &empty).unwrap().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn every_reference_decoder_case_has_a_native_typed_path() {
|
|
let display_name = OSD::Map(HashMap::from([
|
|
("id".into(), OSD::UUID(UUID::zero())),
|
|
("username".into(), OSD::String("resident".into())),
|
|
("display_name".into(), OSD::String("Resident".into())),
|
|
("legacy_first_name".into(), OSD::String("Test".into())),
|
|
("legacy_last_name".into(), OSD::String("Resident".into())),
|
|
("is_display_name_default".into(), OSD::Boolean(false)),
|
|
("display_name_next_update".into(), OSD::Date(UNIX_EPOCH)),
|
|
("last_updated".into(), OSD::Date(UNIX_EPOCH)),
|
|
]));
|
|
let cases = [
|
|
(
|
|
"SearchStatRequest",
|
|
map([("classified_id", OSD::UUID(UUID::zero()))]),
|
|
),
|
|
(
|
|
"UploadBakedTexture",
|
|
map([
|
|
("state", OSD::String("complete".into())),
|
|
("new_asset", OSD::UUID(UUID::zero())),
|
|
]),
|
|
),
|
|
(
|
|
"ObjectMedia",
|
|
map([
|
|
("object_id", OSD::UUID(UUID::zero())),
|
|
("verb", OSD::String("GET".into())),
|
|
]),
|
|
),
|
|
(
|
|
"AttachmentResources",
|
|
map([
|
|
(
|
|
"summary",
|
|
OSD::Map(HashMap::from([
|
|
("available".into(), OSD::Array(Vec::new())),
|
|
("used".into(), OSD::Array(Vec::new())),
|
|
])),
|
|
),
|
|
("attachments", OSD::Array(Vec::new())),
|
|
]),
|
|
),
|
|
(
|
|
"LandResources",
|
|
map([("parcel_id", OSD::UUID(UUID::zero()))]),
|
|
),
|
|
(
|
|
"GetDisplayNames",
|
|
map([
|
|
("agents", OSD::Array(vec![display_name.clone()])),
|
|
("bad_ids", OSD::Array(Vec::new())),
|
|
]),
|
|
),
|
|
(
|
|
"SetDisplayNameReply",
|
|
map([
|
|
("content", display_name.clone()),
|
|
("reason", OSD::String("ok".into())),
|
|
("status", OSD::Integer(200)),
|
|
]),
|
|
),
|
|
(
|
|
"DisplayNameUpdate",
|
|
map([(
|
|
"agent",
|
|
match display_name.clone() {
|
|
OSD::Map(mut agent) => {
|
|
agent.insert("old_display_name".into(), OSD::String("Old".into()));
|
|
OSD::Map(agent)
|
|
}
|
|
_ => unreachable!(),
|
|
},
|
|
)]),
|
|
),
|
|
(
|
|
"ObjectPhysicsProperties",
|
|
map([(
|
|
"ObjectData",
|
|
OSD::Array(vec![OSD::Map(HashMap::from([
|
|
("LocalID".into(), OSD::Integer(7)),
|
|
("Density".into(), OSD::Real(1000.0)),
|
|
("Friction".into(), OSD::Real(0.5)),
|
|
("GravityMultiplier".into(), OSD::Real(1.0)),
|
|
("Restitution".into(), OSD::Real(0.2)),
|
|
("PhysicsShapeType".into(), OSD::Integer(2)),
|
|
]))]),
|
|
)]),
|
|
),
|
|
(
|
|
"BulkUpdateInventory",
|
|
map([
|
|
(
|
|
"AgentData",
|
|
OSD::Array(vec![OSD::Map(HashMap::from([
|
|
("AgentID".into(), OSD::UUID(UUID::zero())),
|
|
("TransactionID".into(), OSD::UUID(UUID::zero())),
|
|
]))]),
|
|
),
|
|
("FolderData", OSD::Array(Vec::new())),
|
|
("ItemData", OSD::Array(Vec::new())),
|
|
]),
|
|
),
|
|
(
|
|
"GetObjectCost",
|
|
map([("object_ids", OSD::Array(vec![OSD::UUID(UUID::zero())]))]),
|
|
),
|
|
];
|
|
|
|
for (name, fixture) in cases {
|
|
let decoded = decode_event(name, &fixture)
|
|
.unwrap()
|
|
.unwrap_or_else(|| panic!("{name} must decode"));
|
|
decoded.serialize().expect("typed codec must serialize");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn object_media_preserves_null_face_slots_from_the_reference_shape() {
|
|
let fixture = map([
|
|
("object_id", OSD::UUID(UUID::zero())),
|
|
(
|
|
"object_media_data",
|
|
OSD::Array(vec![
|
|
OSD::Undefined,
|
|
OSD::Map(HashMap::from([
|
|
("alt_image_enable".into(), OSD::Boolean(false)),
|
|
("auto_loop".into(), OSD::Boolean(false)),
|
|
("auto_play".into(), OSD::Boolean(true)),
|
|
("auto_scale".into(), OSD::Boolean(false)),
|
|
("auto_zoom".into(), OSD::Boolean(false)),
|
|
("controls".into(), OSD::Integer(0)),
|
|
(
|
|
"current_url".into(),
|
|
OSD::String("https://example.test/".into()),
|
|
),
|
|
("first_click_interact".into(), OSD::Boolean(true)),
|
|
("height_pixels".into(), OSD::Integer(512)),
|
|
(
|
|
"home_url".into(),
|
|
OSD::String("https://example.test/".into()),
|
|
),
|
|
("perms_control".into(), OSD::Integer(7)),
|
|
("perms_interact".into(), OSD::Integer(7)),
|
|
("whitelist".into(), OSD::Array(Vec::new())),
|
|
("whitelist_enable".into(), OSD::Boolean(false)),
|
|
("width_pixels".into(), OSD::Integer(1024)),
|
|
])),
|
|
]),
|
|
),
|
|
("object_media_version", OSD::String("x-mv:1/0".into())),
|
|
]);
|
|
let decoded = decode_event("ObjectMedia", &fixture)
|
|
.unwrap()
|
|
.expect("object media response");
|
|
let encoded = decoded.serialize().unwrap();
|
|
assert!(matches!(
|
|
encoded.get("object_media_data"),
|
|
Some(OSD::Array(values)) if matches!(values.first(), Some(OSD::Undefined))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn remaining_native_reference_switch_cases_decode_without_stubs() {
|
|
let simple_cases = [
|
|
(
|
|
"SimConsoleResponse",
|
|
map([("body", OSD::String("ok".into()))]),
|
|
),
|
|
(
|
|
"ForceCloseChatterBoxSession",
|
|
map([
|
|
("reason", OSD::String("closed".into())),
|
|
("session_id", OSD::UUID(UUID::zero())),
|
|
]),
|
|
),
|
|
(
|
|
"NavMeshStatusUpdate",
|
|
map([
|
|
("region_id", OSD::UUID(UUID::zero())),
|
|
("status", OSD::String("complete".into())),
|
|
("version", OSD::Integer(4)),
|
|
]),
|
|
),
|
|
(
|
|
"ObjectMediaNavigate",
|
|
map([
|
|
("current_url", OSD::String("https://example.test/".into())),
|
|
("object_id", OSD::UUID(UUID::zero())),
|
|
("texture_index", OSD::Integer(2)),
|
|
]),
|
|
),
|
|
(
|
|
"RegionInfo",
|
|
map([
|
|
("parcel_local_id", OSD::Integer(7)),
|
|
("region_name", OSD::String("Region".into())),
|
|
(
|
|
"voice_credentials",
|
|
OSD::Map(HashMap::from([(
|
|
"channel_uri".to_owned(),
|
|
OSD::String("sip:region@example.test".into()),
|
|
)])),
|
|
),
|
|
]),
|
|
),
|
|
(
|
|
"SetDisplayName",
|
|
map([(
|
|
"display_name",
|
|
OSD::Array(vec![OSD::String("Old".into()), OSD::String("New".into())]),
|
|
)]),
|
|
),
|
|
(
|
|
"AgentDropGroup",
|
|
map([(
|
|
"AgentData",
|
|
OSD::Array(vec![OSD::Map(HashMap::from([
|
|
("AgentID".to_owned(), OSD::UUID(UUID::zero())),
|
|
("GroupID".to_owned(), OSD::UUID(UUID::zero())),
|
|
]))]),
|
|
)]),
|
|
),
|
|
];
|
|
for (name, fixture) in simple_cases {
|
|
assert!(
|
|
decode_event(name, &fixture).unwrap().is_some(),
|
|
"{name} must decode"
|
|
);
|
|
}
|
|
|
|
let object_id = UUID::random().unwrap();
|
|
let costs = OSDMap::new_with_dictionary(HashMap::from([(
|
|
object_id.to_string(),
|
|
OSD::Map(HashMap::from([
|
|
("linked_set_resource_cost".into(), OSD::Real(1.0)),
|
|
("resource_cost".into(), OSD::Real(2.0)),
|
|
("physics_cost".into(), OSD::Real(3.0)),
|
|
("linked_set_physics_cost".into(), OSD::Real(4.0)),
|
|
])),
|
|
)]))
|
|
.unwrap();
|
|
assert!(decode_event("GetObjectCost", &costs).unwrap().is_some());
|
|
|
|
let binary = OSDParser::serialize_llsd_binary_with_osd(OSD::Map(HashMap::from([(
|
|
"material".to_owned(),
|
|
OSD::String("native".to_owned()),
|
|
)])))
|
|
.unwrap();
|
|
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::best());
|
|
encoder.write_all(&binary).unwrap();
|
|
let render = map([("Zipped", OSD::Binary(encoder.finish().unwrap()))]);
|
|
let decoded = decode_event("RenderMaterials", &render)
|
|
.unwrap()
|
|
.expect("render materials");
|
|
let typed = (decoded.as_ref() as &dyn std::any::Any)
|
|
.downcast_ref::<RenderMaterialsMessage>()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(&typed.material_data, OSD::Map(values) if values.get("material") == Some(&OSD::String("native".into())))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn message_utils_ip_and_dictionary_conversions_match_reference_shapes() {
|
|
assert_eq!(
|
|
to_ip(OSD::Binary(vec![127, 0, 0, 1])).unwrap(),
|
|
"127.0.0.1".parse::<IpAddr>().unwrap()
|
|
);
|
|
assert_eq!(
|
|
to_ip(OSD::Binary(vec![1, 2, 3])).unwrap(),
|
|
IpAddr::V4(Ipv4Addr::UNSPECIFIED)
|
|
);
|
|
assert_eq!(
|
|
from_ip("192.0.2.1".parse().unwrap()).unwrap(),
|
|
OSD::Binary(vec![192, 0, 2, 1])
|
|
);
|
|
assert_eq!(
|
|
from_ip(IpAddr::V4(Ipv4Addr::UNSPECIFIED)).unwrap(),
|
|
OSD::Undefined
|
|
);
|
|
|
|
let strings = HashMap::from([
|
|
("a".to_owned(), "one".to_owned()),
|
|
("b".to_owned(), "two".to_owned()),
|
|
]);
|
|
let map = from_dictionary_string(strings.clone()).unwrap();
|
|
assert_eq!(
|
|
to_dictionary_string(OSD::Map(map.snapshot())).unwrap(),
|
|
strings
|
|
);
|
|
|
|
let uris = HashMap::from([(
|
|
Uri("https://key.example.test/".to_owned()),
|
|
Uri("https://value.example.test/".to_owned()),
|
|
)]);
|
|
let map = from_dictionary_uri(uris.clone()).unwrap();
|
|
assert_eq!(to_dictionary_uri(OSD::Map(map.snapshot())).unwrap(), uris);
|
|
assert!(to_dictionary_string(OSD::Integer(1)).unwrap().is_empty());
|
|
}
|
|
}
|