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);
13pub const MIN_DYNAMIC_NETWORK_ID: u64 = 100;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct LocalId(pub u64);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
25pub struct ClientId(pub u64);
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
39pub struct ComponentKind(pub u16);
40
41pub const INPUT_COMMAND_KIND: ComponentKind = ComponentKind(128);
44
45pub const WORKSPACE_DEFINITION_KIND: ComponentKind = ComponentKind(129);
47
48pub const WORKSPACE_BOUNDS_KIND: ComponentKind = ComponentKind(130);
50
51pub const WORKSPACE_MEMBERSHIP_KIND: ComponentKind = ComponentKind(131);
53
54pub const EXTRACTION_BEAM_KIND: ComponentKind = ComponentKind(1024);
56
57pub const DATA_STORE_KIND: ComponentKind = ComponentKind(1025);
59
60pub const RESOURCE_KIND: ComponentKind = ComponentKind(1026);
62
63pub const TOOL_KIND: ComponentKind = ComponentKind(1027);
65
66pub const PRIORITY_POOL_KIND: ComponentKind = ComponentKind(1028);
68
69pub const INTEGRITY_POOL_KIND: ComponentKind = ComponentKind(1029);
71
72pub const DATA_DROP_KIND: ComponentKind = ComponentKind(1030);
74
75pub const BEAM_MARKER_KIND: ComponentKind = ComponentKind(13);
79
80pub const ACTION_USE_TOOL: u32 = 1 << 2;
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
85#[repr(C)]
86pub struct Transform {
87 pub x: f32,
89 pub y: f32,
91 pub z: f32,
93 pub rotation: f32,
95 pub entity_type: u16,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101#[repr(u8)]
102pub enum AgentKind {
103 Standard = 0,
104 Heavy = 1,
105 Carrier = 2,
106}
107
108pub const ENTITY_TYPE_AGENT: u16 = 1;
110pub const ENTITY_TYPE_AI_AGENT: u16 = 2;
111pub const ENTITY_TYPE_HEAVY_AGENT: u16 = 3;
112pub const ENTITY_TYPE_CARRIER_AGENT: u16 = 4;
113pub const ENTITY_TYPE_RESOURCE: u16 = 5;
114pub const ENTITY_TYPE_DATA_DROP: u16 = 6;
115pub const ENTITY_TYPE_TRAINING_TARGET: u16 = 10;
116pub const ENTITY_TYPE_BEAM: u16 = 20;
117
118#[must_use]
123pub const fn get_default_properties(entity_type: u16) -> (u16, u16) {
124 match entity_type {
125 ENTITY_TYPE_AGENT | ENTITY_TYPE_AI_AGENT => (200, 100),
126 ENTITY_TYPE_HEAVY_AGENT => (1500, 500),
127 ENTITY_TYPE_CARRIER_AGENT => (600, 200),
128 ENTITY_TYPE_RESOURCE => (500, 0),
129 ENTITY_TYPE_TRAINING_TARGET => (100, 50),
130 ENTITY_TYPE_DATA_DROP | ENTITY_TYPE_BEAM => (1, 0),
131 _ => (100, 100),
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
137pub struct ToolId(pub u8);
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
141pub struct ZoneId(pub u64);
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145#[repr(u8)]
146pub enum PayloadType {
147 RawPayload = 0,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
152#[repr(u8)]
153pub enum InteractionBeamType {
154 PulseBeam = 0,
155 TrackingBeam = 1,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[repr(u8)]
161pub enum AIState {
162 Patrol = 0,
163 Aggro = 1,
164 Combat = 2,
165 Return = 3,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
170pub enum RespawnLocation {
171 NearestSafeZone,
173 Station(u64),
175 Coordinate(f32, f32),
177}
178
179#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
181pub enum PlayerInputKind {
182 Move { x: f32, y: f32 },
184 ToggleExtraction { target: NetworkId },
186 FireTool,
188 CursorMove {
190 x: f32,
192 y: f32,
194 },
195}
196
197pub const MAX_ACTIONS: usize = 128;
200
201pub const ALLOWED_ACTIONS_MASK: u32 = ACTION_USE_TOOL;
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct InputCommand {
207 pub tick: u64,
209 pub actions: Vec<PlayerInputKind>,
211 #[serde(default)]
213 pub actions_mask: u32,
214 pub last_seen_input_tick: Option<u64>,
216}
217
218impl InputCommand {
219 #[must_use]
221 pub fn clamped(mut self) -> Self {
222 for action in &mut self.actions {
223 match action {
224 PlayerInputKind::Move { x, y } => {
225 *x = x.clamp(-1.0, 1.0);
226 *y = y.clamp(-1.0, 1.0);
227 }
228 PlayerInputKind::CursorMove { x, y } => {
229 *x = x.clamp(0.0, 1.0);
230 *y = y.clamp(0.0, 1.0);
231 }
232 PlayerInputKind::ToggleExtraction { .. } | PlayerInputKind::FireTool => {}
233 }
234 }
235 self
236 }
237
238 pub fn validate(&self) -> Result<(), &'static str> {
243 if self.actions.len() > MAX_ACTIONS {
244 return Err("Too many actions in InputCommand");
245 }
246 if (self.actions_mask & !ALLOWED_ACTIONS_MASK) != 0 {
247 return Err("Unknown bits in actions_mask");
248 }
249 Ok(())
250 }
251}
252
253#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
255pub struct ExtractionBeam {
256 pub active: bool,
257 pub target: Option<NetworkId>,
258 #[serde(default)]
259 pub extraction_range: f32,
260 #[serde(default)]
261 pub base_extraction_rate: u16,
262}
263
264#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
266pub struct DataStore {
267 pub payload_count: u16,
268 pub capacity: u16,
269}
270
271#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
272pub struct Resource {
273 pub payload_remaining: u16,
274 pub total_capacity: u16,
275}
276
277#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
279pub struct Tool {
280 pub cooldown_ticks: u16,
281 pub last_fired_tick: u64,
282}
283
284#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
286pub struct PriorityPool {
287 pub current: u16,
288 pub max: u16,
289}
290
291#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
293pub struct IntegrityPool {
294 pub current: u16,
295 pub max: u16,
296}
297
298#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
300pub struct DataDrop {
301 pub amount: u16,
302}
303
304#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
310pub struct AgentProperties {
311 pub integrity: u16,
312 pub max_integrity: u16,
313 pub priority: u16,
314 pub max_priority: u16,
315 pub energy: u16,
316 pub max_energy: u16,
317 pub priority_regen_per_s: u16,
318 pub energy_regen_per_s: u16,
319}
320
321impl Default for AgentProperties {
322 fn default() -> Self {
324 Self {
325 integrity: 100,
326 max_integrity: 100,
327 priority: 100,
328 max_priority: 100,
329 energy: 100,
330 max_energy: 100,
331 priority_regen_per_s: 0,
332 energy_regen_per_s: 0,
333 }
334 }
335}
336
337pub const MAX_WORKSPACE_STRING_BYTES: usize = 64;
342
343#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
346#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
347pub struct WorkspaceStringError {
348 pub len: usize,
350 pub max: usize,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
361#[serde(try_from = "String", into = "String")]
362pub struct WorkspaceName(String);
363
364fn validate_workspace_string(s: &str) -> Result<(), WorkspaceStringError> {
365 if s.len() > MAX_WORKSPACE_STRING_BYTES {
366 return Err(WorkspaceStringError {
367 len: s.len(),
368 max: MAX_WORKSPACE_STRING_BYTES,
369 });
370 }
371 Ok(())
372}
373
374impl WorkspaceName {
375 #[must_use = "the validated WorkspaceName must be used"]
383 pub fn new(s: impl Into<String>) -> Result<Self, WorkspaceStringError> {
384 let s = s.into();
385 validate_workspace_string(&s)?;
386 Ok(Self(s))
387 }
388
389 #[must_use]
391 pub fn as_str(&self) -> &str {
392 &self.0
393 }
394}
395
396impl TryFrom<String> for WorkspaceName {
397 type Error = WorkspaceStringError;
398 fn try_from(s: String) -> Result<Self, Self::Error> {
399 Self::new(s)
400 }
401}
402
403impl From<WorkspaceName> for String {
404 fn from(n: WorkspaceName) -> String {
405 n.0
406 }
407}
408
409impl std::fmt::Display for WorkspaceName {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 self.0.fmt(f)
412 }
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
422#[serde(try_from = "String", into = "String")]
423pub struct PermissionString(String);
424
425impl PermissionString {
426 #[must_use = "the validated PermissionString must be used"]
434 pub fn new(s: impl Into<String>) -> Result<Self, WorkspaceStringError> {
435 let s = s.into();
436 validate_workspace_string(&s)?;
437 Ok(Self(s))
438 }
439
440 #[must_use]
442 pub fn as_str(&self) -> &str {
443 &self.0
444 }
445}
446
447impl TryFrom<String> for PermissionString {
448 type Error = WorkspaceStringError;
449 fn try_from(s: String) -> Result<Self, Self::Error> {
450 Self::new(s)
451 }
452}
453
454impl From<PermissionString> for String {
455 fn from(p: PermissionString) -> String {
456 p.0
457 }
458}
459
460impl std::fmt::Display for PermissionString {
461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462 self.0.fmt(f)
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
468pub enum WorkspaceAccessPolicy {
469 Open,
471 Permission(PermissionString),
476 InviteOnly,
478 Locked,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct WorkspaceDefinition {
485 pub name: WorkspaceName,
490 pub capacity: u32,
491 pub access: WorkspaceAccessPolicy,
492 pub is_template: bool,
493}
494
495#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
497pub struct WorkspaceBounds {
498 pub min_x: f32,
499 pub min_y: f32,
500 pub max_x: f32,
501 pub max_y: f32,
502}
503
504#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
506pub struct WorkspaceMembership(pub NetworkId);
507
508use std::sync::atomic::{AtomicU64, Ordering};
509use thiserror::Error;
510
511#[derive(Debug, Error, PartialEq, Eq)]
512pub enum AllocatorError {
513 #[error("NetworkId overflow (reached u64::MAX)")]
514 Overflow,
515 #[error("NetworkId allocator exhausted (reached limit)")]
516 Exhausted,
517}
518
519#[derive(Debug)]
524pub struct NetworkIdAllocator {
525 start_id: u64,
526 next: AtomicU64,
527}
528
529impl Default for NetworkIdAllocator {
530 fn default() -> Self {
531 Self::new(MIN_DYNAMIC_NETWORK_ID)
532 }
533}
534
535impl NetworkIdAllocator {
536 #[must_use]
538 pub fn new(start_id: u64) -> Self {
539 let start_id = start_id.max(MIN_DYNAMIC_NETWORK_ID);
540 Self {
541 start_id,
542 next: AtomicU64::new(start_id),
543 }
544 }
545
546 pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
551 let val = self
552 .next
553 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
554 if curr == u64::MAX {
555 None
556 } else {
557 Some(curr + 1)
558 }
559 })
560 .map_err(|_| AllocatorError::Overflow)?;
561
562 if val == 0 {
563 return Err(AllocatorError::Exhausted);
564 }
565
566 Ok(NetworkId(val))
567 }
568
569 pub fn reset(&self) {
572 self.next.store(self.start_id, Ordering::Relaxed);
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 #[test]
581 fn test_primitive_derives() {
582 let nid1 = NetworkId(42);
583 let nid2 = nid1;
584 assert_eq!(nid1, nid2);
585
586 let lid1 = LocalId(42);
587 let lid2 = LocalId(42);
588 assert_eq!(lid1, lid2);
589
590 let cid = ClientId(99);
591 assert_eq!(format!("{cid:?}"), "ClientId(99)");
592
593 let kind = ComponentKind(1);
594 assert_eq!(kind.0, 1);
595 }
596
597 #[test]
598 fn test_input_command_clamping() {
599 let cmd = InputCommand {
600 tick: 1,
601 actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
602 actions_mask: 0,
603 last_seen_input_tick: None,
604 };
605 let clamped = cmd.clamped();
606 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
607 assert!((x - 1.0).abs() < f32::EPSILON);
608 assert!((y - -1.0).abs() < f32::EPSILON);
609 } else {
610 panic!("Expected Move action");
611 }
612
613 let valid = InputCommand {
614 tick: 1,
615 actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
616 actions_mask: 0,
617 last_seen_input_tick: None,
618 };
619 let clamped = valid.clamped();
620 if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
621 assert!((x - 0.5).abs() < f32::EPSILON);
622 assert!((y - -0.2).abs() < f32::EPSILON);
623 } else {
624 panic!("Expected Move action");
625 }
626 }
627
628 #[test]
629 fn test_agent_properties_non_zero_default() {
630 let properties = AgentProperties::default();
631 assert!(properties.max_integrity > 0);
632 assert!(properties.max_priority > 0);
633 assert!(properties.max_energy > 0);
634 assert_eq!(properties.integrity, properties.max_integrity);
635 }
636
637 #[test]
638 fn test_get_default_properties() {
639 assert_eq!(get_default_properties(ENTITY_TYPE_AGENT), (200, 100));
640 assert_eq!(get_default_properties(ENTITY_TYPE_AI_AGENT), (200, 100));
641 assert_eq!(get_default_properties(ENTITY_TYPE_HEAVY_AGENT), (1500, 500));
642 assert_eq!(
643 get_default_properties(ENTITY_TYPE_CARRIER_AGENT),
644 (600, 200)
645 );
646 assert_eq!(get_default_properties(ENTITY_TYPE_RESOURCE), (500, 0));
647 assert_eq!(get_default_properties(ENTITY_TYPE_DATA_DROP), (1, 0));
648 assert_eq!(
649 get_default_properties(ENTITY_TYPE_TRAINING_TARGET),
650 (100, 50)
651 );
652 assert_eq!(get_default_properties(ENTITY_TYPE_BEAM), (1, 0));
653 assert_eq!(get_default_properties(999), (100, 100)); }
655
656 #[test]
657 fn test_network_id_allocator_boundary() {
658 let allocator = NetworkIdAllocator::default();
660 let id1 = allocator.allocate().unwrap();
661 assert_eq!(id1.0, MIN_DYNAMIC_NETWORK_ID);
662
663 let allocator_custom = NetworkIdAllocator::new(1);
665 let id_custom = allocator_custom.allocate().unwrap();
666 assert_eq!(id_custom.0, MIN_DYNAMIC_NETWORK_ID);
667
668 allocator_custom.allocate().unwrap();
670 allocator_custom.reset();
671 let id_reset = allocator_custom.allocate().unwrap();
672 assert_eq!(id_reset.0, MIN_DYNAMIC_NETWORK_ID);
673 }
674}