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 platform event (Phase 1 / VS-02).
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub enum PlatformEvent {
8    /// A resource was completely exhausted of its payload.
9    ResourceExhausted {
10        /// The network ID of the resource that was exhausted.
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    WorkspaceManifest {
20        /// The collection of metadata key-value pairs.
21        manifest: BTreeMap<String, String>,
22    },
23    /// Interaction (e.g. damage) applied to an entity.
24    Interaction {
25        source: NetworkId,
26        target: NetworkId,
27        amount: u16,
28    },
29    /// An entity has been terminated.
30    Termination { target: NetworkId },
31    /// An entity has been reinitialized.
32    Reinitialization { target: NetworkId, x: f32, y: f32 },
33    /// A data drop was collected by an agent.
34    PayloadCollected {
35        /// The network ID of the data drop that was collected.
36        network_id: NetworkId,
37        /// The amount of payload collected.
38        amount: u16,
39    },
40}
41
42impl PlatformEvent {
43    /// Converts a `PlatformEvent` into a `WireEvent`.
44    #[must_use]
45    pub fn into_wire_event(self) -> WireEvent {
46        WireEvent::PlatformEvent(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 `PlatformTransport::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 new entity has been spawned in the world.
143    EntitySpawned {
144        /// The client involved in the spawn (or targeted).
145        client_id: ClientId,
146        /// The unique network identifier for the entity.
147        network_id: NetworkId,
148        /// The kind/type of entity being spawned.
149        kind: u16,
150    },
151    /// An entity has been despawned from the world.
152    EntityDespawned {
153        /// The client involved in the despawn (or targeted).
154        client_id: ClientId,
155        /// The network identifier of the entity to remove.
156        network_id: NetworkId,
157    },
158    /// A session authentication request from the client.
159    Auth {
160        /// The session token obtained from the Control Plane.
161        session_token: String,
162    },
163    /// A WebTransport session was closed by the remote or due to error.
164    SessionClosed(ClientId),
165    /// A WebTransport stream was reset.
166    StreamReset(ClientId),
167    /// A fragment of a larger message.
168    Fragment {
169        /// The client that sent the fragment.
170        client_id: ClientId,
171        /// The fragment data.
172        fragment: FragmentedEvent,
173    },
174    /// A testing command to trigger a stress test (Phase 1/Playground only).
175    StressTest {
176        /// The client that requested the stress test.
177        client_id: ClientId,
178        /// Number of entities to spawn.
179        count: u16,
180        /// Whether spawned entities should rotate.
181        rotate: bool,
182    },
183    /// A testing command to spawn a specific entity (Phase 1/Playground only).
184    Spawn {
185        /// The client that requested the spawn.
186        client_id: ClientId,
187        /// Which entity type to spawn.
188        entity_type: u16,
189        /// Position X
190        x: f32,
191        /// Position Y
192        y: f32,
193        /// Initial rotation
194        rot: f32,
195    },
196    /// A command to clear all entities from the world (Phase 1/Playground only).
197    ClearWorld {
198        /// The client that requested the clear.
199        client_id: ClientId,
200    },
201    /// Client requests to start a gameplay session: spawns the session agent and grants Possession.
202    StartSession {
203        /// The client starting the session.
204        client_id: ClientId,
205    },
206    /// A request from a client to receive the current workspace manifest.
207    RequestWorkspaceManifest {
208        /// The client that requested the manifest.
209        client_id: ClientId,
210    },
211    /// A local event indicating the client transport has been disconnected.
212    Disconnected(ClientId),
213    /// A discrete platform event (e.g. exhaustion, termination).
214    PlatformEvent {
215        /// The client involved (or targeted).
216        client_id: ClientId,
217        /// The event data.
218        event: PlatformEvent,
219    },
220    /// A batch of replication updates sent together to save bandwidth/packets.
221    ReplicationBatch {
222        /// The client that should receive the batch.
223        client_id: ClientId,
224        /// The collection of updates.
225        events: Vec<ReplicationEvent>,
226    },
227}
228
229impl NetworkEvent {
230    /// Returns true if this event is capable of being sent over the wire.
231    #[must_use]
232    pub const fn is_wire(&self) -> bool {
233        match self {
234            Self::Ping { .. }
235            | Self::Pong { .. }
236            | Self::Auth { .. }
237            | Self::Fragment { .. }
238            | Self::StressTest { .. }
239            | Self::Spawn { .. }
240            | Self::ClearWorld { .. }
241            | Self::StartSession { .. }
242            | Self::RequestWorkspaceManifest { .. }
243            | Self::EntitySpawned { .. }
244            | Self::EntityDespawned { .. }
245            | Self::PlatformEvent { .. }
246            | Self::ReplicationBatch { .. } => true,
247            Self::ClientConnected(_)
248            | Self::ClientDisconnected(_)
249            | Self::UnreliableMessage { .. }
250            | Self::ReliableMessage { .. }
251            | Self::SessionClosed(_)
252            | Self::StreamReset(_)
253            | Self::Disconnected(_) => false,
254        }
255    }
256}
257
258/// A restricted view of `NetworkEvent` for over-the-wire transport.
259/// Prevents local-only variants (like `ClientConnected`) from being sent/received.
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub enum WireEvent {
262    /// A heartbeat ping.
263    Ping {
264        /// The client's tick/timestamp when the ping was sent.
265        tick: u64,
266    },
267    /// A heartbeat pong.
268    Pong {
269        /// The original tick/timestamp from the ping.
270        tick: u64,
271    },
272    /// A session authentication request.
273    Auth {
274        /// The session token.
275        session_token: String,
276    },
277    /// A fragment of a larger message.
278    Fragment(FragmentedEvent),
279    /// A testing command to trigger a stress test.
280    StressTest {
281        /// Number of entities to spawn.
282        count: u16,
283        /// Whether spawned entities should rotate.
284        rotate: bool,
285    },
286    /// A testing command to spawn a specific entity.
287    Spawn {
288        /// Which entity type to spawn.
289        entity_type: u16,
290        /// Position X
291        x: f32,
292        /// Position Y
293        y: f32,
294        /// Initial rotation
295        rot: f32,
296    },
297    /// A command to clear all entities from the world.
298    ClearWorld,
299    /// A new entity has been spawned.
300    EntitySpawned {
301        /// The entity ID.
302        network_id: NetworkId,
303        /// The entity type.
304        kind: u16,
305    },
306    /// An entity has been despawned.
307    EntityDespawned {
308        /// The entity ID.
309        network_id: NetworkId,
310    },
311    /// Client requests to start a gameplay session: spawns the session agent and grants Possession.
312    StartSession,
313    /// A request to receive the current workspace manifest.
314    RequestWorkspaceManifest,
315    /// A discrete platform event.
316    PlatformEvent(PlatformEvent),
317    /// A batch of replication updates.
318    ReplicationBatch(Vec<ReplicationEvent>),
319}
320
321impl WireEvent {
322    /// Converts a `WireEvent` into a `NetworkEvent` for a specific client context.
323    #[must_use]
324    pub fn into_network_event(self, client_id: crate::types::ClientId) -> NetworkEvent {
325        match self {
326            Self::Ping { tick } => NetworkEvent::Ping { client_id, tick },
327            Self::Pong { tick } => NetworkEvent::Pong { tick },
328            Self::Auth { session_token } => NetworkEvent::Auth { session_token },
329            Self::Fragment(fragment) => NetworkEvent::Fragment {
330                client_id,
331                fragment,
332            },
333            Self::StressTest { count, rotate } => NetworkEvent::StressTest {
334                client_id,
335                count,
336                rotate,
337            },
338            Self::Spawn {
339                entity_type,
340                x,
341                y,
342                rot,
343            } => NetworkEvent::Spawn {
344                client_id,
345                entity_type,
346                x,
347                y,
348                rot,
349            },
350            Self::ClearWorld => NetworkEvent::ClearWorld { client_id },
351            Self::StartSession => NetworkEvent::StartSession { client_id },
352            Self::RequestWorkspaceManifest => NetworkEvent::RequestWorkspaceManifest { client_id },
353            Self::PlatformEvent(event) => NetworkEvent::PlatformEvent { client_id, event },
354            Self::EntitySpawned { network_id, kind } => NetworkEvent::EntitySpawned {
355                client_id,
356                network_id,
357                kind,
358            },
359            Self::EntityDespawned { network_id } => NetworkEvent::EntityDespawned {
360                client_id,
361                network_id,
362            },
363            Self::ReplicationBatch(events) => NetworkEvent::ReplicationBatch { client_id, events },
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_network_event_is_wire() {
374        assert!(
375            NetworkEvent::Ping {
376                client_id: ClientId(1),
377                tick: 100
378            }
379            .is_wire()
380        );
381        assert!(
382            NetworkEvent::PlatformEvent {
383                client_id: ClientId(1),
384                event: PlatformEvent::ResourceExhausted {
385                    network_id: NetworkId(1)
386                }
387            }
388            .is_wire()
389        );
390        assert!(
391            NetworkEvent::ReplicationBatch {
392                client_id: ClientId(1),
393                events: vec![]
394            }
395            .is_wire()
396        );
397        assert!(!NetworkEvent::ClientConnected(ClientId(1)).is_wire());
398        assert!(!NetworkEvent::ClientDisconnected(ClientId(1)).is_wire());
399        assert!(!NetworkEvent::Disconnected(ClientId(1)).is_wire());
400
401        // Test new wire events
402        assert!(
403            NetworkEvent::EntitySpawned {
404                client_id: ClientId(1),
405                network_id: NetworkId(1),
406                kind: 1
407            }
408            .is_wire()
409        );
410        assert!(
411            NetworkEvent::EntityDespawned {
412                client_id: ClientId(1),
413                network_id: NetworkId(1)
414            }
415            .is_wire()
416        );
417        assert!(
418            NetworkEvent::RequestWorkspaceManifest {
419                client_id: ClientId(1)
420            }
421            .is_wire()
422        );
423    }
424
425    #[test]
426    fn test_wire_event_conversion_roundtrip() {
427        let wire = WireEvent::PlatformEvent(PlatformEvent::ResourceExhausted {
428            network_id: NetworkId(42),
429        });
430        let client_id = ClientId(7);
431        let network = wire.clone().into_network_event(client_id);
432
433        if let NetworkEvent::PlatformEvent {
434            client_id: cid,
435            event,
436        } = network
437        {
438            assert_eq!(cid, client_id);
439            assert_eq!(
440                event,
441                PlatformEvent::ResourceExhausted {
442                    network_id: NetworkId(42)
443                }
444            );
445        } else {
446            panic!("Conversion failed to preserve PlatformEvent variant");
447        }
448
449        // Test ReplicationBatch conversion
450        let event = ReplicationEvent {
451            network_id: NetworkId(1),
452            component_kind: ComponentKind(1),
453            payload: vec![1, 2, 3],
454            tick: 100,
455        };
456        let batch_wire = WireEvent::ReplicationBatch(vec![event.clone()]);
457        let batch_network = batch_wire.into_network_event(client_id);
458        if let NetworkEvent::ReplicationBatch {
459            client_id: cid,
460            events,
461        } = batch_network
462        {
463            assert_eq!(cid, client_id);
464            assert!(!events.is_empty());
465            assert_eq!(events[0].tick, 100);
466            assert_eq!(events[0].payload, vec![1, 2, 3]);
467            assert_eq!(events[0].network_id, NetworkId(1));
468            assert_eq!(events[0].component_kind, ComponentKind(1));
469        } else {
470            panic!("Conversion failed to preserve ReplicationBatch variant");
471        }
472    }
473}