steel_core/player/lifecycle/
mod.rs1mod 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#[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 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 pub(crate) fn begin_domain_switch(&self, token: PendingWorldChangeToken) -> bool {
298 self.lifecycle.lock().begin_domain_switch(token)
299 }
300
301 pub(crate) fn is_domain_switch_queued(&self, token: PendingWorldChangeToken) -> bool {
303 self.lifecycle.lock().domain_switch_queued(token)
304 }
305
306 pub(crate) fn is_domain_switch_detached(&self, token: PendingWorldChangeToken) -> bool {
308 self.lifecycle.lock().domain_switch_detached(token)
309 }
310
311 pub(crate) fn mark_domain_switch_detached(&self, token: PendingWorldChangeToken) -> bool {
313 self.lifecycle.lock().mark_domain_switch_detached(token)
314 }
315
316 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 pub(crate) fn mark_domain_switch_live(&self, token: PendingWorldChangeToken) -> bool {
328 self.lifecycle.lock().mark_domain_switch_live(token)
329 }
330
331 pub(crate) fn finish_domain_switch(&self, token: PendingWorldChangeToken) -> bool {
333 self.lifecycle.lock().finish_domain_switch(token)
334 }
335
336 pub(crate) fn begin_respawn_transition(&self, token: PendingWorldChangeToken) -> bool {
338 self.lifecycle.lock().begin_respawn(token)
339 }
340
341 pub(crate) fn is_respawn_transition_pending(&self, token: PendingWorldChangeToken) -> bool {
343 self.lifecycle.lock().respawn_pending(token)
344 }
345
346 pub(crate) fn finish_respawn_transition(&self, token: PendingWorldChangeToken) -> bool {
348 self.lifecycle.lock().finish_respawn(token)
349 }
350
351 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 pub fn is_domain_switching(&self) -> bool {
372 self.lifecycle.lock().domain_switching()
373 }
374
375 pub(crate) fn domain_switch_blocks_gameplay(&self) -> bool {
377 self.lifecycle.lock().domain_switch_blocks_gameplay()
378 }
379
380 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 #[must_use]
402 pub fn has_joined_world(&self) -> bool {
403 self.lifecycle.lock().joined_world()
404 }
405
406 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 #[must_use]
418 pub fn has_client_loaded(&self) -> bool {
419 self.lifecycle.lock().client_loaded()
420 }
421
422 pub fn set_client_loaded(&self, client_loaded: bool) {
424 self.lifecycle.lock().set_client_loaded(client_loaded);
425 }
426
427 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
442pub(crate) enum ResetReason {
443 InitialJoin,
445 Respawn,
447 EndCredits,
449 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}