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