ferravex_net/
lib.rs

1//! Bounded, versioned network protocol shared by native and browser game clients.
2//!
3//! The protocol never accepts client positions or client-selected time steps.
4//! Native UDP is intentionally a thin primitive: authentication, encryption,
5//! browser transports, and NAT traversal are outside this prototype.
6pub mod delta;
7pub mod history;
8pub mod reliable;
9
10use std::collections::VecDeque;
11use std::fmt;
12
13pub const MAX_PACKET_BYTES: usize = 1200;
14pub const MAX_PLAYERS: usize = 16;
15pub const MAX_INPUTS_PER_PACKET: usize = 6;
16pub const MAX_QUEUED_INPUTS: usize = 64;
17pub const BUTTON_JUMP: u8 = 1;
18pub const BUTTON_FIRE: u8 = 2;
19pub const BUTTON_RELOAD: u8 = 4;
20pub const MAX_PITCH: f32 = 1.55;
21const MAGIC: &[u8; 4] = b"FVRX";
22/// Version 3 carries jump grace/buffer timers and held state for exact motor replay.
23pub const PROTOCOL_VERSION: u8 = 3;
24
25#[derive(Clone, Copy, Debug, Default, PartialEq)]
26pub struct InputFrame {
27    pub sequence: u32,
28    /// Strafe right in [-1, 1].
29    pub move_x: f32,
30    /// Move forward in [-1, 1]; yaw zero faces negative Z.
31    pub move_z: f32,
32    pub yaw: f32,
33    /// Positive pitch looks up.
34    pub pitch: f32,
35    pub buttons: u8,
36}
37impl InputFrame {
38    pub fn sanitized(mut self) -> Self {
39        let finite = |v: f32| if v.is_finite() { v } else { 0.0 };
40        self.move_x = finite(self.move_x).clamp(-1.0, 1.0);
41        self.move_z = finite(self.move_z).clamp(-1.0, 1.0);
42        self.yaw = (finite(self.yaw) + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU)
43            - std::f32::consts::PI;
44        self.pitch = finite(self.pitch).clamp(-MAX_PITCH, MAX_PITCH);
45        self.buttons &= BUTTON_JUMP | BUTTON_FIRE | BUTTON_RELOAD;
46        self
47    }
48    pub fn is_valid(self) -> bool {
49        self.move_x.is_finite()
50            && self.move_x.abs() <= 1.0
51            && self.move_z.is_finite()
52            && self.move_z.abs() <= 1.0
53            && self.yaw.is_finite()
54            && self.yaw.abs() <= std::f32::consts::PI
55            && self.pitch.is_finite()
56            && self.pitch.abs() <= MAX_PITCH
57            && self.buttons & !(BUTTON_JUMP | BUTTON_FIRE | BUTTON_RELOAD) == 0
58    }
59}
60
61#[derive(Clone, Debug, PartialEq)]
62pub enum ClientPacket {
63    EventAck {
64        session: u64,
65        sequence: u64,
66    },
67    Hello {
68        nonce: u64,
69    },
70    Input {
71        session: u64,
72        inputs: Vec<InputFrame>,
73    },
74}
75#[derive(Clone, Debug, PartialEq)]
76pub enum ServerPacket {
77    ReliableEvent {
78        session: u64,
79        event: reliable::EventPacket,
80    },
81    Welcome {
82        nonce: u64,
83        session: u64,
84        player_id: u8,
85        tick: u32,
86    },
87    Snapshot(Snapshot),
88}
89#[derive(Clone, Debug, Default, PartialEq)]
90pub struct Snapshot {
91    /// Session cookie prevents delayed packets from a prior connection being accepted.
92    pub session: u64,
93    pub tick: u32,
94    pub ack_sequence: u32,
95    pub ack_valid: bool,
96    pub players: Vec<PlayerSnapshot>,
97}
98#[derive(Clone, Copy, Debug, Default, PartialEq)]
99pub struct PlayerSnapshot {
100    pub id: u8,
101    pub position: [f32; 3],
102    pub velocity: [f32; 3],
103    pub yaw: f32,
104    pub pitch: f32,
105    pub health: u8,
106    pub ammo: u8,
107    pub grounded: bool,
108    pub jump_coyote: f32,
109    pub jump_buffer: f32,
110    pub jump_held: bool,
111    pub fire_cooldown: u16,
112    pub reload_ticks: u16,
113    pub respawn_ticks: u16,
114}
115impl PlayerSnapshot {
116    fn is_valid(self) -> bool {
117        (self.id as usize) < MAX_PLAYERS
118            && self
119                .position
120                .into_iter()
121                .all(|v| v.is_finite() && v.abs() <= 4096.0)
122            && self
123                .velocity
124                .into_iter()
125                .all(|v| v.is_finite() && v.abs() <= 256.0)
126            && self.yaw.is_finite()
127            && self.yaw.abs() <= std::f32::consts::PI
128            && self.pitch.is_finite()
129            && self.pitch.abs() <= MAX_PITCH
130            && self.jump_coyote.is_finite()
131            && (0. ..=0.5).contains(&self.jump_coyote)
132            && self.jump_buffer.is_finite()
133            && (0. ..=0.5).contains(&self.jump_buffer)
134            && self.health <= 100
135            && self.ammo <= 30
136            && self.fire_cooldown <= 4096
137            && self.reload_ticks <= 4096
138            && self.respawn_ticks <= 4096
139    }
140}
141
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143pub enum ProtocolError {
144    TooLarge,
145    Truncated,
146    Header,
147    Version,
148    Kind,
149    Invalid,
150    Trailing,
151}
152impl fmt::Display for ProtocolError {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        write!(f, "invalid Ferravex packet: {self:?}")
155    }
156}
157impl std::error::Error for ProtocolError {}
158
159/// RFC-1982-style comparison. Exactly half a sequence space is ambiguous and rejected.
160pub fn sequence_newer(candidate: u32, previous: u32) -> bool {
161    let delta = candidate.wrapping_sub(previous);
162    delta != 0 && delta < (1 << 31)
163}
164
165/// Deduplicates input redundancy and bounds per-client simulation backlog.
166#[derive(Debug, Default)]
167pub struct InputQueue {
168    queue: VecDeque<InputFrame>,
169    newest: Option<u32>,
170}
171impl InputQueue {
172    pub fn push(&mut self, input: InputFrame) -> bool {
173        if !input.is_valid()
174            || self.queue.len() >= MAX_QUEUED_INPUTS
175            || self
176                .newest
177                .is_some_and(|old| !sequence_newer(input.sequence, old))
178        {
179            return false;
180        }
181        self.newest = Some(input.sequence);
182        self.queue.push_back(input);
183        true
184    }
185    pub fn pop(&mut self) -> Option<InputFrame> {
186        self.queue.pop_front()
187    }
188    /// Coalesce a burst to its newest movement/look sample without losing a
189    /// queued action press. The authority still simulates exactly one fixed step.
190    /// This prevents redundant commands recovered after loss from adding permanent
191    /// control latency. Previously consumed commands cannot re-enter the queue.
192    pub fn pop_latest(&mut self) -> Option<InputFrame> {
193        let mut latest = self.queue.pop_back()?;
194        for older in self.queue.drain(..) {
195            latest.buttons |= older.buttons;
196        }
197        Some(latest)
198    }
199    pub fn len(&self) -> usize {
200        self.queue.len()
201    }
202    pub fn is_empty(&self) -> bool {
203        self.queue.is_empty()
204    }
205}
206
207fn header(kind: u8) -> Vec<u8> {
208    let mut out = Vec::with_capacity(MAX_PACKET_BYTES);
209    out.extend_from_slice(MAGIC);
210    out.extend_from_slice(&[PROTOCOL_VERSION, kind]);
211    out
212}
213fn put_u32(out: &mut Vec<u8>, n: u32) {
214    out.extend_from_slice(&n.to_le_bytes());
215}
216fn put_u64(out: &mut Vec<u8>, n: u64) {
217    out.extend_from_slice(&n.to_le_bytes());
218}
219fn put_f32(out: &mut Vec<u8>, n: f32) {
220    put_u32(out, n.to_bits());
221}
222fn put_u16(out: &mut Vec<u8>, n: u16) {
223    out.extend_from_slice(&n.to_le_bytes());
224}
225fn put_input(out: &mut Vec<u8>, input: &InputFrame) {
226    put_u32(out, input.sequence);
227    for n in [input.move_x, input.move_z, input.yaw, input.pitch] {
228        put_f32(out, n);
229    }
230    out.push(input.buttons);
231}
232
233pub fn encode_client(packet: &ClientPacket) -> Result<Vec<u8>, ProtocolError> {
234    let mut out = match packet {
235        ClientPacket::EventAck { .. } => header(6),
236        ClientPacket::Hello { .. } => header(1),
237        ClientPacket::Input { .. } => header(2),
238    };
239    match packet {
240        ClientPacket::EventAck { session, sequence } => {
241            if *session == 0 {
242                return Err(ProtocolError::Invalid);
243            }
244            put_u64(&mut out, *session);
245            put_u64(&mut out, *sequence);
246        }
247        ClientPacket::Hello { nonce } => put_u64(&mut out, *nonce),
248        ClientPacket::Input { session, inputs } => {
249            if *session == 0
250                || inputs.is_empty()
251                || inputs.len() > MAX_INPUTS_PER_PACKET
252                || inputs.iter().any(|input| !input.is_valid())
253            {
254                return Err(ProtocolError::Invalid);
255            }
256            put_u64(&mut out, *session);
257            out.push(inputs.len() as u8);
258            for input in inputs {
259                put_input(&mut out, input);
260            }
261        }
262    }
263    Ok(out)
264}
265pub fn encode_server(packet: &ServerPacket) -> Result<Vec<u8>, ProtocolError> {
266    let mut out = match packet {
267        ServerPacket::ReliableEvent { .. } => header(5),
268        ServerPacket::Welcome { .. } => header(3),
269        ServerPacket::Snapshot(_) => header(4),
270    };
271    match packet {
272        ServerPacket::ReliableEvent { session, event } => {
273            if *session == 0 || event.payload.len() > reliable::EVENT_BYTES {
274                return Err(ProtocolError::Invalid);
275            }
276            put_u64(&mut out, *session);
277            put_u64(&mut out, event.sequence);
278            put_u16(&mut out, event.payload.len() as u16);
279            out.extend_from_slice(&event.payload);
280        }
281        ServerPacket::Welcome {
282            nonce,
283            session,
284            player_id,
285            tick,
286        } => {
287            if *session == 0 || *player_id as usize >= MAX_PLAYERS {
288                return Err(ProtocolError::Invalid);
289            }
290            put_u64(&mut out, *nonce);
291            put_u64(&mut out, *session);
292            out.push(*player_id);
293            put_u32(&mut out, *tick);
294        }
295        ServerPacket::Snapshot(snapshot) => {
296            if snapshot.session == 0 || snapshot.players.len() > MAX_PLAYERS {
297                return Err(ProtocolError::Invalid);
298            }
299            let mut ids = 0u16;
300            for p in &snapshot.players {
301                if !p.is_valid() || ids & (1u16 << p.id) != 0 {
302                    return Err(ProtocolError::Invalid);
303                }
304                ids |= 1u16 << p.id;
305            }
306            put_u64(&mut out, snapshot.session);
307            put_u32(&mut out, snapshot.tick);
308            put_u32(&mut out, snapshot.ack_sequence);
309            out.push(snapshot.ack_valid as u8);
310            out.push(snapshot.players.len() as u8);
311            for p in &snapshot.players {
312                out.push(p.id);
313                for n in p
314                    .position
315                    .into_iter()
316                    .chain(p.velocity)
317                    .chain([p.yaw, p.pitch])
318                {
319                    put_f32(&mut out, n);
320                }
321                out.extend_from_slice(&[p.health, p.ammo, p.grounded as u8]);
322                put_f32(&mut out, p.jump_coyote);
323                put_f32(&mut out, p.jump_buffer);
324                out.push(p.jump_held as u8);
325                put_u16(&mut out, p.fire_cooldown);
326                put_u16(&mut out, p.reload_ticks);
327                put_u16(&mut out, p.respawn_ticks);
328            }
329        }
330    }
331    if out.len() > MAX_PACKET_BYTES {
332        return Err(ProtocolError::TooLarge);
333    }
334    Ok(out)
335}
336
337struct Reader<'a> {
338    data: &'a [u8],
339    pos: usize,
340}
341impl<'a> Reader<'a> {
342    fn new(data: &'a [u8]) -> Result<(Self, u8), ProtocolError> {
343        if data.len() > MAX_PACKET_BYTES {
344            return Err(ProtocolError::TooLarge);
345        }
346        if data.len() < 6 {
347            return Err(ProtocolError::Truncated);
348        }
349        if &data[..4] != MAGIC {
350            return Err(ProtocolError::Header);
351        }
352        if data[4] != PROTOCOL_VERSION {
353            return Err(ProtocolError::Version);
354        }
355        Ok((Self { data, pos: 6 }, data[5]))
356    }
357    fn bytes<const N: usize>(&mut self) -> Result<[u8; N], ProtocolError> {
358        let bytes = self
359            .data
360            .get(self.pos..self.pos + N)
361            .ok_or(ProtocolError::Truncated)?;
362        self.pos += N;
363        Ok(bytes.try_into().expect("slice length checked"))
364    }
365    fn u8(&mut self) -> Result<u8, ProtocolError> {
366        Ok(self.bytes::<1>()?[0])
367    }
368    fn u16(&mut self) -> Result<u16, ProtocolError> {
369        Ok(u16::from_le_bytes(self.bytes()?))
370    }
371    fn u32(&mut self) -> Result<u32, ProtocolError> {
372        Ok(u32::from_le_bytes(self.bytes()?))
373    }
374    fn u64(&mut self) -> Result<u64, ProtocolError> {
375        Ok(u64::from_le_bytes(self.bytes()?))
376    }
377    fn f32(&mut self) -> Result<f32, ProtocolError> {
378        Ok(f32::from_bits(self.u32()?))
379    }
380    fn boolean(&mut self) -> Result<bool, ProtocolError> {
381        match self.u8()? {
382            0 => Ok(false),
383            1 => Ok(true),
384            _ => Err(ProtocolError::Invalid),
385        }
386    }
387    fn end(self) -> Result<(), ProtocolError> {
388        if self.pos == self.data.len() {
389            Ok(())
390        } else {
391            Err(ProtocolError::Trailing)
392        }
393    }
394    fn input(&mut self) -> Result<InputFrame, ProtocolError> {
395        let frame = InputFrame {
396            sequence: self.u32()?,
397            move_x: self.f32()?,
398            move_z: self.f32()?,
399            yaw: self.f32()?,
400            pitch: self.f32()?,
401            buttons: self.u8()?,
402        };
403        if !frame.is_valid() {
404            return Err(ProtocolError::Invalid);
405        }
406        Ok(frame)
407    }
408}
409pub fn decode_client(data: &[u8]) -> Result<ClientPacket, ProtocolError> {
410    let (mut reader, kind) = Reader::new(data)?;
411    let packet = match kind {
412        6 => {
413            let session = reader.u64()?;
414            let sequence = reader.u64()?;
415            if session == 0 {
416                return Err(ProtocolError::Invalid);
417            }
418            ClientPacket::EventAck { session, sequence }
419        }
420        1 => ClientPacket::Hello {
421            nonce: reader.u64()?,
422        },
423        2 => {
424            let session = reader.u64()?;
425            let count = reader.u8()? as usize;
426            if session == 0 || count == 0 || count > MAX_INPUTS_PER_PACKET {
427                return Err(ProtocolError::Invalid);
428            }
429            let mut inputs = Vec::with_capacity(count);
430            for _ in 0..count {
431                inputs.push(reader.input()?);
432            }
433            ClientPacket::Input { session, inputs }
434        }
435        _ => return Err(ProtocolError::Kind),
436    };
437    reader.end()?;
438    Ok(packet)
439}
440pub fn decode_server(data: &[u8]) -> Result<ServerPacket, ProtocolError> {
441    let (mut r, kind) = Reader::new(data)?;
442    let packet = match kind {
443        5 => {
444            let session = r.u64()?;
445            let sequence = r.u64()?;
446            let len = r.u16()? as usize;
447            if session == 0 || len > reliable::EVENT_BYTES {
448                return Err(ProtocolError::Invalid);
449            }
450            let payload = r
451                .data
452                .get(r.pos..r.pos + len)
453                .ok_or(ProtocolError::Truncated)?
454                .to_vec();
455            r.pos += len;
456            ServerPacket::ReliableEvent {
457                session,
458                event: reliable::EventPacket { sequence, payload },
459            }
460        }
461        3 => {
462            let nonce = r.u64()?;
463            let session = r.u64()?;
464            let player_id = r.u8()?;
465            let tick = r.u32()?;
466            if session == 0 || player_id as usize >= MAX_PLAYERS {
467                return Err(ProtocolError::Invalid);
468            }
469            ServerPacket::Welcome {
470                nonce,
471                session,
472                player_id,
473                tick,
474            }
475        }
476        4 => {
477            let session = r.u64()?;
478            if session == 0 {
479                return Err(ProtocolError::Invalid);
480            }
481            let tick = r.u32()?;
482            let ack_sequence = r.u32()?;
483            let ack_valid = r.boolean()?;
484            let count = r.u8()? as usize;
485            if count > MAX_PLAYERS {
486                return Err(ProtocolError::Invalid);
487            }
488            let mut players = Vec::with_capacity(count);
489            let mut ids = 0u16;
490            for _ in 0..count {
491                let p = PlayerSnapshot {
492                    id: r.u8()?,
493                    position: [r.f32()?, r.f32()?, r.f32()?],
494                    velocity: [r.f32()?, r.f32()?, r.f32()?],
495                    yaw: r.f32()?,
496                    pitch: r.f32()?,
497                    health: r.u8()?,
498                    ammo: r.u8()?,
499                    grounded: r.boolean()?,
500                    jump_coyote: r.f32()?,
501                    jump_buffer: r.f32()?,
502                    jump_held: r.boolean()?,
503                    fire_cooldown: r.u16()?,
504                    reload_ticks: r.u16()?,
505                    respawn_ticks: r.u16()?,
506                };
507                if !p.is_valid() || ids & (1u16 << p.id) != 0 {
508                    return Err(ProtocolError::Invalid);
509                }
510                ids |= 1u16 << p.id;
511                players.push(p);
512            }
513            ServerPacket::Snapshot(Snapshot {
514                session,
515                tick,
516                ack_sequence,
517                ack_valid,
518                players,
519            })
520        }
521        _ => return Err(ProtocolError::Kind),
522    };
523    r.end()?;
524    Ok(packet)
525}
526
527/// Native sockets are optional by target; packet codecs remain available on wasm.
528#[cfg(not(target_arch = "wasm32"))]
529pub mod udp {
530    use super::MAX_PACKET_BYTES;
531    use std::{
532        io,
533        net::{SocketAddr, ToSocketAddrs, UdpSocket},
534    };
535    pub struct Socket {
536        inner: UdpSocket,
537    }
538    impl Socket {
539        pub fn bind(addr: impl ToSocketAddrs) -> io::Result<Self> {
540            let inner = UdpSocket::bind(addr)?;
541            inner.set_nonblocking(true)?;
542            Ok(Self { inner })
543        }
544        pub fn connect(addr: impl ToSocketAddrs) -> io::Result<Self> {
545            let remote = addr.to_socket_addrs()?.next().ok_or_else(|| {
546                io::Error::new(io::ErrorKind::InvalidInput, "empty remote address")
547            })?;
548            let this = Self::bind(if remote.is_ipv4() {
549                "0.0.0.0:0"
550            } else {
551                "[::]:0"
552            })?;
553            this.inner.connect(remote)?;
554            Ok(this)
555        }
556        pub fn local_addr(&self) -> io::Result<SocketAddr> {
557            self.inner.local_addr()
558        }
559        pub fn send(&self, bytes: &[u8]) -> io::Result<usize> {
560            check_size(bytes)?;
561            self.inner.send(bytes)
562        }
563        pub fn send_to(&self, bytes: &[u8], addr: SocketAddr) -> io::Result<usize> {
564            check_size(bytes)?;
565            self.inner.send_to(bytes, addr)
566        }
567        /// The extra byte detects oversized/truncated UDP datagrams. Pass the
568        /// result to the codec, which rejects lengths above MAX_PACKET_BYTES.
569        /// Windows discards oversized datagrams and returns InvalidData; callers
570        /// should count those as rejected packets and continue their poll budget.
571        /// Only an empty socket returns None.
572        pub fn recv_from(
573            &self,
574            buffer: &mut [u8; MAX_PACKET_BYTES + 1],
575        ) -> io::Result<Option<(usize, SocketAddr)>> {
576            match self.inner.recv_from(buffer) {
577                Ok(pair) => Ok(Some(pair)),
578                Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
579                #[cfg(windows)]
580                Err(e) if e.raw_os_error() == Some(10040) => Err(io::Error::new(
581                    io::ErrorKind::InvalidData,
582                    "oversized UDP datagram discarded by Winsock",
583                )),
584                Err(e) => Err(e),
585            }
586        }
587    }
588    fn check_size(bytes: &[u8]) -> io::Result<()> {
589        if bytes.len() > MAX_PACKET_BYTES {
590            Err(io::Error::new(
591                io::ErrorKind::InvalidInput,
592                "packet exceeds MTU budget",
593            ))
594        } else {
595            Ok(())
596        }
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603    #[test]
604    fn codec_round_trip_and_budget() {
605        let frame = InputFrame {
606            sequence: u32::MAX,
607            move_x: 0.5,
608            move_z: -1.0,
609            yaw: 2.0,
610            pitch: 0.1,
611            buttons: 7,
612        };
613        for packet in [
614            ClientPacket::Hello { nonce: 123 },
615            ClientPacket::Input {
616                session: 99,
617                inputs: vec![frame; MAX_INPUTS_PER_PACKET],
618            },
619        ] {
620            assert_eq!(
621                decode_client(&encode_client(&packet).unwrap()).unwrap(),
622                packet
623            );
624        }
625        let players = (0..MAX_PLAYERS)
626            .map(|id| PlayerSnapshot {
627                id: id as u8,
628                health: 100,
629                ammo: 30,
630                jump_coyote: 0.1,
631                jump_buffer: 0.12,
632                jump_held: true,
633                ..Default::default()
634            })
635            .collect();
636        let packet = ServerPacket::Snapshot(Snapshot {
637            session: 99,
638            tick: 44,
639            ack_valid: true,
640            ack_sequence: 3,
641            players,
642        });
643        let bytes = encode_server(&packet).unwrap();
644        assert_eq!(bytes.len(), 840);
645        assert!(bytes.len() < MAX_PACKET_BYTES);
646        assert_eq!(decode_server(&bytes).unwrap(), packet);
647        for length in 0..bytes.len() {
648            assert!(decode_server(&bytes[..length]).is_err());
649        }
650    }
651    #[test]
652    fn jump_state_numeric_and_version_limits_fail_closed() {
653        let player = PlayerSnapshot {
654            health: 100,
655            ammo: 30,
656            ..Default::default()
657        };
658        let make = |p| {
659            ServerPacket::Snapshot(Snapshot {
660                session: 1,
661                players: vec![p],
662                ..Default::default()
663            })
664        };
665        for bad in [f32::NAN, f32::INFINITY, -0.01, 0.51] {
666            assert!(
667                encode_server(&make(PlayerSnapshot {
668                    jump_coyote: bad,
669                    ..player
670                }))
671                .is_err()
672            );
673            assert!(
674                encode_server(&make(PlayerSnapshot {
675                    jump_buffer: bad,
676                    ..player
677                }))
678                .is_err()
679            );
680        }
681        let mut bytes = encode_server(&make(player)).unwrap();
682        bytes[4] = 2;
683        assert_eq!(decode_server(&bytes), Err(ProtocolError::Version));
684    }
685    #[test]
686    fn reliable_event_wire_is_bounded_session_bound_and_versioned() {
687        let ack = ClientPacket::EventAck {
688            session: 77,
689            sequence: 123,
690        };
691        assert_eq!(decode_client(&encode_client(&ack).unwrap()).unwrap(), ack);
692        let packet = ServerPacket::ReliableEvent {
693            session: 77,
694            event: reliable::EventPacket {
695                sequence: 123,
696                payload: vec![4; reliable::EVENT_BYTES],
697            },
698        };
699        let bytes = encode_server(&packet).unwrap();
700        assert!(bytes.len() <= MAX_PACKET_BYTES);
701        assert_eq!(decode_server(&bytes).unwrap(), packet);
702        for n in 0..bytes.len() {
703            assert!(decode_server(&bytes[..n]).is_err());
704        }
705        let mut old = bytes.clone();
706        old[4] = 1;
707        assert_eq!(decode_server(&old), Err(ProtocolError::Version));
708        assert!(
709            encode_client(&ClientPacket::EventAck {
710                session: 0,
711                sequence: 1
712            })
713            .is_err()
714        );
715        assert!(
716            encode_server(&ServerPacket::ReliableEvent {
717                session: 77,
718                event: reliable::EventPacket {
719                    sequence: 0,
720                    payload: vec![0; reliable::EVENT_BYTES + 1]
721                }
722            })
723            .is_err()
724        );
725    }
726    #[test]
727    fn reject_bad_numeric_and_trailing_data() {
728        let p = ClientPacket::Input {
729            session: 3,
730            inputs: vec![InputFrame {
731                move_x: f32::NAN,
732                ..Default::default()
733            }],
734        };
735        assert!(encode_client(&p).is_err());
736        let mut bytes = encode_client(&ClientPacket::Hello { nonce: 5 }).unwrap();
737        bytes.push(0);
738        assert_eq!(decode_client(&bytes), Err(ProtocolError::Trailing));
739        let frame = InputFrame {
740            pitch: f32::INFINITY,
741            ..Default::default()
742        };
743        assert!(frame.sanitized().is_valid());
744    }
745    #[test]
746    fn queue_is_bounded_deduplicated_and_wrap_safe() {
747        assert!(sequence_newer(0, u32::MAX));
748        assert!(!sequence_newer(u32::MAX, 0));
749        assert!(!sequence_newer(1 << 31, 0));
750        let mut q = InputQueue::default();
751        assert!(q.push(InputFrame {
752            sequence: u32::MAX,
753            ..Default::default()
754        }));
755        assert!(q.push(InputFrame::default()));
756        assert!(!q.push(InputFrame::default()));
757        for seq in 1..MAX_QUEUED_INPUTS as u32 {
758            q.push(InputFrame {
759                sequence: seq,
760                ..Default::default()
761            });
762        }
763        assert_eq!(q.len(), MAX_QUEUED_INPUTS);
764        assert!(!q.push(InputFrame {
765            sequence: 200,
766            ..Default::default()
767        }));
768    }
769    #[test]
770    fn coalescing_keeps_latest_movement_and_recovers_action_press() {
771        let mut queue = InputQueue::default();
772        queue.push(InputFrame {
773            sequence: 1,
774            move_z: 1.,
775            buttons: BUTTON_FIRE,
776            ..Default::default()
777        });
778        queue.push(InputFrame {
779            sequence: 2,
780            move_x: 1.,
781            ..Default::default()
782        });
783        let input = queue.pop_latest().unwrap();
784        assert_eq!(input.sequence, 2);
785        assert_eq!(input.move_z, 0.);
786        assert_eq!(input.move_x, 1.);
787        assert_eq!(input.buttons, BUTTON_FIRE);
788        assert!(queue.is_empty());
789        assert!(!queue.push(InputFrame {
790            sequence: 1,
791            buttons: BUTTON_FIRE,
792            ..Default::default()
793        }));
794    }
795    #[test]
796    fn arbitrary_bytes_never_panic() {
797        let mut state = 0x12345678u32;
798        for len in 0..=MAX_PACKET_BYTES + 5 {
799            let bytes: Vec<_> = (0..len)
800                .map(|_| {
801                    state ^= state << 13;
802                    state ^= state >> 17;
803                    state ^= state << 5;
804                    state as u8
805                })
806                .collect();
807            let _ = decode_client(&bytes);
808            let _ = decode_server(&bytes);
809        }
810    }
811    #[cfg(not(target_arch = "wasm32"))]
812    #[test]
813    fn native_udp_actual_socket_round_trip() {
814        use std::{thread, time::Duration};
815        let server = udp::Socket::bind("127.0.0.1:0").unwrap();
816        let client = udp::Socket::connect(server.local_addr().unwrap()).unwrap();
817        let bytes = encode_client(&ClientPacket::Hello { nonce: 88 }).unwrap();
818        client.send(&bytes).unwrap();
819        let mut buffer = [0; MAX_PACKET_BYTES + 1];
820        for _ in 0..100 {
821            if let Some((n, peer)) = server.recv_from(&mut buffer).unwrap() {
822                assert_eq!(
823                    decode_client(&buffer[..n]).unwrap(),
824                    ClientPacket::Hello { nonce: 88 }
825                );
826                server.send_to(&buffer[..n], peer).unwrap();
827                for _ in 0..100 {
828                    if let Some((n, _)) = client.recv_from(&mut buffer).unwrap() {
829                        assert_eq!(
830                            decode_client(&buffer[..n]).unwrap(),
831                            ClientPacket::Hello { nonce: 88 }
832                        );
833                        return;
834                    }
835                    thread::sleep(Duration::from_millis(1));
836                }
837                panic!("loopback UDP response not received");
838            }
839            thread::sleep(Duration::from_millis(1));
840        }
841        panic!("loopback UDP packet not received");
842    }
843}