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 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 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 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}