fix: end spawning

This commit is contained in:
Alexander Medvedev
2026-06-27 14:57:40 +02:00
parent 03a90a40b1
commit 1dbfb75ecc
19 changed files with 645 additions and 362 deletions

View File

@@ -190,16 +190,20 @@ impl PacketRead for SInventoryTransaction {
fn read<R: Read>(buf: &mut R) -> Result<Self, Error> {
let legacy_request_id = VarInt::read(buf)?;
let has_legacy_slots = bool::read(buf)?;
let mut legacy_set_item_slots = Vec::new();
if legacy_request_id.0 != 0 {
if has_legacy_slots {
let len = VarUInt::read(buf)?.0;
for _ in 0..len {
legacy_set_item_slots.push(LegacySetItemSlot::read(buf)?);
}
}
let _has_transaction_type = bool::read(buf)?;
let transaction_type = VarUInt::read(buf)?;
let _has_tr_data = bool::read(buf)?;
let has_value = bool::read(buf)?;
let mut actions = Vec::new();
if has_value {

View File

@@ -20,6 +20,7 @@ pub mod request_chunk_radius;
pub mod request_network_settings;
pub mod resource_pack_response;
pub mod set_local_player_as_initialized;
pub mod set_player_inventory_options;
pub mod text;
pub use actor_event::*;
@@ -44,4 +45,5 @@ pub use request_chunk_radius::*;
pub use request_network_settings::*;
pub use resource_pack_response::*;
pub use set_local_player_as_initialized::*;
pub use set_player_inventory_options::*;
pub use text::*;

View File

@@ -0,0 +1,24 @@
use crate::serial::PacketRead;
use pumpkin_macros::packet;
use std::io::{Error, Read};
#[packet(307)]
pub struct SSetPlayerInventoryOptions {
pub left_inventory_tab: u8,
pub right_inventory_tab: u8,
pub filtering: bool,
pub inventory_layout: u8,
pub crafting_layout: u8,
}
impl PacketRead for SSetPlayerInventoryOptions {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
Ok(Self {
left_inventory_tab: u8::read(reader)?,
right_inventory_tab: u8::read(reader)?,
filtering: bool::read(reader)?,
inventory_layout: u8::read(reader)?,
crafting_layout: u8::read(reader)?,
})
}
}

View File

@@ -12,7 +12,7 @@ use pumpkin_data::{Block, chunk::ChunkStatus, fluid::Fluid};
use pumpkin_nbt::{compound::NbtCompound, nbt_long_array};
use rustc_hash::FxHashMap;
use tokio::sync::Mutex;
use tracing::debug;
use tracing::{debug, trace};
use uuid::Uuid;
use crate::{
@@ -291,6 +291,9 @@ impl ChunkEntityData {
}
let mut map = FxHashMap::default();
for entity_nbt in chunk_entity_data.entities {
if entity_nbt.is_empty() {
continue;
}
let uuid = if let Some(uuid) = entity_nbt.get_int_array("UUID") {
if uuid.len() != 4 {
debug!(
@@ -309,7 +312,7 @@ impl ChunkEntityData {
| (uuid[3] as u128),
)
} else {
debug!(
trace!(
"Entity in chunk {},{} is missing UUID: {:?}",
position.x, position.y, entity_nbt
);

View File

@@ -314,7 +314,7 @@ impl BlockBehaviour for SignBlock {
fn player_placed<'a>(&'a self, args: PlayerPlacedArgs<'a>) -> BlockFuture<'a, ()> {
Box::pin(async move {
match &args.player.client {
match args.player.client.as_ref() {
crate::net::ClientPlatform::Java(java) => {
java.send_sign_packet(*args.position, true).await;
}
@@ -454,7 +454,7 @@ impl BlockBehaviour for SignBlock {
let is_facing_front_text =
is_facing_front_text(args.world, args.position, args.block, args.player);
match &args.player.client {
match args.player.client.as_ref() {
ClientPlatform::Java(java) => {
java.send_sign_packet(*args.position, is_facing_front_text)
.await;

View File

@@ -365,7 +365,7 @@ pub async fn send_bedrock_commands_packet(
constraints: Vec::new(),
};
if let crate::net::ClientPlatform::Bedrock(bedrock_client) = &player.client {
if let crate::net::ClientPlatform::Bedrock(bedrock_client) = player.client.as_ref() {
bedrock_client.send_game_packet(&packet).await;
}
}

View File

@@ -58,7 +58,7 @@ impl CommandExecutor for TargetSelfExecutor {
let name = &player.gameprofile.name;
info!("[{name}: Transferring {name} to {hostname}:{port}]");
match &player.client {
match player.client.as_ref() {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&JavaCTransfer::new(hostname, VarInt(port)))
@@ -121,7 +121,7 @@ impl CommandExecutor for TargetPlayerExecutor {
}
for p in players {
match &p.client {
match p.client.as_ref() {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&JavaCTransfer::new(hostname, VarInt(port)))

View File

@@ -5,7 +5,7 @@ use crate::{
world::{
World,
chunker::is_within_view_distance,
portal::{NetherPortal, PortalManager, PortalSearchResult, SourcePortalInfo},
portal::{NetherPortal, PortalProcessor, PortalType, SourcePortalInfo},
},
};
use arc_swap::ArcSwap;
@@ -775,7 +775,7 @@ pub struct Entity {
pub portal_cooldown: AtomicU32,
pub portal_manager: Mutex<Option<Mutex<PortalManager>>>,
pub portal_manager: Mutex<Option<Mutex<PortalProcessor>>>,
/// Custom name for the entity
pub custom_name: ArcSwap<Option<TextComponent>>,
/// Indicates whether the entity's custom name is visible
@@ -2121,100 +2121,46 @@ impl Entity {
let mut manager_guard = self.portal_manager.lock().await;
let mut should_remove = false;
if let Some(pmanager_mutex) = manager_guard.as_ref() {
let mut portal_manager = pmanager_mutex.lock().await;
if portal_manager.tick() {
let mut portal_processor = pmanager_mutex.lock().await;
if portal_processor.process_portal_teleportation(
&self.world.load(),
caller.as_ref(),
true,
) {
self.portal_cooldown
.store(self.default_portal_cooldown(), Ordering::Relaxed);
let pos = self.pos.load();
let current_yaw = self.yaw.load();
let dimensions = self.entity_dimension.load();
let scale_factor_new = portal_manager.portal_world.dimension.coordinate_scale;
let scale_factor_current = self.world.load().dimension.coordinate_scale;
let scale_factor = scale_factor_current / scale_factor_new;
let target_pos =
BlockPos::floored(pos.x * scale_factor, pos.y, pos.z * scale_factor);
let dest_world = portal_manager.portal_world.clone();
let source_portal = portal_manager.source_portal.clone();
let source_axis = source_portal.as_ref().map(|p| p.axis);
drop(portal_manager);
let is_end_portal = dest_world.dimension == Dimension::THE_END
|| self.world.load().dimension == Dimension::THE_END;
let (teleport_pos, new_yaw) = if is_end_portal {
if dest_world.dimension == Dimension::THE_END {
// Entering the End: spawn on the obsidian platform at (100, 50, 0)
(Vector3::new(100.5f64, 50.0f64, 0.5f64), None)
} else {
// Leaving the End through the exit portal: return to overworld spawn
let info = dest_world.level_info.load();
(
Vector3::new(
f64::from(info.spawn_x) + 0.5,
f64::from(info.spawn_y),
f64::from(info.spawn_z) + 0.5,
),
None,
)
}
} else if let Some(dest_result) =
NetherPortal::search_for_portal(&dest_world, target_pos).await
{
let base_pos = source_portal.as_ref().map_or_else(
|| dest_result.get_teleport_position(),
|source| {
let source_result = PortalSearchResult {
lower_corner: source.lower_corner,
axis: source.axis,
width: source.width,
height: source.height,
};
let relative_pos = source_result.entity_pos_in_portal(pos, &dimensions);
dest_result.calculate_exit_position(relative_pos, &dimensions)
},
);
let final_pos =
dest_result.find_open_position(&dest_world, base_pos, &dimensions);
let yaw = dest_result.calculate_teleport_yaw(current_yaw, source_axis);
(final_pos, Some(yaw))
} else if let Some((build_pos, axis, is_fallback)) =
NetherPortal::find_safe_location(
&dest_world,
target_pos,
pumpkin_data::block_properties::HorizontalAxis::X,
let transition = portal_processor
.portal_type
.get_portal_destination(
&self.world.load(),
portal_processor.destination_world.clone(),
caller,
portal_processor.entry_position,
portal_processor.source_portal.clone(),
)
.await
{
NetherPortal::build_portal_frame(&dest_world, build_pos, axis, is_fallback)
.await;
drop(portal_processor);
if let Some(transition) = transition {
let dest_world = transition.new_world.clone();
let yaw = transition.yaw;
let pitch = transition.pitch;
let teleport_pos = transition.position;
// Teleport the main entity
caller
.clone()
.teleport(teleport_pos, yaw, pitch, dest_world.clone())
.await;
let new_portal = PortalSearchResult {
lower_corner: build_pos,
axis,
width: 2,
height: 3,
};
let center_pos = new_portal.get_teleport_position();
let final_pos =
new_portal.find_open_position(&dest_world, center_pos, &dimensions);
let yaw = new_portal.calculate_teleport_yaw(current_yaw, source_axis);
(final_pos, Some(yaw))
} else {
(target_pos.0.to_f64(), None)
};
// Teleport the main entity
caller
.clone()
.teleport(teleport_pos, new_yaw, None, dest_world.clone())
.await;
// Teleport all passengers recursively along with the vehicle
let yaw_delta = new_yaw.map(|y| y - current_yaw);
Self::teleport_passengers_recursive(self, teleport_pos, yaw_delta, &dest_world)
.await;
} else if portal_manager.ticks_in_portal == 0 {
// Teleport all passengers recursively along with the vehicle
let yaw_delta = yaw.map(|y| y - self.yaw.load());
Self::teleport_passengers_recursive(self, teleport_pos, yaw_delta, &dest_world)
.await;
}
} else if portal_processor.portal_time == 0 {
should_remove = true;
}
}
@@ -2262,7 +2208,12 @@ impl Entity {
})
}
pub async fn try_use_portal(&self, portal_delay: u32, portal_world: Arc<World>, pos: BlockPos) {
pub async fn try_use_portal(
&self,
_portal_delay: u32,
portal_world: Arc<World>,
pos: BlockPos,
) {
// Passengers don't teleport independently - they wait for their vehicle
if self.has_vehicle().await {
return;
@@ -2295,7 +2246,15 @@ impl Entity {
let mut manager = self.portal_manager.lock().await;
let world = self.world.load();
if manager.is_none() {
let mut new_manager = PortalManager::new(portal_delay, portal_world, pos);
let portal_type = if portal_world.dimension == Dimension::THE_END
|| self.world.load().dimension == Dimension::THE_END
{
PortalType::End
} else {
PortalType::Nether
};
let mut new_manager = PortalProcessor::new(portal_type, pos, portal_world);
if let Some(portal) = NetherPortal::get_on_axis(
&world,
@@ -2326,8 +2285,8 @@ impl Entity {
*manager = Some(Mutex::new(new_manager));
} else if let Some(manager) = manager.as_ref() {
let mut manager = manager.lock().await;
manager.pos = pos;
manager.in_portal = true;
manager.entry_position = pos;
manager.inside_portal_this_tick = true;
}
}
@@ -2712,7 +2671,7 @@ impl Entity {
let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
for player in world.players.load().iter() {
if let ClientPlatform::Bedrock(client) = &player.client {
if let ClientPlatform::Bedrock(client) = player.client.as_ref() {
let center = player.get_entity().chunk_pos.load();
let view_distance =
crate::world::chunker::get_view_distance(player).get() as i32;
@@ -2755,7 +2714,7 @@ impl Entity {
let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
for player in world.players.load().iter() {
if let ClientPlatform::Java(client) = &player.client {
if let ClientPlatform::Java(client) = player.client.as_ref() {
// Apply Chebyshev distance check
let center = player.get_entity().chunk_pos.load();
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32;

View File

@@ -411,7 +411,7 @@ pub struct Player {
/// The player's game profile information, including their username and UUID.
pub gameprofile: GameProfile,
/// The client connection associated with the player.
pub client: ClientPlatform,
pub client: Arc<ClientPlatform>,
/// The player's inventory.
pub inventory: Arc<PlayerInventory>,
/// The player's `EnderChest` inventory.
@@ -571,7 +571,7 @@ impl Player {
#[expect(clippy::too_many_lines)]
pub async fn new(
client: ClientPlatform,
client: Arc<ClientPlatform>,
gameprofile: GameProfile,
config: PlayerConfig,
world: Arc<World>,
@@ -1729,7 +1729,7 @@ impl Player {
}
pub async fn show_title(&self, text: &TextComponent, mode: &TitleMode) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(client) => match mode {
TitleMode::Title => client.enqueue_packet(&CTitleText::new(text)).await,
TitleMode::SubTitle => client.enqueue_packet(&CSubtitle::new(text)).await,
@@ -1757,7 +1757,7 @@ impl Player {
}
pub async fn send_title_animation(&self, fade_in: i32, stay: i32, fade_out: i32) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CTitleAnimation::new(fade_in, stay, fade_out))
@@ -1915,11 +1915,10 @@ impl Player {
*xp -= 1;
}
}
let (chunk_of_chunks, total_sent_chunks) = {
let mut chunk_manager = self.chunk_manager.lock().await;
chunk_manager.pull_new_chunks();
let chunks = if let ClientPlatform::Java(_) = self.client {
let chunks = if let ClientPlatform::Java(_) = self.client.as_ref() {
// Java clients can only send a limited amount of chunks per tick.
// If we have sent too many chunks without receiving an ack, we stop sending chunks.
chunk_manager
@@ -1930,11 +1929,12 @@ impl Player {
};
(chunks, chunk_manager.sent_chunks_count())
};
if let Some(chunk_of_chunks) = chunk_of_chunks {
self.client.send_chunks(&chunk_of_chunks).await;
if let ClientPlatform::Bedrock(bedrock_client) = &self.client
let client = self.client.clone();
tokio::spawn(async move {
client.send_chunks(&chunk_of_chunks).await;
});
if let ClientPlatform::Bedrock(bedrock_client) = self.client.as_ref()
&& !self.bedrock_spawned.load(Ordering::Relaxed)
&& total_sent_chunks > 4
{
@@ -1944,7 +1944,6 @@ impl Player {
self.bedrock_spawned.store(true, Ordering::Relaxed);
}
}
self.tick_counter.fetch_add(1, Ordering::Relaxed);
self.living_entity
.entity
@@ -1978,7 +1977,6 @@ impl Player {
.await;
}
}
self.last_attacked_ticks.fetch_add(1, Ordering::Relaxed);
let caller: Arc<dyn EntityBase> = self.clone();
@@ -1996,7 +1994,6 @@ impl Player {
// Timeout/keep alive handling
self.tick_client_load_timeout();
// Idle timeout handling
let now = Instant::now();
let idle_timeout_minutes = server.player_idle_timeout.load(Ordering::Relaxed);
@@ -2141,7 +2138,7 @@ impl Player {
/// Updates the current abilities the player has.
pub async fn send_abilities_update(&self) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let mut b = 0;
let abilities = &self.abilities.lock().await;
@@ -2244,7 +2241,7 @@ impl Player {
}
pub async fn send_stats(&self) {
if let ClientPlatform::Java(java) = &self.client {
if let ClientPlatform::Java(java) = self.client.as_ref() {
let stats_guard = self.stats.lock().await;
let packet_stats: Vec<Statistic> = stats_guard
.stats
@@ -2394,7 +2391,7 @@ impl Player {
self.permission_lvl.store(lvl);
self.send_permission_lvl_update();
if let ClientPlatform::Bedrock(_) = &self.client {
if let ClientPlatform::Bedrock(_) = self.client.as_ref() {
client_suggestions::send_bedrock_commands_packet(self, server, command_dispatcher)
.await;
} else {
@@ -2405,7 +2402,7 @@ impl Player {
/// Sends the world time to only this player.
pub async fn send_time(&self, world: &World) {
let l_world = world.level_time.lock().await;
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java_client) => {
java_client
.enqueue_packet(&CUpdateTime::new(
@@ -2624,7 +2621,7 @@ impl Player {
return;
}
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CSetHealth::new(
@@ -2898,7 +2895,7 @@ impl Player {
}],
));
match &self.client {
match self.client.as_ref() {
crate::net::ClientPlatform::Java(client) => {
client
.enqueue_packet(&CGameEvent::new(
@@ -3021,7 +3018,7 @@ impl Player {
/// Sends a custom payload packet to this player (Java edition only).
pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) {
if let ClientPlatform::Java(java) = &self.client {
if let ClientPlatform::Java(java) = self.client.as_ref() {
java.enqueue_packet(&CCustomPayload::new(channel, data))
.await;
}
@@ -3114,7 +3111,7 @@ impl Player {
}
pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CSystemChatMessage::new(text, overlay))

View File

@@ -468,13 +468,13 @@ impl PumpkinServer {
},
PacketHandlerResult::ReadyToPlay(profile,config) => {
if let Some((player, world)) = server_clone
.add_player(ClientPlatform::Java(java_client), profile, Some(config))
.add_player(Arc::new(ClientPlatform::Java(java_client)), profile, Some(config))
.await
{
world
.spawn_java_player(&server_clone.basic_config, &player, &server_clone)
.await;
if let ClientPlatform::Java(client) = &player.client {
if let ClientPlatform::Java(client) = player.client.as_ref() {
*client.player.lock().await = Some(player.clone());
client.progress_player_packets(&player, &server_clone).await;
@@ -558,7 +558,7 @@ impl PumpkinServer {
}
PacketHandlerResult::ReadyToPlay(profile, config) => {
if let Some((player, _world)) = server_clone
.add_player(ClientPlatform::Bedrock(client_clone.clone()), profile, Some(config))
.add_player(Arc::new(ClientPlatform::Bedrock(client_clone.clone())), profile, Some(config))
.await
{
*client_clone.player.lock().await = Some(player.clone());

View File

@@ -53,6 +53,7 @@ use pumpkin_protocol::{
request_network_settings::SRequestNetworkSettings,
resource_pack_response::SResourcePackResponse,
set_local_player_as_initialized::SSetLocalPlayerAsInitialized,
set_player_inventory_options::SSetPlayerInventoryOptions,
text::SText,
},
},
@@ -118,6 +119,7 @@ pub struct BedrockClient {
pub be_clients: Arc<Mutex<HashMap<SocketAddr, Arc<Self>>>>,
tasks: TaskTracker,
rt_handle: tokio::runtime::Handle,
outgoing_packet_queue_send: Sender<OutgoingPacket>,
/// A queue of serialized packets to send to the network
outgoing_packet_queue_recv: Mutex<Option<Receiver<OutgoingPacket>>>,
@@ -166,6 +168,7 @@ impl BedrockClient {
let (send, recv) = tokio::sync::mpsc::channel(4096);
let (priority_send, priority_recv) = tokio::sync::mpsc::channel(4096);
let (incoming_send, incoming_recv) = tokio::sync::mpsc::channel(4096);
let rt_handle = tokio::runtime::Handle::current();
Self {
socket,
player: Mutex::new(None),
@@ -176,6 +179,7 @@ impl BedrockClient {
network_writer: Arc::new(RwLock::new(UDPNetworkEncoder::new())),
network_reader: Mutex::new(UDPNetworkDecoder::new()),
tasks: TaskTracker::new(),
rt_handle,
outgoing_packet_queue_send: send,
outgoing_packet_queue_recv: Mutex::new(Some(recv)),
outgoing_packet_priority_send: priority_send,
@@ -338,19 +342,62 @@ impl BedrockClient {
return;
};
let mut valid_chunks = Vec::with_capacity(chunks.len());
for chunk in chunks {
let event = ChunkSend::new(player.world(), chunk.clone());
let event = server.plugin_manager.fire(event).await;
if event.cancelled {
continue;
if !event.cancelled {
valid_chunks.push(chunk.clone());
}
}
self.enqueue_packet_internal(&CLevelChunk {
dimension: 0,
cache_enabled: false,
chunk,
})
.await;
if valid_chunks.is_empty() {
return;
}
let mut serialize_tasks = Vec::with_capacity(valid_chunks.len());
for chunk in valid_chunks {
serialize_tasks.push(tokio::task::spawn_blocking(move || {
let mut packet_payload = Vec::new();
let packet = CLevelChunk {
dimension: 0,
cache_enabled: false,
chunk: &chunk,
};
packet
.write_packet(&mut packet_payload)
.map(|()| packet_payload)
}));
}
let mut encoded_payloads = Vec::with_capacity(serialize_tasks.len());
for task in serialize_tasks {
match task.await {
Ok(Ok(payload)) => encoded_payloads.push(payload),
Ok(Err(e)) => error!("Failed to serialize Bedrock chunk: {:?}", e),
Err(e) => error!("Join error in Bedrock chunk serialization: {:?}", e),
}
}
let mut packets_to_enqueue = Vec::with_capacity(encoded_payloads.len());
{
let encoder = self.network_writer.read().await;
for payload in encoded_payloads {
let mut packet_buf = Vec::new();
match encoder.write_game_packet(
CLevelChunk::PACKET_ID as u16,
SubClient::Main,
SubClient::Main,
&payload,
&mut packet_buf,
) {
Ok(()) => packets_to_enqueue.push(packet_buf),
Err(err) => error!("Failed to write game packet wrapper: {err}"),
}
}
}
for packet_buf in packets_to_enqueue {
self.enqueue_packet_data(packet_buf.into()).await;
}
}
@@ -1040,6 +1087,10 @@ impl BedrockClient {
&SSetLocalPlayerAsInitialized::read(reader)?,
);
}
SSetPlayerInventoryOptions::PACKET_ID => {
let _ = SSetPlayerInventoryOptions::read(reader)?;
// Ignore for now
}
SPlayerAction::PACKET_ID => {
self.handle_player_action(player, server, SPlayerAction::read(reader)?)
.await;
@@ -1210,20 +1261,9 @@ impl BedrockClient {
{
if self.close_token.is_cancelled() {
None
} else if tokio::runtime::Handle::try_current().is_ok() {
Some(self.tasks.spawn(task))
} else {
warn!("No Tokio runtime in current thread; running task on dedicated runtime thread");
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to build fallback runtime");
rt.block_on(async move {
let _ = task.await;
});
});
None
let _guard = self.rt_handle.enter();
Some(self.tasks.spawn(task))
}
}
}

View File

@@ -213,7 +213,7 @@ impl Context {
/// - `player`: The player for which the commands will be reloaded.
pub async fn reload_commands_for(&self, player: &Arc<Player>) {
let command_dispatcher = self.server.command_dispatcher.read().await;
if let ClientPlatform::Bedrock(_) = &player.client {
if let ClientPlatform::Bedrock(_) = player.client.as_ref() {
client_suggestions::send_bedrock_commands_packet(
player,
&self.server,

View File

@@ -27,7 +27,7 @@ impl ToFromWasmEvent for PacketReceivedEvent {
.add_player(self.player.clone())
.expect("failed to add player resource");
let packet = match &self.player.client {
let packet = match self.player.client.as_ref() {
ClientPlatform::Java(client) => {
let version = client.version.load();
generated_packets::deserialize_java_serverbound_packet(
@@ -75,7 +75,7 @@ impl ToFromWasmEvent for PacketSentEvent {
.add_player(self.player.clone())
.expect("failed to add player resource");
let packet = match &self.player.client {
let packet = match self.player.client.as_ref() {
ClientPlatform::Java(_) => {
generated_packets::clientbound_java_any_to_wit(self.packet.as_ref())
.map_or(ClientboundPacket::Unknown, ClientboundPacket::Java)

View File

@@ -1084,7 +1084,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
port: u16,
) -> wasmtime::Result<()> {
let player = player_from_resource(self, &player)?;
if let crate::net::ClientPlatform::Java(client) = &player.client {
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
client
.send_packet_now(&pumpkin_protocol::java::client::play::CTransfer::new(
&host,
@@ -1425,7 +1425,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
player: Resource<Player>,
) -> wasmtime::Result<Option<Resource<pumpkin::plugin::player::JavaPlayer>>> {
let player = player_from_resource(self, &player)?;
if let crate::net::ClientPlatform::Java(_) = player.client {
if let crate::net::ClientPlatform::Java(_) = player.client.as_ref() {
Ok(Some(self.add_java_player(player)?))
} else {
Ok(None)
@@ -1437,7 +1437,7 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
player: Resource<Player>,
) -> wasmtime::Result<Option<Resource<pumpkin::plugin::player::BedrockPlayer>>> {
let player = player_from_resource(self, &player)?;
if let crate::net::ClientPlatform::Bedrock(_) = player.client {
if let crate::net::ClientPlatform::Bedrock(_) = player.client.as_ref() {
Ok(Some(self.add_bedrock_player(player)?))
} else {
Ok(None)
@@ -1607,7 +1607,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
.provider
.clone();
if let crate::net::ClientPlatform::Java(_) = player.client {
if let crate::net::ClientPlatform::Java(_) = player.client.as_ref() {
player
.client
.send_packet_now(&pumpkin_protocol::java::client::play::CCustomPayload::new(
@@ -1757,7 +1757,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
external_title: dialog.external_title.as_ref().map(|t| text_component_from_resource(self, t)),
};
if let crate::net::ClientPlatform::Java(client) = &player.client {
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
match client.connection_state.load() {
pumpkin_protocol::ConnectionState::Config => {
client
@@ -1797,7 +1797,7 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState {
.provider
.clone();
if let crate::net::ClientPlatform::Java(client) = &player.client {
if let crate::net::ClientPlatform::Java(client) = player.client.as_ref() {
match client.connection_state.load() {
pumpkin_protocol::ConnectionState::Config => {
client
@@ -1847,7 +1847,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
.provider
.clone();
if let crate::net::ClientPlatform::Bedrock(client) = &player.client {
if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() {
Ok(to_wasm_bedrock_version(client.version.load()))
} else {
Ok(pumpkin::plugin::player::BedrockMinecraftVersion::Unknown)
@@ -2048,7 +2048,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
tick: VarULong(0),
};
if let crate::net::ClientPlatform::Bedrock(client) = &player.client {
if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() {
client.send_game_packet(&packet).await;
}
@@ -2068,7 +2068,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
.provider
.clone();
if let crate::net::ClientPlatform::Bedrock(client) = &player.client {
if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() {
let data = client.client_data.load();
(**data).as_ref().map_or_else(
|| Err(wasmtime::Error::msg("client data not available")),
@@ -2140,7 +2140,7 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState {
.provider
.clone();
if let crate::net::ClientPlatform::Bedrock(client) = &player.client {
if let crate::net::ClientPlatform::Bedrock(client) = player.client.as_ref() {
let form_id = client.next_form_id.fetch_add(1, Ordering::Relaxed);
let locale_str = player.config.load().locale.clone();

View File

@@ -454,7 +454,7 @@ impl Server {
/// You still have to spawn the `Player` in a `World` to let them join and make them visible.
pub async fn add_player(
&self,
client: ClientPlatform,
client: Arc<ClientPlatform>,
profile: GameProfile,
config: Option<PlayerConfig>,
) -> Option<(Arc<Player>, Arc<World>)> {

View File

@@ -94,7 +94,7 @@ impl Bossbar {
/// Extra methods for [`Player`] to send and manage the bossbar.
impl Player {
pub async fn send_bossbar(&self, bossbar: &Bossbar) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::Add {
title: bossbar.title.clone(),
@@ -123,7 +123,7 @@ impl Player {
}
pub async fn remove_bossbar(&self, uuid: Uuid) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::Remove;
@@ -141,7 +141,7 @@ impl Player {
}
pub async fn update_bossbar_health(&self, uuid: &Uuid, health: f32) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateHealth(health);
@@ -159,7 +159,7 @@ impl Player {
}
pub async fn update_bossbar_title(&self, uuid: &Uuid, title: TextComponent) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateTile(title);
@@ -183,7 +183,7 @@ impl Player {
dividers: BossbarDivisions,
_flags: BossbarFlags,
) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateStyle {
color: (color as u8).into(),
@@ -207,7 +207,7 @@ impl Player {
}
pub async fn update_bossbar_flags(&self, uuid: &Uuid, flags: BossbarFlags) {
match &self.client {
match self.client.as_ref() {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateFlags(flags.bits());

View File

@@ -49,7 +49,7 @@ pub async fn update_position(player: &Arc<Player>) {
return;
}
match &player.client {
match player.client.as_ref() {
ClientPlatform::Java(java_client) => {
java_client
.send_packet_now(&CCenterChunk {
@@ -93,7 +93,7 @@ pub async fn update_position(player: &Arc<Player>) {
player.watched_section.store(new_cylindrical);
if let ClientPlatform::Java(_) = &player.client {
if let ClientPlatform::Java(_) = player.client.as_ref() {
for chunk in &unloading_chunks {
player
.client

View File

@@ -143,7 +143,6 @@ use rand::{RngExt, rng};
use scoreboard::Scoreboard;
use time::LevelTime;
use tokio::sync::Mutex;
use tokio::task::JoinSet;
pub mod border;
pub mod bossbar;
@@ -507,7 +506,7 @@ impl World {
let mut recipients_by_version: BTreeMap<JavaMinecraftVersion, Vec<&'a JavaClient>> =
BTreeMap::new();
for player in players {
if let ClientPlatform::Java(java_client) = &player.client {
if let ClientPlatform::Java(java_client) = player.client.as_ref() {
recipients_by_version
.entry(java_client.version.load())
.or_default()
@@ -556,7 +555,7 @@ impl World {
pub fn broadcast_packet_all_sync<P: ClientPacket>(&self, packet: &P) {
let players = self.players.load();
for player in players.iter() {
match &player.client {
match player.client.as_ref() {
ClientPlatform::Java(java) => {
if let Ok(data) =
JavaClient::serialize_packet_for_version(packet, java.version.load())
@@ -624,7 +623,7 @@ impl World {
let mut be_recipients = Vec::new();
for player in players.iter() {
if let ClientPlatform::Bedrock(be_client) = &player.client {
if let ClientPlatform::Bedrock(be_client) = player.client.as_ref() {
be_recipients.push(be_client.clone());
}
}
@@ -700,7 +699,7 @@ impl World {
if except.contains(&p.gameprofile.id) {
continue;
}
match &p.client {
match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet),
}
@@ -725,7 +724,7 @@ impl World {
if except.contains(&p.gameprofile.id) {
continue;
}
match &p.client {
match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()),
}
@@ -897,79 +896,91 @@ impl World {
pub async fn tick(self: &Arc<Self>, server: Arc<Server>) {
let start = tokio::time::Instant::now();
// IMPORTANT: send flush_block_updates first to prevent issues with CAcknowledgeBlockChange
self.flush_block_updates().await;
self.flush_synced_block_events().await;
self.update_active_chunks();
self.tick_environment().await;
let chunk_start = tokio::time::Instant::now();
self.tick_chunks().await;
let chunk_elapsed = chunk_start.elapsed();
let world_for_chunks = self.clone();
let chunk_future = async move {
let t = tokio::time::Instant::now();
world_for_chunks.tick_chunks().await;
t.elapsed()
};
let player_start = tokio::time::Instant::now();
let players = self.players.load().clone();
let players = self.players.load();
let player_count = players.len();
let players_cache = Arc::new(
players
.iter()
.map(|player| {
let entity = player.get_entity();
let pos = entity.pos.load();
let bb = entity.bounding_box.load().expand(1.0, 0.5, 1.0);
(player.clone(), pos, bb)
})
.collect::<Vec<_>>(),
);
let mut player_tasks = tokio::task::JoinSet::new();
for player in players.iter() {
let player_clone = player.clone();
let server_clone = server.clone();
player_tasks.spawn(async move {
player_clone.tick(&server_clone).await;
});
}
while let Some(res) = player_tasks.join_next().await {
if let Err(e) = res {
error!("Player tick panicked: {:?}", e);
let server_for_players = server.clone();
let player_future = async move {
let t = tokio::time::Instant::now();
let mut tasks = tokio::task::JoinSet::new();
for player in players.iter() {
let p_clone = player.clone();
let s_clone = server_for_players.clone();
tasks.spawn(async move {
p_clone.tick(&s_clone).await;
});
}
}
let player_elapsed = player_start.elapsed();
let entity_start = tokio::time::Instant::now();
let entities_to_tick = self.entities.load().clone();
let entity_count = entities_to_tick.len();
let mut entity_tasks = tokio::task::JoinSet::new();
for entity in entities_to_tick.iter() {
let entity_clone = entity.clone();
let server_clone = server.clone();
let players_clone = players.clone();
entity_tasks.spawn(async move {
entity_clone.get_entity().age.fetch_add(1, Relaxed);
entity_clone.tick(&entity_clone, &server_clone).await;
let entity_inner = entity_clone.get_entity();
let entity_bb = entity_inner.bounding_box.load();
for player in players_clone.iter() {
let player_pos = player.get_entity().pos.load();
let entity_pos = entity_inner.pos.load();
if (player_pos.x - entity_pos.x).abs() < 5.0
&& (player_pos.y - entity_pos.y).abs() < 5.0
&& (player_pos.z - entity_pos.z).abs() < 5.0
&& player
.get_entity()
.bounding_box
.load()
.expand(1.0, 0.5, 1.0)
.intersects(&entity_bb)
{
entity_clone.on_player_collision(player).await;
break;
}
while let Some(res) = tasks.join_next().await {
if let Err(e) = res {
error!("Player tick panicked: {:?}", e);
}
});
}
while let Some(res) = entity_tasks.join_next().await {
if let Err(e) = res {
error!("Entity tick panicked: {:?}", e);
}
}
let entity_elapsed = entity_start.elapsed();
t.elapsed()
};
let entities_to_tick = self.entities.load();
let entity_count = entities_to_tick.len();
let server_for_entities = server.clone();
let entity_future = async move {
let t = tokio::time::Instant::now();
let mut tasks = tokio::task::JoinSet::new();
for entity in entities_to_tick.iter() {
let e_clone = entity.clone();
let s_clone = server_for_entities.clone();
let p_cache = players_cache.clone();
tasks.spawn(async move {
e_clone.get_entity().age.fetch_add(1, Relaxed);
e_clone.tick(&e_clone, &s_clone).await;
let entity_inner = e_clone.get_entity();
let entity_pos = entity_inner.pos.load();
let entity_bb = entity_inner.bounding_box.load();
for (player, player_pos, player_bb) in p_cache.iter() {
if (player_pos.x - entity_pos.x).abs() < 5.0
&& (player_pos.y - entity_pos.y).abs() < 5.0
&& (player_pos.z - entity_pos.z).abs() < 5.0
&& player_bb.intersects(&entity_bb)
{
e_clone.on_player_collision(player).await;
break;
}
}
});
}
while let Some(res) = tasks.join_next().await {
if let Err(e) = res {
error!("Entity tick panicked: {:?}", e);
}
}
t.elapsed()
};
let block_entity_start = tokio::time::Instant::now();
let active_chunks = self.active_chunks.load();
let block_entities: Vec<Arc<dyn BlockEntity>> = self
.block_entities
@@ -979,23 +990,34 @@ impl World {
.collect();
let block_entity_count = block_entities.len();
let mut block_entity_tasks = tokio::task::JoinSet::new();
for block_entity in block_entities {
let world_clone = self.clone();
block_entity_tasks.spawn(async move {
block_entity.tick(&world_clone).await;
});
}
while let Some(res) = block_entity_tasks.join_next().await {
if let Err(e) = res {
error!("Block entity tick panicked: {:?}", e);
let world_for_be = self.clone();
let block_entity_future = async move {
let t = tokio::time::Instant::now();
let mut tasks = tokio::task::JoinSet::new();
for be in block_entities {
let be_clone = be.clone();
let w_clone = world_for_be.clone();
tasks.spawn(async move {
be_clone.tick(&w_clone).await;
});
}
}
let block_entity_elapsed = block_entity_start.elapsed();
while let Some(res) = tasks.join_next().await {
if let Err(e) = res {
error!("Block entity panicked: {:?}", e);
}
}
t.elapsed()
};
let (chunk_elapsed, player_elapsed, entity_elapsed, block_entity_elapsed) = tokio::join!(
chunk_future,
player_future,
entity_future,
block_entity_future
);
self.level.chunk_loading.lock().unwrap().send_change();
// Tick the End dragon fight (only on THE_END worlds).
if let Some(ref fight_mutex) = self.dragon_fight {
dragon_fight::DragonFight::tick(fight_mutex, self).await;
}
@@ -1069,7 +1091,7 @@ impl World {
});
for p in recipients {
match &p.client {
match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => {
for (block_pos, block_state_id) in &updates {
@@ -1161,65 +1183,84 @@ impl World {
}
}
#[expect(clippy::too_many_lines)]
pub async fn tick_chunks(self: &Arc<Self>) {
let active_chunks = self.active_chunks.load();
let tick_data = self.level.get_tick_data(&active_chunks);
// ONE JoinSet for all chunk operations
let mut chunk_tasks = tokio::task::JoinSet::new();
// 1. Spawn Block Ticks
for scheduled_tick in tick_data.block_ticks {
let block = self.get_block(&scheduled_tick.position);
if let Some(pumpkin_block) = self.block_registry.get_pumpkin_block(block.id) {
pumpkin_block
.on_scheduled_tick(OnScheduledTickArgs {
world: self,
block,
position: &scheduled_tick.position,
})
.await;
}
}
for scheduled_tick in tick_data.fluid_ticks {
let fluid = self.get_fluid(&scheduled_tick.position);
if let Some(pumpkin_fluid) = self.block_registry.get_pumpkin_fluid(fluid.id) {
pumpkin_fluid
.on_scheduled_tick(self, fluid, &scheduled_tick.position)
.await;
}
}
for scheduled_tick in tick_data.random_ticks {
let (block, fluid) = match (scheduled_tick.tick_block, scheduled_tick.tick_fluid) {
(true, true) => {
let (block, fluid) = self.get_block_and_fluid(&scheduled_tick.position);
(Some(block), Some(fluid))
let world = self.clone();
let pos = scheduled_tick.position; // Clone for the move closure
chunk_tasks.spawn(async move {
let block = world.get_block(&pos);
if let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id) {
pumpkin_block
.on_scheduled_tick(OnScheduledTickArgs {
world: &world,
block,
position: &pos,
})
.await;
}
(true, false) => (Some(self.get_block(&scheduled_tick.position)), None),
(false, true) => (None, Some(self.get_fluid(&scheduled_tick.position))),
(false, false) => (None, None),
};
if let Some(block) = block
&& let Some(pumpkin_block) = self.block_registry.get_pumpkin_block(block.id)
{
pumpkin_block
.random_tick(RandomTickArgs {
world: self,
block,
position: &scheduled_tick.position,
})
.await;
}
if let Some(fluid) = fluid
&& let Some(pumpkin_fluid) = self.block_registry.get_pumpkin_fluid(fluid.id)
{
pumpkin_fluid
.random_tick(fluid, self, &scheduled_tick.position)
.await;
}
});
}
let spawn_state = self.spawn_state.load();
// 2. Spawn Fluid Ticks
for scheduled_tick in tick_data.fluid_ticks {
let world = self.clone();
let pos = scheduled_tick.position;
chunk_tasks.spawn(async move {
let fluid = world.get_fluid(&pos);
if let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id) {
pumpkin_fluid.on_scheduled_tick(&world, fluid, &pos).await;
}
});
}
// TODO gamerule this.spawnEnemies || this.spawnFriendlies
// 3. Spawn Random Ticks
for scheduled_tick in tick_data.random_ticks {
let world = self.clone();
let pos = scheduled_tick.position;
let tick_block = scheduled_tick.tick_block;
let tick_fluid = scheduled_tick.tick_fluid;
chunk_tasks.spawn(async move {
let (block, fluid) = match (tick_block, tick_fluid) {
(true, true) => {
let (b, f) = world.get_block_and_fluid(&pos);
(Some(b), Some(f))
}
(true, false) => (Some(world.get_block(&pos)), None),
(false, true) => (None, Some(world.get_fluid(&pos))),
(false, false) => (None, None),
};
if let Some(block) = block
&& let Some(pumpkin_block) = world.block_registry.get_pumpkin_block(block.id)
{
pumpkin_block
.random_tick(RandomTickArgs {
world: &world,
block,
position: &pos,
})
.await;
}
if let Some(fluid) = fluid
&& let Some(pumpkin_fluid) = world.block_registry.get_pumpkin_fluid(fluid.id)
{
pumpkin_fluid.random_tick(fluid, &world, &pos).await;
}
});
}
// 4. Calculate Spawn List (Sequential setup)
let spawn_state = self.spawn_state.load();
let (spawn_mobs, spawn_monsters, peaceful) = {
let lock = self.level_info.load();
(
@@ -1231,43 +1272,43 @@ impl World {
let spawn_passives = self.level_time.lock().await.time_of_day % 400 == 0;
let spawn_enemies = !peaceful && spawn_monsters && spawn_mobs;
let spawn_passives = spawn_passives && spawn_mobs;
let spawn_list: Vec<&'static MobCategory> =
natural_spawner::get_filtered_spawning_categories(
&spawn_state,
spawn_mobs,
spawn_enemies,
spawn_passives,
);
if spawn_list.is_empty() {
return;
}
let spawn_list = Arc::new(natural_spawner::get_filtered_spawning_categories(
&spawn_state,
spawn_mobs,
spawn_enemies,
spawn_passives,
));
let mut spawning_chunks = Vec::new();
for pos in active_chunks.iter() {
if let Some(chunk) = self.level.read_chunk_sync(pos, std::clone::Clone::clone) {
spawning_chunks.push((*pos, chunk));
// 5. Spawn Chunk Spawners into the SAME JoinSet
if !spawn_list.is_empty() {
let mut spawning_chunks = Vec::new();
for pos in active_chunks.iter() {
if let Some(chunk) = self.level.read_chunk_sync(pos, std::clone::Clone::clone) {
spawning_chunks.push((*pos, chunk));
}
}
spawning_chunks.shuffle(&mut rng());
for (pos, chunk) in spawning_chunks {
let world = self.clone();
let s_list = spawn_list.clone();
let s_state = spawn_state.clone();
chunk_tasks.spawn(async move {
world
.tick_spawning_chunk(pos, &chunk, &s_list, &s_state)
.await;
});
}
}
// log::debug!("spawning list size {}", spawn_list.len());
spawning_chunks.shuffle(&mut rng());
let mut spawn_tasks = JoinSet::new();
let spawn_list = Arc::new(spawn_list);
for (pos, chunk) in spawning_chunks {
let world = self.clone();
let spawn_list = spawn_list.clone();
let spawn_state = spawn_state.clone();
let chunk = chunk.clone();
spawn_tasks.spawn(async move {
world
.tick_spawning_chunk(pos, &chunk, &spawn_list, &spawn_state)
.await;
});
while let Some(res) = chunk_tasks.join_next().await {
if let Err(e) = res {
error!("Chunk task panicked: {:?}", e);
}
}
while spawn_tasks.join_next().await.is_some() {}
}
pub fn get_fluid_collisions(self: &Arc<Self>, bounding_box: BoundingBox) -> Vec<&Fluid> {
@@ -2894,7 +2935,7 @@ impl World {
player.send_active_effects().await;
self.send_player_equipment(player).await;
if let crate::net::ClientPlatform::Java(java_client) = &player.client
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref()
&& server.advanced_config.recipe.send_recipes
{
java_client
@@ -2956,7 +2997,7 @@ impl World {
yaw: f32,
pitch: f32,
) {
if let ClientPlatform::Java(client) = &player.client {
if let ClientPlatform::Java(client) = player.client.as_ref() {
self.worldborder.lock().await.init_client(client).await;
}
@@ -3003,7 +3044,7 @@ impl World {
};
for player in self.players.load().iter() {
let mut sound_id = Sound::EntityGenericExplode as u16;
if let ClientPlatform::Java(java_client) = &player.client {
if let ClientPlatform::Java(java_client) = player.client.as_ref() {
sound_id = remap_sound_id_for_version(sound_id, java_client.version.load());
}
let sound = IdOr::<SoundEvent>::Id(sound_id);
@@ -3228,7 +3269,7 @@ impl World {
.await;
// Ensure at least the center chunk is sent synchronously before teleport.
if let crate::net::ClientPlatform::Java(java_client) = &player.client {
if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() {
let center_chunk = player.get_entity().chunk_pos.load();
let chunk = target_world
.level
@@ -5051,7 +5092,7 @@ impl World {
});
for p in recipients {
match &p.client {
match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet),
}
@@ -5107,7 +5148,7 @@ impl World {
let mut bedrock_recipients = Vec::new();
for p in recipients {
match &p.client {
match p.client.as_ref() {
ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()),
}

View File

@@ -1,7 +1,11 @@
use std::sync::Arc;
use pumpkin_data::Block;
use pumpkin_data::block_properties::HorizontalAxis;
use pumpkin_data::dimension::Dimension;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::BlockFlags;
use super::World;
@@ -31,23 +35,215 @@ impl From<&PortalSearchResult> for SourcePortalInfo {
}
}
pub struct PortalManager {
pub portal_delay: u32,
pub portal_world: Arc<World>,
pub pos: BlockPos,
pub ticks_in_portal: u32,
pub in_portal: bool,
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PortalType {
Nether,
End,
}
impl PortalType {
pub fn get_portal_transition_time(
&self,
current_world: &World,
entity: &dyn crate::entity::EntityBase,
) -> u32 {
match self {
Self::End => 0,
Self::Nether => {
let entity_type = entity.get_entity().entity_type;
let level_info = current_world.level_info.load();
match entity_type.id {
id if id == pumpkin_data::entity::EntityType::PLAYER.id => (current_world
.get_player_by_id(entity.get_entity().entity_id))
.map_or(80, |player| match player.gamemode.load() {
pumpkin_util::GameMode::Creative => {
level_info.game_rules.players_nether_portal_creative_delay as u32
}
_ => level_info.game_rules.players_nether_portal_default_delay as u32,
}),
_ => 0,
}
}
}
}
#[expect(clippy::too_many_lines)]
pub async fn get_portal_destination(
&self,
current_level: &World,
dest_world: Arc<World>,
caller: &Arc<dyn crate::entity::EntityBase>,
_portal_entry_pos: BlockPos,
source_portal: Option<SourcePortalInfo>,
) -> Option<TeleportTransition> {
match self {
Self::End => {
let is_end_portal = dest_world.dimension == Dimension::THE_END
|| current_level.dimension == Dimension::THE_END;
if is_end_portal {
if dest_world.dimension == Dimension::THE_END {
// Entering the End: spawn on the obsidian platform at (100, 49, 0) for players, or (100, 50, 0) for other entities
let is_player = caller
.get_living_entity()
.is_some_and(crate::entity::living::LivingEntity::is_player);
let y = if is_player { 49.0 } else { 50.0 };
// Ensure chunks covering the platform are loaded/generated
dest_world
.get_block_state_async(&BlockPos::new(98, 49, -2))
.await;
dest_world
.get_block_state_async(&BlockPos::new(102, 49, 2))
.await;
// Generate/regenerate the obsidian platform (5x5 obsidian at Y=48, and 5x5x3 air above it)
let platform_pos = BlockPos::new(100, 49, 0);
for dx in -2..=2 {
for dz in -2..=2 {
for dy in -1..3 {
let block = if dy == -1 {
Block::OBSIDIAN
} else {
Block::AIR
};
let target_pos = BlockPos::new(
platform_pos.0.x + dx,
platform_pos.0.y + dy,
platform_pos.0.z + dz,
);
dest_world
.set_block_state(
&target_pos,
block.default_state.id,
BlockFlags::NOTIFY_ALL,
)
.await;
}
}
}
Some(TeleportTransition {
new_world: dest_world,
position: Vector3::new(100.5f64, y, 0.5f64),
yaw: Some(90.0f32),
pitch: None,
})
} else {
// Leaving the End through the exit portal: return to overworld spawn
let info = dest_world.level_info.load();
Some(TeleportTransition {
new_world: dest_world,
position: Vector3::new(
f64::from(info.spawn_x) + 0.5,
f64::from(info.spawn_y),
f64::from(info.spawn_z) + 0.5,
),
yaw: None,
pitch: None,
})
}
} else {
None
}
}
Self::Nether => {
let pos = caller.get_entity().pos.load();
let current_yaw = caller.get_entity().yaw.load();
let dimensions = caller.get_entity().entity_dimension.load();
let scale_factor_new = dest_world.dimension.coordinate_scale;
let scale_factor_current = current_level.dimension.coordinate_scale;
let scale_factor = scale_factor_current / scale_factor_new;
let target_pos =
BlockPos::floored(pos.x * scale_factor, pos.y, pos.z * scale_factor);
let source_axis = source_portal.as_ref().map(|p| p.axis);
let (final_pos, yaw) = if let Some(dest_result) =
NetherPortal::search_for_portal(&dest_world, target_pos).await
{
let base_pos = source_portal.as_ref().map_or_else(
|| dest_result.get_teleport_position(),
|source| {
let source_result = PortalSearchResult {
lower_corner: source.lower_corner,
axis: source.axis,
width: source.width,
height: source.height,
};
let relative_pos = source_result.entity_pos_in_portal(pos, &dimensions);
dest_result.calculate_exit_position(relative_pos, &dimensions)
},
);
let final_pos =
dest_result.find_open_position(&dest_world, base_pos, &dimensions);
let yaw = dest_result.calculate_teleport_yaw(current_yaw, source_axis);
(final_pos, Some(yaw))
} else if let Some((build_pos, axis, is_fallback)) =
NetherPortal::find_safe_location(
&dest_world,
target_pos,
pumpkin_data::block_properties::HorizontalAxis::X,
)
.await
{
NetherPortal::build_portal_frame(&dest_world, build_pos, axis, is_fallback)
.await;
let new_portal = PortalSearchResult {
lower_corner: build_pos,
axis,
width: 2,
height: 3,
};
let center_pos = new_portal.get_teleport_position();
let final_pos =
new_portal.find_open_position(&dest_world, center_pos, &dimensions);
let yaw = new_portal.calculate_teleport_yaw(current_yaw, source_axis);
(final_pos, Some(yaw))
} else {
(target_pos.0.to_f64(), None)
};
Some(TeleportTransition {
new_world: dest_world,
position: final_pos,
yaw,
pitch: None,
})
}
}
}
}
pub struct TeleportTransition {
pub new_world: Arc<World>,
pub position: Vector3<f64>,
pub yaw: Option<f32>,
pub pitch: Option<f32>,
}
pub struct PortalProcessor {
pub portal_type: PortalType,
pub entry_position: BlockPos,
pub portal_time: u32,
pub inside_portal_this_tick: bool,
pub destination_world: Arc<World>,
pub source_portal: Option<SourcePortalInfo>,
}
impl PortalManager {
pub const fn new(portal_delay: u32, portal_world: Arc<World>, pos: BlockPos) -> Self {
impl PortalProcessor {
pub const fn new(
portal_type: PortalType,
entry_position: BlockPos,
destination_world: Arc<World>,
) -> Self {
Self {
portal_delay,
portal_world,
pos,
ticks_in_portal: 0,
in_portal: true,
portal_type,
entry_position,
portal_time: 0,
inside_portal_this_tick: true,
destination_world,
source_portal: None,
}
}
@@ -56,18 +252,35 @@ impl PortalManager {
self.source_portal = Some(info);
}
pub const fn tick(&mut self) -> bool {
if self.in_portal {
self.in_portal = false;
self.ticks_in_portal += 1;
self.ticks_in_portal >= self.portal_delay
} else {
if self.ticks_in_portal < 4 {
self.ticks_in_portal = 0;
pub fn process_portal_teleportation(
&mut self,
current_world: &World,
entity: &dyn crate::entity::EntityBase,
allowed_to_teleport: bool,
) -> bool {
if self.inside_portal_this_tick {
self.inside_portal_this_tick = false;
if allowed_to_teleport {
self.portal_time += 1;
let transition_time = self
.portal_type
.get_portal_transition_time(current_world, entity);
self.portal_time >= transition_time
} else {
self.ticks_in_portal -= 4;
false
}
} else {
self.decay_tick();
false
}
}
pub const fn decay_tick(&mut self) {
self.portal_time = self.portal_time.saturating_sub(4);
}
#[must_use]
pub const fn has_expired(&self) -> bool {
self.portal_time == 0
}
}