cultnet-rs: a direct send measures the window after reclaiming expired sends

`send` checked `window_admits` before `send_many` purged deadline-passed
packets, so the first direct send after the lowest unacked packet expired
was refused for acknowledged bytes that `send_many` would already have
released. `send` now purges first, as `send_many` does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 14:46:20 +02:00
commit 1833df58d4
2 changed files with 36 additions and 0 deletions

View file

@ -574,6 +574,11 @@ impl CultNetRudpSession {
payload: Vec<u8>,
options: CultNetRudpSendOptions,
) -> Result<CultNetRudpPacket> {
// 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()))

View file

@ -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(())
}