From 1dbfb75eccbe392e209824e36fcb2d7d9e3be1b0 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 27 Jun 2026 14:57:40 +0200 Subject: [PATCH] fix: end spawning --- .../bedrock/server/inventory_transaction.rs | 6 +- pumpkin-protocol/src/bedrock/server/mod.rs | 2 + .../server/set_player_inventory_options.rs | 24 ++ pumpkin-world/src/chunk/format/mod.rs | 7 +- pumpkin/src/block/blocks/signs.rs | 4 +- pumpkin/src/command/client_suggestions.rs | 2 +- pumpkin/src/command/commands/transfer.rs | 4 +- pumpkin/src/entity/mod.rs | 151 +++---- pumpkin/src/entity/player.rs | 39 +- pumpkin/src/lib.rs | 6 +- pumpkin/src/net/bedrock/mod.rs | 82 +++- pumpkin/src/plugin/api/context.rs | 2 +- .../wasm/wasm_host/wit/v0_1/events/server.rs | 4 +- .../loader/wasm/wasm_host/wit/v0_1/player.rs | 20 +- pumpkin/src/server/mod.rs | 2 +- pumpkin/src/world/bossbar.rs | 12 +- pumpkin/src/world/chunker.rs | 4 +- pumpkin/src/world/mod.rs | 379 ++++++++++-------- pumpkin/src/world/portal/mod.rs | 257 +++++++++++- 19 files changed, 645 insertions(+), 362 deletions(-) create mode 100644 pumpkin-protocol/src/bedrock/server/set_player_inventory_options.rs diff --git a/pumpkin-protocol/src/bedrock/server/inventory_transaction.rs b/pumpkin-protocol/src/bedrock/server/inventory_transaction.rs index 0b7de9425..446e3720f 100644 --- a/pumpkin-protocol/src/bedrock/server/inventory_transaction.rs +++ b/pumpkin-protocol/src/bedrock/server/inventory_transaction.rs @@ -190,16 +190,20 @@ impl PacketRead for SInventoryTransaction { fn read(buf: &mut R) -> Result { 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 { diff --git a/pumpkin-protocol/src/bedrock/server/mod.rs b/pumpkin-protocol/src/bedrock/server/mod.rs index 91e2585d4..70bca7a27 100644 --- a/pumpkin-protocol/src/bedrock/server/mod.rs +++ b/pumpkin-protocol/src/bedrock/server/mod.rs @@ -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::*; diff --git a/pumpkin-protocol/src/bedrock/server/set_player_inventory_options.rs b/pumpkin-protocol/src/bedrock/server/set_player_inventory_options.rs new file mode 100644 index 000000000..79f1491c9 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/server/set_player_inventory_options.rs @@ -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(reader: &mut R) -> Result { + 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)?, + }) + } +} diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index b285d66b3..4b2862b36 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -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 ); diff --git a/pumpkin/src/block/blocks/signs.rs b/pumpkin/src/block/blocks/signs.rs index 801b5505a..df6e38df8 100644 --- a/pumpkin/src/block/blocks/signs.rs +++ b/pumpkin/src/block/blocks/signs.rs @@ -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; diff --git a/pumpkin/src/command/client_suggestions.rs b/pumpkin/src/command/client_suggestions.rs index 9a37dbeb9..86c80c650 100644 --- a/pumpkin/src/command/client_suggestions.rs +++ b/pumpkin/src/command/client_suggestions.rs @@ -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; } } diff --git a/pumpkin/src/command/commands/transfer.rs b/pumpkin/src/command/commands/transfer.rs index 38c59997f..df188c91e 100644 --- a/pumpkin/src/command/commands/transfer.rs +++ b/pumpkin/src/command/commands/transfer.rs @@ -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))) diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 021ca3aba..2ad1b31fc 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -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>>, + pub portal_manager: Mutex>>, /// Custom name for the entity pub custom_name: ArcSwap>, /// 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, pos: BlockPos) { + pub async fn try_use_portal( + &self, + _portal_delay: u32, + portal_world: Arc, + 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; diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index c80752d95..242105510 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -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, /// The player's inventory. pub inventory: Arc, /// The player's `EnderChest` inventory. @@ -571,7 +571,7 @@ impl Player { #[expect(clippy::too_many_lines)] pub async fn new( - client: ClientPlatform, + client: Arc, gameprofile: GameProfile, config: PlayerConfig, world: Arc, @@ -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 = 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 = 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)) diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs index 3abd66358..e45bb31e3 100644 --- a/pumpkin/src/lib.rs +++ b/pumpkin/src/lib.rs @@ -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()); diff --git a/pumpkin/src/net/bedrock/mod.rs b/pumpkin/src/net/bedrock/mod.rs index 10accaa70..d0bfbe841 100644 --- a/pumpkin/src/net/bedrock/mod.rs +++ b/pumpkin/src/net/bedrock/mod.rs @@ -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>>>, tasks: TaskTracker, + rt_handle: tokio::runtime::Handle, outgoing_packet_queue_send: Sender, /// A queue of serialized packets to send to the network outgoing_packet_queue_recv: Mutex>>, @@ -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)) } } } diff --git a/pumpkin/src/plugin/api/context.rs b/pumpkin/src/plugin/api/context.rs index 1245b7514..3a21b2fc6 100644 --- a/pumpkin/src/plugin/api/context.rs +++ b/pumpkin/src/plugin/api/context.rs @@ -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) { 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, diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs index fc956df4f..518e31110 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs @@ -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) diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 0a1b28a47..e3deef818 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -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, ) -> wasmtime::Result>> { 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, ) -> wasmtime::Result>> { 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(); diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 47dfd3389..b03592b3f 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -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, profile: GameProfile, config: Option, ) -> Option<(Arc, Arc)> { diff --git a/pumpkin/src/world/bossbar.rs b/pumpkin/src/world/bossbar.rs index 1c5818cb3..7fc70e27b 100644 --- a/pumpkin/src/world/bossbar.rs +++ b/pumpkin/src/world/bossbar.rs @@ -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()); diff --git a/pumpkin/src/world/chunker.rs b/pumpkin/src/world/chunker.rs index f452f22b6..4178bd582 100644 --- a/pumpkin/src/world/chunker.rs +++ b/pumpkin/src/world/chunker.rs @@ -49,7 +49,7 @@ pub async fn update_position(player: &Arc) { 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.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 diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 9dad91a40..822e587fc 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -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> = 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(&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, server: Arc) { 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::>(), + ); - 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> = 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) { 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, 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::::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()), } diff --git a/pumpkin/src/world/portal/mod.rs b/pumpkin/src/world/portal/mod.rs index 69f19ae8d..40149d55e 100644 --- a/pumpkin/src/world/portal/mod.rs +++ b/pumpkin/src/world/portal/mod.rs @@ -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, - 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, + caller: &Arc, + _portal_entry_pos: BlockPos, + source_portal: Option, + ) -> Option { + 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, + pub position: Vector3, + pub yaw: Option, + pub pitch: Option, +} + +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, pub source_portal: Option, } -impl PortalManager { - pub const fn new(portal_delay: u32, portal_world: Arc, pos: BlockPos) -> Self { +impl PortalProcessor { + pub const fn new( + portal_type: PortalType, + entry_position: BlockPos, + destination_world: Arc, + ) -> 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 + } }