Skip to main content

libretro_core/
netplay.rs

1//! Netpacket session and packet wrappers.
2//!
3//! Netplay callbacks combine event-like notifications with accept/reject
4//! decisions, so they remain explicit `Core` trait methods while packet sending
5//! is scoped through `NetpacketSession`.
6
7use crate::raw;
8use std::ffi::CString;
9use std::ptr;
10
11#[derive(Clone, Copy, Debug)]
12pub struct NetpacketSession {
13    client_id: NetplayClientId,
14    send: raw::retro_netpacket_send_t,
15    poll_receive: raw::retro_netpacket_poll_receive_t,
16}
17
18impl NetpacketSession {
19    pub(crate) const fn new(
20        client_id: NetplayClientId,
21        send: raw::retro_netpacket_send_t,
22        poll_receive: raw::retro_netpacket_poll_receive_t,
23    ) -> Option<Self> {
24        if send.is_some() {
25            Some(Self {
26                client_id,
27                send,
28                poll_receive,
29            })
30        } else {
31            None
32        }
33    }
34
35    pub const fn client_id(self) -> NetplayClientId {
36        self.client_id
37    }
38
39    pub const fn can_poll_receive(self) -> bool {
40        self.poll_receive.is_some()
41    }
42
43    pub fn send(self, target: NetpacketTarget, flags: NetpacketFlags, data: &[u8]) {
44        let Some(send) = self.send else {
45            return;
46        };
47        let data_ptr = if data.is_empty() {
48            ptr::null()
49        } else {
50            data.as_ptr().cast()
51        };
52        unsafe { send(flags.as_raw(), data_ptr, data.len(), target.as_raw()) };
53    }
54
55    pub fn flush(self, target: NetpacketTarget) {
56        self.send(
57            target,
58            NetpacketFlags::reliable().with_flush_hint(true),
59            &[],
60        );
61    }
62
63    pub fn poll_receive(self) -> bool {
64        let Some(poll_receive) = self.poll_receive else {
65            return false;
66        };
67        unsafe { poll_receive() };
68        true
69    }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub struct Netpacket<'a> {
74    pub client_id: NetplayClientId,
75    pub data: &'a [u8],
76}
77
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
79pub struct NetplayClientId(u16);
80
81impl NetplayClientId {
82    pub const fn new(id: u16) -> Self {
83        Self(id)
84    }
85
86    pub const fn host() -> Self {
87        Self(0)
88    }
89
90    pub const fn as_raw(self) -> u16 {
91        self.0
92    }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
96pub enum NetpacketTarget {
97    Client(NetplayClientId),
98    Broadcast,
99}
100
101impl NetpacketTarget {
102    pub const fn as_raw(self) -> u16 {
103        match self {
104            Self::Client(client) => client.as_raw(),
105            Self::Broadcast => raw::RETRO_NETPACKET_BROADCAST,
106        }
107    }
108}
109
110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
111pub enum NetpacketDelivery {
112    #[default]
113    Unreliable,
114    Reliable,
115    Unsequenced,
116}
117
118#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
119pub struct NetpacketFlags {
120    delivery: NetpacketDelivery,
121    flush: bool,
122}
123
124pub(crate) struct NetpacketInterfaceStorage {
125    _protocol_version: Option<CString>,
126    pub(crate) raw: raw::retro_netpacket_callback,
127}
128
129impl NetpacketInterfaceStorage {
130    pub(crate) fn new(protocol_version: Option<&str>) -> Self {
131        let protocol_version = protocol_version.map(crate::sanitize_cstring);
132        let protocol_version_ptr = protocol_version
133            .as_ref()
134            .map(|version| version.as_ptr())
135            .unwrap_or(ptr::null());
136        Self {
137            _protocol_version: protocol_version,
138            raw: raw::retro_netpacket_callback {
139                start: Some(crate::netpacket_start_trampoline),
140                receive: Some(crate::netpacket_receive_trampoline),
141                stop: Some(crate::netpacket_stop_trampoline),
142                poll: Some(crate::netpacket_poll_trampoline),
143                connected: Some(crate::netpacket_connected_trampoline),
144                disconnected: Some(crate::netpacket_disconnected_trampoline),
145                protocol_version: protocol_version_ptr,
146            },
147        }
148    }
149}
150
151impl NetpacketFlags {
152    pub const fn unreliable() -> Self {
153        Self {
154            delivery: NetpacketDelivery::Unreliable,
155            flush: false,
156        }
157    }
158
159    pub const fn reliable() -> Self {
160        Self {
161            delivery: NetpacketDelivery::Reliable,
162            flush: false,
163        }
164    }
165
166    pub const fn unsequenced() -> Self {
167        Self {
168            delivery: NetpacketDelivery::Unsequenced,
169            flush: false,
170        }
171    }
172
173    pub const fn with_flush_hint(mut self, flush: bool) -> Self {
174        self.flush = flush;
175        self
176    }
177
178    pub const fn delivery(self) -> NetpacketDelivery {
179        self.delivery
180    }
181
182    pub const fn flush_hint(self) -> bool {
183        self.flush
184    }
185
186    pub const fn as_raw(self) -> i32 {
187        let delivery = match self.delivery {
188            NetpacketDelivery::Unreliable => raw::RETRO_NETPACKET_UNRELIABLE,
189            NetpacketDelivery::Reliable => raw::RETRO_NETPACKET_RELIABLE,
190            NetpacketDelivery::Unsequenced => raw::RETRO_NETPACKET_UNSEQUENCED,
191        };
192        if self.flush {
193            delivery | raw::RETRO_NETPACKET_FLUSH_HINT
194        } else {
195            delivery
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn netpacket_flags_encode_valid_delivery_modes() {
206        assert_eq!(NetpacketFlags::unreliable().as_raw(), 0);
207        assert_eq!(
208            NetpacketFlags::reliable().with_flush_hint(true).as_raw(),
209            raw::RETRO_NETPACKET_RELIABLE | raw::RETRO_NETPACKET_FLUSH_HINT
210        );
211        assert_eq!(
212            NetpacketFlags::unsequenced().as_raw(),
213            raw::RETRO_NETPACKET_UNSEQUENCED
214        );
215    }
216
217    #[test]
218    fn netpacket_target_preserves_broadcast_constant() {
219        assert_eq!(
220            NetpacketTarget::Broadcast.as_raw(),
221            raw::RETRO_NETPACKET_BROADCAST
222        );
223        assert_eq!(NetpacketTarget::Client(NetplayClientId::new(7)).as_raw(), 7);
224    }
225
226    #[test]
227    fn netpacket_session_sends_and_flushes_with_typed_targets() {
228        unsafe extern "C" fn capture_send(
229            flags: i32,
230            buf: *const std::ffi::c_void,
231            len: usize,
232            client_id: u16,
233        ) {
234            assert_eq!(
235                flags,
236                raw::RETRO_NETPACKET_RELIABLE | raw::RETRO_NETPACKET_FLUSH_HINT
237            );
238            assert!(buf.is_null());
239            assert_eq!(len, 0);
240            assert_eq!(client_id, 7);
241        }
242
243        let session = NetpacketSession::new(NetplayClientId::host(), Some(capture_send), None)
244            .expect("send callback should create session");
245        session.flush(NetpacketTarget::Client(NetplayClientId::new(7)));
246        assert!(!session.poll_receive());
247    }
248}