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