Skip to main content

aetheris_protocol/
types.rs

1//! Protocol-level primitive types.
2pub const PROTOCOL_VERSION: u32 = 2;
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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
30pub struct ComponentKind(pub u16);
31
32/// Standard transform component used for replication (`ComponentKind` 1).
33#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
34#[repr(C)]
35pub struct Transform {
36    /// Position X
37    pub x: f32,
38    /// Position Y
39    pub y: f32,
40    /// Position Z
41    pub z: f32,
42    /// Rotation in radians
43    pub rotation: f32,
44    /// The high-level entity type identifier for early client rendering.
45    pub entity_type: u16,
46}
47
48/// Ship classification for rendering and stat selection.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[repr(u8)]
51pub enum ShipClass {
52    Interceptor = 0,
53    Dreadnought = 1,
54    Hauler = 2,
55}
56
57/// Unique identifier for a weapon type.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
59pub struct WeaponId(pub u8);
60
61/// A globally unique sector/room identifier.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
63pub struct SectorId(pub u64);
64
65/// Material types extracted from asteroids.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[repr(u8)]
68pub enum OreType {
69    RawOre = 0,
70}
71
72/// Projectile delivery classification.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[repr(u8)]
75pub enum ProjectileType {
76    PulseLaser = 0,
77    SeekerMissile = 1,
78}
79
80/// NPC Drone behavior state Machine.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82#[repr(u8)]
83pub enum AIState {
84    Patrol = 0,
85    Aggro = 1,
86    Combat = 2,
87    Return = 3,
88}
89
90/// Definitive respawn target semantics.
91#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
92pub enum RespawnLocation {
93    /// The server calculates dynamically the Nearest Safe Zone.
94    NearestSafeZone,
95    /// Respawn docked at a specific station entity.
96    Station(u64),
97    /// Respawn at arbitrary x, y coordinates (admin/debug).
98    Coordinate(f32, f32),
99}
100
101/// Aggregated user input for a single simulation tick.
102#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
103pub struct InputCommand {
104    /// The client-side tick this input was generated at.
105    pub tick: u64,
106    /// Movement X [-1.0, 1.0]
107    pub move_x: f32,
108    /// Movement Y [-1.0, 1.0]
109    pub move_y: f32,
110    /// Bitmask for actions (M1028 bits: 1=Primary, 2=Secondary, 4=Interact).
111    pub actions: u32,
112}
113
114impl InputCommand {
115    /// Returns a new `InputCommand` with `move_x` and `move_y` clamped to the [-1.0, 1.0] range.
116    #[must_use]
117    pub fn clamped(mut self) -> Self {
118        self.move_x = self.move_x.clamp(-1.0, 1.0);
119        self.move_y = self.move_y.clamp(-1.0, 1.0);
120        self
121    }
122}
123
124/// Basic vitals for any ship entity.
125///
126/// NOTE: Zero values in maxima (`max_hp`, `max_shield`, `max_energy`) represent an uninitialized
127/// or dead state. Logic that performs divisions or percentage calculations must verify
128/// non-zero maxima.
129#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
130pub struct ShipStats {
131    pub hp: u16,
132    pub max_hp: u16,
133    pub shield: u16,
134    pub max_shield: u16,
135    pub energy: u16,
136    pub max_energy: u16,
137    pub shield_regen_per_s: u16,
138    pub energy_regen_per_s: u16,
139}
140
141impl Default for ShipStats {
142    /// Returns a baseline valid state (100 HP/Shield/Energy).
143    fn default() -> Self {
144        Self {
145            hp: 100,
146            max_hp: 100,
147            shield: 100,
148            max_shield: 100,
149            energy: 100,
150            max_energy: 100,
151            shield_regen_per_s: 0,
152            energy_regen_per_s: 0,
153        }
154    }
155}
156
157use std::sync::atomic::{AtomicU64, Ordering};
158use thiserror::Error;
159
160#[derive(Debug, Error, PartialEq, Eq)]
161pub enum AllocatorError {
162    #[error("NetworkId overflow (reached u64::MAX)")]
163    Overflow,
164    #[error("NetworkId allocator exhausted (reached limit)")]
165    Exhausted,
166}
167
168/// Authoritative allocator for [`NetworkId`]s.
169///
170/// Used by the server to ensure IDs are unique and monotonically increasing.
171/// Thread-safe and lock-free.
172#[derive(Debug)]
173pub struct NetworkIdAllocator {
174    start_id: u64,
175    next: AtomicU64,
176}
177
178impl Default for NetworkIdAllocator {
179    fn default() -> Self {
180        Self::new(1)
181    }
182}
183
184impl NetworkIdAllocator {
185    /// Creates a new allocator starting from a specific ID. 0 is reserved.
186    #[must_use]
187    pub fn new(start_id: u64) -> Self {
188        Self {
189            start_id,
190            next: AtomicU64::new(start_id),
191        }
192    }
193
194    /// Allocates a new unique [`NetworkId`].
195    ///
196    /// # Errors
197    /// Returns [`AllocatorError::Overflow`] if the next ID would exceed `u64::MAX`.
198    pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
199        let val = self
200            .next
201            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
202                if curr == u64::MAX {
203                    None
204                } else {
205                    Some(curr + 1)
206                }
207            })
208            .map_err(|_| AllocatorError::Overflow)?;
209
210        if val == 0 {
211            return Err(AllocatorError::Exhausted);
212        }
213
214        Ok(NetworkId(val))
215    }
216
217    /// Resets the allocator to its initial `start_id`.
218    /// Use only in tests or clear-world scenarios.
219    pub fn reset(&self) {
220        self.next.store(self.start_id, Ordering::Relaxed);
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_primitive_derives() {
230        let nid1 = NetworkId(42);
231        let nid2 = nid1;
232        assert_eq!(nid1, nid2);
233
234        let lid1 = LocalId(42);
235        let lid2 = LocalId(42);
236        assert_eq!(lid1, lid2);
237
238        let cid = ClientId(99);
239        assert_eq!(format!("{cid:?}"), "ClientId(99)");
240
241        let kind = ComponentKind(1);
242        assert_eq!(kind.0, 1);
243    }
244
245    #[test]
246    fn test_input_command_clamping() {
247        let cmd = InputCommand {
248            tick: 1,
249            move_x: 2.0,
250            move_y: -5.0,
251            actions: 0,
252        };
253        let clamped = cmd.clamped();
254        assert!((clamped.move_x - 1.0).abs() < f32::EPSILON);
255        assert!((clamped.move_y - -1.0).abs() < f32::EPSILON);
256
257        let valid = InputCommand {
258            tick: 1,
259            move_x: 0.5,
260            move_y: -0.2,
261            actions: 0,
262        };
263        let clamped = valid.clamped();
264        assert!((clamped.move_x - 0.5).abs() < f32::EPSILON);
265        assert!((clamped.move_y - -0.2).abs() < f32::EPSILON);
266    }
267
268    #[test]
269    fn test_ship_stats_non_zero_default() {
270        let stats = ShipStats::default();
271        assert!(stats.max_hp > 0);
272        assert!(stats.max_shield > 0);
273        assert!(stats.max_energy > 0);
274        assert_eq!(stats.hp, stats.max_hp);
275    }
276}