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