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