1pub const PROTOCOL_VERSION: u32 = 3;
3
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub struct NetworkId(pub u64);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct LocalId(pub u64);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22pub struct ClientId(pub u64);
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36pub struct ComponentKind(pub u16);
37
38pub const INPUT_COMMAND_KIND: ComponentKind = ComponentKind(128);
41
42pub const ROOM_DEFINITION_KIND: ComponentKind = ComponentKind(129);
44
45pub const ROOM_BOUNDS_KIND: ComponentKind = ComponentKind(130);
47
48pub const ROOM_MEMBERSHIP_KIND: ComponentKind = ComponentKind(131);
50
51pub const MINING_BEAM_KIND: ComponentKind = ComponentKind(1024);
53
54pub const CARGO_HOLD_KIND: ComponentKind = ComponentKind(1025);
56
57pub const ASTEROID_KIND: ComponentKind = ComponentKind(1026);
59
60#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
62#[repr(C)]
63pub struct Transform {
64 pub x: f32,
66 pub y: f32,
68 pub z: f32,
70 pub rotation: f32,
72 pub entity_type: u16,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
78#[repr(u8)]
79pub enum ShipClass {
80 Interceptor = 0,
81 Dreadnought = 1,
82 Hauler = 2,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
87pub struct WeaponId(pub u8);
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
91pub struct SectorId(pub u64);
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95#[repr(u8)]
96pub enum OreType {
97 RawOre = 0,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
102#[repr(u8)]
103pub enum ProjectileType {
104 PulseLaser = 0,
105 SeekerMissile = 1,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110#[repr(u8)]
111pub enum AIState {
112 Patrol = 0,
113 Aggro = 1,
114 Combat = 2,
115 Return = 3,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
120pub enum RespawnLocation {
121 NearestSafeZone,
123 Station(u64),
125 Coordinate(f32, f32),
127}
128
129#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
131pub enum PlayerInputKind {
132 Move { x: f32, y: f32 },
134 ToggleMining { target: NetworkId },
136 FirePrimary,
138}
139
140pub const MAX_ACTIONS: usize = 128;
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct InputCommand {
147 pub tick: u64,
149 pub actions: Vec<PlayerInputKind>,
151 pub last_seen_input_tick: Option<u64>,
153}
154
155impl InputCommand {
156 #[must_use]
158 pub fn clamped(mut self) -> Self {
159 for action in &mut self.actions {
160 if let PlayerInputKind::Move { x, y } = action {
161 *x = x.clamp(-1.0, 1.0);
162 *y = y.clamp(-1.0, 1.0);
163 }
164 }
165 self
166 }
167
168 pub fn validate(&self) -> Result<(), &'static str> {
173 if self.actions.len() > MAX_ACTIONS {
174 return Err("Too many actions in InputCommand");
175 }
176 Ok(())
177 }
178}
179
180#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
182pub struct MiningBeam {
183 pub active: bool,
184 pub target: Option<NetworkId>,
185 #[serde(default)]
186 pub mining_range: f32,
187 #[serde(default)]
188 pub base_mining_rate: u16,
189}
190
191#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
193pub struct CargoHold {
194 pub ore_count: u16,
195 pub capacity: u16,
196}
197
198#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
200pub struct Asteroid {
201 pub ore_remaining: u16,
202 pub total_capacity: u16,
203}
204
205#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
211pub struct ShipStats {
212 pub hp: u16,
213 pub max_hp: u16,
214 pub shield: u16,
215 pub max_shield: u16,
216 pub energy: u16,
217 pub max_energy: u16,
218 pub shield_regen_per_s: u16,
219 pub energy_regen_per_s: u16,
220}
221
222impl Default for ShipStats {
223 fn default() -> Self {
225 Self {
226 hp: 100,
227 max_hp: 100,
228 shield: 100,
229 max_shield: 100,
230 energy: 100,
231 max_energy: 100,
232 shield_regen_per_s: 0,
233 energy_regen_per_s: 0,
234 }
235 }
236}
237
238pub const MAX_ROOM_STRING_BYTES: usize = 64;
243
244#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
247#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
248pub struct RoomStringError {
249 pub len: usize,
251 pub max: usize,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
262#[serde(try_from = "String", into = "String")]
263pub struct RoomName(String);
264
265impl RoomName {
266 #[must_use = "the validated RoomName must be used"]
274 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
275 let s = s.into();
276 if s.len() > MAX_ROOM_STRING_BYTES {
277 return Err(RoomStringError {
278 len: s.len(),
279 max: MAX_ROOM_STRING_BYTES,
280 });
281 }
282 Ok(Self(s))
283 }
284
285 #[must_use]
287 pub fn as_str(&self) -> &str {
288 &self.0
289 }
290}
291
292impl TryFrom<String> for RoomName {
293 type Error = RoomStringError;
294 fn try_from(s: String) -> Result<Self, Self::Error> {
295 Self::new(s)
296 }
297}
298
299impl From<RoomName> for String {
300 fn from(n: RoomName) -> String {
301 n.0
302 }
303}
304
305impl std::fmt::Display for RoomName {
306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307 self.0.fmt(f)
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
318#[serde(try_from = "String", into = "String")]
319pub struct PermissionString(String);
320
321impl PermissionString {
322 #[must_use = "the validated PermissionString must be used"]
330 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
331 let s = s.into();
332 if s.len() > MAX_ROOM_STRING_BYTES {
333 return Err(RoomStringError {
334 len: s.len(),
335 max: MAX_ROOM_STRING_BYTES,
336 });
337 }
338 Ok(Self(s))
339 }
340
341 #[must_use]
343 pub fn as_str(&self) -> &str {
344 &self.0
345 }
346}
347
348impl TryFrom<String> for PermissionString {
349 type Error = RoomStringError;
350 fn try_from(s: String) -> Result<Self, Self::Error> {
351 Self::new(s)
352 }
353}
354
355impl From<PermissionString> for String {
356 fn from(p: PermissionString) -> String {
357 p.0
358 }
359}
360
361impl std::fmt::Display for PermissionString {
362 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363 self.0.fmt(f)
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
369pub enum RoomAccessPolicy {
370 Open,
372 Permission(PermissionString),
377 InviteOnly,
379 Locked,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385pub struct RoomDefinition {
386 pub name: RoomName,
391 pub capacity: u32,
392 pub access: RoomAccessPolicy,
393 pub is_template: bool,
394}
395
396#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
398pub struct RoomBounds {
399 pub min_x: f32,
400 pub min_y: f32,
401 pub max_x: f32,
402 pub max_y: f32,
403}
404
405#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
407pub struct RoomMembership(pub NetworkId);
408
409use std::sync::atomic::{AtomicU64, Ordering};
410use thiserror::Error;
411
412#[derive(Debug, Error, PartialEq, Eq)]
413pub enum AllocatorError {
414 #[error("NetworkId overflow (reached u64::MAX)")]
415 Overflow,
416 #[error("NetworkId allocator exhausted (reached limit)")]
417 Exhausted,
418}
419
420#[derive(Debug)]
425pub struct NetworkIdAllocator {
426 start_id: u64,
427 next: AtomicU64,
428}
429
430impl Default for NetworkIdAllocator {
431 fn default() -> Self {
432 Self::new(1)
433 }
434}
435
436impl NetworkIdAllocator {
437 #[must_use]
439 pub fn new(start_id: u64) -> Self {
440 Self {
441 start_id,
442 next: AtomicU64::new(start_id),
443 }
444 }
445
446 pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
451 let val = self
452 .next
453 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
454 if curr == u64::MAX {
455 None
456 } else {
457 Some(curr + 1)
458 }
459 })
460 .map_err(|_| AllocatorError::Overflow)?;
461
462 if val == 0 {
463 return Err(AllocatorError::Exhausted);
464 }
465
466 Ok(NetworkId(val))
467 }
468
469 pub fn reset(&self) {
472 self.next.store(self.start_id, Ordering::Relaxed);
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn test_primitive_derives() {
482 let nid1 = NetworkId(42);
483 let nid2 = nid1;
484 assert_eq!(nid1, nid2);
485
486 let lid1 = LocalId(42);
487 let lid2 = LocalId(42);
488 assert_eq!(lid1, lid2);
489
490 let cid = ClientId(99);
491 assert_eq!(format!("{cid:?}"), "ClientId(99)");
492
493 let kind = ComponentKind(1);
494 assert_eq!(kind.0, 1);
495 }
496
497 #[test]
498 fn test_input_command_clamping() {
499 let cmd = InputCommand {
500 tick: 1,
501 actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
502 last_seen_input_tick: None,
503 };
504 let clamped = cmd.clamped();
505 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
506 assert!((x - 1.0).abs() < f32::EPSILON);
507 assert!((y - -1.0).abs() < f32::EPSILON);
508 } else {
509 panic!("Expected Move action");
510 }
511
512 let valid = InputCommand {
513 tick: 1,
514 actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
515 last_seen_input_tick: None,
516 };
517 let clamped = valid.clamped();
518 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
519 assert!((x - 0.5).abs() < f32::EPSILON);
520 assert!((y - -0.2).abs() < f32::EPSILON);
521 } else {
522 panic!("Expected Move action");
523 }
524 }
525
526 #[test]
527 fn test_ship_stats_non_zero_default() {
528 let stats = ShipStats::default();
529 assert!(stats.max_hp > 0);
530 assert!(stats.max_shield > 0);
531 assert!(stats.max_energy > 0);
532 assert_eq!(stats.hp, stats.max_hp);
533 }
534}