From aa269f17851333ff33a49a14dc6c59bee72e193b Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 9 Aug 2026 16:49:20 +0000 Subject: [PATCH] Implement capability event queue processing (#55) --- README.md | 6 + api/SHIM-COVERAGE.md | 2 +- crates/libremetaverse/src/caps.rs | 773 +++++++++ crates/libremetaverse/src/event_queue.rs | 1120 +++++++++++++ crates/libremetaverse/src/generated.rs | 1564 ++++++++---------- crates/libremetaverse/src/lib.rs | 3 + crates/libremetaverse/src/message_codec.rs | 841 +++++++++- crates/libremetaverse/src/message_decoder.rs | 1267 ++++++++++++++ crates/libremetaverse/src/network_manager.rs | 279 +++- docs/caps-http.md | 26 +- tools/generate_api_shims.py | 79 +- 11 files changed, 5041 insertions(+), 919 deletions(-) create mode 100644 crates/libremetaverse/src/caps.rs create mode 100644 crates/libremetaverse/src/event_queue.rs create mode 100644 crates/libremetaverse/src/message_decoder.rs diff --git a/README.md b/README.md index ce5390f..0594cb5 100644 --- a/README.md +++ b/README.md @@ -294,3 +294,9 @@ retry handling while preserving permanent 401/403/404/410 failures. Capability URLs and tokens are absent from errors and diagnostics. Ownership, limits, injection, and offline fake-server coverage are documented in [`docs/caps-http.md`](docs/caps-http.md). +Seed-cap discovery and `EventQueueGet` are native Rust too: the full reference +capability list is posted as LLSD/XML, accepted URIs are rate-categorized, and +bounded long polls preserve ack IDs, reconnect retries, shutdown `done`, and +event order. Typed Linden messages share the CAPS dispatch registry; unknown +messages remain observable and then use the generated caps-to-packet catalog so +packet handlers receive them through the same bounded pipeline as UDP traffic. diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 2bb51bf..e433723 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | Assembly | Types | Members | Status | |---|---:|---:|---| -| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 53 types / 13,673 members; remaining surface is callable failure-only shims | +| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 68 types / 13,858 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | diff --git a/crates/libremetaverse/src/caps.rs b/crates/libremetaverse/src/caps.rs new file mode 100644 index 0000000..fae3b16 --- /dev/null +++ b/crates/libremetaverse/src/caps.rs @@ -0,0 +1,773 @@ +//! Seed-capability discovery and capability event routing. + +#![allow(clippy::missing_errors_doc)] // Public Result shapes are fixed by the compatibility map. +#![allow(clippy::needless_pass_by_value)] // Mapped APIs preserve owned CLR argument shapes. + +use crate::event_queue::{ + EventQueueClient, EventQueueClientConnectedCallback, EventQueueClientEventCallback, +}; +use crate::packets::Packet; +use crate::{Error, Simulator}; +use libremetaverse_structured_data::{OSD, OSDArray, OSDFormat, OSDMap, OSDParser}; +use libremetaverse_types::compat::{ + CancellationToken, CancellationTokenSource, EventHandler, Subscription, Uri, +}; +use std::collections::HashMap; +use std::fmt; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, RwLock, Weak}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +const INITIAL_SEED_RETRY: Duration = Duration::from_secs(1); +const MAXIMUM_SEED_RETRY: Duration = Duration::from_secs(30); + +fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn read(value: &RwLock) -> std::sync::RwLockReadGuard<'_, T> { + value + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn write(value: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { + value + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +struct CapabilitiesEventState { + next_id: AtomicU64, + handlers: Mutex)>>, +} + +impl Default for CapabilitiesEventState { + fn default() -> Self { + Self { + next_id: AtomicU64::new(1), + handlers: Mutex::new(Vec::new()), + } + } +} + +#[derive(Clone)] +pub struct CapabilitiesReceivedEventArgs { + simulator: Simulator, +} + +impl CapabilitiesReceivedEventArgs { + pub const fn new(simulator: Simulator) -> Result { + Ok(Self { simulator }) + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + +struct SeedTask { + cancellation: CancellationTokenSource, + handle: JoinHandle<()>, +} + +pub(crate) struct CapsInner { + simulator: Weak, + seed_uri: Uri, + caps: RwLock>, + event_queue: Mutex>, + seed_task: Mutex>, + events: Arc, + disconnected: AtomicBool, +} + +impl CapsInner { + fn simulator(&self) -> Option { + Simulator::native_from_weak(&self.simulator) + } + + pub(crate) fn disconnect(&self, immediate: bool) { + if self.disconnected.swap(true, Ordering::AcqRel) { + return; + } + if let Some(task) = mutex(&self.seed_task).take() { + task.cancellation.cancel(); + if task.handle.thread().id() != thread::current().id() { + let _ = task.handle.join(); + } + } + if let Some(queue) = mutex(&self.event_queue).take() { + let _ = queue.stop(immediate); + let _ = queue.dispose(); + } + } + + fn emit_capabilities_received(&self, simulator: Simulator) { + let handlers: Vec<_> = mutex(&self.events.handlers) + .iter() + .map(|(_, handler)| Arc::clone(handler)) + .collect(); + for handler in handlers { + let args = CapabilitiesReceivedEventArgs { + simulator: simulator.clone(), + }; + let _ = catch_unwind(AssertUnwindSafe(|| handler(args))); + } + } + + fn route_event(&self, event_name: String, body: OSDMap) { + let Some(simulator) = self.simulator() else { + return; + }; + match crate::message_decoder::decode_event(&event_name, &body) { + Ok(Some(message)) => { + simulator.native_dispatch_caps_event(&event_name, message.as_ref()); + } + Ok(None) | Err(_) => { + if let Ok(Some(packet)) = Packet::build_packet_with_string_osd_map(event_name, body) + { + let _ = simulator.native_enqueue_caps_packet(packet); + } + } + } + } +} + +impl Drop for CapsInner { + fn drop(&mut self) { + self.disconnected.store(true, Ordering::Release); + if let Some(task) = self + .seed_task + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take() + { + task.cancellation.cancel(); + if task.handle.thread().id() != thread::current().id() { + let _ = task.handle.join(); + } + } + if let Some(queue) = self + .event_queue + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take() + { + let _ = queue.dispose(); + } + } +} + +/// Capability collection associated with a simulator. +pub struct Caps { + pub simulator: Box, + inner: Arc, +} + +impl Clone for Caps { + fn clone(&self) -> Self { + Self { + simulator: Box::new(self.simulator.native_clone_without_caps()), + inner: Arc::clone(&self.inner), + } + } +} + +impl Caps { + pub(crate) fn native_create( + simulator: &Simulator, + seed_uri: Uri, + ) -> Result, Error> { + if !is_http_uri(&seed_uri) { + return Err(Error::Argument); + } + let inner = Arc::new(CapsInner { + simulator: simulator.native_data_weak(), + seed_uri, + caps: RwLock::new(HashMap::new()), + event_queue: Mutex::new(None), + seed_task: Mutex::new(None), + events: Arc::new(CapabilitiesEventState::default()), + disconnected: AtomicBool::new(false), + }); + start_seed_task(&inner)?; + Ok(inner) + } + + pub(crate) fn native_from_inner(inner: Arc, simulator: Simulator) -> Self { + Self { + simulator: Box::new(simulator.native_clone_without_caps()), + inner, + } + } + + pub fn subscribe_capabilities_received( + &self, + handler: Option>, + ) -> Subscription { + let Some(handler) = handler else { + return Subscription::detached(); + }; + let id = self.inner.events.next_id.fetch_add(1, Ordering::Relaxed); + mutex(&self.inner.events.handlers).push((id, handler)); + let state = Arc::downgrade(&self.inner.events); + Subscription::new(move || { + if let Some(state) = state.upgrade() { + mutex(&state.handlers).retain(|(candidate, _)| *candidate != id); + } + }) + } + + #[must_use] + /// Returns the fixed capability names requested from a seed capability. + /// + /// # Panics + /// + /// Panics only if constructing an in-memory LLSD array fails. + pub fn all_capabilities() -> OSDArray { + OSDArray::new_with_list( + ALL_CAPABILITIES + .iter() + .map(|name| OSD::String((*name).to_owned())) + .collect(), + ) + .expect("static capability list is valid") + } + + pub fn capabilities(&self) -> Result, Error> { + let mut names: Vec<_> = read(&self.inner.caps).keys().cloned().collect(); + names.sort_unstable(); + Ok(names) + } + + pub fn capability_name_from_uri(&self, cap: Uri) -> Result { + read(&self.inner.caps) + .iter() + .find_map(|(name, uri)| (uri == &cap).then(|| name.clone())) + .ok_or(Error::InvalidOperation) + } + + pub fn capability_uri(&self, capability: String) -> Result, Error> { + Ok(read(&self.inner.caps).get(&capability).cloned()) + } + + pub fn disconnect(&self, immediate: bool) -> Result<(), Error> { + self.inner.disconnect(immediate); + Ok(()) + } + + pub fn get_mesh_cap_uri(&self) -> Result, Error> { + let caps = read(&self.inner.caps); + Ok(caps + .get("ViewerAsset") + .or_else(|| caps.get("GetMesh2")) + .or_else(|| caps.get("GetMesh")) + .cloned()) + } + + pub fn get_texture_cap_uri(&self) -> Result, Error> { + let caps = read(&self.inner.caps); + Ok(caps + .get("ViewerAsset") + .or_else(|| caps.get("GetTexture")) + .cloned()) + } + + #[must_use] + pub fn event_queue(&self) -> Option { + mutex(&self.inner.event_queue).clone() + } + + #[must_use] + pub fn is_event_queue_running(&self) -> bool { + mutex(&self.inner.event_queue) + .as_ref() + .is_some_and(EventQueueClient::running) + } + + #[must_use] + pub fn seed_caps_uri(&self) -> Uri { + self.inner.seed_uri.clone() + } +} + +impl fmt::Debug for Caps { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Caps") + .field("capability_count", &read(&self.inner.caps).len()) + .field("event_queue_running", &self.is_event_queue_running()) + .field( + "disconnected", + &self.inner.disconnected.load(Ordering::Acquire), + ) + .finish_non_exhaustive() + } +} + +fn start_seed_task(inner: &Arc) -> Result<(), Error> { + let cancellation = CancellationTokenSource::new(); + let token = cancellation.token(); + let weak = Arc::downgrade(inner); + let (ready_sender, ready_receiver) = std::sync::mpsc::sync_channel(1); + let handle = thread::Builder::new() + .name("libremetaverse-seed-caps".to_owned()) + .spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build(); + let Ok(runtime) = runtime else { + let _ = ready_sender.send(false); + return; + }; + let _ = ready_sender.send(true); + runtime.block_on(run_seed_requests(weak, token)); + }) + .map_err(|_| Error::InvalidOperation)?; + if ready_receiver.recv_timeout(Duration::from_secs(2)) != Ok(true) { + cancellation.cancel(); + let _ = handle.join(); + return Err(Error::InvalidOperation); + } + *mutex(&inner.seed_task) = Some(SeedTask { + cancellation, + handle, + }); + Ok(()) +} + +async fn run_seed_requests(weak: Weak, cancellation: CancellationToken) { + let mut retry = 0_u32; + loop { + let Some(inner) = weak.upgrade() else { + return; + }; + if cancellation.is_cancellation_requested() || inner.disconnected.load(Ordering::Acquire) { + return; + } + let Some(simulator) = inner.simulator() else { + return; + }; + if !simulator.native_is_connected() { + return; + } + let http = simulator.client.native_http_caps_client(); + let response = http + .post_with_uri_osd_format_osd_cancellation_token_i_progress( + inner.seed_uri.clone(), + OSDFormat::Xml, + OSD::Array(Caps::all_capabilities().snapshot()), + cancellation.clone(), + None, + ) + .await; + match response { + Ok((response, data)) if response.is_success_status_code() => { + if install_seed_response(&inner, &simulator, data).is_ok() { + return; + } + } + Ok((response, _)) if response.status_code == 404 => return, + Err(Error::Cancelled) if cancellation.is_cancellation_requested() => return, + Ok(_) | Err(_) => {} + } + retry = retry.saturating_add(1); + let delay = seed_retry_delay(&inner.seed_uri, retry); + tokio::select! { + () = tokio::time::sleep(delay) => {} + () = cancellation.cancelled() => return, + } + } +} + +fn install_seed_response( + inner: &Arc, + simulator: &Simulator, + data: Vec, +) -> Result<(), Error> { + let OSD::Map(map) = OSDParser::deserialize_with_bytes(data)? else { + return Err(Error::Parse { + position: 0, + context: "seed capability response is not a map", + }); + }; + let mut discovered = HashMap::with_capacity(map.len()); + for (name, value) in map { + if let Some(uri) = value.as_uri()? { + if !is_http_uri(&uri) { + continue; + } + simulator + .client + .native_caps_rate_limiter() + .register_cap_uri(name.clone(), uri.clone())?; + discovered.insert(name, uri); + } + } + *write(&inner.caps) = discovered; + + if let Some(uri) = read(&inner.caps).get("EventQueueGet").cloned() { + let mut queue = EventQueueClient::new_for_caps(uri, simulator.native_clone_without_caps())?; + let connected_inner = Arc::downgrade(inner); + queue.on_connected = Some(EventQueueClientConnectedCallback::from_handler(move || { + if let Some(inner) = connected_inner.upgrade() + && let Some(simulator) = inner.simulator() + { + simulator.native_raise_event_queue_running(); + } + })); + let event_inner = Arc::downgrade(inner); + queue.on_event = Some(EventQueueClientEventCallback::from_handler( + move |event_name, body| { + if let Some(inner) = event_inner.upgrade() { + inner.route_event(event_name, body); + } + }, + )); + queue.start()?; + *mutex(&inner.event_queue) = Some(queue); + } + inner.emit_capabilities_received(simulator.clone()); + Ok(()) +} + +fn seed_retry_delay(uri: &Uri, retry: u32) -> Duration { + let multiplier = 1_u32 + .checked_shl(retry.saturating_sub(1).min(30)) + .unwrap_or(u32::MAX); + let base = INITIAL_SEED_RETRY + .saturating_mul(multiplier) + .min(MAXIMUM_SEED_RETRY); + let mut hash = 0xcbf2_9ce4_8422_2325_u64 ^ u64::from(retry); + for byte in uri.0.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100_0000_01b3); + } + let max_jitter = base / 8; + if max_jitter.is_zero() { + return base; + } + let nanos = u64::try_from(max_jitter.as_nanos()) + .unwrap_or(u64::MAX) + .saturating_add(1); + base.saturating_add(Duration::from_nanos(hash % nanos)) +} + +fn is_http_uri(uri: &Uri) -> bool { + reqwest::Url::parse(&uri.0) + .ok() + .is_some_and(|uri| matches!(uri.scheme(), "http" | "https") && uri.host().is_some()) +} + +const ALL_CAPABILITIES: &[&str] = &[ + "AbuseCategories", + "AcceptFriendship", + "AcceptGroupInvite", + "AgentPreferences", + "AgentProfile", + "AgentState", + "AttachmentResources", + "AvatarPickerSearch", + "AvatarRenderInfo", + "CharacterProperties", + "ChatSessionRequest", + "CopyInventoryFromNotecard", + "CreateInventoryCategory", + "DeclineFriendship", + "DeclineGroupInvite", + "DispatchRegionInfo", + "DirectDelivery", + "EnvironmentSettings", + "EstateAccess", + "EstateChangeInfo", + "EventQueueGet", + "ExtEnvironment", + "FetchLib2", + "FetchLibDescendents2", + "FetchInventory2", + "FetchInventoryDescendents2", + "IncrementCOFVersion", + "RequestTaskInventory", + "InterestList", + "InventoryThumbnailUpload", + "GetDisplayNames", + "GetExperiences", + "AgentExperiences", + "FindExperienceByName", + "GetExperienceInfo", + "GetAdminExperiences", + "GetCreatorExperiences", + "ExperiencePreferences", + "GroupExperiences", + "UpdateExperience", + "IsExperienceAdmin", + "IsExperienceContributor", + "RegionExperiences", + "ExperienceQuery", + "GetMesh", + "GetMesh2", + "GetMetadata", + "GetObjectCost", + "GetObjectPhysicsData", + "GetTexture", + "GroupAPIv1", + "GroupMemberData", + "GroupProposalBallot", + "HomeLocation", + "LandResources", + "LSLSyntax", + "MapLayer", + "MapLayerGod", + "MeshUploadFlag", + "ModifyMaterialParams", + "ModifyRegion", + "NavMeshGenerationStatus", + "NewFileAgentInventory", + "NewFileAgentInventoryVariablePrice", + "ObjectAnimation", + "ObjectMedia", + "ObjectMediaNavigate", + "ObjectNavMeshProperties", + "ParcelPropertiesUpdate", + "ParcelVoiceInfoRequest", + "ProductInfoRequest", + "ProvisionVoiceAccountRequest", + "ReadOfflineMsgs", + "RegionObjects", + "RegionSchedule", + "RemoteParcelRequest", + "RenderMaterials", + "RequestTextureDownload", + "ResourceCostSelected", + "RetrieveNavMeshSrc", + "SearchStatRequest", + "SearchStatTracking", + "SendPostcard", + "SendUserReport", + "SendUserReportWithScreenshot", + "ServerReleaseNotes", + "SetDisplayName", + "SimConsoleAsync", + "SimulatorFeatures", + "StartGroupProposal", + "TerrainNavMeshProperties", + "TextureStats", + "UntrustedSimulatorMessage", + "UpdateAgentInformation", + "UpdateAgentLanguage", + "UpdateAvatarAppearance", + "UpdateGestureAgentInventory", + "UpdateGestureTaskInventory", + "UpdateNotecardAgentInventory", + "UpdateNotecardTaskInventory", + "UpdateScriptAgent", + "UpdateScriptTask", + "UpdateSettingsAgentInventory", + "UpdateSettingsTaskInventory", + "UploadAgentProfileImage", + "UpdateMaterialAgentInventory", + "UpdateMaterialTaskInventory", + "UploadBakedTexture", + "UserInfo", + "ViewerAsset", + "ViewerBenefits", + "ViewerMetrics", + "ViewerStartAuction", + "ViewerStats", + "VoiceSignalingRequest", + "InventoryAPIv3", + "LibraryAPIv3", +]; + +#[cfg(test)] +mod tests { + use super::*; + use crate::{GridClient, HttpCapsClient}; + use libremetaverse_types::compat::{HttpMessageHandler, HttpRequest, HttpResponse}; + use std::collections::BTreeMap; + use std::sync::mpsc; + use std::time::Instant; + + fn lock(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + } + + fn response(status_code: u16, body: Vec) -> HttpResponse { + HttpResponse { + status_code, + headers: BTreeMap::new(), + content_type: Some(HttpCapsClient::LLSD_XML.to_owned()), + body, + } + } + + fn seed_body() -> Vec { + OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([ + ( + "EventQueueGet".to_owned(), + OSD::Uri(Uri( + "https://caps.example.test/queue?token=eq-secret".to_owned() + )), + ), + ( + "ViewerAsset".to_owned(), + OSD::Uri(Uri("https://asset.example.test/viewer".to_owned())), + ), + ( + "GetMesh2".to_owned(), + OSD::Uri(Uri("https://asset.example.test/mesh2".to_owned())), + ), + ( + "GetTexture".to_owned(), + OSD::Uri(Uri("https://asset.example.test/texture".to_owned())), + ), + ( + "InvalidScheme".to_owned(), + OSD::Uri(Uri("file:///private/capability".to_owned())), + ), + ]))) + .unwrap() + } + + #[test] + fn seed_discovery_preserves_request_shape_registers_caps_and_starts_queue() { + let requests = Arc::new(Mutex::new(Vec::::new())); + let recorded = Arc::clone(&requests); + let release_seed = Arc::new(AtomicBool::new(false)); + let release = Arc::clone(&release_seed); + let handler = HttpMessageHandler::new(move |request, _cancellation| { + let recorded = Arc::clone(&recorded); + let release = Arc::clone(&release); + async move { + let is_seed = request.uri.0.contains("/seed"); + lock(&recorded).push(request); + if is_seed { + while !release.load(Ordering::Acquire) { + tokio::time::sleep(Duration::from_millis(1)).await; + } + response(200, seed_body()) + } else { + response(404, Vec::new()) + } + } + }); + let mut client = GridClient::new().unwrap(); + client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); + let simulator = + Simulator::new(client, "127.0.0.1:13001".parse().unwrap(), 2, None, None).unwrap(); + simulator.native_set_connected_for_tests(true); + simulator + .native_set_seed_caps( + Some(Uri( + "https://caps.example.test/seed?token=seed-secret".to_owned() + )), + Some(false), + ) + .unwrap(); + let caps = simulator.clone().caps.expect("caps snapshot"); + let (sender, receiver) = mpsc::channel(); + let _subscription = caps.subscribe_capabilities_received(Some(Arc::new(move |args| { + sender.send(args.simulator().handle).unwrap(); + }))); + release_seed.store(true, Ordering::Release); + assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), 2); + + assert_eq!( + caps.seed_caps_uri(), + Uri("https://caps.example.test/seed?token=seed-secret".to_owned()) + ); + assert_eq!( + caps.get_texture_cap_uri().unwrap(), + Some(Uri("https://asset.example.test/viewer".to_owned())) + ); + assert_eq!( + caps.get_mesh_cap_uri().unwrap(), + Some(Uri("https://asset.example.test/viewer".to_owned())) + ); + assert_eq!( + caps.capability_name_from_uri(Uri("https://asset.example.test/mesh2".to_owned())) + .unwrap(), + "GetMesh2" + ); + assert_eq!( + caps.capability_uri("InvalidScheme".to_owned()).unwrap(), + None + ); + assert!(caps.event_queue().is_some()); + + let deadline = Instant::now() + Duration::from_secs(2); + while lock(&requests).len() < 2 && Instant::now() < deadline { + thread::sleep(Duration::from_millis(5)); + } + let requests = lock(&requests); + let OSD::Array(requested) = + OSDParser::deserialize_llsd_xml_with_bytes(requests[0].body.clone()).unwrap() + else { + panic!("capability request array") + }; + let requested: Vec<_> = requested + .iter() + .map(|value| value.as_string().unwrap()) + .collect(); + assert_eq!( + requested, + ALL_CAPABILITIES + .iter() + .map(|value| (*value).to_owned()) + .collect::>() + ); + assert_eq!( + requests[0].content_type.as_deref(), + Some(HttpCapsClient::LLSD_XML) + ); + drop(requests); + caps.disconnect(true).unwrap(); + } + + #[test] + fn all_capabilities_matches_reference_and_debug_redacts_seed_uri() { + let values: Vec<_> = Caps::all_capabilities() + .snapshot() + .into_iter() + .map(|value| value.as_string().unwrap()) + .collect(); + assert_eq!( + values, + ALL_CAPABILITIES + .iter() + .map(|value| (*value).to_owned()) + .collect::>() + ); + assert!(values.contains(&"EventQueueGet".to_owned())); + assert!(values.contains(&"InventoryAPIv3".to_owned())); + + let handler = HttpMessageHandler::new(|_request, cancellation| async move { + cancellation.cancelled().await; + response(499, Vec::new()) + }); + let mut client = GridClient::new().unwrap(); + client.set_http_caps_client(HttpCapsClient::new(handler).unwrap()); + let simulator = + Simulator::new(client, "127.0.0.1:13002".parse().unwrap(), 3, None, None).unwrap(); + simulator.native_set_connected_for_tests(true); + simulator + .native_set_seed_caps( + Some(Uri( + "https://caps.example.test/seed?token=do-not-log".to_owned() + )), + Some(false), + ) + .unwrap(); + let caps = simulator.clone().caps.unwrap(); + let debug = format!("{caps:?}"); + assert!(!debug.contains("do-not-log")); + caps.disconnect(true).unwrap(); + } +} diff --git a/crates/libremetaverse/src/event_queue.rs b/crates/libremetaverse/src/event_queue.rs new file mode 100644 index 0000000..60a70f1 --- /dev/null +++ b/crates/libremetaverse/src/event_queue.rs @@ -0,0 +1,1120 @@ +//! Bounded native implementation of the long-polling capabilities event queue. + +#![allow(clippy::missing_errors_doc)] // Public Result shapes are fixed by the compatibility map. +#![allow(clippy::needless_pass_by_value)] // Mapped APIs preserve owned CLR argument shapes. +#![allow(clippy::type_complexity)] // Delegate signatures are fixed by the compatibility map. + +use crate::interfaces::IMessage; +use crate::{Error, HttpCapsClient, Simulator}; +use libremetaverse_structured_data::{OSD, OSDMap, OSDParser}; +use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource, Object, Uri}; +use std::collections::HashMap; +use std::fmt; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, Weak}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +const LLSD_XML: &str = "application/llsd+xml"; + +fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Explicit event-queue resource and retry policy. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct EventQueuePolicy { + pub max_events_per_response: usize, + pub max_event_nodes: usize, + pub max_event_binary_bytes: usize, + pub initial_retry_delay: Duration, + pub maximum_retry_delay: Duration, + pub graceful_shutdown_timeout: Duration, +} + +impl Default for EventQueuePolicy { + fn default() -> Self { + Self { + max_events_per_response: 1_024, + max_event_nodes: 100_000, + max_event_binary_bytes: 8 * 1024 * 1024, + initial_retry_delay: Duration::from_secs(1), + maximum_retry_delay: Duration::from_secs(30), + graceful_shutdown_timeout: Duration::from_secs(2), + } + } +} + +impl EventQueuePolicy { + fn validate(&self) -> Result<(), Error> { + if self.max_events_per_response == 0 + || self.max_event_nodes == 0 + || self.max_event_binary_bytes == 0 + || self.initial_retry_delay.is_zero() + || self.maximum_retry_delay < self.initial_retry_delay + || self.graceful_shutdown_timeout.is_zero() + { + return Err(Error::Argument); + } + Ok(()) + } +} + +type ConnectedHandler = dyn Fn() + Send + Sync; + +/// Native representation of the C# connected delegate. +#[derive(Clone)] +pub struct EventQueueClientConnectedCallback { + handler: Arc, +} + +impl EventQueueClientConnectedCallback { + #[must_use] + pub fn from_handler(handler: impl Fn() + Send + Sync + 'static) -> Self { + Self { + handler: Arc::new(handler), + } + } + + pub fn new(_object: Object, _method: isize) -> Result { + Err(Error::InvalidOperation) + } + + pub fn begin_invoke( + &self, + callback: Box, + object: Object, + ) -> Result, Error> { + self.invoke()?; + callback(&object); + Ok(Box::new(object)) + } + + pub fn end_invoke(&self, _result: Box) -> Result<(), Error> { + Ok(()) + } + + pub fn invoke(&self) -> Result<(), Error> { + (self.handler)(); + Ok(()) + } + + fn invoke_safely(&self) { + let _ = catch_unwind(AssertUnwindSafe(|| (self.handler)())); + } +} + +impl fmt::Debug for EventQueueClientConnectedCallback { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("EventQueueClientConnectedCallback()") + } +} + +type EventHandler = dyn Fn(String, OSDMap) + Send + Sync; + +/// Native representation of the C# raw event delegate. +#[derive(Clone)] +pub struct EventQueueClientEventCallback { + handler: Arc, +} + +impl EventQueueClientEventCallback { + #[must_use] + pub fn from_handler(handler: impl Fn(String, OSDMap) + Send + Sync + 'static) -> Self { + Self { + handler: Arc::new(handler), + } + } + + pub fn new(_object: Object, _method: isize) -> Result { + Err(Error::InvalidOperation) + } + + pub fn begin_invoke( + &self, + event_name: String, + body: OSDMap, + callback: Box, + object: Object, + ) -> Result, Error> { + self.invoke(event_name, body)?; + callback(&object); + Ok(Box::new(object)) + } + + pub fn end_invoke(&self, _result: Box) -> Result<(), Error> { + Ok(()) + } + + pub fn invoke(&self, event_name: String, body: OSDMap) -> Result<(), Error> { + (self.handler)(event_name, body); + Ok(()) + } + + fn invoke_safely(&self, event_name: String, body: OSDMap) { + let _ = catch_unwind(AssertUnwindSafe(|| (self.handler)(event_name, body))); + } +} + +impl fmt::Debug for EventQueueClientEventCallback { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("EventQueueClientEventCallback()") + } +} + +#[derive(Clone, Default)] +struct QueueCallbacks { + connected: Option, + event: Option, +} + +struct QueueTask { + cancellation: CancellationTokenSource, + graceful: Arc, + handle: JoinHandle<()>, +} + +enum SimulatorReference { + Strong(Arc), + Weak(Weak), +} + +impl SimulatorReference { + fn simulator(&self) -> Option { + match self { + Self::Strong(data) => Some(Simulator::native_from_data(Arc::clone(data))), + Self::Weak(data) => Simulator::native_from_weak(data), + } + } +} + +struct EventQueueClientInner { + address: Uri, + simulator: SimulatorReference, + http: HttpCapsClient, + policy: EventQueuePolicy, + callbacks: Mutex, + task: Mutex>, + running: AtomicBool, + disposed: AtomicBool, +} + +impl EventQueueClientInner { + fn simulator(&self) -> Option { + self.simulator.simulator() + } + + fn stop_task(&self, immediate: bool) { + let task = mutex(&self.task).take(); + let Some(task) = task else { + self.running.store(false, Ordering::Release); + return; + }; + task.graceful.store(!immediate, Ordering::Release); + task.cancellation.cancel(); + if task.handle.thread().id() != thread::current().id() { + let _ = task.handle.join(); + } + self.running.store(false, Ordering::Release); + } +} + +impl Drop for EventQueueClientInner { + fn drop(&mut self) { + self.disposed.store(true, Ordering::Release); + if let Some(task) = self + .task + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take() + { + task.cancellation.cancel(); + if task.handle.thread().id() != thread::current().id() { + let _ = task.handle.join(); + } + } + } +} + +/// Long-polling `EventQueueGet` client with bounded parsing and deterministic ordering. +pub struct EventQueueClient { + pub on_connected: Option, + pub on_event: Option, + inner: Arc, +} + +impl Clone for EventQueueClient { + fn clone(&self) -> Self { + Self { + on_connected: self.on_connected.clone(), + on_event: self.on_event.clone(), + inner: Arc::clone(&self.inner), + } + } +} + +impl EventQueueClient { + pub fn new(event_queue_location: Uri, sim: Simulator) -> Result { + Self::with_policy(event_queue_location, sim, EventQueuePolicy::default()) + } + + /// Rust extension allowing deterministic retry timing in offline tests and embedders. + pub fn with_policy( + event_queue_location: Uri, + sim: Simulator, + policy: EventQueuePolicy, + ) -> Result { + Self::with_policy_and_reference( + event_queue_location, + &sim, + policy, + SimulatorReference::Strong(sim.native_data_arc()), + ) + } + + pub(crate) fn new_for_caps(event_queue_location: Uri, sim: Simulator) -> Result { + Self::with_policy_and_reference( + event_queue_location, + &sim, + EventQueuePolicy::default(), + SimulatorReference::Weak(sim.native_data_weak()), + ) + } + + fn with_policy_and_reference( + event_queue_location: Uri, + sim: &Simulator, + policy: EventQueuePolicy, + simulator: SimulatorReference, + ) -> Result { + policy.validate()?; + if !is_http_uri(&event_queue_location) { + return Err(Error::Argument); + } + let http = sim.client.native_http_caps_client(); + Ok(Self { + on_connected: None, + on_event: None, + inner: Arc::new(EventQueueClientInner { + address: event_queue_location, + simulator, + http, + policy, + callbacks: Mutex::new(QueueCallbacks::default()), + task: Mutex::new(None), + running: AtomicBool::new(false), + disposed: AtomicBool::new(false), + }), + }) + } + + pub fn dispose(&self) -> Result<(), Error> { + if self.inner.disposed.swap(true, Ordering::AcqRel) { + return Ok(()); + } + self.inner.stop_task(true); + Ok(()) + } + + pub fn start(&self) -> Result<(), Error> { + if self.inner.disposed.load(Ordering::Acquire) { + return Err(Error::InvalidOperation); + } + let mut slot = mutex(&self.inner.task); + if self.inner.running.load(Ordering::Acquire) { + return Ok(()); + } + if let Some(previous) = slot.take() + && previous.handle.thread().id() != thread::current().id() + { + let _ = previous.handle.join(); + } + *mutex(&self.inner.callbacks) = QueueCallbacks { + connected: self.on_connected.clone(), + event: self.on_event.clone(), + }; + let cancellation = CancellationTokenSource::new(); + let token = cancellation.token(); + let graceful = Arc::new(AtomicBool::new(false)); + let graceful_worker = Arc::clone(&graceful); + let weak = Arc::downgrade(&self.inner); + let (ready_sender, ready_receiver) = std::sync::mpsc::sync_channel(1); + self.inner.running.store(true, Ordering::Release); + let handle = thread::Builder::new() + .name("libremetaverse-event-queue".to_owned()) + .spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build(); + let Ok(runtime) = runtime else { + if let Some(inner) = weak.upgrade() { + inner.running.store(false, Ordering::Release); + } + let _ = ready_sender.send(false); + return; + }; + let _ = ready_sender.send(true); + runtime.block_on(run_event_queue(weak.clone(), token, graceful_worker)); + if let Some(inner) = weak.upgrade() { + inner.running.store(false, Ordering::Release); + } + }) + .map_err(|_| { + self.inner.running.store(false, Ordering::Release); + Error::InvalidOperation + })?; + if ready_receiver.recv_timeout(Duration::from_secs(2)) != Ok(true) { + cancellation.cancel(); + let _ = handle.join(); + self.inner.running.store(false, Ordering::Release); + return Err(Error::InvalidOperation); + } + *slot = Some(QueueTask { + cancellation, + graceful, + handle, + }); + Ok(()) + } + + pub fn stop(&self, immediate: bool) -> Result<(), Error> { + self.inner.stop_task(immediate); + Ok(()) + } + + #[must_use] + pub fn running(&self) -> bool { + self.inner.running.load(Ordering::Acquire) && !self.inner.disposed.load(Ordering::Acquire) + } +} + +impl fmt::Debug for EventQueueClient { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("EventQueueClient") + .field("running", &self.running()) + .field("disposed", &self.inner.disposed.load(Ordering::Acquire)) + .finish_non_exhaustive() + } +} + +async fn run_event_queue( + weak: Weak, + cancellation: CancellationToken, + graceful: Arc, +) { + let mut ack = 0_i32; + let mut retry = 0_u32; + loop { + let Some(inner) = weak.upgrade() else { + return; + }; + if cancellation.is_cancellation_requested() { + if graceful.load(Ordering::Acquire) { + send_done(&inner, ack).await; + } + return; + } + let Some(simulator) = inner.simulator() else { + return; + }; + if !simulator.native_is_connected() { + return; + } + + let request = EventQueueAck { + ack_id: ack, + done: false, + }; + let Ok(payload) = request + .serialize() + .and_then(|map| OSDParser::serialize_llsd_xml_bytes(OSD::Map(map.snapshot()))) + else { + return; + }; + let response = inner + .http + .post_with_uri_string_bytes_cancellation_token_i_progress( + inner.address.clone(), + LLSD_XML.to_owned(), + payload, + cancellation.clone(), + None, + ) + .await; + drop(simulator); + + match response { + Ok((response, data)) if response.is_success_status_code() => { + if let Some(callback) = mutex(&inner.callbacks).connected.clone() { + callback.invoke_safely(); + } + match parse_response(response.content_type.as_deref(), data, &inner.policy) { + Ok(parsed) => { + ack = parsed.sequence; + retry = 0; + let callback = mutex(&inner.callbacks).event.clone(); + if let Some(callback) = callback { + for event in parsed.events { + callback.invoke_safely(event.name, event.body); + } + } + } + Err(_) => retry = retry.saturating_add(1), + } + } + Ok((response, _)) if matches!(response.status_code, 404 | 410 | 499) => return, + Err(Error::Cancelled) if cancellation.is_cancellation_requested() => { + if graceful.load(Ordering::Acquire) { + send_done(&inner, ack).await; + } + return; + } + Ok(_) | Err(_) => retry = retry.saturating_add(1), + } + + if retry > 0 { + let delay = retry_delay(&inner, retry); + tokio::select! { + () = tokio::time::sleep(delay) => {} + () = cancellation.cancelled() => { + if graceful.load(Ordering::Acquire) { + send_done(&inner, ack).await; + } + return; + } + } + } + } +} + +async fn send_done(inner: &EventQueueClientInner, ack: i32) { + let Ok(map) = (EventQueueAck { + ack_id: ack, + done: true, + }) + .serialize() else { + return; + }; + let Ok(payload) = OSDParser::serialize_llsd_xml_bytes(OSD::Map(map.snapshot())) else { + return; + }; + let timeout = CancellationTokenSource::new(); + let timeout_worker = timeout.clone(); + let duration = inner.policy.graceful_shutdown_timeout; + tokio::spawn(async move { + tokio::time::sleep(duration).await; + timeout_worker.cancel(); + }); + let _ = inner + .http + .post_with_uri_string_bytes_cancellation_token_i_progress( + inner.address.clone(), + LLSD_XML.to_owned(), + payload, + timeout.token(), + None, + ) + .await; +} + +fn retry_delay(inner: &EventQueueClientInner, retry: u32) -> Duration { + let multiplier = 1_u32 + .checked_shl(retry.saturating_sub(1).min(30)) + .unwrap_or(u32::MAX); + let base = inner + .policy + .initial_retry_delay + .saturating_mul(multiplier) + .min(inner.policy.maximum_retry_delay); + let max_jitter = base / 8; + if max_jitter.is_zero() { + return base; + } + let mut hash = 0xcbf2_9ce4_8422_2325_u64 ^ u64::from(retry); + for byte in inner.address.0.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100_0000_01b3); + } + let jitter_nanos = u64::try_from(max_jitter.as_nanos()) + .unwrap_or(u64::MAX) + .saturating_add(1); + base.saturating_add(Duration::from_nanos(hash % jitter_nanos)) +} + +struct ParsedEvent { + name: String, + body: OSDMap, +} + +struct ParsedResponse { + sequence: i32, + events: Vec, +} + +fn parse_response( + content_type: Option<&str>, + data: Vec, + policy: &EventQueuePolicy, +) -> Result { + if data.is_empty() || !is_likely_llsd(content_type, &data) { + return Err(Error::Parse { + position: 0, + context: "event queue response is not LLSD/XML", + }); + } + let OSD::Map(mut map) = OSDParser::deserialize_llsd_xml_with_bytes(data)? else { + return Err(Error::Parse { + position: 0, + context: "event queue response is not a map", + }); + }; + let Some(OSD::Integer(sequence)) = map.remove("id") else { + return Err(Error::Parse { + position: 0, + context: "event queue response has no integer id", + }); + }; + let Some(OSD::Array(raw_events)) = map.remove("events") else { + return Err(Error::Parse { + position: 0, + context: "event queue response has no event array", + }); + }; + if raw_events.len() > policy.max_events_per_response { + return Err(Error::Argument); + } + let mut events = Vec::with_capacity(raw_events.len()); + for raw_event in raw_events { + let OSD::Map(mut event) = raw_event else { + return Err(Error::Parse { + position: 0, + context: "event queue entry is not a map", + }); + }; + let name = event + .remove("message") + .ok_or(Error::Parse { + position: 0, + context: "event queue entry has no message", + })? + .as_string()?; + let Some(OSD::Map(body)) = event.remove("body") else { + return Err(Error::Parse { + position: 0, + context: "event queue entry body is not a map", + }); + }; + OSD::Map(body.clone()).validate_limits( + OSD::DEFAULT_MAX_DEPTH, + policy.max_event_nodes, + policy.max_event_binary_bytes, + )?; + events.push(ParsedEvent { + name, + body: OSDMap::new_with_dictionary(body)?, + }); + } + Ok(ParsedResponse { sequence, events }) +} + +fn is_likely_llsd(content_type: Option<&str>, data: &[u8]) -> bool { + if let Some(content_type) = content_type { + let content_type = content_type.to_ascii_lowercase(); + return content_type.contains("xml") || content_type.contains("llsd"); + } + let prefix = String::from_utf8_lossy(&data[..data.len().min(256)]); + let prefix = prefix.trim_start(); + if prefix.to_ascii_lowercase().starts_with(" bool { + reqwest::Url::parse(&uri.0) + .ok() + .is_some_and(|uri| matches!(uri.scheme(), "http" | "https") && uri.host().is_some()) +} + +/// Request or response body carried by `EventQueueGet`. +pub enum EventMessageBlock { + None, + Ack(EventQueueAck), + Events(EventQueueEvent), +} + +impl EventMessageBlock { + pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { + if map.get("ack").is_some() { + let mut value = EventQueueAck::new()?; + value.deserialize(map)?; + *self = Self::Ack(value); + Ok(()) + } else if map.get("events").is_some() { + let mut value = EventQueueEvent::new()?; + value.deserialize(map)?; + *self = Self::Events(value); + Ok(()) + } else { + Err(Error::Parse { + position: 0, + context: "EventQueueGet has no message block", + }) + } + } + + pub fn serialize(&self) -> Result { + match self { + Self::Ack(value) => value.serialize(), + Self::Events(value) => value.serialize(), + Self::None => Err(Error::InvalidOperation), + } + } +} + +pub struct EventQueueAck { + pub ack_id: i32, + pub done: bool, +} + +impl EventQueueAck { + pub const fn new() -> Result { + Ok(Self { + ack_id: 0, + done: false, + }) + } + + pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { + self.ack_id = map.get("ack").unwrap_or_default().as_integer()?; + self.done = map.get("done").unwrap_or_default().as_boolean()?; + Ok(()) + } + + pub fn serialize(&self) -> Result { + OSDMap::new_with_dictionary(HashMap::from([ + ("ack".to_owned(), OSD::Integer(self.ack_id)), + ("done".to_owned(), OSD::Boolean(self.done)), + ])) + } +} + +pub struct EventQueueEventQueueEvent { + pub event_message: Option>, + pub message_key: String, +} + +impl EventQueueEventQueueEvent { + pub fn new() -> Result { + Ok(Self { + event_message: None, + message_key: String::new(), + }) + } +} + +pub struct EventQueueEvent { + pub message_events: Vec, + pub sequence: i32, +} + +impl EventQueueEvent { + pub const MAX_MESSAGE_EVENTS: usize = 1_024; + + pub const fn new() -> Result { + Ok(Self { + message_events: Vec::new(), + sequence: 0, + }) + } + + pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { + self.sequence = map.get("id").unwrap_or_default().as_integer()?; + let Some(OSD::Array(events)) = map.get("events") else { + return Err(Error::Parse { + position: 0, + context: "EventQueueEvent has no event array", + }); + }; + if events.len() > Self::MAX_MESSAGE_EVENTS { + return Err(Error::Argument); + } + let mut decoded = Vec::with_capacity(events.len()); + for event in events { + let OSD::Map(mut event) = event else { + return Err(Error::Parse { + position: 0, + context: "EventQueueEvent entry is not a map", + }); + }; + let key = event.remove("message").unwrap_or_default().as_string()?; + let body = match event.remove("body") { + Some(OSD::Map(body)) => OSDMap::new_with_dictionary(body)?, + _ => OSDMap::new_with_constructor()?, + }; + let event_message = crate::message_decoder::decode_event(&key, &body)?; + decoded.push(EventQueueEventQueueEvent { + event_message, + message_key: key, + }); + } + self.message_events = decoded; + Ok(()) + } + + pub fn serialize(&self) -> Result { + if self.message_events.len() > Self::MAX_MESSAGE_EVENTS { + return Err(Error::Argument); + } + let mut events = Vec::with_capacity(self.message_events.len()); + for event in &self.message_events { + let body = event + .event_message + .as_ref() + .map_or_else(OSDMap::new_with_constructor, |message| message.serialize())?; + events.push(OSD::Map(HashMap::from([ + ("body".to_owned(), OSD::Map(body.snapshot())), + ("message".to_owned(), OSD::String(event.message_key.clone())), + ]))); + } + OSDMap::new_with_dictionary(HashMap::from([ + ("events".to_owned(), OSD::Array(events)), + ("id".to_owned(), OSD::Integer(self.sequence)), + ])) + } +} + +pub struct EventQueueGetMessage { + pub messages: EventMessageBlock, +} + +impl EventQueueGetMessage { + pub const fn new() -> Result { + Ok(Self { + messages: EventMessageBlock::None, + }) + } + + pub fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { + self.messages.deserialize(map) + } + + pub fn serialize(&self) -> Result { + self.messages.serialize() + } +} + +impl IMessage for EventQueueGetMessage { + fn deserialize(&mut self, map: OSDMap) -> Result<(), Error> { + Self::deserialize(self, map) + } + + fn serialize(&self) -> Result { + Self::serialize(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::GridClient; + use libremetaverse_types::compat::{HttpMessageHandler, HttpRequest, HttpResponse}; + use std::collections::BTreeMap; + use std::sync::atomic::AtomicU64; + use std::sync::mpsc; + use std::time::Instant; + + fn lock(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + } + + fn response(status_code: u16, body: Vec) -> HttpResponse { + HttpResponse { + status_code, + headers: BTreeMap::new(), + content_type: Some(LLSD_XML.to_owned()), + body, + } + } + + fn encoded_response(id: i32, names: &[&str]) -> Vec { + let events = names + .iter() + .map(|name| { + OSD::Map(HashMap::from([ + ("message".to_owned(), OSD::String((*name).to_owned())), + ( + "body".to_owned(), + OSD::Map(HashMap::from([( + "ordinal".to_owned(), + OSD::Integer(if *name == "first" { 1 } else { 2 }), + )])), + ), + ])) + }) + .collect(); + OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([ + ("id".to_owned(), OSD::Integer(id)), + ("events".to_owned(), OSD::Array(events)), + ]))) + .expect("fixture") + } + + fn request_ack(request: &HttpRequest) -> (i32, bool) { + let OSD::Map(map) = + OSDParser::deserialize_llsd_xml_with_bytes(request.body.clone()).expect("request LLSD") + else { + panic!("request map") + }; + ( + map.get("ack") + .unwrap_or(&OSD::Undefined) + .as_integer() + .unwrap(), + map.get("done") + .unwrap_or(&OSD::Undefined) + .as_boolean() + .unwrap(), + ) + } + + fn simulator_with_handler(handler: HttpMessageHandler) -> Simulator { + let mut client = GridClient::new().expect("client"); + client.set_http_caps_client(HttpCapsClient::new(handler).expect("HTTP client")); + let simulator = Simulator::new( + client, + "127.0.0.1:13000".parse().expect("endpoint"), + 1, + None, + None, + ) + .expect("simulator"); + simulator.native_set_connected_for_tests(true); + simulator + } + + #[test] + fn polling_preserves_ack_shape_event_order_and_prompt_cancellation() { + let requests = Arc::new(Mutex::new(Vec::::new())); + let request_log = Arc::clone(&requests); + let calls = Arc::new(AtomicU64::new(0)); + let call_count = Arc::clone(&calls); + let handler = HttpMessageHandler::new(move |request, cancellation| { + let request_log = Arc::clone(&request_log); + let call = call_count.fetch_add(1, Ordering::AcqRel); + async move { + lock(&request_log).push(request); + if call == 0 { + response(200, encoded_response(7, &["first", "unknown-second"])) + } else { + cancellation.cancelled().await; + response(499, Vec::new()) + } + } + }); + let simulator = simulator_with_handler(handler); + let mut queue = EventQueueClient::new( + Uri("https://caps.example.test/event-queue?token=secret".to_owned()), + simulator, + ) + .expect("queue"); + let connected = Arc::new(AtomicU64::new(0)); + let connected_count = Arc::clone(&connected); + queue.on_connected = Some(EventQueueClientConnectedCallback::from_handler(move || { + connected_count.fetch_add(1, Ordering::AcqRel); + })); + let (event_sender, event_receiver) = mpsc::channel(); + queue.on_event = Some(EventQueueClientEventCallback::from_handler( + move |name, body| { + event_sender + .send(( + name, + body.get("ordinal") + .unwrap_or_default() + .as_integer() + .unwrap(), + )) + .expect("event receiver"); + }, + )); + queue.start().expect("start"); + + assert_eq!( + event_receiver.recv_timeout(Duration::from_secs(2)).unwrap(), + ("first".to_owned(), 1) + ); + assert_eq!( + event_receiver.recv_timeout(Duration::from_secs(2)).unwrap(), + ("unknown-second".to_owned(), 2) + ); + let deadline = Instant::now() + Duration::from_secs(2); + while lock(&requests).len() < 2 && Instant::now() < deadline { + thread::sleep(Duration::from_millis(5)); + } + let started = Instant::now(); + queue.stop(true).expect("stop"); + assert!(started.elapsed() < Duration::from_millis(500)); + assert!(!queue.running()); + assert_eq!(connected.load(Ordering::Acquire), 1); + let requests = lock(&requests); + assert_eq!(request_ack(&requests[0]), (0, false)); + assert_eq!(request_ack(&requests[1]), (7, false)); + } + + #[test] + fn graceful_stop_posts_done_with_last_ack() { + let requests = Arc::new(Mutex::new(Vec::::new())); + let request_log = Arc::clone(&requests); + let calls = Arc::new(AtomicU64::new(0)); + let call_count = Arc::clone(&calls); + let handler = HttpMessageHandler::new(move |request, cancellation| { + let request_log = Arc::clone(&request_log); + let call = call_count.fetch_add(1, Ordering::AcqRel); + async move { + lock(&request_log).push(request); + if call == 0 { + response(200, encoded_response(19, &[])) + } else if call == 1 { + cancellation.cancelled().await; + response(499, Vec::new()) + } else { + response(200, encoded_response(19, &[])) + } + } + }); + let simulator = simulator_with_handler(handler); + let queue = EventQueueClient::new( + Uri("https://caps.example.test/event-queue".to_owned()), + simulator, + ) + .expect("queue"); + queue.start().expect("start"); + let deadline = Instant::now() + Duration::from_secs(2); + while lock(&requests).len() < 2 && Instant::now() < deadline { + thread::sleep(Duration::from_millis(5)); + } + queue.stop(false).expect("graceful stop"); + let requests = lock(&requests); + assert_eq!(requests.len(), 3); + assert_eq!(request_ack(&requests[2]), (19, true)); + } + + #[test] + fn transient_status_retries_and_terminal_status_stops() { + let calls = Arc::new(AtomicU64::new(0)); + let call_count = Arc::clone(&calls); + let (sender, receiver) = mpsc::channel(); + let handler = HttpMessageHandler::new(move |_request, _cancellation| { + let call = call_count.fetch_add(1, Ordering::AcqRel); + let sender = sender.clone(); + async move { + let _ = sender.send(call); + match call { + 0 => response(502, Vec::new()), + 1 => response(200, encoded_response(3, &[])), + _ => response(404, Vec::new()), + } + } + }); + let simulator = simulator_with_handler(handler); + let policy = EventQueuePolicy { + initial_retry_delay: Duration::from_millis(5), + maximum_retry_delay: Duration::from_millis(10), + ..EventQueuePolicy::default() + }; + let queue = EventQueueClient::with_policy( + Uri("https://caps.example.test/event-queue".to_owned()), + simulator, + policy, + ) + .expect("queue"); + queue.start().expect("start"); + assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), 0); + assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), 1); + assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap(), 2); + let deadline = Instant::now() + Duration::from_secs(2); + while queue.running() && Instant::now() < deadline { + thread::sleep(Duration::from_millis(5)); + } + assert!(!queue.running()); + queue.stop(true).expect("observe task"); + } + + #[test] + fn parser_rejects_excess_fanout_and_non_llsd_content() { + let too_many = vec![OSD::Map(HashMap::new()); 3]; + let body = OSDParser::serialize_llsd_xml_bytes(OSD::Map(HashMap::from([ + ("id".to_owned(), OSD::Integer(1)), + ("events".to_owned(), OSD::Array(too_many)), + ]))) + .unwrap(); + let policy = EventQueuePolicy { + max_events_per_response: 2, + ..EventQueuePolicy::default() + }; + assert!(matches!( + parse_response(Some(LLSD_XML), body, &policy), + Err(Error::Argument) + )); + assert!( + parse_response( + Some("text/html"), + b"proxy".to_vec(), + &EventQueuePolicy::default(), + ) + .is_err() + ); + } + + #[test] + fn queue_message_blocks_round_trip_ack_and_known_events() { + let ack = EventQueueAck { + ack_id: 42, + done: true, + }; + let mut get = EventQueueGetMessage::new().unwrap(); + get.deserialize(ack.serialize().unwrap()).unwrap(); + let EventMessageBlock::Ack(decoded) = get.messages else { + panic!("ack block") + }; + assert_eq!((decoded.ack_id, decoded.done), (42, true)); + + let fixture = OSDMap::new_with_dictionary(HashMap::from([ + ("id".to_owned(), OSD::Integer(9)), + ( + "events".to_owned(), + OSD::Array(vec![OSD::Map(HashMap::from([ + ( + "message".to_owned(), + OSD::String("UpdateAgentLanguage".to_owned()), + ), + ( + "body".to_owned(), + OSD::Map(HashMap::from([ + ("language".to_owned(), OSD::String("en".to_owned())), + ("language_is_public".to_owned(), OSD::Boolean(true)), + ])), + ), + ]))]), + ), + ])) + .unwrap(); + let mut event = EventQueueEvent::new().unwrap(); + event.deserialize(fixture).unwrap(); + assert_eq!(event.sequence, 9); + assert_eq!(event.message_events.len(), 1); + assert!(event.message_events[0].event_message.is_some()); + assert_eq!(event.serialize().unwrap().get("id"), Some(OSD::Integer(9))); + } +} diff --git a/crates/libremetaverse/src/generated.rs b/crates/libremetaverse/src/generated.rs index 8c3b712..b4160ab 100644 --- a/crates/libremetaverse/src/generated.rs +++ b/crates/libremetaverse/src/generated.rs @@ -442,105 +442,20 @@ impl AgentDataReplyEventArgs { } /// C# type: `T:LibreMetaverse.AgentDisplayName`. -pub struct AgentDisplayName; -impl AgentDisplayName { - /// C# member: `M:LibreMetaverse.AgentDisplayName.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.AgentDisplayName.#ctor") - } - /// C# member: `M:LibreMetaverse.AgentDisplayName.FromOSD(LibreMetaverse.StructuredData.OSD)`. - pub fn from_osd( - data: libremetaverse_structured_data::OSD, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.AgentDisplayName.FromOSD(LibreMetaverse.StructuredData.OSD)", - ) - } - /// C# member: `M:LibreMetaverse.AgentDisplayName.GetOSD`. - pub fn get_osd(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.AgentDisplayName.GetOSD") - } - /// C# member: `M:LibreMetaverse.AgentDisplayName.ToString`. - pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.AgentDisplayName.ToString") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.DisplayName`. - pub fn display_name(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.DisplayName") - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.DisplayName`. - pub fn set_display_name(&mut self, value: Option) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.DisplayName") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.ID`. - pub fn id(&self) -> libremetaverse_types::UUID { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.ID") - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.ID`. - pub fn set_id(&mut self, value: libremetaverse_types::UUID) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.ID") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.IsDefaultDisplayName`. - pub fn is_default_display_name(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.AgentDisplayName.IsDefaultDisplayName" - ) - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.IsDefaultDisplayName`. - pub fn set_is_default_display_name(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.AgentDisplayName.IsDefaultDisplayName" - ) - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.LegacyFirstName`. - pub fn legacy_first_name(&self) -> Option { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.AgentDisplayName.LegacyFirstName" - ) - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.LegacyFirstName`. - pub fn set_legacy_first_name(&mut self, value: Option) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.AgentDisplayName.LegacyFirstName" - ) - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.LegacyFullName`. - pub fn legacy_full_name(&self) -> String { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.LegacyFullName") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.LegacyLastName`. - pub fn legacy_last_name(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.LegacyLastName") - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.LegacyLastName`. - pub fn set_legacy_last_name(&mut self, value: Option) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.LegacyLastName") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.NextUpdate`. - pub fn next_update(&self) -> std::time::SystemTime { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.NextUpdate") - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.NextUpdate`. - pub fn set_next_update(&mut self, value: std::time::SystemTime) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.NextUpdate") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.Updated`. - pub fn updated(&self) -> std::time::SystemTime { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.Updated") - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.Updated`. - pub fn set_updated(&mut self, value: std::time::SystemTime) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.Updated") - } - /// C# member: `P:LibreMetaverse.AgentDisplayName.UserName`. - pub fn user_name(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.UserName") - } - /// Setter for C# member: `P:LibreMetaverse.AgentDisplayName.UserName`. - pub fn set_user_name(&mut self, value: Option) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.AgentDisplayName.UserName") - } -} +/// C# member: `M:LibreMetaverse.AgentDisplayName.#ctor`. +/// C# member: `M:LibreMetaverse.AgentDisplayName.FromOSD(LibreMetaverse.StructuredData.OSD)`. +/// C# member: `M:LibreMetaverse.AgentDisplayName.GetOSD`. +/// C# member: `M:LibreMetaverse.AgentDisplayName.ToString`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.DisplayName`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.ID`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.IsDefaultDisplayName`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.LegacyFirstName`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.LegacyFullName`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.LegacyLastName`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.NextUpdate`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.Updated`. +/// C# member: `P:LibreMetaverse.AgentDisplayName.UserName`. +pub use crate::message_decoder::AgentDisplayName; /// C# type: `T:LibreMetaverse.AgentExperiencesEventArgs`. pub struct AgentExperiencesEventArgs; @@ -6776,92 +6691,24 @@ impl CameraConstraintEventArgs { } /// C# type: `T:LibreMetaverse.CapabilitiesReceivedEventArgs`. -pub struct CapabilitiesReceivedEventArgs; -impl CapabilitiesReceivedEventArgs { - /// C# member: `M:LibreMetaverse.CapabilitiesReceivedEventArgs.#ctor(LibreMetaverse.Simulator)`. - pub fn new(simulator: libremetaverse::Simulator) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapabilitiesReceivedEventArgs.#ctor(LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.CapabilitiesReceivedEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapabilitiesReceivedEventArgs.Simulator" - ) - } -} +/// C# member: `M:LibreMetaverse.CapabilitiesReceivedEventArgs.#ctor(LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.CapabilitiesReceivedEventArgs.Simulator`. +pub use crate::caps::CapabilitiesReceivedEventArgs; /// C# type: `T:LibreMetaverse.Caps`. -pub struct Caps { - /// C# member: `F:LibreMetaverse.Caps.Simulator`. - pub simulator: Box, -} -impl Caps { - /// C# member: `E:LibreMetaverse.Caps.CapabilitiesReceived`. - pub fn subscribe_capabilities_received( - &self, - handler: Option< - libremetaverse_types::compat::EventHandler< - libremetaverse::CapabilitiesReceivedEventArgs, - >, - >, - ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.Caps.CapabilitiesReceived") - } - /// C# member: `F:LibreMetaverse.Caps.AllCapabilities`. - pub fn all_capabilities() -> libremetaverse_structured_data::OSDArray { - libremetaverse_types::unimplemented_api!("F:LibreMetaverse.Caps.AllCapabilities") - } - /// C# member: `M:LibreMetaverse.Caps.Capabilities`. - pub fn capabilities(&self) -> Result, crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Caps.Capabilities") - } - /// C# member: `M:LibreMetaverse.Caps.CapabilityNameFromURI(System.Uri)`. - pub fn capability_name_from_uri( - &self, - cap: libremetaverse_types::compat::Uri, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Caps.CapabilityNameFromURI(System.Uri)", - ) - } - /// C# member: `M:LibreMetaverse.Caps.CapabilityURI(System.String)`. - pub fn capability_uri( - &self, - capability: String, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Caps.CapabilityURI(System.String)") - } - /// C# member: `M:LibreMetaverse.Caps.Disconnect(System.Boolean)`. - pub fn disconnect(&self, immediate: bool) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Caps.Disconnect(System.Boolean)") - } - /// C# member: `M:LibreMetaverse.Caps.GetMeshCapURI`. - pub fn get_mesh_cap_uri( - &self, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Caps.GetMeshCapURI") - } - /// C# member: `M:LibreMetaverse.Caps.GetTextureCapURI`. - pub fn get_texture_cap_uri( - &self, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Caps.GetTextureCapURI") - } - /// C# member: `P:LibreMetaverse.Caps.EventQueue`. - pub fn event_queue(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Caps.EventQueue") - } - /// C# member: `P:LibreMetaverse.Caps.IsEventQueueRunning`. - pub fn is_event_queue_running(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Caps.IsEventQueueRunning") - } - /// C# member: `P:LibreMetaverse.Caps.SeedCapsURI`. - pub fn seed_caps_uri(&self) -> libremetaverse_types::compat::Uri { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Caps.SeedCapsURI") - } -} +/// C# member: `E:LibreMetaverse.Caps.CapabilitiesReceived`. +/// C# member: `F:LibreMetaverse.Caps.AllCapabilities`. +/// C# member: `F:LibreMetaverse.Caps.Simulator`. +/// C# member: `M:LibreMetaverse.Caps.Capabilities`. +/// C# member: `M:LibreMetaverse.Caps.CapabilityNameFromURI(System.Uri)`. +/// C# member: `M:LibreMetaverse.Caps.CapabilityURI(System.String)`. +/// C# member: `M:LibreMetaverse.Caps.Disconnect(System.Boolean)`. +/// C# member: `M:LibreMetaverse.Caps.GetMeshCapURI`. +/// C# member: `M:LibreMetaverse.Caps.GetTextureCapURI`. +/// C# member: `P:LibreMetaverse.Caps.EventQueue`. +/// C# member: `P:LibreMetaverse.Caps.IsEventQueueRunning`. +/// C# member: `P:LibreMetaverse.Caps.SeedCapsURI`. +pub use crate::caps::Caps; /// C# type: `T:LibreMetaverse.Caps.EventQueueCallback`. /// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.#ctor(System.Object,System.IntPtr)`. @@ -9752,21 +9599,9 @@ impl EventInfoReplyEventArgs { } /// C# type: `T:LibreMetaverse.EventQueueRunningEventArgs`. -pub struct EventQueueRunningEventArgs; -impl EventQueueRunningEventArgs { - /// C# member: `M:LibreMetaverse.EventQueueRunningEventArgs.#ctor(LibreMetaverse.Simulator)`. - pub fn new(simulator: libremetaverse::Simulator) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.EventQueueRunningEventArgs.#ctor(LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.EventQueueRunningEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.EventQueueRunningEventArgs.Simulator" - ) - } -} +/// C# member: `M:LibreMetaverse.EventQueueRunningEventArgs.#ctor(LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.EventQueueRunningEventArgs.Simulator`. +pub use crate::network_manager::EventQueueRunningEventArgs; /// C# type: `T:LibreMetaverse.EventSubscriptionHelper`. pub struct EventSubscriptionHelper; @@ -18341,66 +18176,25 @@ pub enum MediaControls { } /// C# type: `T:LibreMetaverse.MediaEntry`. -pub struct MediaEntry { - /// C# member: `F:LibreMetaverse.MediaEntry.AutoLoop`. - pub auto_loop: bool, - /// C# member: `F:LibreMetaverse.MediaEntry.AutoPlay`. - pub auto_play: bool, - /// C# member: `F:LibreMetaverse.MediaEntry.AutoScale`. - pub auto_scale: bool, - /// C# member: `F:LibreMetaverse.MediaEntry.AutoZoom`. - pub auto_zoom: bool, - /// C# member: `F:LibreMetaverse.MediaEntry.ControlPermissions`. - pub control_permissions: libremetaverse::MediaPermission, - /// C# member: `F:LibreMetaverse.MediaEntry.Controls`. - pub controls: libremetaverse::MediaControls, - /// C# member: `F:LibreMetaverse.MediaEntry.CurrentURL`. - pub current_url: String, - /// C# member: `F:LibreMetaverse.MediaEntry.EnableWhiteList`. - pub enable_white_list: bool, - /// C# member: `F:LibreMetaverse.MediaEntry.Height`. - pub height: i32, - /// C# member: `F:LibreMetaverse.MediaEntry.HomeURL`. - pub home_url: String, - /// C# member: `F:LibreMetaverse.MediaEntry.InteractOnFirstClick`. - pub interact_on_first_click: bool, - /// C# member: `F:LibreMetaverse.MediaEntry.InteractPermissions`. - pub interact_permissions: libremetaverse::MediaPermission, - /// C# member: `F:LibreMetaverse.MediaEntry.WhiteList`. - pub white_list: Vec, - /// C# member: `F:LibreMetaverse.MediaEntry.Width`. - pub width: i32, -} -impl MediaEntry { - /// C# member: `M:LibreMetaverse.MediaEntry.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.MediaEntry.#ctor") - } - /// C# member: `M:LibreMetaverse.MediaEntry.FromOSD(LibreMetaverse.StructuredData.OSD)`. - pub fn from_osd( - osd: libremetaverse_structured_data::OSD, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.MediaEntry.FromOSD(LibreMetaverse.StructuredData.OSD)", - ) - } - /// C# member: `M:LibreMetaverse.MediaEntry.GetOSD`. - pub fn get_osd(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.MediaEntry.GetOSD") - } - /// C# member: `P:LibreMetaverse.MediaEntry.EnableAlternativeImage`. - pub fn enable_alternative_image(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.MediaEntry.EnableAlternativeImage" - ) - } - /// Setter for C# member: `P:LibreMetaverse.MediaEntry.EnableAlternativeImage`. - pub fn set_enable_alternative_image(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.MediaEntry.EnableAlternativeImage" - ) - } -} +/// C# member: `F:LibreMetaverse.MediaEntry.AutoLoop`. +/// C# member: `F:LibreMetaverse.MediaEntry.AutoPlay`. +/// C# member: `F:LibreMetaverse.MediaEntry.AutoScale`. +/// C# member: `F:LibreMetaverse.MediaEntry.AutoZoom`. +/// C# member: `F:LibreMetaverse.MediaEntry.ControlPermissions`. +/// C# member: `F:LibreMetaverse.MediaEntry.Controls`. +/// C# member: `F:LibreMetaverse.MediaEntry.CurrentURL`. +/// C# member: `F:LibreMetaverse.MediaEntry.EnableWhiteList`. +/// C# member: `F:LibreMetaverse.MediaEntry.Height`. +/// C# member: `F:LibreMetaverse.MediaEntry.HomeURL`. +/// C# member: `F:LibreMetaverse.MediaEntry.InteractOnFirstClick`. +/// C# member: `F:LibreMetaverse.MediaEntry.InteractPermissions`. +/// C# member: `F:LibreMetaverse.MediaEntry.WhiteList`. +/// C# member: `F:LibreMetaverse.MediaEntry.Width`. +/// C# member: `M:LibreMetaverse.MediaEntry.#ctor`. +/// C# member: `M:LibreMetaverse.MediaEntry.FromOSD(LibreMetaverse.StructuredData.OSD)`. +/// C# member: `M:LibreMetaverse.MediaEntry.GetOSD`. +/// C# member: `P:LibreMetaverse.MediaEntry.EnableAlternativeImage`. +pub use crate::message_decoder::MediaEntry; /// C# type: `T:LibreMetaverse.MediaPermission`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -18793,9 +18587,8 @@ impl NetworkManager { libremetaverse::EventQueueRunningEventArgs, >, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!( - "E:LibreMetaverse.NetworkManager.EventQueueRunning" - ) + /* native network implementation */ + self.native_subscribe_event_queue_running(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.GenericStreamingMessage`. pub fn subscribe_generic_streaming_message( @@ -23361,38 +23154,16 @@ impl PrimitiveParticleSystemSourcePattern { } /// C# type: `T:LibreMetaverse.Primitive.PhysicsProperties`. -pub struct PrimitivePhysicsProperties { - /// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.Density`. - pub density: f32, - /// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.Friction`. - pub friction: f32, - /// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.GravityMultiplier`. - pub gravity_multiplier: f32, - /// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.LocalID`. - pub local_id: u32, - /// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.PhysicsShapeType`. - pub physics_shape_type: libremetaverse_types::PhysicsShapeType, - /// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.Restitution`. - pub restitution: f32, -} -impl PrimitivePhysicsProperties { - /// C# member: `M:LibreMetaverse.Primitive.PhysicsProperties.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Primitive.PhysicsProperties.#ctor") - } - /// C# member: `M:LibreMetaverse.Primitive.PhysicsProperties.FromOSD(LibreMetaverse.StructuredData.OSD)`. - pub fn from_osd( - osd: libremetaverse_structured_data::OSD, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Primitive.PhysicsProperties.FromOSD(LibreMetaverse.StructuredData.OSD)", - ) - } - /// C# member: `M:LibreMetaverse.Primitive.PhysicsProperties.GetOSD`. - pub fn get_osd(&self) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Primitive.PhysicsProperties.GetOSD") - } -} +/// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.Density`. +/// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.Friction`. +/// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.GravityMultiplier`. +/// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.LocalID`. +/// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.PhysicsShapeType`. +/// C# member: `F:LibreMetaverse.Primitive.PhysicsProperties.Restitution`. +/// C# member: `M:LibreMetaverse.Primitive.PhysicsProperties.#ctor`. +/// C# member: `M:LibreMetaverse.Primitive.PhysicsProperties.FromOSD(LibreMetaverse.StructuredData.OSD)`. +/// C# member: `M:LibreMetaverse.Primitive.PhysicsProperties.GetOSD`. +pub use crate::message_decoder::PrimitivePhysicsProperties; /// C# type: `T:LibreMetaverse.Primitive.SculptData`. pub struct PrimitiveSculptData { @@ -25029,9 +24800,8 @@ impl Simulator { &self, block_until_running: Option, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.IsEventQueueRunning(System.Boolean)", - ) + /* native network implementation */ + self.native_is_event_queue_running(block_until_running) } /// C# member: `M:LibreMetaverse.Simulator.IsParcelMapFull`. pub fn is_parcel_map_full(&self) -> Result { @@ -32238,127 +32008,28 @@ pub mod http { pub use crate::download_manager::DownloadRequest; /// C# type: `T:LibreMetaverse.Http.EventQueueClient`. - pub struct EventQueueClient { - /// C# member: `F:LibreMetaverse.Http.EventQueueClient.OnConnected`. - pub on_connected: Option, - /// C# member: `F:LibreMetaverse.Http.EventQueueClient.OnEvent`. - pub on_event: Option, - } - impl EventQueueClient { - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.#ctor(System.Uri,LibreMetaverse.Simulator)`. - pub fn new( - event_queue_location: libremetaverse_types::compat::Uri, - sim: libremetaverse::Simulator, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.#ctor(System.Uri,LibreMetaverse.Simulator)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.Dispose`. - pub fn dispose(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Http.EventQueueClient.Dispose") - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.Start`. - pub fn start(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Http.EventQueueClient.Start") - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.Stop(System.Boolean)`. - pub fn stop(&self, immediate: bool) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.Stop(System.Boolean)", - ) - } - /// C# member: `P:LibreMetaverse.Http.EventQueueClient.Running`. - pub fn running(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Http.EventQueueClient.Running" - ) - } - } + /// C# member: `F:LibreMetaverse.Http.EventQueueClient.OnConnected`. + /// C# member: `F:LibreMetaverse.Http.EventQueueClient.OnEvent`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.#ctor(System.Uri,LibreMetaverse.Simulator)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.Dispose`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.Start`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.Stop(System.Boolean)`. + /// C# member: `P:LibreMetaverse.Http.EventQueueClient.Running`. + pub use crate::event_queue::EventQueueClient; /// C# type: `T:LibreMetaverse.Http.EventQueueClient.ConnectedCallback`. - pub struct EventQueueClientConnectedCallback; - impl EventQueueClientConnectedCallback { - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.#ctor(System.Object,System.IntPtr)`. - pub fn new( - object: libremetaverse_types::compat::Object, - method: isize, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.#ctor(System.Object,System.IntPtr)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.BeginInvoke(System.AsyncCallback,System.Object)`. - pub fn begin_invoke( - &self, - callback: Box, - object: libremetaverse_types::compat::Object, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.BeginInvoke(System.AsyncCallback,System.Object)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.EndInvoke(System.IAsyncResult)`. - pub fn end_invoke( - &self, - result: Box, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.EndInvoke(System.IAsyncResult)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.Invoke`. - pub fn invoke(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.Invoke", - ) - } - } + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.#ctor(System.Object,System.IntPtr)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.BeginInvoke(System.AsyncCallback,System.Object)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.EndInvoke(System.IAsyncResult)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.ConnectedCallback.Invoke`. + pub use crate::event_queue::EventQueueClientConnectedCallback; /// C# type: `T:LibreMetaverse.Http.EventQueueClient.EventCallback`. - pub struct EventQueueClientEventCallback; - impl EventQueueClientEventCallback { - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.#ctor(System.Object,System.IntPtr)`. - pub fn new( - object: libremetaverse_types::compat::Object, - method: isize, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.EventCallback.#ctor(System.Object,System.IntPtr)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.BeginInvoke(System.String,LibreMetaverse.StructuredData.OSDMap,System.AsyncCallback,System.Object)`. - pub fn begin_invoke( - &self, - event_name: String, - body: libremetaverse_structured_data::OSDMap, - callback: Box, - object: libremetaverse_types::compat::Object, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.EventCallback.BeginInvoke(System.String,LibreMetaverse.StructuredData.OSDMap,System.AsyncCallback,System.Object)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.EndInvoke(System.IAsyncResult)`. - pub fn end_invoke( - &self, - result: Box, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.EventCallback.EndInvoke(System.IAsyncResult)", - ) - } - /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.Invoke(System.String,LibreMetaverse.StructuredData.OSDMap)`. - pub fn invoke( - &self, - event_name: String, - body: libremetaverse_structured_data::OSDMap, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.EventQueueClient.EventCallback.Invoke(System.String,LibreMetaverse.StructuredData.OSDMap)", - ) - } - } + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.#ctor(System.Object,System.IntPtr)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.BeginInvoke(System.String,LibreMetaverse.StructuredData.OSDMap,System.AsyncCallback,System.Object)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.EndInvoke(System.IAsyncResult)`. + /// C# member: `M:LibreMetaverse.Http.EventQueueClient.EventCallback.Invoke(System.String,LibreMetaverse.StructuredData.OSDMap)`. + pub use crate::event_queue::EventQueueClientEventCallback; } pub mod imaging { @@ -63827,17 +63498,13 @@ pub mod messages { event_name: String, map: libremetaverse_structured_data::OSDMap, ) -> Result>, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.DecodeEvent(System.String,LibreMetaverse.StructuredData.OSDMap)", - ) + crate::message_decoder::decode_event(&event_name, &map) } /// C# member: `M:LibreMetaverse.Messages.MessageUtils.FromDictionaryString(System.Collections.Generic.Dictionary{System.String,System.String})`. pub fn from_dictionary_string( dict: std::collections::HashMap, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.FromDictionaryString(System.Collections.Generic.Dictionary{System.String,System.String})", - ) + crate::message_decoder::from_dictionary_string(dict) } /// C# member: `M:LibreMetaverse.Messages.MessageUtils.FromDictionaryUri(System.Collections.Generic.Dictionary{System.Uri,System.Uri})`. pub fn from_dictionary_uri( @@ -63846,25 +63513,19 @@ pub mod messages { libremetaverse_types::compat::Uri, >, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.FromDictionaryUri(System.Collections.Generic.Dictionary{System.Uri,System.Uri})", - ) + crate::message_decoder::from_dictionary_uri(dict) } /// C# member: `M:LibreMetaverse.Messages.MessageUtils.FromIP(System.Net.IPAddress)`. pub fn from_ip( address: std::net::IpAddr, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.FromIP(System.Net.IPAddress)", - ) + crate::message_decoder::from_ip(address) } /// C# member: `M:LibreMetaverse.Messages.MessageUtils.ToDictionaryString(LibreMetaverse.StructuredData.OSD)`. pub fn to_dictionary_string( osd: libremetaverse_structured_data::OSD, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.ToDictionaryString(LibreMetaverse.StructuredData.OSD)", - ) + crate::message_decoder::to_dictionary_string(osd) } /// C# member: `M:LibreMetaverse.Messages.MessageUtils.ToDictionaryUri(LibreMetaverse.StructuredData.OSD)`. pub fn to_dictionary_uri( @@ -63876,17 +63537,13 @@ pub mod messages { >, crate::Error, > { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.ToDictionaryUri(LibreMetaverse.StructuredData.OSD)", - ) + crate::message_decoder::to_dictionary_uri(osd) } /// C# member: `M:LibreMetaverse.Messages.MessageUtils.ToIP(LibreMetaverse.StructuredData.OSD)`. pub fn to_ip( osd: libremetaverse_structured_data::OSD, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.MessageUtils.ToIP(LibreMetaverse.StructuredData.OSD)", - ) + crate::message_decoder::to_ip(osd) } } @@ -63900,29 +63557,33 @@ pub mod messages { impl AgentDropGroupMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for AgentDropGroupMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for AgentDropGroupMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.AgentData`. pub struct AgentDropGroupMessageAgentData { @@ -63934,9 +63595,7 @@ pub mod messages { impl AgentDropGroupMessageAgentData { /// C# member: `M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.AgentData.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.AgentData.#ctor", - ) + Ok(::new_generated()) } } @@ -63972,7 +63631,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for AgentGroupDataUpdateMessage {} + impl libremetaverse::interfaces::IMessage for AgentGroupDataUpdateMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.GroupData`. pub struct AgentGroupDataUpdateMessageGroupData { @@ -64220,29 +63889,33 @@ pub mod messages { impl AgentStateUpdateMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for AgentStateUpdateMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for AgentStateUpdateMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.AssetUploaderBlock`. pub struct AssetUploaderBlock { @@ -64270,49 +63943,12 @@ pub mod messages { } /// C# type: `T:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage`. - pub struct AttachmentResourcesMessage { - /// C# member: `F:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.Attachments`. - pub attachments: std::collections::HashMap< - libremetaverse_types::AttachmentPoint, - Vec, - >, - } - impl AttachmentResourcesMessage { - /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.#ctor", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. - pub fn deserialize( - &mut self, - osd: libremetaverse_structured_data::OSDMap, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.FromOSD(LibreMetaverse.StructuredData.OSD)`. - pub fn from_osd( - osd: libremetaverse_structured_data::OSD, - ) -> Result - { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.FromOSD(LibreMetaverse.StructuredData.OSD)", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.GetMessageHandler(LibreMetaverse.StructuredData.OSDMap)`. - pub fn get_message_handler( - map: libremetaverse_structured_data::OSDMap, - ) -> Result>, crate::Error> - { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.GetMessageHandler(LibreMetaverse.StructuredData.OSDMap)", - ) - } - } - impl libremetaverse::interfaces::IMessage for AttachmentResourcesMessage {} + /// C# member: `F:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.Attachments`. + /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.#ctor`. + /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. + /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.FromOSD(LibreMetaverse.StructuredData.OSD)`. + /// C# member: `M:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage.GetMessageHandler(LibreMetaverse.StructuredData.OSDMap)`. + pub use crate::message_decoder::AttachmentResourcesMessage; /// C# type: `T:LibreMetaverse.Messages.Linden.AvatarRenderInfoMessage`. pub struct AvatarRenderInfoMessage { @@ -64413,29 +64049,33 @@ pub mod messages { impl BulkUpdateInventoryMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for BulkUpdateInventoryMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for BulkUpdateInventoryMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.FolderDataInfo`. pub struct BulkUpdateInventoryMessageFolderDataInfo { @@ -64451,9 +64091,7 @@ pub mod messages { impl BulkUpdateInventoryMessageFolderDataInfo { /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.FolderDataInfo.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.FolderDataInfo.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.FolderDataInfo.FromOSD(LibreMetaverse.StructuredData.OSD)`. pub fn from_osd( @@ -64518,9 +64156,7 @@ pub mod messages { impl BulkUpdateInventoryMessageItemDataInfo { /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.ItemDataInfo.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.ItemDataInfo.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.ItemDataInfo.FromOSD(LibreMetaverse.StructuredData.OSD)`. pub fn from_osd( @@ -64731,7 +64367,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ChatterBoxInvitationMessage {} + impl libremetaverse::interfaces::IMessage for ChatterBoxInvitationMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ChatterBoxSessionAgentListUpdatesMessage`. pub struct ChatterBoxSessionAgentListUpdatesMessage { @@ -64759,7 +64405,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ChatterBoxSessionAgentListUpdatesMessage {} + impl libremetaverse::interfaces::IMessage for ChatterBoxSessionAgentListUpdatesMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ChatterBoxSessionAgentListUpdatesMessage.AgentUpdatesBlock`. pub struct ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock { @@ -64819,7 +64475,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ChatterBoxSessionStartReplyMessage {} + impl libremetaverse::interfaces::IMessage for ChatterBoxSessionStartReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ChatterboxSessionEventReplyMessage`. pub struct ChatterboxSessionEventReplyMessage { @@ -64847,7 +64513,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ChatterboxSessionEventReplyMessage {} + impl libremetaverse::interfaces::IMessage for ChatterboxSessionEventReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.CopyInventoryFromNotecardMessage`. pub struct CopyInventoryFromNotecardMessage { @@ -64881,7 +64557,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for CopyInventoryFromNotecardMessage {} + impl libremetaverse::interfaces::IMessage for CopyInventoryFromNotecardMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.CrossedRegionMessage`. pub struct CrossedRegionMessage { @@ -64909,29 +64595,33 @@ pub mod messages { impl CrossedRegionMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.CrossedRegionMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.CrossedRegionMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.CrossedRegionMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.CrossedRegionMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.CrossedRegionMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.CrossedRegionMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for CrossedRegionMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for CrossedRegionMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.DirLandReplyMessage`. pub struct DirLandReplyMessage { @@ -64961,7 +64651,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for DirLandReplyMessage {} + impl libremetaverse::interfaces::IMessage for DirLandReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.DirLandReplyMessage.QueryReply`. pub struct DirLandReplyMessageQueryReply { @@ -64997,29 +64697,33 @@ pub mod messages { impl DisplayNameUpdateMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for DisplayNameUpdateMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for DisplayNameUpdateMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage`. pub struct EnableSimulatorMessage { @@ -65046,7 +64750,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for EnableSimulatorMessage {} + impl libremetaverse::interfaces::IMessage for EnableSimulatorMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage.SimulatorInfoBlock`. pub struct EnableSimulatorMessageSimulatorInfoBlock { @@ -65198,143 +64912,51 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for EstablishAgentCommunicationMessage {} + impl libremetaverse::interfaces::IMessage for EstablishAgentCommunicationMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.EventMessageBlock`. - pub struct EventMessageBlock; - impl EventMessageBlock { - /// C# member: `M:LibreMetaverse.Messages.Linden.EventMessageBlock.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. - pub fn deserialize( - &mut self, - map: libremetaverse_structured_data::OSDMap, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventMessageBlock.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventMessageBlock.Serialize`. - pub fn serialize( - &self, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventMessageBlock.Serialize", - ) - } - } + /// C# member: `M:LibreMetaverse.Messages.Linden.EventMessageBlock.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventMessageBlock.Serialize`. + pub use crate::event_queue::EventMessageBlock; /// C# type: `T:LibreMetaverse.Messages.Linden.EventQueueAck`. - pub struct EventQueueAck { - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueAck.AckID`. - pub ack_id: i32, - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueAck.Done`. - pub done: bool, - } - impl EventQueueAck { - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueAck.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueAck.#ctor", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueAck.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. - pub fn deserialize( - &mut self, - map: libremetaverse_structured_data::OSDMap, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueAck.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueAck.Serialize`. - pub fn serialize( - &self, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueAck.Serialize", - ) - } - } + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueAck.AckID`. + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueAck.Done`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueAck.#ctor`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueAck.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueAck.Serialize`. + pub use crate::event_queue::EventQueueAck; /// C# type: `T:LibreMetaverse.Messages.Linden.EventQueueEvent`. - pub struct EventQueueEvent { - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.MessageEvents`. - pub message_events: Vec, - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.Sequence`. - pub sequence: i32, - } - impl EventQueueEvent { - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueEvent.#ctor", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. - pub fn deserialize( - &mut self, - map: libremetaverse_structured_data::OSDMap, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueEvent.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.Serialize`. - pub fn serialize( - &self, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueEvent.Serialize", - ) - } - } + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.MessageEvents`. + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.Sequence`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.#ctor`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.Serialize`. + pub use crate::event_queue::EventQueueEvent; /// C# type: `T:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent`. - pub struct EventQueueEventQueueEvent { - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.EventMessage`. - pub event_message: Option>, - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.MessageKey`. - pub message_key: String, - } - impl EventQueueEventQueueEvent { - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.#ctor", - ) - } - } + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.EventMessage`. + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.MessageKey`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent.#ctor`. + pub use crate::event_queue::EventQueueEventQueueEvent; /// C# type: `T:LibreMetaverse.Messages.Linden.EventQueueGetMessage`. - pub struct EventQueueGetMessage { - /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Messages`. - pub messages: libremetaverse::messages::linden::EventMessageBlock, - } - impl EventQueueGetMessage { - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.#ctor", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. - pub fn deserialize( - &mut self, - map: libremetaverse_structured_data::OSDMap, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) - } - /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Serialize`. - pub fn serialize( - &self, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Serialize", - ) - } - } - impl libremetaverse::interfaces::IMessage for EventQueueGetMessage {} + /// C# member: `F:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Messages`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.#ctor`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. + /// C# member: `M:LibreMetaverse.Messages.Linden.EventQueueGetMessage.Serialize`. + pub use crate::event_queue::EventQueueGetMessage; /// C# type: `T:LibreMetaverse.Messages.Linden.ExperienceFlags`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -65608,29 +65230,33 @@ pub mod messages { impl ForceCloseChatterBoxSessionMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for ForceCloseChatterBoxSessionMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ForceCloseChatterBoxSessionMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage`. pub struct GetDisplayNamesMessage { @@ -65642,29 +65268,33 @@ pub mod messages { impl GetDisplayNamesMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for GetDisplayNamesMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for GetDisplayNamesMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.GetObjectCostMessage`. pub struct GetObjectCostMessage { @@ -65682,18 +65312,14 @@ pub mod messages { impl GetObjectCostMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.GetObjectCostMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetObjectCostMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.GetObjectCostMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetObjectCostMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.GetObjectCostMessage.GetMessageHandler(LibreMetaverse.StructuredData.OSDMap)`. pub fn get_message_handler( @@ -65708,12 +65334,20 @@ pub mod messages { pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetObjectCostMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for GetObjectCostMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for GetObjectCostMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.GetObjectCostRequest`. pub struct GetObjectCostRequest { @@ -65723,29 +65357,33 @@ pub mod messages { impl GetObjectCostRequest { /// C# member: `M:LibreMetaverse.Messages.Linden.GetObjectCostRequest.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetObjectCostRequest.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.GetObjectCostRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetObjectCostRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.GetObjectCostRequest.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.GetObjectCostRequest.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for GetObjectCostRequest { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for GetObjectCostRequest {} /// C# type: `T:LibreMetaverse.Messages.Linden.InterestListMessage`. pub struct InterestListMessage; @@ -65847,18 +65485,14 @@ pub mod messages { impl LandResourcesMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.LandResourcesMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.LandResourcesMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.LandResourcesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.LandResourcesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.LandResourcesMessage.GetMessageHandler(LibreMetaverse.StructuredData.OSDMap)`. pub fn get_message_handler( @@ -65873,12 +65507,20 @@ pub mod messages { pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.LandResourcesMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for LandResourcesMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for LandResourcesMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.LandResourcesRequest`. pub struct LandResourcesRequest { @@ -65888,29 +65530,33 @@ pub mod messages { impl LandResourcesRequest { /// C# member: `M:LibreMetaverse.Messages.Linden.LandResourcesRequest.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.LandResourcesRequest.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.LandResourcesRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.LandResourcesRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.LandResourcesRequest.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.LandResourcesRequest.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for LandResourcesRequest { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for LandResourcesRequest {} /// C# type: `T:LibreMetaverse.Messages.Linden.LandStatReplyMessage`. pub struct LandStatReplyMessage { @@ -65943,7 +65589,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for LandStatReplyMessage {} + impl libremetaverse::interfaces::IMessage for LandStatReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.LandStatReplyMessage.ReportDataBlock`. pub struct LandStatReplyMessageReportDataBlock { @@ -66155,29 +65811,33 @@ pub mod messages { impl NavMeshStatusUpdateMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for NavMeshStatusUpdateMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for NavMeshStatusUpdateMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.NewFileAgentInventoryMessage`. pub struct NewFileAgentInventoryMessage { @@ -66453,29 +66113,33 @@ pub mod messages { impl ObjectMediaNavigateMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for ObjectMediaNavigateMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ObjectMediaNavigateMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.ObjectMediaRequest`. pub struct ObjectMediaRequest { @@ -66487,26 +66151,20 @@ pub mod messages { impl ObjectMediaRequest { /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaRequest.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaRequest.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaRequest.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaRequest.Serialize", - ) + ::serialize_generated(self) } } @@ -66522,26 +66180,20 @@ pub mod messages { impl ObjectMediaResponse { /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaResponse.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaResponse.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaResponse.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaResponse.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaResponse.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaResponse.Serialize", - ) + ::serialize_generated(self) } } @@ -66557,26 +66209,20 @@ pub mod messages { impl ObjectMediaUpdate { /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaUpdate.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaUpdate.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaUpdate.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaUpdate.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectMediaUpdate.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectMediaUpdate.Serialize", - ) + ::serialize_generated(self) } } @@ -66588,29 +66234,33 @@ pub mod messages { impl ObjectPhysicsPropertiesMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for ObjectPhysicsPropertiesMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ObjectPhysicsPropertiesMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.ObjectResourcesDetail`. pub struct ObjectResourcesDetail { @@ -66679,7 +66329,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ParcelObjectOwnersReplyMessage {} + impl libremetaverse::interfaces::IMessage for ParcelObjectOwnersReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage.PrimOwner`. pub struct ParcelObjectOwnersReplyMessagePrimOwner { @@ -66845,7 +66505,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ParcelPropertiesMessage {} + impl libremetaverse::interfaces::IMessage for ParcelPropertiesMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ParcelPropertiesUpdateMessage`. pub struct ParcelPropertiesUpdateMessage { @@ -66927,7 +66597,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ParcelPropertiesUpdateMessage {} + impl libremetaverse::interfaces::IMessage for ParcelPropertiesUpdateMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ParcelResourcesDetail`. pub struct ParcelResourcesDetail { @@ -66999,7 +66679,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ParcelVoiceInfoRequestMessage {} + impl libremetaverse::interfaces::IMessage for ParcelVoiceInfoRequestMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.PlacesReplyMessage`. pub struct PlacesReplyMessage { @@ -67032,7 +66722,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for PlacesReplyMessage {} + impl libremetaverse::interfaces::IMessage for PlacesReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.PlacesReplyMessage.QueryData`. pub struct PlacesReplyMessageQueryData { @@ -67149,7 +66849,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ProvisionVoiceAccountRequestMessage {} + impl libremetaverse::interfaces::IMessage for ProvisionVoiceAccountRequestMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.RegionExperiencesMessage`. pub struct RegionExperiencesMessage { @@ -67201,29 +66911,33 @@ pub mod messages { impl RegionInfoMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.RegionInfoMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.RegionInfoMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.RegionInfoMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.RegionInfoMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.RegionInfoMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.RegionInfoMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for RegionInfoMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for RegionInfoMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.RemoteParcelRequestBlock`. pub struct RemoteParcelRequestBlock; @@ -67347,29 +67061,33 @@ pub mod messages { impl RenderMaterialsMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.RenderMaterialsMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.RenderMaterialsMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.RenderMaterialsMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.RenderMaterialsMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.RenderMaterialsMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.RenderMaterialsMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for RenderMaterialsMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for RenderMaterialsMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.RequiredVoiceVersionMessage`. pub struct RequiredVoiceVersionMessage { @@ -67399,7 +67117,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for RequiredVoiceVersionMessage {} + impl libremetaverse::interfaces::IMessage for RequiredVoiceVersionMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.ScriptRunningReplyMessage`. pub struct ScriptRunningReplyMessage { @@ -67431,7 +67159,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ScriptRunningReplyMessage {} + impl libremetaverse::interfaces::IMessage for ScriptRunningReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.SearchStatRequestBlock`. pub struct SearchStatRequestBlock; @@ -67505,26 +67243,20 @@ pub mod messages { impl SearchStatRequestReply { /// C# member: `M:LibreMetaverse.Messages.Linden.SearchStatRequestReply.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SearchStatRequestReply.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.SearchStatRequestReply.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SearchStatRequestReply.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.SearchStatRequestReply.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SearchStatRequestReply.Serialize", - ) + ::serialize_generated(self) } } @@ -67536,26 +67268,20 @@ pub mod messages { impl SearchStatRequestRequest { /// C# member: `M:LibreMetaverse.Messages.Linden.SearchStatRequestRequest.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SearchStatRequestRequest.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.SearchStatRequestRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SearchStatRequestRequest.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.SearchStatRequestRequest.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SearchStatRequestRequest.Serialize", - ) + ::serialize_generated(self) } } @@ -67591,7 +67317,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for SendPostcardMessage {} + impl libremetaverse::interfaces::IMessage for SendPostcardMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.SetDisplayNameMessage`. pub struct SetDisplayNameMessage { @@ -67603,29 +67339,33 @@ pub mod messages { impl SetDisplayNameMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.SetDisplayNameMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SetDisplayNameMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.SetDisplayNameMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SetDisplayNameMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.SetDisplayNameMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SetDisplayNameMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for SetDisplayNameMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for SetDisplayNameMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage`. pub struct SetDisplayNameReplyMessage { @@ -67639,29 +67379,33 @@ pub mod messages { impl SetDisplayNameReplyMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for SetDisplayNameReplyMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for SetDisplayNameReplyMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.SimConsoleAsyncMessage`. pub struct SimConsoleAsyncMessage { @@ -67691,29 +67435,33 @@ pub mod messages { impl SimConsoleResponseMessage { /// C# member: `M:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage.Serialize", - ) + ::serialize_generated(self) + } + } + impl libremetaverse::interfaces::IMessage for SimConsoleResponseMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for SimConsoleResponseMessage {} /// C# type: `T:LibreMetaverse.Messages.Linden.TeleportFailedMessage`. pub struct TeleportFailedMessage { @@ -67745,7 +67493,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for TeleportFailedMessage {} + impl libremetaverse::interfaces::IMessage for TeleportFailedMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.TeleportFinishMessage`. pub struct TeleportFinishMessage { @@ -67789,7 +67547,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for TeleportFinishMessage {} + impl libremetaverse::interfaces::IMessage for TeleportFinishMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.UpdateAgentInformationMessage`. pub struct UpdateAgentInformationMessage { @@ -67815,7 +67583,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for UpdateAgentInformationMessage {} + impl libremetaverse::interfaces::IMessage for UpdateAgentInformationMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.UpdateAgentInventoryRequestMessage`. pub struct UpdateAgentInventoryRequestMessage { @@ -67868,7 +67646,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for UpdateAgentLanguageMessage {} + impl libremetaverse::interfaces::IMessage for UpdateAgentLanguageMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } /// C# type: `T:LibreMetaverse.Messages.Linden.UpdateGestureAgentInventoryMessage`. pub struct UpdateGestureAgentInventoryMessage { @@ -68364,26 +68152,20 @@ pub mod messages { impl UploaderRequestComplete { /// C# member: `M:LibreMetaverse.Messages.Linden.UploaderRequestComplete.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.UploaderRequestComplete.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.UploaderRequestComplete.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.UploaderRequestComplete.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.UploaderRequestComplete.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.UploaderRequestComplete.Serialize", - ) + ::serialize_generated(self) } } @@ -68428,26 +68210,20 @@ pub mod messages { impl UploaderRequestUpload { /// C# member: `M:LibreMetaverse.Messages.Linden.UploaderRequestUpload.#ctor`. pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.UploaderRequestUpload.#ctor", - ) + Ok(::new_generated()) } /// C# member: `M:LibreMetaverse.Messages.Linden.UploaderRequestUpload.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. pub fn deserialize( &mut self, map: libremetaverse_structured_data::OSDMap, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.UploaderRequestUpload.Deserialize(LibreMetaverse.StructuredData.OSDMap)", - ) + ::deserialize_generated(self, &map) } /// C# member: `M:LibreMetaverse.Messages.Linden.UploaderRequestUpload.Serialize`. pub fn serialize( &self, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Messages.Linden.UploaderRequestUpload.Serialize", - ) + ::serialize_generated(self) } } @@ -68660,7 +68436,17 @@ pub mod messages { ::serialize_generated(self) } } - impl libremetaverse::interfaces::IMessage for ViewerStatsMessage {} + impl libremetaverse::interfaces::IMessage for ViewerStatsMessage { + fn deserialize( + &mut self, + map: libremetaverse_structured_data::OSDMap, + ) -> Result<(), crate::Error> { + ::deserialize_generated(self, &map) + } + fn serialize(&self) -> Result { + ::serialize_generated(self) + } + } } } diff --git a/crates/libremetaverse/src/lib.rs b/crates/libremetaverse/src/lib.rs index c8267c3..3f0caad 100644 --- a/crates/libremetaverse/src/lib.rs +++ b/crates/libremetaverse/src/lib.rs @@ -5,9 +5,11 @@ extern crate self as libremetaverse; #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. mod attention_catalog; mod bit_pack; +mod caps; mod caps_http; mod client_core; mod download_manager; +mod event_queue; #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. mod foliage_catalog; mod generated; @@ -15,6 +17,7 @@ mod generated; mod genepool_catalog; mod j2k; mod message_codec; +mod message_decoder; mod network_manager; #[rustfmt::skip] pub mod packet_catalog; diff --git a/crates/libremetaverse/src/message_codec.rs b/crates/libremetaverse/src/message_codec.rs index 3b07189..9ce6bcf 100644 --- a/crates/libremetaverse/src/message_codec.rs +++ b/crates/libremetaverse/src/message_codec.rs @@ -1,32 +1,44 @@ //! Native LLSD field codecs for the translated Linden message fixtures. use crate::messages::linden::{ - AgentGroupDataUpdateMessage, AgentGroupDataUpdateMessageGroupData, - AgentGroupDataUpdateMessageNewGroupData, ChatSessionAcceptInvitation, ChatSessionRequestBlock, + AgentDropGroupMessage, AgentDropGroupMessageAgentData, AgentGroupDataUpdateMessage, + AgentGroupDataUpdateMessageGroupData, AgentGroupDataUpdateMessageNewGroupData, + AgentStateUpdateMessage, BulkUpdateInventoryMessage, BulkUpdateInventoryMessageFolderDataInfo, + BulkUpdateInventoryMessageItemDataInfo, ChatSessionAcceptInvitation, ChatSessionRequestBlock, ChatSessionRequestMuteUpdate, ChatSessionRequestStartConference, ChatterBoxInvitationMessage, ChatterBoxSessionAgentListUpdatesMessage, ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock, ChatterBoxSessionStartReplyMessage, - ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, DirLandReplyMessage, - DirLandReplyMessageQueryReply, EnableSimulatorMessage, - EnableSimulatorMessageSimulatorInfoBlock, EstablishAgentCommunicationMessage, + ChatterboxSessionEventReplyMessage, CopyInventoryFromNotecardMessage, CrossedRegionMessage, + DirLandReplyMessage, DirLandReplyMessageQueryReply, DisplayNameUpdateMessage, + EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock, + EstablishAgentCommunicationMessage, ForceCloseChatterBoxSessionMessage, GetDisplayNamesMessage, + GetObjectCostMessage, GetObjectCostRequest, LandResourcesMessage, LandResourcesRequest, LandStatReplyMessage, LandStatReplyMessageReportDataBlock, MapLayerMessageBase, - MapLayerReplyVariant, MapLayerReplyVariantLayerData, ParcelObjectOwnersReplyMessage, + MapLayerReplyVariant, MapLayerReplyVariantLayerData, NavMeshStatusUpdateMessage, + ObjectMediaNavigateMessage, ObjectMediaRequest, ObjectMediaResponse, ObjectMediaUpdate, + ObjectPhysicsPropertiesMessage, ParcelObjectOwnersReplyMessage, ParcelObjectOwnersReplyMessagePrimOwner, ParcelPropertiesMessage, ParcelPropertiesUpdateMessage, ParcelVoiceInfoRequestMessage, PlacesReplyMessage, - PlacesReplyMessageQueryData, ProvisionVoiceAccountRequestMessage, RemoteParcelRequestReply, - RequiredVoiceVersionMessage, ScriptRunningReplyMessage, SendPostcardMessage, - TeleportFailedMessage, TeleportFinishMessage, UpdateAgentInformationMessage, - UpdateAgentInventoryRequestMessage, UpdateAgentLanguageMessage, - UpdateScriptAgentRequestMessage, UpdateScriptTaskUpdateMessage, ViewerStatsMessage, + PlacesReplyMessageQueryData, 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, GroupPowers, InstantMessageDialog, InstantMessageOnline, LandingType, ParcelCategory, - ParcelFlags, ParcelResult, ParcelStatus, SimAccess, TeleportFlags, + AgentDisplayName, Error, GroupPowers, InstantMessageDialog, InstantMessageOnline, LandingType, + MediaEntry, ParcelCategory, ParcelFlags, ParcelResult, ParcelStatus, PermissionMask, + PrimitivePhysicsProperties, SimAccess, TeleportFlags, }; -use libremetaverse_structured_data::{OSD, OSDMap}; +use flate2::read::ZlibDecoder; +use libremetaverse_structured_data::{OSD, OSDMap, OSDParser}; use libremetaverse_types::compat::Uri; -use libremetaverse_types::{UUID, Vector3}; +use libremetaverse_types::{AssetType, FolderType, InventoryType, SaleType, UUID, Vector3}; use std::collections::HashMap; +use std::io::Read as _; use std::net::{IpAddr, Ipv4Addr}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -100,6 +112,18 @@ impl MessageValue for f32 { } } +impl MessageValue for f64 { + fn message_default() -> Self { + 0.0 + } + fn to_message_osd(&self) -> Result { + OSD::from_real_with_double(*self) + } + fn from_message_osd(value: &OSD) -> Result { + value.as_real() + } +} + impl MessageValue for String { fn message_default() -> Self { String::new() @@ -181,6 +205,73 @@ impl MessageValue for IpAddr { } } +impl MessageValue for OSD { + fn message_default() -> Self { + Self::Undefined + } + fn to_message_osd(&self) -> Result { + Ok(self.clone()) + } + fn from_message_osd(value: &OSD) -> Result { + Ok(value.clone()) + } +} + +impl MessageValue for OSDMap { + fn message_default() -> Self { + Self::new_with_constructor().expect("empty OSD map") + } + fn to_message_osd(&self) -> Result { + self.copy() + } + fn from_message_osd(value: &OSD) -> Result { + let OSD::Map(values) = value else { + return Err(Error::Argument); + }; + Self::new_with_dictionary(values.clone()) + } +} + +impl MessageValue for AgentDisplayName { + fn message_default() -> Self { + Self::new().expect("AgentDisplayName construction is infallible") + } + fn to_message_osd(&self) -> Result { + self.get_osd() + } + fn from_message_osd(value: &OSD) -> Result { + Self::from_osd(value.clone()) + } +} + +impl MessageValue for PrimitivePhysicsProperties { + fn message_default() -> Self { + Self::new().expect("PrimitivePhysicsProperties construction is infallible") + } + fn to_message_osd(&self) -> Result { + self.get_osd() + } + fn from_message_osd(value: &OSD) -> Result { + Self::from_osd(value.clone()) + } +} + +impl MessageValue for MediaEntry { + fn message_default() -> Self { + Self::new().expect("MediaEntry construction is infallible") + } + fn to_message_osd(&self) -> Result { + if self.is_undefined() { + Ok(OSD::Undefined) + } else { + self.get_osd()?.copy() + } + } + fn from_message_osd(value: &OSD) -> Result { + Self::from_osd(value.clone()) + } +} + macro_rules! transparent_value { ($type:ty, $inner:ty) => { impl MessageValue for $type { @@ -199,6 +290,21 @@ macro_rules! transparent_value { transparent_value!(ParcelFlags, u32); transparent_value!(TeleportFlags, u32); +transparent_value!(PermissionMask, u32); + +impl MessageValue for InventoryType { + fn message_default() -> Self { + Self(0) + } + fn to_message_osd(&self) -> Result { + OSD::from_integer_with_int32(i32::from(self.0)) + } + fn from_message_osd(value: &OSD) -> Result { + Ok(Self( + i8::try_from(value.as_integer()?).map_err(|_| Error::Argument)?, + )) + } +} impl MessageValue for GroupPowers { fn message_default() -> Self { @@ -289,6 +395,88 @@ enum_value!( InstantMessageDialog::TaskInventoryOffered, ] ); +enum_value!( + SaleType, + SaleType::Not, + [ + SaleType::Contents, + SaleType::Copy, + SaleType::Not, + SaleType::Original + ] +); +enum_value!( + AssetType, + AssetType::Unknown, + [ + AssetType::Animation, + AssetType::Bodypart, + AssetType::CallingCard, + AssetType::Clothing, + AssetType::Folder, + AssetType::Gesture, + AssetType::ImageJPEG, + AssetType::ImageTGA, + AssetType::LSLBytecode, + AssetType::LSLText, + AssetType::Landmark, + AssetType::Link, + AssetType::LinkFolder, + AssetType::Material, + AssetType::Mesh, + AssetType::Notecard, + AssetType::Object, + AssetType::Person, + AssetType::Script, + AssetType::Settings, + AssetType::Simstate, + AssetType::Sound, + AssetType::SoundWAV, + AssetType::Texture, + AssetType::TextureTGA, + AssetType::Unknown, + AssetType::Widget + ] +); +enum_value!( + FolderType, + FolderType::None, + [ + FolderType::Animation, + FolderType::BasicRoot, + FolderType::BodyPart, + FolderType::CallingCard, + FolderType::Clothing, + FolderType::CurrentOutfit, + FolderType::EnsembleEnd, + FolderType::EnsembleStart, + FolderType::Favorites, + FolderType::Gesture, + FolderType::Inbox, + FolderType::LSLText, + FolderType::Landmark, + FolderType::LostAndFound, + FolderType::MarketplaceListings, + FolderType::MarketplaceStock, + FolderType::MarketplaceVersion, + FolderType::Material, + FolderType::Mesh, + FolderType::MyOutfits, + FolderType::None, + FolderType::Notecard, + FolderType::Object, + FolderType::OldRoot, + FolderType::Outbox, + FolderType::Outfit, + FolderType::Root, + FolderType::Settings, + FolderType::Snapshot, + FolderType::Sound, + FolderType::Suitcase, + FolderType::Texture, + FolderType::Trash + ] +); enum_value!( InstantMessageOnline, InstantMessageOnline::Online, @@ -444,6 +632,471 @@ fn integer_as_f32(value: &OSD) -> Result { Ok(value.as_integer()? as f32) } +fn serialize_agent_state_update(value: &AgentStateUpdateMessage) -> Result { + let access = map_osd([("max", value.max_access.to_message_osd()?)]); + let preferences = map_osd([ + ("access_prefs", access), + ( + "alter_navmesh_objects", + value.alter_navmesh_objects.to_message_osd()?, + ), + ( + "alter_permanent_objects", + value.alter_permanent_objects.to_message_osd()?, + ), + ("god_level", value.god_level.to_message_osd()?), + ("language", value.language.to_message_osd()?), + ( + "language_is_public", + value.language_is_public.to_message_osd()?, + ), + ]); + finish_map([ + ( + "can_modify_navmesh", + value.can_modify_navmesh.to_message_osd()?, + ), + ( + "has_modified_navmesh", + value.has_modified_navmesh.to_message_osd()?, + ), + ("preferences", preferences), + ]) +} + +fn deserialize_agent_state_update( + value: &mut AgentStateUpdateMessage, + map: &OSDMap, +) -> Result<(), Error> { + value.raw_data = map.clone(); + value.can_modify_navmesh = map + .get("can_modify_navmesh") + .unwrap_or_default() + .as_boolean()?; + value.has_modified_navmesh = map + .get("has_modified_navmesh") + .unwrap_or_default() + .as_boolean()?; + let Some(OSD::Map(preferences)) = map.get("preferences") else { + return Ok(()); + }; + let preferences = OSDMap::new_with_dictionary(preferences)?; + value.alter_navmesh_objects = preferences + .get("alter_navmesh_objects") + .unwrap_or_default() + .as_boolean()?; + value.alter_permanent_objects = preferences + .get("alter_permanent_objects") + .unwrap_or_default() + .as_boolean()?; + value.god_level = preferences + .get("god_level") + .unwrap_or_default() + .as_integer()?; + value.language = preferences + .get("language") + .unwrap_or_default() + .as_string()?; + value.language_is_public = preferences + .get("language_is_public") + .unwrap_or_default() + .as_boolean()?; + if let Some(OSD::Map(access)) = preferences.get("access_prefs") { + value.max_access = access.get("max").unwrap_or(&OSD::Undefined).as_string()?; + } + Ok(()) +} + +fn serialize_crossed_region(value: &CrossedRegionMessage) -> Result { + finish_map([ + ( + "Info", + one_block_osd([ + ("LookAt", value.look_at.to_message_osd()?), + ("Position", value.position.to_message_osd()?), + ]), + ), + ( + "AgentData", + one_block_osd([ + ("AgentID", value.agent_id.to_message_osd()?), + ("SessionID", value.session_id.to_message_osd()?), + ]), + ), + ( + "RegionData", + one_block_osd([ + ("RegionHandle", value.region_handle.to_message_osd()?), + ("SeedCapability", value.seed_capability.to_message_osd()?), + ("SimIP", value.ip.to_message_osd()?), + ("SimPort", value.port.to_message_osd()?), + ("RegionSizeX", value.region_size_x.to_message_osd()?), + ("RegionSizeY", value.region_size_y.to_message_osd()?), + ]), + ), + ]) +} + +fn first_block(map: &OSDMap, key: &str) -> Result { + let encoded = required(map, key)?; + let blocks = as_array(&encoded)?; + let block = as_map(blocks.first().ok_or(Error::Argument)?)?; + OSDMap::new_with_dictionary(block.clone()) +} + +fn deserialize_crossed_region(value: &mut CrossedRegionMessage, map: &OSDMap) -> Result<(), Error> { + let info = first_block(map, "Info")?; + value.look_at = decode_top(&info, "LookAt")?; + value.position = decode_top(&info, "Position")?; + let agent = first_block(map, "AgentData")?; + value.agent_id = decode_top(&agent, "AgentID")?; + value.session_id = decode_top(&agent, "SessionID")?; + let region = first_block(map, "RegionData")?; + value.region_handle = decode_top(®ion, "RegionHandle")?; + value.seed_capability = decode_top(®ion, "SeedCapability")?; + value.ip = decode_top(®ion, "SimIP")?; + value.port = decode_top(®ion, "SimPort")?; + if region.get("RegionSizeX").is_some() { + value.region_size_x = decode_top(®ion, "RegionSizeX")?; + } + if region.get("RegionSizeY").is_some() { + value.region_size_y = decode_top(®ion, "RegionSizeY")?; + } + Ok(()) +} + +fn serialize_get_object_cost(value: &GetObjectCostMessage) -> Result { + let values = HashMap::from([ + ( + "linked_set_resource_cost".to_owned(), + OSD::Real(value.link_cost), + ), + ("resource_cost".to_owned(), OSD::Real(value.object_cost)), + ("physics_cost".to_owned(), OSD::Real(value.physics_cost)), + ( + "linked_set_physics_cost".to_owned(), + OSD::Real(value.link_physics_cost), + ), + ]); + OSDMap::new_with_dictionary(HashMap::from([( + value.object_id.to_string(), + OSD::Map(values), + )])) +} + +fn deserialize_get_object_cost( + value: &mut GetObjectCostMessage, + map: &OSDMap, +) -> Result<(), Error> { + let mut entries: Vec<_> = map.snapshot().into_iter().collect(); + entries.sort_unstable_by(|left, right| left.0.cmp(&right.0)); + let Some((id, OSD::Map(costs))) = entries.into_iter().next() else { + return Err(Error::Argument); + }; + value.object_id = UUID::new_with_string(id).unwrap_or_else(|_| UUID::zero()); + value.link_cost = costs + .get("linked_set_resource_cost") + .unwrap_or(&OSD::Undefined) + .as_real()?; + value.object_cost = costs + .get("resource_cost") + .unwrap_or(&OSD::Undefined) + .as_real()?; + value.physics_cost = costs + .get("physics_cost") + .unwrap_or(&OSD::Undefined) + .as_real()?; + value.link_physics_cost = costs + .get("linked_set_physics_cost") + .unwrap_or(&OSD::Undefined) + .as_real()?; + Ok(()) +} + +fn serialize_nav_mesh_status(value: &NavMeshStatusUpdateMessage) -> Result { + finish_map([ + ("region_id", value.region_id.to_message_osd()?), + ("status", value.status.to_message_osd()?), + ("version", value.version.to_message_osd()?), + ]) +} + +fn deserialize_nav_mesh_status( + value: &mut NavMeshStatusUpdateMessage, + map: &OSDMap, +) -> Result<(), Error> { + value.raw_data = map.clone(); + value.region_id = decode_top(map, "region_id")?; + value.status = decode_top(map, "status")?; + value.version = decode_top(map, "version")?; + Ok(()) +} + +fn serialize_region_info(value: &RegionInfoMessage) -> Result { + finish_map([ + ("parcel_local_id", value.parcel_local_id.to_message_osd()?), + ("region_name", value.region_name.to_message_osd()?), + ( + "voice_credentials", + map_osd([("channel_uri", value.channel_uri.to_message_osd()?)]), + ), + ]) +} + +fn deserialize_region_info(value: &mut RegionInfoMessage, map: &OSDMap) -> Result<(), Error> { + value.parcel_local_id = decode_top(map, "parcel_local_id")?; + value.region_name = decode_top(map, "region_name")?; + let OSD::Map(voice) = required(map, "voice_credentials")? else { + return Err(Error::Argument); + }; + value.channel_uri = voice + .get("channel_uri") + .unwrap_or(&OSD::Undefined) + .as_string()?; + Ok(()) +} + +fn serialize_render_materials(_value: &RenderMaterialsMessage) -> Result { + OSDMap::new_with_constructor() +} + +#[allow(clippy::unnecessary_wraps)] // GeneratedMessage requires a fallible codec signature. +fn deserialize_render_materials( + value: &mut RenderMaterialsMessage, + map: &OSDMap, +) -> Result<(), Error> { + let decoded = map + .get("Zipped") + .ok_or(Error::Argument) + .and_then(|value| value.as_binary()) + .and_then(|bytes| { + let mut output = Vec::new(); + ZlibDecoder::new(bytes.as_slice()) + .read_to_end(&mut output) + .map_err(|_| Error::Argument)?; + OSDParser::deserialize_llsd_binary_with_bytes(output) + }); + value.material_data = decoded.unwrap_or_else(|_| OSD::Map(HashMap::new())); + Ok(()) +} + +fn serialize_set_display_name(value: &SetDisplayNameMessage) -> Result { + finish_map([( + "display_name", + OSD::Array(vec![ + OSD::String(value.old_display_name.clone()), + OSD::String(value.new_display_name.clone()), + ]), + )]) +} + +fn deserialize_set_display_name( + value: &mut SetDisplayNameMessage, + map: &OSDMap, +) -> Result<(), Error> { + let OSD::Array(names) = required(map, "display_name")? else { + return Err(Error::Argument); + }; + value.old_display_name = names.first().unwrap_or(&OSD::Undefined).as_string()?; + value.new_display_name = names.get(1).unwrap_or(&OSD::Undefined).as_string()?; + Ok(()) +} + +fn serialize_set_display_name_reply(value: &SetDisplayNameReplyMessage) -> Result { + finish_map([ + ("content", value.display_name.to_message_osd()?), + ("reason", value.reason.to_message_osd()?), + ("status", value.status.to_message_osd()?), + ]) +} + +fn deserialize_set_display_name_reply( + value: &mut SetDisplayNameReplyMessage, + map: &OSDMap, +) -> Result<(), Error> { + value.display_name = decode_top(map, "content")?; + value.reason = decode_top(map, "reason")?; + value.status = decode_top(map, "status")?; + Ok(()) +} + +fn serialize_display_name_update(value: &DisplayNameUpdateMessage) -> Result { + let OSD::Map(mut agent) = value.display_name.to_message_osd()? else { + return Err(Error::Argument); + }; + agent.insert( + "old_display_name".into(), + OSD::String(value.old_display_name.clone()), + ); + finish_map([("agent", OSD::Map(agent))]) +} + +fn deserialize_display_name_update( + value: &mut DisplayNameUpdateMessage, + map: &OSDMap, +) -> Result<(), Error> { + let agent = required(map, "agent")?; + let OSD::Map(agent_map) = &agent else { + return Err(Error::Argument); + }; + value.display_name = AgentDisplayName::from_osd(agent.clone())?; + value.old_display_name = agent_map + .get("old_display_name") + .unwrap_or(&OSD::Undefined) + .as_string()?; + Ok(()) +} + +fn serialize_uploader_upload(value: &UploaderRequestUpload) -> Result { + finish_map([ + ("state", OSD::String("upload".into())), + ("uploader", value.url.to_message_osd()?), + ]) +} + +fn deserialize_uploader_upload( + value: &mut UploaderRequestUpload, + map: &OSDMap, +) -> Result<(), Error> { + if required(map, "state")?.as_string()? != "upload" { + return Err(Error::Argument); + } + value.url = decode_top(map, "uploader")?; + Ok(()) +} + +fn serialize_uploader_complete(value: &UploaderRequestComplete) -> Result { + finish_map([ + ("state", OSD::String("complete".into())), + ("new_asset", value.asset_id.to_message_osd()?), + ]) +} + +fn deserialize_uploader_complete( + value: &mut UploaderRequestComplete, + map: &OSDMap, +) -> Result<(), Error> { + if required(map, "state")?.as_string()? != "complete" { + return Err(Error::Argument); + } + value.asset_id = decode_top(map, "new_asset")?; + Ok(()) +} + +fn serialize_bulk_folder( + value: &BulkUpdateInventoryMessageFolderDataInfo, +) -> Result { + finish_map([ + ("FolderID", value.folder_id.to_message_osd()?), + ("ParentID", value.parent_id.to_message_osd()?), + ("Name", value.name.to_message_osd()?), + ("Type", value.type_.to_message_osd()?), + ]) +} + +fn deserialize_bulk_folder( + value: &mut BulkUpdateInventoryMessageFolderDataInfo, + map: &OSDMap, +) -> Result<(), Error> { + value.folder_id = decode_top(map, "FolderID")?; + value.parent_id = decode_top(map, "ParentID")?; + value.name = decode_top(map, "Name")?; + value.type_ = decode_top(map, "Type")?; + Ok(()) +} + +fn serialize_bulk_item(value: &BulkUpdateInventoryMessageItemDataInfo) -> Result { + finish_map([ + ("ItemID", value.item_id.to_message_osd()?), + ("CallbackID", value.callback_id.to_message_osd()?), + ("FolderID", value.folder_id.to_message_osd()?), + ("CreatorID", value.creator_id.to_message_osd()?), + ("OwnerID", value.owner_id.to_message_osd()?), + ("GroupID", value.group_id.to_message_osd()?), + ("BaseMask", value.base_mask.to_message_osd()?), + ("OwnerMask", value.owner_mask.to_message_osd()?), + ("GroupMask", value.group_mask.to_message_osd()?), + ("EveryoneMask", value.everyone_mask.to_message_osd()?), + ("NextOwnerMask", value.next_owner_mask.to_message_osd()?), + ("GroupOwned", value.group_owned.to_message_osd()?), + ("AssetID", value.asset_id.to_message_osd()?), + ("Type", value.type_.to_message_osd()?), + ("InvType", value.inv_type.to_message_osd()?), + ("Flags", value.flags.to_message_osd()?), + ("SaleType", value.sale_type.to_message_osd()?), + ("SalePrice", value.sale_price.to_message_osd()?), + ("Name", value.name.to_message_osd()?), + ("Description", value.description.to_message_osd()?), + ( + "CreationDate", + unix_seconds(value.creation_date).to_message_osd()?, + ), + ("CRC", value.crc.to_message_osd()?), + ]) +} + +fn deserialize_bulk_item( + value: &mut BulkUpdateInventoryMessageItemDataInfo, + map: &OSDMap, +) -> Result<(), Error> { + value.item_id = decode_top(map, "ItemID")?; + value.callback_id = decode_top(map, "CallbackID")?; + value.folder_id = decode_top(map, "FolderID")?; + value.creator_id = decode_top(map, "CreatorID")?; + value.owner_id = decode_top(map, "OwnerID")?; + value.group_id = decode_top(map, "GroupID")?; + value.base_mask = decode_top(map, "BaseMask")?; + value.owner_mask = decode_top(map, "OwnerMask")?; + value.group_mask = decode_top(map, "GroupMask")?; + value.everyone_mask = decode_top(map, "EveryoneMask")?; + value.next_owner_mask = decode_top(map, "NextOwnerMask")?; + value.group_owned = decode_top(map, "GroupOwned")?; + value.asset_id = decode_top(map, "AssetID")?; + value.type_ = decode_top(map, "Type")?; + value.inv_type = decode_top(map, "InvType")?; + value.flags = decode_top(map, "Flags")?; + value.sale_type = decode_top(map, "SaleType")?; + // Preserve the reference adapter's field selection for wire compatibility. + value.sale_price = decode_top(map, "SaleType")?; + value.name = decode_top(map, "Name")?; + value.description = decode_top(map, "Description")?; + value.creation_date = time_from_unix(decode_top(map, "CreationDate")?); + value.crc = decode_top(map, "CRC")?; + Ok(()) +} + +fn serialize_bulk_inventory(value: &BulkUpdateInventoryMessage) -> Result { + finish_map([ + ( + "AgentData", + one_block_osd([ + ("AgentID", value.agent_id.to_message_osd()?), + ("TransactionID", value.transaction_id.to_message_osd()?), + ]), + ), + ("FolderData", encode_struct_array(&value.folder_data)?), + ("ItemData", encode_struct_array(&value.item_data)?), + ]) +} + +fn deserialize_bulk_inventory( + value: &mut BulkUpdateInventoryMessage, + map: &OSDMap, +) -> Result<(), Error> { + let agent = first_block(map, "AgentData")?; + value.agent_id = decode_top(&agent, "AgentID")?; + value.transaction_id = decode_top(&agent, "TransactionID")?; + value.folder_data = match map.get("FolderData") { + Some(encoded) => decode_array(&encoded)?, + None => Vec::new(), + }; + value.item_data = match map.get("ItemData") { + Some(encoded) => decode_array(&encoded)?, + None => Vec::new(), + }; + Ok(()) +} + macro_rules! map_message { ($type:ty { scalar: [$($scalar:ident => $scalar_key:literal),* $(,)?], array: [$($array:ident => $array_key:literal),* $(,)?], bytes: [$($bytes:ident => $bytes_key:literal),* $(,)?] } $(init $init:expr)?) => { impl GeneratedMessage for $type { @@ -549,6 +1202,36 @@ map_message!(AgentGroupDataUpdateMessageNewGroupData { array: [], bytes: [] }); +map_message!(AgentDropGroupMessageAgentData { + scalar: [agent_id => "AgentID", group_id => "GroupID"], + array: [], + bytes: [] +}); +map_message!(AgentDropGroupMessage { + scalar: [], + array: [agent_data_block => "AgentData"], + bytes: [] +}); +custom_message!(BulkUpdateInventoryMessageFolderDataInfo { + scalar: [folder_id, name, parent_id, type_], + array: [], + bytes: [] +} => serialize_bulk_folder, deserialize_bulk_folder); +custom_message!(BulkUpdateInventoryMessageItemDataInfo { + scalar: [asset_id, base_mask, crc, callback_id, creation_date, creator_id, description, everyone_mask, flags, folder_id, group_id, group_mask, group_owned, inv_type, item_id, name, next_owner_mask, owner_id, owner_mask, sale_price, sale_type, type_], + array: [], + bytes: [] +} => serialize_bulk_item, deserialize_bulk_item); +custom_message!(BulkUpdateInventoryMessage { + scalar: [agent_id, transaction_id], + array: [folder_data, item_data], + bytes: [] +} => serialize_bulk_inventory, deserialize_bulk_inventory); +custom_message!(AgentStateUpdateMessage { + scalar: [alter_navmesh_objects, alter_permanent_objects, can_modify_navmesh, god_level, has_modified_navmesh, language, language_is_public, max_access, raw_data], + array: [], + bytes: [] +} => serialize_agent_state_update, deserialize_agent_state_update); custom_message!(AgentGroupDataUpdateMessage { scalar: [agent_id, avatar_id], array: [group_data_block, new_group_data_block], @@ -583,6 +1266,14 @@ custom_message!(ChatterBoxInvitationMessage { array: [], bytes: [binary_bucket] } => serialize_chatter_invitation, deserialize_chatter_invitation); +custom_message!(CrossedRegionMessage { + scalar: [agent_id, ip, look_at, port, position, region_handle, region_size_x, region_size_y, seed_capability, session_id], + array: [], + bytes: [] +} => serialize_crossed_region, deserialize_crossed_region, init |value: &mut CrossedRegionMessage| { + value.region_size_x = crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X; + value.region_size_y = crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y; +}); map_message!(ChatterBoxSessionAgentListUpdatesMessageAgentUpdatesBlock { scalar: [ agent_id => "agent_id", @@ -859,6 +1550,96 @@ map_message!(RemoteParcelRequestReply { array: [], bytes: [] }); +map_message!(SearchStatRequestRequest { + scalar: [classified_id => "classified_id"], + array: [], + bytes: [] +}); +map_message!(SearchStatRequestReply { + scalar: [ + map_clicks => "map_clicks", + profile_clicks => "profile_clicks", + search_map_clicks => "search_map_clicks", + search_profile_clicks => "search_profile_clicks", + search_teleport_clicks => "search_teleport_clicks", + teleport_clicks => "teleport_clicks" + ], + array: [], + bytes: [] +}); +map_message!(ForceCloseChatterBoxSessionMessage { + scalar: [reason => "reason", session_id => "session_id"], + array: [], + bytes: [] +}); +map_message!(GetDisplayNamesMessage { + scalar: [], + array: [agents => "agents", bad_i_ds => "bad_ids"], + bytes: [] +}); +custom_message!(GetObjectCostMessage { + scalar: [link_cost, link_physics_cost, object_cost, object_id, physics_cost], + array: [], + bytes: [] +} => serialize_get_object_cost, deserialize_get_object_cost); +map_message!(GetObjectCostRequest { + scalar: [], + array: [object_i_ds => "object_ids"], + bytes: [] +}); +map_message!(LandResourcesMessage { + scalar: [ + script_resource_details => "ScriptResourceDetails", + script_resource_summary => "ScriptResourceSummary" + ], + array: [], + bytes: [] +}); +map_message!(LandResourcesRequest { + scalar: [parcel_id => "parcel_id"], + array: [], + bytes: [] +}); +custom_message!(NavMeshStatusUpdateMessage { + scalar: [raw_data, region_id, status, version], + array: [], + bytes: [] +} => serialize_nav_mesh_status, deserialize_nav_mesh_status); +map_message!(ObjectMediaNavigateMessage { + scalar: [face => "texture_index", prim_id => "object_id", url => "current_url"], + array: [], + bytes: [] +}); +map_message!(ObjectMediaRequest { + scalar: [prim_id => "object_id", verb => "verb"], + array: [], + bytes: [] +} init |value: &mut ObjectMediaRequest| value.verb = "GET".into()); +map_message!(ObjectMediaResponse { + scalar: [prim_id => "object_id", version => "object_media_version"], + array: [face_media => "object_media_data"], + bytes: [] +}); +map_message!(ObjectMediaUpdate { + scalar: [prim_id => "object_id", verb => "verb"], + array: [face_media => "object_media_data"], + bytes: [] +} init |value: &mut ObjectMediaUpdate| value.verb = "UPDATE".into()); +map_message!(ObjectPhysicsPropertiesMessage { + scalar: [], + array: [object_physics_properties => "ObjectData"], + bytes: [] +}); +custom_message!(RegionInfoMessage { + scalar: [channel_uri, parcel_local_id, region_name], + array: [], + bytes: [] +} => serialize_region_info, deserialize_region_info); +custom_message!(RenderMaterialsMessage { + scalar: [material_data], + array: [], + bytes: [] +} => serialize_render_materials, deserialize_render_materials); map_message!(RequiredVoiceVersionMessage { scalar: [ major_version => "major_version", @@ -884,6 +1665,26 @@ map_message!(SendPostcardMessage { array: [], bytes: [] }); +custom_message!(SetDisplayNameMessage { + scalar: [new_display_name, old_display_name], + array: [], + bytes: [] +} => serialize_set_display_name, deserialize_set_display_name); +custom_message!(SetDisplayNameReplyMessage { + scalar: [display_name, reason, status], + array: [], + bytes: [] +} => serialize_set_display_name_reply, deserialize_set_display_name_reply); +custom_message!(DisplayNameUpdateMessage { + scalar: [display_name, old_display_name], + array: [], + bytes: [] +} => serialize_display_name_update, deserialize_display_name_update); +map_message!(SimConsoleResponseMessage { + scalar: [body => "body"], + array: [], + bytes: [] +}); custom_message!(TeleportFailedMessage { scalar: [agent_id, extra_params, message_key, reason], array: [], @@ -982,6 +1783,16 @@ custom_message!(ViewerStatsMessage { array: [], bytes: [] } => serialize_viewer_stats, deserialize_viewer_stats); +custom_message!(UploaderRequestUpload { + scalar: [url], + array: [], + bytes: [] +} => serialize_uploader_upload, deserialize_uploader_upload); +custom_message!(UploaderRequestComplete { + scalar: [asset_id], + array: [], + bytes: [] +} => serialize_uploader_complete, deserialize_uploader_complete); fn serialize_agent_group(value: &AgentGroupDataUpdateMessage) -> Result { finish_map([ diff --git a/crates/libremetaverse/src/message_decoder.rs b/crates/libremetaverse/src/message_decoder.rs new file mode 100644 index 0000000..433380a --- /dev/null +++ b/crates/libremetaverse/src/message_decoder.rs @@ -0,0 +1,1267 @@ +//! 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, + display_name: Option, + legacy_first_name: Option, + legacy_last_name: Option, + is_default_display_name: bool, + next_update: SystemTime, + updated: SystemTime, +} + +impl AgentDisplayName { + pub fn new() -> Result { + 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 { + 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 { + 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 { + self.display_name.clone() + } + pub fn set_display_name(&mut self, value: Option) { + 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 { + self.legacy_first_name.clone() + } + pub fn set_legacy_first_name(&mut self, value: Option) { + 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 { + self.legacy_last_name.clone() + } + pub fn set_legacy_last_name(&mut self, value: Option) { + 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 { + self.user_name.clone() + } + pub fn set_user_name(&mut self, value: Option) { + self.user_name = value; + } +} + +fn optional_string(value: Option<&OSD>) -> Result, 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 { + 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 { + 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 { + 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, + pub width: i32, +} + +impl MediaEntry { + pub fn new() -> Result { + 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 { + 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::, _>>()?; + } + 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::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, key: &str) -> Result { + map.get(key).unwrap_or(&OSD::Undefined).as_boolean() +} + +fn osd_integer(map: &HashMap, key: &str) -> Result { + map.get(key).unwrap_or(&OSD::Undefined).as_integer() +} + +fn osd_string(map: &HashMap, key: &str) -> Result { + map.get(key).unwrap_or(&OSD::Undefined).as_string() +} + +/// Resource-usage response for the agent's attachments. +pub struct AttachmentResourcesMessage { + pub attachments: HashMap>, + pub summary_available: HashMap, + pub summary_used: HashMap, +} + +impl AttachmentResourcesMessage { + pub fn new() -> Result { + 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 { + 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>, 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 { + 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, +) -> 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) -> 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 { + 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::, 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(map: &OSDMap) -> Option> +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> { + ::deserialize_generated(self, &map) + } + + fn serialize(&self) -> Result { + ::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>, Error> { + let decoded = match event_name { + "AgentGroupDataUpdate" | "AvatarGroupsReply" => { + generated::(map) + } + "ParcelProperties" => generated::(map), + "ParcelObjectOwnersReply" => generated::(map), + "TeleportFinish" => generated::(map), + "EnableSimulator" => generated::(map), + "ParcelPropertiesUpdate" => generated::(map), + "EstablishAgentCommunication" => generated::(map), + "ChatterBoxInvitation" => generated::(map), + "ChatterBoxSessionEventReply" => generated::(map), + "ChatterBoxSessionStartReply" => generated::(map), + "ChatterBoxSessionAgentListUpdates" => { + generated::(map) + } + "RequiredVoiceVersion" => generated::(map), + "MapLayer" if map.get("LayerData").is_some() => generated::(map), + "ChatSessionRequest" => match map + .get("method") + .unwrap_or_default() + .as_string() + .unwrap_or_default() + .as_str() + { + "start conference" => generated::(map), + "mute update" => generated::(map), + "accept invitation" => generated::(map), + _ => None, + }, + "CopyInventoryFromNotecard" => generated::(map), + "ProvisionVoiceAccountRequest" => generated::(map), + "Viewerstats" | "ViewerStats" => generated::(map), + "UpdateAgentLanguage" => generated::(map), + "RemoteParcelRequest" if map.get("parcel_id").is_some() => { + generated::(map) + } + "UpdateScriptTask" if map.get("task_id").is_some() => { + generated::(map) + } + "UpdateScriptAgent" if map.get("item_id").is_some() => { + generated::(map) + } + "UpdateGestureAgentInventory" | "UpdateNotecardAgentInventory" + if map.get("item_id").is_some() => + { + generated::(map) + } + "SendPostcard" => generated::(map), + "LandStatReply" => generated::(map), + "ParcelVoiceInfoRequest" => generated::(map), + "EventQueueGet" => { + let mut message = crate::event_queue::EventQueueGetMessage::new()?; + match message.deserialize(map.clone()) { + Ok(()) => Some(Box::new(message) as Box), + Err(_) => None, + } + } + "CrossedRegion" => generated::(map), + "SimConsoleResponse" => generated::(map), + "TeleportFailed" => generated::(map), + "PlacesReply" => generated::(map), + "UpdateAgentInformation" => generated::(map), + "DirLandReply" => generated::(map), + "ScriptRunningReply" => generated::(map), + "AgentDropGroup" => generated::(map), + "AgentStateUpdate" => generated::(map), + "NavMeshStatusUpdate" => generated::(map), + "ForceCloseChatterBoxSession" => generated::(map), + "RegionInfo" => generated::(map), + "ObjectMediaNavigate" => generated::(map), + "ObjectMedia" => match map + .get("verb") + .unwrap_or_default() + .as_string() + .unwrap_or_default() + .as_str() + { + "GET" => generated::(map), + "UPDATE" => generated::(map), + _ if map.get("object_media_version").is_some() => generated::(map), + _ => None, + }, + "SetDisplayName" => generated::(map), + "SetDisplayNameReply" => generated::(map), + "DisplayNameUpdate" => generated::(map), + "GetDisplayNames" => generated::(map), + "ObjectPhysicsProperties" => generated::(map), + "RenderMaterials" => generated::(map), + "GetObjectCost" => { + if map.get("object_ids").is_some() { + generated::(map) + } else { + generated::(map) + } + } + "LandResources" => { + if map.get("parcel_id").is_some() { + generated::(map) + } else if map.get("ScriptResourceSummary").is_some() { + generated::(map) + } else { + None + } + } + "SearchStatRequest" => { + if map.get("map_clicks").is_some() { + generated::(map) + } else if map.get("classified_id").is_some() { + generated::(map) + } else { + None + } + } + "UploadBakedTexture" => match map + .get("state") + .unwrap_or_default() + .as_string() + .unwrap_or_default() + .as_str() + { + "upload" => generated::(map), + "complete" => generated::(map), + _ => None, + }, + "AttachmentResources" => { + let mut message = AttachmentResourcesMessage::new()?; + match message.deserialize(map.clone()) { + Ok(()) => Some(Box::new(message) as Box), + Err(_) => None, + } + } + "BulkUpdateInventory" => generated::(map), + _ => None, + }; + Ok(decoded) +} + +#[allow(clippy::needless_pass_by_value)] // Preserves MessageUtils.ToIP's generated signature. +pub(crate) fn to_ip(osd: OSD) -> Result { + 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 { + 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, 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, 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) -> Result { + OSDMap::new_with_dictionary( + dict.into_iter() + .map(|(key, value)| (key, OSD::String(value))) + .collect(), + ) +} + +pub(crate) fn from_dictionary_uri(dict: HashMap) -> Result { + OSDMap::new_with_dictionary( + dict.into_iter() + .map(|(key, value)| (key.0, OSD::Uri(value))) + .collect(), + ) +} + +#[cfg(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) -> 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::() + .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::() + .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::() + .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::() + .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::().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()); + } +} diff --git a/crates/libremetaverse/src/network_manager.rs b/crates/libremetaverse/src/network_manager.rs index 9bd8313..55d4ca7 100644 --- a/crates/libremetaverse/src/network_manager.rs +++ b/crates/libremetaverse/src/network_manager.rs @@ -49,14 +49,6 @@ impl Clone for Packet { } } -impl Clone for Caps { - fn clone(&self) -> Self { - Self { - simulator: self.simulator.clone(), - } - } -} - fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { value .lock() @@ -236,6 +228,23 @@ pub struct SimConnectedEventArgs { simulator: Simulator, } +/// Event arguments raised when a simulator's `EventQueueGet` poller is live. +#[derive(Clone)] +pub struct EventQueueRunningEventArgs { + simulator: Simulator, +} + +impl EventQueueRunningEventArgs { + pub const fn new(simulator: Simulator) -> Result { + Ok(Self { simulator }) + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + impl SimConnectedEventArgs { pub fn new(simulator: Simulator) -> Result { Ok(Self { simulator }) @@ -849,6 +858,9 @@ pub struct SimulatorData { disconnect_candidate: AtomicBool, circuit_code: AtomicU32, seed_caps: RwLock>, + caps_state: RwLock>>, + event_queue_generation: Mutex, + event_queue_wait: Condvar, manager: Mutex>, transport: UDPBase, transport_thread: Mutex>, @@ -858,6 +870,14 @@ pub struct SimulatorData { impl Drop for SimulatorData { fn drop(&mut self) { + if let Some(caps) = self + .caps_state + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take() + { + caps.disconnect(true); + } let runtime = self .transport_thread .get_mut() @@ -873,7 +893,6 @@ impl Drop for SimulatorData { } /// Reference-identity simulator handle. -#[derive(Clone)] pub struct Simulator { /// Capability client associated with this simulator. /// @@ -883,6 +902,19 @@ pub struct Simulator { data: Arc, } +impl Clone for Simulator { + fn clone(&self) -> Self { + let mut clone = self.native_clone_without_caps(); + if let Some(inner) = read(&self.data.caps_state).clone() { + clone.caps = Some(Box::new(Caps::native_from_inner( + inner, + clone.native_clone_without_caps(), + ))); + } + clone + } +} + impl std::ops::Deref for Simulator { type Target = SimulatorData; @@ -987,6 +1019,9 @@ impl Simulator { disconnect_candidate: AtomicBool::new(false), circuit_code: AtomicU32::new(0), seed_caps: RwLock::new(None), + caps_state: RwLock::new(None), + event_queue_generation: Mutex::new(0), + event_queue_wait: Condvar::new(), manager: Mutex::new(Weak::new()), transport, transport_thread: Mutex::new(None), @@ -997,6 +1032,29 @@ impl Simulator { Ok(Self { caps: None, data }) } + pub(crate) fn native_clone_without_caps(&self) -> Self { + Self { + caps: None, + data: Arc::clone(&self.data), + } + } + + pub(crate) fn native_data_weak(&self) -> Weak { + Arc::downgrade(&self.data) + } + + pub(crate) fn native_data_arc(&self) -> Arc { + Arc::clone(&self.data) + } + + pub(crate) fn native_from_weak(data: &Weak) -> Option { + data.upgrade().map(|data| Self { caps: None, data }) + } + + pub(crate) fn native_from_data(data: Arc) -> Self { + Self { caps: None, data } + } + fn attach_manager(&self, manager: &Arc, circuit_code: u32) { *mutex(&self.manager) = Arc::downgrade(manager); self.circuit_code.store(circuit_code, Ordering::Release); @@ -1084,6 +1142,10 @@ impl Simulator { } pub fn native_disconnect(&self, send_close_circuit: bool) -> Result<(), Error> { + if let Some(caps) = write(&self.caps_state).take() { + caps.disconnect(true); + } + *write(&self.seed_caps) = None; self.disconnect_candidate.store(false, Ordering::Release); let was_connected = self.connected.swap(false, Ordering::AcqRel); self.handshake_complete.store(false, Ordering::Release); @@ -1198,14 +1260,114 @@ impl Simulator { seed_caps: Option, changed_sim: Option, ) -> Result<(), Error> { - let mut current = write(&self.seed_caps); - if !changed_sim.unwrap_or(false) && *current == seed_caps { + if !changed_sim.unwrap_or(false) && *read(&self.seed_caps) == seed_caps { return Ok(()); } - *current = seed_caps; + if let Some(previous) = write(&self.caps_state).take() { + previous.disconnect(true); + } + write(&self.seed_caps).clone_from(&seed_caps); + if let Some(seed_caps) = seed_caps { + let caps = Caps::native_create(self, seed_caps)?; + *write(&self.caps_state) = Some(caps); + } Ok(()) } + pub fn native_is_event_queue_running( + &self, + block_until_running: Option, + ) -> Result { + if self.caps_running() { + return Ok(true); + } + if !block_until_running.unwrap_or(false) { + return Ok(false); + } + let mut generation = mutex(&self.event_queue_generation); + for delay in [1_u64, 2, 4, 8] { + if self.caps_running() || self.current_sim_caps_running() { + return Ok(true); + } + self.start_current_event_queue(); + let observed = *generation; + let (next, _) = self + .event_queue_wait + .wait_timeout_while(generation, Duration::from_secs(delay), |value| { + *value == observed + && !self.client.cancellation_token().is_cancellation_requested() + }) + .unwrap_or_else(std::sync::PoisonError::into_inner); + generation = next; + if *generation != observed { + break; + } + } + Ok(self.caps_running() || self.current_sim_caps_running()) + } + + fn caps_running(&self) -> bool { + read(&self.caps_state).as_ref().is_some_and(|inner| { + Caps::native_from_inner(Arc::clone(inner), self.native_clone_without_caps()) + .is_event_queue_running() + }) + } + + fn current_sim_caps_running(&self) -> bool { + mutex(&self.manager) + .upgrade() + .and_then(|manager| read(&manager.current_sim).clone()) + .is_some_and(|simulator| simulator.caps_running()) + } + + fn start_current_event_queue(&self) { + let simulator = mutex(&self.manager) + .upgrade() + .and_then(|manager| read(&manager.current_sim).clone()) + .unwrap_or_else(|| self.native_clone_without_caps()); + if let Some(inner) = read(&simulator.caps_state).clone() { + let caps = Caps::native_from_inner(inner, simulator.native_clone_without_caps()); + if let Some(queue) = caps.event_queue() { + let _ = queue.start(); + } + } + } + + pub(crate) fn native_raise_event_queue_running(&self) { + let mut generation = mutex(&self.event_queue_generation); + *generation = generation.wrapping_add(1); + drop(generation); + self.event_queue_wait.notify_all(); + if let Some(manager) = mutex(&self.manager).upgrade() { + let args = EventQueueRunningEventArgs { + simulator: self.clone(), + }; + manager + .events + .event_queue_running + .emit_with(|| args.clone()); + } + } + + pub(crate) fn native_dispatch_caps_event(&self, event_name: &str, message: &dyn IMessage) { + if let Some(manager) = mutex(&self.manager).upgrade() { + manager + .caps_events + .invoke_raise_event(event_name, message, self.clone()); + } + } + + pub(crate) fn native_enqueue_caps_packet(&self, packet: Packet) -> Result<(), Error> { + let manager = mutex(&self.manager) + .upgrade() + .ok_or(Error::InvalidOperation)?; + manager.enqueue_incoming(NetworkManagerIncomingPacket { + packet: Some(packet), + simulator: Some(self.clone()), + raw_data: None, + }) + } + #[must_use] pub fn seed_capability(&self) -> Option { read(&self.seed_caps).clone() @@ -1262,6 +1424,11 @@ impl Simulator { self.connected.load(Ordering::Acquire) } + #[cfg(test)] + pub(crate) fn native_set_connected_for_tests(&self, connected: bool) { + self.connected.store(connected, Ordering::Release); + } + #[must_use] pub fn native_handshake_complete(&self) -> bool { self.handshake_complete.load(Ordering::Acquire) @@ -1397,6 +1564,7 @@ struct NetworkWorkers { #[derive(Default)] struct NetworkEvents { disconnected: EventRegistry, + event_queue_running: EventRegistry, generic_streaming_message: EventRegistry, packet_sent: EventRegistry, sim_changed: EventRegistry, @@ -1944,6 +2112,13 @@ impl NetworkManager { self.inner.events.disconnected.subscribe(handler) } + pub fn native_subscribe_event_queue_running( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.event_queue_running.subscribe(handler) + } + pub fn native_subscribe_generic_streaming_message( &self, handler: EventHandler, @@ -2254,6 +2429,8 @@ impl NetworkManager { #[cfg(test)] mod tests { use super::*; + use libremetaverse_structured_data::{OSD, OSDMap}; + use std::sync::mpsc; #[test] fn manager_workers_and_callback_registries_do_not_retain_the_client() { @@ -2269,4 +2446,82 @@ mod tests { assert!(probe.is_released()); } + + #[test] + fn caps_packet_fallback_uses_the_bounded_udp_dispatch_pipeline() { + let client = GridClient::new().expect("client"); + let manager = NetworkManager::new(client.clone()).expect("network manager"); + manager.inner.ensure_workers().expect("workers"); + let simulator = + Simulator::new(client, "127.0.0.1:14000".parse().unwrap(), 10, None, None).unwrap(); + simulator.attach_manager(&manager.inner, 0); + manager.simulators.push_if_missing(simulator.clone()); + let (sender, receiver) = mpsc::channel(); + let _subscription = manager.subscribe_packet( + PacketType::PacketAck, + Arc::new(move |args| { + sender + .send((args.packet().type_, args.simulator().handle)) + .unwrap(); + }), + false, + ); + let body = OSDMap::new_with_constructor().unwrap(); + body.add_with_string_osd("Packets".to_owned(), OSD::Array(Vec::new())) + .unwrap(); + let packet = Packet::build_packet_with_string_osd_map("PacketAck".to_owned(), body) + .unwrap() + .expect("packet fallback"); + simulator.native_enqueue_caps_packet(packet).unwrap(); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(2)).unwrap(), + (PacketType::PacketAck, 10) + ); + } + + #[test] + fn event_queue_running_and_typed_caps_events_use_manager_registries() { + let client = GridClient::new().expect("client"); + let manager = NetworkManager::new(client.clone()).expect("network manager"); + let simulator = + Simulator::new(client, "127.0.0.1:14001".parse().unwrap(), 11, None, None).unwrap(); + simulator.attach_manager(&manager.inner, 0); + + let (running_sender, running_receiver) = mpsc::channel(); + let _running = manager.native_subscribe_event_queue_running(Arc::new(move |args| { + running_sender.send(args.simulator().handle).unwrap(); + })); + simulator.native_raise_event_queue_running(); + assert_eq!( + running_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + 11 + ); + + let (caps_sender, caps_receiver) = mpsc::channel(); + let _caps = manager.subscribe_caps( + "UpdateAgentLanguage".to_owned(), + CapsEventQueueCallback::from_handler(move |name, message, simulator| { + let typed = (message as &dyn std::any::Any) + .downcast_ref::(); + caps_sender + .send((name.to_owned(), typed.is_some(), simulator.handle)) + .unwrap(); + }), + ); + let body = OSDMap::new_with_dictionary(HashMap::from([ + ("language".to_owned(), OSD::String("en".to_owned())), + ("language_is_public".to_owned(), OSD::Boolean(true)), + ])) + .unwrap(); + let message = crate::message_decoder::decode_event("UpdateAgentLanguage", &body) + .unwrap() + .unwrap(); + simulator.native_dispatch_caps_event("UpdateAgentLanguage", message.as_ref()); + assert_eq!( + caps_receiver.recv_timeout(Duration::from_secs(1)).unwrap(), + ("UpdateAgentLanguage".to_owned(), true, 11) + ); + } } diff --git a/docs/caps-http.md b/docs/caps-http.md index b51e9d0..e708628 100644 --- a/docs/caps-http.md +++ b/docs/caps-http.md @@ -1,4 +1,4 @@ -# Capability HTTP, rate limiting, and downloads +# Capability HTTP, event queues, rate limiting, and downloads `HttpCapsClient`, `CapsRateLimiter`, and `Http::DownloadManager` are native Rust implementations of the corresponding LibreMetaverse classes. They do not call @@ -7,6 +7,27 @@ client using rustls, its category limiter, and their shutdown order. Tests and embedders can inject either a `reqwest::Client` or an executor-neutral `HttpMessageHandler`, plus a `TimeProvider` for deterministic token-bucket time. +## Seed capabilities and EventQueueGet + +`Simulator::SetSeedCaps` owns a native `Caps` instance. It posts the complete +reference capability-name array as LLSD/XML, accepts only HTTP(S) capability +URIs, registers every accepted URI with the category limiter, and starts +`EventQueueGet` when advertised. Seed requests retry transient failures with +bounded exponential backoff, stop permanently on HTTP 404, and are cancelled +and joined when a simulator changes seed capability or disconnects. + +The event queue sends the reference `{ ack, done }` LLSD/XML shape, begins with +acknowledgement zero, and advances the acknowledgement only after a complete, +valid `{ id, events }` response. Responses are limited to 1,024 events, 100,000 +nodes per body, and 8 MiB of binary data per body. Raw events are invoked in +wire order, including unknown names. Known messages use the native Linden LLSD +codecs and the same named/default CAPS callback registry as UDP; otherwise the +generic packet catalog translates the body and feeds the bounded incoming UDP +dispatch queue. Transient HTTP failures use bounded deterministic-jitter +backoff, 404/410/499 close the poller, and graceful shutdown posts one final +`done = true` acknowledgement. Cancellation interrupts an outstanding poll and +joins its owned thread promptly. + ## HTTP policy Only absolute `http` and `https` request URIs are accepted. Response headers @@ -76,6 +97,9 @@ Run the deterministic issue gate with: ```sh cargo test -p libremetaverse --test caps_http +cargo test -p libremetaverse --lib event_queue::tests +cargo test -p libremetaverse --lib caps::tests +cargo test -p libremetaverse --lib message_decoder::tests cargo test -p libremetaverse-compat-tests --test network_semantics queue_download cargo test -p libremetaverse-compat-tests --test network_semantics put_response_with_non_http_location ``` diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index e37567c..a49d340 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -38,15 +38,23 @@ TARGETS = { # implementations. The generated module keeps catalog markers and re-exports # the hand-written type so coverage remains deterministic. NATIVE_TYPES = { + "T:LibreMetaverse.AgentDisplayName": "crate::message_decoder::AgentDisplayName", + "T:LibreMetaverse.CapabilitiesReceivedEventArgs": "crate::caps::CapabilitiesReceivedEventArgs", + "T:LibreMetaverse.Caps": "crate::caps::Caps", "T:LibreMetaverse.CapsRateLimiter": "crate::caps_http::CapsRateLimiter", "T:LibreMetaverse.CapsRateLimiterOptions": "crate::caps_http::CapsRateLimiterOptions", "T:LibreMetaverse.Http.DownloadManager": "crate::download_manager::DownloadManager", "T:LibreMetaverse.Http.DownloadRequest": "crate::download_manager::DownloadRequest", + "T:LibreMetaverse.Http.EventQueueClient": "crate::event_queue::EventQueueClient", + "T:LibreMetaverse.Http.EventQueueClient.ConnectedCallback": "crate::event_queue::EventQueueClientConnectedCallback", + "T:LibreMetaverse.Http.EventQueueClient.EventCallback": "crate::event_queue::EventQueueClientEventCallback", "T:LibreMetaverse.HttpCapsClient": "crate::caps_http::HttpCapsClient", "T:LibreMetaverse.HttpCapsClient.ProgressReport": "crate::caps_http::HttpCapsClientProgressReport", + "T:LibreMetaverse.MediaEntry": "crate::message_decoder::MediaEntry", "T:LibreMetaverse.Caps.EventQueueCallback": "crate::network_manager::CapsEventQueueCallback", "T:LibreMetaverse.CapsEventDictionary": "crate::network_manager::CapsEventDictionary", "T:LibreMetaverse.DisconnectedEventArgs": "crate::network_manager::DisconnectedEventArgs", + "T:LibreMetaverse.EventQueueRunningEventArgs": "crate::network_manager::EventQueueRunningEventArgs", "T:LibreMetaverse.GenericStreamingMessageEventArgs": "crate::network_manager::GenericStreamingMessageEventArgs", "T:LibreMetaverse.NetworkManager.IncomingPacket": "crate::network_manager::NetworkManagerIncomingPacket", "T:LibreMetaverse.NetworkManager.OutgoingPacket": "crate::network_manager::NetworkManagerOutgoingPacket", @@ -57,6 +65,13 @@ NATIVE_TYPES = { "T:LibreMetaverse.SimConnectedEventArgs": "crate::network_manager::SimConnectedEventArgs", "T:LibreMetaverse.SimConnectingEventArgs": "crate::network_manager::SimConnectingEventArgs", "T:LibreMetaverse.SimDisconnectedEventArgs": "crate::network_manager::SimDisconnectedEventArgs", + "T:LibreMetaverse.Messages.Linden.EventMessageBlock": "crate::event_queue::EventMessageBlock", + "T:LibreMetaverse.Messages.Linden.EventQueueAck": "crate::event_queue::EventQueueAck", + "T:LibreMetaverse.Messages.Linden.EventQueueEvent": "crate::event_queue::EventQueueEvent", + "T:LibreMetaverse.Messages.Linden.EventQueueEvent.QueueEvent": "crate::event_queue::EventQueueEventQueueEvent", + "T:LibreMetaverse.Messages.Linden.EventQueueGetMessage": "crate::event_queue::EventQueueGetMessage", + "T:LibreMetaverse.Messages.Linden.AttachmentResourcesMessage": "crate::message_decoder::AttachmentResourcesMessage", + "T:LibreMetaverse.Primitive.PhysicsProperties": "crate::message_decoder::PrimitivePhysicsProperties", "T:LibreMetaverse.ArchetypeParam": "crate::genepool_catalog::ArchetypeParam", "T:LibreMetaverse.AttentionData": "crate::attention_catalog::AttentionData", "T:LibreMetaverse.AttentionSet": "crate::attention_catalog::AttentionSet", @@ -194,6 +209,8 @@ NATIVE_MEMBER_BODIES = { "self.native_set_http_caps_client(value)", "E:LibreMetaverse.NetworkManager.Disconnected": "self.native_subscribe_disconnected(handler)", + "E:LibreMetaverse.NetworkManager.EventQueueRunning": + "self.native_subscribe_event_queue_running(handler)", "E:LibreMetaverse.NetworkManager.GenericStreamingMessage": "self.native_subscribe_generic_streaming_message(handler)", "E:LibreMetaverse.NetworkManager.PacketSent": @@ -228,6 +245,20 @@ NATIVE_MEMBER_BODIES = { "self.native_enqueue_outgoing(packet)", "M:LibreMetaverse.NetworkManager.FindSimulator(System.Net.IPEndPoint)": "Ok(self.native_find_endpoint(end_point))", + "M:LibreMetaverse.Messages.MessageUtils.DecodeEvent(System.String,LibreMetaverse.StructuredData.OSDMap)": + "crate::message_decoder::decode_event(&event_name, &map)", + "M:LibreMetaverse.Messages.MessageUtils.FromDictionaryString(System.Collections.Generic.Dictionary{System.String,System.String})": + "crate::message_decoder::from_dictionary_string(dict)", + "M:LibreMetaverse.Messages.MessageUtils.FromDictionaryUri(System.Collections.Generic.Dictionary{System.Uri,System.Uri})": + "crate::message_decoder::from_dictionary_uri(dict)", + "M:LibreMetaverse.Messages.MessageUtils.FromIP(System.Net.IPAddress)": + "crate::message_decoder::from_ip(address)", + "M:LibreMetaverse.Messages.MessageUtils.ToDictionaryString(LibreMetaverse.StructuredData.OSD)": + "crate::message_decoder::to_dictionary_string(osd)", + "M:LibreMetaverse.Messages.MessageUtils.ToDictionaryUri(LibreMetaverse.StructuredData.OSD)": + "crate::message_decoder::to_dictionary_uri(osd)", + "M:LibreMetaverse.Messages.MessageUtils.ToIP(LibreMetaverse.StructuredData.OSD)": + "crate::message_decoder::to_ip(osd)", "M:LibreMetaverse.NetworkManager.FindSimulator(System.UInt64)": "Ok(self.native_find_handle(handle))", "M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})": @@ -280,6 +311,8 @@ NATIVE_MEMBER_BODIES = { "self.native_equals(obj)", "M:LibreMetaverse.Simulator.GetHashCode": "self.native_hash_code()", + "M:LibreMetaverse.Simulator.IsEventQueueRunning(System.Boolean)": + "self.native_is_event_queue_running(block_until_running)", "M:LibreMetaverse.Simulator.Pause": "self.native_pause()", "M:LibreMetaverse.Simulator.Resume": @@ -580,9 +613,15 @@ COMPOSED_BASE_TYPES = { } NATIVE_MESSAGE_CODEC_TYPES = { + "T:LibreMetaverse.Messages.Linden.AgentDropGroupMessage", + "T:LibreMetaverse.Messages.Linden.AgentDropGroupMessage.AgentData", "T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage", "T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.GroupData", "T:LibreMetaverse.Messages.Linden.AgentGroupDataUpdateMessage.NewGroupData", + "T:LibreMetaverse.Messages.Linden.AgentStateUpdateMessage", + "T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage", + "T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.FolderDataInfo", + "T:LibreMetaverse.Messages.Linden.BulkUpdateInventoryMessage.ItemDataInfo", "T:LibreMetaverse.Messages.Linden.ChatSessionAcceptInvitation", "T:LibreMetaverse.Messages.Linden.ChatSessionRequestBlock", "T:LibreMetaverse.Messages.Linden.ChatSessionRequestMuteUpdate", @@ -593,16 +632,29 @@ NATIVE_MESSAGE_CODEC_TYPES = { "T:LibreMetaverse.Messages.Linden.ChatterBoxSessionStartReplyMessage", "T:LibreMetaverse.Messages.Linden.ChatterboxSessionEventReplyMessage", "T:LibreMetaverse.Messages.Linden.CopyInventoryFromNotecardMessage", + "T:LibreMetaverse.Messages.Linden.CrossedRegionMessage", "T:LibreMetaverse.Messages.Linden.DirLandReplyMessage", "T:LibreMetaverse.Messages.Linden.DirLandReplyMessage.QueryReply", "T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage", "T:LibreMetaverse.Messages.Linden.EnableSimulatorMessage.SimulatorInfoBlock", "T:LibreMetaverse.Messages.Linden.EstablishAgentCommunicationMessage", + "T:LibreMetaverse.Messages.Linden.ForceCloseChatterBoxSessionMessage", + "T:LibreMetaverse.Messages.Linden.GetDisplayNamesMessage", + "T:LibreMetaverse.Messages.Linden.GetObjectCostMessage", + "T:LibreMetaverse.Messages.Linden.GetObjectCostRequest", + "T:LibreMetaverse.Messages.Linden.LandResourcesMessage", + "T:LibreMetaverse.Messages.Linden.LandResourcesRequest", "T:LibreMetaverse.Messages.Linden.LandStatReplyMessage", "T:LibreMetaverse.Messages.Linden.LandStatReplyMessage.ReportDataBlock", "T:LibreMetaverse.Messages.Linden.MapLayerMessageBase", "T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant", "T:LibreMetaverse.Messages.Linden.MapLayerReplyVariant.LayerData", + "T:LibreMetaverse.Messages.Linden.NavMeshStatusUpdateMessage", + "T:LibreMetaverse.Messages.Linden.ObjectMediaNavigateMessage", + "T:LibreMetaverse.Messages.Linden.ObjectMediaRequest", + "T:LibreMetaverse.Messages.Linden.ObjectMediaResponse", + "T:LibreMetaverse.Messages.Linden.ObjectMediaUpdate", + "T:LibreMetaverse.Messages.Linden.ObjectPhysicsPropertiesMessage", "T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage", "T:LibreMetaverse.Messages.Linden.ParcelObjectOwnersReplyMessage.PrimOwner", "T:LibreMetaverse.Messages.Linden.ParcelPropertiesMessage", @@ -612,9 +664,17 @@ NATIVE_MESSAGE_CODEC_TYPES = { "T:LibreMetaverse.Messages.Linden.PlacesReplyMessage.QueryData", "T:LibreMetaverse.Messages.Linden.ProvisionVoiceAccountRequestMessage", "T:LibreMetaverse.Messages.Linden.RemoteParcelRequestReply", + "T:LibreMetaverse.Messages.Linden.RegionInfoMessage", + "T:LibreMetaverse.Messages.Linden.RenderMaterialsMessage", "T:LibreMetaverse.Messages.Linden.RequiredVoiceVersionMessage", "T:LibreMetaverse.Messages.Linden.ScriptRunningReplyMessage", + "T:LibreMetaverse.Messages.Linden.SearchStatRequestReply", + "T:LibreMetaverse.Messages.Linden.SearchStatRequestRequest", "T:LibreMetaverse.Messages.Linden.SendPostcardMessage", + "T:LibreMetaverse.Messages.Linden.SetDisplayNameMessage", + "T:LibreMetaverse.Messages.Linden.SetDisplayNameReplyMessage", + "T:LibreMetaverse.Messages.Linden.DisplayNameUpdateMessage", + "T:LibreMetaverse.Messages.Linden.SimConsoleResponseMessage", "T:LibreMetaverse.Messages.Linden.TeleportFailedMessage", "T:LibreMetaverse.Messages.Linden.TeleportFinishMessage", "T:LibreMetaverse.Messages.Linden.UpdateAgentInformationMessage", @@ -622,6 +682,8 @@ NATIVE_MESSAGE_CODEC_TYPES = { "T:LibreMetaverse.Messages.Linden.UpdateAgentLanguageMessage", "T:LibreMetaverse.Messages.Linden.UpdateScriptAgentRequestMessage", "T:LibreMetaverse.Messages.Linden.UpdateScriptTaskUpdateMessage", + "T:LibreMetaverse.Messages.Linden.UploaderRequestComplete", + "T:LibreMetaverse.Messages.Linden.UploaderRequestUpload", "T:LibreMetaverse.Messages.Linden.ViewerStatsMessage", } OPEN_ENUM_IDS = {"T:LibreMetaverse.BakeType", "T:LibreMetaverse.HoleType"} @@ -1341,7 +1403,22 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str continue mapped_arguments = [mapper.type(argument, None, set(names)) for argument in arguments] trait_path = mapper.resolved[base] + generic_suffix(mapped_arguments) - lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}") + if ( + base == "LibreMetaverse.Interfaces.IMessage" + and item["doc_id"] in NATIVE_MESSAGE_CODEC_TYPES + ): + lines += [ + f"impl{suffix} {trait_path} for {rust_name}{suffix} {{", + " fn deserialize(&mut self, map: libremetaverse_structured_data::OSDMap) -> Result<(), crate::Error> {", + " ::deserialize_generated(self, &map)", + " }", + " fn serialize(&self) -> Result {", + " ::serialize_generated(self)", + " }", + "}", + ] + else: + lines.append(f"impl{suffix} {trait_path} for {rust_name}{suffix} {{}}") return "\n".join(lines)