Skip to main content

aetheris_protocol/
events.rs

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