Skip to main content

aetheris_protocol/
events.rs

1use crate::types::{ClientId, ComponentKind, NetworkId};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5/// A reliable discrete game event (Phase 1 / VS-02).
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub enum GameEvent {
8    /// An asteroid was completely depleted of its ore.
9    AsteroidDepleted {
10        /// The network ID of the asteroid that was depleted.
11        network_id: NetworkId,
12    },
13    /// Explicitly informs a client that they now own/control a specific entity.
14    Possession {
15        /// The network ID of the entity now owned by the client.
16        network_id: NetworkId,
17    },
18    /// Sends extensible server-side metadata (versions, counters, debug data).
19    SystemManifest {
20        /// The collection of metadata key-value pairs.
21        manifest: BTreeMap<String, String>,
22    },
23}
24
25impl GameEvent {
26    /// Converts a `GameEvent` into a `WireEvent`.
27    #[must_use]
28    pub fn into_wire_event(self) -> WireEvent {
29        WireEvent::GameEvent(self)
30    }
31}
32
33/// An event representing a fragment of a larger message.
34/// Used for MTU stability to prevent packet drops and enable reassembly.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct FragmentedEvent {
37    /// Unique identifier for the fragmented message.
38    pub message_id: u32,
39    /// The index of this fragment (0-based).
40    pub fragment_index: u16,
41    /// Total number of fragments for this message.
42    pub total_fragments: u16,
43    /// The raw payload of this fragment.
44    #[serde(with = "serde_bytes")]
45    pub payload: Vec<u8>,
46}
47
48/// An event representing a change to a single component on a single entity.
49/// Produced by `WorldState::extract_deltas()` on the server.
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct ReplicationEvent {
52    /// Which entity changed.
53    pub network_id: NetworkId,
54    /// Which component type changed.
55    pub component_kind: ComponentKind,
56    /// The serialized delta payload (only the changed fields).
57    /// In Phase 1, this is a full snapshot per component for simplicity.
58    #[serde(with = "serde_bytes")]
59    pub payload: Vec<u8>,
60    /// The server tick at which this change was recorded.
61    pub tick: u64,
62}
63
64/// An inbound update to be applied to the ECS.
65/// Produced by `Encoder::decode()` on the receiving end.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct ComponentUpdate {
68    /// The entity to update.
69    pub network_id: NetworkId,
70    /// Which component type to update.
71    pub component_kind: ComponentKind,
72    /// The deserialized field values.
73    #[serde(with = "serde_bytes")]
74    pub payload: Vec<u8>,
75    /// The tick this update originated from.
76    pub tick: u64,
77}
78
79/// Events produced by `GameTransport::poll_events()`.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub enum NetworkEvent {
82    /// A new client has connected and been assigned a `ClientId`.
83    ClientConnected(ClientId),
84    /// A client has disconnected (graceful or timeout).
85    ClientDisconnected(ClientId),
86    /// Raw unreliable data received from a client.
87    UnreliableMessage {
88        /// The client that sent the message.
89        client_id: ClientId,
90        /// The raw message bytes.
91        #[serde(with = "serde_bytes")]
92        data: Vec<u8>,
93    },
94    /// Raw reliable data received from a client.
95    ReliableMessage {
96        /// The client that sent the message.
97        client_id: ClientId,
98        /// The raw message bytes.
99        #[serde(with = "serde_bytes")]
100        data: Vec<u8>,
101    },
102    /// A heartbeat ping from a client.
103    Ping {
104        /// The client that sent the ping.
105        client_id: ClientId,
106        /// The client's tick/timestamp when the ping was sent.
107        tick: u64,
108    },
109    /// A heartbeat pong from the server.
110    Pong {
111        /// The original tick/timestamp from the ping.
112        tick: u64,
113    },
114    /// A session authentication request from the client.
115    Auth {
116        /// The session token obtained from the Control Plane.
117        session_token: String,
118    },
119    /// A WebTransport session was closed by the remote or due to error.
120    SessionClosed(ClientId),
121    /// A WebTransport stream was reset.
122    StreamReset(ClientId),
123    /// A fragment of a larger message.
124    Fragment {
125        /// The client that sent the fragment.
126        client_id: ClientId,
127        /// The fragment data.
128        fragment: FragmentedEvent,
129    },
130    /// A testing command to trigger a stress test (Phase 1/Playground only).
131    StressTest {
132        /// The client that requested the stress test.
133        client_id: ClientId,
134        /// Number of entities to spawn.
135        count: u16,
136        /// Whether spawned entities should rotate.
137        rotate: bool,
138    },
139    /// A testing command to spawn a specific entity (Phase 1/Playground only).
140    Spawn {
141        /// The client that requested the spawn.
142        client_id: ClientId,
143        /// Which entity type to spawn.
144        entity_type: u16,
145        /// Position X
146        x: f32,
147        /// Position Y
148        y: f32,
149        /// Initial rotation
150        rot: f32,
151    },
152    /// A command to clear all entities from the world (Phase 1/Playground only).
153    ClearWorld {
154        /// The client that requested the clear.
155        client_id: ClientId,
156    },
157    /// Client requests to start a gameplay session: spawns the session ship and grants Possession.
158    StartSession {
159        /// The client starting the session.
160        client_id: ClientId,
161    },
162    /// A request from a client to receive the current system manifest.
163    RequestSystemManifest {
164        /// The client that requested the manifest.
165        client_id: ClientId,
166    },
167    /// A local event indicating the client transport has been disconnected.
168    Disconnected(ClientId),
169    /// A discrete game event (e.g. depletion, destruction).
170    GameEvent {
171        /// The client involved (or targeted).
172        client_id: ClientId,
173        /// The event data.
174        event: GameEvent,
175    },
176    /// A batch of replication updates sent together to save bandwidth/packets.
177    ReplicationBatch {
178        /// The client that should receive the batch.
179        client_id: ClientId,
180        /// The collection of updates.
181        events: Vec<ReplicationEvent>,
182    },
183}
184
185impl NetworkEvent {
186    /// Returns true if this event is capable of being sent over the wire.
187    #[must_use]
188    pub const fn is_wire(&self) -> bool {
189        match self {
190            Self::Ping { .. }
191            | Self::Pong { .. }
192            | Self::Auth { .. }
193            | Self::Fragment { .. }
194            | Self::StressTest { .. }
195            | Self::Spawn { .. }
196            | Self::ClearWorld { .. }
197            | Self::StartSession { .. }
198            | Self::RequestSystemManifest { .. }
199            | Self::GameEvent { .. }
200            | Self::ReplicationBatch { .. } => true,
201            Self::ClientConnected(_)
202            | Self::ClientDisconnected(_)
203            | Self::UnreliableMessage { .. }
204            | Self::ReliableMessage { .. }
205            | Self::SessionClosed(_)
206            | Self::StreamReset(_)
207            | Self::Disconnected(_) => false,
208        }
209    }
210}
211
212/// A restricted view of `NetworkEvent` for over-the-wire transport.
213/// Prevents local-only variants (like `ClientConnected`) from being sent/received.
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub enum WireEvent {
216    /// A heartbeat ping.
217    Ping {
218        /// The client's tick/timestamp when the ping was sent.
219        tick: u64,
220    },
221    /// A heartbeat pong.
222    Pong {
223        /// The original tick/timestamp from the ping.
224        tick: u64,
225    },
226    /// A session authentication request.
227    Auth {
228        /// The session token.
229        session_token: String,
230    },
231    /// A fragment of a larger message.
232    Fragment(FragmentedEvent),
233    /// A testing command to trigger a stress test.
234    StressTest {
235        /// Number of entities to spawn.
236        count: u16,
237        /// Whether spawned entities should rotate.
238        rotate: bool,
239    },
240    /// A testing command to spawn a specific entity.
241    Spawn {
242        /// Which entity type to spawn.
243        entity_type: u16,
244        /// Position X
245        x: f32,
246        /// Position Y
247        y: f32,
248        /// Initial rotation
249        rot: f32,
250    },
251    /// A command to clear all entities from the world.
252    ClearWorld,
253    /// Client requests to start a gameplay session: spawns the session ship and grants Possession.
254    StartSession,
255    /// A request to receive the current system manifest.
256    RequestSystemManifest,
257    /// A discrete game event.
258    GameEvent(GameEvent),
259    /// A batch of replication updates.
260    ReplicationBatch(Vec<ReplicationEvent>),
261}
262
263impl WireEvent {
264    /// Converts a `WireEvent` into a `NetworkEvent` for a specific client context.
265    #[must_use]
266    pub fn into_network_event(self, client_id: crate::types::ClientId) -> NetworkEvent {
267        match self {
268            Self::Ping { tick } => NetworkEvent::Ping { client_id, tick },
269            Self::Pong { tick } => NetworkEvent::Pong { tick },
270            Self::Auth { session_token } => NetworkEvent::Auth { session_token },
271            Self::Fragment(fragment) => NetworkEvent::Fragment {
272                client_id,
273                fragment,
274            },
275            Self::StressTest { count, rotate } => NetworkEvent::StressTest {
276                client_id,
277                count,
278                rotate,
279            },
280            Self::Spawn {
281                entity_type,
282                x,
283                y,
284                rot,
285            } => NetworkEvent::Spawn {
286                client_id,
287                entity_type,
288                x,
289                y,
290                rot,
291            },
292            Self::ClearWorld => NetworkEvent::ClearWorld { client_id },
293            Self::StartSession => NetworkEvent::StartSession { client_id },
294            Self::RequestSystemManifest => NetworkEvent::RequestSystemManifest { client_id },
295            Self::GameEvent(event) => NetworkEvent::GameEvent { client_id, event },
296            Self::ReplicationBatch(events) => NetworkEvent::ReplicationBatch { client_id, events },
297        }
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    #[test]
306    fn test_network_event_is_wire() {
307        assert!(
308            NetworkEvent::Ping {
309                client_id: ClientId(1),
310                tick: 100
311            }
312            .is_wire()
313        );
314        assert!(
315            NetworkEvent::GameEvent {
316                client_id: ClientId(1),
317                event: GameEvent::AsteroidDepleted {
318                    network_id: NetworkId(1)
319                }
320            }
321            .is_wire()
322        );
323        assert!(
324            NetworkEvent::ReplicationBatch {
325                client_id: ClientId(1),
326                events: vec![]
327            }
328            .is_wire()
329        );
330        assert!(!NetworkEvent::ClientConnected(ClientId(1)).is_wire());
331        assert!(!NetworkEvent::ClientDisconnected(ClientId(1)).is_wire());
332    }
333
334    #[test]
335    fn test_wire_event_conversion_roundtrip() {
336        let wire = WireEvent::GameEvent(GameEvent::AsteroidDepleted {
337            network_id: NetworkId(42),
338        });
339        let client_id = ClientId(7);
340        let network = wire.clone().into_network_event(client_id);
341
342        if let NetworkEvent::GameEvent {
343            client_id: cid,
344            event,
345        } = network
346        {
347            assert_eq!(cid, client_id);
348            assert_eq!(
349                event,
350                GameEvent::AsteroidDepleted {
351                    network_id: NetworkId(42)
352                }
353            );
354        } else {
355            panic!("Conversion failed to preserve GameEvent variant");
356        }
357
358        // Test ReplicationBatch conversion
359        let event = ReplicationEvent {
360            network_id: NetworkId(1),
361            component_kind: ComponentKind(1),
362            payload: vec![1, 2, 3],
363            tick: 100,
364        };
365        let batch_wire = WireEvent::ReplicationBatch(vec![event.clone()]);
366        let batch_network = batch_wire.into_network_event(client_id);
367        if let NetworkEvent::ReplicationBatch {
368            client_id: cid,
369            events,
370        } = batch_network
371        {
372            assert_eq!(cid, client_id);
373            assert!(!events.is_empty());
374            assert_eq!(events[0].tick, 100);
375            assert_eq!(events[0].payload, vec![1, 2, 3]);
376            assert_eq!(events[0].network_id, NetworkId(1));
377            assert_eq!(events[0].component_kind, ComponentKind(1));
378        } else {
379            panic!("Conversion failed to preserve ReplicationBatch variant");
380        }
381    }
382}