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}