1use std::sync::Arc;
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 self.server.clone(),
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.seen_credits.lock() = *old_player.seen_credits.lock();
76 let residence = old_player.residence.lock().clone();
77 *self.residence.lock() = residence;
78
79 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}