aetheris_protocol/events.rs
1use crate::types::{ClientId, ComponentKind, NetworkId};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4
5/// A reliable discrete platform event (Phase 1 / VS-02).
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub enum PlatformEvent {
8 /// A resource was completely exhausted of its payload.
9 ResourceExhausted {
10 /// The network ID of the resource that was exhausted.
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 WorkspaceManifest {
20 /// The collection of metadata key-value pairs.
21 manifest: BTreeMap<String, String>,
22 },
23 /// Interaction (e.g. damage) applied to an entity.
24 Interaction {
25 source: NetworkId,
26 target: NetworkId,
27 amount: u16,
28 },
29 /// An entity has been terminated.
30 Termination { target: NetworkId },
31 /// An entity has been reinitialized.
32 Reinitialization { target: NetworkId, x: f32, y: f32 },
33 /// A data drop was collected by an agent.
34 PayloadCollected {
35 /// The network ID of the data drop that was collected.
36 network_id: NetworkId,
37 /// The amount of payload collected.
38 amount: u16,
39 },
40}
41
42impl PlatformEvent {
43 /// Converts a `PlatformEvent` into a `WireEvent`.
44 #[must_use]
45 pub fn into_wire_event(self) -> WireEvent {
46 WireEvent::PlatformEvent(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 `PlatformTransport::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 new entity has been spawned in the world.
143 EntitySpawned {
144 /// The client involved in the spawn (or targeted).
145 client_id: ClientId,
146 /// The unique network identifier for the entity.
147 network_id: NetworkId,
148 /// The kind/type of entity being spawned.
149 kind: u16,
150 },
151 /// An entity has been despawned from the world.
152 EntityDespawned {
153 /// The client involved in the despawn (or targeted).
154 client_id: ClientId,
155 /// The network identifier of the entity to remove.
156 network_id: NetworkId,
157 },
158 /// A session authentication request from the client.
159 Auth {
160 /// The session token obtained from the Control Plane.
161 session_token: String,
162 },
163 /// A WebTransport session was closed by the remote or due to error.
164 SessionClosed(ClientId),
165 /// A WebTransport stream was reset.
166 StreamReset(ClientId),
167 /// A fragment of a larger message.
168 Fragment {
169 /// The client that sent the fragment.
170 client_id: ClientId,
171 /// The fragment data.
172 fragment: FragmentedEvent,
173 },
174 /// A testing command to trigger a stress test (Phase 1/Playground only).
175 StressTest {
176 /// The client that requested the stress test.
177 client_id: ClientId,
178 /// Number of entities to spawn.
179 count: u16,
180 /// Whether spawned entities should rotate.
181 rotate: bool,
182 },
183 /// A testing command to spawn a specific entity (Phase 1/Playground only).
184 Spawn {
185 /// The client that requested the spawn.
186 client_id: ClientId,
187 /// Which entity type to spawn.
188 entity_type: u16,
189 /// Position X
190 x: f32,
191 /// Position Y
192 y: f32,
193 /// Initial rotation
194 rot: f32,
195 },
196 /// A command to clear all entities from the world (Phase 1/Playground only).
197 ClearWorld {
198 /// The client that requested the clear.
199 client_id: ClientId,
200 },
201 /// Client requests to start a gameplay session: spawns the session agent and grants Possession.
202 StartSession {
203 /// The client starting the session.
204 client_id: ClientId,
205 },
206 /// A request from a client to receive the current workspace manifest.
207 RequestWorkspaceManifest {
208 /// The client that requested the manifest.
209 client_id: ClientId,
210 },
211 /// A local event indicating the client transport has been disconnected.
212 Disconnected(ClientId),
213 /// A discrete platform event (e.g. exhaustion, termination).
214 PlatformEvent {
215 /// The client involved (or targeted).
216 client_id: ClientId,
217 /// The event data.
218 event: PlatformEvent,
219 },
220 /// A batch of replication updates sent together to save bandwidth/packets.
221 ReplicationBatch {
222 /// The client that should receive the batch.
223 client_id: ClientId,
224 /// The collection of updates.
225 events: Vec<ReplicationEvent>,
226 },
227}
228
229impl NetworkEvent {
230 /// Returns true if this event is capable of being sent over the wire.
231 #[must_use]
232 pub const fn is_wire(&self) -> bool {
233 match self {
234 Self::Ping { .. }
235 | Self::Pong { .. }
236 | Self::Auth { .. }
237 | Self::Fragment { .. }
238 | Self::StressTest { .. }
239 | Self::Spawn { .. }
240 | Self::ClearWorld { .. }
241 | Self::StartSession { .. }
242 | Self::RequestWorkspaceManifest { .. }
243 | Self::EntitySpawned { .. }
244 | Self::EntityDespawned { .. }
245 | Self::PlatformEvent { .. }
246 | Self::ReplicationBatch { .. } => true,
247 Self::ClientConnected(_)
248 | Self::ClientDisconnected(_)
249 | Self::UnreliableMessage { .. }
250 | Self::ReliableMessage { .. }
251 | Self::SessionClosed(_)
252 | Self::StreamReset(_)
253 | Self::Disconnected(_) => false,
254 }
255 }
256}
257
258/// A restricted view of `NetworkEvent` for over-the-wire transport.
259/// Prevents local-only variants (like `ClientConnected`) from being sent/received.
260#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
261pub enum WireEvent {
262 /// A heartbeat ping.
263 Ping {
264 /// The client's tick/timestamp when the ping was sent.
265 tick: u64,
266 },
267 /// A heartbeat pong.
268 Pong {
269 /// The original tick/timestamp from the ping.
270 tick: u64,
271 },
272 /// A session authentication request.
273 Auth {
274 /// The session token.
275 session_token: String,
276 },
277 /// A fragment of a larger message.
278 Fragment(FragmentedEvent),
279 /// A testing command to trigger a stress test.
280 StressTest {
281 /// Number of entities to spawn.
282 count: u16,
283 /// Whether spawned entities should rotate.
284 rotate: bool,
285 },
286 /// A testing command to spawn a specific entity.
287 Spawn {
288 /// Which entity type to spawn.
289 entity_type: u16,
290 /// Position X
291 x: f32,
292 /// Position Y
293 y: f32,
294 /// Initial rotation
295 rot: f32,
296 },
297 /// A command to clear all entities from the world.
298 ClearWorld,
299 /// A new entity has been spawned.
300 EntitySpawned {
301 /// The entity ID.
302 network_id: NetworkId,
303 /// The entity type.
304 kind: u16,
305 },
306 /// An entity has been despawned.
307 EntityDespawned {
308 /// The entity ID.
309 network_id: NetworkId,
310 },
311 /// Client requests to start a gameplay session: spawns the session agent and grants Possession.
312 StartSession,
313 /// A request to receive the current workspace manifest.
314 RequestWorkspaceManifest,
315 /// A discrete platform event.
316 PlatformEvent(PlatformEvent),
317 /// A batch of replication updates.
318 ReplicationBatch(Vec<ReplicationEvent>),
319}
320
321impl WireEvent {
322 /// Converts a `WireEvent` into a `NetworkEvent` for a specific client context.
323 #[must_use]
324 pub fn into_network_event(self, client_id: crate::types::ClientId) -> NetworkEvent {
325 match self {
326 Self::Ping { tick } => NetworkEvent::Ping { client_id, tick },
327 Self::Pong { tick } => NetworkEvent::Pong { tick },
328 Self::Auth { session_token } => NetworkEvent::Auth { session_token },
329 Self::Fragment(fragment) => NetworkEvent::Fragment {
330 client_id,
331 fragment,
332 },
333 Self::StressTest { count, rotate } => NetworkEvent::StressTest {
334 client_id,
335 count,
336 rotate,
337 },
338 Self::Spawn {
339 entity_type,
340 x,
341 y,
342 rot,
343 } => NetworkEvent::Spawn {
344 client_id,
345 entity_type,
346 x,
347 y,
348 rot,
349 },
350 Self::ClearWorld => NetworkEvent::ClearWorld { client_id },
351 Self::StartSession => NetworkEvent::StartSession { client_id },
352 Self::RequestWorkspaceManifest => NetworkEvent::RequestWorkspaceManifest { client_id },
353 Self::PlatformEvent(event) => NetworkEvent::PlatformEvent { client_id, event },
354 Self::EntitySpawned { network_id, kind } => NetworkEvent::EntitySpawned {
355 client_id,
356 network_id,
357 kind,
358 },
359 Self::EntityDespawned { network_id } => NetworkEvent::EntityDespawned {
360 client_id,
361 network_id,
362 },
363 Self::ReplicationBatch(events) => NetworkEvent::ReplicationBatch { client_id, events },
364 }
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_network_event_is_wire() {
374 assert!(
375 NetworkEvent::Ping {
376 client_id: ClientId(1),
377 tick: 100
378 }
379 .is_wire()
380 );
381 assert!(
382 NetworkEvent::PlatformEvent {
383 client_id: ClientId(1),
384 event: PlatformEvent::ResourceExhausted {
385 network_id: NetworkId(1)
386 }
387 }
388 .is_wire()
389 );
390 assert!(
391 NetworkEvent::ReplicationBatch {
392 client_id: ClientId(1),
393 events: vec![]
394 }
395 .is_wire()
396 );
397 assert!(!NetworkEvent::ClientConnected(ClientId(1)).is_wire());
398 assert!(!NetworkEvent::ClientDisconnected(ClientId(1)).is_wire());
399 assert!(!NetworkEvent::Disconnected(ClientId(1)).is_wire());
400
401 // Test new wire events
402 assert!(
403 NetworkEvent::EntitySpawned {
404 client_id: ClientId(1),
405 network_id: NetworkId(1),
406 kind: 1
407 }
408 .is_wire()
409 );
410 assert!(
411 NetworkEvent::EntityDespawned {
412 client_id: ClientId(1),
413 network_id: NetworkId(1)
414 }
415 .is_wire()
416 );
417 assert!(
418 NetworkEvent::RequestWorkspaceManifest {
419 client_id: ClientId(1)
420 }
421 .is_wire()
422 );
423 }
424
425 #[test]
426 fn test_wire_event_conversion_roundtrip() {
427 let wire = WireEvent::PlatformEvent(PlatformEvent::ResourceExhausted {
428 network_id: NetworkId(42),
429 });
430 let client_id = ClientId(7);
431 let network = wire.clone().into_network_event(client_id);
432
433 if let NetworkEvent::PlatformEvent {
434 client_id: cid,
435 event,
436 } = network
437 {
438 assert_eq!(cid, client_id);
439 assert_eq!(
440 event,
441 PlatformEvent::ResourceExhausted {
442 network_id: NetworkId(42)
443 }
444 );
445 } else {
446 panic!("Conversion failed to preserve PlatformEvent variant");
447 }
448
449 // Test ReplicationBatch conversion
450 let event = ReplicationEvent {
451 network_id: NetworkId(1),
452 component_kind: ComponentKind(1),
453 payload: vec![1, 2, 3],
454 tick: 100,
455 };
456 let batch_wire = WireEvent::ReplicationBatch(vec![event.clone()]);
457 let batch_network = batch_wire.into_network_event(client_id);
458 if let NetworkEvent::ReplicationBatch {
459 client_id: cid,
460 events,
461 } = batch_network
462 {
463 assert_eq!(cid, client_id);
464 assert!(!events.is_empty());
465 assert_eq!(events[0].tick, 100);
466 assert_eq!(events[0].payload, vec![1, 2, 3]);
467 assert_eq!(events[0].network_id, NetworkId(1));
468 assert_eq!(events[0].component_kind, ComponentKind(1));
469 } else {
470 panic!("Conversion failed to preserve ReplicationBatch variant");
471 }
472 }
473}