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