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
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
105pub struct WeaponId(pub u8);
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
109pub struct SectorId(pub u64);
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113#[repr(u8)]
114pub enum OreType {
115 RawOre = 0,
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[repr(u8)]
121pub enum ProjectileType {
122 PulseLaser = 0,
123 SeekerMissile = 1,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[repr(u8)]
129pub enum AIState {
130 Patrol = 0,
131 Aggro = 1,
132 Combat = 2,
133 Return = 3,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
138pub enum RespawnLocation {
139 NearestSafeZone,
141 Station(u64),
143 Coordinate(f32, f32),
145}
146
147#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
149pub enum PlayerInputKind {
150 Move { x: f32, y: f32 },
152 ToggleMining { target: NetworkId },
154 FirePrimary,
156}
157
158pub const MAX_ACTIONS: usize = 128;
161
162pub const ALLOWED_ACTIONS_MASK: u32 = ACTION_FIRE_WEAPON;
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct InputCommand {
168 pub tick: u64,
170 pub actions: Vec<PlayerInputKind>,
172 #[serde(default)]
174 pub actions_mask: u32,
175 pub last_seen_input_tick: Option<u64>,
177}
178
179impl InputCommand {
180 #[must_use]
182 pub fn clamped(mut self) -> Self {
183 for action in &mut self.actions {
184 if let PlayerInputKind::Move { x, y } = action {
185 *x = x.clamp(-1.0, 1.0);
186 *y = y.clamp(-1.0, 1.0);
187 }
188 }
189 self
190 }
191
192 pub fn validate(&self) -> Result<(), &'static str> {
197 if self.actions.len() > MAX_ACTIONS {
198 return Err("Too many actions in InputCommand");
199 }
200 if (self.actions_mask & !ALLOWED_ACTIONS_MASK) != 0 {
201 return Err("Unknown bits in actions_mask");
202 }
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
209pub struct MiningBeam {
210 pub active: bool,
211 pub target: Option<NetworkId>,
212 #[serde(default)]
213 pub mining_range: f32,
214 #[serde(default)]
215 pub base_mining_rate: u16,
216}
217
218#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
220pub struct CargoHold {
221 pub ore_count: u16,
222 pub capacity: u16,
223}
224
225#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
226pub struct Asteroid {
227 pub ore_remaining: u16,
228 pub total_capacity: u16,
229}
230
231#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
233pub struct Weapon {
234 pub cooldown_ticks: u16,
235 pub last_fired_tick: u64,
236}
237
238#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
240pub struct ShieldPool {
241 pub current: u16,
242 pub max: u16,
243}
244
245#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
247pub struct HullPool {
248 pub current: u16,
249 pub max: u16,
250}
251
252#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
254pub struct CargoDrop {
255 pub quantity: u16,
256}
257
258#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
264pub struct ShipStats {
265 pub hp: u16,
266 pub max_hp: u16,
267 pub shield: u16,
268 pub max_shield: u16,
269 pub energy: u16,
270 pub max_energy: u16,
271 pub shield_regen_per_s: u16,
272 pub energy_regen_per_s: u16,
273}
274
275impl Default for ShipStats {
276 fn default() -> Self {
278 Self {
279 hp: 100,
280 max_hp: 100,
281 shield: 100,
282 max_shield: 100,
283 energy: 100,
284 max_energy: 100,
285 shield_regen_per_s: 0,
286 energy_regen_per_s: 0,
287 }
288 }
289}
290
291pub const MAX_ROOM_STRING_BYTES: usize = 64;
296
297#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
300#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
301pub struct RoomStringError {
302 pub len: usize,
304 pub max: usize,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
315#[serde(try_from = "String", into = "String")]
316pub struct RoomName(String);
317
318impl RoomName {
319 #[must_use = "the validated RoomName must be used"]
327 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
328 let s = s.into();
329 if s.len() > MAX_ROOM_STRING_BYTES {
330 return Err(RoomStringError {
331 len: s.len(),
332 max: MAX_ROOM_STRING_BYTES,
333 });
334 }
335 Ok(Self(s))
336 }
337
338 #[must_use]
340 pub fn as_str(&self) -> &str {
341 &self.0
342 }
343}
344
345impl TryFrom<String> for RoomName {
346 type Error = RoomStringError;
347 fn try_from(s: String) -> Result<Self, Self::Error> {
348 Self::new(s)
349 }
350}
351
352impl From<RoomName> for String {
353 fn from(n: RoomName) -> String {
354 n.0
355 }
356}
357
358impl std::fmt::Display for RoomName {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 self.0.fmt(f)
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
371#[serde(try_from = "String", into = "String")]
372pub struct PermissionString(String);
373
374impl PermissionString {
375 #[must_use = "the validated PermissionString must be used"]
383 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
384 let s = s.into();
385 if s.len() > MAX_ROOM_STRING_BYTES {
386 return Err(RoomStringError {
387 len: s.len(),
388 max: MAX_ROOM_STRING_BYTES,
389 });
390 }
391 Ok(Self(s))
392 }
393
394 #[must_use]
396 pub fn as_str(&self) -> &str {
397 &self.0
398 }
399}
400
401impl TryFrom<String> for PermissionString {
402 type Error = RoomStringError;
403 fn try_from(s: String) -> Result<Self, Self::Error> {
404 Self::new(s)
405 }
406}
407
408impl From<PermissionString> for String {
409 fn from(p: PermissionString) -> String {
410 p.0
411 }
412}
413
414impl std::fmt::Display for PermissionString {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 self.0.fmt(f)
417 }
418}
419
420#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
422pub enum RoomAccessPolicy {
423 Open,
425 Permission(PermissionString),
430 InviteOnly,
432 Locked,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct RoomDefinition {
439 pub name: RoomName,
444 pub capacity: u32,
445 pub access: RoomAccessPolicy,
446 pub is_template: bool,
447}
448
449#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
451pub struct RoomBounds {
452 pub min_x: f32,
453 pub min_y: f32,
454 pub max_x: f32,
455 pub max_y: f32,
456}
457
458#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
460pub struct RoomMembership(pub NetworkId);
461
462use std::sync::atomic::{AtomicU64, Ordering};
463use thiserror::Error;
464
465#[derive(Debug, Error, PartialEq, Eq)]
466pub enum AllocatorError {
467 #[error("NetworkId overflow (reached u64::MAX)")]
468 Overflow,
469 #[error("NetworkId allocator exhausted (reached limit)")]
470 Exhausted,
471}
472
473#[derive(Debug)]
478pub struct NetworkIdAllocator {
479 start_id: u64,
480 next: AtomicU64,
481}
482
483impl Default for NetworkIdAllocator {
484 fn default() -> Self {
485 Self::new(1)
486 }
487}
488
489impl NetworkIdAllocator {
490 #[must_use]
492 pub fn new(start_id: u64) -> Self {
493 Self {
494 start_id,
495 next: AtomicU64::new(start_id),
496 }
497 }
498
499 pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
504 let val = self
505 .next
506 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
507 if curr == u64::MAX {
508 None
509 } else {
510 Some(curr + 1)
511 }
512 })
513 .map_err(|_| AllocatorError::Overflow)?;
514
515 if val == 0 {
516 return Err(AllocatorError::Exhausted);
517 }
518
519 Ok(NetworkId(val))
520 }
521
522 pub fn reset(&self) {
525 self.next.store(self.start_id, Ordering::Relaxed);
526 }
527}
528
529#[cfg(test)]
530mod tests {
531 use super::*;
532
533 #[test]
534 fn test_primitive_derives() {
535 let nid1 = NetworkId(42);
536 let nid2 = nid1;
537 assert_eq!(nid1, nid2);
538
539 let lid1 = LocalId(42);
540 let lid2 = LocalId(42);
541 assert_eq!(lid1, lid2);
542
543 let cid = ClientId(99);
544 assert_eq!(format!("{cid:?}"), "ClientId(99)");
545
546 let kind = ComponentKind(1);
547 assert_eq!(kind.0, 1);
548 }
549
550 #[test]
551 fn test_input_command_clamping() {
552 let cmd = InputCommand {
553 tick: 1,
554 actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
555 actions_mask: 0,
556 last_seen_input_tick: None,
557 };
558 let clamped = cmd.clamped();
559 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
560 assert!((x - 1.0).abs() < f32::EPSILON);
561 assert!((y - -1.0).abs() < f32::EPSILON);
562 } else {
563 panic!("Expected Move action");
564 }
565
566 let valid = InputCommand {
567 tick: 1,
568 actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
569 actions_mask: 0,
570 last_seen_input_tick: None,
571 };
572 let clamped = valid.clamped();
573 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
574 assert!((x - 0.5).abs() < f32::EPSILON);
575 assert!((y - -0.2).abs() < f32::EPSILON);
576 } else {
577 panic!("Expected Move action");
578 }
579 }
580
581 #[test]
582 fn test_ship_stats_non_zero_default() {
583 let stats = ShipStats::default();
584 assert!(stats.max_hp > 0);
585 assert!(stats.max_shield > 0);
586 assert!(stats.max_energy > 0);
587 assert_eq!(stats.hp, stats.max_hp);
588 }
589}