Skip to main content

steel_core/player/lifecycle/
mod.rs

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