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
79impl From<ReplicationEvent> for ComponentUpdate {
80    fn from(event: ReplicationEvent) -> Self {
81        Self {
82            network_id: event.network_id,
83            component_kind: event.component_kind,
84            payload: event.payload,
85            tick: event.tick,
86        }
87    }
88}
89
90/// Events produced by `GameTransport::poll_events()`.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub enum NetworkEvent {
93    /// A new client has connected and been assigned a `ClientId`.
94    ClientConnected(ClientId),
95    /// A client has disconnected (graceful or timeout).
96    ClientDisconnected(ClientId),
97    /// Raw unreliable data received from a client.
98    UnreliableMessage {
99        /// The client that sent the message.
100        client_id: ClientId,
101        /// The raw message bytes.
102        #[serde(with = "serde_bytes")]
103        data: Vec<u8>,
104    },
105    /// Raw reliable data received from a client.
106    ReliableMessage {
107        /// The client that sent the message.
108        client_id: ClientId,
109        /// The raw message bytes.
110        #[serde(with = "serde_bytes")]
111        data: Vec<u8>,
112    },
113    /// A heartbeat ping from a client.
114    Ping {
115        /// The client that sent the ping.
116        client_id: ClientId,
117        /// The client's tick/timestamp when the ping was sent.
118        tick: u64,
119    },
120    /// A heartbeat pong from the server.
121    Pong {
122        /// The original tick/timestamp from the ping.
123        tick: u64,
124    },
125    /// A session authentication request from the client.
126    Auth {
127        /// The session token obtained from the Control Plane.
128        session_token: String,
129    },
130    /// A WebTransport session was closed by the remote or due to error.
131    SessionClosed(ClientId),
132    /// A WebTransport stream was reset.
133    StreamReset(ClientId),
134    /// A fragment of a larger message.
135    Fragment {
136        /// The client that sent the fragment.
137        client_id: ClientId,
138        /// The fragment data.
139        fragment: FragmentedEvent,
140    },
141    /// A testing command to trigger a stress test (Phase 1/Playground only).
142    StressTest {
143        /// The client that requested the stress test.
144        client_id: ClientId,
145        /// Number of entities to spawn.
146        count: u16,
147        /// Whether spawned entities should rotate.
148        rotate: bool,
149    },
150    /// A testing command to spawn a specific entity (Phase 1/Playground only).
151    Spawn {
152        /// The client that requested the spawn.
153        client_id: ClientId,
154        /// Which entity type to spawn.
155        entity_type: u16,
156        /// Position X
157        x: f32,
158        /// Position Y
159        y: f32,
160        /// Initial rotation
161        rot: f32,
162    },
163    /// A command to clear all entities from the world (Phase 1/Playground only).
164    ClearWorld {
165        /// The client that requested the clear.
166        client_id: ClientId,
167    },
168    /// Client requests to start a gameplay session: spawns the session ship and grants Possession.
169    StartSession {
170        /// The client starting the session.
171        client_id: ClientId,
172    },
173    /// A request from a client to receive the current system manifest.
174    RequestSystemManifest {
175        /// The client that requested the manifest.
176        client_id: ClientId,
177    },
178    /// A local event indicating the client transport has been disconnected.
179    Disconnected(ClientId),
180    /// A discrete game event (e.g. depletion, destruction).
181    GameEvent {
182        /// The client involved (or targeted).
183        client_id: ClientId,
184        /// The event data.
185        event: GameEvent,
186    },
187    /// A batch of replication updates sent together to save bandwidth/packets.
188    ReplicationBatch {
189        /// The client that should receive the batch.
190        client_id: ClientId,
191        /// The collection of updates.
192        events: Vec<ReplicationEvent>,
193    },
194}
195
196impl NetworkEvent {
197    /// Returns true if this event is capable of being sent over the wire.
198    #[must_use]
199    pub const fn is_wire(&self) -> bool {
200        match self {
201            Self::Ping { .. }
202            | Self::Pong { .. }
203            | Self::Auth { .. }
204            | Self::Fragment { .. }
205            | Self::StressTest { .. }
206            | Self::Spawn { .. }
207            | Self::ClearWorld { .. }
208            | Self::StartSession { .. }
209            | Self::RequestSystemManifest { .. }
210            | Self::GameEvent { .. }
211            | Self::ReplicationBatch { .. } => true,
212            Self::ClientConnected(_)
213            | Self::ClientDisconnected(_)
214            | Self::UnreliableMessage { .. }
215            | Self::ReliableMessage { .. }
216            | Self::SessionClosed(_)
217            | Self::StreamReset(_)
218            | Self::Disconnected(_) => false,
219        }
220    }
221}
222
223/// A restricted view of `NetworkEvent` for over-the-wire transport.
224/// Prevents local-only variants (like `ClientConnected`) from being sent/received.
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub enum WireEvent {
227    /// A heartbeat ping.
228    Ping {
229        /// The client's tick/timestamp when the ping was sent.
230        tick: u64,
231    },
232    /// A heartbeat pong.
233    Pong {
234        /// The original tick/timestamp from the ping.
235        tick: u64,
236    },
237    /// A session authentication request.
238    Auth {
239        /// The session token.
240        session_token: String,
241    },
242    /// A fragment of a larger message.
243    Fragment(FragmentedEvent),
244    /// A testing command to trigger a stress test.
245    StressTest {
246        /// Number of entities to spawn.
247        count: u16,
248        /// Whether spawned entities should rotate.
249        rotate: bool,
250    },
251    /// A testing command to spawn a specific entity.
252    Spawn {
253        /// Which entity type to spawn.
254        entity_type: u16,
255        /// Position X
256        x: f32,
257        /// Position Y
258        y: f32,
259        /// Initial rotation
260        rot: f32,
261    },
262    /// A command to clear all entities from the world.
263    ClearWorld,
264    /// Client requests to start a gameplay session: spawns the session ship and grants Possession.
265    StartSession,
266    /// A request to receive the current system manifest.
267    RequestSystemManifest,
268    /// A discrete game event.
269    GameEvent(GameEvent),
270    /// A batch of replication updates.
271    ReplicationBatch(Vec<ReplicationEvent>),
272}
273
274impl WireEvent {
275    /// Converts a `WireEvent` into a `NetworkEvent` for a specific client context.
276    #[must_use]
277    pub fn into_network_event(self, client_id: crate::types::ClientId) -> NetworkEvent {
278        match self {
279            Self::Ping { tick } => NetworkEvent::Ping { client_id, tick },
280            Self::Pong { tick } => NetworkEvent::Pong { tick },
281            Self::Auth { session_token } => NetworkEvent::Auth { session_token },
282            Self::Fragment(fragment) => NetworkEvent::Fragment {
283                client_id,
284                fragment,
285            },
286            Self::StressTest { count, rotate } => NetworkEvent::StressTest {
287                client_id,
288                count,
289                rotate,
290            },
291            Self::Spawn {
292                entity_type,
293                x,
294                y,
295                rot,
296            } => NetworkEvent::Spawn {
297                client_id,
298                entity_type,
299                x,
300                y,
301                rot,
302            },
303            Self::ClearWorld => NetworkEvent::ClearWorld { client_id },
304            Self::StartSession => NetworkEvent::StartSession { client_id },
305            Self::RequestSystemManifest => NetworkEvent::RequestSystemManifest { client_id },
306            Self::GameEvent(event) => NetworkEvent::GameEvent { client_id, event },
307            Self::ReplicationBatch(events) => NetworkEvent::ReplicationBatch { client_id, events },
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    #[test]
317    fn test_network_event_is_wire() {
318        assert!(
319            NetworkEvent::Ping {
320                client_id: ClientId(1),
321                tick: 100
322            }
323            .is_wire()
324        );
325        assert!(
326            NetworkEvent::GameEvent {
327                client_id: ClientId(1),
328                event: GameEvent::AsteroidDepleted {
329                    network_id: NetworkId(1)
330                }
331            }
332            .is_wire()
333        );
334        assert!(
335            NetworkEvent::ReplicationBatch {
336                client_id: ClientId(1),
337                events: vec![]
338            }
339            .is_wire()
340        );
341        assert!(!NetworkEvent::ClientConnected(ClientId(1)).is_wire());
342        assert!(!NetworkEvent::ClientDisconnected(ClientId(1)).is_wire());
343    }
344
345    #[test]
346    fn test_wire_event_conversion_roundtrip() {
347        let wire = WireEvent::GameEvent(GameEvent::AsteroidDepleted {
348            network_id: NetworkId(42),
349        });
350        let client_id = ClientId(7);
351        let network = wire.clone().into_network_event(client_id);
352
353        if let NetworkEvent::GameEvent {
354            client_id: cid,
355            event,
356        } = network
357        {
358            assert_eq!(cid, client_id);
359            assert_eq!(
360                event,
361                GameEvent::AsteroidDepleted {
362                    network_id: NetworkId(42)
363                }
364            );
365        } else {
366            panic!("Conversion failed to preserve GameEvent variant");
367        }
368
369        // Test ReplicationBatch conversion
370        let event = ReplicationEvent {
371            network_id: NetworkId(1),
372            component_kind: ComponentKind(1),
373            payload: vec![1, 2, 3],
374            tick: 100,
375        };
376        let batch_wire = WireEvent::ReplicationBatch(vec![event.clone()]);
377        let batch_network = batch_wire.into_network_event(client_id);
378        if let NetworkEvent::ReplicationBatch {
379            client_id: cid,
380            events,
381        } = batch_network
382        {
383            assert_eq!(cid, client_id);
384            assert!(!events.is_empty());
385            assert_eq!(events[0].tick, 100);
386            assert_eq!(events[0].payload, vec![1, 2, 3]);
387            assert_eq!(events[0].network_id, NetworkId(1));
388            assert_eq!(events[0].component_kind, ComponentKind(1));
389        } else {
390            panic!("Conversion failed to preserve ReplicationBatch variant");
391        }
392    }
393}