steel_core/player/lifecycle/
mod.rs1mod 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#[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 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 pub(crate) fn begin_domain_switch(&self, token: PendingWorldChangeToken) -> bool {
295 self.lifecycle.lock().begin_domain_switch(token)
296 }
297
298 pub(crate) fn is_domain_switch_queued(&self, token: PendingWorldChangeToken) -> bool {
300 self.lifecycle.lock().domain_switch_queued(token)
301 }
302
303 pub(crate) fn is_domain_switch_detached(&self, token: PendingWorldChangeToken) -> bool {
305 self.lifecycle.lock().domain_switch_detached(token)
306 }
307
308 pub(crate) fn mark_domain_switch_detached(&self, token: PendingWorldChangeToken) -> bool {
310 self.lifecycle.lock().mark_domain_switch_detached(token)
311 }
312
313 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 pub(crate) fn mark_domain_switch_live(&self, token: PendingWorldChangeToken) -> bool {
325 self.lifecycle.lock().mark_domain_switch_live(token)
326 }
327
328 pub(crate) fn finish_domain_switch(&self, token: PendingWorldChangeToken) -> bool {
330 self.lifecycle.lock().finish_domain_switch(token)
331 }
332
333 pub(crate) fn begin_respawn_transition(&self, token: PendingWorldChangeToken) -> bool {
335 self.lifecycle.lock().begin_respawn(token)
336 }
337
338 pub(crate) fn is_respawn_transition_pending(&self, token: PendingWorldChangeToken) -> bool {
340 self.lifecycle.lock().respawn_pending(token)
341 }
342
343 pub(crate) fn finish_respawn_transition(&self, token: PendingWorldChangeToken) -> bool {
345 self.lifecycle.lock().finish_respawn(token)
346 }
347
348 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 pub fn is_domain_switching(&self) -> bool {
369 self.lifecycle.lock().domain_switching()
370 }
371
372 pub(crate) fn domain_switch_blocks_gameplay(&self) -> bool {
374 self.lifecycle.lock().domain_switch_blocks_gameplay()
375 }
376
377 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 #[must_use]
399 pub fn has_joined_world(&self) -> bool {
400 self.lifecycle.lock().joined_world()
401 }
402
403 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 #[must_use]
415 pub fn has_client_loaded(&self) -> bool {
416 self.lifecycle.lock().client_loaded()
417 }
418
419 pub fn set_client_loaded(&self, client_loaded: bool) {
421 self.lifecycle.lock().set_client_loaded(client_loaded);
422 }
423
424 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
439pub(crate) enum ResetReason {
440 InitialJoin,
442 Respawn,
444 EndCredits,
446 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}