mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
Rebased onto current master after #2296 (PlayerRespawnEvent). - Fire PlayerChangeWorldEvent (cancellable) before the cross-dimension transfer in World::respawn_player; a plugin may redirect new_world, override position/yaw/pitch, or cancel to keep the player in the current world. - Use the safe transfer ordering (remove_player -> unload_watched_chunks -> change_world -> set_world -> publish) so no observer sees the player in a world whose chunk manager doesn't match, and update the entity's world reference (set_world) to fix the latent stale-world bug. - When cancelled or the target world can't be resolved, fall back to the current world's spawn. - The non-cancellable PlayerRespawnEvent (#2296) now fires after this event and observes the resolved world automatically; document the ordering and mutation-vs-cancellation semantics on both events.
This commit is contained in:
@@ -8,7 +8,20 @@ use super::PlayerEvent;
|
||||
|
||||
/// An event that occurs when a player gets teleported to another world.
|
||||
///
|
||||
/// This event contains information about the player changing worlds.
|
||||
/// `new_world` and the position/yaw/pitch fields are mutable; when not
|
||||
/// cancelled the modified values are applied as-is to the transfer and the
|
||||
/// following [`PlayerRespawnEvent`](super::player_respawn::PlayerRespawnEvent).
|
||||
/// Cancelling drops the whole event: the world stays unchanged and any edits
|
||||
/// are dropped, with the respawn using the values resolved before the event
|
||||
/// fired. To keep the player in the current world at a chosen position, don't
|
||||
/// cancel — set `new_world` to `previous_world` (matching UUID) and override
|
||||
/// the position/yaw/pitch fields; the transfer is skipped but those values
|
||||
/// take effect. When redirecting to a different world, set position/yaw/pitch
|
||||
/// valid for it.
|
||||
///
|
||||
/// On a cross-dimension respawn this fires before the non-cancellable
|
||||
/// [`PlayerRespawnEvent`](super::player_respawn::PlayerRespawnEvent), which
|
||||
/// then observes the resolved world.
|
||||
#[cancellable]
|
||||
#[derive(Event, Clone)]
|
||||
pub struct PlayerChangeWorldEvent {
|
||||
|
||||
@@ -10,6 +10,15 @@ use super::PlayerEvent;
|
||||
///
|
||||
/// This is a notification fired once a respawn destination (world, position and
|
||||
/// rotation) has been determined. It is not cancellable.
|
||||
///
|
||||
/// On a cross-dimension respawn the cancellable
|
||||
/// [`PlayerChangeWorldEvent`](super::player_change_world::PlayerChangeWorldEvent)
|
||||
/// fires first, before the world transfer; this event fires after the transfer
|
||||
/// and reflects the world the player actually ended up in — including
|
||||
/// `PlayerChangeWorldEvent` redirects and the no-op fallback (player stays in
|
||||
/// the previous world) when that event is cancelled. See that event's docs for
|
||||
/// the mutation-vs-cancellation semantics; in particular, edits made by a
|
||||
/// cancelled `PlayerChangeWorldEvent` do not carry over here.
|
||||
#[derive(Event, Clone)]
|
||||
pub struct PlayerRespawnEvent {
|
||||
/// The player who respawned.
|
||||
|
||||
@@ -43,8 +43,8 @@ use crate::{
|
||||
plugin::{
|
||||
block::block_break::BlockBreakEvent,
|
||||
player::{
|
||||
player_join::PlayerJoinEvent, player_leave::PlayerLeaveEvent,
|
||||
player_respawn::PlayerRespawnEvent,
|
||||
player_change_world::PlayerChangeWorldEvent, player_join::PlayerJoinEvent,
|
||||
player_leave::PlayerLeaveEvent, player_respawn::PlayerRespawnEvent,
|
||||
},
|
||||
},
|
||||
server::Server,
|
||||
@@ -3292,11 +3292,10 @@ impl World {
|
||||
)
|
||||
};
|
||||
|
||||
// Get target world (may be different from current world for cross-dimension respawn)
|
||||
let target_world = if respawn_dimension == self.dimension {
|
||||
// Candidate destination world for a cross-dimension respawn.
|
||||
let candidate_world = if respawn_dimension == self.dimension {
|
||||
None
|
||||
} else {
|
||||
// Cross-dimension respawn: get target world from server
|
||||
self.server.upgrade().map_or_else(
|
||||
|| {
|
||||
warn!("Could not get server for cross-dimension respawn");
|
||||
@@ -3312,38 +3311,76 @@ impl World {
|
||||
)
|
||||
};
|
||||
|
||||
// Handle cross-dimension transfer if we found a different target world
|
||||
let (target_world, position) = if let Some(ref new_world) = target_world {
|
||||
debug!(
|
||||
"Cross-dimension respawn: {} -> {}",
|
||||
self.dimension.minecraft_name, new_world.dimension.minecraft_name
|
||||
);
|
||||
// Fire PlayerChangeWorldEvent (cancellable) before the transfer; it runs before
|
||||
// the non-cancellable PlayerRespawnEvent, which observes the resolved world.
|
||||
let (resolved_world, position, yaw, pitch) = if let Some(new_world) = candidate_world {
|
||||
if let Some(server) = self.server.upgrade() {
|
||||
let event = server
|
||||
.plugin_manager
|
||||
.fire(PlayerChangeWorldEvent {
|
||||
player: player.clone(),
|
||||
previous_world: self.clone(),
|
||||
new_world: new_world.clone(),
|
||||
position,
|
||||
yaw,
|
||||
pitch,
|
||||
cancelled: false,
|
||||
})
|
||||
.await;
|
||||
|
||||
// Remove player from current world
|
||||
self.remove_player(player, false).await;
|
||||
new_world.players.rcu(|current_list| {
|
||||
let mut new_list = (**current_list).clone();
|
||||
new_list.push(player.clone());
|
||||
new_list
|
||||
});
|
||||
if event.cancelled {
|
||||
(None, position, yaw, pitch)
|
||||
} else {
|
||||
let destination = event.new_world;
|
||||
let position = event.position;
|
||||
let yaw = event.yaw;
|
||||
let pitch = event.pitch;
|
||||
|
||||
// Update chunk manager to target world
|
||||
player
|
||||
.chunk_manager
|
||||
.lock()
|
||||
.await
|
||||
.change_world(&self.level, new_world.clone());
|
||||
// Skip the transfer if redirected back to the current world.
|
||||
if destination.uuid != self.uuid {
|
||||
debug!(
|
||||
"Cross-dimension respawn: {} -> {}",
|
||||
self.dimension.minecraft_name, destination.dimension.minecraft_name
|
||||
);
|
||||
|
||||
// Unload watched chunks from current world
|
||||
player.unload_watched_chunks(self).await;
|
||||
// Detach from the old world before publishing into the new one, so no
|
||||
// observer sees the player in a world whose chunk manager doesn't match.
|
||||
self.remove_player(player, false).await;
|
||||
player.unload_watched_chunks(self).await;
|
||||
player
|
||||
.chunk_manager
|
||||
.lock()
|
||||
.await
|
||||
.change_world(&self.level, destination.clone());
|
||||
player.living_entity.entity.set_world(destination.clone());
|
||||
destination.players.rcu(|current_list| {
|
||||
let mut new_list = (**current_list).clone();
|
||||
new_list.push(player.clone());
|
||||
new_list
|
||||
});
|
||||
}
|
||||
|
||||
(new_world.clone(), position)
|
||||
(Some(destination), position, yaw, pitch)
|
||||
}
|
||||
} else {
|
||||
warn!("Server dropped during cross-dimension respawn");
|
||||
(None, position, yaw, pitch)
|
||||
}
|
||||
} else {
|
||||
if respawn_dimension != self.dimension {
|
||||
warn!(
|
||||
"Target world {:?} not found, using world spawn in {:?}",
|
||||
respawn_dimension, self.dimension
|
||||
);
|
||||
}
|
||||
(None, position, yaw, pitch)
|
||||
};
|
||||
|
||||
// Cancelled or unresolved cross-dimension respawns fall back to the current
|
||||
// world's spawn below; otherwise the resolved values from the event apply.
|
||||
let (target_world, position, yaw, pitch) = if let Some(ref new_world) = resolved_world {
|
||||
(new_world.clone(), position, yaw, pitch)
|
||||
} else if respawn_dimension != self.dimension {
|
||||
// Cross-dimension failed - fall back to current world's spawn
|
||||
warn!(
|
||||
"Target world {:?} not found, using world spawn in {:?}",
|
||||
respawn_dimension, self.dimension
|
||||
);
|
||||
// FIXME: This spawn position calculation is incorrect. Should use vanilla's
|
||||
// proper spawn position calculation (see #1381).
|
||||
let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4);
|
||||
@@ -3354,9 +3391,9 @@ impl World {
|
||||
(top + 1).into(),
|
||||
f64::from(spawn_z) + 0.5,
|
||||
);
|
||||
(self.clone(), fallback_pos)
|
||||
(self.clone(), fallback_pos, spawn_yaw, spawn_pitch)
|
||||
} else {
|
||||
(self.clone(), position)
|
||||
(self.clone(), position, yaw, pitch)
|
||||
};
|
||||
|
||||
// Notify plugins that the player has respawned (non-cancellable).
|
||||
|
||||
Reference in New Issue
Block a user