Skip to main content

steel_core/player/lifecycle/
respawn_restore.rs

1use std::sync::{Arc, Weak};
2
3use crate::{entity::LivingEntity as _, world::World};
4
5use super::super::{Abilities, Player, experience::Experience};
6
7impl Player {
8    /// Constructs the detached player entity used by vanilla respawn replacement.
9    ///
10    /// The caller remains responsible for replacing the session binding and world
11    /// membership. Chat continuity belongs to [`super::super::PlayerSession`] and
12    /// is therefore shared by both incarnations. For a death replacement,
13    /// `transfer_inventory` must be `keepInventory || old spectator`.
14    pub(crate) fn new_respawn_replacement(
15        self: &Arc<Self>,
16        target_world: Arc<World>,
17        restore_all: bool,
18        transfer_inventory: bool,
19        spawn_block_valid: bool,
20    ) -> Arc<Self> {
21        let mut replacement = Self::new(
22            self.gameprofile.clone(),
23            Arc::clone(&self.connection),
24            Arc::clone(&self.session),
25            target_world,
26            Weak::clone(&self.server),
27            Arc::clone(&self.config),
28            self.base.id(),
29            self.client_information(),
30        );
31        // Vanilla ServerPlayer.restoreFrom retains the same ender chest container.
32        replacement.ender_chest_inventory = Arc::clone(&self.ender_chest_inventory);
33        let replacement = Arc::new(replacement);
34
35        replacement.restore_respawn_state_from(
36            self,
37            restore_all,
38            transfer_inventory,
39            spawn_block_valid,
40        );
41        replacement.copy_respawn_scoreboard_tags_from(self);
42        replacement
43    }
44
45    fn restore_respawn_state_from(
46        &self,
47        old_player: &Self,
48        restore_all: bool,
49        transfer_inventory: bool,
50        spawn_block_valid: bool,
51    ) {
52        let game_mode = old_player.game_mode();
53        self.restore_game_modes(game_mode, old_player.previous_game_mode());
54        {
55            let mut abilities = self.abilities.lock();
56            *abilities = Abilities::default();
57            abilities.update_for_game_mode(game_mode);
58        }
59
60        {
61            let old_attributes = old_player.living_base.attributes().lock();
62            let mut attributes = self.living_base.attributes().lock();
63            attributes.assign_base_values(&old_attributes);
64            if restore_all {
65                attributes.assign_permanent_modifiers(&old_attributes);
66            }
67        }
68
69        let permission_state = old_player.permissions.lock().clone();
70        *self.permissions.lock() = permission_state;
71        {
72            let old_stats = old_player.stats.lock();
73            self.stats.lock().stats.clone_from(&old_stats.stats);
74        }
75        *self.advancements.lock() = old_player.advancements.lock().clone();
76        *self.seen_credits.lock() = *old_player.seen_credits.lock();
77        let residence = old_player.residence.lock().clone();
78        *self.residence.lock() = residence;
79
80        // Vanilla keeps the packet listener across respawn, so its teleport ID
81        // remains monotonic even though the pending position belongs to the new player.
82        self.teleport_state.lock().teleport_id = old_player.teleport_state.lock().teleport_id;
83
84        if spawn_block_valid {
85            let old_respawn_config = old_player.respawn_config.lock();
86            self.respawn_config.lock().clone_from(&old_respawn_config);
87        }
88
89        if restore_all {
90            self.restore_all_respawn_state_from(old_player);
91        } else {
92            self.set_health(self.get_max_health());
93            if transfer_inventory {
94                self.restore_inventory_experience_and_score_from(old_player);
95            }
96        }
97
98        let enchantment_seed = old_player.experience.lock().enchantment_seed();
99        let mut experience = self.experience.lock();
100        *experience = experience.with_enchantment_seed(enchantment_seed);
101    }
102
103    fn restore_all_respawn_state_from(&self, old_player: &Self) {
104        self.set_health(old_player.get_health());
105        *self.food_data.lock() = old_player.food_data.lock().clone();
106
107        for effect in old_player.living_base.active_mob_effects() {
108            self.add_mob_effect(effect);
109        }
110
111        self.restore_inventory_experience_and_score_from(old_player);
112        self.base
113            .set_portal_process(old_player.base.portal_process());
114    }
115
116    fn restore_inventory_experience_and_score_from(&self, old_player: &Self) {
117        let inventory = old_player.inventory.lock().replacement_copy();
118        *self.inventory.lock() = inventory;
119        let experience = old_player.experience.lock();
120        *self.experience.lock() = Experience::from_parts(
121            experience.level(),
122            experience.progress(),
123            experience.total_points(),
124        );
125        self.set_score(old_player.score());
126    }
127
128    fn copy_respawn_scoreboard_tags_from(&self, old_player: &Self) {
129        for tag in old_player.base.tags() {
130            self.base.add_tag(tag);
131        }
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use std::sync::Arc;
138
139    use glam::DVec3;
140    use steel_registry::{
141        init_vanilla_registry, item_stack::ItemStack, vanilla_attributes, vanilla_custom_stats,
142        vanilla_items, vanilla_mob_effects,
143    };
144    use steel_utils::{BlockPos, Identifier, types::GameType};
145
146    use crate::{
147        behavior::init_behaviors,
148        entity::{
149            Entity as _, LivingEntity as _, MobEffectInstance,
150            attribute::{AttributeModifier, AttributeModifierOperation},
151        },
152        inventory::container::Container as _,
153        level_data::RespawnData,
154        player::{ClientInformation, PlayerRespawnConfig, experience::Experience},
155        portal::{PortalKind, PortalProcessor},
156        test_support::{TestPlayerBuilder, fresh_test_world},
157    };
158
159    const ENTITY_ID: i32 = 17;
160
161    #[test]
162    fn replacement_retains_ender_chest_for_death_and_end_respawns() {
163        init_vanilla_registry();
164        let world_fixture = fresh_test_world("respawn_restore_ender_chest");
165        let world = &world_fixture.world;
166        let old_player = TestPlayerBuilder::new(Arc::clone(world), "Respawning", ENTITY_ID).build();
167        old_player
168            .ender_chest_inventory
169            .lock()
170            .set_item(0, ItemStack::with_count(&vanilla_items::DIAMOND, 4));
171
172        for restore_all in [false, true] {
173            let replacement =
174                old_player.new_respawn_replacement(Arc::clone(world), restore_all, false, true);
175
176            assert!(Arc::ptr_eq(
177                &replacement.ender_chest_inventory,
178                &old_player.ender_chest_inventory,
179            ));
180            assert_eq!(
181                replacement.ender_chest_inventory.lock().get_item(0).count(),
182                4
183            );
184        }
185    }
186
187    #[test]
188    fn replacement_is_fresh_detached_entity_in_same_session() {
189        init_vanilla_registry();
190        let source_world_fixture = fresh_test_world("respawn_restore_identity_source");
191        let source_world = &source_world_fixture.world;
192        let target_world_fixture = fresh_test_world("respawn_restore_identity_target");
193        let target_world = &target_world_fixture.world;
194        let client_information = ClientInformation {
195            language: "en_gb".to_owned(),
196            view_distance: 3,
197            ..ClientInformation::default()
198        };
199        let old_player = TestPlayerBuilder::new(Arc::clone(source_world), "Respawning", ENTITY_ID)
200            .client_information(client_information)
201            .build();
202        old_player.teleport_state.lock().teleport_id = 41;
203
204        let replacement =
205            old_player.new_respawn_replacement(Arc::clone(target_world), false, false, true);
206
207        assert_eq!(replacement.id(), old_player.id());
208        assert_eq!(replacement.uuid(), old_player.uuid());
209        assert_ne!(replacement.generation(), old_player.generation());
210        assert!(!Arc::ptr_eq(&replacement, &old_player));
211        assert!(Arc::ptr_eq(&replacement.connection, &old_player.connection));
212        assert!(Arc::ptr_eq(&replacement.session, &old_player.session));
213        assert!(Arc::ptr_eq(&replacement.config, &old_player.config));
214        assert!(Arc::ptr_eq(&replacement.get_world(), target_world));
215        assert_eq!(replacement.client_information().language, "en_gb");
216        assert_eq!(replacement.client_information().view_distance, 3);
217        assert_eq!(replacement.teleport_state.lock().teleport_id, 41);
218        replacement.synchronize_respawn_replacement(DVec3::new(1.0, 64.0, 1.0), (0.0, 0.0));
219        let mut teleport_state = replacement.teleport_state.lock();
220        assert_eq!(teleport_state.teleport_id, 42);
221        assert!(teleport_state.try_accept(41).is_none());
222        assert!(teleport_state.try_accept(42).is_some());
223        drop(teleport_state);
224        assert!(old_player.session.is_current_player(&old_player));
225        assert!(!old_player.session.is_current_player(&replacement));
226        assert!(!target_world.contains_player(&replacement));
227    }
228
229    #[test]
230    fn respawn_preserves_enchantment_seed_independently_of_experience_transfer() {
231        init_vanilla_registry();
232        init_behaviors();
233        let world_fixture = fresh_test_world("respawn_restore_enchantment_seed");
234        let world = &world_fixture.world;
235        let old_player = TestPlayerBuilder::new(Arc::clone(world), "Enchant", ENTITY_ID).build();
236
237        for seed in [0, 0x1234_5678, i32::MIN] {
238            *old_player.experience.lock() =
239                Experience::from_parts(8, 0.25, 91).with_enchantment_seed(seed);
240
241            for (restore_all, transfer_inventory) in [(false, false), (false, true), (true, false)]
242            {
243                let replacement = old_player.new_respawn_replacement(
244                    Arc::clone(world),
245                    restore_all,
246                    transfer_inventory,
247                    false,
248                );
249                let experience = replacement.experience.lock();
250                assert_eq!(
251                    experience.enchantment_seed(),
252                    seed,
253                    "restore_all={restore_all}, transfer_inventory={transfer_inventory}"
254                );
255                assert_eq!(
256                    experience.level(),
257                    if restore_all || transfer_inventory {
258                        8
259                    } else {
260                        0
261                    }
262                );
263            }
264            assert_eq!(old_player.experience.lock().enchantment_seed(), seed);
265        }
266    }
267
268    #[test]
269    #[expect(
270        clippy::too_many_lines,
271        reason = "the test keeps the vanilla restore-all state table in one comparison"
272    )]
273    fn restore_all_copies_vanilla_restore_state_without_mutating_source() {
274        init_vanilla_registry();
275        init_behaviors();
276        let source_world_fixture = fresh_test_world("respawn_restore_all_source");
277        let source_world = &source_world_fixture.world;
278        let target_world_fixture = fresh_test_world("respawn_restore_all_target");
279        let target_world = &target_world_fixture.world;
280        let old_player =
281            TestPlayerBuilder::new(Arc::clone(source_world), "Credits", ENTITY_ID).build();
282        let permanent_modifier_id = Identifier::vanilla_static("respawn_restore_test");
283
284        old_player.restore_game_modes(GameType::Spectator, Some(GameType::Creative));
285        old_player.abilities.lock().walking_speed = 0.7;
286        {
287            let mut attributes = old_player.attributes().lock();
288            attributes.set_base_value(vanilla_attributes::MAX_HEALTH, 30.0);
289            assert!(attributes.add_modifier(
290                vanilla_attributes::MAX_HEALTH,
291                AttributeModifier {
292                    id: permanent_modifier_id.clone(),
293                    amount: 5.0,
294                    operation: AttributeModifierOperation::AddValue,
295                },
296                true,
297            ));
298        }
299        old_player.set_health(12.0);
300        old_player.food_data.lock().food_level = 7;
301        assert!(old_player.add_mob_effect(MobEffectInstance::with_duration(
302            vanilla_mob_effects::SPEED,
303            200,
304            1,
305        )));
306        old_player
307            .inventory
308            .lock()
309            .set_item(0, ItemStack::with_count(&vanilla_items::OAK_LOG, 4));
310        *old_player.experience.lock() = Experience::from_parts(8, 0.25, 91);
311        old_player.experience.lock().dirty = false;
312        old_player.set_score(42);
313        old_player
314            .base
315            .set_portal_process(Some(PortalProcessor::new(
316                PortalKind::Nether,
317                BlockPos::new(1, 64, 1),
318            )));
319        old_player.award_custom_stat_with_count(&vanilla_custom_stats::JUMP, 9);
320        old_player
321            .permissions
322            .lock()
323            .groups
324            .push("builders".to_owned());
325        old_player.permissions.lock().version = 7;
326        *old_player.seen_credits.lock() = true;
327        *old_player.won_game.lock() = true;
328        old_player.base.add_tag("credits-return".to_owned());
329        let respawn_config = PlayerRespawnConfig::new(
330            RespawnData::of(target_world.key.clone(), BlockPos::new(4, 70, 6), 90.0, 0.0),
331            false,
332        );
333        *old_player.respawn_config.lock() = Some(respawn_config.clone());
334        old_player
335            .base
336            .set_position_local(DVec3::new(10.0, 80.0, 10.0));
337        old_player.mark_joined_world();
338
339        let replacement =
340            old_player.new_respawn_replacement(Arc::clone(target_world), true, false, true);
341
342        assert_eq!(replacement.game_mode(), GameType::Spectator);
343        assert_eq!(replacement.previous_game_mode(), Some(GameType::Creative));
344        let abilities = replacement.abilities.lock();
345        assert!(abilities.invulnerable);
346        assert!(abilities.may_fly);
347        assert!(abilities.flying);
348        assert_eq!(abilities.walking_speed, 0.1);
349        drop(abilities);
350        assert_eq!(replacement.get_health().to_bits(), 12.0_f32.to_bits());
351        assert_eq!(replacement.get_max_health().to_bits(), 35.0_f32.to_bits());
352        assert!(
353            replacement
354                .attributes()
355                .lock()
356                .has_modifier(vanilla_attributes::MAX_HEALTH, &permanent_modifier_id)
357        );
358        assert_eq!(replacement.food_data.lock().food_level, 7);
359        assert_eq!(
360            replacement.mob_effect(vanilla_mob_effects::SPEED),
361            old_player.mob_effect(vanilla_mob_effects::SPEED)
362        );
363        assert_eq!(replacement.inventory.lock().get_item(0).count(), 4);
364        assert_eq!(replacement.experience.lock().level(), 8);
365        assert_eq!(
366            replacement.experience.lock().progress().to_bits(),
367            0.25_f32.to_bits()
368        );
369        assert_eq!(replacement.experience.lock().total_points(), 91);
370        assert!(replacement.experience.lock().dirty);
371        assert!(!old_player.experience.lock().dirty);
372        assert_eq!(replacement.score(), 42);
373        assert_eq!(
374            replacement.base.portal_process(),
375            old_player.base.portal_process()
376        );
377        assert_eq!(replacement.stats(), old_player.stats());
378        assert_eq!(replacement.permissions.lock().groups, ["builders"]);
379        assert_eq!(replacement.permissions.lock().version, 7);
380        assert!(*replacement.seen_credits.lock());
381        assert_eq!(replacement.respawn_config(), Some(respawn_config));
382        assert_eq!(replacement.tags(), ["credits-return"]);
383        assert_eq!(replacement.position(), DVec3::ZERO);
384        assert!(!replacement.has_joined_world());
385        assert!(!*replacement.won_game.lock());
386
387        replacement.inventory.lock().clear_content();
388        replacement.food_data.lock().food_level = 2;
389        assert!(replacement.remove_mob_effect(vanilla_mob_effects::SPEED));
390        assert_eq!(old_player.inventory.lock().get_item(0).count(), 4);
391        assert_eq!(old_player.food_data.lock().food_level, 7);
392        assert!(old_player.has_mob_effect(vanilla_mob_effects::SPEED));
393    }
394
395    #[test]
396    fn death_restore_keeps_fresh_state_unless_inventory_transfer_is_requested() {
397        init_vanilla_registry();
398        init_behaviors();
399        let source_world_fixture = fresh_test_world("respawn_restore_death_source");
400        let source_world = &source_world_fixture.world;
401        let target_world_fixture = fresh_test_world("respawn_restore_death_target");
402        let target_world = &target_world_fixture.world;
403        let old_player =
404            TestPlayerBuilder::new(Arc::clone(source_world), "Death", ENTITY_ID).build();
405        let permanent_modifier_id = Identifier::vanilla_static("death_restore_test");
406
407        {
408            let mut attributes = old_player.attributes().lock();
409            attributes.set_base_value(vanilla_attributes::MAX_HEALTH, 30.0);
410            assert!(attributes.add_modifier(
411                vanilla_attributes::MAX_HEALTH,
412                AttributeModifier {
413                    id: permanent_modifier_id.clone(),
414                    amount: 5.0,
415                    operation: AttributeModifierOperation::AddValue,
416                },
417                true,
418            ));
419        }
420        old_player.set_health(0.0);
421        old_player.food_data.lock().food_level = 4;
422        assert!(old_player.add_mob_effect(MobEffectInstance::with_duration(
423            vanilla_mob_effects::SPEED,
424            100,
425            0,
426        )));
427        old_player
428            .inventory
429            .lock()
430            .set_item(0, ItemStack::with_count(&vanilla_items::STONE, 3));
431        *old_player.experience.lock() = Experience::from_parts(4, 0.5, 36);
432        old_player.experience.lock().dirty = false;
433        old_player.set_score(11);
434        old_player
435            .base
436            .set_portal_process(Some(PortalProcessor::new(
437                PortalKind::Nether,
438                BlockPos::new(2, 64, 2),
439            )));
440        *old_player.respawn_config.lock() = Some(PlayerRespawnConfig::new(
441            RespawnData::of(target_world.key.clone(), BlockPos::new(8, 70, 8), 0.0, 0.0),
442            false,
443        ));
444
445        let fresh =
446            old_player.new_respawn_replacement(Arc::clone(target_world), false, false, false);
447        let transferred =
448            old_player.new_respawn_replacement(Arc::clone(target_world), false, true, true);
449
450        assert_eq!(fresh.get_health().to_bits(), 30.0_f32.to_bits());
451        assert!(
452            !fresh
453                .attributes()
454                .lock()
455                .has_modifier(vanilla_attributes::MAX_HEALTH, &permanent_modifier_id)
456        );
457        assert_eq!(fresh.food_data.lock().food_level, 20);
458        assert!(!fresh.has_mob_effect(vanilla_mob_effects::SPEED));
459        assert!(fresh.inventory.lock().get_item(0).is_empty());
460        assert_eq!(fresh.experience.lock().total_points(), 0);
461        assert_eq!(fresh.score(), 0);
462        assert_eq!(fresh.base.portal_process(), None);
463        assert_eq!(fresh.respawn_config(), None);
464
465        assert_eq!(transferred.get_health().to_bits(), 30.0_f32.to_bits());
466        assert_eq!(transferred.inventory.lock().get_item(0).count(), 3);
467        assert_eq!(transferred.experience.lock().level(), 4);
468        assert_eq!(transferred.experience.lock().total_points(), 36);
469        assert!(transferred.experience.lock().dirty);
470        assert_eq!(transferred.score(), 11);
471        assert_eq!(transferred.respawn_config(), old_player.respawn_config());
472
473        assert_eq!(old_player.get_health().to_bits(), 0.0_f32.to_bits());
474        assert_eq!(old_player.inventory.lock().get_item(0).count(), 3);
475        assert_eq!(old_player.experience.lock().total_points(), 36);
476        assert!(old_player.has_mob_effect(vanilla_mob_effects::SPEED));
477    }
478}