diff --git a/README.md b/README.md index 6c0c11b..97e96bf 100644 --- a/README.md +++ b/README.md @@ -272,3 +272,13 @@ zerocode expansion, ACK state, and reliable windows have explicit limits; linked cancellation and final drop release every socket task. The executor, wire, backpressure, retry, and security contracts are documented in [`docs/udp-transport.md`](docs/udp-transport.md). +The native network manager now layers ordered packet and CAPS callback +registries, typed RAII subscriptions, synchronized simulator collections, +cancellable connection events, ACK-gated circuit setup, current-simulator and +seed-capability selection, CAPS `EnableSimulator`, UDP `DisableSimulator`, ping +handling, typed region-handshake replies, two-interval keepalive detection, and +deterministic disconnect reasons on that transport. Callback lists are +snapshotted before invocation, and manager/transport workers retain no dropped +client owner. The ownership, +dispatch, lifecycle, and fake-server verification contracts are documented in +[`docs/network-manager.md`](docs/network-manager.md). diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index 25bf381..dc76d41 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: 32 types / 13,459 members; remaining surface is callable failure-only shims | +| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 47 types / 13,615 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-types/src/compat.rs b/crates/libremetaverse-types/src/compat.rs index 53f96a7..792f9b3 100644 --- a/crates/libremetaverse-types/src/compat.rs +++ b/crates/libremetaverse-types/src/compat.rs @@ -466,8 +466,61 @@ impl Drop for CancellationRegistration { } } -#[derive(Debug, Default, Eq, Hash, PartialEq)] -pub struct Subscription; +/// RAII guard for a mapped event subscription. +/// +/// Dropping or explicitly closing the guard invokes the removal callback once. +/// The callback normally owns only a weak reference to the event registry, so +/// an abandoned subscription cannot keep its publisher alive. +#[must_use = "dropping the subscription immediately unregisters the handler"] +pub struct Subscription { + close: Option>, +} + +impl Subscription { + pub fn new(close: impl FnOnce() + Send + Sync + 'static) -> Self { + Self { + close: Some(Box::new(close)), + } + } + + pub fn close(mut self) { + if let Some(close) = self.close.take() { + close(); + } + } + + #[must_use] + pub const fn is_active(&self) -> bool { + self.close.is_some() + } + + pub const fn detached() -> Self { + Self { close: None } + } +} + +impl Default for Subscription { + fn default() -> Self { + Self::detached() + } +} + +impl fmt::Debug for Subscription { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Subscription") + .field("active", &self.is_active()) + .finish() + } +} + +impl Drop for Subscription { + fn drop(&mut self) { + if let Some(close) = self.close.take() { + close(); + } + } +} pub type EventHandler = Arc; diff --git a/crates/libremetaverse/src/client_core.rs b/crates/libremetaverse/src/client_core.rs index 09cf898..507e6f7 100644 --- a/crates/libremetaverse/src/client_core.rs +++ b/crates/libremetaverse/src/client_core.rs @@ -223,12 +223,23 @@ impl ClientRuntime { /// Construction is side-effect free: it creates no runtime, task, socket, or /// HTTP request. Network-facing services are attached explicitly and are owned /// until ordered shutdown or drop. +#[derive(Clone)] pub struct GridClient { pub(crate) settings: Settings, pub(crate) time_provider: TimeProvider, runtime: Arc, } +#[cfg(test)] +pub(crate) struct ClientRetentionProbe(std::sync::Weak); + +#[cfg(test)] +impl ClientRetentionProbe { + pub(crate) fn is_released(&self) -> bool { + self.0.upgrade().is_none() + } +} + impl GridClient { #[must_use] pub fn builder() -> GridClientBuilder { @@ -301,6 +312,11 @@ impl GridClient { pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> { self.runtime.shutdown() } + + #[cfg(test)] + pub(crate) fn retention_probe(&self) -> ClientRetentionProbe { + ClientRetentionProbe(Arc::downgrade(&self.runtime)) + } } impl fmt::Debug for GridClient { @@ -325,7 +341,9 @@ impl fmt::Debug for GridClient { impl Drop for GridClient { fn drop(&mut self) { - let _ = self.runtime.shutdown(); + if Arc::strong_count(&self.runtime) == 1 { + let _ = self.runtime.shutdown(); + } } } @@ -432,6 +450,11 @@ impl Settings { } } + /// Mutable access to the mapped agent settings for native composition. + pub fn agent_settings_mut(&mut self) -> &mut AgentSettings { + &mut self.agent + } + #[must_use] pub fn connection_mut(&mut self) -> &mut ConnectionSettings { &mut self.connection diff --git a/crates/libremetaverse/src/generated.rs b/crates/libremetaverse/src/generated.rs index 1e04b44..773109b 100644 --- a/crates/libremetaverse/src/generated.rs +++ b/crates/libremetaverse/src/generated.rs @@ -6864,51 +6864,11 @@ impl Caps { } /// C# type: `T:LibreMetaverse.Caps.EventQueueCallback`. -pub struct CapsEventQueueCallback; -impl CapsEventQueueCallback { - /// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.#ctor(System.Object,System.IntPtr)`. - pub fn new( - object: libremetaverse_types::compat::Object, - method: isize, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Caps.EventQueueCallback.#ctor(System.Object,System.IntPtr)", - ) - } - /// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.BeginInvoke(System.String,LibreMetaverse.Interfaces.IMessage,LibreMetaverse.Simulator,System.AsyncCallback,System.Object)`. - pub fn begin_invoke( - &self, - caps_key: String, - message: Box, - simulator: libremetaverse::Simulator, - callback: Box, - object: libremetaverse_types::compat::Object, - ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Caps.EventQueueCallback.BeginInvoke(System.String,LibreMetaverse.Interfaces.IMessage,LibreMetaverse.Simulator,System.AsyncCallback,System.Object)", - ) - } - /// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.EndInvoke(System.IAsyncResult)`. - pub fn end_invoke( - &self, - result: Box, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Caps.EventQueueCallback.EndInvoke(System.IAsyncResult)", - ) - } - /// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.Invoke(System.String,LibreMetaverse.Interfaces.IMessage,LibreMetaverse.Simulator)`. - pub fn invoke( - &self, - caps_key: String, - message: Box, - simulator: libremetaverse::Simulator, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Caps.EventQueueCallback.Invoke(System.String,LibreMetaverse.Interfaces.IMessage,LibreMetaverse.Simulator)", - ) - } -} +/// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.#ctor(System.Object,System.IntPtr)`. +/// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.BeginInvoke(System.String,LibreMetaverse.Interfaces.IMessage,LibreMetaverse.Simulator,System.AsyncCallback,System.Object)`. +/// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.EndInvoke(System.IAsyncResult)`. +/// C# member: `M:LibreMetaverse.Caps.EventQueueCallback.Invoke(System.String,LibreMetaverse.Interfaces.IMessage,LibreMetaverse.Simulator)`. +pub use crate::network_manager::CapsEventQueueCallback; /// C# type: `T:LibreMetaverse.CapsCategory`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -6933,38 +6893,11 @@ pub enum CapsCategory { } /// C# type: `T:LibreMetaverse.CapsEventDictionary`. -pub struct CapsEventDictionary { - /// C# member: `F:LibreMetaverse.CapsEventDictionary.Client`. - pub client: libremetaverse::GridClient, -} -impl CapsEventDictionary { - /// C# member: `M:LibreMetaverse.CapsEventDictionary.#ctor(LibreMetaverse.GridClient)`. - pub fn new(client: libremetaverse::GridClient) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapsEventDictionary.#ctor(LibreMetaverse.GridClient)", - ) - } - /// C# member: `M:LibreMetaverse.CapsEventDictionary.RegisterEvent(System.String,LibreMetaverse.Caps.EventQueueCallback)`. - pub fn register_event( - &self, - caps_event: String, - event_handler: libremetaverse::CapsEventQueueCallback, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapsEventDictionary.RegisterEvent(System.String,LibreMetaverse.Caps.EventQueueCallback)", - ) - } - /// C# member: `M:LibreMetaverse.CapsEventDictionary.UnregisterEvent(System.String,LibreMetaverse.Caps.EventQueueCallback)`. - pub fn unregister_event( - &self, - caps_event: String, - event_handler: libremetaverse::CapsEventQueueCallback, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapsEventDictionary.UnregisterEvent(System.String,LibreMetaverse.Caps.EventQueueCallback)", - ) - } -} +/// C# member: `F:LibreMetaverse.CapsEventDictionary.Client`. +/// C# member: `M:LibreMetaverse.CapsEventDictionary.#ctor(LibreMetaverse.GridClient)`. +/// C# member: `M:LibreMetaverse.CapsEventDictionary.RegisterEvent(System.String,LibreMetaverse.Caps.EventQueueCallback)`. +/// C# member: `M:LibreMetaverse.CapsEventDictionary.UnregisterEvent(System.String,LibreMetaverse.Caps.EventQueueCallback)`. +pub use crate::network_manager::CapsEventDictionary; /// C# type: `T:LibreMetaverse.CapsRateLimiter`. pub struct CapsRateLimiter; @@ -8627,26 +8560,10 @@ impl DirectoryManagerSearchTypeFlags { } /// C# type: `T:LibreMetaverse.DisconnectedEventArgs`. -pub struct DisconnectedEventArgs; -impl DisconnectedEventArgs { - /// C# member: `M:LibreMetaverse.DisconnectedEventArgs.#ctor(LibreMetaverse.NetworkManager.DisconnectType,System.String)`. - pub fn new( - reason: libremetaverse::NetworkManagerDisconnectType, - message: String, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.DisconnectedEventArgs.#ctor(LibreMetaverse.NetworkManager.DisconnectType,System.String)", - ) - } - /// C# member: `P:LibreMetaverse.DisconnectedEventArgs.Message`. - pub fn message(&self) -> String { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.DisconnectedEventArgs.Message") - } - /// C# member: `P:LibreMetaverse.DisconnectedEventArgs.Reason`. - pub fn reason(&self) -> libremetaverse::NetworkManagerDisconnectType { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.DisconnectedEventArgs.Reason") - } -} +/// C# member: `M:LibreMetaverse.DisconnectedEventArgs.#ctor(LibreMetaverse.NetworkManager.DisconnectType,System.String)`. +/// C# member: `P:LibreMetaverse.DisconnectedEventArgs.Message`. +/// C# member: `P:LibreMetaverse.DisconnectedEventArgs.Reason`. +pub use crate::network_manager::DisconnectedEventArgs; /// C# type: `T:LibreMetaverse.DisplayNameUpdateEventArgs`. pub struct DisplayNameUpdateEventArgs; @@ -10808,37 +10725,11 @@ pub use crate::genepool_catalog::Genepool; pub use crate::genepool_catalog::GenepoolArchetype; /// C# type: `T:LibreMetaverse.GenericStreamingMessageEventArgs`. -pub struct GenericStreamingMessageEventArgs; -impl GenericStreamingMessageEventArgs { - /// C# member: `M:LibreMetaverse.GenericStreamingMessageEventArgs.#ctor(LibreMetaverse.Simulator,LibreMetaverse.GenericStreamingMethod,System.Byte[])`. - pub fn new( - simulator: libremetaverse::Simulator, - method: libremetaverse::GenericStreamingMethod, - data: Vec, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.GenericStreamingMessageEventArgs.#ctor(LibreMetaverse.Simulator,LibreMetaverse.GenericStreamingMethod,System.Byte[])", - ) - } - /// C# member: `P:LibreMetaverse.GenericStreamingMessageEventArgs.Data`. - pub fn data(&self) -> Vec { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.GenericStreamingMessageEventArgs.Data" - ) - } - /// C# member: `P:LibreMetaverse.GenericStreamingMessageEventArgs.Method`. - pub fn method(&self) -> libremetaverse::GenericStreamingMethod { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.GenericStreamingMessageEventArgs.Method" - ) - } - /// C# member: `P:LibreMetaverse.GenericStreamingMessageEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.GenericStreamingMessageEventArgs.Simulator" - ) - } -} +/// C# member: `M:LibreMetaverse.GenericStreamingMessageEventArgs.#ctor(LibreMetaverse.Simulator,LibreMetaverse.GenericStreamingMethod,System.Byte[])`. +/// C# member: `P:LibreMetaverse.GenericStreamingMessageEventArgs.Data`. +/// C# member: `P:LibreMetaverse.GenericStreamingMessageEventArgs.Method`. +/// C# member: `P:LibreMetaverse.GenericStreamingMessageEventArgs.Simulator`. +pub use crate::network_manager::GenericStreamingMessageEventArgs; /// C# type: `T:LibreMetaverse.GenericStreamingMethod`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -19333,19 +19224,17 @@ impl NavMeshStatusUpdateEventArgs { } /// C# type: `T:LibreMetaverse.NetworkManager`. -pub struct NetworkManager { - /// C# member: `F:LibreMetaverse.NetworkManager.LoginResponseData`. - pub login_response_data: Option, - /// C# member: `F:LibreMetaverse.NetworkManager.Simulators`. - pub simulators: Vec, -} +pub use crate::network_manager::NetworkManager; +/// C# member: `F:LibreMetaverse.NetworkManager.LoginResponseData`. +/// C# member: `F:LibreMetaverse.NetworkManager.Simulators`. impl NetworkManager { /// C# member: `E:LibreMetaverse.NetworkManager.Disconnected`. pub fn subscribe_disconnected( &self, handler: libremetaverse_types::compat::EventHandler, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.NetworkManager.Disconnected") + /* native network implementation */ + self.native_subscribe_disconnected(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.EventQueueRunning`. pub fn subscribe_event_queue_running( @@ -19365,9 +19254,8 @@ impl NetworkManager { libremetaverse::GenericStreamingMessageEventArgs, >, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!( - "E:LibreMetaverse.NetworkManager.GenericStreamingMessage" - ) + /* native network implementation */ + self.native_subscribe_generic_streaming_message(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.LoggedOut`. pub fn subscribe_logged_out( @@ -19388,28 +19276,32 @@ impl NetworkManager { &self, handler: libremetaverse_types::compat::EventHandler, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.NetworkManager.PacketSent") + /* native network implementation */ + self.native_subscribe_packet_sent(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.SimChanged`. pub fn subscribe_sim_changed( &self, handler: libremetaverse_types::compat::EventHandler, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.NetworkManager.SimChanged") + /* native network implementation */ + self.native_subscribe_sim_changed(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.SimConnected`. pub fn subscribe_sim_connected( &self, handler: libremetaverse_types::compat::EventHandler, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.NetworkManager.SimConnected") + /* native network implementation */ + self.native_subscribe_sim_connected(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.SimConnecting`. pub fn subscribe_sim_connecting( &self, handler: libremetaverse_types::compat::EventHandler, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.NetworkManager.SimConnecting") + /* native network implementation */ + self.native_subscribe_sim_connecting(handler) } /// C# member: `E:LibreMetaverse.NetworkManager.SimDisconnected`. pub fn subscribe_sim_disconnected( @@ -19418,13 +19310,13 @@ impl NetworkManager { libremetaverse::SimDisconnectedEventArgs, >, ) -> libremetaverse_types::compat::Subscription { - libremetaverse_types::unimplemented_api!("E:LibreMetaverse.NetworkManager.SimDisconnected") + /* native network implementation */ + self.native_subscribe_sim_disconnected(handler) } /// C# member: `M:LibreMetaverse.NetworkManager.#ctor(LibreMetaverse.GridClient)`. pub fn new(client: libremetaverse::GridClient) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.#ctor(LibreMetaverse.GridClient)", - ) + /* native network implementation */ + crate::network_manager::NetworkManager::native_new(client) } /// C# member: `M:LibreMetaverse.NetworkManager.AbortLogin`. pub fn abort_login(&self) -> Result<(), crate::Error> { @@ -19452,8 +19344,14 @@ impl NetworkManager { set_default: bool, seedcaps: Option, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri)", + /* native network implementation */ + self.native_connect( + std::net::SocketAddr::new(ip, port), + handle, + set_default, + seedcaps, + crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X, + crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y, ) } /// C# member: `M:LibreMetaverse.NetworkManager.Connect(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)`. @@ -19467,8 +19365,14 @@ impl NetworkManager { size_x: u32, size_y: u32, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)", + /* native network implementation */ + self.native_connect( + std::net::SocketAddr::new(ip, port), + handle, + set_default, + seedcaps, + size_x, + size_y, ) } /// C# member: `M:LibreMetaverse.NetworkManager.Connect(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri)`. @@ -19479,8 +19383,14 @@ impl NetworkManager { set_default: bool, seedcaps: Option, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri)", + /* native network implementation */ + self.native_connect( + end_point, + handle, + set_default, + seedcaps, + crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X, + crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y, ) } /// C# member: `M:LibreMetaverse.NetworkManager.Connect(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)`. @@ -19493,9 +19403,8 @@ impl NetworkManager { size_x: u32, size_y: u32, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)", - ) + /* native network implementation */ + self.native_connect(end_point, handle, set_default, seedcaps, size_x, size_y) } /// C# member: `M:LibreMetaverse.NetworkManager.ConnectAsync(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri)`. pub async fn connect_with_ip_address_u_int16_u_int64_boolean_uri_ebec1fff( @@ -19506,9 +19415,16 @@ impl NetworkManager { set_default: bool, seedcaps: Option, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.ConnectAsync(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri)", + /* native network implementation */ + self.native_connect_async( + std::net::SocketAddr::new(ip, port), + handle, + set_default, + seedcaps, + crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X, + crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y, ) + .await } /// C# member: `M:LibreMetaverse.NetworkManager.ConnectAsync(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)`. pub async fn connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32_20e51cc0( @@ -19520,9 +19436,9 @@ impl NetworkManager { size_x: u32, size_y: u32, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.ConnectAsync(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)", - ) + /* native network implementation */ + self.native_connect_async(end_point, handle, set_default, seedcaps, size_x, size_y) + .await } /// C# member: `M:LibreMetaverse.NetworkManager.DefaultLoginParams(System.String,System.String,System.String,System.String,System.String)`. pub fn default_login_params( @@ -19543,45 +19459,40 @@ impl NetworkManager { simulator: libremetaverse::Simulator, send_close_circuit: bool, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.DisconnectSim(LibreMetaverse.Simulator,System.Boolean)", - ) + /* native network implementation */ + self.native_disconnect_sim(simulator, send_close_circuit) } /// C# member: `M:LibreMetaverse.NetworkManager.EnqueueIncoming(LibreMetaverse.NetworkManager.IncomingPacket)`. pub fn enqueue_incoming( &self, packet: libremetaverse::NetworkManagerIncomingPacket, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.EnqueueIncoming(LibreMetaverse.NetworkManager.IncomingPacket)", - ) + /* native network implementation */ + self.native_enqueue_incoming(packet) } /// C# member: `M:LibreMetaverse.NetworkManager.EnqueueOutgoing(LibreMetaverse.NetworkManager.OutgoingPacket)`. pub fn enqueue_outgoing( &self, packet: libremetaverse::NetworkManagerOutgoingPacket, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.EnqueueOutgoing(LibreMetaverse.NetworkManager.OutgoingPacket)", - ) + /* native network implementation */ + self.native_enqueue_outgoing(packet) } /// C# member: `M:LibreMetaverse.NetworkManager.FindSimulator(System.Net.IPEndPoint)`. pub fn find_simulator_with_ip_end_point( &self, end_point: std::net::SocketAddr, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.FindSimulator(System.Net.IPEndPoint)", - ) + /* native network implementation */ + Ok(self.native_find_endpoint(end_point)) } /// C# member: `M:LibreMetaverse.NetworkManager.FindSimulator(System.UInt64)`. pub fn find_simulator_with_u_int64( &self, handle: u64, ) -> Result, crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.FindSimulator(System.UInt64)", - ) + /* native network implementation */ + Ok(self.native_find_handle(handle)) } /// C# member: `M:LibreMetaverse.NetworkManager.GetHashedMAC`. pub fn get_hashed_mac() -> Result { @@ -19733,9 +19644,8 @@ impl NetworkManager { libremetaverse::PacketReceivedEventArgs, >, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})", - ) + /* native network implementation */ + self.native_register_callback(type_, callback, true) } /// C# member: `M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs},System.Boolean)`. pub fn register_callback_with_packet_type_event_handler_boolean( @@ -19746,9 +19656,8 @@ impl NetworkManager { >, is_async: bool, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs},System.Boolean)", - ) + /* native network implementation */ + self.native_register_callback(type_, callback, is_async) } /// C# member: `M:LibreMetaverse.NetworkManager.RegisterEventCallback(System.String,LibreMetaverse.Caps.EventQueueCallback)`. pub fn register_event_callback( @@ -19756,9 +19665,8 @@ impl NetworkManager { caps_event: String, callback: libremetaverse::CapsEventQueueCallback, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.RegisterEventCallback(System.String,LibreMetaverse.Caps.EventQueueCallback)", - ) + /* native network implementation */ + self.native_register_caps(caps_event, callback) } /// C# member: `M:LibreMetaverse.NetworkManager.RegisterLoginResponseCallback(LibreMetaverse.NetworkManager.LoginResponseCallback)`. pub fn register_login_response_callback_with_login_response_callback( @@ -19788,9 +19696,8 @@ impl NetworkManager { &self, packet: libremetaverse::packets::Packet, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.SendPacket(LibreMetaverse.Packets.Packet)", - ) + /* native network implementation */ + self.native_send_packet(packet, None) } /// C# member: `M:LibreMetaverse.NetworkManager.SendPacket(LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)`. pub fn send_packet_with_packet_simulator( @@ -19798,18 +19705,16 @@ impl NetworkManager { packet: libremetaverse::packets::Packet, simulator: Option, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.SendPacket(LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)", - ) + /* native network implementation */ + self.native_send_packet(packet, simulator) } /// C# member: `M:LibreMetaverse.NetworkManager.Shutdown(LibreMetaverse.NetworkManager.DisconnectType)`. pub fn shutdown_with_disconnect_type( &self, type_: libremetaverse::NetworkManagerDisconnectType, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.Shutdown(LibreMetaverse.NetworkManager.DisconnectType)", - ) + /* native network implementation */ + self.native_shutdown(type_, format!("{type_:?}")) } /// C# member: `M:LibreMetaverse.NetworkManager.Shutdown(LibreMetaverse.NetworkManager.DisconnectType,System.String)`. pub fn shutdown_with_disconnect_type_string( @@ -19817,9 +19722,8 @@ impl NetworkManager { type_: libremetaverse::NetworkManagerDisconnectType, message: String, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.Shutdown(LibreMetaverse.NetworkManager.DisconnectType,System.String)", - ) + /* native network implementation */ + self.native_shutdown(type_, message) } /// C# member: `M:LibreMetaverse.NetworkManager.ShutdownAsync(LibreMetaverse.NetworkManager.DisconnectType,System.String)`. pub async fn shutdown_with_disconnect_type_string_7641243c( @@ -19827,9 +19731,8 @@ impl NetworkManager { type_: libremetaverse::NetworkManagerDisconnectType, message: String, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.ShutdownAsync(LibreMetaverse.NetworkManager.DisconnectType,System.String)", - ) + /* native network implementation */ + self.native_shutdown_async(type_, message).await } /// C# member: `M:LibreMetaverse.NetworkManager.StartLocation(System.String,System.Int32,System.Int32,System.Int32)`. pub fn start_location(sim: String, x: i32, y: i32, z: i32) -> Result { @@ -19845,9 +19748,8 @@ impl NetworkManager { libremetaverse::PacketReceivedEventArgs, >, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.UnregisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})", - ) + /* native network implementation */ + self.native_unregister_callback(type_, callback) } /// C# member: `M:LibreMetaverse.NetworkManager.UnregisterEventCallback(System.String,LibreMetaverse.Caps.EventQueueCallback)`. pub fn unregister_event_callback( @@ -19855,9 +19757,8 @@ impl NetworkManager { caps_event: String, callback: libremetaverse::CapsEventQueueCallback, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.UnregisterEventCallback(System.String,LibreMetaverse.Caps.EventQueueCallback)", - ) + /* native network implementation */ + self.native_unregister_caps(caps_event, callback) } /// C# member: `M:LibreMetaverse.NetworkManager.UnregisterLoginResponseCallback(LibreMetaverse.NetworkManager.LoginResponseCallback)`. pub fn unregister_login_response_callback( @@ -19882,31 +19783,38 @@ impl NetworkManager { } /// C# member: `P:LibreMetaverse.NetworkManager.CircuitCode`. pub fn circuit_code(&self) -> u32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.CircuitCode") + /* native network implementation */ + self.native_circuit_code() } /// Setter for C# member: `P:LibreMetaverse.NetworkManager.CircuitCode`. pub fn set_circuit_code(&mut self, value: u32) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.CircuitCode") + /* native network implementation */ + self.native_set_circuit_code(value) } /// C# member: `P:LibreMetaverse.NetworkManager.Connected`. pub fn connected(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.Connected") + /* native network implementation */ + self.native_connected() } /// Setter for C# member: `P:LibreMetaverse.NetworkManager.Connected`. pub fn set_connected(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.Connected") + /* native network implementation */ + self.native_set_connected(value) } /// C# member: `P:LibreMetaverse.NetworkManager.CurrentSim`. pub fn current_sim(&self) -> Option { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.CurrentSim") + /* native network implementation */ + self.native_current_sim() } /// Setter for C# member: `P:LibreMetaverse.NetworkManager.CurrentSim`. pub fn set_current_sim(&mut self, value: Option) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.CurrentSim") + /* native network implementation */ + self.native_set_current_sim(value) } /// C# member: `P:LibreMetaverse.NetworkManager.InboxCount`. pub fn inbox_count(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.InboxCount") + /* native network implementation */ + self.native_inbox_count() } /// C# member: `P:LibreMetaverse.NetworkManager.LoginErrorKey`. pub fn login_error_key(&self) -> String { @@ -19950,7 +19858,8 @@ impl NetworkManager { } /// C# member: `P:LibreMetaverse.NetworkManager.OutboxCount`. pub fn outbox_count(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.NetworkManager.OutboxCount") + /* native network implementation */ + self.native_outbox_count() } } @@ -19969,20 +19878,10 @@ pub enum NetworkManagerDisconnectType { } /// C# type: `T:LibreMetaverse.NetworkManager.IncomingPacket`. -pub struct NetworkManagerIncomingPacket { - /// C# member: `F:LibreMetaverse.NetworkManager.IncomingPacket.Packet`. - pub packet: Option, - /// C# member: `F:LibreMetaverse.NetworkManager.IncomingPacket.Simulator`. - pub simulator: Option, -} -impl NetworkManagerIncomingPacket { - /// C# member: `M:LibreMetaverse.NetworkManager.IncomingPacket.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.IncomingPacket.#ctor", - ) - } -} +/// C# member: `F:LibreMetaverse.NetworkManager.IncomingPacket.Packet`. +/// C# member: `F:LibreMetaverse.NetworkManager.IncomingPacket.Simulator`. +/// C# member: `M:LibreMetaverse.NetworkManager.IncomingPacket.#ctor`. +pub use crate::network_manager::NetworkManagerIncomingPacket; /// C# type: `T:LibreMetaverse.NetworkManager.LoginResponseCallback`. pub struct NetworkManagerLoginResponseCallback; @@ -20036,32 +19935,14 @@ impl NetworkManagerLoginResponseCallback { } /// C# type: `T:LibreMetaverse.NetworkManager.OutgoingPacket`. -pub struct NetworkManagerOutgoingPacket { - /// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.Buffer`. - pub buffer: libremetaverse::UDPPacketBuffer, - /// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.ResendCount`. - pub resend_count: i32, - /// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.SequenceNumber`. - pub sequence_number: u32, - /// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.Simulator`. - pub simulator: libremetaverse::Simulator, - /// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.TickCount`. - pub tick_count: i32, - /// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.Type`. - pub type_: libremetaverse::packets::PacketType, -} -impl NetworkManagerOutgoingPacket { - /// C# member: `M:LibreMetaverse.NetworkManager.OutgoingPacket.#ctor(LibreMetaverse.Simulator,LibreMetaverse.UDPPacketBuffer,LibreMetaverse.Packets.PacketType)`. - pub fn new( - simulator: libremetaverse::Simulator, - buffer: libremetaverse::UDPPacketBuffer, - type_: libremetaverse::packets::PacketType, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.NetworkManager.OutgoingPacket.#ctor(LibreMetaverse.Simulator,LibreMetaverse.UDPPacketBuffer,LibreMetaverse.Packets.PacketType)", - ) - } -} +/// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.Buffer`. +/// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.ResendCount`. +/// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.SequenceNumber`. +/// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.Simulator`. +/// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.TickCount`. +/// C# member: `F:LibreMetaverse.NetworkManager.OutgoingPacket.Type`. +/// C# member: `M:LibreMetaverse.NetworkManager.OutgoingPacket.#ctor(LibreMetaverse.Simulator,LibreMetaverse.UDPPacketBuffer,LibreMetaverse.Packets.PacketType)`. +pub use crate::network_manager::NetworkManagerOutgoingPacket; /// C# type: `T:LibreMetaverse.ObjectAnimationEventArgs`. pub struct ObjectAnimationEventArgs; @@ -21328,54 +21209,12 @@ impl ObservableDictionary { } /// C# type: `T:LibreMetaverse.PacketEventDictionary`. -pub struct PacketEventDictionary { - /// C# member: `F:LibreMetaverse.PacketEventDictionary.Client`. - pub client: libremetaverse::GridClient, -} -impl PacketEventDictionary { - /// C# member: `M:LibreMetaverse.PacketEventDictionary.#ctor(LibreMetaverse.GridClient)`. - pub fn new(client: libremetaverse::GridClient) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.PacketEventDictionary.#ctor(LibreMetaverse.GridClient)", - ) - } - /// C# member: `M:LibreMetaverse.PacketEventDictionary.InvokeRaiseEvent(LibreMetaverse.Packets.PacketType,LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)`. - pub fn invoke_raise_event( - &self, - packet_type: libremetaverse::packets::PacketType, - packet: libremetaverse::packets::Packet, - simulator: libremetaverse::Simulator, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.PacketEventDictionary.InvokeRaiseEvent(LibreMetaverse.Packets.PacketType,LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)", - ) - } - /// C# member: `M:LibreMetaverse.PacketEventDictionary.RegisterEvent(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs},System.Boolean)`. - pub fn register_event( - &self, - packet_type: libremetaverse::packets::PacketType, - event_handler: libremetaverse_types::compat::EventHandler< - libremetaverse::PacketReceivedEventArgs, - >, - is_async: bool, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.PacketEventDictionary.RegisterEvent(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs},System.Boolean)", - ) - } - /// C# member: `M:LibreMetaverse.PacketEventDictionary.UnregisterEvent(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})`. - pub fn unregister_event( - &self, - packet_type: libremetaverse::packets::PacketType, - event_handler: libremetaverse_types::compat::EventHandler< - libremetaverse::PacketReceivedEventArgs, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.PacketEventDictionary.UnregisterEvent(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})", - ) - } -} +/// C# member: `F:LibreMetaverse.PacketEventDictionary.Client`. +/// C# member: `M:LibreMetaverse.PacketEventDictionary.#ctor(LibreMetaverse.GridClient)`. +/// C# member: `M:LibreMetaverse.PacketEventDictionary.InvokeRaiseEvent(LibreMetaverse.Packets.PacketType,LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)`. +/// C# member: `M:LibreMetaverse.PacketEventDictionary.RegisterEvent(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs},System.Boolean)`. +/// C# member: `M:LibreMetaverse.PacketEventDictionary.UnregisterEvent(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})`. +pub use crate::network_manager::PacketEventDictionary; /// C# type: `T:LibreMetaverse.PacketFrequency`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -21390,55 +21229,17 @@ pub enum PacketFrequency { } /// C# type: `T:LibreMetaverse.PacketReceivedEventArgs`. -pub struct PacketReceivedEventArgs; -impl PacketReceivedEventArgs { - /// C# member: `M:LibreMetaverse.PacketReceivedEventArgs.#ctor(LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)`. - pub fn new( - packet: libremetaverse::packets::Packet, - simulator: libremetaverse::Simulator, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.PacketReceivedEventArgs.#ctor(LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.PacketReceivedEventArgs.Packet`. - pub fn packet(&self) -> libremetaverse::packets::Packet { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.PacketReceivedEventArgs.Packet") - } - /// C# member: `P:LibreMetaverse.PacketReceivedEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.PacketReceivedEventArgs.Simulator" - ) - } -} +/// C# member: `M:LibreMetaverse.PacketReceivedEventArgs.#ctor(LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.PacketReceivedEventArgs.Packet`. +/// C# member: `P:LibreMetaverse.PacketReceivedEventArgs.Simulator`. +pub use crate::network_manager::PacketReceivedEventArgs; /// C# type: `T:LibreMetaverse.PacketSentEventArgs`. -pub struct PacketSentEventArgs; -impl PacketSentEventArgs { - /// C# member: `M:LibreMetaverse.PacketSentEventArgs.#ctor(System.Byte[],System.Int32,LibreMetaverse.Simulator)`. - pub fn new( - data: Vec, - bytes_sent: i32, - simulator: libremetaverse::Simulator, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.PacketSentEventArgs.#ctor(System.Byte[],System.Int32,LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.PacketSentEventArgs.Data`. - pub fn data(&self) -> Vec { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.PacketSentEventArgs.Data") - } - /// C# member: `P:LibreMetaverse.PacketSentEventArgs.SentBytes`. - pub fn sent_bytes(&self) -> i32 { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.PacketSentEventArgs.SentBytes") - } - /// C# member: `P:LibreMetaverse.PacketSentEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.PacketSentEventArgs.Simulator") - } -} +/// C# member: `M:LibreMetaverse.PacketSentEventArgs.#ctor(System.Byte[],System.Int32,LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.PacketSentEventArgs.Data`. +/// C# member: `P:LibreMetaverse.PacketSentEventArgs.SentBytes`. +/// C# member: `P:LibreMetaverse.PacketSentEventArgs.Simulator`. +pub use crate::network_manager::PacketSentEventArgs; /// C# type: `T:LibreMetaverse.PacketSettings`. #[derive(Clone, Debug, Eq, PartialEq)] @@ -25516,94 +25317,27 @@ impl SimAccess { } /// C# type: `T:LibreMetaverse.SimChangedEventArgs`. -pub struct SimChangedEventArgs; -impl SimChangedEventArgs { - /// C# member: `M:LibreMetaverse.SimChangedEventArgs.#ctor(LibreMetaverse.Simulator)`. - pub fn new( - previous_simulator: Option, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.SimChangedEventArgs.#ctor(LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.SimChangedEventArgs.PreviousSimulator`. - pub fn previous_simulator(&self) -> Option { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.SimChangedEventArgs.PreviousSimulator" - ) - } -} +/// C# member: `M:LibreMetaverse.SimChangedEventArgs.#ctor(LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.SimChangedEventArgs.PreviousSimulator`. +pub use crate::network_manager::SimChangedEventArgs; /// C# type: `T:LibreMetaverse.SimConnectedEventArgs`. -pub struct SimConnectedEventArgs; -impl SimConnectedEventArgs { - /// C# member: `M:LibreMetaverse.SimConnectedEventArgs.#ctor(LibreMetaverse.Simulator)`. - pub fn new(simulator: libremetaverse::Simulator) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.SimConnectedEventArgs.#ctor(LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.SimConnectedEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.SimConnectedEventArgs.Simulator") - } -} +/// C# member: `M:LibreMetaverse.SimConnectedEventArgs.#ctor(LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.SimConnectedEventArgs.Simulator`. +pub use crate::network_manager::SimConnectedEventArgs; /// C# type: `T:LibreMetaverse.SimConnectingEventArgs`. -pub struct SimConnectingEventArgs; -impl SimConnectingEventArgs { - /// C# member: `M:LibreMetaverse.SimConnectingEventArgs.#ctor(LibreMetaverse.Simulator)`. - pub fn new(simulator: libremetaverse::Simulator) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.SimConnectingEventArgs.#ctor(LibreMetaverse.Simulator)", - ) - } - /// C# member: `P:LibreMetaverse.SimConnectingEventArgs.Cancel`. - pub fn cancel(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.SimConnectingEventArgs.Cancel") - } - /// Setter for C# member: `P:LibreMetaverse.SimConnectingEventArgs.Cancel`. - pub fn set_cancel(&mut self, value: bool) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.SimConnectingEventArgs.Cancel") - } - /// C# member: `P:LibreMetaverse.SimConnectingEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.SimConnectingEventArgs.Simulator" - ) - } -} +/// C# member: `M:LibreMetaverse.SimConnectingEventArgs.#ctor(LibreMetaverse.Simulator)`. +/// C# member: `P:LibreMetaverse.SimConnectingEventArgs.Cancel`. +/// C# member: `P:LibreMetaverse.SimConnectingEventArgs.Simulator`. +pub use crate::network_manager::SimConnectingEventArgs; /// C# type: `T:LibreMetaverse.SimDisconnectedEventArgs`. -pub struct SimDisconnectedEventArgs; -impl SimDisconnectedEventArgs { - /// C# member: `M:LibreMetaverse.SimDisconnectedEventArgs.#ctor(LibreMetaverse.Simulator,LibreMetaverse.NetworkManager.DisconnectType,System.Boolean)`. - pub fn new( - simulator: libremetaverse::Simulator, - reason: libremetaverse::NetworkManagerDisconnectType, - was_connected: bool, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.SimDisconnectedEventArgs.#ctor(LibreMetaverse.Simulator,LibreMetaverse.NetworkManager.DisconnectType,System.Boolean)", - ) - } - /// C# member: `P:LibreMetaverse.SimDisconnectedEventArgs.Reason`. - pub fn reason(&self) -> libremetaverse::NetworkManagerDisconnectType { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.SimDisconnectedEventArgs.Reason") - } - /// C# member: `P:LibreMetaverse.SimDisconnectedEventArgs.Simulator`. - pub fn simulator(&self) -> libremetaverse::Simulator { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.SimDisconnectedEventArgs.Simulator" - ) - } - /// C# member: `P:LibreMetaverse.SimDisconnectedEventArgs.WasConnected`. - pub fn was_connected(&self) -> bool { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.SimDisconnectedEventArgs.WasConnected" - ) - } -} +/// C# member: `M:LibreMetaverse.SimDisconnectedEventArgs.#ctor(LibreMetaverse.Simulator,LibreMetaverse.NetworkManager.DisconnectType,System.Boolean)`. +/// C# member: `P:LibreMetaverse.SimDisconnectedEventArgs.Reason`. +/// C# member: `P:LibreMetaverse.SimDisconnectedEventArgs.Simulator`. +/// C# member: `P:LibreMetaverse.SimDisconnectedEventArgs.WasConnected`. +pub use crate::network_manager::SimDisconnectedEventArgs; /// C# type: `T:LibreMetaverse.SimParcelsDownloadedEventArgs`. pub struct SimParcelsDownloadedEventArgs; @@ -25641,108 +25375,56 @@ impl SimParcelsDownloadedEventArgs { } /// C# type: `T:LibreMetaverse.Simulator`. -pub struct Simulator { - /// C# member: `F:LibreMetaverse.Simulator.Access`. - pub access: libremetaverse::SimAccess, - /// C# member: `F:LibreMetaverse.Simulator.AgentMovementComplete`. - pub agent_movement_complete: bool, - /// C# member: `F:LibreMetaverse.Simulator.BillableFactor`. - pub billable_factor: f32, - /// C# member: `F:LibreMetaverse.Simulator.CPUClass`. - pub cpu_class: i32, - /// C# member: `F:LibreMetaverse.Simulator.CPURatio`. - pub cpu_ratio: i32, - /// C# member: `F:LibreMetaverse.Simulator.Caps`. - pub caps: Option>, - /// C# member: `F:LibreMetaverse.Simulator.Client`. - pub client: libremetaverse::GridClient, - /// C# member: `F:LibreMetaverse.Simulator.ColoLocation`. - pub colo_location: String, - /// C# member: `F:LibreMetaverse.Simulator.DataPool`. - pub data_pool: Option, - /// C# member: `F:LibreMetaverse.Simulator.Features`. - pub features: libremetaverse::SimulatorFeatures, - /// C# member: `F:LibreMetaverse.Simulator.Flags`. - pub flags: libremetaverse::RegionFlags, - /// C# member: `F:LibreMetaverse.Simulator.GlobalToLocalID`. - pub global_to_local_id: - std::sync::RwLock>, - /// C# member: `F:LibreMetaverse.Simulator.Handle`. - pub handle: u64, - /// C# member: `F:LibreMetaverse.Simulator.ID`. - pub id: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.IsEstateManager`. - pub is_estate_manager: bool, - /// C# member: `F:LibreMetaverse.Simulator.Name`. - pub name: String, - /// C# member: `F:LibreMetaverse.Simulator.ObjectsAvatars`. - pub objects_avatars: std::sync::RwLock>, - /// C# member: `F:LibreMetaverse.Simulator.ObjectsPrimitives`. - pub objects_primitives: - std::sync::RwLock>, - /// C# member: `F:LibreMetaverse.Simulator.ParcelOverlay`. - pub parcel_overlay: Vec, - /// C# member: `F:LibreMetaverse.Simulator.ParcelOverlaysReceived`. - pub parcel_overlays_received: i32, - /// C# member: `F:LibreMetaverse.Simulator.ProductName`. - pub product_name: String, - /// C# member: `F:LibreMetaverse.Simulator.ProductSku`. - pub product_sku: String, - /// C# member: `F:LibreMetaverse.Simulator.Protocols`. - pub protocols: libremetaverse::RegionProtocols, - /// C# member: `F:LibreMetaverse.Simulator.RegionID`. - pub region_id: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.Sequence`. - pub sequence: i32, - /// C# member: `F:LibreMetaverse.Simulator.SimOwner`. - pub sim_owner: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.SimVersion`. - pub sim_version: String, - /// C# member: `F:LibreMetaverse.Simulator.SizeX`. - pub size_x: u32, - /// C# member: `F:LibreMetaverse.Simulator.SizeY`. - pub size_y: u32, - /// C# member: `F:LibreMetaverse.Simulator.Stats`. - pub stats: libremetaverse::SimulatorSimStats, - /// C# member: `F:LibreMetaverse.Simulator.Terrain`. - pub terrain: Vec, - /// C# member: `F:LibreMetaverse.Simulator.TerrainBase0`. - pub terrain_base0: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainBase1`. - pub terrain_base1: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainBase2`. - pub terrain_base2: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainBase3`. - pub terrain_base3: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainDetail0`. - pub terrain_detail0: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainDetail1`. - pub terrain_detail1: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainDetail2`. - pub terrain_detail2: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainDetail3`. - pub terrain_detail3: libremetaverse_types::UUID, - /// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange00`. - pub terrain_height_range00: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange01`. - pub terrain_height_range01: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange10`. - pub terrain_height_range10: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange11`. - pub terrain_height_range11: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight00`. - pub terrain_start_height00: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight01`. - pub terrain_start_height01: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight10`. - pub terrain_start_height10: f32, - /// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight11`. - pub terrain_start_height11: f32, - /// C# member: `F:LibreMetaverse.Simulator.WaterHeight`. - pub water_height: f32, - /// C# member: `F:LibreMetaverse.Simulator.WindSpeeds`. - pub wind_speeds: Option>, -} +pub use crate::network_manager::Simulator; +/// C# member: `F:LibreMetaverse.Simulator.Access`. +/// C# member: `F:LibreMetaverse.Simulator.AgentMovementComplete`. +/// C# member: `F:LibreMetaverse.Simulator.BillableFactor`. +/// C# member: `F:LibreMetaverse.Simulator.CPUClass`. +/// C# member: `F:LibreMetaverse.Simulator.CPURatio`. +/// C# member: `F:LibreMetaverse.Simulator.Caps`. +/// C# member: `F:LibreMetaverse.Simulator.Client`. +/// C# member: `F:LibreMetaverse.Simulator.ColoLocation`. +/// C# member: `F:LibreMetaverse.Simulator.DataPool`. +/// C# member: `F:LibreMetaverse.Simulator.Features`. +/// C# member: `F:LibreMetaverse.Simulator.Flags`. +/// C# member: `F:LibreMetaverse.Simulator.GlobalToLocalID`. +/// C# member: `F:LibreMetaverse.Simulator.Handle`. +/// C# member: `F:LibreMetaverse.Simulator.ID`. +/// C# member: `F:LibreMetaverse.Simulator.IsEstateManager`. +/// C# member: `F:LibreMetaverse.Simulator.Name`. +/// C# member: `F:LibreMetaverse.Simulator.ObjectsAvatars`. +/// C# member: `F:LibreMetaverse.Simulator.ObjectsPrimitives`. +/// C# member: `F:LibreMetaverse.Simulator.ParcelOverlay`. +/// C# member: `F:LibreMetaverse.Simulator.ParcelOverlaysReceived`. +/// C# member: `F:LibreMetaverse.Simulator.ProductName`. +/// C# member: `F:LibreMetaverse.Simulator.ProductSku`. +/// C# member: `F:LibreMetaverse.Simulator.Protocols`. +/// C# member: `F:LibreMetaverse.Simulator.RegionID`. +/// C# member: `F:LibreMetaverse.Simulator.Sequence`. +/// C# member: `F:LibreMetaverse.Simulator.SimOwner`. +/// C# member: `F:LibreMetaverse.Simulator.SimVersion`. +/// C# member: `F:LibreMetaverse.Simulator.SizeX`. +/// C# member: `F:LibreMetaverse.Simulator.SizeY`. +/// C# member: `F:LibreMetaverse.Simulator.Stats`. +/// C# member: `F:LibreMetaverse.Simulator.Terrain`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainBase0`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainBase1`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainBase2`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainBase3`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainDetail0`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainDetail1`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainDetail2`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainDetail3`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange00`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange01`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange10`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainHeightRange11`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight00`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight01`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight10`. +/// C# member: `F:LibreMetaverse.Simulator.TerrainStartHeight11`. +/// C# member: `F:LibreMetaverse.Simulator.WaterHeight`. +/// C# member: `F:LibreMetaverse.Simulator.WindSpeeds`. impl Simulator { /// C# member: `F:LibreMetaverse.Simulator.DefaultRegionSizeX`. pub const DEFAULT_REGION_SIZE_X: u32 = 256; @@ -25756,40 +25438,41 @@ impl Simulator { size_x: Option, size_y: Option, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.#ctor(LibreMetaverse.GridClient,System.Net.IPEndPoint,System.UInt64,System.UInt32,System.UInt32)", - ) + /* native network implementation */ + crate::network_manager::Simulator::native_new(client, address, handle, size_x, size_y) } /// C# member: `M:LibreMetaverse.Simulator.Connect(System.Boolean)`. pub fn connect_with_boolean(&self, move_to_sim: bool) -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.Connect(System.Boolean)") + /* native network implementation */ + self.native_connect(move_to_sim) } /// C# member: `M:LibreMetaverse.Simulator.ConnectAsync(System.Boolean)`. pub async fn connect_with_boolean_8eb5bd05( &self, move_to_sim: bool, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.ConnectAsync(System.Boolean)", - ) + /* native network implementation */ + self.native_connect_async(move_to_sim).await } /// C# member: `M:LibreMetaverse.Simulator.Disconnect(System.Boolean)`. pub fn disconnect(&self, send_close_circuit: bool) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.Disconnect(System.Boolean)", - ) + /* native network implementation */ + self.native_disconnect(send_close_circuit) } /// C# member: `M:LibreMetaverse.Simulator.Dispose`. pub fn dispose(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.Dispose") + /* native network implementation */ + self.native_dispose() } /// C# member: `M:LibreMetaverse.Simulator.Equals(System.Object)`. pub fn equals(&self, obj: Option) -> bool { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.Simulator.Equals(System.Object)") + /* native network implementation */ + self.native_equals(obj) } /// C# member: `M:LibreMetaverse.Simulator.GetHashCode`. pub fn get_hash_code(&self) -> i32 { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.Simulator.GetHashCode") + /* native network implementation */ + self.native_hash_code() } /// C# member: `M:LibreMetaverse.Simulator.IsEventQueueRunning(System.Boolean)`. pub fn is_event_queue_running( @@ -25806,17 +25489,18 @@ impl Simulator { } /// C# member: `M:LibreMetaverse.Simulator.Pause`. pub fn pause(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.Pause") + /* native network implementation */ + self.native_pause() } /// C# member: `M:LibreMetaverse.Simulator.Resume`. pub fn resume(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.Resume") + /* native network implementation */ + self.native_resume() } /// C# member: `M:LibreMetaverse.Simulator.SendPacket(LibreMetaverse.Packets.Packet)`. pub fn send_packet(&self, packet: libremetaverse::packets::Packet) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SendPacket(LibreMetaverse.Packets.Packet)", - ) + /* native network implementation */ + self.native_send_packet(packet) } /// C# member: `M:LibreMetaverse.Simulator.SendPacketData(System.Byte[],System.Int32,LibreMetaverse.Packets.PacketType,System.Boolean)`. pub fn send_packet_data( @@ -25826,13 +25510,13 @@ impl Simulator { type_: libremetaverse::packets::PacketType, do_zerocode: bool, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SendPacketData(System.Byte[],System.Int32,LibreMetaverse.Packets.PacketType,System.Boolean)", - ) + /* native network implementation */ + self.native_send_packet_data(data, data_length, type_, do_zerocode) } /// C# member: `M:LibreMetaverse.Simulator.SendPing`. pub fn send_ping(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Simulator.SendPing") + /* native network implementation */ + self.native_send_ping() } /// C# member: `M:LibreMetaverse.Simulator.SetSeedCaps(System.Uri,System.Boolean)`. pub fn set_seed_caps( @@ -25840,9 +25524,8 @@ impl Simulator { seedcaps: Option, changed_sim: Option, ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.SetSeedCaps(System.Uri,System.Boolean)", - ) + /* native network implementation */ + self.native_set_seed_caps(seedcaps, changed_sim) } /// C# member: `M:LibreMetaverse.Simulator.TerrainHeightAtPoint(System.Int32,System.Int32,System.Single@)`. pub fn terrain_height_at_point( @@ -25857,52 +25540,52 @@ impl Simulator { } /// C# member: `M:LibreMetaverse.Simulator.ToString`. pub fn to_string(&self) -> String { - libremetaverse_types::unimplemented_api!("M:LibreMetaverse.Simulator.ToString") + /* native network implementation */ + self.native_to_string() } /// C# member: `M:LibreMetaverse.Simulator.UseCircuitCode(System.Boolean)`. pub fn use_circuit_code_with_boolean(&self, wait_for_ack: bool) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.UseCircuitCode(System.Boolean)", - ) + /* native network implementation */ + self.native_use_circuit_code(wait_for_ack) } /// C# member: `M:LibreMetaverse.Simulator.UseCircuitCodeAsync(System.Boolean)`. pub async fn use_circuit_code_with_boolean_6c41c00a( &self, wait_for_ack: bool, ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Simulator.UseCircuitCodeAsync(System.Boolean)", - ) + /* native network implementation */ + self.native_use_circuit_code_async(wait_for_ack).await } /// C# member: `M:LibreMetaverse.Simulator.op_Equality(LibreMetaverse.Simulator,LibreMetaverse.Simulator)`. pub fn eq( lhs: Option, rhs: Option, ) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.Simulator.op_Equality(LibreMetaverse.Simulator,LibreMetaverse.Simulator)" - ) + /* native network implementation */ + lhs == rhs } /// C# member: `M:LibreMetaverse.Simulator.op_Inequality(LibreMetaverse.Simulator,LibreMetaverse.Simulator)`. pub fn ne( lhs: Option, rhs: Option, ) -> bool { - libremetaverse_types::unimplemented_api!( - "M:LibreMetaverse.Simulator.op_Inequality(LibreMetaverse.Simulator,LibreMetaverse.Simulator)" - ) + /* native network implementation */ + lhs != rhs } /// C# member: `P:LibreMetaverse.Simulator.Connected`. pub fn connected(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.Connected") + /* native network implementation */ + self.native_is_connected() } /// C# member: `P:LibreMetaverse.Simulator.HandshakeComplete`. pub fn handshake_complete(&self) -> bool { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.HandshakeComplete") + /* native network implementation */ + self.native_handshake_complete() } /// C# member: `P:LibreMetaverse.Simulator.IPEndPoint`. pub fn ip_end_point(&self) -> std::net::SocketAddr { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.Simulator.IPEndPoint") + /* native network implementation */ + self.native_ip_end_point() } /// C# member: `P:LibreMetaverse.Simulator.ParcelMap`. pub fn parcel_map(&self) -> Vec { @@ -64013,7 +63696,7 @@ pub mod interfaces { } /// C# type: `T:LibreMetaverse.Interfaces.IMessage`. - pub trait IMessage { + pub trait IMessage: std::any::Any { /// C# member: `M:LibreMetaverse.Interfaces.IMessage.Deserialize(LibreMetaverse.StructuredData.OSDMap)`. fn deserialize( &mut self, diff --git a/crates/libremetaverse/src/lib.rs b/crates/libremetaverse/src/lib.rs index 3e04749..f064040 100644 --- a/crates/libremetaverse/src/lib.rs +++ b/crates/libremetaverse/src/lib.rs @@ -13,6 +13,7 @@ mod generated; mod genepool_catalog; mod j2k; mod message_codec; +mod network_manager; #[rustfmt::skip] pub mod packet_catalog; mod packet_wire; @@ -205,6 +206,7 @@ pub use libremetaverse_imaging as imaging_abstractions; pub use libremetaverse_structured_data as structured_data; pub use libremetaverse_types as types; pub use libremetaverse_types::Error; +pub use network_manager::SimulatorCollection; pub use udp_transport::{ AgentThrottleSender, UdpPacketHandler, UdpThrottleCategory, UdpTransportConfig, UdpTransportError, UdpTransportStats, diff --git a/crates/libremetaverse/src/network_manager.rs b/crates/libremetaverse/src/network_manager.rs new file mode 100644 index 0000000..9bd8313 --- /dev/null +++ b/crates/libremetaverse/src/network_manager.rs @@ -0,0 +1,2272 @@ +//! Native network-manager, packet-dispatch, and simulator lifecycle runtime. +//! +//! The C# implementation uses reference-identity objects protected by several +//! locks and channel processors. Rust models those objects as cheap `Arc` +//! handles. Callback lists are snapshotted while locked and always invoked +//! after the lock is released. + +#![allow(clippy::missing_errors_doc)] // Result shapes are fixed by the compatibility map. +#![allow(clippy::needless_pass_by_value)] // Owned mapped parameters preserve the public API. +#![allow(clippy::too_many_lines)] // Packet dispatch keeps the complete protocol switch together. +#![allow(clippy::type_complexity)] // Delegate signatures are fixed by the compatibility map. + +use crate::interfaces::IMessage; +use crate::packets::{ + AgentPausePacket, AgentResumePacket, CloseCircuitPacket, CompletePingCheckPacket, + GenericStreamingMessagePacket, KickUserPacket, Packet, PacketType, RegionHandshakePacket, + RegionHandshakeReplyPacket, StartPingCheckPacket, UseCircuitCodePacket, +}; +use crate::udp_transport::{UDPBase, UDPPacketBuffer, UdpPacketHandler, UdpTransportConfig}; +use crate::{ + Avatar, Caps, Error, GenericStreamingMethod, GridClient, Helpers, LoginResponseData, Primitive, + RegionFlags, RegionProtocols, SimAccess, SimulatorDataPool, SimulatorFeatures, + SimulatorSimStats, TerrainPatch, +}; +use libremetaverse_types::compat::{EventHandler, Object, Subscription, Uri}; +use libremetaverse_types::{UUID, Vector2}; +use std::collections::HashMap; +use std::fmt; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering}; +use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, sync_channel}; +use std::sync::{Arc, Condvar, Mutex, RwLock, Weak}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +impl Clone for crate::packets::Header { + fn clone(&self) -> Self { + crate::packet_wire::clone_header(self) + } +} + +impl Clone for Packet { + fn clone(&self) -> Self { + Self { + has_variable_blocks: self.has_variable_blocks, + header: self.header.clone(), + type_: self.type_, + } + } +} + +impl Clone for Caps { + fn clone(&self) -> Self { + Self { + simulator: self.simulator.clone(), + } + } +} + +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) +} + +fn invoke_safely(handler: &EventHandler, value: T) { + let _ = catch_unwind(AssertUnwindSafe(|| handler(value))); +} + +fn wait_for_cancellation( + token: &libremetaverse_types::compat::CancellationToken, + duration: Duration, +) -> bool { + let started = std::time::Instant::now(); + while !token.is_cancellation_requested() { + let remaining = duration.saturating_sub(started.elapsed()); + if remaining.is_zero() { + return false; + } + thread::sleep(remaining.min(Duration::from_millis(50))); + } + true +} + +fn positive_millisecond_duration(value: i32) -> Duration { + Duration::from_millis(u64::try_from(value.max(1)).unwrap_or(1)) +} + +async fn run_blocking_compat( + name: &str, + operation: impl FnOnce() -> T + Send + 'static, +) -> Result { + let (sender, receiver) = tokio::sync::oneshot::channel(); + thread::Builder::new() + .name(name.to_owned()) + .spawn(move || { + let _ = sender.send(operation()); + }) + .map_err(|_| Error::InvalidOperation)?; + receiver.await.map_err(|_| Error::Cancelled) +} + +struct EventRegistryState { + next_id: AtomicU64, + handlers: Mutex)>>, +} + +struct EventRegistry { + state: Arc>, +} + +impl Default for EventRegistry { + fn default() -> Self { + Self { + state: Arc::new(EventRegistryState { + next_id: AtomicU64::new(1), + handlers: Mutex::new(Vec::new()), + }), + } + } +} + +impl EventRegistry { + fn subscribe(&self, handler: EventHandler) -> Subscription { + let id = self.state.next_id.fetch_add(1, Ordering::Relaxed); + mutex(&self.state.handlers).push((id, handler)); + let state = Arc::downgrade(&self.state); + Subscription::new(move || { + let Some(state) = state.upgrade() else { + return; + }; + mutex(&state.handlers).retain(|(candidate, _)| *candidate != id); + }) + } + + fn emit_with(&self, mut value: impl FnMut() -> T) { + let handlers: Vec<_> = mutex(&self.state.handlers) + .iter() + .map(|(_, handler)| Arc::clone(handler)) + .collect(); + for handler in handlers { + invoke_safely(&handler, value()); + } + } +} + +/// C# `PacketReceivedEventArgs` with shared simulator identity. +#[derive(Clone)] +pub struct PacketReceivedEventArgs { + packet: Packet, + simulator: Simulator, +} + +impl PacketReceivedEventArgs { + pub fn new(packet: Packet, simulator: Simulator) -> Result { + Ok(Self { packet, simulator }) + } + + #[must_use] + pub fn packet(&self) -> Packet { + self.packet.clone() + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + +#[derive(Clone)] +pub struct PacketSentEventArgs { + data: Vec, + sent_bytes: i32, + simulator: Simulator, +} + +impl PacketSentEventArgs { + pub fn new(data: Vec, sent_bytes: i32, simulator: Simulator) -> Result { + let sent = usize::try_from(sent_bytes).map_err(|_| Error::Argument)?; + if sent > data.len() { + return Err(Error::Argument); + } + Ok(Self { + data, + sent_bytes, + simulator, + }) + } + + #[must_use] + pub fn data(&self) -> Vec { + self.data.clone() + } + + #[must_use] + pub const fn sent_bytes(&self) -> i32 { + self.sent_bytes + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + +#[derive(Clone)] +pub struct SimChangedEventArgs { + previous_simulator: Option, +} + +impl SimChangedEventArgs { + pub fn new(previous_simulator: Option) -> Result { + Ok(Self { previous_simulator }) + } + + #[must_use] + pub fn previous_simulator(&self) -> Option { + self.previous_simulator.clone() + } +} + +#[derive(Clone)] +pub struct SimConnectedEventArgs { + simulator: Simulator, +} + +impl SimConnectedEventArgs { + pub fn new(simulator: Simulator) -> Result { + Ok(Self { simulator }) + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + +#[derive(Clone)] +pub struct SimConnectingEventArgs { + simulator: Simulator, + cancel: Arc, +} + +impl SimConnectingEventArgs { + pub fn new(simulator: Simulator) -> Result { + Ok(Self { + simulator, + cancel: Arc::new(AtomicBool::new(false)), + }) + } + + #[must_use] + pub fn cancel(&self) -> bool { + self.cancel.load(Ordering::Acquire) + } + + pub fn set_cancel(&mut self, value: bool) { + self.cancel.store(value, Ordering::Release); + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + +#[derive(Clone)] +pub struct SimDisconnectedEventArgs { + simulator: Simulator, + reason: crate::NetworkManagerDisconnectType, + was_connected: bool, +} + +impl SimDisconnectedEventArgs { + pub fn new( + simulator: Simulator, + reason: crate::NetworkManagerDisconnectType, + was_connected: bool, + ) -> Result { + Ok(Self { + simulator, + reason, + was_connected, + }) + } + + #[must_use] + pub const fn reason(&self) -> crate::NetworkManagerDisconnectType { + self.reason + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } + + #[must_use] + pub const fn was_connected(&self) -> bool { + self.was_connected + } +} + +#[derive(Clone)] +pub struct DisconnectedEventArgs { + reason: crate::NetworkManagerDisconnectType, + message: String, +} + +impl DisconnectedEventArgs { + pub fn new( + reason: crate::NetworkManagerDisconnectType, + message: String, + ) -> Result { + Ok(Self { reason, message }) + } + + #[must_use] + pub fn message(&self) -> String { + self.message.clone() + } + + #[must_use] + pub const fn reason(&self) -> crate::NetworkManagerDisconnectType { + self.reason + } +} + +#[derive(Clone)] +pub struct GenericStreamingMessageEventArgs { + simulator: Simulator, + method: GenericStreamingMethod, + data: Vec, +} + +impl GenericStreamingMessageEventArgs { + pub fn new( + simulator: Simulator, + method: GenericStreamingMethod, + data: Vec, + ) -> Result { + Ok(Self { + simulator, + method, + data, + }) + } + + #[must_use] + pub fn data(&self) -> Vec { + self.data.clone() + } + + #[must_use] + pub const fn method(&self) -> GenericStreamingMethod { + self.method + } + + #[must_use] + pub fn simulator(&self) -> Simulator { + self.simulator.clone() + } +} + +#[derive(Clone, Default)] +struct RegisteredPacketCallbacks { + handlers: Vec>, + is_async: bool, +} + +struct AsyncPacketBatch { + default_handlers: Vec>, + specific_handlers: Vec>, + packet: Packet, + simulator: Simulator, +} + +enum PacketWorkerCommand { + Dispatch(AsyncPacketBatch), + Stop, +} + +struct PacketEventState { + callbacks: Mutex>, + async_sender: SyncSender, + worker: Mutex>>, +} + +impl Drop for PacketEventState { + fn drop(&mut self) { + let _ = self.async_sender.try_send(PacketWorkerCommand::Stop); + if let Some(worker) = self + .worker + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take() + && worker.thread().id() != thread::current().id() + { + let _ = worker.join(); + } + } +} + +/// Ordered packet callback registry matching `PacketEventDictionary`. +pub struct PacketEventDictionary { + pub client: GridClient, + state: Arc, +} + +impl PacketEventDictionary { + pub fn new(client: GridClient) -> Result { + let (sender, receiver) = sync_channel(256); + let state = Arc::new(PacketEventState { + callbacks: Mutex::new(HashMap::new()), + async_sender: sender, + worker: Mutex::new(None), + }); + let weak = Arc::downgrade(&state); + let worker = thread::Builder::new() + .name("libremetaverse-packet-events".to_owned()) + .spawn(move || packet_event_worker(receiver, weak)) + .map_err(|_| Error::InvalidOperation)?; + *mutex(&state.worker) = Some(worker); + Ok(Self { client, state }) + } + + pub fn register_event( + &self, + packet_type: PacketType, + event_handler: EventHandler, + is_async: bool, + ) -> Result<(), Error> { + let mut table = mutex(&self.state.callbacks); + let callbacks = table.entry(packet_type).or_default(); + callbacks.is_async |= is_async; + callbacks.handlers.push(event_handler); + Ok(()) + } + + pub fn subscribe( + &self, + packet_type: PacketType, + event_handler: EventHandler, + is_async: bool, + ) -> Subscription { + let identity = Arc::as_ptr(&event_handler).cast::<()>() as usize; + let _ = self.register_event(packet_type, event_handler, is_async); + let state = Arc::downgrade(&self.state); + Subscription::new(move || { + let Some(state) = state.upgrade() else { + return; + }; + let mut table = mutex(&state.callbacks); + let Some(callbacks) = table.get_mut(&packet_type) else { + return; + }; + if let Some(index) = callbacks + .handlers + .iter() + .rposition(|handler| Arc::as_ptr(handler).cast::<()>() as usize == identity) + { + callbacks.handlers.remove(index); + } + if callbacks.handlers.is_empty() { + table.remove(&packet_type); + } + }) + } + + pub fn unregister_event( + &self, + packet_type: PacketType, + event_handler: EventHandler, + ) -> Result<(), Error> { + let mut table = mutex(&self.state.callbacks); + let Some(callbacks) = table.get_mut(&packet_type) else { + return Ok(()); + }; + if let Some(index) = callbacks + .handlers + .iter() + .rposition(|handler| Arc::ptr_eq(handler, &event_handler)) + { + callbacks.handlers.remove(index); + } + if callbacks.handlers.is_empty() { + table.remove(&packet_type); + } + Ok(()) + } + + pub fn invoke_raise_event( + &self, + packet_type: PacketType, + packet: Packet, + simulator: Simulator, + ) -> Result<(), Error> { + self.raise_event(packet_type, packet, simulator) + } + + fn raise_event( + &self, + packet_type: PacketType, + packet: Packet, + simulator: Simulator, + ) -> Result<(), Error> { + let (defaults, specific) = { + let table = mutex(&self.state.callbacks); + ( + table.get(&PacketType::Default).cloned().unwrap_or_default(), + table.get(&packet_type).cloned().unwrap_or_default(), + ) + }; + let default_async = defaults.is_async; + let specific_async = specific.is_async; + let args = PacketReceivedEventArgs::new(packet.clone(), simulator.clone())?; + + if !default_async { + for handler in &defaults.handlers { + invoke_safely(handler, args.clone()); + } + } + if !specific_async { + for handler in &specific.handlers { + invoke_safely(handler, args.clone()); + } + // The reference implementation returns as soon as a concrete + // packet type has synchronous handlers. This also deliberately + // suppresses a default asynchronous chain for that dispatch. + if !specific.handlers.is_empty() { + return Ok(()); + } + } + + let default_handlers: Vec> = if default_async { + defaults.handlers + } else { + Vec::new() + }; + let specific_handlers: Vec> = if specific_async { + specific.handlers + } else { + Vec::new() + }; + if default_handlers.is_empty() && specific_handlers.is_empty() { + return Ok(()); + } + self.state + .async_sender + .try_send(PacketWorkerCommand::Dispatch(AsyncPacketBatch { + default_handlers, + specific_handlers, + packet, + simulator, + })) + .map_err(|_| Error::InvalidOperation) + } +} + +fn packet_event_worker(receiver: Receiver, state: Weak) { + while state.strong_count() != 0 { + match receiver.recv_timeout(Duration::from_millis(100)) { + Ok(PacketWorkerCommand::Dispatch(batch)) => { + let Ok(args) = PacketReceivedEventArgs::new(batch.packet, batch.simulator) else { + continue; + }; + for handler in batch + .default_handlers + .into_iter() + .chain(batch.specific_handlers) + { + invoke_safely(&handler, args.clone()); + } + } + Ok(PacketWorkerCommand::Stop) | Err(RecvTimeoutError::Disconnected) => break, + Err(RecvTimeoutError::Timeout) => {} + } + } +} + +type CapsHandler = dyn Fn(&str, &dyn IMessage, Simulator) + Send + Sync; + +#[derive(Clone)] +pub struct CapsEventQueueCallback { + id: u64, + handler: Arc, +} + +impl CapsEventQueueCallback { + pub fn from_handler( + handler: impl Fn(&str, &dyn IMessage, Simulator) + Send + Sync + 'static, + ) -> Self { + static NEXT_ID: AtomicU64 = AtomicU64::new(1); + Self { + id: NEXT_ID.fetch_add(1, Ordering::Relaxed), + handler: Arc::new(handler), + } + } + + pub fn new(_object: Object, _method: isize) -> Result { + Err(Error::InvalidOperation) + } + + pub fn begin_invoke( + &self, + caps_key: String, + message: Box, + simulator: Simulator, + callback: Box, + object: Object, + ) -> Result, Error> { + (self.handler)(&caps_key, message.as_ref(), simulator); + callback(&object); + Ok(Box::new(object)) + } + + pub fn end_invoke(&self, _result: Box) -> Result<(), Error> { + Ok(()) + } + + pub fn invoke( + &self, + caps_key: String, + message: Box, + simulator: Simulator, + ) -> Result<(), Error> { + (self.handler)(&caps_key, message.as_ref(), simulator); + Ok(()) + } + + fn invoke_ref(&self, caps_key: &str, message: &dyn IMessage, simulator: Simulator) { + let _ = catch_unwind(AssertUnwindSafe(|| { + (self.handler)(caps_key, message, simulator); + })); + } +} + +pub struct CapsEventDictionary { + pub client: GridClient, + callbacks: Arc>>>, +} + +impl CapsEventDictionary { + pub fn new(client: GridClient) -> Result { + Ok(Self { + client, + callbacks: Arc::new(Mutex::new(HashMap::new())), + }) + } + + pub fn register_event( + &self, + caps_event: String, + event_handler: CapsEventQueueCallback, + ) -> Result<(), Error> { + mutex(&self.callbacks) + .entry(caps_event) + .or_default() + .push(event_handler); + Ok(()) + } + + pub fn subscribe( + &self, + caps_event: String, + event_handler: CapsEventQueueCallback, + ) -> Subscription { + let id = event_handler.id; + let _ = self.register_event(caps_event.clone(), event_handler); + let callbacks = Arc::downgrade(&self.callbacks); + Subscription::new(move || { + let Some(callbacks) = callbacks.upgrade() else { + return; + }; + let mut table = mutex(&callbacks); + let Some(entries) = table.get_mut(&caps_event) else { + return; + }; + entries.retain(|entry| entry.id != id); + if entries.is_empty() { + table.remove(&caps_event); + } + }) + } + + pub fn unregister_event( + &self, + caps_event: String, + event_handler: CapsEventQueueCallback, + ) -> Result<(), Error> { + let mut table = mutex(&self.callbacks); + if let Some(entries) = table.get_mut(&caps_event) { + if let Some(index) = entries + .iter() + .rposition(|entry| entry.id == event_handler.id) + { + entries.remove(index); + } + if entries.is_empty() { + table.remove(&caps_event); + } + } + Ok(()) + } + + pub fn invoke_raise_event( + &self, + caps_event: &str, + message: &dyn IMessage, + simulator: Simulator, + ) { + let (defaults, specific) = { + let table = mutex(&self.callbacks); + ( + table.get("").cloned().unwrap_or_default(), + table.get(caps_event).cloned().unwrap_or_default(), + ) + }; + for callback in defaults.into_iter().chain(specific) { + callback.invoke_ref(caps_event, message, simulator.clone()); + } + } +} + +/// Mutable, thread-safe equivalent of the C# public simulator list. +#[derive(Clone, Default)] +pub struct SimulatorCollection(Arc>>); + +impl SimulatorCollection { + #[must_use] + pub fn snapshot(&self) -> Vec { + read(&self.0).clone() + } + + #[must_use] + pub fn len(&self) -> usize { + read(&self.0).len() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn push_if_missing(&self, simulator: Simulator) -> Simulator { + let mut simulators = write(&self.0); + if let Some(existing) = simulators + .iter() + .find(|existing| existing.ip_end_point() == simulator.ip_end_point()) + { + return existing.clone(); + } + simulators.push(simulator.clone()); + simulator + } + + fn remove(&self, simulator: &Simulator) -> bool { + let mut simulators = write(&self.0); + let old_len = simulators.len(); + simulators.retain(|candidate| candidate != simulator); + old_len != simulators.len() + } + + fn drain(&self) -> Vec { + std::mem::take(&mut *write(&self.0)) + } +} + +impl fmt::Debug for SimulatorCollection { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_list() + .entries(self.snapshot().iter()) + .finish() + } +} + +struct TransportThread { + shutdown: libremetaverse_types::compat::CancellationTokenSource, + handle: JoinHandle<()>, +} + +/// Shared data exposed through the C# `Simulator` field surface. +pub struct SimulatorData { + pub access: SimAccess, + pub agent_movement_complete: bool, + pub billable_factor: f32, + pub cpu_class: i32, + pub cpu_ratio: i32, + pub client: GridClient, + pub colo_location: String, + pub data_pool: Option, + pub features: SimulatorFeatures, + pub flags: RegionFlags, + pub global_to_local_id: RwLock>, + pub handle: u64, + pub id: UUID, + pub is_estate_manager: bool, + pub name: String, + pub objects_avatars: RwLock>, + pub objects_primitives: RwLock>, + pub parcel_overlay: Vec, + pub parcel_overlays_received: i32, + pub product_name: String, + pub product_sku: String, + pub protocols: RegionProtocols, + pub region_id: UUID, + pub sequence: i32, + pub sim_owner: UUID, + pub sim_version: String, + pub size_x: u32, + pub size_y: u32, + pub stats: SimulatorSimStats, + pub terrain: Vec, + pub terrain_base0: UUID, + pub terrain_base1: UUID, + pub terrain_base2: UUID, + pub terrain_base3: UUID, + pub terrain_detail0: UUID, + pub terrain_detail1: UUID, + pub terrain_detail2: UUID, + pub terrain_detail3: UUID, + pub terrain_height_range00: f32, + pub terrain_height_range01: f32, + pub terrain_height_range10: f32, + pub terrain_height_range11: f32, + pub terrain_start_height00: f32, + pub terrain_start_height01: f32, + pub terrain_start_height10: f32, + pub terrain_start_height11: f32, + pub water_height: f32, + pub wind_speeds: Option>, + endpoint: std::net::SocketAddr, + connected: AtomicBool, + handshake_complete: AtomicBool, + handshake_wait: Condvar, + handshake_wait_lock: Mutex<()>, + disconnect_candidate: AtomicBool, + circuit_code: AtomicU32, + seed_caps: RwLock>, + manager: Mutex>, + transport: UDPBase, + transport_thread: Mutex>, + ping_id: AtomicU32, + pause_serial: AtomicU32, +} + +impl Drop for SimulatorData { + fn drop(&mut self) { + let runtime = self + .transport_thread + .get_mut() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .take(); + if let Some(runtime) = runtime { + runtime.shutdown.cancel(); + if runtime.handle.thread().id() != thread::current().id() { + let _ = runtime.handle.join(); + } + } + } +} + +/// Reference-identity simulator handle. +#[derive(Clone)] +pub struct Simulator { + /// Capability client associated with this simulator. + /// + /// This remains on the value surface, rather than behind `Deref`, because + /// existing translated code consumes the public C# field by value. + pub caps: Option>, + data: Arc, +} + +impl std::ops::Deref for Simulator { + type Target = SimulatorData; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl PartialEq for Simulator { + fn eq(&self, other: &Self) -> bool { + self.ip_end_point() == other.ip_end_point() + } +} + +impl Eq for Simulator {} + +impl fmt::Debug for Simulator { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("Simulator") + .field("endpoint", &self.native_ip_end_point()) + .field("handle", &self.handle) + .field("connected", &self.native_is_connected()) + .field("handshake_complete", &self.native_handshake_complete()) + .finish_non_exhaustive() + } +} + +impl Simulator { + pub fn native_new( + client: GridClient, + address: std::net::SocketAddr, + handle: u64, + size_x: Option, + size_y: Option, + ) -> Result { + let cancellation = client.cancellation_token(); + let simulator_reference = Arc::new(Mutex::new(Weak::new())); + let handler: Arc = Arc::new(SimulatorUdpHandler { + simulator: Arc::clone(&simulator_reference), + }); + let transport = UDPBase::client( + address, + UdpTransportConfig::default(), + handler, + cancellation, + )?; + let data = Arc::new(SimulatorData { + access: SimAccess::UNKNOWN, + agent_movement_complete: false, + billable_factor: 0.0, + cpu_class: 0, + cpu_ratio: 0, + client, + colo_location: String::new(), + data_pool: None, + features: SimulatorFeatures, + flags: RegionFlags(0), + global_to_local_id: RwLock::new(HashMap::new()), + handle, + id: UUID::zero(), + is_estate_manager: false, + name: String::new(), + objects_avatars: RwLock::new(HashMap::new()), + objects_primitives: RwLock::new(HashMap::new()), + parcel_overlay: vec![0; 4096], + parcel_overlays_received: 0, + product_name: String::new(), + product_sku: String::new(), + protocols: RegionProtocols(0), + region_id: UUID::zero(), + sequence: 0, + sim_owner: UUID::zero(), + sim_version: String::new(), + size_x: size_x.unwrap_or(Self::DEFAULT_REGION_SIZE_X), + size_y: size_y.unwrap_or(Self::DEFAULT_REGION_SIZE_Y), + stats: SimulatorSimStats, + terrain: Vec::new(), + terrain_base0: UUID::zero(), + terrain_base1: UUID::zero(), + terrain_base2: UUID::zero(), + terrain_base3: UUID::zero(), + terrain_detail0: UUID::zero(), + terrain_detail1: UUID::zero(), + terrain_detail2: UUID::zero(), + terrain_detail3: UUID::zero(), + terrain_height_range00: 0.0, + terrain_height_range01: 0.0, + terrain_height_range10: 0.0, + terrain_height_range11: 0.0, + terrain_start_height00: 0.0, + terrain_start_height01: 0.0, + terrain_start_height10: 0.0, + terrain_start_height11: 0.0, + water_height: 0.0, + wind_speeds: None, + endpoint: address, + connected: AtomicBool::new(false), + handshake_complete: AtomicBool::new(false), + handshake_wait: Condvar::new(), + handshake_wait_lock: Mutex::new(()), + disconnect_candidate: AtomicBool::new(false), + circuit_code: AtomicU32::new(0), + seed_caps: RwLock::new(None), + manager: Mutex::new(Weak::new()), + transport, + transport_thread: Mutex::new(None), + ping_id: AtomicU32::new(0), + pause_serial: AtomicU32::new(0), + }); + *mutex(&simulator_reference) = Arc::downgrade(&data); + Ok(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); + } + + pub fn native_connect(&self, _move_to_sim: bool) -> Result { + if self.native_is_connected() { + self.native_use_circuit_code(true)?; + return Ok(true); + } + let mut transport_thread = mutex(&self.transport_thread); + if transport_thread.is_none() { + let shutdown = libremetaverse_types::compat::CancellationTokenSource::default(); + let shutdown_token = shutdown.token(); + let transport = self.transport.clone(); + let (ready_sender, ready_receiver) = sync_channel(1); + let handle = thread::Builder::new() + .name(format!("libremetaverse-udp-{}", self.endpoint)) + .spawn(move || { + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build(); + let Ok(runtime) = runtime else { + let _ = + ready_sender.send(Err(crate::UdpTransportError::RuntimeUnavailable)); + return; + }; + runtime.block_on(async move { + let result = transport.start_transport(); + let started = result.is_ok(); + let _ = ready_sender.send(result); + if started { + shutdown_token.cancelled().await; + transport.stop_async().await; + } + }); + }) + .map_err(|_| Error::InvalidOperation)?; + let timeout = + positive_millisecond_duration(self.client.settings_ref().timing.login_timeout); + match ready_receiver.recv_timeout(timeout) { + Ok(Ok(())) => { + *transport_thread = Some(TransportThread { shutdown, handle }); + } + Ok(Err(error)) => { + let _ = handle.join(); + return Err(error.into()); + } + Err(_) => { + shutdown.cancel(); + let _ = handle.join(); + return Ok(false); + } + } + } + self.connected.store(true, Ordering::Release); + self.handshake_complete.store(false, Ordering::Release); + drop(transport_thread); + self.native_use_circuit_code(true)?; + let timeout = + positive_millisecond_duration(self.client.settings_ref().timing.login_timeout); + let guard = mutex(&self.handshake_wait_lock); + let (guard, _) = self + .handshake_wait + .wait_timeout_while(guard, timeout, |()| { + !self.handshake_complete.load(Ordering::Acquire) + && !self.client.cancellation_token().is_cancellation_requested() + }) + .unwrap_or_else(std::sync::PoisonError::into_inner); + drop(guard); + if !self.handshake_complete.load(Ordering::Acquire) + && let Some(manager) = mutex(&self.manager).upgrade() + { + manager.simulators.remove(self); + } + Ok(true) + } + + pub async fn native_connect_async(&self, move_to_sim: bool) -> Result { + let simulator = self.clone(); + run_blocking_compat("libremetaverse-simulator-connect", move || { + simulator.native_connect(move_to_sim) + }) + .await? + } + + pub fn native_disconnect(&self, send_close_circuit: bool) -> Result<(), Error> { + self.disconnect_candidate.store(false, Ordering::Release); + let was_connected = self.connected.swap(false, Ordering::AcqRel); + self.handshake_complete.store(false, Ordering::Release); + self.handshake_wait.notify_all(); + if !was_connected { + return Ok(()); + } + if send_close_circuit { + let close = CloseCircuitPacket::new_with_constructor()?; + let data = close.to_bytes_with_method()?; + let _ = self.native_send_packet_data( + data.clone(), + i32::try_from(data.len()).map_err(|_| Error::Argument)?, + PacketType::CloseCircuit, + false, + ); + } + if let Some(runtime) = mutex(&self.transport_thread).take() { + runtime.shutdown.cancel(); + if runtime.handle.thread().id() != thread::current().id() { + let _ = runtime.handle.join(); + } + } + Ok(()) + } + + pub fn native_dispose(&self) -> Result<(), Error> { + self.native_disconnect(false) + } + + #[must_use] + pub fn native_equals(&self, obj: Option) -> bool { + obj.and_then(|value| value.downcast_arc::()) + .is_some_and(|other| *self == *other) + } + + #[must_use] + pub fn native_hash_code(&self) -> i32 { + let folded = self.handle ^ (self.handle >> 32); + let low = u32::try_from(folded & u64::from(u32::MAX)).unwrap_or_default(); + low.cast_signed() + } + + pub fn native_pause(&self) -> Result<(), Error> { + let mut packet = AgentPausePacket::new_with_constructor()?; + packet.agent_data.agent_id = UUID::zero(); + packet.agent_data.session_id = UUID::zero(); + packet.agent_data.serial_num = self.pause_serial.fetch_add(1, Ordering::AcqRel); + let data = packet.to_bytes_with_method()?; + self.native_send_packet_data( + data.clone(), + i32::try_from(data.len()).map_err(|_| Error::Argument)?, + PacketType::AgentPause, + false, + ) + } + + pub fn native_resume(&self) -> Result<(), Error> { + let mut packet = AgentResumePacket::new_with_constructor()?; + packet.agent_data.agent_id = UUID::zero(); + packet.agent_data.session_id = UUID::zero(); + packet.agent_data.serial_num = self.pause_serial.fetch_add(1, Ordering::AcqRel); + let data = packet.to_bytes_with_method()?; + self.native_send_packet_data( + data.clone(), + i32::try_from(data.len()).map_err(|_| Error::Argument)?, + PacketType::AgentResume, + false, + ) + } + + pub fn native_send_packet(&self, packet: Packet) -> Result<(), Error> { + let data = crate::packet_wire::encode_base_packet(&packet)?; + self.native_send_packet_data( + data.clone(), + i32::try_from(data.len()).map_err(|_| Error::Argument)?, + packet.type_, + packet.header.zerocoded, + ) + } + + pub fn native_send_packet_data( + &self, + data: Vec, + data_length: i32, + packet_type: PacketType, + do_zerocode: bool, + ) -> Result<(), Error> { + let length = usize::try_from(data_length).map_err(|_| Error::Argument)?; + let payload = data.get(..length).ok_or(Error::Argument)?.to_vec(); + self.transport + .try_send_packet(payload, self.endpoint, packet_type, do_zerocode) + .map(|_| ()) + .map_err(Into::into) + } + + pub fn native_send_ping(&self) -> Result<(), Error> { + let mut ping = StartPingCheckPacket::new_with_constructor()?; + ping.ping_id.ping_id = self.ping_id.fetch_add(1, Ordering::Relaxed).to_le_bytes()[0]; + ping.ping_id.oldest_unacked = 0; + let data = ping.to_bytes_with_method()?; + self.native_send_packet_data( + data.clone(), + i32::try_from(data.len()).map_err(|_| Error::Argument)?, + PacketType::StartPingCheck, + false, + ) + } + + pub fn native_set_seed_caps( + &self, + seed_caps: Option, + changed_sim: Option, + ) -> Result<(), Error> { + let mut current = write(&self.seed_caps); + if !changed_sim.unwrap_or(false) && *current == seed_caps { + return Ok(()); + } + *current = seed_caps; + Ok(()) + } + + #[must_use] + pub fn seed_capability(&self) -> Option { + read(&self.seed_caps).clone() + } + + pub fn native_use_circuit_code(&self, wait_for_ack: bool) -> Result<(), Error> { + let data = self.use_circuit_code_bytes()?; + if wait_for_ack { + let receiver = self + .transport + .try_send_packet_wait_ack(data, self.endpoint, PacketType::UseCircuitCode, false) + .map_err(Error::from)?; + let timeout = + positive_millisecond_duration(self.client.settings_ref().timing.login_timeout); + let _ = receiver.recv_timeout(timeout); + } else { + self.transport + .try_send_packet(data, self.endpoint, PacketType::UseCircuitCode, false) + .map_err(Error::from)?; + } + Ok(()) + } + + pub async fn native_use_circuit_code_async(&self, wait_for_ack: bool) -> Result { + let data = self.use_circuit_code_bytes()?; + if !wait_for_ack { + self.transport + .try_send_packet(data, self.endpoint, PacketType::UseCircuitCode, false) + .map_err(Error::from)?; + return Ok(true); + } + let receiver = self + .transport + .try_send_packet_wait_ack(data, self.endpoint, PacketType::UseCircuitCode, false) + .map_err(Error::from)?; + let timeout = + positive_millisecond_duration(self.client.settings_ref().timing.login_timeout); + run_blocking_compat("libremetaverse-circuit-ack", move || { + receiver.recv_timeout(timeout).is_ok() + }) + .await + } + + fn use_circuit_code_bytes(&self) -> Result, Error> { + let mut packet = UseCircuitCodePacket::new_with_constructor()?; + packet.circuit_code.code = self.circuit_code.load(Ordering::Acquire); + packet.circuit_code.id = UUID::zero(); + packet.circuit_code.session_id = UUID::zero(); + packet.to_bytes_with_method() + } + + #[must_use] + pub fn native_is_connected(&self) -> bool { + self.connected.load(Ordering::Acquire) + } + + #[must_use] + pub fn native_handshake_complete(&self) -> bool { + self.handshake_complete.load(Ordering::Acquire) + } + + #[must_use] + pub fn native_ip_end_point(&self) -> std::net::SocketAddr { + self.data.endpoint + } + + #[must_use] + pub fn native_to_string(&self) -> String { + if self.name.is_empty() { + format!("({})", self.endpoint) + } else { + format!("{} ({})", self.name, self.endpoint) + } + } + + fn note_packet_received(&self, _packet_type: PacketType) { + self.disconnect_candidate.store(false, Ordering::Release); + } + + fn mark_disconnect_candidate(&self) -> bool { + self.disconnect_candidate.swap(true, Ordering::AcqRel) + } + + fn send_buffer(&self, buffer: UDPPacketBuffer) -> Result<(), Error> { + self.transport.async_begin_send(buffer) + } +} + +struct SimulatorUdpHandler { + simulator: Arc>>, +} + +impl UdpPacketHandler for SimulatorUdpHandler { + fn packet_received(&self, buffer: UDPPacketBuffer) { + let Some(data) = mutex(&self.simulator).upgrade() else { + return; + }; + let simulator = Simulator { caps: None, data }; + let Ok(length) = usize::try_from(buffer.data_length) else { + return; + }; + let Some(payload) = buffer.data.get(..length) else { + return; + }; + let Ok(payload_length) = i32::try_from(payload.len()) else { + return; + }; + let mut packet_end = payload_length - 1; + let mut zero_buffer = vec![0_u8; UdpTransportConfig::default().max_decoded_packet_size]; + let Ok(packet) = + crate::packet_wire::build_packet_from_bytes(payload, &mut packet_end, &mut zero_buffer) + else { + return; + }; + simulator.note_packet_received(packet.type_); + let manager = mutex(&simulator.manager).upgrade(); + if let Some(manager) = manager { + let _ = manager.enqueue_incoming(NetworkManagerIncomingPacket { + simulator: Some(simulator), + packet: Some(packet), + raw_data: Some(payload.to_vec()), + }); + } + } + + fn packet_sent(&self, buffer: UDPPacketBuffer, bytes_sent: usize) { + let Some(data) = mutex(&self.simulator).upgrade() else { + return; + }; + let simulator = Simulator { caps: None, data }; + let manager = mutex(&simulator.manager).upgrade(); + if let Some(manager) = manager { + manager.raise_packet_sent(buffer.data, bytes_sent, simulator); + } + } +} + +#[derive(Clone)] +pub struct NetworkManagerIncomingPacket { + pub packet: Option, + pub simulator: Option, + raw_data: Option>, +} + +impl NetworkManagerIncomingPacket { + pub fn new() -> Result { + Ok(Self { + packet: None, + simulator: None, + raw_data: None, + }) + } +} + +#[derive(Clone)] +pub struct NetworkManagerOutgoingPacket { + pub buffer: UDPPacketBuffer, + pub resend_count: i32, + pub sequence_number: u32, + pub simulator: Simulator, + pub tick_count: i32, + pub type_: PacketType, +} + +impl NetworkManagerOutgoingPacket { + pub fn new( + simulator: Simulator, + buffer: UDPPacketBuffer, + type_: PacketType, + ) -> Result { + Ok(Self { + simulator, + buffer, + sequence_number: 0, + resend_count: 0, + tick_count: 0, + type_, + }) + } +} + +struct NetworkWorkers { + incoming: SyncSender, + outgoing: SyncSender, + cancellation: libremetaverse_types::compat::CancellationTokenSource, + handles: Vec>, +} + +#[derive(Default)] +struct NetworkEvents { + disconnected: EventRegistry, + generic_streaming_message: EventRegistry, + packet_sent: EventRegistry, + sim_changed: EventRegistry, + sim_connected: EventRegistry, + sim_connecting: EventRegistry, + sim_disconnected: EventRegistry, +} + +struct NetworkManagerInner { + client: GridClient, + simulators: SimulatorCollection, + packet_events: PacketEventDictionary, + caps_events: CapsEventDictionary, + events: NetworkEvents, + current_sim: RwLock>, + connected: AtomicBool, + circuit_code: AtomicU32, + inbox_count: AtomicI32, + outbox_count: AtomicI32, + workers: Mutex>, + disconnect_lock: Mutex<()>, + shutting_down: AtomicBool, +} + +impl NetworkManagerInner { + fn ensure_workers(self: &Arc) -> Result<(), Error> { + let mut workers = mutex(&self.workers); + if workers.is_some() { + return Ok(()); + } + let capacity = usize::try_from(crate::Settings::udp_receive_queue_capacity()) + .map_err(|_| Error::InvalidOperation)? + .max(1); + let (incoming_sender, incoming_receiver) = + sync_channel::(capacity); + let (outgoing_sender, outgoing_receiver) = + sync_channel::(capacity); + let cancellation = libremetaverse_types::compat::CancellationTokenSource::default(); + let incoming_token = cancellation.token(); + let outgoing_token = cancellation.token(); + let keepalive_token = cancellation.token(); + let incoming_weak = Arc::downgrade(self); + let outgoing_weak = Arc::downgrade(self); + let keepalive_weak = Arc::downgrade(self); + + let incoming_handle = thread::Builder::new() + .name("libremetaverse-network-incoming".to_owned()) + .spawn(move || { + while !incoming_token.is_cancellation_requested() { + match incoming_receiver.recv_timeout(Duration::from_millis(50)) { + Ok(incoming) => { + let Some(inner) = incoming_weak.upgrade() else { + break; + }; + inner.inbox_count.fetch_sub(1, Ordering::AcqRel); + let raw_data = incoming.raw_data; + let (Some(packet), Some(simulator)) = + (incoming.packet, incoming.simulator) + else { + continue; + }; + inner.process_internal_packet(&packet, &simulator, raw_data.as_deref()); + let _ = + inner + .packet_events + .raise_event(packet.type_, packet, simulator); + } + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => break, + } + } + }) + .map_err(|_| Error::InvalidOperation)?; + + let outgoing_handle = thread::Builder::new() + .name("libremetaverse-network-outgoing".to_owned()) + .spawn(move || { + while !outgoing_token.is_cancellation_requested() { + match outgoing_receiver.recv_timeout(Duration::from_millis(50)) { + Ok(outgoing) => { + let Some(inner) = outgoing_weak.upgrade() else { + break; + }; + inner.outbox_count.fetch_sub(1, Ordering::AcqRel); + let _ = outgoing.simulator.send_buffer(outgoing.buffer); + } + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => break, + } + } + }) + .map_err(|_| Error::InvalidOperation)?; + + let timeout = + positive_millisecond_duration(self.client.settings_ref().timing.simulator_timeout); + let keepalive_handle = thread::Builder::new() + .name("libremetaverse-network-keepalive".to_owned()) + .spawn(move || { + while !wait_for_cancellation(&keepalive_token, timeout) { + let Some(inner) = keepalive_weak.upgrade() else { + break; + }; + inner.keepalive_tick(); + } + }) + .map_err(|_| Error::InvalidOperation)?; + + *workers = Some(NetworkWorkers { + incoming: incoming_sender, + outgoing: outgoing_sender, + cancellation, + handles: vec![incoming_handle, outgoing_handle, keepalive_handle], + }); + Ok(()) + } + + fn process_internal_packet( + &self, + packet: &Packet, + simulator: &Simulator, + raw_data: Option<&[u8]>, + ) { + match packet.type_ { + PacketType::RegionHandshake => { + let Some(raw_data) = raw_data else { + return; + }; + let mut position = 0; + let Ok(_incoming) = + RegionHandshakePacket::new_with_bytes_int32(raw_data.to_vec(), &mut position) + else { + return; + }; + + let Ok(mut reply) = RegionHandshakeReplyPacket::new_with_constructor() else { + return; + }; + reply.agent_data.agent_id = UUID::zero(); + reply.agent_data.session_id = UUID::zero(); + reply.region_info.flags = 0x1 | 0x2 | 0x4; + let Ok(data) = reply.to_bytes_with_method() else { + return; + }; + let Ok(length) = i32::try_from(data.len()) else { + return; + }; + if simulator + .native_send_packet_data(data, length, PacketType::RegionHandshakeReply, true) + .is_err() + { + return; + } + simulator.connected.store(true, Ordering::Release); + simulator.handshake_complete.store(true, Ordering::Release); + simulator.handshake_wait.notify_all(); + } + PacketType::StartPingCheck => { + let Some(raw_data) = raw_data else { + return; + }; + let Ok(mut incoming) = StartPingCheckPacket::new_with_constructor() else { + return; + }; + let Ok(mut packet_end) = i32::try_from(raw_data.len()) else { + return; + }; + packet_end -= 1; + let mut position = 0; + let mut zero_buffer = + vec![0_u8; UdpTransportConfig::default().max_decoded_packet_size]; + if incoming + .from_bytes_with_bytes_int32_int32_bytes( + raw_data.to_vec(), + &mut position, + &mut packet_end, + Some(&mut zero_buffer), + ) + .is_err() + { + return; + } + if incoming.ping_id.oldest_unacked > 0 { + let _ = simulator.transport.try_flush_acks(simulator.endpoint); + } + let Ok(mut response) = CompletePingCheckPacket::new_with_constructor() else { + return; + }; + response.ping_id.ping_id = incoming.ping_id.ping_id; + let Ok(mut data) = response.to_bytes_with_method() else { + return; + }; + if let Some(flags) = data.first_mut() { + *flags &= !Helpers::MSG_RELIABLE; + } + let Ok(length) = i32::try_from(data.len()) else { + return; + }; + let _ = simulator.native_send_packet_data( + data, + length, + PacketType::CompletePingCheck, + false, + ); + } + PacketType::DisableSimulator => { + let _ = self.disconnect_simulator(simulator.clone(), false); + } + PacketType::KickUser => { + let Some(raw_data) = raw_data else { + return; + }; + let Ok(mut incoming) = KickUserPacket::new_with_constructor() else { + return; + }; + let Ok(mut packet_end) = i32::try_from(raw_data.len()) else { + return; + }; + packet_end -= 1; + let mut position = 0; + let mut zero_buffer = + vec![0_u8; UdpTransportConfig::default().max_decoded_packet_size]; + if incoming + .from_bytes_with_bytes_int32_int32_bytes( + raw_data.to_vec(), + &mut position, + &mut packet_end, + Some(&mut zero_buffer), + ) + .is_err() + { + return; + } + let message = String::from_utf8_lossy(&incoming.user_info.reason) + .trim_end_matches('\0') + .to_owned(); + let _ = self.shutdown( + crate::NetworkManagerDisconnectType::ServerInitiated, + message, + ); + } + PacketType::GenericStreamingMessage => { + let Some(raw_data) = raw_data else { + return; + }; + let Ok(mut incoming) = GenericStreamingMessagePacket::new_with_constructor() else { + return; + }; + let Ok(mut packet_end) = i32::try_from(raw_data.len()) else { + return; + }; + packet_end -= 1; + let mut position = 0; + let mut zero_buffer = + vec![0_u8; UdpTransportConfig::default().max_decoded_packet_size]; + if incoming + .from_bytes_with_bytes_int32_int32_bytes( + raw_data.to_vec(), + &mut position, + &mut packet_end, + Some(&mut zero_buffer), + ) + .is_err() + { + return; + } + let method = if incoming.method_data.method + == GenericStreamingMethod::GltfMaterialOverride as u16 + { + GenericStreamingMethod::GltfMaterialOverride + } else { + GenericStreamingMethod::Unknown + }; + let Ok(args) = GenericStreamingMessageEventArgs::new( + simulator.clone(), + method, + incoming.data_block.data, + ) else { + return; + }; + self.events + .generic_streaming_message + .emit_with(|| args.clone()); + } + _ => {} + } + } + + fn disconnect_simulator( + &self, + simulator: Simulator, + send_close_circuit: bool, + ) -> Result<(), Error> { + let disconnect_guard = mutex(&self.disconnect_lock); + let was_connected = simulator.connected(); + simulator.native_disconnect(send_close_circuit)?; + if !self.simulators.remove(&simulator) { + return Ok(()); + } + if read(&self.current_sim).as_ref() == Some(&simulator) { + *write(&self.current_sim) = None; + } + let args = SimDisconnectedEventArgs::new( + simulator.clone(), + crate::NetworkManagerDisconnectType::NetworkTimeout, + was_connected, + )?; + drop(disconnect_guard); + self.events.sim_disconnected.emit_with(|| args.clone()); + if self.simulators.is_empty() { + self.shutdown( + crate::NetworkManagerDisconnectType::SimShutdown, + format!( + "You have disconnected from {}.", + simulator.native_to_string() + ), + )?; + } + Ok(()) + } + + fn enqueue_incoming(&self, incoming: NetworkManagerIncomingPacket) -> Result<(), Error> { + let workers = mutex(&self.workers); + let Some(workers) = workers.as_ref() else { + return Err(Error::InvalidOperation); + }; + workers + .incoming + .try_send(incoming) + .map_err(|_| Error::InvalidOperation)?; + self.inbox_count.fetch_add(1, Ordering::AcqRel); + Ok(()) + } + + fn enqueue_outgoing(&self, outgoing: NetworkManagerOutgoingPacket) -> Result<(), Error> { + let workers = mutex(&self.workers); + let Some(workers) = workers.as_ref() else { + return Err(Error::InvalidOperation); + }; + workers + .outgoing + .try_send(outgoing) + .map_err(|_| Error::InvalidOperation)?; + self.outbox_count.fetch_add(1, Ordering::AcqRel); + Ok(()) + } + + fn stop_workers(&self) { + let workers = mutex(&self.workers).take(); + let Some(workers) = workers else { + return; + }; + workers.cancellation.cancel(); + drop(workers.incoming); + drop(workers.outgoing); + for handle in workers.handles { + if handle.thread().id() != thread::current().id() { + let _ = handle.join(); + } + } + self.inbox_count.store(0, Ordering::Release); + self.outbox_count.store(0, Ordering::Release); + } + + fn keepalive_tick(&self) { + if !self.connected.load(Ordering::Acquire) { + return; + } + let current = read(&self.current_sim).clone(); + let Some(current) = current else { + return; + }; + if current.mark_disconnect_candidate() { + let _ = self.shutdown( + crate::NetworkManagerDisconnectType::NetworkTimeout, + "NetworkTimeout".to_owned(), + ); + } else { + let _ = current.native_send_ping(); + } + } + + fn raise_packet_sent(&self, data: Vec, bytes_sent: usize, simulator: Simulator) { + let Ok(bytes_sent) = i32::try_from(bytes_sent) else { + return; + }; + let Ok(args) = PacketSentEventArgs::new(data, bytes_sent, simulator) else { + return; + }; + self.events.packet_sent.emit_with(|| args.clone()); + } + + fn set_current_sim(&self, simulator: Simulator, seed_caps: Option) -> Result<(), Error> { + let previous = { + let mut current = write(&self.current_sim); + if current.as_ref() == Some(&simulator) { + simulator.native_set_seed_caps(seed_caps, Some(false))?; + return Ok(()); + } + let previous = current.clone(); + *current = Some(simulator.clone()); + previous + }; + simulator.native_set_seed_caps(seed_caps, Some(previous.as_ref() != Some(&simulator)))?; + let args = SimChangedEventArgs::new(previous)?; + self.events.sim_changed.emit_with(|| args.clone()); + Ok(()) + } + + fn shutdown( + &self, + reason: crate::NetworkManagerDisconnectType, + message: String, + ) -> Result<(), Error> { + if self.shutting_down.swap(true, Ordering::AcqRel) { + return Ok(()); + } + let current = write(&self.current_sim).take(); + let mut simulators: Vec<_> = self + .simulators + .drain() + .into_iter() + .filter(|simulator| current.as_ref() != Some(simulator)) + .collect(); + if let Some(current) = current { + simulators.push(current); + } + let send_close = matches!( + reason, + crate::NetworkManagerDisconnectType::ClientInitiated + | crate::NetworkManagerDisconnectType::NetworkTimeout + ); + for simulator in simulators { + let was_connected = simulator.connected(); + let _ = simulator.native_disconnect(send_close); + let args = SimDisconnectedEventArgs::new(simulator, reason, was_connected)?; + self.events.sim_disconnected.emit_with(|| args.clone()); + } + self.stop_workers(); + self.connected.store(false, Ordering::Release); + let args = DisconnectedEventArgs::new(reason, message)?; + self.events.disconnected.emit_with(|| args.clone()); + self.shutting_down.store(false, Ordering::Release); + Ok(()) + } +} + +impl Drop for NetworkManagerInner { + fn drop(&mut self) { + self.stop_workers(); + for simulator in self.simulators.drain() { + let _ = simulator.native_disconnect(false); + } + } +} + +/// Native implementation of C# `NetworkManager`. +pub struct NetworkManager { + pub login_response_data: Option, + pub simulators: SimulatorCollection, + inner: Arc, +} + +impl fmt::Debug for NetworkManager { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("NetworkManager") + .field("connected", &self.native_connected()) + .field("circuit_code", &self.native_circuit_code()) + .field("simulators", &self.simulators) + .finish_non_exhaustive() + } +} + +impl NetworkManager { + pub fn native_new(client: GridClient) -> Result { + let simulators = SimulatorCollection::default(); + let packet_events = PacketEventDictionary::new(client.clone())?; + let caps_events = CapsEventDictionary::new(client.clone())?; + let inner = Arc::new(NetworkManagerInner { + client, + simulators: simulators.clone(), + packet_events, + caps_events, + events: NetworkEvents::default(), + current_sim: RwLock::new(None), + connected: AtomicBool::new(false), + circuit_code: AtomicU32::new(0), + inbox_count: AtomicI32::new(0), + outbox_count: AtomicI32::new(0), + workers: Mutex::new(None), + disconnect_lock: Mutex::new(()), + shutting_down: AtomicBool::new(false), + }); + let weak_inner = Arc::downgrade(&inner); + inner.caps_events.register_event( + "EnableSimulator".to_owned(), + CapsEventQueueCallback::from_handler(move |_, message, _| { + let Some(inner) = weak_inner.upgrade() else { + return; + }; + if !inner.client.settings_ref().agent.multiple_sims { + return; + } + let any_message = message as &dyn std::any::Any; + let Some(message) = + any_message.downcast_ref::() + else { + return; + }; + let manager = NetworkManager { + login_response_data: None, + simulators: inner.simulators.clone(), + inner: Arc::clone(&inner), + }; + for simulator in &message.simulators { + let Ok(port) = u16::try_from(simulator.port) else { + continue; + }; + let endpoint = std::net::SocketAddr::new(simulator.ip, port); + if manager.native_find_endpoint(endpoint).is_some() { + continue; + } + let _ = manager.native_connect( + endpoint, + simulator.region_handle, + false, + None, + simulator.region_size_x, + simulator.region_size_y, + ); + } + }), + )?; + Ok(Self { + login_response_data: None, + simulators, + inner, + }) + } + + pub fn native_subscribe_disconnected( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.disconnected.subscribe(handler) + } + + pub fn native_subscribe_generic_streaming_message( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner + .events + .generic_streaming_message + .subscribe(handler) + } + + pub fn native_subscribe_packet_sent( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.packet_sent.subscribe(handler) + } + + pub fn native_subscribe_sim_changed( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.sim_changed.subscribe(handler) + } + + pub fn native_subscribe_sim_connected( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.sim_connected.subscribe(handler) + } + + pub fn native_subscribe_sim_connecting( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.sim_connecting.subscribe(handler) + } + + pub fn native_subscribe_sim_disconnected( + &self, + handler: EventHandler, + ) -> Subscription { + self.inner.events.sim_disconnected.subscribe(handler) + } + + pub fn subscribe_packet( + &self, + packet_type: PacketType, + handler: EventHandler, + is_async: bool, + ) -> Subscription { + self.inner + .packet_events + .subscribe(packet_type, handler, is_async) + } + + pub fn subscribe_caps( + &self, + caps_event: String, + handler: CapsEventQueueCallback, + ) -> Subscription { + self.inner.caps_events.subscribe(caps_event, handler) + } + + /// Dispatches a decoded CAPS event through the default and named handler + /// chains. This is the native boundary used by a CAPS event-queue client. + pub fn dispatch_caps_event( + &self, + caps_event: &str, + message: &dyn IMessage, + simulator: Simulator, + ) { + self.inner + .caps_events + .invoke_raise_event(caps_event, message, simulator); + } + + pub fn native_register_callback( + &self, + packet_type: PacketType, + callback: EventHandler, + is_async: bool, + ) -> Result<(), Error> { + self.inner + .packet_events + .register_event(packet_type, callback, is_async) + } + + pub fn native_unregister_callback( + &self, + packet_type: PacketType, + callback: EventHandler, + ) -> Result<(), Error> { + self.inner + .packet_events + .unregister_event(packet_type, callback) + } + + pub fn native_register_caps( + &self, + caps_event: String, + callback: CapsEventQueueCallback, + ) -> Result<(), Error> { + self.inner.caps_events.register_event(caps_event, callback) + } + + pub fn native_unregister_caps( + &self, + caps_event: String, + callback: CapsEventQueueCallback, + ) -> Result<(), Error> { + self.inner + .caps_events + .unregister_event(caps_event, callback) + } + + pub fn native_connect( + &self, + endpoint: std::net::SocketAddr, + handle: u64, + set_default: bool, + seed_caps: Option, + size_x: u32, + size_y: u32, + ) -> Result, Error> { + self.inner.ensure_workers()?; + let simulator = self + .native_find_endpoint(endpoint) + .unwrap_or(Simulator::native_new( + self.inner.client.clone(), + endpoint, + handle, + Some(size_x), + Some(size_y), + )?); + simulator.attach_manager(&self.inner, self.inner.circuit_code.load(Ordering::Acquire)); + let simulator = self.simulators.push_if_missing(simulator); + + if !simulator.connected() { + self.inner.connected.store(true, Ordering::Release); + let args = SimConnectingEventArgs::new(simulator.clone())?; + self.inner.events.sim_connecting.emit_with(|| args.clone()); + if args.cancel() { + self.simulators.remove(&simulator); + return Ok(None); + } + match simulator.native_connect(set_default) { + Ok(true) => {} + Ok(false) => { + self.simulators.remove(&simulator); + return Ok(None); + } + Err(error) => { + let _ = simulator.native_disconnect(false); + self.simulators.remove(&simulator); + return Err(error); + } + } + if set_default { + self.inner.set_current_sim(simulator.clone(), seed_caps)?; + } + let args = SimConnectedEventArgs::new(simulator.clone())?; + self.inner.events.sim_connected.emit_with(|| args.clone()); + } else if set_default { + simulator.native_use_circuit_code(true)?; + self.inner.set_current_sim(simulator.clone(), seed_caps)?; + } + Ok(Some(simulator)) + } + + pub async fn native_connect_async( + &self, + endpoint: std::net::SocketAddr, + handle: u64, + set_default: bool, + seed_caps: Option, + size_x: u32, + size_y: u32, + ) -> Result, Error> { + let manager = Self { + login_response_data: None, + simulators: self.simulators.clone(), + inner: Arc::clone(&self.inner), + }; + run_blocking_compat("libremetaverse-manager-connect", move || { + manager.native_connect(endpoint, handle, set_default, seed_caps, size_x, size_y) + }) + .await? + } + + pub fn native_disconnect_sim( + &self, + simulator: Simulator, + send_close_circuit: bool, + ) -> Result<(), Error> { + self.inner + .disconnect_simulator(simulator, send_close_circuit) + } + + pub fn native_enqueue_incoming( + &self, + incoming: NetworkManagerIncomingPacket, + ) -> Result<(), Error> { + self.inner.enqueue_incoming(incoming) + } + + pub fn native_enqueue_outgoing( + &self, + outgoing: NetworkManagerOutgoingPacket, + ) -> Result<(), Error> { + self.inner.enqueue_outgoing(outgoing) + } + + #[must_use] + pub fn native_find_endpoint(&self, endpoint: std::net::SocketAddr) -> Option { + self.simulators + .snapshot() + .into_iter() + .find(|simulator| simulator.ip_end_point() == endpoint) + } + + #[must_use] + pub fn native_find_handle(&self, handle: u64) -> Option { + self.simulators + .snapshot() + .into_iter() + .find(|simulator| simulator.handle == handle) + } + + pub fn native_send_packet( + &self, + packet: Packet, + simulator: Option, + ) -> Result<(), Error> { + let simulator = simulator + .or_else(|| self.native_current_sim()) + .or_else(|| self.simulators.snapshot().into_iter().next()) + .ok_or(Error::InvalidOperation)?; + simulator.native_send_packet(packet) + } + + pub fn native_shutdown( + &self, + reason: crate::NetworkManagerDisconnectType, + message: String, + ) -> Result<(), Error> { + self.inner.shutdown(reason, message) + } + + pub async fn native_shutdown_async( + &self, + reason: crate::NetworkManagerDisconnectType, + message: String, + ) -> Result<(), Error> { + let inner = Arc::clone(&self.inner); + run_blocking_compat("libremetaverse-manager-shutdown", move || { + inner.shutdown(reason, message) + }) + .await? + } + + #[must_use] + pub fn native_circuit_code(&self) -> u32 { + self.inner.circuit_code.load(Ordering::Acquire) + } + + pub fn native_set_circuit_code(&self, value: u32) { + self.inner.circuit_code.store(value, Ordering::Release); + for simulator in self.simulators.snapshot() { + simulator.circuit_code.store(value, Ordering::Release); + } + } + + #[must_use] + pub fn native_connected(&self) -> bool { + self.inner.connected.load(Ordering::Acquire) + } + + pub fn native_set_connected(&self, value: bool) { + self.inner.connected.store(value, Ordering::Release); + } + + #[must_use] + pub fn native_current_sim(&self) -> Option { + read(&self.inner.current_sim).clone() + } + + pub fn native_set_current_sim(&self, value: Option) { + *write(&self.inner.current_sim) = value; + } + + #[must_use] + pub fn native_inbox_count(&self) -> i32 { + self.inner.inbox_count.load(Ordering::Acquire) + } + + #[must_use] + pub fn native_outbox_count(&self) -> i32 { + self.inner.outbox_count.load(Ordering::Acquire) + } + + /// Executes one deterministic keepalive transition without sleeping. + pub fn poll_keepalive(&self) { + self.inner.keepalive_tick(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn manager_workers_and_callback_registries_do_not_retain_the_client() { + let client = GridClient::new().expect("client"); + let probe = client.retention_probe(); + let manager = NetworkManager::new(client.clone()).expect("network manager"); + manager.inner.ensure_workers().expect("manager workers"); + let subscription = manager.subscribe_packet(PacketType::Default, Arc::new(|_| {}), true); + + drop(subscription); + drop(manager); + drop(client); + + assert!(probe.is_released()); + } +} diff --git a/crates/libremetaverse/src/packet_wire.rs b/crates/libremetaverse/src/packet_wire.rs index 3fbf5f2..1abdccc 100644 --- a/crates/libremetaverse/src/packet_wire.rs +++ b/crates/libremetaverse/src/packet_wire.rs @@ -367,6 +367,16 @@ pub(crate) fn ack_length(header: &Header) -> Result { } } +pub(crate) fn encode_base_packet(packet: &Packet) -> Result, Error> { + let capacity = header_length(packet.header.frequency) + .checked_add(ack_length(&packet.header)?) + .ok_or(Error::Argument)?; + let mut writer = WireWriter::with_capacity(capacity)?; + encode_header(&packet.header, &mut writer)?; + encode_acks(&packet.header, &mut writer)?; + Ok(writer.into_inner()) +} + pub(crate) fn decode_header( bytes: &[u8], position: &mut i32, diff --git a/crates/libremetaverse/src/udp_transport.rs b/crates/libremetaverse/src/udp_transport.rs index cabf28d..7b6acdc 100644 --- a/crates/libremetaverse/src/udp_transport.rs +++ b/crates/libremetaverse/src/udp_transport.rs @@ -11,6 +11,7 @@ use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::mpsc::{Receiver as AckReceiver, SyncSender as AckSender, sync_channel}; use std::sync::{Arc, Mutex}; use std::time::Duration; use tokio::net::UdpSocket; @@ -966,6 +967,7 @@ impl UDPBase { packet_type, do_zerocode, response: response_sender, + acknowledgement: None, }; tokio::select! { result = sender.send(command) => { @@ -999,6 +1001,7 @@ impl UDPBase { packet_type, do_zerocode, response, + acknowledgement: None, }) .map_err(|error| { if matches!(error, mpsc::error::TrySendError::Full(_)) { @@ -1014,6 +1017,64 @@ impl UDPBase { Ok(receiver) } + /// Queues a packet and resolves only after its reliable sequence is + /// acknowledged by the peer. The receiver disconnects if the reliable + /// packet is dropped or the transport shuts down. + pub(crate) fn try_send_packet_wait_ack( + &self, + data: Vec, + destination: SocketAddr, + packet_type: PacketType, + do_zerocode: bool, + ) -> Result, UdpTransportError> { + let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; + let (response, _queued) = oneshot::channel(); + let (acknowledgement, receiver) = sync_channel(1); + sender + .try_send(CoordinatorCommand::Packet { + data, + destination, + packet_type, + do_zerocode, + response, + acknowledgement: Some(acknowledgement), + }) + .map_err(|error| { + if matches!(error, mpsc::error::TrySendError::Full(_)) { + self.inner + .stats + .dropped_send_queue + .fetch_add(1, Ordering::Relaxed); + UdpTransportError::Backpressure + } else { + UdpTransportError::Cancelled + } + })?; + Ok(receiver) + } + + /// Queues an immediate flush of acknowledgements pending for `destination`. + /// + /// The network manager uses this for the protocol's `OldestUnacked` + /// request in `StartPingCheck`, matching `Simulator.SendAcks()` without + /// exposing coordinator state or blocking the packet callback thread. + pub(crate) fn try_flush_acks(&self, destination: SocketAddr) -> Result<(), UdpTransportError> { + let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; + sender + .try_send(CoordinatorCommand::FlushAcks { destination }) + .map_err(|error| { + if matches!(error, mpsc::error::TrySendError::Full(_)) { + self.inner + .stats + .dropped_send_queue + .fetch_add(1, Ordering::Relaxed); + UdpTransportError::Backpressure + } else { + UdpTransportError::Cancelled + } + }) + } + pub fn update_throttle(&self, throttle: AgentThrottle) -> Result<(), UdpTransportError> { let sender = self.command_sender().ok_or(UdpTransportError::NotRunning)?; sender @@ -1059,6 +1120,10 @@ enum CoordinatorCommand { packet_type: PacketType, do_zerocode: bool, response: oneshot::Sender>, + acknowledgement: Option>, + }, + FlushAcks { + destination: SocketAddr, }, UpdateThrottle(AgentThrottle), } @@ -1076,6 +1141,7 @@ struct ReliablePacket { category: UdpThrottleCategory, last_sent: Instant, resend_count: u32, + acknowledgement: Option>, } struct PeerState { @@ -1167,6 +1233,7 @@ impl CoordinatorState { destination: SocketAddr, packet_type: PacketType, do_zerocode: bool, + acknowledgement: Option>, ) -> Result<(WriteCommand, u32), UdpTransportError> { let mut data = encode_for_transport(data, do_zerocode, self.config.protocol_mtu)?; if data.len() < 6 { @@ -1198,8 +1265,11 @@ impl CoordinatorState { category, last_sent: Instant::now(), resend_count: 0, + acknowledgement, }, ); + } else if let Some(acknowledgement) = acknowledgement { + let _ = acknowledgement.try_send(()); } Ok((WriteCommand::Datagram { buffer, category }, sequence)) } @@ -1410,8 +1480,10 @@ async fn process_command( packet_type, do_zerocode, response, + acknowledgement, } => { - let result = state.prepare_packet(data, destination, packet_type, do_zerocode); + let result = + state.prepare_packet(data, destination, packet_type, do_zerocode, acknowledgement); let result = match result { Ok((write, sequence)) => writer .send(write) @@ -1422,6 +1494,9 @@ async fn process_command( }; let _ = response.send(result); } + CoordinatorCommand::FlushAcks { destination } => { + send_peer_acks(state, destination, writer).await; + } CoordinatorCommand::UpdateThrottle(throttle) => { let _ = writer.send(WriteCommand::UpdateThrottle(throttle)).await; } @@ -1489,7 +1564,11 @@ async fn process_incoming( return; }; for ack in appended_acks.into_iter().chain(standalone_acks) { - peer.need_ack.remove(&ack); + if let Some(mut reliable) = peer.need_ack.remove(&ack) + && let Some(acknowledgement) = reliable.acknowledgement.take() + { + let _ = acknowledgement.try_send(()); + } counters .acknowledgements_received .fetch_add(1, Ordering::Relaxed); @@ -1561,7 +1640,8 @@ async fn send_peer_acks( } Err(_) => return, }; - let Ok((write, _)) = state.prepare_packet(bytes, endpoint, PacketType::PacketAck, false) else { + let Ok((write, _)) = state.prepare_packet(bytes, endpoint, PacketType::PacketAck, false, None) + else { return; }; if writer.send(write).await.is_ok() { @@ -1759,7 +1839,7 @@ mod tests { let destination: SocketAddr = "127.0.0.1:13000".parse().unwrap(); let packet = vec![Helpers::MSG_RELIABLE, 0, 0, 0, 0, 0, 1]; coordinator - .prepare_packet(packet, destination, PacketType::ObjectUpdate, false) + .prepare_packet(packet, destination, PacketType::ObjectUpdate, false, None) .unwrap(); assert!(coordinator.take_resends().is_empty()); diff --git a/crates/libremetaverse/tests/network_manager.rs b/crates/libremetaverse/tests/network_manager.rs new file mode 100644 index 0000000..78029fb --- /dev/null +++ b/crates/libremetaverse/tests/network_manager.rs @@ -0,0 +1,899 @@ +use libremetaverse::interfaces::IMessage; +use libremetaverse::messages::linden::{ + EnableSimulatorMessage, EnableSimulatorMessageSimulatorInfoBlock, +}; +use libremetaverse::packets::{ + DisableSimulatorPacket, Packet, PacketAckPacket, PacketAckPacketPacketsBlock, PacketType, + RegionHandshakePacket, RegionHandshakeReplyPacket, StartPingCheckPacket, UseCircuitCodePacket, +}; +use libremetaverse::{ + CapsEventDictionary, CapsEventQueueCallback, GridClient, Helpers, NetworkManager, + NetworkManagerDisconnectType, PacketEventDictionary, Simulator, +}; +use libremetaverse_types::compat::{EventHandler, Uri}; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::mpsc::{Receiver, Sender, channel}; +use std::sync::{Arc, Barrier, Mutex, Weak}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +fn block_on(future: F) -> F::Output { + use std::task::{Context, Poll, Wake, Waker}; + + struct ThreadWake(thread::Thread); + + impl Wake for ThreadWake { + fn wake(self: Arc) { + self.0.unpark(); + } + } + + let waker = Waker::from(Arc::new(ThreadWake(thread::current()))); + let mut context = Context::from_waker(&waker); + let mut future = std::pin::pin!(future); + loop { + match future.as_mut().poll(&mut context) { + Poll::Ready(output) => return output, + Poll::Pending => thread::park(), + } + } +} + +fn loopback(port: u16) -> SocketAddr { + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port) +} + +fn simulator(client: &GridClient) -> Simulator { + Simulator::new( + client.clone(), + loopback(13000), + 0x1000, + Some(256), + Some(256), + ) + .expect("simulator") +} + +fn base_packet(packet_type: PacketType) -> Packet { + Packet::build_packet_with_packet_type(packet_type).expect("base packet") +} + +fn wait_until(mut condition: impl FnMut() -> bool) { + let deadline = Instant::now() + Duration::from_secs(2); + while !condition() { + assert!(Instant::now() < deadline, "condition timed out"); + thread::sleep(Duration::from_millis(2)); + } +} + +#[test] +fn packet_callbacks_preserve_filtering_order_async_policy_and_reentrancy() { + let client = GridClient::new().expect("client"); + let sim = simulator(&client); + let events = Arc::new(PacketEventDictionary::new(client).expect("packet events")); + let order = Arc::new(Mutex::new(Vec::new())); + + let default_order = Arc::clone(&order); + events + .register_event( + PacketType::Default, + Arc::new(move |_| default_order.lock().unwrap().push("default")), + false, + ) + .unwrap(); + let specific_order = Arc::clone(&order); + events + .register_event( + PacketType::StartPingCheck, + Arc::new(move |_| specific_order.lock().unwrap().push("specific")), + false, + ) + .unwrap(); + + events + .invoke_raise_event( + PacketType::StartPingCheck, + base_packet(PacketType::StartPingCheck), + sim.clone(), + ) + .unwrap(); + assert_eq!(*order.lock().unwrap(), ["default", "specific"]); + + order.lock().unwrap().clear(); + events + .invoke_raise_event( + PacketType::CompletePingCheck, + base_packet(PacketType::CompletePingCheck), + sim.clone(), + ) + .unwrap(); + assert_eq!(*order.lock().unwrap(), ["default"]); + + let reentrant_count = Arc::new(AtomicUsize::new(0)); + let weak_events: Weak = Arc::downgrade(&events); + let count = Arc::clone(&reentrant_count); + let reentrant: EventHandler<_> = Arc::new(move |_| { + count.fetch_add(1, Ordering::Relaxed); + if let Some(events) = weak_events.upgrade() { + events + .register_event(PacketType::UseCircuitCode, Arc::new(|_| {}), false) + .unwrap(); + } + }); + events + .register_event(PacketType::UseCircuitCode, reentrant, false) + .unwrap(); + events + .invoke_raise_event( + PacketType::UseCircuitCode, + base_packet(PacketType::UseCircuitCode), + sim.clone(), + ) + .unwrap(); + assert_eq!(reentrant_count.load(Ordering::Relaxed), 1); + + let async_events = PacketEventDictionary::new(sim.client.clone()).unwrap(); + let removed_async: EventHandler<_> = Arc::new(|_| {}); + async_events + .register_event(PacketType::StartPingCheck, Arc::clone(&removed_async), true) + .unwrap(); + let caller_thread = thread::current().id(); + let (thread_sender, thread_receiver) = channel(); + async_events + .register_event( + PacketType::StartPingCheck, + Arc::new(move |_| thread_sender.send(thread::current().id()).unwrap()), + false, + ) + .unwrap(); + async_events + .unregister_event(PacketType::StartPingCheck, removed_async) + .unwrap(); + async_events + .invoke_raise_event( + PacketType::StartPingCheck, + base_packet(PacketType::StartPingCheck), + sim, + ) + .unwrap(); + assert_ne!( + thread_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + caller_thread, + "C# keeps a packet type asynchronous after its first async registration" + ); +} + +#[test] +fn synchronous_specific_dispatch_suppresses_default_async_chain_like_reference() { + let mixed_events = PacketEventDictionary::new(GridClient::new().unwrap()).unwrap(); + let (default_sender, default_receiver) = channel(); + mixed_events + .register_event( + PacketType::Default, + Arc::new(move |_| default_sender.send(()).unwrap()), + true, + ) + .unwrap(); + let specific_calls = Arc::new(AtomicUsize::new(0)); + let calls = Arc::clone(&specific_calls); + mixed_events + .register_event( + PacketType::StartPingCheck, + Arc::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + }), + false, + ) + .unwrap(); + mixed_events + .invoke_raise_event( + PacketType::StartPingCheck, + base_packet(PacketType::StartPingCheck), + simulator(&mixed_events.client), + ) + .unwrap(); + assert_eq!(specific_calls.load(Ordering::Relaxed), 1); + assert!( + default_receiver + .recv_timeout(Duration::from_millis(50)) + .is_err(), + "C# returns after a synchronous specific chain without queuing the default async chain" + ); +} + +struct TestCapsMessage; +impl IMessage for TestCapsMessage {} + +#[test] +fn caps_callbacks_are_ordered_and_subscription_guards_unregister() { + let client = GridClient::new().expect("client"); + let sim = simulator(&client); + let events = CapsEventDictionary::new(client).expect("CAPS events"); + let order = Arc::new(Mutex::new(Vec::new())); + + let default_order = Arc::clone(&order); + let default = CapsEventQueueCallback::from_handler(move |_, _, _| { + default_order.lock().unwrap().push("default"); + }); + let specific_order = Arc::clone(&order); + let specific = CapsEventQueueCallback::from_handler(move |_, _, _| { + specific_order.lock().unwrap().push("specific"); + }); + let default_guard = events.subscribe(String::new(), default); + let specific_guard = events.subscribe("EnableSimulator".into(), specific); + + events.invoke_raise_event("EnableSimulator", &TestCapsMessage, sim.clone()); + assert_eq!(*order.lock().unwrap(), ["default", "specific"]); + + drop(specific_guard); + order.lock().unwrap().clear(); + events.invoke_raise_event("EnableSimulator", &TestCapsMessage, sim); + assert_eq!(*order.lock().unwrap(), ["default"]); + drop(default_guard); +} + +#[test] +fn concurrent_registration_and_removal_is_deterministic() { + const CALLBACKS: usize = 16; + let client = GridClient::new().expect("client"); + let sim = simulator(&client); + let events = Arc::new(PacketEventDictionary::new(client).unwrap()); + let barrier = Arc::new(Barrier::new(CALLBACKS + 1)); + let calls = Arc::new(AtomicUsize::new(0)); + let (guard_sender, guard_receiver) = channel(); + + thread::scope(|scope| { + for _ in 0..CALLBACKS { + let events = Arc::clone(&events); + let barrier = Arc::clone(&barrier); + let calls = Arc::clone(&calls); + let guard_sender = guard_sender.clone(); + scope.spawn(move || { + let guard = events.subscribe( + PacketType::StartPingCheck, + Arc::new(move |_| { + calls.fetch_add(1, Ordering::Relaxed); + }), + false, + ); + guard_sender.send(guard).unwrap(); + barrier.wait(); + }); + } + barrier.wait(); + }); + drop(guard_sender); + let guards: Vec<_> = guard_receiver.into_iter().collect(); + + events + .invoke_raise_event( + PacketType::StartPingCheck, + base_packet(PacketType::StartPingCheck), + sim.clone(), + ) + .unwrap(); + assert_eq!(calls.load(Ordering::Relaxed), CALLBACKS); + + drop(guards); + calls.store(0, Ordering::Relaxed); + events + .invoke_raise_event( + PacketType::StartPingCheck, + base_packet(PacketType::StartPingCheck), + sim, + ) + .unwrap(); + assert_eq!(calls.load(Ordering::Relaxed), 0); +} + +#[test] +fn sim_connecting_can_cancel_without_leaking_collection_state() { + let client = GridClient::new().expect("client"); + let manager = NetworkManager::new(client).expect("manager"); + let guard = manager.subscribe_sim_connecting(Arc::new(|mut args| args.set_cancel(true))); + + let connected = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + loopback(9), + 0x2000, + true, + Some(Uri("http://caps.invalid/seed".into())), + 512, + 256, + ) + .unwrap(); + + assert!(connected.is_none()); + assert!(manager.simulators.is_empty()); + assert!( + manager.connected(), + "C# marks the manager connected before raising the cancellable event" + ); + assert!(manager.current_sim().is_none()); + drop(guard); +} + +enum ServerCommand { + Ping, + Disable, + Stop, +} + +#[derive(Debug, Eq, PartialEq)] +enum ServerReport { + Ack(u32), + Circuit(u32), + HandshakeReply(u32), + Ping(u8), +} + +struct FakeServer { + endpoint: SocketAddr, + commands: Sender, + reports: Receiver, + handle: Option>, +} + +impl Drop for FakeServer { + fn drop(&mut self) { + let _ = self.commands.send(ServerCommand::Stop); + if let Some(handle) = self.handle.take() { + let _ = handle.join(); + } + } +} + +fn packet_type(bytes: &[u8]) -> Option { + let mut packet_end = i32::try_from(bytes.len()).ok()?.checked_sub(1)?; + Packet::build_packet_with_bytes_int32_bytes(bytes.to_vec(), &mut packet_end, vec![0; 8192]) + .ok() + .map(|packet| packet.type_) +} + +fn ack(sequence: u32) -> Vec { + let mut packet = PacketAckPacket::new_with_constructor().unwrap(); + packet.packets = vec![PacketAckPacketPacketsBlock { id: sequence }]; + let mut bytes = packet.to_bytes_with_method().unwrap(); + bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); + bytes +} + +fn serve_ping_command( + socket: &UdpSocket, + client_endpoint: SocketAddr, + report_sender: &Sender, + buffer: &mut [u8], +) { + let mut ping = StartPingCheckPacket::new_with_constructor().unwrap(); + ping.ping_id.ping_id = 42; + ping.ping_id.oldest_unacked = 1; + let mut bytes = ping.to_bytes_with_method().unwrap(); + bytes[0] &= !Helpers::MSG_ZEROCODED; + bytes[0] |= Helpers::MSG_RELIABLE; + bytes[1..5].copy_from_slice(&99_u32.to_be_bytes()); + socket.send_to(&bytes, client_endpoint).unwrap(); + + loop { + let (length, _) = socket.recv_from(buffer).expect("ping response"); + match packet_type(&buffer[..length]) { + Some(PacketType::PacketAck) => { + let mut position = 0; + let response = + PacketAckPacket::new_with_bytes_int32(buffer[..length].to_vec(), &mut position) + .unwrap(); + assert_eq!(response.packets.len(), 1); + report_sender + .send(ServerReport::Ack(response.packets[0].id)) + .unwrap(); + } + Some(PacketType::CompletePingCheck) => { + let mut position = 0; + let response = + libremetaverse::packets::CompletePingCheckPacket::new_with_bytes_int32( + buffer[..length].to_vec(), + &mut position, + ) + .unwrap(); + report_sender + .send(ServerReport::Ping(response.ping_id.ping_id)) + .unwrap(); + break; + } + _ => {} + } + } +} + +fn spawn_fake_server() -> FakeServer { + let socket = UdpSocket::bind(loopback(0)).expect("fake UDP server"); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let endpoint = socket.local_addr().unwrap(); + let (command_sender, command_receiver) = channel(); + let (report_sender, report_receiver) = channel(); + let handle = thread::spawn(move || { + let mut buffer = [0_u8; 4096]; + let (length, client_endpoint) = socket.recv_from(&mut buffer).expect("UseCircuitCode"); + assert_eq!( + packet_type(&buffer[..length]), + Some(PacketType::UseCircuitCode) + ); + let mut position = 0; + let use_circuit = + UseCircuitCodePacket::new_with_bytes_int32(buffer[..length].to_vec(), &mut position) + .expect("decode UseCircuitCode"); + report_sender + .send(ServerReport::Circuit(use_circuit.circuit_code.code)) + .unwrap(); + let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); + socket.send_to(&ack(sequence), client_endpoint).unwrap(); + + let mut handshake = RegionHandshakePacket::new_with_constructor().unwrap(); + handshake.region_info.sim_name = b"Fake Region".to_vec(); + let mut bytes = handshake.to_bytes_with_method().unwrap(); + bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); + socket.send_to(&bytes, client_endpoint).unwrap(); + + loop { + let (length, _) = socket.recv_from(&mut buffer).expect("handshake reply"); + let received_type = packet_type(&buffer[..length]); + if received_type == Some(PacketType::RegionHandshakeReply) { + let mut position = 0; + let mut packet_end = i32::try_from(length).unwrap() - 1; + let mut zero_buffer = vec![0_u8; 8192]; + let mut response = RegionHandshakeReplyPacket::new_with_constructor().unwrap(); + response + .from_bytes_with_bytes_int32_int32_bytes( + buffer[..length].to_vec(), + &mut position, + &mut packet_end, + Some(&mut zero_buffer), + ) + .unwrap(); + report_sender + .send(ServerReport::HandshakeReply(response.region_info.flags)) + .unwrap(); + break; + } + } + + while let Ok(command) = command_receiver.recv() { + match command { + ServerCommand::Ping => { + serve_ping_command(&socket, client_endpoint, &report_sender, &mut buffer); + } + ServerCommand::Disable => { + let disable = DisableSimulatorPacket::new_with_constructor().unwrap(); + let mut bytes = disable.to_bytes_with_method().unwrap(); + bytes[0] &= !(Helpers::MSG_RELIABLE | Helpers::MSG_ZEROCODED); + socket.send_to(&bytes, client_endpoint).unwrap(); + } + ServerCommand::Stop => break, + } + } + }); + FakeServer { + endpoint, + commands: command_sender, + reports: report_receiver, + handle: Some(handle), + } +} + +fn spawn_ack_only_server() -> (SocketAddr, JoinHandle<()>) { + let socket = UdpSocket::bind(loopback(0)).expect("ACK-only UDP server"); + socket + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + let endpoint = socket.local_addr().unwrap(); + let handle = thread::spawn(move || { + let mut buffer = [0_u8; 4096]; + let (length, client_endpoint) = socket.recv_from(&mut buffer).expect("UseCircuitCode"); + assert_eq!( + packet_type(&buffer[..length]), + Some(PacketType::UseCircuitCode) + ); + let sequence = u32::from_be_bytes(buffer[1..5].try_into().unwrap()); + socket.send_to(&ack(sequence), client_endpoint).unwrap(); + }); + (endpoint, handle) +} + +fn assert_ping_exchange(server: &FakeServer, packet_receiver: &Receiver) { + server.commands.send(ServerCommand::Ping).unwrap(); + assert_eq!( + packet_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + PacketType::StartPingCheck + ); + assert_eq!( + server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), + ServerReport::Ack(99) + ); + assert_eq!( + server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), + ServerReport::Ping(42) + ); +} + +fn assert_reuse_preserves_connected_override( + manager: &mut NetworkManager, + server: &FakeServer, + simulator: &Simulator, +) { + manager.set_connected(false); + let reused = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + server.endpoint, + simulator.handle, + false, + None, + simulator.size_x, + simulator.size_y, + ) + .unwrap() + .unwrap(); + assert_eq!(reused, simulator.clone()); + assert!( + !manager.connected(), + "C# does not alter manager Connected when reusing a connected simulator" + ); + manager.set_connected(true); +} + +#[test] +fn fake_server_drives_circuit_handshake_ping_disable_and_disconnect_reasons() { + let server = spawn_fake_server(); + let client = GridClient::new().expect("client"); + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(0x1122_3344); + let (changed_sender, changed_receiver) = channel(); + let (connected_sender, connected_receiver) = channel(); + let (packet_sender, packet_receiver) = channel(); + let (sim_disconnected_sender, sim_disconnected_receiver) = channel(); + let (disconnected_sender, disconnected_receiver) = channel(); + let guards = [ + manager.subscribe_sim_changed(Arc::new(move |args| { + changed_sender + .send(args.previous_simulator().is_none()) + .unwrap(); + })), + manager.subscribe_sim_connected(Arc::new(move |args| { + connected_sender.send(args.simulator().handle).unwrap(); + })), + manager.subscribe_sim_disconnected(Arc::new(move |args| { + sim_disconnected_sender + .send((args.reason(), args.was_connected())) + .unwrap(); + })), + manager.subscribe_disconnected(Arc::new(move |args| { + disconnected_sender.send(args.reason()).unwrap(); + })), + ]; + manager + .register_callback_with_packet_type_event_handler_boolean( + PacketType::StartPingCheck, + Arc::new(move |args| packet_sender.send(args.packet().type_).unwrap()), + false, + ) + .unwrap(); + + let seed = Uri("http://caps.invalid/seed".into()); + let sim = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + server.endpoint, + 0x1234_0000, + true, + Some(seed.clone()), + 512, + 256, + ) + .unwrap() + .expect("connected simulator"); + + assert_eq!( + server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), + ServerReport::Circuit(0x1122_3344) + ); + assert!( + changed_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap() + ); + assert_eq!( + connected_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + sim.handle + ); + assert_eq!(manager.current_sim(), Some(sim.clone())); + assert_eq!(sim.seed_capability(), Some(seed)); + assert_eq!(sim.size_x, 512); + assert_eq!(sim.size_y, 256); + wait_until(|| sim.handshake_complete()); + assert_eq!( + server.reports.recv_timeout(Duration::from_secs(1)).unwrap(), + ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) + ); + + assert_reuse_preserves_connected_override(&mut manager, &server, &sim); + + assert_ping_exchange(&server, &packet_receiver); + + server.commands.send(ServerCommand::Disable).unwrap(); + assert_eq!( + sim_disconnected_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + (NetworkManagerDisconnectType::NetworkTimeout, true) + ); + assert_eq!( + disconnected_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + NetworkManagerDisconnectType::SimShutdown + ); + assert!(manager.simulators.is_empty()); + assert!(manager.current_sim().is_none()); + assert!(!manager.connected()); + drop(guards); +} + +#[test] +fn concurrent_disconnect_emits_each_transition_once() { + const CALLERS: usize = 12; + let server = spawn_fake_server(); + let client = GridClient::new().expect("client"); + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(7); + let sim_disconnects = Arc::new(AtomicUsize::new(0)); + let global_disconnects = Arc::new(AtomicUsize::new(0)); + let sim_count = Arc::clone(&sim_disconnects); + let sim_guard = manager.subscribe_sim_disconnected(Arc::new(move |args| { + assert!(args.was_connected()); + sim_count.fetch_add(1, Ordering::Relaxed); + })); + let global_count = Arc::clone(&global_disconnects); + let global_guard = manager.subscribe_disconnected(Arc::new(move |_| { + global_count.fetch_add(1, Ordering::Relaxed); + })); + let sim = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + server.endpoint, + 9, + true, + None, + 256, + 256, + ) + .unwrap() + .unwrap(); + let manager = Arc::new(manager); + let barrier = Arc::new(Barrier::new(CALLERS)); + + thread::scope(|scope| { + for _ in 0..CALLERS { + let manager = Arc::clone(&manager); + let simulator = sim.clone(); + let barrier = Arc::clone(&barrier); + scope.spawn(move || { + barrier.wait(); + manager.disconnect_sim(simulator, false).unwrap(); + }); + } + }); + + assert_eq!(sim_disconnects.load(Ordering::Relaxed), 1); + assert_eq!(global_disconnects.load(Ordering::Relaxed), 1); + assert!(manager.simulators.is_empty()); + assert!(!manager.connected()); + drop(sim_guard); + drop(global_guard); +} + +#[test] +fn sim_disconnected_callback_can_reenter_disconnect_without_deadlock() { + let server = spawn_fake_server(); + let client = GridClient::new().expect("client"); + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(8); + let simulator = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + server.endpoint, + 10, + true, + None, + 256, + 256, + ) + .unwrap() + .unwrap(); + let manager = Arc::new(manager); + let weak_manager = Arc::downgrade(&manager); + let callback_completed = Arc::new(AtomicBool::new(false)); + let completed = Arc::clone(&callback_completed); + let guard = manager.subscribe_sim_disconnected(Arc::new(move |args| { + let manager = weak_manager.upgrade().expect("manager during callback"); + manager + .disconnect_sim(args.simulator(), false) + .expect("reentrant disconnect"); + completed.store(true, Ordering::Release); + })); + + manager.disconnect_sim(simulator, false).unwrap(); + + assert!(callback_completed.load(Ordering::Acquire)); + assert!(manager.simulators.is_empty()); + assert!(!manager.connected()); + drop(guard); +} + +#[test] +fn keepalive_requires_two_silent_intervals_before_network_timeout() { + let server = spawn_fake_server(); + let client = GridClient::new().expect("client"); + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(11); + let (disconnected_sender, disconnected_receiver) = channel(); + let guard = manager.subscribe_disconnected(Arc::new(move |args| { + disconnected_sender.send(args.reason()).unwrap(); + })); + let sim = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + server.endpoint, + 12, + true, + None, + 256, + 256, + ) + .unwrap() + .unwrap(); + wait_until(|| sim.handshake_complete()); + + manager.poll_keepalive(); + assert!(manager.connected()); + assert!( + disconnected_receiver + .recv_timeout(Duration::from_millis(50)) + .is_err() + ); + + manager.poll_keepalive(); + assert_eq!( + disconnected_receiver + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + NetworkManagerDisconnectType::NetworkTimeout + ); + assert!(!manager.connected()); + assert!(manager.current_sim().is_none()); + drop(guard); +} + +#[test] +fn enable_simulator_caps_event_adds_each_new_region_once() { + let first_server = spawn_fake_server(); + let second_server = spawn_fake_server(); + let mut client = GridClient::new().expect("client"); + client.settings().agent_settings_mut().multiple_sims = true; + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(17); + let current = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + first_server.endpoint, + 18, + true, + None, + 256, + 256, + ) + .unwrap() + .unwrap(); + + let mut message = EnableSimulatorMessage::new().unwrap(); + let mut info = EnableSimulatorMessageSimulatorInfoBlock::new().unwrap(); + info.ip = second_server.endpoint.ip(); + info.port = i32::from(second_server.endpoint.port()); + info.region_handle = 19; + info.region_size_x = 1024; + info.region_size_y = 512; + message.simulators = vec![info]; + + manager.dispatch_caps_event("EnableSimulator", &message, current.clone()); + manager.dispatch_caps_event("EnableSimulator", &message, current); + + assert_eq!(manager.simulators.len(), 2); + let added = manager + .find_simulator_with_ip_end_point(second_server.endpoint) + .unwrap() + .expect("enabled simulator"); + assert_eq!(added.handle, 19); + assert_eq!(added.size_x, 1024); + assert_eq!(added.size_y, 512); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::Circuit(17) + ); + assert_eq!( + second_server + .reports + .recv_timeout(Duration::from_secs(1)) + .unwrap(), + ServerReport::HandshakeReply(0x1 | 0x2 | 0x4) + ); + + manager + .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) + .unwrap(); +} + +#[test] +fn async_connect_and_shutdown_do_not_require_an_ambient_tokio_runtime() { + let server = spawn_fake_server(); + let client = GridClient::new().expect("client"); + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(23); + + let simulator = block_on( + manager.connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32_20e51cc0( + server.endpoint, + 24, + true, + None, + 256, + 256, + ), + ) + .unwrap() + .expect("async connection"); + + assert!(simulator.connected()); + assert_eq!(manager.current_sim(), Some(simulator)); + block_on(manager.shutdown_with_disconnect_type_string_7641243c( + NetworkManagerDisconnectType::ClientInitiated, + "ClientInitiated".into(), + )) + .unwrap(); + assert!(!manager.connected()); +} + +#[test] +fn handshake_timeout_preserves_reference_connect_result_and_shutdown_tears_down_current_sim() { + let (endpoint, server) = spawn_ack_only_server(); + let mut client = GridClient::new().expect("client"); + client.settings().timing().login_timeout = 25; + let mut manager = NetworkManager::new(client).expect("manager"); + manager.set_circuit_code(25); + let sim_disconnects = Arc::new(AtomicUsize::new(0)); + let disconnect_count = Arc::clone(&sim_disconnects); + let guard = manager.subscribe_sim_disconnected(Arc::new(move |_| { + disconnect_count.fetch_add(1, Ordering::Relaxed); + })); + + let simulator = manager + .connect_with_ip_end_point_u_int64_boolean_uri_u_int32_u_int32( + endpoint, 26, true, None, 256, 256, + ) + .unwrap() + .expect("C# returns the UDP-connected simulator after handshake timeout"); + server.join().unwrap(); + + assert!(!simulator.handshake_complete()); + assert!(manager.simulators.is_empty()); + assert_eq!(manager.current_sim(), Some(simulator.clone())); + manager + .shutdown_with_disconnect_type(NetworkManagerDisconnectType::ClientInitiated) + .unwrap(); + assert!(!simulator.connected()); + assert_eq!(sim_disconnects.load(Ordering::Relaxed), 1); + drop(guard); +} diff --git a/docs/network-manager.md b/docs/network-manager.md new file mode 100644 index 0000000..9141174 --- /dev/null +++ b/docs/network-manager.md @@ -0,0 +1,79 @@ +# Native network manager and simulator lifecycle + +`NetworkManager`, `Simulator`, `PacketEventDictionary`, and +`CapsEventDictionary` implement the native Rust boundary corresponding to the +LibreMetaverse connection and dispatch layer. They use the generated packet +codecs and the bounded `UDPBase` transport; they do not invoke .NET code or +start a helper process. + +## Ownership and execution + +A manager owns bounded incoming and outgoing channels plus one keepalive +worker. Each simulator owns a `UDPBase` transport running on a dedicated +current-thread Tokio executor so the mapped synchronous connection API does not +depend on the caller already having entered a runtime. Async connection methods +move blocking compatibility work to short-lived named standard threads and +return their result through a runtime-neutral one-shot future. Every worker +holds only a weak manager reference between operations. Dropping the last +manager and subscription guard therefore releases all manager-owned +`GridClient` handles, cancels the workers, joins their threads, and closes +simulator transports. + +The public `SimulatorCollection` is a shared snapshot collection. Lookup, +addition, removal, current-simulator selection, and disconnect transitions are +synchronized. User callbacks receive cloned handles only after internal locks +have been released. `Subscription` is an RAII guard: dropping or closing it +removes exactly its registration through a weak registry reference. + +## Dispatch semantics + +Packet callbacks preserve registration order. `PacketType::Default` handlers +are selected before handlers for the concrete packet type. Once any callback +registered for a packet type requests asynchronous dispatch, that packet type +remains asynchronous after later removals, matching the conservative C# +delegate state. A bounded worker executes each asynchronous default/specific +batch in order. Callback panics are isolated and cannot terminate a processor +or poison a callback registry. + +CAPS dispatch snapshots the default (`""`) and named callback chains, then +invokes them in that order without holding the registry lock. The built-in +`EnableSimulator` handler honors `Agent.MultipleSims`, ignores duplicate +endpoints, validates ports, and connects each newly advertised region with its +reported handle and dimensions. Incoming `DisableSimulator`, `KickUser`, +`RegionHandshake`, `StartPingCheck`, and generic-streaming UDP packets perform +their reference lifecycle actions before user dispatch. Ping replies preserve +the incoming ping identifier and immediately flush pending acknowledgements +when the simulator reports a nonzero `OldestUnacked` sequence. + +## Connection and shutdown behavior + +Connecting reuses an existing endpoint or inserts one simulator atomically, +starts the bounded manager workers, raises cancellable `SimConnecting`, sends a +reliable `UseCircuitCode`, and waits up to `Timing.LoginTimeout` for its actual +protocol ACK. Async compatibility methods use a runtime-neutral blocking +bridge, so they can be polled without an ambient Tokio runtime. Circuit-code +changes propagate to every tracked simulator. A default connection stores its +seed capability, updates `CurrentSim`, raises `SimChanged`, and then raises +`SimConnected`. A received region handshake is decoded and answered with the +reference `RegionHandshakeReply` flags before the simulator is marked complete. + +The keepalive uses the same two-interval disconnect-candidate transition as the +C# timer: traffic clears the candidate, the first silent interval marks it and +sends a ping, and the second shuts the manager down with `NetworkTimeout`. +Concurrent disconnect attempts are serialized so one simulator and one global +transition are emitted. Per-simulator removal reports `NetworkTimeout`; losing +the last simulator reports `SimShutdown`. Explicit shutdown preserves the +caller-provided reason/message and sends `CloseCircuit` only for client or +network-timeout shutdowns. + +The deterministic tests use loopback fake simulators and require no live grid: + +```sh +cargo test -p libremetaverse --test network_manager +``` + +They cover ACK-gated circuit setup, typed handshake reply/state, ping response, +CAPS enable, UDP disable, current/seed selection, callback ordering and +filtering, RAII unregistration, runtime-neutral async calls, concurrent +registration/removal, reentrant/concurrent disconnect, and keepalive timeout +reasons. diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index 0f3e26c..213bd38 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -38,6 +38,19 @@ TARGETS = { # implementations. The generated module keeps catalog markers and re-exports # the hand-written type so coverage remains deterministic. NATIVE_TYPES = { + "T:LibreMetaverse.Caps.EventQueueCallback": "crate::network_manager::CapsEventQueueCallback", + "T:LibreMetaverse.CapsEventDictionary": "crate::network_manager::CapsEventDictionary", + "T:LibreMetaverse.DisconnectedEventArgs": "crate::network_manager::DisconnectedEventArgs", + "T:LibreMetaverse.GenericStreamingMessageEventArgs": "crate::network_manager::GenericStreamingMessageEventArgs", + "T:LibreMetaverse.NetworkManager.IncomingPacket": "crate::network_manager::NetworkManagerIncomingPacket", + "T:LibreMetaverse.NetworkManager.OutgoingPacket": "crate::network_manager::NetworkManagerOutgoingPacket", + "T:LibreMetaverse.PacketEventDictionary": "crate::network_manager::PacketEventDictionary", + "T:LibreMetaverse.PacketReceivedEventArgs": "crate::network_manager::PacketReceivedEventArgs", + "T:LibreMetaverse.PacketSentEventArgs": "crate::network_manager::PacketSentEventArgs", + "T:LibreMetaverse.SimChangedEventArgs": "crate::network_manager::SimChangedEventArgs", + "T:LibreMetaverse.SimConnectedEventArgs": "crate::network_manager::SimConnectedEventArgs", + "T:LibreMetaverse.SimConnectingEventArgs": "crate::network_manager::SimConnectingEventArgs", + "T:LibreMetaverse.SimDisconnectedEventArgs": "crate::network_manager::SimDisconnectedEventArgs", "T:LibreMetaverse.ArchetypeParam": "crate::genepool_catalog::ArchetypeParam", "T:LibreMetaverse.AttentionData": "crate::attention_catalog::AttentionData", "T:LibreMetaverse.AttentionSet": "crate::attention_catalog::AttentionSet", @@ -132,7 +145,9 @@ NATIVE_TYPES = { # hand-written, while its fixed public methods remain generator-audited. NATIVE_DECLARATIONS = { "T:LibreMetaverse.GridClient": "crate::client_core::GridClient", + "T:LibreMetaverse.NetworkManager": "crate::network_manager::NetworkManager", "T:LibreMetaverse.Settings": "crate::client_core::Settings", + "T:LibreMetaverse.Simulator": "crate::network_manager::Simulator", "T:LibreMetaverse.StructuredData.OSDParser": "crate::model::OSDParser", } @@ -163,6 +178,122 @@ NATIVE_MEMBER_BODIES = { "self.shutdown().map_err(Into::into)", "M:LibreMetaverse.GridClient.ToString": "String::new()", + "E:LibreMetaverse.NetworkManager.Disconnected": + "self.native_subscribe_disconnected(handler)", + "E:LibreMetaverse.NetworkManager.GenericStreamingMessage": + "self.native_subscribe_generic_streaming_message(handler)", + "E:LibreMetaverse.NetworkManager.PacketSent": + "self.native_subscribe_packet_sent(handler)", + "E:LibreMetaverse.NetworkManager.SimChanged": + "self.native_subscribe_sim_changed(handler)", + "E:LibreMetaverse.NetworkManager.SimConnected": + "self.native_subscribe_sim_connected(handler)", + "E:LibreMetaverse.NetworkManager.SimConnecting": + "self.native_subscribe_sim_connecting(handler)", + "E:LibreMetaverse.NetworkManager.SimDisconnected": + "self.native_subscribe_sim_disconnected(handler)", + "M:LibreMetaverse.NetworkManager.#ctor(LibreMetaverse.GridClient)": + "crate::network_manager::NetworkManager::native_new(client)", + "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri)": + "self.native_connect(std::net::SocketAddr::new(ip, port), handle, set_default, seedcaps, crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X, crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y)", + "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)": + "self.native_connect(std::net::SocketAddr::new(ip, port), handle, set_default, seedcaps, size_x, size_y)", + "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri)": + "self.native_connect(end_point, handle, set_default, seedcaps, crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X, crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y)", + "M:LibreMetaverse.NetworkManager.Connect(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)": + "self.native_connect(end_point, handle, set_default, seedcaps, size_x, size_y)", + "M:LibreMetaverse.NetworkManager.ConnectAsync(System.Net.IPAddress,System.UInt16,System.UInt64,System.Boolean,System.Uri)": + "self.native_connect_async(std::net::SocketAddr::new(ip, port), handle, set_default, seedcaps, crate::network_manager::Simulator::DEFAULT_REGION_SIZE_X, crate::network_manager::Simulator::DEFAULT_REGION_SIZE_Y).await", + "M:LibreMetaverse.NetworkManager.ConnectAsync(System.Net.IPEndPoint,System.UInt64,System.Boolean,System.Uri,System.UInt32,System.UInt32)": + "self.native_connect_async(end_point, handle, set_default, seedcaps, size_x, size_y).await", + "M:LibreMetaverse.NetworkManager.DisconnectSim(LibreMetaverse.Simulator,System.Boolean)": + "self.native_disconnect_sim(simulator, send_close_circuit)", + "M:LibreMetaverse.NetworkManager.EnqueueIncoming(LibreMetaverse.NetworkManager.IncomingPacket)": + "self.native_enqueue_incoming(packet)", + "M:LibreMetaverse.NetworkManager.EnqueueOutgoing(LibreMetaverse.NetworkManager.OutgoingPacket)": + "self.native_enqueue_outgoing(packet)", + "M:LibreMetaverse.NetworkManager.FindSimulator(System.Net.IPEndPoint)": + "Ok(self.native_find_endpoint(end_point))", + "M:LibreMetaverse.NetworkManager.FindSimulator(System.UInt64)": + "Ok(self.native_find_handle(handle))", + "M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})": + "self.native_register_callback(type_, callback, true)", + "M:LibreMetaverse.NetworkManager.RegisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs},System.Boolean)": + "self.native_register_callback(type_, callback, is_async)", + "M:LibreMetaverse.NetworkManager.RegisterEventCallback(System.String,LibreMetaverse.Caps.EventQueueCallback)": + "self.native_register_caps(caps_event, callback)", + "M:LibreMetaverse.NetworkManager.SendPacket(LibreMetaverse.Packets.Packet)": + "self.native_send_packet(packet, None)", + "M:LibreMetaverse.NetworkManager.SendPacket(LibreMetaverse.Packets.Packet,LibreMetaverse.Simulator)": + "self.native_send_packet(packet, simulator)", + "M:LibreMetaverse.NetworkManager.Shutdown(LibreMetaverse.NetworkManager.DisconnectType)": + "self.native_shutdown(type_, format!(\"{type_:?}\"))", + "M:LibreMetaverse.NetworkManager.Shutdown(LibreMetaverse.NetworkManager.DisconnectType,System.String)": + "self.native_shutdown(type_, message)", + "M:LibreMetaverse.NetworkManager.ShutdownAsync(LibreMetaverse.NetworkManager.DisconnectType,System.String)": + "self.native_shutdown_async(type_, message).await", + "M:LibreMetaverse.NetworkManager.UnregisterCallback(LibreMetaverse.Packets.PacketType,System.EventHandler{LibreMetaverse.PacketReceivedEventArgs})": + "self.native_unregister_callback(type_, callback)", + "M:LibreMetaverse.NetworkManager.UnregisterEventCallback(System.String,LibreMetaverse.Caps.EventQueueCallback)": + "self.native_unregister_caps(caps_event, callback)", + "P:LibreMetaverse.NetworkManager.CircuitCode": + "self.native_circuit_code()", + "P:LibreMetaverse.NetworkManager.CircuitCode#set": + "self.native_set_circuit_code(value)", + "P:LibreMetaverse.NetworkManager.Connected": + "self.native_connected()", + "P:LibreMetaverse.NetworkManager.Connected#set": + "self.native_set_connected(value)", + "P:LibreMetaverse.NetworkManager.CurrentSim": + "self.native_current_sim()", + "P:LibreMetaverse.NetworkManager.CurrentSim#set": + "self.native_set_current_sim(value)", + "P:LibreMetaverse.NetworkManager.InboxCount": + "self.native_inbox_count()", + "P:LibreMetaverse.NetworkManager.OutboxCount": + "self.native_outbox_count()", + "M:LibreMetaverse.Simulator.#ctor(LibreMetaverse.GridClient,System.Net.IPEndPoint,System.UInt64,System.UInt32,System.UInt32)": + "crate::network_manager::Simulator::native_new(client, address, handle, size_x, size_y)", + "M:LibreMetaverse.Simulator.Connect(System.Boolean)": + "self.native_connect(move_to_sim)", + "M:LibreMetaverse.Simulator.ConnectAsync(System.Boolean)": + "self.native_connect_async(move_to_sim).await", + "M:LibreMetaverse.Simulator.Disconnect(System.Boolean)": + "self.native_disconnect(send_close_circuit)", + "M:LibreMetaverse.Simulator.Dispose": + "self.native_dispose()", + "M:LibreMetaverse.Simulator.Equals(System.Object)": + "self.native_equals(obj)", + "M:LibreMetaverse.Simulator.GetHashCode": + "self.native_hash_code()", + "M:LibreMetaverse.Simulator.Pause": + "self.native_pause()", + "M:LibreMetaverse.Simulator.Resume": + "self.native_resume()", + "M:LibreMetaverse.Simulator.SendPacket(LibreMetaverse.Packets.Packet)": + "self.native_send_packet(packet)", + "M:LibreMetaverse.Simulator.SendPacketData(System.Byte[],System.Int32,LibreMetaverse.Packets.PacketType,System.Boolean)": + "self.native_send_packet_data(data, data_length, type_, do_zerocode)", + "M:LibreMetaverse.Simulator.SendPing": + "self.native_send_ping()", + "M:LibreMetaverse.Simulator.SetSeedCaps(System.Uri,System.Boolean)": + "self.native_set_seed_caps(seedcaps, changed_sim)", + "M:LibreMetaverse.Simulator.ToString": + "self.native_to_string()", + "M:LibreMetaverse.Simulator.UseCircuitCode(System.Boolean)": + "self.native_use_circuit_code(wait_for_ack)", + "M:LibreMetaverse.Simulator.UseCircuitCodeAsync(System.Boolean)": + "self.native_use_circuit_code_async(wait_for_ack).await", + "M:LibreMetaverse.Simulator.op_Equality(LibreMetaverse.Simulator,LibreMetaverse.Simulator)": + "lhs == rhs", + "M:LibreMetaverse.Simulator.op_Inequality(LibreMetaverse.Simulator,LibreMetaverse.Simulator)": + "lhs != rhs", + "P:LibreMetaverse.Simulator.Connected": + "self.native_is_connected()", + "P:LibreMetaverse.Simulator.HandshakeComplete": + "self.native_handshake_complete()", + "P:LibreMetaverse.Simulator.IPEndPoint": + "self.native_ip_end_point()", "P:LibreMetaverse.GridClient.Settings": "&mut self.settings", "P:LibreMetaverse.GridClient.TimeProvider": @@ -337,6 +468,11 @@ CLIENT_CORE_NATIVE_OWNERS = ( "WorldSettings", ) +NETWORK_NATIVE_OWNERS = ( + "NetworkManager", + "Simulator", +) + NATIVE_OWNER_BOUNDS = { "T:LibreMetaverse.CacheDictionary`2": { "TKey": "TKey: Clone + PartialEq", @@ -477,6 +613,7 @@ NATIVE_MESSAGE_CODEC_TYPES = { OPEN_ENUM_IDS = {"T:LibreMetaverse.BakeType", "T:LibreMetaverse.HoleType"} TRAIT_SUPERTRAITS = { "T:LibreMetaverse.IBakingTextureProvider": "std::any::Any", + "T:LibreMetaverse.Interfaces.IMessage": "std::any::Any", "T:LibreMetaverse.Appearance.ICurrentOutfitPolicy": "Send + Sync", } CORE_NAMESPACES = ( @@ -1053,11 +1190,12 @@ def render_body(signature: str, member_id: str, error_model: str, asyncness: str if trait: signature = signature.removeprefix("pub ") if body := native_member_body(member_id): - marker = ( - " /* native client-core implementation */" - if any(f"LibreMetaverse.{owner}." in member_id for owner in CLIENT_CORE_NATIVE_OWNERS) - else "" - ) + if any(f"LibreMetaverse.{owner}." in member_id for owner in CLIENT_CORE_NATIVE_OWNERS): + marker = " /* native client-core implementation */" + elif any(f"LibreMetaverse.{owner}." in member_id for owner in NETWORK_NATIVE_OWNERS): + marker = " /* native network implementation */" + else: + marker = "" return f" {signature} {{{marker} {body} }}" failure = ( f"libremetaverse_types::not_implemented({json.dumps(member_id)})" diff --git a/tools/generate_rust_mapping.py b/tools/generate_rust_mapping.py index 9e9af3f..e50da89 100644 --- a/tools/generate_rust_mapping.py +++ b/tools/generate_rust_mapping.py @@ -971,7 +971,10 @@ def validate_generated_shims() -> None: if default_types - {"UUID"}: raise ValueError(f"generated shim derives a plausible Default: {path.relative_to(ROOT)}") for body in function_bodies(text): - if "native client-core implementation" in body: + if ( + "native client-core implementation" in body + or "native network implementation" in body + ): continue if FAKE_BODY.search(body): raise ValueError(f"generated shim returns a plausible fallback: {path.relative_to(ROOT)}")