pub const PROTOCOL_VERSION: u32 = 3;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct NetworkId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LocalId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ClientId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ComponentKind(pub u16);
pub const INPUT_COMMAND_KIND: ComponentKind = ComponentKind(128);
pub const WORKSPACE_DEFINITION_KIND: ComponentKind = ComponentKind(129);
pub const WORKSPACE_BOUNDS_KIND: ComponentKind = ComponentKind(130);
pub const WORKSPACE_MEMBERSHIP_KIND: ComponentKind = ComponentKind(131);
pub const EXTRACTION_BEAM_KIND: ComponentKind = ComponentKind(1024);
pub const DATA_STORE_KIND: ComponentKind = ComponentKind(1025);
pub const RESOURCE_KIND: ComponentKind = ComponentKind(1026);
pub const TOOL_KIND: ComponentKind = ComponentKind(1027);
pub const PRIORITY_POOL_KIND: ComponentKind = ComponentKind(1028);
pub const INTEGRITY_POOL_KIND: ComponentKind = ComponentKind(1029);
pub const DATA_DROP_KIND: ComponentKind = ComponentKind(1030);
pub const BEAM_MARKER_KIND: ComponentKind = ComponentKind(13);
pub const ACTION_USE_TOOL: u32 = 1 << 2;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[repr(C)]
pub struct Transform {
pub x: f32,
pub y: f32,
pub z: f32,
pub rotation: f32,
pub entity_type: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum AgentKind {
Standard = 0,
Heavy = 1,
Carrier = 2,
}
pub const ENTITY_TYPE_AGENT: u16 = 1;
pub const ENTITY_TYPE_AI_AGENT: u16 = 2;
pub const ENTITY_TYPE_HEAVY_AGENT: u16 = 3;
pub const ENTITY_TYPE_CARRIER_AGENT: u16 = 4;
pub const ENTITY_TYPE_RESOURCE: u16 = 5;
pub const ENTITY_TYPE_DATA_DROP: u16 = 6;
pub const ENTITY_TYPE_TRAINING_TARGET: u16 = 10;
pub const ENTITY_TYPE_BEAM: u16 = 20;
#[must_use]
pub const fn get_default_properties(entity_type: u16) -> (u16, u16) {
match entity_type {
ENTITY_TYPE_AGENT | ENTITY_TYPE_AI_AGENT => (200, 100),
ENTITY_TYPE_HEAVY_AGENT => (1500, 500),
ENTITY_TYPE_CARRIER_AGENT => (600, 200),
ENTITY_TYPE_RESOURCE => (500, 0),
ENTITY_TYPE_TRAINING_TARGET => (100, 50),
ENTITY_TYPE_DATA_DROP | ENTITY_TYPE_BEAM => (1, 0),
_ => (100, 100),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ToolId(pub u8);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct ZoneId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum PayloadType {
RawPayload = 0,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum InteractionBeamType {
PulseBeam = 0,
TrackingBeam = 1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum AIState {
Patrol = 0,
Aggro = 1,
Combat = 2,
Return = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum RespawnLocation {
NearestSafeZone,
Station(u64),
Coordinate(f32, f32),
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub enum PlayerInputKind {
Move { x: f32, y: f32 },
ToggleExtraction { target: NetworkId },
FireTool,
CursorMove {
x: f32,
y: f32,
},
}
pub const MAX_ACTIONS: usize = 128;
pub const ALLOWED_ACTIONS_MASK: u32 = ACTION_USE_TOOL;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputCommand {
pub tick: u64,
pub actions: Vec<PlayerInputKind>,
#[serde(default)]
pub actions_mask: u32,
pub last_seen_input_tick: Option<u64>,
}
impl InputCommand {
#[must_use]
pub fn clamped(mut self) -> Self {
for action in &mut self.actions {
match action {
PlayerInputKind::Move { x, y } => {
*x = x.clamp(-1.0, 1.0);
*y = y.clamp(-1.0, 1.0);
}
PlayerInputKind::CursorMove { x, y } => {
*x = x.clamp(0.0, 1.0);
*y = y.clamp(0.0, 1.0);
}
PlayerInputKind::ToggleExtraction { .. } | PlayerInputKind::FireTool => {}
}
}
self
}
pub fn validate(&self) -> Result<(), &'static str> {
if self.actions.len() > MAX_ACTIONS {
return Err("Too many actions in InputCommand");
}
if (self.actions_mask & !ALLOWED_ACTIONS_MASK) != 0 {
return Err("Unknown bits in actions_mask");
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct ExtractionBeam {
pub active: bool,
pub target: Option<NetworkId>,
#[serde(default)]
pub extraction_range: f32,
#[serde(default)]
pub base_extraction_rate: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct DataStore {
pub payload_count: u16,
pub capacity: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct Resource {
pub payload_remaining: u16,
pub total_capacity: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct Tool {
pub cooldown_ticks: u16,
pub last_fired_tick: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct PriorityPool {
pub current: u16,
pub max: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct IntegrityPool {
pub current: u16,
pub max: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub struct DataDrop {
pub amount: u16,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AgentProperties {
pub integrity: u16,
pub max_integrity: u16,
pub priority: u16,
pub max_priority: u16,
pub energy: u16,
pub max_energy: u16,
pub priority_regen_per_s: u16,
pub energy_regen_per_s: u16,
}
impl Default for AgentProperties {
fn default() -> Self {
Self {
integrity: 100,
max_integrity: 100,
priority: 100,
max_priority: 100,
energy: 100,
max_energy: 100,
priority_regen_per_s: 0,
energy_regen_per_s: 0,
}
}
}
pub const MAX_WORKSPACE_STRING_BYTES: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
pub struct WorkspaceStringError {
pub len: usize,
pub max: usize,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct WorkspaceName(String);
fn validate_workspace_string(s: &str) -> Result<(), WorkspaceStringError> {
if s.len() > MAX_WORKSPACE_STRING_BYTES {
return Err(WorkspaceStringError {
len: s.len(),
max: MAX_WORKSPACE_STRING_BYTES,
});
}
Ok(())
}
impl WorkspaceName {
#[must_use = "the validated WorkspaceName must be used"]
pub fn new(s: impl Into<String>) -> Result<Self, WorkspaceStringError> {
let s = s.into();
validate_workspace_string(&s)?;
Ok(Self(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for WorkspaceName {
type Error = WorkspaceStringError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<WorkspaceName> for String {
fn from(n: WorkspaceName) -> String {
n.0
}
}
impl std::fmt::Display for WorkspaceName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct PermissionString(String);
impl PermissionString {
#[must_use = "the validated PermissionString must be used"]
pub fn new(s: impl Into<String>) -> Result<Self, WorkspaceStringError> {
let s = s.into();
validate_workspace_string(&s)?;
Ok(Self(s))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for PermissionString {
type Error = WorkspaceStringError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(s)
}
}
impl From<PermissionString> for String {
fn from(p: PermissionString) -> String {
p.0
}
}
impl std::fmt::Display for PermissionString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum WorkspaceAccessPolicy {
Open,
Permission(PermissionString),
InviteOnly,
Locked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceDefinition {
pub name: WorkspaceName,
pub capacity: u32,
pub access: WorkspaceAccessPolicy,
pub is_template: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct WorkspaceBounds {
pub min_x: f32,
pub min_y: f32,
pub max_x: f32,
pub max_y: f32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct WorkspaceMembership(pub NetworkId);
use std::sync::atomic::{AtomicU64, Ordering};
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum AllocatorError {
#[error("NetworkId overflow (reached u64::MAX)")]
Overflow,
#[error("NetworkId allocator exhausted (reached limit)")]
Exhausted,
}
#[derive(Debug)]
pub struct NetworkIdAllocator {
start_id: u64,
next: AtomicU64,
}
impl Default for NetworkIdAllocator {
fn default() -> Self {
Self::new(1)
}
}
impl NetworkIdAllocator {
#[must_use]
pub fn new(start_id: u64) -> Self {
Self {
start_id,
next: AtomicU64::new(start_id),
}
}
pub fn allocate(&self) -> Result<NetworkId, AllocatorError> {
let val = self
.next
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |curr| {
if curr == u64::MAX {
None
} else {
Some(curr + 1)
}
})
.map_err(|_| AllocatorError::Overflow)?;
if val == 0 {
return Err(AllocatorError::Exhausted);
}
Ok(NetworkId(val))
}
pub fn reset(&self) {
self.next.store(self.start_id, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_primitive_derives() {
let nid1 = NetworkId(42);
let nid2 = nid1;
assert_eq!(nid1, nid2);
let lid1 = LocalId(42);
let lid2 = LocalId(42);
assert_eq!(lid1, lid2);
let cid = ClientId(99);
assert_eq!(format!("{cid:?}"), "ClientId(99)");
let kind = ComponentKind(1);
assert_eq!(kind.0, 1);
}
#[test]
fn test_input_command_clamping() {
let cmd = InputCommand {
tick: 1,
actions: vec![PlayerInputKind::Move { x: 2.0, y: -5.0 }],
actions_mask: 0,
last_seen_input_tick: None,
};
let clamped = cmd.clamped();
if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
assert!((x - 1.0).abs() < f32::EPSILON);
assert!((y - -1.0).abs() < f32::EPSILON);
} else {
panic!("Expected Move action");
}
let valid = InputCommand {
tick: 1,
actions: vec![PlayerInputKind::Move { x: 0.5, y: -0.2 }],
actions_mask: 0,
last_seen_input_tick: None,
};
let clamped = valid.clamped();
if let PlayerInputKind::Move { x, y } = clamped.actions[0] {
assert!((x - 0.5).abs() < f32::EPSILON);
assert!((y - -0.2).abs() < f32::EPSILON);
} else {
panic!("Expected Move action");
}
}
#[test]
fn test_agent_properties_non_zero_default() {
let properties = AgentProperties::default();
assert!(properties.max_integrity > 0);
assert!(properties.max_priority > 0);
assert!(properties.max_energy > 0);
assert_eq!(properties.integrity, properties.max_integrity);
}
#[test]
fn test_get_default_properties() {
assert_eq!(get_default_properties(ENTITY_TYPE_AGENT), (200, 100));
assert_eq!(get_default_properties(ENTITY_TYPE_AI_AGENT), (200, 100));
assert_eq!(get_default_properties(ENTITY_TYPE_HEAVY_AGENT), (1500, 500));
assert_eq!(
get_default_properties(ENTITY_TYPE_CARRIER_AGENT),
(600, 200)
);
assert_eq!(get_default_properties(ENTITY_TYPE_RESOURCE), (500, 0));
assert_eq!(get_default_properties(ENTITY_TYPE_DATA_DROP), (1, 0));
assert_eq!(
get_default_properties(ENTITY_TYPE_TRAINING_TARGET),
(100, 50)
);
assert_eq!(get_default_properties(ENTITY_TYPE_BEAM), (1, 0));
assert_eq!(get_default_properties(999), (100, 100)); }
}