Skip to main content

sectorsync_core/
entity.rs

1//! Entity metadata, authority roles, tags, and dirty state.
2
3use crate::ids::{EntityHandle, EntityId, OwnerEpoch, PolicyId, StationId, Tick};
4use crate::spatial::{Bounds, Position3};
5
6/// Bitset of entity tags. Higher-level code can assign tag meanings.
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub struct EntityTags(u64);
9
10impl EntityTags {
11    /// Empty tag set.
12    pub const EMPTY: Self = Self(0);
13
14    /// Creates tags from raw bits.
15    pub const fn from_bits(bits: u64) -> Self {
16        Self(bits)
17    }
18
19    /// Returns raw tag bits.
20    pub const fn bits(self) -> u64 {
21        self.0
22    }
23
24    /// Returns whether all bits in `mask` are present.
25    pub const fn contains(self, mask: Self) -> bool {
26        (self.0 & mask.0) == mask.0
27    }
28
29    /// Returns whether any bit in `mask` is present.
30    pub const fn intersects(self, mask: Self) -> bool {
31        (self.0 & mask.0) != 0
32    }
33
34    /// Adds all tags in `mask`.
35    pub fn insert(&mut self, mask: Self) {
36        self.0 |= mask.0;
37    }
38
39    /// Removes all tags in `mask`.
40    pub fn remove(&mut self, mask: Self) {
41        self.0 &= !mask.0;
42    }
43}
44
45/// Component-level dirty bitset used by replication planning.
46#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
47pub struct DirtyMask(u64);
48
49impl DirtyMask {
50    /// No dirty components.
51    pub const NONE: Self = Self(0);
52    /// Transform or bounds changed.
53    pub const TRANSFORM: Self = Self(1 << 0);
54    /// Replication policy changed.
55    pub const POLICY: Self = Self(1 << 1);
56    /// Entity tags changed.
57    pub const TAGS: Self = Self(1 << 2);
58    /// Custom component changed.
59    pub const CUSTOM: Self = Self(1 << 63);
60
61    /// Returns raw dirty bits.
62    pub const fn bits(self) -> u64 {
63        self.0
64    }
65
66    /// Returns whether all bits in `mask` are present.
67    pub const fn contains(self, mask: Self) -> bool {
68        (self.0 & mask.0) == mask.0
69    }
70
71    /// Marks components dirty.
72    pub fn insert(&mut self, mask: Self) {
73        self.0 |= mask.0;
74    }
75
76    /// Clears dirty bits present in `mask`.
77    pub fn remove(&mut self, mask: Self) {
78        self.0 &= !mask.0;
79    }
80
81    /// Clears all dirty bits.
82    pub fn clear(&mut self) {
83        self.0 = 0;
84    }
85}
86
87/// Authority role for an entity copy stored in a station.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum EntityRole {
90    /// This station is the authoritative owner.
91    Owned {
92        /// Owner epoch for stale message detection.
93        owner_epoch: OwnerEpoch,
94    },
95    /// This station has a read-only ghost copy from an owner station.
96    Ghost {
97        /// Authoritative owner station.
98        owner_station: StationId,
99        /// Owner epoch for stale message detection.
100        owner_epoch: OwnerEpoch,
101        /// Tick after which this ghost can be discarded.
102        expires_at: Tick,
103    },
104}
105
106impl EntityRole {
107    /// Returns whether this copy is authoritative.
108    pub const fn is_owned(self) -> bool {
109        matches!(self, Self::Owned { .. })
110    }
111
112    /// Returns the owner epoch for ordering handoffs and stale messages.
113    pub const fn owner_epoch(self) -> OwnerEpoch {
114        match self {
115            Self::Owned { owner_epoch }
116            | Self::Ghost {
117                owner_epoch,
118                owner_station: _,
119                expires_at: _,
120            } => owner_epoch,
121        }
122    }
123}
124
125/// Station-local entity record.
126#[derive(Clone, Debug, PartialEq)]
127pub struct EntityRecord {
128    /// Stable entity identifier.
129    pub id: EntityId,
130    /// Station-local dense handle.
131    pub handle: EntityHandle,
132    /// Current position.
133    pub position: Position3,
134    /// Entity bounds.
135    pub bounds: Bounds,
136    /// Compiled sync policy id.
137    pub policy_id: PolicyId,
138    /// User-defined tags.
139    pub tags: EntityTags,
140    /// Owner or ghost role.
141    pub role: EntityRole,
142    /// Dirty component mask.
143    pub dirty: DirtyMask,
144}
145
146impl EntityRecord {
147    /// Creates an authoritative entity record.
148    pub fn owned(
149        id: EntityId,
150        handle: EntityHandle,
151        position: Position3,
152        bounds: Bounds,
153        policy_id: PolicyId,
154        owner_epoch: OwnerEpoch,
155    ) -> Self {
156        Self {
157            id,
158            handle,
159            position,
160            bounds,
161            policy_id,
162            tags: EntityTags::EMPTY,
163            role: EntityRole::Owned { owner_epoch },
164            dirty: DirtyMask::TRANSFORM,
165        }
166    }
167
168    /// Creates a read-only ghost entity record.
169    #[allow(clippy::too_many_arguments)]
170    pub fn ghost(
171        id: EntityId,
172        handle: EntityHandle,
173        position: Position3,
174        bounds: Bounds,
175        policy_id: PolicyId,
176        owner_station: StationId,
177        owner_epoch: OwnerEpoch,
178        expires_at: Tick,
179    ) -> Self {
180        Self {
181            id,
182            handle,
183            position,
184            bounds,
185            policy_id,
186            tags: EntityTags::EMPTY,
187            role: EntityRole::Ghost {
188                owner_station,
189                owner_epoch,
190                expires_at,
191            },
192            dirty: DirtyMask::TRANSFORM,
193        }
194    }
195
196    /// Returns whether this record is authoritative in its station.
197    pub const fn is_owned(&self) -> bool {
198        self.role.is_owned()
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn entity_tags_support_contains_intersects_and_remove() {
208        let mut tags = EntityTags::from_bits(0b1011);
209
210        assert!(tags.contains(EntityTags::from_bits(0b0011)));
211        assert!(tags.intersects(EntityTags::from_bits(0b1000)));
212        assert!(!tags.intersects(EntityTags::from_bits(0b0100_0000)));
213
214        tags.remove(EntityTags::from_bits(0b0010));
215        assert_eq!(tags.bits(), 0b1001);
216    }
217}