aetheris_protocol/events.rs
1use crate::types::{ClientId, ComponentKind, NetworkId};
2use serde::{Deserialize, Serialize};
3
4/// A reliable discrete game event (Phase 1 / VS-02).
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub enum GameEvent {
7 /// An asteroid was completely depleted of its ore.
8 AsteroidDepleted {
9 /// The network ID of the asteroid that was depleted.
10 network_id: NetworkId,
11 },
12}
13
14/// An event representing a fragment of a larger message.
15/// Used for MTU stability to prevent packet drops and enable reassembly.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct FragmentedEvent {
18 /// Unique identifier for the fragmented message.
19 pub message_id: u32,
20 /// The index of this fragment (0-based).
21 pub fragment_index: u16,
22 /// Total number of fragments for this message.
23 pub total_fragments: u16,
24 /// The raw payload of this fragment.
25 #[serde(with = "serde_bytes")]
26 pub payload: Vec<u8>,
27}
28
29/// An event representing a change to a single component on a single entity.
30/// Produced by `WorldState::extract_deltas()` on the server.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct ReplicationEvent {
33 /// Which entity changed.
34 pub network_id: NetworkId,
35 /// Which component type changed.
36 pub component_kind: ComponentKind,
37 /// The serialized delta payload (only the changed fields).
38 /// In Phase 1, this is a full snapshot per component for simplicity.
39 #[serde(with = "serde_bytes")]
40 pub payload: Vec<u8>,
41 /// The server tick at which this change was recorded.
42 pub tick: u64,
43}
44
45/// An inbound update to be applied to the ECS.
46/// Produced by `Encoder::decode()` on the receiving end.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct ComponentUpdate {
49 /// The entity to update.
50 pub network_id: NetworkId,
51 /// Which component type to update.
52 pub component_kind: ComponentKind,
53 /// The deserialized field values.
54 #[serde(with = "serde_bytes")]
55 pub payload: Vec<u8>,
56 /// The tick this update originated from.
57 pub tick: u64,
58}
59
60/// Events produced by `GameTransport::poll_events()`.
61#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
62pub enum NetworkEvent {
63 /// A new client has connected and been assigned a `ClientId`.
64 ClientConnected(ClientId),
65 /// A client has disconnected (graceful or timeout).
66 ClientDisconnected(ClientId),
67 /// Raw unreliable data received from a client.
68 UnreliableMessage {
69 /// The client that sent the message.
70 client_id: ClientId,
71 /// The raw message bytes.
72 #[serde(with = "serde_bytes")]
73 data: Vec<u8>,
74 },
75 /// Raw reliable data received from a client.
76 ReliableMessage {
77 /// The client that sent the message.
78 client_id: ClientId,
79 /// The raw message bytes.
80 #[serde(with = "serde_bytes")]
81 data: Vec<u8>,
82 },
83 /// A heartbeat ping from a client.
84 Ping {
85 /// The client that sent the ping.
86 client_id: ClientId,
87 /// The client's tick/timestamp when the ping was sent.
88 tick: u64,
89 },
90 /// A heartbeat pong from the server.
91 Pong {
92 /// The original tick/timestamp from the ping.
93 tick: u64,
94 },
95 /// A session authentication request from the client.
96 Auth {
97 /// The session token obtained from the Control Plane.
98 session_token: String,
99 },
100 /// A WebTransport session was closed by the remote or due to error.
101 SessionClosed(ClientId),
102 /// A WebTransport stream was reset.
103 StreamReset(ClientId),
104 /// A fragment of a larger message.
105 Fragment {
106 /// The client that sent the fragment.
107 client_id: ClientId,
108 /// The fragment data.
109 fragment: FragmentedEvent,
110 },
111 /// A testing command to trigger a stress test (Phase 1/Playground only).
112 StressTest {
113 /// The client that requested the stress test.
114 client_id: ClientId,
115 /// Number of entities to spawn.
116 count: u16,
117 /// Whether spawned entities should rotate.
118 rotate: bool,
119 },
120 /// A testing command to spawn a specific entity (Phase 1/Playground only).
121 Spawn {
122 /// The client that requested the spawn.
123 client_id: ClientId,
124 /// Which entity type to spawn.
125 entity_type: u16,
126 /// Position X
127 x: f32,
128 /// Position Y
129 y: f32,
130 /// Initial rotation
131 rot: f32,
132 },
133 /// A command to clear all entities from the world (Phase 1/Playground only).
134 ClearWorld {
135 /// The client that requested the clear.
136 client_id: ClientId,
137 },
138 /// A local event indicating the client transport has been disconnected.
139 Disconnected(ClientId),
140 /// A discrete game event (e.g. depletion, destruction).
141 GameEvent {
142 /// The client involved (or targeted).
143 client_id: ClientId,
144 /// The event data.
145 event: GameEvent,
146 },
147}
148
149impl NetworkEvent {
150 /// Returns true if this event is capable of being sent over the wire.
151 #[must_use]
152 pub const fn is_wire(&self) -> bool {
153 match self {
154 Self::Ping { .. }
155 | Self::Pong { .. }
156 | Self::Auth { .. }
157 | Self::Fragment { .. }
158 | Self::StressTest { .. }
159 | Self::Spawn { .. }
160 | Self::ClearWorld { .. }
161 | Self::GameEvent { .. } => true,
162 Self::ClientConnected(_)
163 | Self::ClientDisconnected(_)
164 | Self::UnreliableMessage { .. }
165 | Self::ReliableMessage { .. }
166 | Self::SessionClosed(_)
167 | Self::StreamReset(_)
168 | Self::Disconnected(_) => false,
169 }
170 }
171}
172
173/// A restricted view of `NetworkEvent` for over-the-wire transport.
174/// Prevents local-only variants (like `ClientConnected`) from being sent/received.
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
176pub enum WireEvent {
177 /// A heartbeat ping.
178 Ping {
179 /// The client's tick/timestamp when the ping was sent.
180 tick: u64,
181 },
182 /// A heartbeat pong.
183 Pong {
184 /// The original tick/timestamp from the ping.
185 tick: u64,
186 },
187 /// A session authentication request.
188 Auth {
189 /// The session token.
190 session_token: String,
191 },
192 /// A fragment of a larger message.
193 Fragment(FragmentedEvent),
194 /// A testing command to trigger a stress test.
195 StressTest {
196 /// Number of entities to spawn.
197 count: u16,
198 /// Whether spawned entities should rotate.
199 rotate: bool,
200 },
201 /// A testing command to spawn a specific entity.
202 Spawn {
203 /// Which entity type to spawn.
204 entity_type: u16,
205 /// Position X
206 x: f32,
207 /// Position Y
208 y: f32,
209 /// Initial rotation
210 rot: f32,
211 },
212 /// A command to clear all entities from the world.
213 ClearWorld,
214 /// A discrete game event.
215 GameEvent(GameEvent),
216}
217
218impl WireEvent {
219 /// Converts a `WireEvent` into a `NetworkEvent` for a specific client context.
220 #[must_use]
221 pub fn into_network_event(self, client_id: crate::types::ClientId) -> NetworkEvent {
222 match self {
223 Self::Ping { tick } => NetworkEvent::Ping { client_id, tick },
224 Self::Pong { tick } => NetworkEvent::Pong { tick },
225 Self::Auth { session_token } => NetworkEvent::Auth { session_token },
226 Self::Fragment(fragment) => NetworkEvent::Fragment {
227 client_id,
228 fragment,
229 },
230 Self::StressTest { count, rotate } => NetworkEvent::StressTest {
231 client_id,
232 count,
233 rotate,
234 },
235 Self::Spawn {
236 entity_type,
237 x,
238 y,
239 rot,
240 } => NetworkEvent::Spawn {
241 client_id,
242 entity_type,
243 x,
244 y,
245 rot,
246 },
247 Self::ClearWorld => NetworkEvent::ClearWorld { client_id },
248 Self::GameEvent(event) => NetworkEvent::GameEvent { client_id, event },
249 }
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn test_network_event_is_wire() {
259 assert!(
260 NetworkEvent::Ping {
261 client_id: ClientId(1),
262 tick: 100
263 }
264 .is_wire()
265 );
266 assert!(
267 NetworkEvent::GameEvent {
268 client_id: ClientId(1),
269 event: GameEvent::AsteroidDepleted {
270 network_id: NetworkId(1)
271 }
272 }
273 .is_wire()
274 );
275 assert!(!NetworkEvent::ClientConnected(ClientId(1)).is_wire());
276 assert!(!NetworkEvent::ClientDisconnected(ClientId(1)).is_wire());
277 }
278
279 #[test]
280 fn test_wire_event_conversion_roundtrip() {
281 let wire = WireEvent::GameEvent(GameEvent::AsteroidDepleted {
282 network_id: NetworkId(42),
283 });
284 let client_id = ClientId(7);
285 let network = wire.clone().into_network_event(client_id);
286
287 if let NetworkEvent::GameEvent {
288 client_id: cid,
289 event,
290 } = network
291 {
292 assert_eq!(cid, client_id);
293 assert_eq!(
294 event,
295 GameEvent::AsteroidDepleted {
296 network_id: NetworkId(42)
297 }
298 );
299 } else {
300 panic!("Conversion failed to preserve GameEvent variant");
301 }
302 }
303}