Skip to main content

steel_core/player/lifecycle/
respawn_restore.rs

1use std::sync::Arc;
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            self.server.clone(),
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.seen_credits.lock() = *old_player.seen_credits.lock();
76        let residence = old_player.residence.lock().clone();
77        *self.residence.lock() = residence;
78
79        // Vanilla keeps the packet listener across respawn, so its teleport ID
80        // remains monotonic even though the pending position belongs to the new player.
81        self.teleport_state.lock().teleport_id = old_player.teleport_state.lock().teleport_id;
82
83        if spawn_block_valid {
84            let old_respawn_config = old_player.respawn_config.lock();
85            self.respawn_config.lock().clone_from(&old_respawn_config);
86        }
87
88        if restore_all {
89            self.restore_all_respawn_state_from(old_player);
90        } else {
91            self.set_health(self.get_max_health());
92            if transfer_inventory {
93                self.restore_inventory_experience_and_score_from(old_player);
94            }
95        }
96    }
97
98    fn restore_all_respawn_state_from(&self, old_player: &Self) {
99        self.set_health(old_player.get_health());
100        *self.food_data.lock() = old_player.food_data.lock().clone();
101
102        for effect in old_player.living_base.active_mob_effects() {
103            self.add_mob_effect(effect);
104        }
105
106        self.restore_inventory_experience_and_score_from(old_player);
107        self.base
108            .set_portal_process(old_player.base.portal_process());
109    }
110
111    fn restore_inventory_experience_and_score_from(&self, old_player: &Self) {
112        let inventory = old_player.inventory.lock().replacement_copy();
113        *self.inventory.lock() = inventory;
114        let experience = old_player.experience.lock();
115        *self.experience.lock() = Experience::from_parts(
116            experience.level(),
117            experience.progress(),
118            experience.total_points(),
119        );
120        self.set_score(old_player.score());
121    }
122
123    fn copy_respawn_scoreboard_tags_from(&self, old_player: &Self) {
124        for tag in old_player.base.tags() {
125            self.base.add_tag(tag);
126        }
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use std::sync::Arc;
133
134    use glam::DVec3;
135    use steel_registry::{
136        init_vanilla_registry, item_stack::ItemStack, vanilla_attributes, vanilla_custom_stats,
137        vanilla_items, vanilla_mob_effects,
138    };
139    use steel_utils::{BlockPos, Identifier, types::GameType};
140
141    use crate::{
142        behavior::init_behaviors,
143        entity::{
144            Entity as _, LivingEntity as _, MobEffectInstance,
145            attribute::{AttributeModifier, AttributeModifierOperation},
146        },
147        inventory::container::Container as _,
148        level_data::RespawnData,
149        player::{ClientInformation, PlayerRespawnConfig, experience::Experience},
150        portal::{PortalKind, PortalProcessor},
151        test_support::{TestPlayerBuilder, fresh_test_world},
152    };
153
154    const ENTITY_ID: i32 = 17;
155
156    #[test]
157    fn replacement_retains_ender_chest_for_death_and_end_respawns() {
158        init_vanilla_registry();
159        let world = fresh_test_world("respawn_restore_ender_chest");
160        let old_player =
161            TestPlayerBuilder::new(Arc::clone(&world), "Respawning", ENTITY_ID).build();
162        old_player
163            .ender_chest_inventory
164            .lock()
165            .set_item(0, ItemStack::with_count(&vanilla_items::DIAMOND, 4));
166
167        for restore_all in [false, true] {
168            let replacement =
169                old_player.new_respawn_replacement(Arc::clone(&world), restore_all, false, true);
170
171            assert!(Arc::ptr_eq(
172                &replacement.ender_chest_inventory,
173                &old_player.ender_chest_inventory,
174            ));
175            assert_eq!(
176                replacement.ender_chest_inventory.lock().get_item(0).count(),
177                4
178            );
179        }
180    }
181
182    #[test]
183    fn replacement_is_fresh_detached_entity_in_same_session() {
184        init_vanilla_registry();
185        let source_world = fresh_test_world("respawn_restore_identity_source");
186        let target_world = fresh_test_world("respawn_restore_identity_target");
187        let client_information = ClientInformation {
188            language: "en_gb".to_owned(),
189            view_distance: 3,
190            ..ClientInformation::default()
191        };
192        let old_player = TestPlayerBuilder::new(source_world, "Respawning", ENTITY_ID)
193            .client_information(client_information)
194            .build();
195        old_player.teleport_state.lock().teleport_id = 41;
196
197        let replacement =
198            old_player.new_respawn_replacement(Arc::clone(&target_world), false, false, true);
199
200        assert_eq!(replacement.id(), old_player.id());
201        assert_eq!(replacement.uuid(), old_player.uuid());
202        assert_ne!(replacement.generation(), old_player.generation());
203        assert!(!Arc::ptr_eq(&replacement, &old_player));
204        assert!(Arc::ptr_eq(&replacement.connection, &old_player.connection));
205        assert!(Arc::ptr_eq(&replacement.session, &old_player.session));
206        assert!(Arc::ptr_eq(&replacement.config, &old_player.config));
207        assert!(Arc::ptr_eq(&replacement.get_world(), &target_world));
208        assert_eq!(replacement.client_information().language, "en_gb");
209        assert_eq!(replacement.client_information().view_distance, 3);
210        assert_eq!(replacement.teleport_state.lock().teleport_id, 41);
211        replacement.synchronize_respawn_replacement(DVec3::new(1.0, 64.0, 1.0), (0.0, 0.0));
212        let mut teleport_state = replacement.teleport_state.lock();
213        assert_eq!(teleport_state.teleport_id, 42);
214        assert!(teleport_state.try_accept(41).is_none());
215        assert!(teleport_state.try_accept(42).is_some());
216        drop(teleport_state);
217        assert!(old_player.session.is_current_player(&old_player));
218        assert!(!old_player.session.is_current_player(&replacement));
219        assert!(!target_world.contains_player(&replacement));
220    }
221
222    #[test]
223    #[expect(
224        clippy::too_many_lines,
225        reason = "the test keeps the vanilla restore-all state table in one comparison"
226    )]
227    fn restore_all_copies_vanilla_restore_state_without_mutating_source() {
228        init_vanilla_registry();
229        init_behaviors();
230        let source_world = fresh_test_world("respawn_restore_all_source");
231        let target_world = fresh_test_world("respawn_restore_all_target");
232        let old_player = TestPlayerBuilder::new(source_world, "Credits", ENTITY_ID).build();
233        let permanent_modifier_id = Identifier::vanilla_static("respawn_restore_test");
234
235        old_player.restore_game_modes(GameType::Spectator, Some(GameType::Creative));
236        old_player.abilities.lock().walking_speed = 0.7;
237        {
238            let mut attributes = old_player.attributes().lock();
239            attributes.set_base_value(vanilla_attributes::MAX_HEALTH, 30.0);
240            assert!(attributes.add_modifier(
241                vanilla_attributes::MAX_HEALTH,
242                AttributeModifier {
243                    id: permanent_modifier_id.clone(),
244                    amount: 5.0,
245                    operation: AttributeModifierOperation::AddValue,
246                },
247                true,
248            ));
249        }
250        old_player.set_health(12.0);
251        old_player.food_data.lock().food_level = 7;
252        assert!(old_player.add_mob_effect(MobEffectInstance::with_duration(
253            vanilla_mob_effects::SPEED,
254            200,
255            1,
256        )));
257        old_player
258            .inventory
259            .lock()
260            .set_item(0, ItemStack::with_count(&vanilla_items::OAK_LOG, 4));
261        *old_player.experience.lock() = Experience::from_parts(8, 0.25, 91);
262        old_player.experience.lock().dirty = false;
263        old_player.set_score(42);
264        old_player
265            .base
266            .set_portal_process(Some(PortalProcessor::new(
267                PortalKind::Nether,
268                BlockPos::new(1, 64, 1),
269            )));
270        old_player.award_custom_stat_with_count(&vanilla_custom_stats::JUMP, 9);
271        old_player
272            .permissions
273            .lock()
274            .groups
275            .push("builders".to_owned());
276        old_player.permissions.lock().version = 7;
277        *old_player.seen_credits.lock() = true;
278        *old_player.won_game.lock() = true;
279        old_player.base.add_tag("credits-return".to_owned());
280        let respawn_config = PlayerRespawnConfig::new(
281            RespawnData::of(target_world.key.clone(), BlockPos::new(4, 70, 6), 90.0, 0.0),
282            false,
283        );
284        *old_player.respawn_config.lock() = Some(respawn_config.clone());
285        old_player
286            .base
287            .set_position_local(DVec3::new(10.0, 80.0, 10.0));
288        old_player.mark_joined_world();
289
290        let replacement = old_player.new_respawn_replacement(target_world, true, false, true);
291
292        assert_eq!(replacement.game_mode(), GameType::Spectator);
293        assert_eq!(replacement.previous_game_mode(), Some(GameType::Creative));
294        let abilities = replacement.abilities.lock();
295        assert!(abilities.invulnerable);
296        assert!(abilities.may_fly);
297        assert!(abilities.flying);
298        assert_eq!(abilities.walking_speed, 0.1);
299        drop(abilities);
300        assert_eq!(replacement.get_health().to_bits(), 12.0_f32.to_bits());
301        assert_eq!(replacement.get_max_health().to_bits(), 35.0_f32.to_bits());
302        assert!(
303            replacement
304                .attributes()
305                .lock()
306                .has_modifier(vanilla_attributes::MAX_HEALTH, &permanent_modifier_id)
307        );
308        assert_eq!(replacement.food_data.lock().food_level, 7);
309        assert_eq!(
310            replacement.mob_effect(vanilla_mob_effects::SPEED),
311            old_player.mob_effect(vanilla_mob_effects::SPEED)
312        );
313        assert_eq!(replacement.inventory.lock().get_item(0).count(), 4);
314        assert_eq!(replacement.experience.lock().level(), 8);
315        assert_eq!(
316            replacement.experience.lock().progress().to_bits(),
317            0.25_f32.to_bits()
318        );
319        assert_eq!(replacement.experience.lock().total_points(), 91);
320        assert!(replacement.experience.lock().dirty);
321        assert!(!old_player.experience.lock().dirty);
322        assert_eq!(replacement.score(), 42);
323        assert_eq!(
324            replacement.base.portal_process(),
325            old_player.base.portal_process()
326        );
327        assert_eq!(replacement.stats(), old_player.stats());
328        assert_eq!(replacement.permissions.lock().groups, ["builders"]);
329        assert_eq!(replacement.permissions.lock().version, 7);
330        assert!(*replacement.seen_credits.lock());
331        assert_eq!(replacement.respawn_config(), Some(respawn_config));
332        assert_eq!(replacement.tags(), ["credits-return"]);
333        assert_eq!(replacement.position(), DVec3::ZERO);
334        assert!(!replacement.has_joined_world());
335        assert!(!*replacement.won_game.lock());
336
337        replacement.inventory.lock().clear_content();
338        replacement.food_data.lock().food_level = 2;
339        assert!(replacement.remove_mob_effect(vanilla_mob_effects::SPEED));
340        assert_eq!(old_player.inventory.lock().get_item(0).count(), 4);
341        assert_eq!(old_player.food_data.lock().food_level, 7);
342        assert!(old_player.has_mob_effect(vanilla_mob_effects::SPEED));
343    }
344
345    #[test]
346    fn death_restore_keeps_fresh_state_unless_inventory_transfer_is_requested() {
347        init_vanilla_registry();
348        init_behaviors();
349        let source_world = fresh_test_world("respawn_restore_death_source");
350        let target_world = fresh_test_world("respawn_restore_death_target");
351        let old_player = TestPlayerBuilder::new(source_world, "Death", ENTITY_ID).build();
352        let permanent_modifier_id = Identifier::vanilla_static("death_restore_test");
353
354        {
355            let mut attributes = old_player.attributes().lock();
356            attributes.set_base_value(vanilla_attributes::MAX_HEALTH, 30.0);
357            assert!(attributes.add_modifier(
358                vanilla_attributes::MAX_HEALTH,
359                AttributeModifier {
360                    id: permanent_modifier_id.clone(),
361                    amount: 5.0,
362                    operation: AttributeModifierOperation::AddValue,
363                },
364                true,
365            ));
366        }
367        old_player.set_health(0.0);
368        old_player.food_data.lock().food_level = 4;
369        assert!(old_player.add_mob_effect(MobEffectInstance::with_duration(
370            vanilla_mob_effects::SPEED,
371            100,
372            0,
373        )));
374        old_player
375            .inventory
376            .lock()
377            .set_item(0, ItemStack::with_count(&vanilla_items::STONE, 3));
378        *old_player.experience.lock() = Experience::from_parts(4, 0.5, 36);
379        old_player.experience.lock().dirty = false;
380        old_player.set_score(11);
381        old_player
382            .base
383            .set_portal_process(Some(PortalProcessor::new(
384                PortalKind::Nether,
385                BlockPos::new(2, 64, 2),
386            )));
387        *old_player.respawn_config.lock() = Some(PlayerRespawnConfig::new(
388            RespawnData::of(target_world.key.clone(), BlockPos::new(8, 70, 8), 0.0, 0.0),
389            false,
390        ));
391
392        let fresh =
393            old_player.new_respawn_replacement(Arc::clone(&target_world), false, false, false);
394        let transferred = old_player.new_respawn_replacement(target_world, false, true, true);
395
396        assert_eq!(fresh.get_health().to_bits(), 30.0_f32.to_bits());
397        assert!(
398            !fresh
399                .attributes()
400                .lock()
401                .has_modifier(vanilla_attributes::MAX_HEALTH, &permanent_modifier_id)
402        );
403        assert_eq!(fresh.food_data.lock().food_level, 20);
404        assert!(!fresh.has_mob_effect(vanilla_mob_effects::SPEED));
405        assert!(fresh.inventory.lock().get_item(0).is_empty());
406        assert_eq!(fresh.experience.lock().total_points(), 0);
407        assert_eq!(fresh.score(), 0);
408        assert_eq!(fresh.base.portal_process(), None);
409        assert_eq!(fresh.respawn_config(), None);
410
411        assert_eq!(transferred.get_health().to_bits(), 30.0_f32.to_bits());
412        assert_eq!(transferred.inventory.lock().get_item(0).count(), 3);
413        assert_eq!(transferred.experience.lock().level(), 4);
414        assert_eq!(transferred.experience.lock().total_points(), 36);
415        assert!(transferred.experience.lock().dirty);
416        assert_eq!(transferred.score(), 11);
417        assert_eq!(transferred.respawn_config(), old_player.respawn_config());
418
419        assert_eq!(old_player.get_health().to_bits(), 0.0_f32.to_bits());
420        assert_eq!(old_player.inventory.lock().get_item(0).count(), 3);
421        assert_eq!(old_player.experience.lock().total_points(), 36);
422        assert!(old_player.has_mob_effect(vanilla_mob_effects::SPEED));
423    }
424}