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
60pub const WEAPON_KIND: ComponentKind = ComponentKind(1027);
62
63pub const SHIELD_POOL_KIND: ComponentKind = ComponentKind(1028);
65
66pub const HULL_POOL_KIND: ComponentKind = ComponentKind(1029);
68
69pub const CARGO_DROP_KIND: ComponentKind = ComponentKind(1030);
71
72pub const PROJECTILE_MARKER_KIND: ComponentKind = ComponentKind(13);
74
75pub const ACTION_FIRE_WEAPON: u32 = 1 << 2;
77
78#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
80#[repr(C)]
81pub struct Transform {
82 pub x: f32,
84 pub y: f32,
86 pub z: f32,
88 pub rotation: f32,
90 pub entity_type: u16,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
96#[repr(u8)]
97pub enum ShipClass {
98 Interceptor = 0,
99 Dreadnought = 1,
100 Hauler = 2,
101}
102
103pub const ENTITY_TYPE_INTERCEPTOR: u16 = 1;
105pub const ENTITY_TYPE_AI_INTERCEPTOR: u16 = 2;
106pub const ENTITY_TYPE_DREADNOUGHT: u16 = 3;
107pub const ENTITY_TYPE_HAULER: u16 = 4;
108pub const ENTITY_TYPE_ASTEROID: u16 = 5;
109pub const ENTITY_TYPE_CARGO_DROP: u16 = 6;
110pub const ENTITY_TYPE_TRAINING_DUMMY: u16 = 10;
111pub const ENTITY_TYPE_PROJECTILE: u16 = 20;
112
113#[must_use]
118pub const fn get_default_stats(entity_type: u16) -> (u16, u16) {
119 match entity_type {
120 ENTITY_TYPE_INTERCEPTOR | ENTITY_TYPE_AI_INTERCEPTOR => (200, 100),
121 ENTITY_TYPE_DREADNOUGHT => (1500, 500),
122 ENTITY_TYPE_HAULER => (600, 200),
123 ENTITY_TYPE_ASTEROID => (500, 0),
124 ENTITY_TYPE_TRAINING_DUMMY => (100, 50),
125 ENTITY_TYPE_CARGO_DROP | ENTITY_TYPE_PROJECTILE => (1, 0),
126 _ => (100, 100),
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
132pub struct WeaponId(pub u8);
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
136pub struct SectorId(pub u64);
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[repr(u8)]
141pub enum OreType {
142 RawOre = 0,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
147#[repr(u8)]
148pub enum ProjectileType {
149 PulseLaser = 0,
150 SeekerMissile = 1,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[repr(u8)]
156pub enum AIState {
157 Patrol = 0,
158 Aggro = 1,
159 Combat = 2,
160 Return = 3,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
165pub enum RespawnLocation {
166 NearestSafeZone,
168 Station(u64),
170 Coordinate(f32, f32),
172}
173
174#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
176pub enum PlayerInputKind {
177 Move { x: f32, y: f32 },
179 ToggleMining { target: NetworkId },
181 FirePrimary,
183}
184
185pub const MAX_ACTIONS: usize = 128;
188
189pub const ALLOWED_ACTIONS_MASK: u32 = ACTION_FIRE_WEAPON;
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct InputCommand {
195 pub tick: u64,
197 pub actions: Vec<PlayerInputKind>,
199 #[serde(default)]
201 pub actions_mask: u32,
202 pub last_seen_input_tick: Option<u64>,
204}
205
206impl InputCommand {
207 #[must_use]
209 pub fn clamped(mut self) -> Self {
210 for action in &mut self.actions {
211 if let PlayerInputKind::Move { x, y } = action {
212 *x = x.clamp(-1.0, 1.0);
213 *y = y.clamp(-1.0, 1.0);
214 }
215 }
216 self
217 }
218
219 pub fn validate(&self) -> Result<(), &'static str> {
224 if self.actions.len() > MAX_ACTIONS {
225 return Err("Too many actions in InputCommand");
226 }
227 if (self.actions_mask & !ALLOWED_ACTIONS_MASK) != 0 {
228 return Err("Unknown bits in actions_mask");
229 }
230 Ok(())
231 }
232}
233
234#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
236pub struct MiningBeam {
237 pub active: bool,
238 pub target: Option<NetworkId>,
239 #[serde(default)]
240 pub mining_range: f32,
241 #[serde(default)]
242 pub base_mining_rate: u16,
243}
244
245#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
247pub struct CargoHold {
248 pub ore_count: u16,
249 pub capacity: u16,
250}
251
252#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
253pub struct Asteroid {
254 pub ore_remaining: u16,
255 pub total_capacity: u16,
256}
257
258#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
260pub struct Weapon {
261 pub cooldown_ticks: u16,
262 pub last_fired_tick: u64,
263}
264
265#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
267pub struct ShieldPool {
268 pub current: u16,
269 pub max: u16,
270}
271
272#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
274pub struct HullPool {
275 pub current: u16,
276 pub max: u16,
277}
278
279#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
281pub struct CargoDrop {
282 pub quantity: u16,
283}
284
285#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
291pub struct ShipStats {
292 pub hp: u16,
293 pub max_hp: u16,
294 pub shield: u16,
295 pub max_shield: u16,
296 pub energy: u16,
297 pub max_energy: u16,
298 pub shield_regen_per_s: u16,
299 pub energy_regen_per_s: u16,
300}
301
302impl Default for ShipStats {
303 fn default() -> Self {
305 Self {
306 hp: 100,
307 max_hp: 100,
308 shield: 100,
309 max_shield: 100,
310 energy: 100,
311 max_energy: 100,
312 shield_regen_per_s: 0,
313 energy_regen_per_s: 0,
314 }
315 }
316}
317
318pub const MAX_ROOM_STRING_BYTES: usize = 64;
323
324#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
327#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
328pub struct RoomStringError {
329 pub len: usize,
331 pub max: usize,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
342#[serde(try_from = "String", into = "String")]
343pub struct RoomName(String);
344
345impl RoomName {
346 #[must_use = "the validated RoomName must be used"]
354 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
355 let s = s.into();
356 if s.len() > MAX_ROOM_STRING_BYTES {
357 return Err(RoomStringError {
358 len: s.len(),
359 max: MAX_ROOM_STRING_BYTES,
360 });
361 }
362 Ok(Self(s))
363 }
364
365 #[must_use]
367 pub fn as_str(&self) -> &str {
368 &self.0
369 }
370}
371
372impl TryFrom<String> for RoomName {
373 type Error = RoomStringError;
374 fn try_from(s: String) -> Result<Self, Self::Error> {
375 Self::new(s)
376 }
377}
378
379impl From<RoomName> for String {
380 fn from(n: RoomName) -> String {
381 n.0
382 }
383}
384
385impl std::fmt::Display for RoomName {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 self.0.fmt(f)
388 }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
398#[serde(try_from = "String", into = "String")]
399pub struct PermissionString(String);
400
401impl PermissionString {
402 #[must_use = "the validated PermissionString must be used"]
410 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
411 let s = s.into();
412 if s.len() > MAX_ROOM_STRING_BYTES {
413 return Err(RoomStringError {
414 len: s.len(),
415 max: MAX_ROOM_STRING_BYTES,
416 });
417 }
418 Ok(Self(s))
419 }
420
421 #[must_use]
423 pub fn as_str(&self) -> &str {
424 &self.0
425 }
426}
427
428impl TryFrom<String> for PermissionString {
429 type Error = RoomStringError;
430 fn try_from(s: String) -> Result<Self, Self::Error> {
431 Self::new(s)
432 }
433}
434
435impl From<PermissionString> for String {
436 fn from(p: PermissionString) -> String {
437 p.0
438 }
439}
440
441impl std::fmt::Display for PermissionString {
442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443 self.0.fmt(f)
444 }
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
449pub enum RoomAccessPolicy {
450 Open,
452 Permission(PermissionString),
457 InviteOnly,
459 Locked,
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct RoomDefinition {
466 pub name: RoomName,
471 pub capacity: u32,
472 pub access: RoomAccessPolicy,
473 pub is_template: bool,
474}
475
476#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
478pub struct RoomBounds {
479 pub min_x: f32,
480 pub min_y: f32,
481 pub max_x: f32,
482 pub max_y: f32,
483}
484
485#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
487pub struct RoomMembership(pub NetworkId);
488
489use std::sync::atomic::{AtomicU64, Ordering};
490use thiserror::Error;
491
492#[derive(Debug, Error, PartialEq, Eq)]
493pub enum AllocatorError {
494 #[error("NetworkId overflow (reached u64::MAX)")]
495 Overflow,
496 #[error("NetworkId allocator exhausted (reached limit)")]
497 Exhausted,
498}
499
500#[derive(Debug)]
505pub struct NetworkIdAllocator {
506 start_id: u64,
507 next: AtomicU64,
508}
509
510impl Default for NetworkIdAllocator {
511 fn default() -> Self {
512 Self::new(1)
513 }
514}
515
516impl NetworkIdAllocator {
517 #[must_use]
519 pub fn new(start_id: u64) -> Self {
520 Self {
521 start_id,
522 next: AtomicU64::new(start_id),
523 }
524 }
525
526 pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
531 let val = self
532 .next
533 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
534 if curr == u64::MAX {
535 None
536 } else {
537 Some(curr + 1)
538 }
539 })
540 .map_err(|_| AllocatorError::Overflow)?;
541
542 if val == 0 {
543 return Err(AllocatorError::Exhausted);
544 }
545
546 Ok(NetworkId(val))
547 }
548
549 pub fn reset(&self) {
552 self.next.store(self.start_id, Ordering::Relaxed);
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559
560 #[test]
561 fn test_primitive_derives() {
562 let nid1 = NetworkId(42);
563 let nid2 = nid1;
564 assert_eq!(nid1, nid2);
565
566 let lid1 = LocalId(42);
567 let lid2 = LocalId(42);
568 assert_eq!(lid1, lid2);
569
570 let cid = ClientId(99);
571 assert_eq!(format!("{cid:?}"), "ClientId(99)");
572
573 let kind = ComponentKind(1);
574 assert_eq!(kind.0, 1);
575 }
576
577 #[test]
578 fn test_input_command_clamping() {
579 let cmd = InputCommand {
580 tick: 1,
581 actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
582 actions_mask: 0,
583 last_seen_input_tick: None,
584 };
585 let clamped = cmd.clamped();
586 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
587 assert!((x - 1.0).abs() < f32::EPSILON);
588 assert!((y - -1.0).abs() < f32::EPSILON);
589 } else {
590 panic!("Expected Move action");
591 }
592
593 let valid = InputCommand {
594 tick: 1,
595 actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
596 actions_mask: 0,
597 last_seen_input_tick: None,
598 };
599 let clamped = valid.clamped();
600 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
601 assert!((x - 0.5).abs() < f32::EPSILON);
602 assert!((y - -0.2).abs() < f32::EPSILON);
603 } else {
604 panic!("Expected Move action");
605 }
606 }
607
608 #[test]
609 fn test_ship_stats_non_zero_default() {
610 let stats = ShipStats::default();
611 assert!(stats.max_hp > 0);
612 assert!(stats.max_shield > 0);
613 assert!(stats.max_energy > 0);
614 assert_eq!(stats.hp, stats.max_hp);
615 }
616
617 #[test]
618 fn test_get_default_stats() {
619 assert_eq!(get_default_stats(ENTITY_TYPE_INTERCEPTOR), (200, 100));
620 assert_eq!(get_default_stats(ENTITY_TYPE_AI_INTERCEPTOR), (200, 100));
621 assert_eq!(get_default_stats(ENTITY_TYPE_DREADNOUGHT), (1500, 500));
622 assert_eq!(get_default_stats(ENTITY_TYPE_HAULER), (600, 200));
623 assert_eq!(get_default_stats(ENTITY_TYPE_ASTEROID), (500, 0));
624 assert_eq!(get_default_stats(ENTITY_TYPE_CARGO_DROP), (1, 0));
625 assert_eq!(get_default_stats(ENTITY_TYPE_TRAINING_DUMMY), (100, 50));
626 assert_eq!(get_default_stats(ENTITY_TYPE_PROJECTILE), (1, 0));
627 assert_eq!(get_default_stats(999), (100, 100)); }
629}