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