diff --git a/packages/cultnet-rs/src/rudp.rs b/packages/cultnet-rs/src/rudp.rs index bf611a15..9e8c4a1c 100644 --- a/packages/cultnet-rs/src/rudp.rs +++ b/packages/cultnet-rs/src/rudp.rs @@ -574,6 +574,11 @@ impl CultNetRudpSession { payload: Vec, options: CultNetRudpSendOptions, ) -> Result { + // Measure the window as `send_many` will: without the deadline-passed + // sends it is about to reclaim. + if options.reliable { + self.purge_expired_reliable(options.now_ms); + } if options.reliable && (!self.queued_reliable.is_empty() || !self.window_admits(self.next_sequence, payload.len())) diff --git a/packages/cultnet-rs/tests/rudp_flow_window.rs b/packages/cultnet-rs/tests/rudp_flow_window.rs index 85c7a864..58065970 100644 --- a/packages/cultnet-rs/tests/rudp_flow_window.rs +++ b/packages/cultnet-rs/tests/rudp_flow_window.rs @@ -249,3 +249,34 @@ fn acknowledged_bytes_stop_counting_when_the_lowest_unacked_packet_expires() -> } Ok(()) } + +#[test] +fn a_direct_send_is_not_refused_for_bytes_whose_lowest_packet_has_expired() -> Result<()> { + let mut sender = connected(1)?; + let expiring = CultNetRudpSendOptions { + reliable: true, + now_ms: 0, + reliable_expire_after_ms: Some(100), + ..Default::default() + }; + let g = sender + .send_many("state", vec![7], expiring, None)? + .remove(0); + for offset in 1..=4u32 { + assert_eq!(send(&mut sender, MIB)?.len(), 1); + sender.receive(&ack_for(g.sequence + offset), 1)?; + } + assert!(sender.send("state", vec![7; MIB], reliable()).is_err()); + + // The first call after g's deadline is a direct send: it measures the + // window without g, as `send_many` would, and goes on the wire. + let at_200 = CultNetRudpSendOptions { + reliable: true, + now_ms: 200, + ..Default::default() + }; + let sent = sender.send("state", vec![7; MIB], at_200)?; + assert_eq!(sent.sequence, g.sequence + 5); + assert_eq!(sender.outstanding_reliable_packet_count(), 1); + Ok(()) +}