Skip to main content

aetheris_protocol/
types.rs

1//! Protocol-level primitive types.
2pub const PROTOCOL_VERSION: u32 = 3;
3
4use serde::{Deserialize, Serialize};
5
6/// A globally unique entity identifier used in all network communication.
7/// Assigned by the server. Immutable for the lifetime of the entity.
8///
9/// This is NOT the ECS's internal entity ID. The `WorldState` adapter
10/// translates between `NetworkId` and the ECS's local handle.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub struct NetworkId(pub u64);
13
14/// The ECS's internal entity handle. Opaque to the network layer.
15/// In Phase 1 (Bevy), this wraps `bevy_ecs::entity::Entity`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17pub struct LocalId(pub u64);
18
19/// A unique identifier for a connected client session.
20/// Assigned by the transport layer on connection, released on disconnect.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22pub struct ClientId(pub u64);
23
24/// A component type identifier. Used by the Encoder to determine
25/// how to serialize/deserialize a specific component's fields.
26///
27/// In Phase 1, this is a simple enum discriminant.
28/// In Phase 3, this may become a compile-time type hash.
29///
30/// ### Reservation Policy (M1020/M1015):
31/// - `0–1023` (except 128): Engine Core (Replicated).
32/// - `1024–2047`: Official Engine Extensions.
33/// - `128`: Explicitly reserved for Input Commands (Transient/Inbound-Only).
34/// - `32768+`: Reserved for Non-Replicated/Inbound variants.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
36pub struct ComponentKind(pub u16);
37
38/// Discriminant for client-to-server input commands.
39/// Tagged as Transient/Inbound-Only.
40pub const INPUT_COMMAND_KIND: ComponentKind = ComponentKind(128);
41
42/// Replicated component for Workspace Definition.
43pub const WORKSPACE_DEFINITION_KIND: ComponentKind = ComponentKind(129);
44
45/// Replicated component for Workspace Bounds.
46pub const WORKSPACE_BOUNDS_KIND: ComponentKind = ComponentKind(130);
47
48/// Replicated component for Workspace Membership.
49pub const WORKSPACE_MEMBERSHIP_KIND: ComponentKind = ComponentKind(131);
50
51/// Replicated component for the extraction beam state.
52pub const EXTRACTION_BEAM_KIND: ComponentKind = ComponentKind(1024);
53
54/// Replicated component for agent data store state (replicated to owner).
55pub const DATA_STORE_KIND: ComponentKind = ComponentKind(1025);
56
57/// Replicated component for resource payload depletion tracking.
58pub const RESOURCE_KIND: ComponentKind = ComponentKind(1026);
59
60/// Replicated component for primary tool state.
61pub const TOOL_KIND: ComponentKind = ComponentKind(1027);
62
63/// Replicated component for priority pool state.
64pub const PRIORITY_POOL_KIND: ComponentKind = ComponentKind(1028);
65
66/// Replicated component for integrity pool state.
67pub const INTEGRITY_POOL_KIND: ComponentKind = ComponentKind(1029);
68
69/// Replicated component for data drop state.
70pub const DATA_DROP_KIND: ComponentKind = ComponentKind(1030);
71
72/// Replicated component for beam marker state.
73///
74/// NOTE: This is intentionally an Engine Core foundational component (Kind < 1024).
75pub const BEAM_MARKER_KIND: ComponentKind = ComponentKind(13);
76
77/// Action bitflag: use primary tool.
78pub const ACTION_USE_TOOL: u32 = 1 << 2;
79
80/// Standard transform component used for replication (`ComponentKind` 1).
81#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
82#[repr(C)]
83pub struct Transform {
84    /// Position X
85    pub x: f32,
86    /// Position Y
87    pub y: f32,
88    /// Position Z
89    pub z: f32,
90    /// Rotation in radians
91    pub rotation: f32,
92    /// The high-level entity type identifier for early client rendering.
93    pub entity_type: u16,
94}
95
96/// Agent classification for rendering and property selection.
97#[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
105/// Constant identifiers for entity types used in replication and rendering.
106pub 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/// Returns the default authoritative vitals (`max_integrity`, `max_priority`) for a given entity type.
116///
117/// These values are the single source of truth for UI and early client-side prediction
118/// before authoritative `AgentProperties` updates arrive.
119#[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/// Unique identifier for a tool type.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
134pub struct ToolId(pub u8);
135
136/// A globally unique zone identifier.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
138pub struct ZoneId(pub u64);
139
140/// Payload types extracted from resources.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[repr(u8)]
143pub enum PayloadType {
144    RawPayload = 0,
145}
146
147/// Beam delivery classification.
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
149#[repr(u8)]
150pub enum InteractionBeamType {
151    PulseBeam = 0,
152    TrackingBeam = 1,
153}
154
155/// NPC Drone behavior state Machine.
156#[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/// Definitive respawn target semantics.
166#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
167pub enum RespawnLocation {
168    /// The server calculates dynamically the Nearest Safe Zone.
169    NearestSafeZone,
170    /// Respawn docked at a specific station entity.
171    Station(u64),
172    /// Respawn at arbitrary x, y coordinates (admin/debug).
173    Coordinate(f32, f32),
174}
175
176/// Individual input actions performed by a player in a single tick.
177#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
178pub enum PlayerInputKind {
179    /// Directional thrust/movement.
180    Move { x: f32, y: f32 },
181    /// Toggle extraction beam on a specific target.
182    ToggleExtraction { target: NetworkId },
183    /// Fire primary tool (for VS-03).
184    FireTool,
185    /// Cursor movement for external compositors or UI integration.
186    CursorMove {
187        /// Normalized X position (0.0 to 1.0)
188        x: f32,
189        /// Normalized Y position (0.0 to 1.0)
190        y: f32,
191    },
192}
193
194/// Maximum allowed actions in a single `InputCommand` to prevent payload `DoS`.
195/// Chosen to stay well within `MAX_SAFE_PAYLOAD_SIZE` (1200 bytes).
196pub const MAX_ACTIONS: usize = 128;
197
198/// Bitmask of all currently supported action flags.
199pub const ALLOWED_ACTIONS_MASK: u32 = ACTION_USE_TOOL;
200
201/// Aggregated user input for a single simulation tick.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct InputCommand {
204    /// The client-side tick this input was generated at.
205    pub tick: u64,
206    /// List of actions performed in this tick.
207    pub actions: Vec<PlayerInputKind>,
208    /// Bitmask of actions for high-frequency binary inputs.
209    #[serde(default)]
210    pub actions_mask: u32,
211    /// The tick of the last server state the client saw before sending this input.
212    pub last_seen_input_tick: Option<u64>,
213}
214
215impl InputCommand {
216    /// Returns a new `InputCommand` with all `Move` inputs clamped to [-1.0, 1.0].
217    #[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    /// Validates the command against protocol constraints.
236    ///
237    /// # Errors
238    /// Returns an error message if the command exceeds `MAX_ACTIONS` or has unknown bits in `actions_mask`.
239    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/// Replicated state for an agent's extraction beam.
251#[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/// Replicated state for an agent's data store.
262#[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/// Replicated state for an agent's primary tool.
275#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
276pub struct Tool {
277    pub cooldown_ticks: u16,
278    pub last_fired_tick: u64,
279}
280
281/// Replicated state for an agent's priority pool.
282#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
283pub struct PriorityPool {
284    pub current: u16,
285    pub max: u16,
286}
287
288/// Replicated state for an agent's integrity pool.
289#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
290pub struct IntegrityPool {
291    pub current: u16,
292    pub max: u16,
293}
294
295/// Replicated state for a data drop entity.
296#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
297pub struct DataDrop {
298    pub amount: u16,
299}
300
301/// Basic properties for any agent entity.
302///
303/// NOTE: Zero values in maxima (`max_integrity`, `max_priority`, `max_energy`) represent an uninitialized
304/// or dead state. Logic that performs divisions or percentage calculations must verify
305/// non-zero maxima.
306#[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    /// Returns a baseline valid state (100 Integrity/Priority/Energy).
320    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
334/// Maximum byte length (UTF-8) for [`WorkspaceName`] and [`PermissionString`].
335///
336/// Chosen well below [`MAX_SAFE_PAYLOAD_SIZE`](crate::MAX_SAFE_PAYLOAD_SIZE)
337/// to leave ample room for the surrounding struct framing in the wire format.
338pub const MAX_WORKSPACE_STRING_BYTES: usize = 64;
339
340/// Error returned when a [`WorkspaceName`] or [`PermissionString`] exceeds the
341/// allowed byte length.
342#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
343#[error("string too long: {len} bytes exceeds the maximum of {max} bytes")]
344pub struct WorkspaceStringError {
345    /// Actual byte length of the rejected string.
346    pub len: usize,
347    /// Maximum allowed byte length ([`MAX_WORKSPACE_STRING_BYTES`]).
348    pub max: usize,
349}
350
351/// A validated workspace name.
352///
353/// Guaranteed not to exceed [`MAX_WORKSPACE_STRING_BYTES`] bytes (UTF-8).
354/// The limit is enforced at construction time via [`WorkspaceName::new`] and at
355/// Serde decode time, so a value held in this type can never produce a payload
356/// that exceeds [`MAX_SAFE_PAYLOAD_SIZE`](crate::MAX_SAFE_PAYLOAD_SIZE).
357#[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    /// Creates a `WorkspaceName`, returning [`WorkspaceStringError`] if `s` exceeds
373    /// [`MAX_WORKSPACE_STRING_BYTES`] bytes.
374    ///
375    /// # Errors
376    ///
377    /// Returns [`WorkspaceStringError`] if the byte length of `s` exceeds
378    /// [`MAX_WORKSPACE_STRING_BYTES`].
379    #[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    /// Returns the name as a string slice.
387    #[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/// A validated access-control permission token.
413///
414/// Guaranteed not to exceed [`MAX_WORKSPACE_STRING_BYTES`] bytes (UTF-8).
415/// Used by [`WorkspaceAccessPolicy::Permission`].
416/// The limit is enforced at construction time via [`PermissionString::new`] and
417/// at Serde decode time.
418#[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    /// Creates a `PermissionString`, returning [`WorkspaceStringError`] if `s`
424    /// exceeds [`MAX_WORKSPACE_STRING_BYTES`] bytes.
425    ///
426    /// # Errors
427    ///
428    /// Returns [`WorkspaceStringError`] if the byte length of `s` exceeds
429    /// [`MAX_WORKSPACE_STRING_BYTES`].
430    #[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    /// Returns the permission token as a string slice.
438    #[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/// Access control policy for the workspace.
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
465pub enum WorkspaceAccessPolicy {
466    /// Anyone can enter.
467    Open,
468    /// Only clients holding the specified [`PermissionString`] token can enter.
469    ///
470    /// The token is replicated verbatim in the wire format and is guaranteed
471    /// not to exceed [`MAX_WORKSPACE_STRING_BYTES`] bytes.
472    Permission(PermissionString),
473    /// Only explicitly invited clients can enter.
474    InviteOnly,
475    /// Locked — no one can enter.
476    Locked,
477}
478
479/// Defines a spatial region as a Workspace.
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct WorkspaceDefinition {
482    /// Human-readable workspace identifier.
483    ///
484    /// Replicated verbatim in the wire format. Guaranteed not to exceed
485    /// [`MAX_WORKSPACE_STRING_BYTES`] bytes (UTF-8) by the [`WorkspaceName`] type.
486    pub name: WorkspaceName,
487    pub capacity: u32,
488    pub access: WorkspaceAccessPolicy,
489    pub is_template: bool,
490}
491
492/// Spatial bounds of the workspace in world coordinates.
493#[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/// Defines which Workspace an entity currently belongs to.
502#[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/// Authoritative allocator for [`NetworkId`]s.
517///
518/// Used by the server to ensure IDs are unique and monotonically increasing.
519/// Thread-safe and lock-free.
520#[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    /// Creates a new allocator starting from a specific ID. 0 is reserved.
534    #[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    /// Allocates a new unique [`NetworkId`].
543    ///
544    /// # Errors
545    /// Returns [`AllocatorError::Overflow`] if the next ID would exceed `u64::MAX`.
546    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    /// Resets the allocator to its initial `start_id`.
566    /// Use only in tests or clear-world scenarios.
567    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)); // Default fallback
650    }
651}