Skip to main content

aetheris_protocol/
types.rs

1//! Protocol-level primitive types.
2pub const PROTOCOL_VERSION: u32 = 3;
3
4use serde::{Deserialize, Serialize};
5
6/// A globally unique entity identifier used in all network communication.
7/// Assigned by the server. Immutable for the lifetime of the entity.
8///
9/// This is NOT the ECS's internal entity ID. The `WorldState` adapter
10/// translates between `NetworkId` and the ECS's local handle.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub struct NetworkId(pub u64);
13
14/// The ECS's internal entity handle. Opaque to the network layer.
15/// In Phase 1 (Bevy), this wraps `bevy_ecs::entity::Entity`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct LocalId(pub u64);
18
19/// A unique identifier for a connected client session.
20/// Assigned by the transport layer on connection, released on disconnect.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22pub struct ClientId(pub u64);
23
24/// A component type identifier. Used by the Encoder to determine
25/// how to serialize/deserialize a specific component's fields.
26///
27/// In Phase 1, this is a simple enum discriminant.
28/// In Phase 3, this may become a compile-time type hash.
29///
30/// ### Reservation Policy (M1020/M1015):
31/// - `0–1023` (except 128): Engine Core (Replicated).
32/// - `1024–2047`: Official Engine Extensions.
33/// - `128`: Explicitly reserved for Input Commands (Transient/Inbound-Only).
34/// - `32768+`: Reserved for Non-Replicated/Inbound variants.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36pub struct ComponentKind(pub u16);
37
38/// Discriminant for client-to-server input commands.
39/// Tagged as Transient/Inbound-Only.
40pub const INPUT_COMMAND_KIND: ComponentKind = ComponentKind(128);
41
42/// Replicated component for the mining laser beam state.
43pub const MINING_BEAM_KIND: ComponentKind = ComponentKind(1024);
44
45/// Replicated component for ship cargo state (replicated to owner).
46pub const CARGO_HOLD_KIND: ComponentKind = ComponentKind(1025);
47
48/// Replicated component for asteroid ore depletion tracking.
49pub const ASTEROID_KIND: ComponentKind = ComponentKind(1026);
50
51/// Standard transform component used for replication (`ComponentKind` 1).
52#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
53#[repr(C)]
54pub struct Transform {
55    /// Position X
56    pub x: f32,
57    /// Position Y
58    pub y: f32,
59    /// Position Z
60    pub z: f32,
61    /// Rotation in radians
62    pub rotation: f32,
63    /// The high-level entity type identifier for early client rendering.
64    pub entity_type: u16,
65}
66
67/// Ship classification for rendering and stat selection.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[repr(u8)]
70pub enum ShipClass {
71    Interceptor = 0,
72    Dreadnought = 1,
73    Hauler = 2,
74}
75
76/// Unique identifier for a weapon type.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
78pub struct WeaponId(pub u8);
79
80/// A globally unique sector/room identifier.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
82pub struct SectorId(pub u64);
83
84/// Material types extracted from asteroids.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
86#[repr(u8)]
87pub enum OreType {
88    RawOre = 0,
89}
90
91/// Projectile delivery classification.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[repr(u8)]
94pub enum ProjectileType {
95    PulseLaser = 0,
96    SeekerMissile = 1,
97}
98
99/// NPC Drone behavior state Machine.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[repr(u8)]
102pub enum AIState {
103    Patrol = 0,
104    Aggro = 1,
105    Combat = 2,
106    Return = 3,
107}
108
109/// Definitive respawn target semantics.
110#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
111pub enum RespawnLocation {
112    /// The server calculates dynamically the Nearest Safe Zone.
113    NearestSafeZone,
114    /// Respawn docked at a specific station entity.
115    Station(u64),
116    /// Respawn at arbitrary x, y coordinates (admin/debug).
117    Coordinate(f32, f32),
118}
119
120/// Individual input actions performed by a player in a single tick.
121#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
122pub enum PlayerInputKind {
123    /// Directional thrust/movement.
124    Move { x: f32, y: f32 },
125    /// Toggle mining beam on a specific target.
126    ToggleMining { target: NetworkId },
127    /// Fire primary weapon (for VS-03).
128    FirePrimary,
129}
130
131/// Maximum allowed actions in a single `InputCommand` to prevent payload `DoS`.
132/// Chosen to stay well within `MAX_SAFE_PAYLOAD_SIZE` (1200 bytes).
133pub const MAX_ACTIONS: usize = 128;
134
135/// Aggregated user input for a single simulation tick.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct InputCommand {
138    /// The client-side tick this input was generated at.
139    pub tick: u64,
140    /// List of actions performed in this tick.
141    pub actions: Vec<PlayerInputKind>,
142}
143
144impl InputCommand {
145    /// Returns a new `InputCommand` with all `Move` inputs clamped to [-1.0, 1.0].
146    #[must_use]
147    pub fn clamped(mut self) -> Self {
148        for action in &mut self.actions {
149            if let PlayerInputKind::Move { x, y } = action {
150                *x = x.clamp(-1.0, 1.0);
151                *y = y.clamp(-1.0, 1.0);
152            }
153        }
154        self
155    }
156
157    /// Validates the command against protocol constraints.
158    ///
159    /// # Errors
160    /// Returns an error message if the command exceeds `MAX_ACTIONS`.
161    pub fn validate(&self) -> Result<(), &'static str> {
162        if self.actions.len() > MAX_ACTIONS {
163            return Err("Too many actions in InputCommand");
164        }
165        Ok(())
166    }
167}
168
169/// Replicated state for a ship's mining beam.
170#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
171pub struct MiningBeam {
172    pub active: bool,
173    pub target: Option<NetworkId>,
174}
175
176/// Replicated state for a ship's cargo hold.
177#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
178pub struct CargoHold {
179    pub ore_count: u16,
180    pub capacity: u16,
181}
182
183/// Replicated state for an asteroid's resource depletion.
184#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
185pub struct Asteroid {
186    pub ore_remaining: u16,
187    pub total_capacity: u16,
188}
189
190/// Basic vitals for any ship entity.
191///
192/// NOTE: Zero values in maxima (`max_hp`, `max_shield`, `max_energy`) represent an uninitialized
193/// or dead state. Logic that performs divisions or percentage calculations must verify
194/// non-zero maxima.
195#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
196pub struct ShipStats {
197    pub hp: u16,
198    pub max_hp: u16,
199    pub shield: u16,
200    pub max_shield: u16,
201    pub energy: u16,
202    pub max_energy: u16,
203    pub shield_regen_per_s: u16,
204    pub energy_regen_per_s: u16,
205}
206
207impl Default for ShipStats {
208    /// Returns a baseline valid state (100 HP/Shield/Energy).
209    fn default() -> Self {
210        Self {
211            hp: 100,
212            max_hp: 100,
213            shield: 100,
214            max_shield: 100,
215            energy: 100,
216            max_energy: 100,
217            shield_regen_per_s: 0,
218            energy_regen_per_s: 0,
219        }
220    }
221}
222
223use std::sync::atomic::{AtomicU64, Ordering};
224use thiserror::Error;
225
226#[derive(Debug, Error, PartialEq, Eq)]
227pub enum AllocatorError {
228    #[error("NetworkId overflow (reached u64::MAX)")]
229    Overflow,
230    #[error("NetworkId allocator exhausted (reached limit)")]
231    Exhausted,
232}
233
234/// Authoritative allocator for [`NetworkId`]s.
235///
236/// Used by the server to ensure IDs are unique and monotonically increasing.
237/// Thread-safe and lock-free.
238#[derive(Debug)]
239pub struct NetworkIdAllocator {
240    start_id: u64,
241    next: AtomicU64,
242}
243
244impl Default for NetworkIdAllocator {
245    fn default() -> Self {
246        Self::new(1)
247    }
248}
249
250impl NetworkIdAllocator {
251    /// Creates a new allocator starting from a specific ID. 0 is reserved.
252    #[must_use]
253    pub fn new(start_id: u64) -> Self {
254        Self {
255            start_id,
256            next: AtomicU64::new(start_id),
257        }
258    }
259
260    /// Allocates a new unique [`NetworkId`].
261    ///
262    /// # Errors
263    /// Returns [`AllocatorError::Overflow`] if the next ID would exceed `u64::MAX`.
264    pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
265        let val = self
266            .next
267            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
268                if curr == u64::MAX {
269                    None
270                } else {
271                    Some(curr + 1)
272                }
273            })
274            .map_err(|_| AllocatorError::Overflow)?;
275
276        if val == 0 {
277            return Err(AllocatorError::Exhausted);
278        }
279
280        Ok(NetworkId(val))
281    }
282
283    /// Resets the allocator to its initial `start_id`.
284    /// Use only in tests or clear-world scenarios.
285    pub fn reset(&self) {
286        self.next.store(self.start_id, Ordering::Relaxed);
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_primitive_derives() {
296        let nid1 = NetworkId(42);
297        let nid2 = nid1;
298        assert_eq!(nid1, nid2);
299
300        let lid1 = LocalId(42);
301        let lid2 = LocalId(42);
302        assert_eq!(lid1, lid2);
303
304        let cid = ClientId(99);
305        assert_eq!(format!("{cid:?}"), "ClientId(99)");
306
307        let kind = ComponentKind(1);
308        assert_eq!(kind.0, 1);
309    }
310
311    #[test]
312    fn test_input_command_clamping() {
313        let cmd = InputCommand {
314            tick: 1,
315            actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
316        };
317        let clamped = cmd.clamped();
318        if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
319            assert!((x - 1.0).abs() < f32::EPSILON);
320            assert!((y - -1.0).abs() < f32::EPSILON);
321        } else {
322            panic!("Expected Move action");
323        }
324
325        let valid = InputCommand {
326            tick: 1,
327            actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
328        };
329        let clamped = valid.clamped();
330        if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
331            assert!((x - 0.5).abs() < f32::EPSILON);
332            assert!((y - -0.2).abs() < f32::EPSILON);
333        } else {
334            panic!("Expected Move action");
335        }
336    }
337
338    #[test]
339    fn test_ship_stats_non_zero_default() {
340        let stats = ShipStats::default();
341        assert!(stats.max_hp > 0);
342        assert!(stats.max_shield > 0);
343        assert!(stats.max_energy > 0);
344        assert_eq!(stats.hp, stats.max_hp);
345    }
346}