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 WORKSPACE_DEFINITION_KIND: ComponentKind = ComponentKind(129);
44
45pub const WORKSPACE_BOUNDS_KIND: ComponentKind = ComponentKind(130);
47
48pub const WORKSPACE_MEMBERSHIP_KIND: ComponentKind = ComponentKind(131);
50
51pub const EXTRACTION_BEAM_KIND: ComponentKind = ComponentKind(1024);
53
54pub const DATA_STORE_KIND: ComponentKind = ComponentKind(1025);
56
57pub const RESOURCE_KIND: ComponentKind = ComponentKind(1026);
59
60pub const TOOL_KIND: ComponentKind = ComponentKind(1027);
62
63pub const PRIORITY_POOL_KIND: ComponentKind = ComponentKind(1028);
65
66pub const INTEGRITY_POOL_KIND: ComponentKind = ComponentKind(1029);
68
69pub const DATA_DROP_KIND: ComponentKind = ComponentKind(1030);
71
72pub const BEAM_MARKER_KIND: ComponentKind = ComponentKind(13);
76
77pub const ACTION_USE_TOOL: u32 = 1 << 2;
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
82#[repr(C)]
83pub struct Transform {
84 pub x: f32,
86 pub y: f32,
88 pub z: f32,
90 pub rotation: f32,
92 pub entity_type: u16,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
98#[repr(u8)]
99pub enum AgentKind {
100 Standard = 0,
101 Heavy = 1,
102 Carrier = 2,
103}
104
105pub const ENTITY_TYPE_AGENT: u16 = 1;
107pub const ENTITY_TYPE_AI_AGENT: u16 = 2;
108pub const ENTITY_TYPE_HEAVY_AGENT: u16 = 3;
109pub const ENTITY_TYPE_CARRIER_AGENT: u16 = 4;
110pub const ENTITY_TYPE_RESOURCE: u16 = 5;
111pub const ENTITY_TYPE_DATA_DROP: u16 = 6;
112pub const ENTITY_TYPE_TRAINING_TARGET: u16 = 10;
113pub const ENTITY_TYPE_BEAM: u16 = 20;
114
115#[must_use]
120pub const fn get_default_properties(entity_type: u16) -> (u16, u16) {
121 match entity_type {
122 ENTITY_TYPE_AGENT | ENTITY_TYPE_AI_AGENT => (200, 100),
123 ENTITY_TYPE_HEAVY_AGENT => (1500, 500),
124 ENTITY_TYPE_CARRIER_AGENT => (600, 200),
125 ENTITY_TYPE_RESOURCE => (500, 0),
126 ENTITY_TYPE_TRAINING_TARGET => (100, 50),
127 ENTITY_TYPE_DATA_DROP | ENTITY_TYPE_BEAM => (1, 0),
128 _ => (100, 100),
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
134pub struct ToolId(pub u8);
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
138pub struct ZoneId(pub u64);
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[repr(u8)]
143pub enum PayloadType {
144 RawPayload = 0,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[repr(u8)]
150pub enum InteractionBeamType {
151 PulseBeam = 0,
152 TrackingBeam = 1,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[repr(u8)]
158pub enum AIState {
159 Patrol = 0,
160 Aggro = 1,
161 Combat = 2,
162 Return = 3,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
167pub enum RespawnLocation {
168 NearestSafeZone,
170 Station(u64),
172 Coordinate(f32, f32),
174}
175
176#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
178pub enum PlayerInputKind {
179 Move { x: f32, y: f32 },
181 ToggleExtraction { target: NetworkId },
183 FireTool,
185 CursorMove {
187 x: f32,
189 y: f32,
191 },
192}
193
194pub const MAX_ACTIONS: usize = 128;
197
198pub const ALLOWED_ACTIONS_MASK: u32 = ACTION_USE_TOOL;
200
201#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct InputCommand {
204 pub tick: u64,
206 pub actions: Vec<PlayerInputKind>,
208 #[serde(default)]
210 pub actions_mask: u32,
211 pub last_seen_input_tick: Option<u64>,
213}
214
215impl InputCommand {
216 #[must_use]
218 pub fn clamped(mut self) -> Self {
219 for action in &mut self.actions {
220 match action {
221 PlayerInputKind::Move { x, y } => {
222 *x = x.clamp(-1.0, 1.0);
223 *y = y.clamp(-1.0, 1.0);
224 }
225 PlayerInputKind::CursorMove { x, y } => {
226 *x = x.clamp(0.0, 1.0);
227 *y = y.clamp(0.0, 1.0);
228 }
229 PlayerInputKind::ToggleExtraction { .. } | PlayerInputKind::FireTool => {}
230 }
231 }
232 self
233 }
234
235 pub fn validate(&self) -> Result<(), &'static str> {
240 if self.actions.len() > MAX_ACTIONS {
241 return Err("Too many actions in InputCommand");
242 }
243 if (self.actions_mask & !ALLOWED_ACTIONS_MASK) != 0 {
244 return Err("Unknown bits in actions_mask");
245 }
246 Ok(())
247 }
248}
249
250#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
252pub struct ExtractionBeam {
253 pub active: bool,
254 pub target: Option<NetworkId>,
255 #[serde(default)]
256 pub extraction_range: f32,
257 #[serde(default)]
258 pub base_extraction_rate: u16,
259}
260
261#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
263pub struct DataStore {
264 pub payload_count: u16,
265 pub capacity: u16,
266}
267
268#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
269pub struct Resource {
270 pub payload_remaining: u16,
271 pub total_capacity: u16,
272}
273
274#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
276pub struct Tool {
277 pub cooldown_ticks: u16,
278 pub last_fired_tick: u64,
279}
280
281#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
283pub struct PriorityPool {
284 pub current: u16,
285 pub max: u16,
286}
287
288#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
290pub struct IntegrityPool {
291 pub current: u16,
292 pub max: u16,
293}
294
295#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
297pub struct DataDrop {
298 pub amount: u16,
299}
300
301#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
307pub struct AgentProperties {
308 pub integrity: u16,
309 pub max_integrity: u16,
310 pub priority: u16,
311 pub max_priority: u16,
312 pub energy: u16,
313 pub max_energy: u16,
314 pub priority_regen_per_s: u16,
315 pub energy_regen_per_s: u16,
316}
317
318impl Default for AgentProperties {
319 fn default() -> Self {
321 Self {
322 integrity: 100,
323 max_integrity: 100,
324 priority: 100,
325 max_priority: 100,
326 energy: 100,
327 max_energy: 100,
328 priority_regen_per_s: 0,
329 energy_regen_per_s: 0,
330 }
331 }
332}
333
334pub const MAX_WORKSPACE_STRING_BYTES: usize = 64;
339
340#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
343#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
344pub struct WorkspaceStringError {
345 pub len: usize,
347 pub max: usize,
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
358#[serde(try_from = "String", into = "String")]
359pub struct WorkspaceName(String);
360
361fn validate_workspace_string(s: &str) -> Result<(), WorkspaceStringError> {
362 if s.len() > MAX_WORKSPACE_STRING_BYTES {
363 return Err(WorkspaceStringError {
364 len: s.len(),
365 max: MAX_WORKSPACE_STRING_BYTES,
366 });
367 }
368 Ok(())
369}
370
371impl WorkspaceName {
372 #[must_use = "the validated WorkspaceName must be used"]
380 pub fn new(s: impl Into<String>) -> Result<Self, WorkspaceStringError> {
381 let s = s.into();
382 validate_workspace_string(&s)?;
383 Ok(Self(s))
384 }
385
386 #[must_use]
388 pub fn as_str(&self) -> &str {
389 &self.0
390 }
391}
392
393impl TryFrom<String> for WorkspaceName {
394 type Error = WorkspaceStringError;
395 fn try_from(s: String) -> Result<Self, Self::Error> {
396 Self::new(s)
397 }
398}
399
400impl From<WorkspaceName> for String {
401 fn from(n: WorkspaceName) -> String {
402 n.0
403 }
404}
405
406impl std::fmt::Display for WorkspaceName {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 self.0.fmt(f)
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
419#[serde(try_from = "String", into = "String")]
420pub struct PermissionString(String);
421
422impl PermissionString {
423 #[must_use = "the validated PermissionString must be used"]
431 pub fn new(s: impl Into<String>) -> Result<Self, WorkspaceStringError> {
432 let s = s.into();
433 validate_workspace_string(&s)?;
434 Ok(Self(s))
435 }
436
437 #[must_use]
439 pub fn as_str(&self) -> &str {
440 &self.0
441 }
442}
443
444impl TryFrom<String> for PermissionString {
445 type Error = WorkspaceStringError;
446 fn try_from(s: String) -> Result<Self, Self::Error> {
447 Self::new(s)
448 }
449}
450
451impl From<PermissionString> for String {
452 fn from(p: PermissionString) -> String {
453 p.0
454 }
455}
456
457impl std::fmt::Display for PermissionString {
458 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
459 self.0.fmt(f)
460 }
461}
462
463#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
465pub enum WorkspaceAccessPolicy {
466 Open,
468 Permission(PermissionString),
473 InviteOnly,
475 Locked,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct WorkspaceDefinition {
482 pub name: WorkspaceName,
487 pub capacity: u32,
488 pub access: WorkspaceAccessPolicy,
489 pub is_template: bool,
490}
491
492#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
494pub struct WorkspaceBounds {
495 pub min_x: f32,
496 pub min_y: f32,
497 pub max_x: f32,
498 pub max_y: f32,
499}
500
501#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
503pub struct WorkspaceMembership(pub NetworkId);
504
505use std::sync::atomic::{AtomicU64, Ordering};
506use thiserror::Error;
507
508#[derive(Debug, Error, PartialEq, Eq)]
509pub enum AllocatorError {
510 #[error("NetworkId overflow (reached u64::MAX)")]
511 Overflow,
512 #[error("NetworkId allocator exhausted (reached limit)")]
513 Exhausted,
514}
515
516#[derive(Debug)]
521pub struct NetworkIdAllocator {
522 start_id: u64,
523 next: AtomicU64,
524}
525
526impl Default for NetworkIdAllocator {
527 fn default() -> Self {
528 Self::new(1)
529 }
530}
531
532impl NetworkIdAllocator {
533 #[must_use]
535 pub fn new(start_id: u64) -> Self {
536 Self {
537 start_id,
538 next: AtomicU64::new(start_id),
539 }
540 }
541
542 pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
547 let val = self
548 .next
549 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
550 if curr == u64::MAX {
551 None
552 } else {
553 Some(curr + 1)
554 }
555 })
556 .map_err(|_| AllocatorError::Overflow)?;
557
558 if val == 0 {
559 return Err(AllocatorError::Exhausted);
560 }
561
562 Ok(NetworkId(val))
563 }
564
565 pub fn reset(&self) {
568 self.next.store(self.start_id, Ordering::Relaxed);
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn test_primitive_derives() {
578 let nid1 = NetworkId(42);
579 let nid2 = nid1;
580 assert_eq!(nid1, nid2);
581
582 let lid1 = LocalId(42);
583 let lid2 = LocalId(42);
584 assert_eq!(lid1, lid2);
585
586 let cid = ClientId(99);
587 assert_eq!(format!("{cid:?}"), "ClientId(99)");
588
589 let kind = ComponentKind(1);
590 assert_eq!(kind.0, 1);
591 }
592
593 #[test]
594 fn test_input_command_clamping() {
595 let cmd = InputCommand {
596 tick: 1,
597 actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
598 actions_mask: 0,
599 last_seen_input_tick: None,
600 };
601 let clamped = cmd.clamped();
602 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
603 assert!((x - 1.0).abs() < f32::EPSILON);
604 assert!((y - -1.0).abs() < f32::EPSILON);
605 } else {
606 panic!("Expected Move action");
607 }
608
609 let valid = InputCommand {
610 tick: 1,
611 actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
612 actions_mask: 0,
613 last_seen_input_tick: None,
614 };
615 let clamped = valid.clamped();
616 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
617 assert!((x - 0.5).abs() < f32::EPSILON);
618 assert!((y - -0.2).abs() < f32::EPSILON);
619 } else {
620 panic!("Expected Move action");
621 }
622 }
623
624 #[test]
625 fn test_agent_properties_non_zero_default() {
626 let properties = AgentProperties::default();
627 assert!(properties.max_integrity > 0);
628 assert!(properties.max_priority > 0);
629 assert!(properties.max_energy > 0);
630 assert_eq!(properties.integrity, properties.max_integrity);
631 }
632
633 #[test]
634 fn test_get_default_properties() {
635 assert_eq!(get_default_properties(ENTITY_TYPE_AGENT), (200, 100));
636 assert_eq!(get_default_properties(ENTITY_TYPE_AI_AGENT), (200, 100));
637 assert_eq!(get_default_properties(ENTITY_TYPE_HEAVY_AGENT), (1500, 500));
638 assert_eq!(
639 get_default_properties(ENTITY_TYPE_CARRIER_AGENT),
640 (600, 200)
641 );
642 assert_eq!(get_default_properties(ENTITY_TYPE_RESOURCE), (500, 0));
643 assert_eq!(get_default_properties(ENTITY_TYPE_DATA_DROP), (1, 0));
644 assert_eq!(
645 get_default_properties(ENTITY_TYPE_TRAINING_TARGET),
646 (100, 50)
647 );
648 assert_eq!(get_default_properties(ENTITY_TYPE_BEAM), (1, 0));
649 assert_eq!(get_default_properties(999), (100, 100)); }
651}