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}
177
178impl NetworkEvent {
179    /// Returns true if this event is capable of being sent over the wire.
180    #[must_use]
181    pub const fn is_wire(&self) -> bool {
182        match self {
183            Self::Ping { .. }
184            | Self::Pong { .. }
185            | Self::Auth { .. }
186            | Self::Fragment { .. }
187            | Self::StressTest { .. }
188            | Self::Spawn { .. }
189            | Self::ClearWorld { .. }
190            | Self::StartSession { .. }
191            | Self::RequestSystemManifest { .. }
192            | Self::GameEvent { .. } => true,
193            Self::ClientConnected(_)
194            | Self::ClientDisconnected(_)
195            | Self::UnreliableMessage { .. }
196            | Self::ReliableMessage { .. }
197            | Self::SessionClosed(_)
198            | Self::StreamReset(_)
199            | Self::Disconnected(_) => false,
200        }
201    }
202}
203
204/// A restricted view of `NetworkEvent` for over-the-wire transport.
205/// Prevents local-only variants (like `ClientConnected`) from being sent/received.
206#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
207pub enum WireEvent {
208    /// A heartbeat ping.
209    Ping {
210        /// The client's tick/timestamp when the ping was sent.
211        tick: u64,
212    },
213    /// A heartbeat pong.
214    Pong {
215        /// The original tick/timestamp from the ping.
216        tick: u64,
217    },
218    /// A session authentication request.
219    Auth {
220        /// The session token.
221        session_token: String,
222    },
223    /// A fragment of a larger message.
224    Fragment(FragmentedEvent),
225    /// A testing command to trigger a stress test.
226    StressTest {
227        /// Number of entities to spawn.
228        count: u16,
229        /// Whether spawned entities should rotate.
230        rotate: bool,
231    },
232    /// A testing command to spawn a specific entity.
233    Spawn {
234        /// Which entity type to spawn.
235        entity_type: u16,
236        /// Position X
237        x: f32,
238        /// Position Y
239        y: f32,
240        /// Initial rotation
241        rot: f32,
242    },
243    /// A command to clear all entities from the world.
244    ClearWorld,
245    /// Client requests to start a gameplay session: spawns the session ship and grants Possession.
246    StartSession,
247    /// A request to receive the current system manifest.
248    RequestSystemManifest,
249    /// A discrete game event.
250    GameEvent(GameEvent),
251}
252
253impl WireEvent {
254    /// Converts a `WireEvent` into a `NetworkEvent` for a specific client context.
255    #[must_use]
256    pub fn into_network_event(self, client_id: crate::types::ClientId) -> NetworkEvent {
257        match self {
258            Self::Ping { tick } => NetworkEvent::Ping { client_id, tick },
259            Self::Pong { tick } => NetworkEvent::Pong { tick },
260            Self::Auth { session_token } => NetworkEvent::Auth { session_token },
261            Self::Fragment(fragment) => NetworkEvent::Fragment {
262                client_id,
263                fragment,
264            },
265            Self::StressTest { count, rotate } => NetworkEvent::StressTest {
266                client_id,
267                count,
268                rotate,
269            },
270            Self::Spawn {
271                entity_type,
272                x,
273                y,
274                rot,
275            } => NetworkEvent::Spawn {
276                client_id,
277                entity_type,
278                x,
279                y,
280                rot,
281            },
282            Self::ClearWorld => NetworkEvent::ClearWorld { client_id },
283            Self::StartSession => NetworkEvent::StartSession { client_id },
284            Self::RequestSystemManifest => NetworkEvent::RequestSystemManifest { client_id },
285            Self::GameEvent(event) => NetworkEvent::GameEvent { client_id, event },
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_network_event_is_wire() {
296        assert!(
297            NetworkEvent::Ping {
298                client_id: ClientId(1),
299                tick: 100
300            }
301            .is_wire()
302        );
303        assert!(
304            NetworkEvent::GameEvent {
305                client_id: ClientId(1),
306                event: GameEvent::AsteroidDepleted {
307                    network_id: NetworkId(1)
308                }
309            }
310            .is_wire()
311        );
312        assert!(!NetworkEvent::ClientConnected(ClientId(1)).is_wire());
313        assert!(!NetworkEvent::ClientDisconnected(ClientId(1)).is_wire());
314    }
315
316    #[test]
317    fn test_wire_event_conversion_roundtrip() {
318        let wire = WireEvent::GameEvent(GameEvent::AsteroidDepleted {
319            network_id: NetworkId(42),
320        });
321        let client_id = ClientId(7);
322        let network = wire.clone().into_network_event(client_id);
323
324        if let NetworkEvent::GameEvent {
325            client_id: cid,
326            event,
327        } = network
328        {
329            assert_eq!(cid, client_id);
330            assert_eq!(
331                event,
332                GameEvent::AsteroidDepleted {
333                    network_id: NetworkId(42)
334                }
335            );
336        } else {
337            panic!("Conversion failed to preserve GameEvent variant");
338        }
339    }
340}