Skip to main content

steel_core/player/lifecycle/
mod.rs

1mod respawn;
2mod spawn_sync;
3mod world_transition;
4
5#[cfg(test)]
6pub(super) use spawn_sync::nullable_game_mode_id;
7
8use super::*;
9use crate::entity::PendingWorldChangeToken;
10
11/// Client lifecycle flags that gate gameplay packet handling.
12#[derive(Debug, Clone, Copy)]
13pub(super) struct PlayerLifecycleState {
14    joined_world: bool,
15    pending_client_loaded: bool,
16    client_loaded_timeout: i32,
17    domain_switch: Option<DomainSwitchState>,
18    respawn: Option<PendingWorldChangeToken>,
19    deferred_death_respawn: bool,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23struct DomainSwitchState {
24    token: PendingWorldChangeToken,
25    phase: DomainSwitchPhase,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29enum DomainSwitchPhase {
30    Queued,
31    Detached,
32    TargetHandshake,
33    Finalizing,
34}
35
36const CLIENT_LOADED_TIMEOUT_TICKS: i32 = 60;
37
38impl Default for PlayerLifecycleState {
39    fn default() -> Self {
40        Self {
41            joined_world: false,
42            pending_client_loaded: false,
43            client_loaded_timeout: CLIENT_LOADED_TIMEOUT_TICKS,
44            domain_switch: None,
45            respawn: None,
46            deferred_death_respawn: false,
47        }
48    }
49}
50
51impl PlayerLifecycleState {
52    #[must_use]
53    pub(super) const fn client_loaded(self) -> bool {
54        self.client_loaded_timeout <= 0
55    }
56
57    #[must_use]
58    pub(super) const fn joined_world(self) -> bool {
59        self.joined_world
60    }
61
62    pub(super) const fn set_joined_world(&mut self, joined_world: bool) {
63        self.joined_world = joined_world;
64    }
65
66    pub(super) const fn set_client_loaded(&mut self, client_loaded: bool) {
67        if !client_loaded {
68            self.pending_client_loaded = false;
69        }
70        self.client_loaded_timeout = if client_loaded {
71            0
72        } else {
73            CLIENT_LOADED_TIMEOUT_TICKS
74        };
75    }
76
77    pub(super) const fn mark_client_loaded_from_network(&mut self) -> bool {
78        if self.joined_world {
79            self.set_client_loaded(true);
80            return true;
81        }
82
83        self.pending_client_loaded = true;
84        false
85    }
86
87    pub(super) const fn apply_pending_client_loaded(&mut self) -> bool {
88        if !self.pending_client_loaded {
89            return false;
90        }
91
92        self.pending_client_loaded = false;
93        self.set_client_loaded(true);
94        true
95    }
96
97    pub(super) const fn tick_client_load_timeout(&mut self) {
98        if self.client_loaded_timeout > 0 {
99            self.client_loaded_timeout -= 1;
100        }
101    }
102
103    #[must_use]
104    pub(super) const fn domain_switching(self) -> bool {
105        self.domain_switch.is_some()
106    }
107
108    #[must_use]
109    pub(super) const fn domain_switch_blocks_gameplay(self) -> bool {
110        matches!(
111            self.domain_switch,
112            Some(DomainSwitchState {
113                phase: DomainSwitchPhase::Queued
114                    | DomainSwitchPhase::Detached
115                    | DomainSwitchPhase::TargetHandshake,
116                ..
117            })
118        )
119    }
120
121    #[must_use]
122    pub(super) const fn gate_domain_switch_packet(
123        &mut self,
124        handshake_packet: bool,
125        defer_death_respawn: bool,
126    ) -> bool {
127        match self.domain_switch {
128            None
129            | Some(DomainSwitchState {
130                phase: DomainSwitchPhase::Finalizing,
131                ..
132            }) => true,
133            Some(DomainSwitchState {
134                phase: DomainSwitchPhase::TargetHandshake,
135                ..
136            }) => handshake_packet,
137            Some(DomainSwitchState {
138                phase: DomainSwitchPhase::Queued | DomainSwitchPhase::Detached,
139                ..
140            }) => {
141                if defer_death_respawn && self.respawn.is_none() {
142                    self.deferred_death_respawn = true;
143                }
144                false
145            }
146        }
147    }
148
149    pub(super) const fn begin_domain_switch(&mut self, token: PendingWorldChangeToken) -> bool {
150        if self.domain_switch.is_some() || self.respawn.is_some() {
151            return false;
152        }
153
154        self.domain_switch = Some(DomainSwitchState {
155            token,
156            phase: DomainSwitchPhase::Queued,
157        });
158        true
159    }
160
161    #[must_use]
162    pub(super) fn domain_switch_queued(self, token: PendingWorldChangeToken) -> bool {
163        matches!(
164            self.domain_switch,
165            Some(DomainSwitchState {
166                token: active,
167                phase: DomainSwitchPhase::Queued,
168            }) if active == token
169        )
170    }
171
172    #[must_use]
173    pub(super) fn domain_switch_detached(self, token: PendingWorldChangeToken) -> bool {
174        matches!(
175            self.domain_switch,
176            Some(DomainSwitchState {
177                token: active,
178                phase: DomainSwitchPhase::Detached,
179            }) if active == token
180        )
181    }
182
183    pub(super) fn mark_domain_switch_detached(&mut self, token: PendingWorldChangeToken) -> bool {
184        let Some(state) = self.domain_switch.as_mut() else {
185            return false;
186        };
187        if state.token != token || state.phase != DomainSwitchPhase::Queued {
188            return false;
189        }
190
191        state.phase = DomainSwitchPhase::Detached;
192        true
193    }
194
195    pub(super) fn mark_domain_switch_target_handshake(
196        &mut self,
197        token: PendingWorldChangeToken,
198    ) -> bool {
199        let Some(state) = self.domain_switch.as_mut() else {
200            return false;
201        };
202        if state.token != token || state.phase != DomainSwitchPhase::Detached {
203            return false;
204        }
205
206        state.phase = DomainSwitchPhase::TargetHandshake;
207        true
208    }
209
210    pub(super) fn mark_domain_switch_live(&mut self, token: PendingWorldChangeToken) -> bool {
211        let Some(state) = self.domain_switch.as_mut() else {
212            return false;
213        };
214        if state.token != token || state.phase != DomainSwitchPhase::TargetHandshake {
215            return false;
216        }
217
218        state.phase = DomainSwitchPhase::Finalizing;
219        true
220    }
221
222    pub(super) fn finish_domain_switch(&mut self, token: PendingWorldChangeToken) -> bool {
223        if !matches!(
224            self.domain_switch,
225            Some(DomainSwitchState { token: active, .. }) if active == token
226        ) {
227            return false;
228        }
229
230        self.domain_switch = None;
231        true
232    }
233
234    pub(super) const fn begin_respawn(&mut self, token: PendingWorldChangeToken) -> bool {
235        if self.respawn.is_some() || self.domain_switch_blocks_gameplay() {
236            return false;
237        }
238
239        self.respawn = Some(token);
240        self.deferred_death_respawn = false;
241        true
242    }
243
244    pub(super) const fn defer_death_respawn(&mut self) {
245        if self.respawn.is_none() {
246            self.deferred_death_respawn = true;
247        }
248    }
249
250    pub(super) const fn take_deferred_death_respawn(&mut self) -> bool {
251        let deferred = self.deferred_death_respawn;
252        self.deferred_death_respawn = false;
253        deferred
254    }
255
256    #[must_use]
257    pub(super) fn respawn_pending(self, token: PendingWorldChangeToken) -> bool {
258        self.respawn == Some(token)
259    }
260
261    pub(super) fn finish_respawn(&mut self, token: PendingWorldChangeToken) -> bool {
262        if !self.respawn_pending(token) {
263            return false;
264        }
265
266        self.respawn = None;
267        true
268    }
269
270    pub(super) fn finish_transition(&mut self, token: PendingWorldChangeToken) -> bool {
271        if matches!(
272            self.domain_switch,
273            Some(DomainSwitchState { token: active, .. }) if active == token
274        ) {
275            self.domain_switch = None;
276            return true;
277        }
278
279        self.finish_respawn(token)
280    }
281}
282
283impl Player {
284    /// Sets the world the player is in.
285    ///
286    /// This is used when the correct world isn't known at construction time
287    /// (e.g., when loading saved player data determines the actual world).
288    pub(crate) fn set_world(&self, world: Arc<World>) {
289        self.base.set_world(Arc::downgrade(&world));
290        self.world.store(world);
291    }
292
293    /// Marks the player as switching domains if they are not already in a transition.
294    pub(crate) fn begin_domain_switch(&self, token: PendingWorldChangeToken) -> bool {
295        self.lifecycle.lock().begin_domain_switch(token)
296    }
297
298    /// Returns whether the queued domain switch still owns this token.
299    pub(crate) fn is_domain_switch_queued(&self, token: PendingWorldChangeToken) -> bool {
300        self.lifecycle.lock().domain_switch_queued(token)
301    }
302
303    /// Returns whether the detached domain switch still owns this token.
304    pub(crate) fn is_domain_switch_detached(&self, token: PendingWorldChangeToken) -> bool {
305        self.lifecycle.lock().domain_switch_detached(token)
306    }
307
308    /// Marks the token-owned domain switch as detached from its source world.
309    pub(crate) fn mark_domain_switch_detached(&self, token: PendingWorldChangeToken) -> bool {
310        self.lifecycle.lock().mark_domain_switch_detached(token)
311    }
312
313    /// Opens only target-world acknowledgement packets before target insertion completes.
314    pub(crate) fn mark_domain_switch_target_handshake(
315        &self,
316        token: PendingWorldChangeToken,
317    ) -> bool {
318        self.lifecycle
319            .lock()
320            .mark_domain_switch_target_handshake(token)
321    }
322
323    /// Marks the token-owned domain switch as live in its target world.
324    pub(crate) fn mark_domain_switch_live(&self, token: PendingWorldChangeToken) -> bool {
325        self.lifecycle.lock().mark_domain_switch_live(token)
326    }
327
328    /// Clears a domain switch only if the caller still owns it.
329    pub(crate) fn finish_domain_switch(&self, token: PendingWorldChangeToken) -> bool {
330        self.lifecycle.lock().finish_domain_switch(token)
331    }
332
333    /// Marks a token as owning respawn preparation if no player transition is active.
334    pub(crate) fn begin_respawn_transition(&self, token: PendingWorldChangeToken) -> bool {
335        self.lifecycle.lock().begin_respawn(token)
336    }
337
338    /// Returns whether respawn preparation still owns this token.
339    pub(crate) fn is_respawn_transition_pending(&self, token: PendingWorldChangeToken) -> bool {
340        self.lifecycle.lock().respawn_pending(token)
341    }
342
343    /// Clears respawn preparation only if the caller still owns it.
344    pub(crate) fn finish_respawn_transition(&self, token: PendingWorldChangeToken) -> bool {
345        self.lifecycle.lock().finish_respawn(token)
346    }
347
348    /// Clears either player transition kind only if the caller owns its token.
349    pub(crate) fn finish_player_transition(&self, token: PendingWorldChangeToken) -> bool {
350        self.lifecycle.lock().finish_transition(token)
351    }
352
353    pub(crate) fn defer_death_respawn(&self) {
354        self.lifecycle.lock().defer_death_respawn();
355    }
356
357    pub(crate) fn retry_deferred_death_respawn(&self) {
358        if !self.lifecycle.lock().take_deferred_death_respawn() {
359            return;
360        }
361        if self.connection.closed() || self.get_health() > 0.0 {
362            return;
363        }
364        self.respawn();
365    }
366
367    /// Returns whether this player is currently switching domains.
368    pub fn is_domain_switching(&self) -> bool {
369        self.lifecycle.lock().domain_switching()
370    }
371
372    /// Returns whether the current domain-switch phase blocks gameplay work.
373    pub(crate) fn domain_switch_blocks_gameplay(&self) -> bool {
374        self.lifecycle.lock().domain_switch_blocks_gameplay()
375    }
376
377    /// Gates a packet against the current domain phase.
378    ///
379    /// A dead player's one-shot respawn request is retained while a queued or
380    /// detached switch blocks normal gameplay packets.
381    pub(crate) fn gate_domain_switch_packet(
382        &self,
383        handshake_packet: bool,
384        perform_respawn: bool,
385    ) -> bool {
386        let defer_death_respawn = perform_respawn && self.get_health() <= 0.0;
387        self.lifecycle
388            .lock()
389            .gate_domain_switch_packet(handshake_packet, defer_death_respawn)
390    }
391
392    #[cfg(test)]
393    pub(crate) fn has_deferred_death_respawn_for_test(&self) -> bool {
394        self.lifecycle.lock().deferred_death_respawn
395    }
396
397    /// Returns whether the server has inserted this player into a world.
398    #[must_use]
399    pub fn has_joined_world(&self) -> bool {
400        self.lifecycle.lock().joined_world()
401    }
402
403    /// Marks this player as inserted into a world.
404    ///
405    /// Returns `true` when a client-loaded acknowledgement arrived before world
406    /// admission and was applied by this call.
407    pub(crate) fn mark_joined_world(&self) -> bool {
408        let mut lifecycle = self.lifecycle.lock();
409        lifecycle.set_joined_world(true);
410        lifecycle.apply_pending_client_loaded()
411    }
412
413    /// Returns whether the client has sent its play-loaded signal.
414    #[must_use]
415    pub fn has_client_loaded(&self) -> bool {
416        self.lifecycle.lock().client_loaded()
417    }
418
419    /// Marks whether the client has loaded into play.
420    pub fn set_client_loaded(&self, client_loaded: bool) {
421        self.lifecycle.lock().set_client_loaded(client_loaded);
422    }
423
424    /// Applies or buffers the client's play-loaded acknowledgement.
425    ///
426    /// Returns `true` when the acknowledgement can run gameplay side effects now.
427    pub fn mark_client_loaded_from_network(&self) -> bool {
428        self.lifecycle.lock().mark_client_loaded_from_network()
429    }
430
431    pub(super) fn tick_client_load_timeout(&self) {
432        self.lifecycle.lock().tick_client_load_timeout();
433    }
434}
435/// Why the player is being reset and spawned into a world.
436///
437/// Controls which packets are sent and how world add/remove is handled.
438#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub(crate) enum ResetReason {
440    /// First time joining the server. `CLogin` was already sent, so `CRespawn` is skipped.
441    InitialJoin,
442    /// Respawning after death in the same world.
443    Respawn,
444    /// Respawning after the End credits screen with vanilla packet flags.
445    EndCredits,
446    /// Teleporting to a different loaded world.
447    WorldChange,
448}
449
450impl ResetReason {
451    pub(super) const fn respawn_data_kept(self) -> i8 {
452        match self {
453            Self::InitialJoin | Self::Respawn => 0x00,
454            Self::EndCredits => 0x01,
455            Self::WorldChange => 0x03,
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::{CLIENT_LOADED_TIMEOUT_TICKS, PlayerLifecycleState};
463    use crate::entity::PendingWorldChangeToken;
464
465    #[test]
466    fn domain_switch_requires_matching_phase_and_token() {
467        let mut state = PlayerLifecycleState::default();
468        let first = PendingWorldChangeToken::for_test(1);
469        let second = PendingWorldChangeToken::for_test(2);
470
471        assert!(state.begin_domain_switch(first));
472        assert!(!state.begin_domain_switch(second));
473        assert!(state.domain_switch_queued(first));
474        assert!(!state.domain_switch_queued(second));
475        assert!(state.domain_switch_blocks_gameplay());
476        assert!(!state.gate_domain_switch_packet(false, false));
477        assert!(!state.gate_domain_switch_packet(true, false));
478
479        assert!(!state.mark_domain_switch_detached(second));
480        assert!(state.mark_domain_switch_detached(first));
481        assert!(!state.mark_domain_switch_detached(first));
482        assert!(state.domain_switch_detached(first));
483        assert!(!state.domain_switch_detached(second));
484        assert!(state.domain_switch_blocks_gameplay());
485        assert!(!state.gate_domain_switch_packet(false, true));
486        assert!(state.deferred_death_respawn);
487
488        assert!(!state.mark_domain_switch_live(second));
489        assert!(!state.mark_domain_switch_live(first));
490        assert!(!state.mark_domain_switch_target_handshake(second));
491        assert!(state.mark_domain_switch_target_handshake(first));
492        assert!(state.domain_switch_blocks_gameplay());
493        assert!(!state.gate_domain_switch_packet(false, false));
494        assert!(state.gate_domain_switch_packet(true, false));
495        assert!(state.mark_domain_switch_live(first));
496        assert!(!state.domain_switch_blocks_gameplay());
497        assert!(state.gate_domain_switch_packet(false, false));
498        assert!(!state.finish_domain_switch(second));
499        assert!(state.domain_switching());
500        assert!(state.begin_respawn(second));
501        assert!(state.respawn_pending(second));
502        assert!(!state.begin_domain_switch(second));
503        assert!(state.finish_domain_switch(first));
504        assert!(!state.domain_switching());
505        assert!(state.respawn_pending(second));
506        assert!(!state.begin_domain_switch(first));
507        assert!(!state.finish_respawn(first));
508        assert!(state.finish_respawn(second));
509        assert!(state.begin_domain_switch(first));
510    }
511
512    #[test]
513    fn client_loaded_flag_is_explicit() {
514        let mut state = PlayerLifecycleState::default();
515
516        assert!(!state.client_loaded());
517        assert!(!state.mark_client_loaded_from_network());
518        assert!(!state.client_loaded());
519
520        state.set_joined_world(true);
521        assert!(state.apply_pending_client_loaded());
522        assert!(state.client_loaded());
523
524        state.set_client_loaded(true);
525        assert!(state.client_loaded());
526        state.set_client_loaded(false);
527        assert!(!state.client_loaded());
528        assert!(!state.apply_pending_client_loaded());
529    }
530
531    #[test]
532    fn client_load_timeout_eventually_marks_loaded() {
533        let mut state = PlayerLifecycleState::default();
534
535        for _ in 0..CLIENT_LOADED_TIMEOUT_TICKS {
536            assert!(!state.client_loaded());
537            state.tick_client_load_timeout();
538        }
539
540        assert!(state.client_loaded());
541    }
542
543    #[test]
544    fn joined_world_flag_is_explicit() {
545        let mut state = PlayerLifecycleState::default();
546
547        assert!(!state.joined_world());
548        state.set_joined_world(true);
549        assert!(state.joined_world());
550        state.set_joined_world(false);
551        assert!(!state.joined_world());
552    }
553}