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}