diff --git a/packages/cultnet-rs/src/rudp.rs b/packages/cultnet-rs/src/rudp.rs index 25382264..674b2996 100644 --- a/packages/cultnet-rs/src/rudp.rs +++ b/packages/cultnet-rs/src/rudp.rs @@ -6,7 +6,9 @@ use std::collections::VecDeque; use std::io::ErrorKind; use std::net::SocketAddr; use std::net::UdpSocket; +use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use uuid::Uuid; use crate::CultNetMessage; use crate::CultNetReconnectController; @@ -105,6 +107,28 @@ pub struct CultNetRudpSendOptions { pub now_ms: u64, } +/// Identifies every transport packet belonging to one non-expiring reliable +/// send. Receipts are local to the session that issued them and cannot survive +/// a peer reset. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CultNetRudpReliableSendReceipt { + session_scope: Uuid, + sequences: Vec, +} + +impl CultNetRudpReliableSendReceipt { + pub fn packet_sequences(&self) -> &[u32] { + &self.sequences + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CultNetRudpReliableSendStatus { + Pending, + Acknowledged, + Invalidated, +} + #[derive(Clone, Debug, PartialEq, Eq)] struct PendingReliablePacket { packet: CultNetRudpPacket, @@ -127,6 +151,7 @@ struct FragmentBuffer { } pub struct CultNetRudpSession { + session_scope: Uuid, connection_id: u32, resend_delay_ms: u64, max_pending_reliable_packets: Option, @@ -149,6 +174,7 @@ pub struct CultNetRudpSession { impl CultNetRudpSession { pub fn new(options: CultNetRudpSessionOptions) -> Self { Self { + session_scope: Uuid::new_v4(), connection_id: options.connection_id, resend_delay_ms: options.resend_delay_ms, max_pending_reliable_packets: options.max_pending_reliable_packets, @@ -181,6 +207,11 @@ impl CultNetRudpSession { self.connected } + pub fn assume_connected(&mut self, now_ms: u64) { + self.connected = true; + self.last_received_at_ms = Some(now_ms); + } + pub fn pending_reliable_sequences(&self) -> Vec { self.pending_reliable.keys().copied().collect() } @@ -202,6 +233,39 @@ impl CultNetRudpSession { Some(pending.packet.clone()) } + fn reliable_send_status( + &self, + receipt: &CultNetRudpReliableSendReceipt, + ) -> CultNetRudpReliableSendStatus { + if receipt.session_scope != self.session_scope { + return CultNetRudpReliableSendStatus::Invalidated; + } + if receipt + .sequences + .iter() + .any(|sequence| self.pending_reliable.contains_key(sequence)) + { + CultNetRudpReliableSendStatus::Pending + } else { + CultNetRudpReliableSendStatus::Acknowledged + } + } + + fn non_expiring_reliable_receipt( + &self, + packets: &[CultNetRudpPacket], + ) -> Result { + if packets.is_empty() || packets.iter().any(|packet| !packet.reliable) { + return Err(anyhow!( + "RUDP acknowledgement receipts require a non-empty reliable packet set" + )); + } + Ok(CultNetRudpReliableSendReceipt { + session_scope: self.session_scope, + sequences: packets.iter().map(|packet| packet.sequence).collect(), + }) + } + pub fn last_received_at_ms(&self) -> Option { self.last_received_at_ms } @@ -224,6 +288,7 @@ impl CultNetRudpSession { } pub fn reset_peer_state(&mut self) { + self.session_scope = Uuid::new_v4(); self.next_sequence = self.initial_sequence; self.next_fragment_id = 1; self.connected = false; @@ -1508,13 +1573,53 @@ impl CultNetRudpSocketTransportConnection { let packets = self.session .send_many(channel_id, payload, options, self.max_fragment_bytes)?; - for packet in packets { - self.send_packet(&packet)?; - } + self.send_data_packets(&packets)?; self.stats.frames_sent += 1; Ok(()) } + /// Sends one frame on a reliable channel whose packets cannot expire and + /// returns the transport receipt used to observe its exact acknowledgement. + pub fn send_reliable( + &mut self, + channel_id: &str, + payload: Vec, + ) -> Result { + let options = channel_send_options(channel_id, now_ms()); + // This lineage has no reliable-expiry concept, so every reliable channel is + // already non-expiring. Restore the expiry half of this guard if one is added. + if !options.reliable { + return Err(anyhow!( + "RUDP acknowledgement receipts require a non-expiring reliable channel" + )); + } + let packets = + self.session + .send_many(channel_id, payload, options, self.max_fragment_bytes)?; + self.send_data_packets(&packets)?; + let receipt = self.session.non_expiring_reliable_receipt(&packets)?; + self.stats.frames_sent += 1; + Ok(receipt) + } + + pub fn reliable_send_status( + &self, + receipt: &CultNetRudpReliableSendReceipt, + ) -> CultNetRudpReliableSendStatus { + self.session.reliable_send_status(receipt) + } + + fn send_data_packets(&mut self, packets: &[CultNetRudpPacket]) -> Result<()> { + let fragmented = packets.len() > 1; + for (index, packet) in packets.iter().enumerate() { + self.send_packet(packet)?; + if fragmented && (index + 1) % 16 == 0 { + thread::sleep(Duration::from_millis(1)); + } + } + Ok(()) + } + pub fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; self.send("schema", payload)