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}
186
187#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
189pub struct CargoHold {
190 pub ore_count: u16,
191 pub capacity: u16,
192}
193
194#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
196pub struct Asteroid {
197 pub ore_remaining: u16,
198 pub total_capacity: u16,
199}
200
201#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
207pub struct ShipStats {
208 pub hp: u16,
209 pub max_hp: u16,
210 pub shield: u16,
211 pub max_shield: u16,
212 pub energy: u16,
213 pub max_energy: u16,
214 pub shield_regen_per_s: u16,
215 pub energy_regen_per_s: u16,
216}
217
218impl Default for ShipStats {
219 fn default() -> Self {
221 Self {
222 hp: 100,
223 max_hp: 100,
224 shield: 100,
225 max_shield: 100,
226 energy: 100,
227 max_energy: 100,
228 shield_regen_per_s: 0,
229 energy_regen_per_s: 0,
230 }
231 }
232}
233
234pub const MAX_ROOM_STRING_BYTES: usize = 64;
239
240#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
243#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
244pub struct RoomStringError {
245 pub len: usize,
247 pub max: usize,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
258#[serde(try_from = "String", into = "String")]
259pub struct RoomName(String);
260
261impl RoomName {
262 #[must_use = "the validated RoomName must be used"]
270 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
271 let s = s.into();
272 if s.len() > MAX_ROOM_STRING_BYTES {
273 return Err(RoomStringError {
274 len: s.len(),
275 max: MAX_ROOM_STRING_BYTES,
276 });
277 }
278 Ok(Self(s))
279 }
280
281 #[must_use]
283 pub fn as_str(&self) -> &str {
284 &self.0
285 }
286}
287
288impl TryFrom<String> for RoomName {
289 type Error = RoomStringError;
290 fn try_from(s: String) -> Result<Self, Self::Error> {
291 Self::new(s)
292 }
293}
294
295impl From<RoomName> for String {
296 fn from(n: RoomName) -> String {
297 n.0
298 }
299}
300
301impl std::fmt::Display for RoomName {
302 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303 self.0.fmt(f)
304 }
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
314#[serde(try_from = "String", into = "String")]
315pub struct PermissionString(String);
316
317impl PermissionString {
318 #[must_use = "the validated PermissionString must be used"]
326 pub fn new(s: impl Into<String>) -> Result<Self, RoomStringError> {
327 let s = s.into();
328 if s.len() > MAX_ROOM_STRING_BYTES {
329 return Err(RoomStringError {
330 len: s.len(),
331 max: MAX_ROOM_STRING_BYTES,
332 });
333 }
334 Ok(Self(s))
335 }
336
337 #[must_use]
339 pub fn as_str(&self) -> &str {
340 &self.0
341 }
342}
343
344impl TryFrom<String> for PermissionString {
345 type Error = RoomStringError;
346 fn try_from(s: String) -> Result<Self, Self::Error> {
347 Self::new(s)
348 }
349}
350
351impl From<PermissionString> for String {
352 fn from(p: PermissionString) -> String {
353 p.0
354 }
355}
356
357impl std::fmt::Display for PermissionString {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 self.0.fmt(f)
360 }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
365pub enum RoomAccessPolicy {
366 Open,
368 Permission(PermissionString),
373 InviteOnly,
375 Locked,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct RoomDefinition {
382 pub name: RoomName,
387 pub capacity: u32,
388 pub access: RoomAccessPolicy,
389 pub is_template: bool,
390}
391
392#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
394pub struct RoomBounds {
395 pub min_x: f32,
396 pub min_y: f32,
397 pub max_x: f32,
398 pub max_y: f32,
399}
400
401#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
403pub struct RoomMembership(pub NetworkId);
404
405use std::sync::atomic::{AtomicU64, Ordering};
406use thiserror::Error;
407
408#[derive(Debug, Error, PartialEq, Eq)]
409pub enum AllocatorError {
410 #[error("NetworkId overflow (reached u64::MAX)")]
411 Overflow,
412 #[error("NetworkId allocator exhausted (reached limit)")]
413 Exhausted,
414}
415
416#[derive(Debug)]
421pub struct NetworkIdAllocator {
422 start_id: u64,
423 next: AtomicU64,
424}
425
426impl Default for NetworkIdAllocator {
427 fn default() -> Self {
428 Self::new(1)
429 }
430}
431
432impl NetworkIdAllocator {
433 #[must_use]
435 pub fn new(start_id: u64) -> Self {
436 Self {
437 start_id,
438 next: AtomicU64::new(start_id),
439 }
440 }
441
442 pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
447 let val = self
448 .next
449 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
450 if curr == u64::MAX {
451 None
452 } else {
453 Some(curr + 1)
454 }
455 })
456 .map_err(|_| AllocatorError::Overflow)?;
457
458 if val == 0 {
459 return Err(AllocatorError::Exhausted);
460 }
461
462 Ok(NetworkId(val))
463 }
464
465 pub fn reset(&self) {
468 self.next.store(self.start_id, Ordering::Relaxed);
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn test_primitive_derives() {
478 let nid1 = NetworkId(42);
479 let nid2 = nid1;
480 assert_eq!(nid1, nid2);
481
482 let lid1 = LocalId(42);
483 let lid2 = LocalId(42);
484 assert_eq!(lid1, lid2);
485
486 let cid = ClientId(99);
487 assert_eq!(format!("{cid:?}"), "ClientId(99)");
488
489 let kind = ComponentKind(1);
490 assert_eq!(kind.0, 1);
491 }
492
493 #[test]
494 fn test_input_command_clamping() {
495 let cmd = InputCommand {
496 tick: 1,
497 actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
498 last_seen_input_tick: None,
499 };
500 let clamped = cmd.clamped();
501 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
502 assert!((x - 1.0).abs() < f32::EPSILON);
503 assert!((y - -1.0).abs() < f32::EPSILON);
504 } else {
505 panic!("Expected Move action");
506 }
507
508 let valid = InputCommand {
509 tick: 1,
510 actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
511 last_seen_input_tick: None,
512 };
513 let clamped = valid.clamped();
514 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
515 assert!((x - 0.5).abs() < f32::EPSILON);
516 assert!((y - -0.2).abs() < f32::EPSILON);
517 } else {
518 panic!("Expected Move action");
519 }
520 }
521
522 #[test]
523 fn test_ship_stats_non_zero_default() {
524 let stats = ShipStats::default();
525 assert!(stats.max_hp > 0);
526 assert!(stats.max_shield > 0);
527 assert!(stats.max_energy > 0);
528 assert_eq!(stats.hp, stats.max_hp);
529 }
530}