Skip to main content

naia_shared/world/delegation/
entity_auth_status.rs

1use naia_serde::SerdeInternal;
2
3use crate::HostType;
4
5/// Authority lifecycle state for a delegated entity as observed by one endpoint.
6#[derive(SerdeInternal, Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub enum EntityAuthStatus {
8    /// No authority over this entity has been granted to any host.
9    Available,
10    /// This host has requested authority but the grant has not arrived yet.
11    Requested,
12    /// This host has been granted authority over the entity.
13    Granted,
14    /// This host is releasing authority; the release is in flight.
15    Releasing,
16    /// This host was denied authority because another host claimed it first.
17    Denied,
18}
19
20impl EntityAuthStatus {
21    /// Returns `true` if no host currently holds authority over this entity.
22    pub fn is_available(&self) -> bool {
23        matches!(self, EntityAuthStatus::Available)
24    }
25
26    /// Returns `true` if this host has requested but not yet been granted authority.
27    pub fn is_requested(&self) -> bool {
28        matches!(self, EntityAuthStatus::Requested)
29    }
30
31    /// Returns `true` if this host currently holds authority over the entity.
32    pub fn is_granted(&self) -> bool {
33        matches!(self, EntityAuthStatus::Granted)
34    }
35
36    /// Returns `true` if this host's authority request was denied.
37    pub fn is_denied(&self) -> bool {
38        matches!(self, EntityAuthStatus::Denied)
39    }
40
41    /// Returns `true` if this host is in the process of releasing authority.
42    pub fn is_releasing(&self) -> bool {
43        matches!(self, EntityAuthStatus::Releasing)
44    }
45}
46
47/// Combined view of an entity's authority status from a specific endpoint's perspective.
48#[derive(Debug)]
49pub struct HostEntityAuthStatus {
50    host_type: HostType,
51    auth_status: EntityAuthStatus,
52}
53
54impl HostEntityAuthStatus {
55    /// Creates a `HostEntityAuthStatus` for `host_type` at the given `auth_status`.
56    pub fn new(host_type: HostType, auth_status: EntityAuthStatus) -> Self {
57        Self {
58            host_type,
59            auth_status,
60        }
61    }
62
63    /// Can this host transition into `Requested`?
64    ///
65    /// Authority delegation is a client-initiated flow: the server owns
66    /// every entity by default and grants/denies client requests. The
67    /// server itself never *requests* authority — it already has it —
68    /// so this method only has meaningful semantics for `HostType::Client`.
69    ///
70    /// Server-side `HostEntityAuthStatus` instances exist (the server
71    /// tracks per-entity auth state in `server_auth_handler.rs`), but
72    /// the only callers of `can_request` are in
73    /// `client/src/world/global_world_manager.rs::entity_request_authority`.
74    /// Reaching a `(HostType::Server, *)` arm here means a server-side
75    /// caller mistakenly drove the client request flow against a server
76    /// auth status — a contract violation worth surfacing loudly.
77    pub fn can_request(&self) -> bool {
78        match (self.host_type, self.auth_status) {
79            (HostType::Client, EntityAuthStatus::Available) => true,
80            (HostType::Client, EntityAuthStatus::Requested) => false,
81            (HostType::Client, EntityAuthStatus::Granted) => false,
82            (HostType::Client, EntityAuthStatus::Releasing) => false,
83            (HostType::Client, EntityAuthStatus::Denied) => false,
84            (HostType::Server, status) => unreachable!(
85                "can_request() is a client-side authority-flow predicate; \
86                 reached with HostType::Server (status={status:?}). The server \
87                 owns entities by default and does not 'request' authority — \
88                 only the client request path in \
89                 client/src/world/global_world_manager.rs::entity_request_authority \
90                 should call this."
91            ),
92        }
93    }
94
95    /// Can this host transition into `Releasing`?
96    ///
97    /// Symmetric to `can_request`: only the client releases authority
98    /// (the server grants/revokes). Server-side instances should never
99    /// reach this method. The only caller is
100    /// `client/src/world/global_world_manager.rs::entity_release_authority`.
101    pub fn can_release(&self) -> bool {
102        match (self.host_type, self.auth_status) {
103            (HostType::Client, EntityAuthStatus::Available) => false,
104            (HostType::Client, EntityAuthStatus::Requested) => true,
105            (HostType::Client, EntityAuthStatus::Granted) => true,
106            (HostType::Client, EntityAuthStatus::Releasing) => false,
107            (HostType::Client, EntityAuthStatus::Denied) => false,
108            (HostType::Server, status) => unreachable!(
109                "can_release() is a client-side authority-flow predicate; \
110                 reached with HostType::Server (status={status:?}). Only the \
111                 client release path in \
112                 client/src/world/global_world_manager.rs::entity_release_authority \
113                 should call this."
114            ),
115        }
116    }
117
118    /// Returns `true` if this host may mutate component properties on the entity.
119    pub fn can_mutate(&self) -> bool {
120        match (self.host_type, self.auth_status) {
121            (HostType::Client, EntityAuthStatus::Available) => false,
122            (HostType::Client, EntityAuthStatus::Requested) => true,
123            (HostType::Client, EntityAuthStatus::Granted) => true,
124            (HostType::Client, EntityAuthStatus::Releasing) => false,
125            (HostType::Client, EntityAuthStatus::Denied) => false,
126            (HostType::Server, EntityAuthStatus::Available) => true,
127            (HostType::Server, EntityAuthStatus::Requested) => true,
128            (HostType::Server, EntityAuthStatus::Granted) => true,
129            (HostType::Server, EntityAuthStatus::Releasing) => true,
130            (HostType::Server, EntityAuthStatus::Denied) => true,
131        }
132    }
133
134    /// Returns `true` if this host may read component values from the entity's delegated properties.
135    pub fn can_read(&self) -> bool {
136        match (self.host_type, self.auth_status) {
137            (HostType::Client, EntityAuthStatus::Available) => true,
138            (HostType::Client, EntityAuthStatus::Requested) => false,
139            (HostType::Client, EntityAuthStatus::Granted) => false,
140            (HostType::Client, EntityAuthStatus::Releasing) => true,
141            (HostType::Client, EntityAuthStatus::Denied) => true,
142            (HostType::Server, EntityAuthStatus::Available) => true,
143            (HostType::Server, EntityAuthStatus::Requested) => true,
144            (HostType::Server, EntityAuthStatus::Granted) => true,
145            (HostType::Server, EntityAuthStatus::Releasing) => true,
146            (HostType::Server, EntityAuthStatus::Denied) => true,
147        }
148    }
149
150    /// Returns `true` if this host may write (serialize) delegated entity properties for the wire.
151    pub fn can_write(&self) -> bool {
152        match (self.host_type, self.auth_status) {
153            (HostType::Client, EntityAuthStatus::Available) => false,
154            (HostType::Client, EntityAuthStatus::Requested) => false,
155            (HostType::Client, EntityAuthStatus::Granted) => true,
156            (HostType::Client, EntityAuthStatus::Releasing) => true,
157            (HostType::Client, EntityAuthStatus::Denied) => false,
158            (HostType::Server, EntityAuthStatus::Available) => true,
159            (HostType::Server, EntityAuthStatus::Requested) => true,
160            (HostType::Server, EntityAuthStatus::Granted) => true,
161            (HostType::Server, EntityAuthStatus::Releasing) => true,
162            (HostType::Server, EntityAuthStatus::Denied) => true,
163        }
164    }
165
166    /// Returns the underlying `EntityAuthStatus` value.
167    pub fn status(&self) -> EntityAuthStatus {
168        self.auth_status
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    //! T0.1 — pin the post-fix invariants: client-side `can_request`/
175    //! `can_release` return their documented values, and server-side
176    //! callers panic with a contract-violation message rather than the
177    //! prior `todo!()` (which masked a hidden invariant as a stub).
178    use super::*;
179
180    #[test]
181    fn client_can_request_only_when_available() {
182        let s = HostEntityAuthStatus::new(HostType::Client, EntityAuthStatus::Available);
183        assert!(s.can_request());
184        for status in [
185            EntityAuthStatus::Requested,
186            EntityAuthStatus::Granted,
187            EntityAuthStatus::Releasing,
188            EntityAuthStatus::Denied,
189        ] {
190            let s = HostEntityAuthStatus::new(HostType::Client, status);
191            assert!(!s.can_request(), "client must not re-request from {status:?}");
192        }
193    }
194
195    #[test]
196    fn client_can_release_only_when_holding_or_requesting() {
197        for status in [EntityAuthStatus::Requested, EntityAuthStatus::Granted] {
198            let s = HostEntityAuthStatus::new(HostType::Client, status);
199            assert!(s.can_release(), "client must release from {status:?}");
200        }
201        for status in [
202            EntityAuthStatus::Available,
203            EntityAuthStatus::Releasing,
204            EntityAuthStatus::Denied,
205        ] {
206            let s = HostEntityAuthStatus::new(HostType::Client, status);
207            assert!(!s.can_release(), "client must not release from {status:?}");
208        }
209    }
210
211    #[test]
212    #[should_panic(expected = "client-side authority-flow predicate")]
213    fn server_can_request_panics_with_contract_message() {
214        let s = HostEntityAuthStatus::new(HostType::Server, EntityAuthStatus::Available);
215        let _ = s.can_request();
216    }
217
218    #[test]
219    #[should_panic(expected = "client-side authority-flow predicate")]
220    fn server_can_release_panics_with_contract_message() {
221        let s = HostEntityAuthStatus::new(HostType::Server, EntityAuthStatus::Granted);
222        let _ = s.can_release();
223    }
224}