use crate::block::entities::{BlockEntity, block_entity_from_nbt}; use dashmap::DashMap; use pumpkin_data::attributes::Attributes; use pumpkin_data::chunk::Biome; use pumpkin_data::item::{BedrockItem, BedrockItemVersion}; use pumpkin_protocol::bedrock::client::item_registry::{CItemRegistry, ItemDefinition}; use pumpkin_protocol::bedrock::client::level_event::{CLevelEvent, LevelEvent}; use pumpkin_protocol::bedrock::client::{CInventoryContent, EntityProperties}; use pumpkin_protocol::bedrock::network_item::{ ContainerName, FullContainerName, NetworkItemDescriptor, NetworkItemStackDescriptor, }; use pumpkin_protocol::codec::data_component::data_to_proto_sound; use pumpkin_world::generation::proto_chunk::GenerationCache; use std::sync::atomic::Ordering::Relaxed; use std::sync::{Arc, Weak}; use std::{ collections::{BTreeMap, HashMap}, sync::atomic::Ordering, }; use tracing::{debug, error, info, trace, warn}; pub mod chunker; pub mod explosion; pub mod loot; pub mod map; pub mod portal; pub mod time; use crate::block::RandomTickArgs; use crate::world::chunker::is_within_view_distance; use crate::world::{chunker::get_view_distance, loot::LootContextParameters}; use crate::{block::BlockEvent, entity::item::ItemEntity}; use crate::{ block::{ self, registry::BlockRegistry, {OnNeighborUpdateArgs, OnScheduledTickArgs}, }, command::client_suggestions, entity::{Entity, EntityBase, NBTStorage, player::Player, r#type::from_type}, error::PumpkinError, net::{ClientPlatform, java::JavaClient}, plugin::{ block::block_break::BlockBreakEvent, player::{ player_change_world::PlayerChangeWorldEvent, player_join::PlayerJoinEvent, player_leave::PlayerLeaveEvent, player_respawn::PlayerRespawnEvent, }, }, server::Server, }; use arc_swap::ArcSwap; use border::Worldborder; use bytes::BufMut; use explosion::Explosion; use pumpkin_config::BasicConfiguration; use pumpkin_data::block_properties::{blocks_movement, is_air}; use pumpkin_data::block_rotation::{Mirror, Rotation}; use pumpkin_data::chunk_gen_settings::GenerationSettings; use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::MobCategory; use pumpkin_data::fluid::{Falling, FluidProperties, FluidState}; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::tracked_data::TrackedData; use pumpkin_data::{ Block, BlockStateId, entity::{EntityStatus, EntityType}, fluid::Fluid, item_stack::ItemStack, particle::Particle, sound::{Sound, SoundCategory}, sound_id_remap::remap_sound_id_for_version, world::{RAW, WorldEvent}, }; use pumpkin_data::{BlockDirection, BlockState, HorizontalFacingExt, translation}; use pumpkin_inventory::crafting::recipe_provider::RecipeProvider; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed}; use pumpkin_protocol::bedrock::client::set_actor_data::{CSetActorData, PropertySyncData}; use pumpkin_protocol::bedrock::client::start_game::{CStartGame, ServerTelemetryData}; use pumpkin_protocol::java::client::play::{ CBlockUpdate, CChunkBatchEnd, CChunkBatchStart, CChunkData, CDisguisedChatMessage, CExplosion, CRespawn, CSetBlockDestroyStage, CWorldEvent, PlayerSpawnData, }; use pumpkin_protocol::java::client::play::{ CPlayerSpawnPosition, CRecipeBookAdd, CRecipeBookSettings, CSystemChatMessage, }; use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata}; use pumpkin_protocol::{ BClientPacket, ClientPacket, IdOr, SoundEvent, bedrock::{ client::{ add_player::CAddPlayer, creative_content::{CCreativeContent, CreativeCategory, Entry, Group}, gamerules_changed::GameRules, player_list::{CPlayerList, PlayerListEntry, Skin}, remove_actor::CRemoveActor, start_game::{Experiments, GamePublishSetting, LevelSettings}, update_attributes::{Attribute, CUpdateAttributes}, }, server::text::SText, }, codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong}, java::{ self, client::play::{ CBlockEntityData, CEntityStatus, CGameEvent, CLogin, CMultiBlockUpdate, CPlayerChatMessage, CPlayerInfoUpdate, CRemoveEntities, CRemovePlayerInfo, CSetSelectedSlot, CSoundEffect, CSpawnEntity, FilterType, GameEvent, InitChat, PlayerAction, PlayerInfoFlags, }, server::play::SChatMessage, }, }; use pumpkin_protocol::{ codec::item_stack_seralizer::ItemStackSerializer, java::client::play::{CBlockEvent, CRemoveMobEffect, CSetEquipment, CUpdateMobEffect}, }; use pumpkin_util::GameMode; use pumpkin_util::resource_location::ResourceLocation; use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::version::JavaMinecraftVersion; use pumpkin_util::{ Difficulty, math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}, }; use pumpkin_util::{ math::{get_section_cord, position::chunk_section_from_pos, vector2::Vector2}, random::{RandomImpl, get_seed, xoroshiro128::Xoroshiro}, }; use pumpkin_world::inventory::Clearable; use pumpkin_world::world::{GetBlockError, WorldPortalExt}; use pumpkin_world::{ CURRENT_BEDROCK_MC_VERSION, biome, chunk::io::Dirtiable, inventory::Inventory, }; use pumpkin_world::{chunk::ChunkData, world::BlockAccessor}; use pumpkin_world::{level::Level, tick::TickPriority}; pub use pumpkin_world::{world::BlockFlags, world_info::LevelData}; use rand::seq::SliceRandom; use rand::{RngExt, rng}; use scoreboard::Scoreboard; use time::LevelTime; use tokio::sync::Mutex; pub mod block_placer; pub mod border; pub mod bossbar; pub mod custom_bossbar; pub mod dragon_fight; pub mod end_podium; pub mod natural_spawner; pub mod scoreboard; pub mod weather; use crate::world::natural_spawner::{SpawnState, spawn_for_chunk}; use pumpkin_config::lighting::LightingEngineConfig; use pumpkin_data::effect::StatusEffect; use pumpkin_world::chunk::ChunkHeightmapType::{self, MotionBlocking}; use uuid::Uuid; use weather::Weather; type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; const MAX_LIGHT_LEVEL: u8 = 15; use rustc_hash::{FxHashMap, FxHashSet}; impl PumpkinError for GetBlockError { fn is_kick(&self) -> bool { false } fn severity(&self) -> tracing::Level { tracing::Level::WARN } fn client_kick_reason(&self) -> Option { None } } /// Represents a Minecraft world, containing entities, players, and the underlying level data. /// /// Each dimension (Overworld, Nether, End) typically has its own `World`. /// /// **Key Responsibilities:** /// /// - Manages the `Level` instance for handling chunk-related operations. /// - Stores and tracks active `Player` entities within the world. /// - Provides a central hub for interacting with the world's entities and environment. pub struct World { /// Represents the World's Unique Identifier pub uuid: Uuid, /// The underlying level, responsible for chunk management and terrain generation. pub level: Arc, pub level_info: Arc>, /// A map of active players within the world, keyed by their unique UUID. pub players: ArcSwap>>, /// A map of active entities within the world, keyed by their unique UUID. /// This does not include players. pub entities: ArcSwap>>, /// The world's scoreboard, used for tracking scores, objectives, and display information. pub scoreboard: Mutex, /// The world's worldborder, defining the playable area and controlling its expansion or contraction. pub worldborder: Mutex, /// The world's time, including counting ticks for weather, time cycles, and statistics. pub level_time: Mutex, /// The type of dimension the world is in. pub dimension: Dimension, pub sea_level: i32, pub min_y: i32, /// The world's weather, including rain and thunder levels. pub weather: Mutex, /// Block Behaviour pub block_registry: Arc, pub server: Weak, synced_block_event_queue: Mutex>, /// A map of unsent block changes, keyed by block position. unsent_block_changes: Mutex>, /// POI storage for fast portal lookups pub portal_poi: Mutex, /// End Dragon fight manager (only present in `THE_END` dimension). pub dragon_fight: Option>, pub spawn_state: ArcSwap, pub active_chunks: ArcSwap>>, pub forced_chunks: std::sync::Mutex>>, /// Block entities indexed by chunk, so ticking only visits the currently /// active chunks instead of scanning every loaded block entity each tick. pub block_entities: DashMap, FxHashMap>>, } impl PartialEq for World { fn eq(&self, other: &Self) -> bool { self.uuid == other.uuid } } impl Eq for World {} impl World { pub async fn get_block_state_id_async(&self, position: &BlockPos) -> BlockStateId { if !self.is_in_build_limit(*position) { return Block::AIR.default_state.id; } let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); self.level .get_or_fetch_chunk(chunk_coordinate, |chunk| { chunk .section .get_block_absolute_y(relative.x as usize, relative.y, relative.z as usize) .unwrap_or(Block::AIR.default_state.id) }) .await } pub async fn get_block_state_async(&self, position: &BlockPos) -> &'static BlockState { let id = self.get_block_state_id_async(position).await; BlockState::from_id(id) } pub async fn get_heightmap_height_async( &self, height_map: ChunkHeightmapType, x: i32, z: i32, ) -> i32 { let chunk_pos = Vector2::new(x >> 4, z >> 4); self.level .get_or_fetch_chunk(chunk_pos, |chunk| { chunk .heightmap .lock() .unwrap() .get(height_map, x, z, self.min_y) }) .await } #[must_use] pub fn load( level: Arc, level_info: Arc>, dimension: Dimension, block_registry: Arc, server: Weak, ) -> Self { // TODO let generation_settings = GenerationSettings::from_dimension(&dimension); // Load portal POI from disk (PoiStorage::new automatically loads from disk if files exist) let portal_poi = portal::PortalPoiStorage::new(level.level_folder.poi_folder.clone()); let dragon_fight = (dimension.minecraft_name == Dimension::THE_END.minecraft_name) .then(|| Mutex::new(dragon_fight::DragonFight::new())); Self { uuid: Uuid::new_v4(), level, level_info, players: ArcSwap::new(Arc::new(Vec::new())), entities: ArcSwap::new(Arc::new(Vec::new())), scoreboard: Mutex::new(Scoreboard::default()), worldborder: Mutex::new(Worldborder::new(0.0, 0.0, 5.999_996_8E7, 0, 5, 300)), level_time: Mutex::new(LevelTime::new()), dimension, weather: Mutex::new(Weather::new()), block_registry, sea_level: generation_settings.sea_level, min_y: i32::from(generation_settings.shape.min_y), synced_block_event_queue: Mutex::new(Vec::new()), unsent_block_changes: Mutex::new(HashMap::new()), portal_poi: Mutex::new(portal_poi), dragon_fight, spawn_state: ArcSwap::new(Arc::new(SpawnState::empty())), active_chunks: ArcSwap::new(Arc::new(FxHashSet::default())), forced_chunks: std::sync::Mutex::new(FxHashSet::default()), server, block_entities: DashMap::new(), } } pub fn update_active_chunks(self: &Arc) { let mut active_chunks = FxHashSet::default(); let sim_dist = self.server.upgrade().map_or(10, |s| { s.advanced_config.networking.java.simulation_distance.get() }) as i32; for player in self.players.load().iter() { let center = player.get_entity().chunk_pos.load(); for dx in -sim_dist..=sim_dist { for dy in -sim_dist..=sim_dist { active_chunks.insert(center.add_raw(dx, dy)); } } } if let Ok(forced) = self.forced_chunks.lock() { active_chunks.extend(forced.iter().copied()); } let mut spawnable_chunks = 0; for pos in &active_chunks { if self.level.is_chunk_loaded(pos) { spawnable_chunks += 1; self.migrate_pending_block_entities(*pos); } } self.active_chunks.store(Arc::new(active_chunks)); self.spawn_state.store(Arc::new(SpawnState::new( spawnable_chunks, &self.entities, self, ))); } pub fn get_lighting_config(&self) -> LightingEngineConfig { self.server .upgrade() .map(|s| s.advanced_config.world.lighting) .unwrap_or_default() } /// Get the world folder name (e.g., `world`, `world_nether`, `world_the_end`). /// Falls back to "world" if the name cannot be determined. pub fn get_world_name(&self) -> &str { self.level .level_folder .root_folder .file_name() .and_then(|n| n.to_str()) .unwrap_or("world") } pub async fn shutdown(&self) { for entity in self.entities.load().iter() { self.save_entity(entity).await; } // Save portal POI to disk let save_result = self.portal_poi.lock().await.save_all(); if let Err(e) = save_result { error!("Failed to save portal POI: {e}"); } self.level.shutdown().await; } /// Serializes a live entity into its current chunk's entity data. The live /// entity list is the source of truth while a chunk is loaded (its saved NBT /// is consumed on load), so this simply appends the entity to the chunk it is /// currently in; the chunk is rewritten from scratch every unload cycle, so /// there is nothing stale to deduplicate. async fn save_entity(&self, entity: &Arc) { let current_chunk = entity.get_entity().block_pos.load().chunk_position(); let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt).await; let chunk = self.level.get_entity_chunk(current_chunk).await; chunk.data.lock().await.push(nbt); chunk.mark_dirty(true); } /// Sends an entity status update to all players tracking the specified entity. pub fn send_entity_status(&self, entity: &Entity, status: EntityStatus) { let chunk_pos = entity.chunk_pos.load(); self.broadcast_to_chunk( chunk_pos, &CEntityStatus::new(entity.entity_id, status as i8), ); } pub fn send_remove_mob_effect(&self, entity: &Entity, effect_type: &'static StatusEffect) { let chunk_pos = entity.chunk_pos.load(); self.broadcast_to_chunk( chunk_pos, &CRemoveMobEffect::new(entity.entity_id.into(), VarInt(i32::from(effect_type.id))), ); } pub fn send_add_mob_effect(&self, entity: &Entity, effect: &pumpkin_data::potion::Effect) { // TODO: only nearby let mut flags: i8 = 0; if effect.ambient { flags |= 0x01; } if effect.show_particles { flags |= 0x02; } if effect.show_icon { flags |= 0x04; } self.broadcast_packet_all(&CUpdateMobEffect::new( VarInt(entity.entity_id), VarInt(i32::from(effect.effect_type.id)), VarInt(i32::from(effect.amplifier)), VarInt(effect.duration), flags, )); } pub fn set_difficulty(&self, difficulty: Difficulty) { let current_info = self.level_info.load(); let mut new_info = (**current_info).clone(); new_info.difficulty = difficulty; self.level_info.store(Arc::new(new_info)); } pub async fn add_synced_block_event(&self, pos: BlockPos, r#type: u8, data: u8) { let mut queue = self.synced_block_event_queue.lock().await; queue.push(BlockEvent { pos, r#type, data }); } pub async fn flush_synced_block_events(self: &Arc) { // THIS IS IMPORTANT // it prevents deadlocks and also removes the need to wait for a lock when adding a new synced block let events = { let mut queue = self.synced_block_event_queue.lock().await; std::mem::take(&mut *queue) }; for event in events { let block = self.get_block(&event.pos); if !self .block_registry .on_synced_block_event(block, self, &event.pos, event.r#type, event.data) .await { continue; } let chunk_pos = event.pos.chunk_position(); self.broadcast_to_chunk( chunk_pos, &CBlockEvent::new( event.pos, event.r#type, event.data, VarInt(block.id.as_u16() as i32), ), ); } } fn collect_java_recipients_by_version<'a>( players: impl Iterator>, ) -> BTreeMap> { let mut recipients_by_version: BTreeMap> = BTreeMap::new(); for player in players { if let ClientPlatform::Java(java_client) = player.client.as_ref() { recipients_by_version .entry(java_client.version.load()) .or_default() .push(java_client); } } recipients_by_version } fn broadcast_java_grouped( packet: &P, recipients_by_version: BTreeMap>, ) { for (version, recipients) in recipients_by_version { let packet_data = match JavaClient::serialize_packet_for_version(packet, version) { Ok(packet_data) => packet_data, Err(err) => { error!( "Failed to serialize packet {} for version {:?}: {}", std::any::type_name::

(), version, err ); continue; } }; for recipient in recipients { recipient.try_enqueue_packet_data(packet_data.clone()); } } } /// Broadcasts a packet to all connected players within the world. /// Please avoid this as we want to replace it with `broadcast_editioned` /// /// Sends the specified packet to every player currently logged in to the world. /// /// **Note:** This function acquires a lock on the `current_players` map, ensuring thread safety. pub fn broadcast_packet_all(&self, packet: &P) { let players = self.players.load(); let recipients_by_version = Self::collect_java_recipients_by_version(players.iter()); Self::broadcast_java_grouped(packet, recipients_by_version); } pub fn broadcast_packet_all_sync(&self, packet: &P) { let players = self.players.load(); for player in players.iter() { match player.client.as_ref() { ClientPlatform::Java(java) => { if let Ok(data) = JavaClient::serialize_packet_for_version(packet, java.version.load()) { java.try_enqueue_packet_data(data); } } ClientPlatform::Bedrock(_) => { // TODO } } } } pub async fn broadcast_system_message(&self, message: &TextComponent, overlay: bool) { let je_packet = CSystemChatMessage::new(message, overlay); let be_packet = Self::component_to_bedrock_text(message); self.broadcast_editioned(&je_packet, &be_packet).await; } fn component_to_bedrock_text(message: &TextComponent) -> SText<'static> { match &*message.0.content { pumpkin_util::text::TextContent::Translate { translate, bedrock_translate, with, } => { let key = bedrock_translate.as_deref().unwrap_or(translate.as_ref()); let parameters = with .iter() .map(pumpkin_util::text::TextComponentBase::to_bedrock_string) .collect(); SText::translation(key.to_string(), parameters) } _ => SText::system_message( message .0 .to_bedrock_legacy(pumpkin_util::translation::Locale::EnUs), ), } } pub async fn broadcast_message( &self, message: &TextComponent, sender_name: &TextComponent, chat_type: u8, target_name: Option<&TextComponent>, ) { let be_packet = SText::new(message.clone().get_text(), sender_name.clone().get_text()); let je_packet = CDisguisedChatMessage::new(message, (chat_type + 1).into(), sender_name, target_name); self.broadcast_editioned(&je_packet, &be_packet).await; } // This should replace broadcast_packet_all at some point pub async fn broadcast_editioned( &self, je_packet: &J, be_packet: &B, ) { let players = self.players.load(); let je_recipients_by_version = Self::collect_java_recipients_by_version(players.iter()); let mut be_recipients = Vec::new(); for player in players.iter() { if let ClientPlatform::Bedrock(be_client) = player.client.as_ref() { be_recipients.push(be_client.clone()); } } Self::broadcast_java_grouped(je_packet, je_recipients_by_version); for recipient in be_recipients { recipient.enqueue_packet(be_packet).await; } } pub async fn broadcast_secure_player_chat( &self, sender: &Arc, chat_message: &SChatMessage<'_>, decorated_message: &TextComponent, ) { let messages_sent: i32 = sender.chat_session.lock().await.messages_sent; let sender_last_seen = { let cache = sender.signature_cache.lock().await; cache.last_seen.clone() }; for recipient in self.players.load().iter() { let messages_received: i32 = recipient.chat_session.lock().await.messages_received; let packet = &CPlayerChatMessage::new( VarInt(messages_received), sender.gameprofile.id, VarInt(messages_sent), chat_message.signature.map(std::convert::Into::into), chat_message.message.into(), chat_message.timestamp, chat_message.salt, sender_last_seen.indexed_for(recipient).await, Some(decorated_message.clone()), FilterType::PassThrough, (RAW + 1).into(), // Custom registry chat_type with no sender name TextComponent::empty(), // Not needed since we're injecting the name in the message for custom formatting None, ); recipient.client.enqueue_packet(packet).await; if let Some(signature) = chat_message.signature { recipient .signature_cache .lock() .await .add_seen_signature(signature); } if recipient.gameprofile.id != sender.gameprofile.id { // Sender may update recipient on signatures recipient hasn't seen recipient .signature_cache .lock() .await .cache_signatures(sender_last_seen.as_ref()); } recipient.chat_session.lock().await.messages_received += 1; } sender.chat_session.lock().await.messages_sent += 1; } pub fn broadcast_packet_except_editioned_sync( &self, except: &[uuid::Uuid], je_packet: &J, be_packet: &B, ) { let players = self.players.load(); let mut java_recipients = Vec::new(); for p in players.iter() { if except.contains(&p.gameprofile.id) { continue; } match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), } } let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, recipients_by_version); } pub async fn broadcast_packet_except_editioned( &self, except: &[uuid::Uuid], je_packet: &J, be_packet: &B, ) { let players = self.players.load(); let mut java_recipients = Vec::new(); let mut bedrock_recipients = Vec::new(); for p in players.iter() { if except.contains(&p.gameprofile.id) { continue; } match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()), } } let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, recipients_by_version); for be_client in bedrock_recipients { be_client.enqueue_packet(be_packet).await; } } /// Broadcasts a packet to all connected players within the world, excluding the specified players. /// /// Sends the specified packet to every player currently logged in to the world, excluding the players listed in the `except` parameter. /// /// **Note:** This function acquires a lock on the `current_players` map, ensuring thread safety. pub fn broadcast_packet_except(&self, except: &[uuid::Uuid], packet: &P) { let players = self.players.load(); let recipients_by_version = Self::collect_java_recipients_by_version( players .iter() .filter(|candidate| !except.contains(&candidate.gameprofile.id)), ); Self::broadcast_java_grouped(packet, recipients_by_version); } pub fn spawn_particle( &self, position: Vector3, offset: Vector3, max_speed: f32, particle_count: i32, particle: Particle, ) { for player in self.players.load().iter() { player.spawn_particle(position, offset, max_speed, particle_count, particle); } } pub fn play_sound(&self, sound: Sound, category: SoundCategory, position: &Vector3) { self.play_sound_raw(sound as u16, category, position, 1.0, 1.0); } pub fn play_sound_event( &self, sound: &pumpkin_data::data_component_impl::IdOr< pumpkin_data::data_component_impl::SoundEvent, >, category: SoundCategory, position: &Vector3, ) { let seed = rng().random::(); let packet = CSoundEffect::new( data_to_proto_sound(sound), category, position, 1.0, 1.0, seed, ); self.broadcast_packet_all(&packet); } pub fn play_sound_fine( &self, sound: Sound, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, ) { self.play_sound_raw(sound as u16, category, position, volume, pitch); } pub fn play_sound_expect( &self, player: &Player, sound: Sound, category: SoundCategory, position: &Vector3, ) { self.play_sound_raw_expect(player, sound as u16, category, position, 1.0, 1.0); } pub fn play_sound_raw( &self, sound_id: u16, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, ) { let seed = rand::rng().random::(); let packet = CSoundEffect::new(IdOr::Id(sound_id), category, position, volume, pitch, seed); // Calculate the number of chunks the sound can be heard from based on its volume. let audible_chunks = f64::from(volume.max(1.0)).ceil() as i32; let chunk_pos = BlockPos::floored_v(*position).chunk_position(); let players = self.players.load(); let recipients = players.iter().filter(|p| { let center = p.get_entity().chunk_pos.load(); // If the sound reaches their chunk, send it! is_within_view_distance(chunk_pos, center, audible_chunks) }); let recipients_by_version = Self::collect_java_recipients_by_version(recipients); Self::broadcast_java_grouped(&packet, recipients_by_version); } pub fn play_sound_raw_expect( &self, player: &Player, sound_id: u16, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, ) { let seed = rand::rng().random::(); let packet = CSoundEffect::new(IdOr::Id(sound_id), category, position, volume, pitch, seed); let audible_chunks = f64::from(volume.max(1.0)).ceil() as i32; let chunk_pos = BlockPos::floored_v(*position).chunk_position(); let players = self.players.load(); let recipients = players.iter().filter(|p| { // Skip the expected player if p.gameprofile.id == player.gameprofile.id { return false; } let center = p.get_entity().chunk_pos.load(); is_within_view_distance(chunk_pos, center, audible_chunks) }); let recipients_by_version = Self::collect_java_recipients_by_version(recipients); Self::broadcast_java_grouped(&packet, recipients_by_version); } pub fn play_block_sound(&self, sound: Sound, category: SoundCategory, position: BlockPos) { let new_vec = Vector3::new( f64::from(position.0.x) + 0.5, f64::from(position.0.y) + 0.5, f64::from(position.0.z) + 0.5, ); self.play_sound(sound, category, &new_vec); } pub fn play_block_sound_expect( &self, player: &Player, sound: Sound, category: SoundCategory, position: BlockPos, ) { let new_vec = Vector3::new( f64::from(position.0.x) + 0.5, f64::from(position.0.y) + 0.5, f64::from(position.0.z) + 0.5, ); self.play_sound_expect(player, sound, category, &new_vec); } #[expect(clippy::too_many_lines)] pub async fn tick(self: &Arc, server: Arc) { let start = tokio::time::Instant::now(); self.flush_block_updates().await; self.flush_synced_block_events().await; self.update_active_chunks(); self.tick_environment().await; 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 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 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; }); } while let Some(res) = tasks.join_next().await { if let Err(e) = res { error!("Player tick panicked: {:?}", e); } } t.elapsed() }; let entities_to_tick = self.entities.load(); let entity_count = entities_to_tick.len(); let server_for_entities = server.clone(); let active_chunks = self.active_chunks.load(); 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() { // Only tick entities that sit in an active (ticking) chunk — the // same set block-entity ticking and mob spawning already use, and // like vanilla, which ticks entities only within the simulation // distance. Use the live position: fast movers such as minecarts // and projectiles write `pos` directly and leave the cached // chunk_pos stale. let entity_pos = entity.get_entity().pos.load(); let entity_chunk = Vector2::new( get_section_cord(entity_pos.x.floor() as i32), get_section_cord(entity_pos.z.floor() as i32), ); if !active_chunks.contains(&entity_chunk) { continue; } 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 active_chunks = self.active_chunks.load(); let mut block_entities: Vec> = Vec::new(); for chunk_pos in active_chunks.iter() { if let Some(chunk_block_entities) = self.block_entities.get(chunk_pos) { block_entities.extend(chunk_block_entities.values().cloned()); } } let block_entity_count = block_entities.len(); 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; }); } 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(); if let Some(ref fight_mutex) = self.dragon_fight { dragon_fight::DragonFight::tick(fight_mutex, self).await; } let total_elapsed = start.elapsed(); if total_elapsed.as_millis() > 50 { debug!( "Slow Tick [{}ms]: Chunks: {:?} | Players({}): {:?} | Entities({}): {:?} | Block Entities({}): {:?}", total_elapsed.as_millis(), chunk_elapsed, player_count, player_elapsed, entity_count, entity_elapsed, block_entity_count, block_entity_elapsed, ); } } pub async fn register_block_change(&self, position: BlockPos, block_state_id: BlockStateId) { self.unsent_block_changes .lock() .await .insert(position, block_state_id); } /// Queues block state changes for broadcast to nearby players. /// /// Call [`flush_block_updates`](Self::flush_block_updates) afterward to send the packets. pub async fn queue_block_updates(&self, changes: &[(BlockPos, BlockStateId)]) { let mut guard = self.unsent_block_changes.lock().await; for (pos, state_id) in changes { guard.insert(*pos, *state_id); } } pub async fn flush_block_updates(&self) { let mut block_state_updates_by_chunk_section: HashMap< Vector3, Vec<(BlockPos, BlockStateId)>, > = HashMap::new(); let changes = { let mut guard = self.unsent_block_changes.lock().await; std::mem::take(&mut *guard) }; for (position, block_state_id) in changes { let chunk_section = chunk_section_from_pos(&position); block_state_updates_by_chunk_section .entry(chunk_section) .or_default() .push((position, block_state_id)); } // TODO: only send packet to players who have the chunks loaded // TODO: Send light updates to update the wire directly next to a broken block for (chunk_section, updates) in block_state_updates_by_chunk_section { if updates.is_empty() { continue; } let chunk_pos = Vector2::new(chunk_section.x, chunk_section.z); if updates.len() == 1 { let (block_pos, block_state_id) = updates[0]; let be_block_id = BlockState::to_be_network_id(block_state_id); self.broadcast_to_chunk_editioned_sync( chunk_pos, &CBlockUpdate::new(block_pos, i32::from(block_state_id.as_u16()).into()), &pumpkin_protocol::bedrock::client::CUpdateBlock::new( block_pos, be_block_id as u32, ), ); } else { let players = self.players.load(); let mut java_recipients = Vec::new(); let recipients = players.iter().filter(|p| { let center = p.get_entity().chunk_pos.load(); let view_distance = get_view_distance(p).get() as i32; is_within_view_distance(chunk_pos, center, view_distance) }); for p in recipients { match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Bedrock(be_client) => { for (block_pos, block_state_id) in &updates { let be_block_id = BlockState::to_be_network_id(*block_state_id); be_client.try_enqueue_packet( &pumpkin_protocol::bedrock::client::CUpdateBlock::new( *block_pos, be_block_id as u32, ), ); } } } } let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped( &CMultiBlockUpdate::new(&updates), recipients_by_version, ); } } } async fn tick_environment(&self) { let (world_age, is_night, time_of_day) = { let mut level_time = self.level_time.lock().await; let (advance_time, advance_weather) = { let lock = self.level_info.load(); ( lock.game_rules.advance_time, lock.game_rules.advance_weather, ) }; level_time.tick_time(advance_time, advance_weather); // Auto-save logic if level_time.world_age % 100 == 0 { self.level.should_unload.store(true, Relaxed); let cleaned_chunks = self.level.clean_memory(); if !cleaned_chunks.is_empty() { self.remove_entities_in_chunks(&cleaned_chunks).await; self.level.clean_entity_chunks(&cleaned_chunks); } // If autosave is configured and this tick will trigger an autosave, don't double notify if self.level.autosave_ticks == 0 { self.level.level_channel.notify(); } else { let autosave = self.level.autosave_ticks as i64; if autosave == 0 || level_time.world_age % autosave != 0 { self.level.level_channel.notify(); } } } if self.level.autosave_ticks > 0 && self.level.save_enabled.load(Relaxed) { let autosave = self.level.autosave_ticks as i64; if autosave > 0 && level_time.world_age % autosave == 0 { self.level.should_save.store(true, Relaxed); self.level.level_channel.notify(); } } ( level_time.world_age, level_time.is_night(), level_time.time_of_day, ) }; let mut weather = self.weather.lock().await; weather.tick_weather(self); if self.should_skip_night() && is_night { let mut level_time = self.level_time.lock().await; let time = time_of_day + 24000; level_time.set_time(time - time % 24000); level_time.send_time(self).await; drop(level_time); for player in self.players.load().iter() { player.wake_up().await; } if weather.weather_cycle_enabled && (weather.raining || weather.thundering) { weather.reset_weather_cycle(self); } } else if world_age % 20 == 0 { let level_time = self.level_time.lock().await; level_time.send_time(self).await; } } #[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 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; } }); } // 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; } }); } // 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(); ( lock.game_rules.spawn_mobs, lock.game_rules.spawn_monsters, lock.difficulty == Difficulty::Peaceful, ) }; 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 = Arc::new(natural_spawner::get_filtered_spawning_categories( &spawn_state, spawn_mobs, spawn_enemies, spawn_passives, )); // 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; }); } } while let Some(res) = chunk_tasks.join_next().await { if let Err(e) = res { error!("Chunk task panicked: {:?}", e); } } // Update chunk inhabited time for active chunks let loaded_chunks = self.level.loaded_chunks.clone(); for pos in active_chunks.iter() { if let Some(chunk) = loaded_chunks.get(pos) { chunk.inhabited_time.fetch_add(1, Relaxed); } } } pub fn get_fluid_collisions(self: &Arc, bounding_box: BoundingBox) -> Vec<&Fluid> { let mut collisions = Vec::new(); let min = bounding_box.min_block_pos(); let max = bounding_box.max_block_pos(); for x in min.0.x..=max.0.x { for y in min.0.y..=max.0.y { for z in min.0.z..=max.0.z { let pos = BlockPos::new(x, y, z); let (fluid, state) = self.get_fluid_and_fluid_state(&pos); if fluid.id != Fluid::EMPTY.id { let height = f64::from(state.height); if height >= bounding_box.min.y { collisions.push(fluid); } } } } } collisions } pub fn check_fluid_collision(self: &Arc, bounding_box: BoundingBox) -> bool { let min = bounding_box.min_block_pos(); let max = bounding_box.max_block_pos(); for x in min.0.x..=max.0.x { for y in min.0.y..=max.0.y { for z in min.0.z..=max.0.z { let pos = BlockPos::new(x, y, z); let (fluid, state) = self.get_fluid_and_fluid_state(&pos); if fluid.id != Fluid::EMPTY.id { let height = f64::from(state.height); if height >= bounding_box.min.y { return true; } } } } } false } // FlowingFluid.getFlow() pub fn get_fluid_velocity( &self, pos0: BlockPos, fluid0: &Fluid, state0: &FluidState, ) -> Vector3 { let mut velo = Vector3::default(); for dir in BlockDirection::horizontal() { let offset = dir.to_offset(); let pos = pos0.offset(offset); let (neighbor_fluid, neighbor_state) = self.get_fluid_and_fluid_state(&pos); if neighbor_fluid.matches_type(fluid0) { let mut neighbor_height = neighbor_state.height; let mut amplitude = 0.0; if neighbor_height == 0.0 { let state_id = self.get_block_state_id(&pos); let block_id = state_id.to_block_id(); let block_state = state_id.to_state(); let blocks_movement = blocks_movement(block_state, block_id); if !blocks_movement { let down_pos = pos.down(); let (down_fluid, down_state) = self.get_fluid_and_fluid_state(&down_pos); if down_fluid.matches_type(fluid0) { neighbor_height = down_state.height; if neighbor_height > 0.0 { amplitude = f64::from(state0.height) - (f64::from(neighbor_height) - 0.888_888_9); } } } } else if neighbor_height > 0.0 { amplitude = f64::from(state0.height) - f64::from(neighbor_height); } if amplitude != 0.0 { velo.x += f64::from(offset.x) * amplitude; velo.z += f64::from(offset.z) * amplitude; } } } if state0.falling { for dir in BlockDirection::horizontal() { let pos = pos0.offset(dir.to_offset()); if self.is_solid_face(fluid0.id, pos, dir.to_block_direction()) || self.is_solid_face(fluid0.id, pos.up(), dir.to_block_direction()) { if velo.length_squared() != 0.0 { velo = velo.normalize(); } velo.y -= 6.0; break; } } } if velo.length_squared() == 0.0 { velo } else { velo.normalize() } } // FlowingFluid.isSolidFace() fn is_solid_face(&self, fluid0_id: u16, pos: BlockPos, direction: BlockDirection) -> bool { let id = self.get_block_state_id(&pos); let fluid = Fluid::from_state_id(id).unwrap_or(&Fluid::EMPTY); if Fluid::same_fluid_type(fluid.id, fluid0_id) { return false; } if direction == BlockDirection::Up { return true; } let block = Block::from_state_id(id); let state = BlockState::from_id(id); // Doesn't count blue ice or packed ice if block == &Block::ICE || block == &Block::FROSTED_ICE { return false; } state.is_side_solid(direction) } pub fn check_outline( bounding_box: &BoundingBox, pos: BlockPos, state: &BlockState, use_outline_shape: bool, mut using_outline_shape: F, ) -> bool where F: FnMut(&BoundingBox), { if state.outline_shapes.is_empty() { // Apparently we need this for air and moving pistons return true; } let mut inside = false; 'shapes: for shape in state.get_block_outline_shapes() { let outline_shape = shape.at_pos(pos); if outline_shape.intersects(bounding_box) { inside = true; if !use_outline_shape { break 'shapes; } using_outline_shape(&outline_shape); } } inside } pub fn check_collision( bounding_box: &BoundingBox, pos: BlockPos, state: &BlockState, use_collision_shape: bool, mut on_collision: F, ) -> bool where F: FnMut(&BoundingBox), { if state.is_air() || !state.is_solid() { return false; } let mut shapes = state .get_block_collision_shapes() .map(|shape| shape.at_pos(pos)); if use_collision_shape { let mut collided = false; for collision_shape in shapes { if collision_shape.intersects(bounding_box) { collided = true; // Convert to BB and trigger the callback on_collision(&collision_shape); } } collided } else { shapes.any(|s| s.intersects(bounding_box)) } } // For adjusting movement pub async fn get_block_collisions( self: &Arc, bounding_box: BoundingBox, entity: &dyn EntityBase, ) -> (Vec, Vec<(usize, BlockPos)>) { let mut collisions = Vec::new(); let mut positions = Vec::new(); let min = BlockPos::floored_v(bounding_box.min.add_raw(0.0, -0.50001, 0.0)); let max = bounding_box.max_block_pos(); let pos_iter = BlockPos::iterate(min, max); for pos in pos_iter { let state = self.get_block_state(&pos); if state.is_air() { continue; } let block = Block::from_state_id(state.id); let mut collided = false; if block == &Block::POWDER_SNOW { if let Some(shape) = crate::block::blocks::powder_snow::collision_shape_for_entity(entity, &pos) .await { let shape = shape.at_pos(pos); if shape.intersects(&bounding_box) { collided = true; collisions.push(shape); } } } else { for shape in state.get_block_collision_shapes() { let shape = shape.at_pos(pos); if shape.intersects(&bounding_box) { collided = true; collisions.push(shape); } } } if collided { positions.push((collisions.len(), pos)); } } (collisions, positions) } pub fn is_space_empty(&self, bounding_box: BoundingBox) -> bool { let min = bounding_box.min_block_pos(); let max = bounding_box.max_block_pos(); for pos in BlockPos::iterate(min, max) { let state = self.get_block_state(&pos); let collided = Self::check_collision(&bounding_box, pos, state, false, |_| ()); if collided { return false; } } true } /// Vanilla's `BlockView.getDismountHeight()`. /// Returns the Y surface height for dismounting at the given block position, /// or `f64::NEG_INFINITY` if no valid surface exists. pub fn get_dismount_height(&self, pos: &BlockPos) -> f64 { let state = self.get_block_state(pos); let max_y = state .get_block_collision_shapes() .map(|s| s.max.y) .fold(f64::NEG_INFINITY, f64::max); if max_y != f64::NEG_INFINITY { return max_y; } // No collision at pos — check block below let below = BlockPos(Vector3::new(pos.0.x, pos.0.y - 1, pos.0.z)); let below_state = self.get_block_state(&below); let below_max_y = below_state .get_block_collision_shapes() .map(|s| s.max.y) .fold(f64::NEG_INFINITY, f64::max); if below_max_y >= 1.0 { below_max_y - 1.0 } else { f64::NEG_INFINITY } } pub async fn tick_spawning_chunk( self: &Arc, chunk_pos: Vector2, chunk: &Arc, spawn_list: &Vec<&'static MobCategory>, spawn_state: &Arc, ) { // this.level.tickThunder(chunk); //TODO check in simulation distance let (is_raining, is_thundering) = { let weather = self.weather.lock().await; (weather.raining, weather.thundering) }; if is_raining && is_thundering && rng().random_range(0..100_000) == 0 { let rand_value = rng().random::() >> 2; let delta = Vector3::new(rand_value & 15, rand_value >> 16 & 15, rand_value >> 8 & 15); let random_pos = Vector3::new( chunk_pos.x << 4, chunk.heightmap.lock().unwrap().get( MotionBlocking, chunk_pos.x << 4, chunk_pos.y << 4, self.min_y, ), chunk_pos.y << 4, ) .add(&delta); // TODO this.getBrightness(LightLayer.SKY, blockPos) >= 15; // TODO heightmap // TODO findLightningRod(blockPos) // TODO encapsulatingFullBlocks if true { // TODO biome.getPrecipitationAt(pos, this.getSeaLevel()) == Biome.Precipitation.RAIN // TODO this.getCurrentDifficultyAt(blockPos); if rng().random::() < 0.0675 && self.get_block(&random_pos.to_block_pos().down()) != &Block::LIGHTNING_ROD { let entity = Entity::new( self.clone(), random_pos.to_f64(), &EntityType::SKELETON_HORSE, ); self.spawn_entity(Arc::new(entity)).await; } let entity = Entity::new( self.clone(), random_pos.to_f64().add_raw(0.5, 0., 0.5), &EntityType::LIGHTNING_BOLT, ); self.spawn_entity(Arc::new(entity)).await; } } if spawn_list.is_empty() { return; } // TODO this.level.canSpawnEntitiesInChunk(chunkPos) let entities = spawn_for_chunk( self, chunk_pos, chunk, spawn_state, spawn_list, is_thundering, ); for entity in entities { self.spawn_entity(entity).await; } } pub async fn get_world_age(&self) -> i64 { self.level_time.lock().await.world_age } pub async fn get_time_of_day(&self) -> i64 { self.level_time.lock().await.time_of_day } pub async fn set_time_of_day(&self, time: i64) { let mut level_time = self.level_time.lock().await; level_time.set_time(time); level_time.send_time(self).await; } pub async fn is_raining(&self) -> bool { self.weather.lock().await.raining } pub async fn is_raining_at(&self, pos: &BlockPos) -> bool { if !self.is_raining().await { return false; } if self.get_heightmap_height(MotionBlocking, pos.0.x, pos.0.z) + 1 > pos.0.y { return false; } self.can_see_sky(pos) && self .get_biome(pos) .weather .is_rain_at(pos.0.x, pos.0.y, pos.0.z, self.sea_level) } pub async fn set_raining(&self, raining: bool) { if let Some(server) = self.server.upgrade() { let world_arc = server.get_world_from_dimension(&self.dimension); let mut event = crate::plugin::api::events::world::weather_change::WeatherChangeEvent::new( world_arc, raining, ); server.plugin_manager.fire(&server, &mut event).await; if event.cancelled { return; } } let mut weather = self.weather.lock().await; if weather.raining != raining { let thunder = weather.thundering; weather.set_weather_parameters(self, 0, 0, raining, thunder); } } pub async fn is_thundering(&self) -> bool { self.weather.lock().await.thundering } pub async fn set_thundering(&self, thundering: bool) { if let Some(server) = self.server.upgrade() { let world_arc = server.get_world_from_dimension(&self.dimension); let mut event = crate::plugin::api::events::world::weather_change::ThunderChangeEvent::new( world_arc, thundering, ); server.plugin_manager.fire(&server, &mut event).await; if event.cancelled { return; } } let mut weather = self.weather.lock().await; if weather.thundering != thundering { let raining = weather.raining; weather.set_weather_parameters(self, 0, 0, raining, thundering); } } /// Gets the y position of the first non air block from the top down pub fn get_top_block(&self, position: Vector2) -> i32 { let chunk_pos = Vector2::new(position.x >> 4, position.y >> 4); let relative_x = (position.x & 15) as usize; let relative_z = (position.y & 15) as usize; self.level .read_chunk_sync(&chunk_pos, |chunk| { let height = chunk.heightmap.lock().unwrap().get( ChunkHeightmapType::WorldSurface, position.x, position.y, self.dimension.min_y, ); if height >= self.dimension.min_y { return height; } for y in (self.dimension.min_y..self.dimension.min_y + self.dimension.height).rev() { if let Some(block_id) = chunk .section .get_block_absolute_y(relative_x, y, relative_z) && !is_air(block_id) { return y; } } self.dimension.min_y }) .unwrap_or(self.dimension.min_y) } pub fn get_heightmap_height(&self, height_map: ChunkHeightmapType, x: i32, z: i32) -> i32 { let chunk_pos = Vector2::new(x >> 4, z >> 4); self.level .read_chunk_sync(&chunk_pos, |chunk| { chunk .heightmap .lock() .unwrap() .get(height_map, x, z, self.min_y) }) .unwrap_or(self.min_y) } #[allow(clippy::too_many_lines)] pub async fn spawn_bedrock_player( &self, base_config: &BasicConfiguration, player: Arc, server: &Arc, ) { static CREATIVE_CONTENT: std::sync::OnceLock<(Vec, Vec)> = std::sync::OnceLock::new(); static BEDROCK_CRAFTING_DATA: std::sync::OnceLock< Vec, > = std::sync::OnceLock::new(); let level_info = server.level_info.load(); let weather = self.weather.lock().await; let runtime_id = player.entity_id() as u64; let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { let position = player.position(); let yaw = player.get_entity().yaw.load(); //info.spawn_angle; let pitch = player.get_entity().pitch.load(); (position, yaw, pitch) } else { let spawn_position = Vector2::new(level_info.spawn_x, level_info.spawn_z); let chunk_pos = Vector2::new(level_info.spawn_x >> 4, level_info.spawn_z >> 4); self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await; let pos_y = self.get_top_block(spawn_position) + 1; // +1 to spawn on top of the block let position = Vector3::new( f64::from(level_info.spawn_x) + 0.5, f64::from(pos_y), f64::from(level_info.spawn_z) + 0.5, ); (position, level_info.spawn_yaw, level_info.spawn_pitch) }; // Todo make the data less spread let level_settings = LevelSettings { seed: self.level.seed.0, spawn_biome_type: 0, custom_biome_name: String::new(), dimension: VarInt(0), generator_type: VarInt(1), world_gamemode: server.defaultgamemode.lock().await.gamemode, hardcore: base_config.hardcore, difficulty: VarInt(level_info.difficulty as i32), spawn_position: BlockPos::new( level_info.spawn_x, level_info.spawn_y, level_info.spawn_z, ), has_achievements_disabled: false, editor_world_type: VarInt(0), is_created_in_editor: false, is_exported_from_editor: false, day_cycle_stop_time: VarInt(-1), education_edition_offer: VarInt(0), has_education_features_enabled: false, education_product_id: String::new(), rain_level: weather.rain_level, lightning_level: weather.thunder_level, has_confirmed_platform_locked_content: false, was_multiplayer_intended: true, was_lan_broadcasting_intended: true, xbox_live_broadcast_setting: GamePublishSetting::Public, platform_broadcast_setting: GamePublishSetting::Public, commands_enabled: level_info.allow_commands, is_texture_packs_required: false, rule_data: GameRules { list_size: VarUInt(0), }, experiments: Experiments { names_size: 0, experiments_ever_toggled: false, }, bonus_chest: false, has_start_with_map_enabled: false, // TODO Bedrock permission level are different permission_level: VarInt(2), server_simulation_distance: server .advanced_config .networking .bedrock .simulation_distance .get() .into(), has_locked_behavior_pack: false, has_locked_resource_pack: false, is_from_locked_world_template: false, is_using_msa_gamertags_only: false, is_from_world_template: false, is_world_template_option_locked: false, is_only_spawning_v1_villagers: false, is_disabling_personas: false, is_disabling_custom_skins: false, emote_chat_muted: false, game_version: CURRENT_BEDROCK_MC_VERSION.into(), limited_world_width: 0, limited_world_height: 0, new_nether: true, edu_shared_uri_button_name: String::new(), edu_shared_uri_link_uri: String::new(), override_force_experimental_gameplay_has_value: false, chat_restriction_level: 0, disable_player_interactions: false, server_editor_connection_policy: VarInt(0), allow_anonymous_block_drops_in_editor_worlds: false, }; drop(level_info); drop(weather); let Some(client) = player.client.bedrock() else { return; }; client .send_game_packet(&CStartGame { entity_id: VarLong(runtime_id as _), runtime_entity_id: VarULong(runtime_id), player_gamemode: player.gamemode.load(), position: Vector3::new(position.x as f32, position.y as f32, position.z as f32), pitch, yaw, level_settings, level_id: String::new(), level_name: "Pumpkin world".to_string(), premium_world_template_id: String::new(), is_trial: false, rewind_history_size: VarInt(0), server_authoritative_block_breaking: true, current_level_time: self.level_time.lock().await.world_age as _, enchantment_seed: VarInt(0), block_properties_size: VarUInt(0), // TODO Make this unique multiplayer_correlation_id: Uuid::default().to_string(), enable_itemstack_net_manager: true, server_version: "Pumpkin Rust Server".to_string(), compound_id: 10, compound_len: VarUInt(0), compound_end: 0, block_registry_checksum: 0, world_template_id: Uuid::nil(), enable_clientside_generation: false, blocknetwork_ids_are_hashed: false, server_auth_sounds: true, is_logging_chat: false, server_join_information: None, telemetry: ServerTelemetryData { server_id: String::new(), scenario_id: String::new(), world_id: String::new(), owner_id: String::new(), }, }) .await; client .send_game_packet(&CItemRegistry { items: BedrockItem::ALL_BEDROCK_ITEMS .iter() .map(|b| ItemDefinition { name: b.registry_key.into(), id: b.id, component_based: b.component_based, item_version: VarInt::from(match b.version { BedrockItemVersion::Legacy => 0, BedrockItemVersion::DataDriven => 1, BedrockItemVersion::None => 2, }), component_data: b.definition_components.into(), }) .collect::>(), }) .await; let (groups, entries) = CREATIVE_CONTENT.get_or_init(|| { let groups = pumpkin_data::bedrock_creative::CREATIVE_GROUPS .iter() .map(|g| { let creative_category = match g.category { 1 => CreativeCategory::Construction, 2 => CreativeCategory::Nature, 3 => CreativeCategory::Equipment, 4 => CreativeCategory::Items, 5 => CreativeCategory::CommandOnly, _ => CreativeCategory::Undefined, }; let icon_item = if g.icon_item_id != 0 { NetworkItemDescriptor { id: VarInt::from(g.icon_item_id), stack_size: 1, aux_value: VarUInt(g.icon_item_aux_value), block_runtime_id: VarInt(0), nbt_data: pumpkin_nbt::Nbt::default(), place_on_blocks: Vec::new(), destroy_blocks: Vec::new(), shield_blocking_tick: 0, } } else { NetworkItemDescriptor::default() }; Group { creative_category, name: g.name.to_string(), icon_item, } }) .collect::>(); let entries = pumpkin_data::bedrock_creative::CREATIVE_ENTRIES .iter() .enumerate() .map(|(i, e)| Entry { id: VarUInt((i + 1) as u32), item: NetworkItemDescriptor { id: VarInt::from(e.item_id), stack_size: 1, aux_value: VarUInt(e.item_aux_value), block_runtime_id: VarInt(0), nbt_data: pumpkin_nbt::Nbt::default(), place_on_blocks: Vec::new(), destroy_blocks: Vec::new(), shield_blocking_tick: 0, }, group_index: VarUInt(e.group_index), }) .collect::>(); (groups, entries) }); client .send_game_packet(&CCreativeContent { groups, entries }) .await; let bedrock_recipes = BEDROCK_CRAFTING_DATA.get_or_init(|| { use pumpkin_data::item::{Item, JavaToBedrockItemMapping}; use pumpkin_data::recipes::{CraftingRecipeTypes, RecipeIngredientTypes}; use pumpkin_protocol::bedrock::client::{ BedrockRecipe, BedrockShapedRecipe, BedrockShapelessRecipe, ItemDescriptorCount, RecipeUnlockRequirement, }; use pumpkin_protocol::bedrock::network_item::NetworkItemDescriptor; use pumpkin_protocol::codec::{var_int::VarInt, var_uint::VarUInt}; let mut mapped_recipes = Vec::new(); let mut network_id_counter = 1u32; for recipe in pumpkin_data::recipes::RECIPES_CRAFTING { let map_ingredient = |ing: &RecipeIngredientTypes| -> ItemDescriptorCount { let item_key = match ing { RecipeIngredientTypes::Simple(name) => Some(*name), RecipeIngredientTypes::Tagged(tag) => { let tag_name = tag.strip_prefix('#').unwrap_or(tag); pumpkin_data::tag::get_tag_ids( pumpkin_data::tag::RegistryKey::Item, tag_name, ) .and_then(|ids| { ids.first().and_then(|&first_id| { Item::from_id(first_id).map(|item| item.registry_key) }) }) } RecipeIngredientTypes::OneOf(names) => names.first().copied(), }; if let Some(key) = item_key { let registry_key = key.strip_prefix("minecraft:").unwrap_or(key); if let Some(item) = Item::from_registry_key(registry_key) && let Some(mapping) = JavaToBedrockItemMapping::from_java_item_id(item.id) { return ItemDescriptorCount { network_id: mapping.bedrock_item.id, metadata_value: mapping.bedrock_data as i16, count: 1, }; } } ItemDescriptorCount { network_id: 0, metadata_value: 0, count: 0, } }; match recipe { CraftingRecipeTypes::CraftingShaped { category: _, group: _, show_notification: _, key, pattern, result, } => { let height = pattern.len() as i32; let width = pattern.iter().map(|s| s.len()).max().unwrap_or(0) as i32; let mut input = Vec::new(); for r in 0..height { let pattern_row = pattern[r as usize]; for c in 0..width { let ch = pattern_row.chars().nth(c as usize).unwrap_or(' '); if ch == ' ' { input.push(ItemDescriptorCount { network_id: 0, metadata_value: 0, count: 0, }); } else { let mut ingredient = None; for &(key_ch, ref ing) in *key { if key_ch == ch { ingredient = Some(ing); break; } } if let Some(ing) = ingredient { input.push(map_ingredient(ing)); } else { input.push(ItemDescriptorCount { network_id: 0, metadata_value: 0, count: 0, }); } } } } let output_item = Item::from_registry_key(result.id); if let Some(item) = output_item && let Some(mapping) = JavaToBedrockItemMapping::from_java_item_id(item.id) { let output_descriptor = NetworkItemDescriptor { id: VarInt::from(mapping.bedrock_item.id), stack_size: result.count as u16, aux_value: VarUInt(mapping.bedrock_data), block_runtime_id: VarInt::from(mapping.bedrock_block_state), nbt_data: pumpkin_nbt::Nbt::default(), place_on_blocks: Vec::new(), destroy_blocks: Vec::new(), shield_blocking_tick: 0, }; mapped_recipes.push(BedrockRecipe::Shaped(BedrockShapedRecipe { recipe_id: format!("pumpkin:recipe_{network_id_counter}"), width: VarInt(width), height: VarInt(height), input, output: vec![output_descriptor], uuid: [0; 16], block: "crafting_table".to_string(), priority: VarInt(1), assume_symmetry: true, unlock_requirement: RecipeUnlockRequirement { context: 1 }, recipe_network_id: VarUInt(network_id_counter), })); network_id_counter += 1; } } CraftingRecipeTypes::CraftingShapeless { category: _, group: _, ingredients, result, } => { let input = ingredients.iter().map(map_ingredient).collect::>(); let output_item = Item::from_registry_key(result.id); if let Some(item) = output_item && let Some(mapping) = JavaToBedrockItemMapping::from_java_item_id(item.id) { let output_descriptor = NetworkItemDescriptor { id: VarInt::from(mapping.bedrock_item.id), stack_size: result.count as u16, aux_value: VarUInt(mapping.bedrock_data), block_runtime_id: VarInt::from(mapping.bedrock_block_state), nbt_data: pumpkin_nbt::Nbt::default(), place_on_blocks: Vec::new(), destroy_blocks: Vec::new(), shield_blocking_tick: 0, }; mapped_recipes.push(BedrockRecipe::Shapeless(BedrockShapelessRecipe { recipe_id: format!("pumpkin:recipe_{network_id_counter}"), input, output: vec![output_descriptor], uuid: [0; 16], block: "crafting_table".to_string(), priority: VarInt(1), unlock_requirement: RecipeUnlockRequirement { context: 1 }, recipe_network_id: VarUInt(network_id_counter), })); network_id_counter += 1; } } _ => {} } } mapped_recipes }); client .send_game_packet(&pumpkin_protocol::bedrock::client::CCraftingData { recipes: bedrock_recipes.clone(), clean_recipes: false, }) .await; client .send_game_packet(&CInventoryContent { container_id: VarUInt(0), // player inventory, slots: futures::future::join_all(player.inventory.main_inventory.iter().map( async |s| { let stack = s.lock().await; NetworkItemStackDescriptor::from(&*stack) }, )) .await, full_container_name: FullContainerName { container_name: ContainerName::Inventory, dynamic_id: None, }, storage_item: NetworkItemStackDescriptor::default(), }) .await; { let mut abilities = player.abilities.lock().await; abilities.set_for_gamemode(player.gamemode.load()); }; let entity = &player.get_entity(); let metadata = entity.bedrock_metadata(); let actor_data = CSetActorData { actor_runtime_id: VarULong(runtime_id), metadata, synced_properties: PropertySyncData { int_properties: HashMap::new(), float_properties: HashMap::new(), }, tick: VarULong(0), }; client.send_game_packet(&actor_data).await; player.send_abilities_update().await; { let command_dispatcher = server.command_dispatcher.read().await; client_suggestions::send_bedrock_commands_packet(&player, server, &command_dispatcher) .await; }; client .enqueue_packet_internal(&CUpdateAttributes { runtime_id: VarULong(runtime_id), attributes: vec![ Attribute { min_value: 0.0, max_value: 3.402_823_5E38, current_value: 0.1, default_min_value: 0.0, default_max_value: 3.402_823_5E38, default_value: 0.1, name: "minecraft:movement".to_string(), modifiers_list_size: VarUInt(0), }, Attribute { min_value: 0.0, max_value: 3.402_823_5E38, current_value: 0.02, default_min_value: 0.0, default_max_value: 3.402_823_5E38, default_value: 0.02, name: "minecraft:underwater_movement".to_string(), modifiers_list_size: VarUInt(0), }, Attribute { min_value: 0.0, max_value: 1.0, current_value: 0.08, default_min_value: 0.0, default_max_value: 1.0, default_value: 0.08, name: "minecraft:gravity".to_string(), modifiers_list_size: VarUInt(0), }, Attribute { min_value: 0.0, max_value: 400.0, current_value: 400.0, default_min_value: 0.0, default_max_value: 400.0, default_value: 400.0, name: "minecraft:air".to_string(), modifiers_list_size: VarUInt(0), }, Attribute { min_value: 0.0, max_value: 20.0, current_value: 20.0, default_min_value: 0.0, default_max_value: 20.0, default_value: 20.0, name: "minecraft:health".to_string(), modifiers_list_size: VarUInt(0), }, Attribute { min_value: 0.0, max_value: 20.0, current_value: 20.0, default_min_value: 0.0, default_max_value: 20.0, default_value: 20.0, name: "minecraft:player.hunger".to_string(), modifiers_list_size: VarUInt(0), }, ], player_tick: VarULong(0), }) .await; // --- MULTIPLAYER BROADCASTING --- let gameprofile = &player.gameprofile; let velocity = player.get_entity().velocity.load(); // 1. Broadcast the new Bedrock player to everyone else (Java + Bedrock) let bedrock_player_list = CPlayerList { action: CPlayerList::ACTION_ADD, entries: vec![PlayerListEntry { uuid: gameprofile.id, entity_unique_id: VarLong(runtime_id as i64), username: gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), build_platform: 0, skin: (**player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, is_sub_client: false, player_color: [0, 0, 0, 0], }], }; let gamemode = player.gamemode.load(); self.broadcast_packet_except_editioned_sync( &[gameprofile.id], &CPlayerInfoUpdate::new( (PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_GAME_MODE | PlayerInfoFlags::UPDATE_LISTED | PlayerInfoFlags::UPDATE_LATENCY | PlayerInfoFlags::UPDATE_LIST_PRIORITY | PlayerInfoFlags::UPDATE_HAT) .bits(), &[pumpkin_protocol::java::client::play::Player { uuid: gameprofile.id, actions: &[ PlayerAction::AddPlayer { name: &gameprofile.name, properties: &gameprofile.properties.load(), }, PlayerAction::UpdateGameMode(VarInt(gamemode as i32)), PlayerAction::UpdateListed(true), PlayerAction::UpdateLatency(VarInt(0)), PlayerAction::UpdateListOrder(VarInt(0)), PlayerAction::UpdateHat(true), ], }], ), &bedrock_player_list, ); let bedrock_add_player = CAddPlayer { uuid: gameprofile.id, username: gameprofile.name.clone(), entity_runtime_id: VarULong(runtime_id), platform_chat_id: String::new(), position: Vector3::new(position.x as f32, position.y as f32, position.z as f32), velocity: Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32), pitch, yaw, head_yaw: yaw, held_item: NetworkItemDescriptor::default(), game_mode: VarInt(match player.gamemode.load() { GameMode::Survival => 0, GameMode::Creative => 1, GameMode::Adventure => 2, GameMode::Spectator => 6, }), metadata: entity.bedrock_metadata(), properties: EntityProperties::default(), ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { entity_unique_id: runtime_id as i64, player_permissions: 0, command_permissions: 0, layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { serialized_layer: 0, abilities_set: 0, ability_value: 0, fly_speed: 0.05, vertical_fly_speed: 0.05, walk_speed: 0.1, }], }, links: Vec::new(), device_id: String::new(), build_platform: 0, }; self.broadcast_packet_except_editioned_sync( &[gameprofile.id], &CSpawnEntity::new( (runtime_id as i32).into(), gameprofile.id, i32::from(EntityType::PLAYER.id).into(), position, pitch, yaw, yaw, 0.into(), velocity, ), &bedrock_add_player, ); // Broadcast metadata to Java players so they can correctly interact with the new player let config = player.config.load(); let mut java_meta_buf = Vec::new(); { let meta = Metadata::new( TrackedData::PLAYER_MODE_CUSTOMISATION, MetaDataType::BYTE, config.skin_parts, ); meta.write(&mut java_meta_buf, &JavaMinecraftVersion::V_1_21_4) .unwrap(); }; { let meta = Metadata::new( TrackedData::PLAYER_MODE_CUSTOMIZATION_ID, MetaDataType::BYTE, config.skin_parts, ); meta.write(&mut java_meta_buf, &JavaMinecraftVersion::V_1_21_4) .unwrap(); }; java_meta_buf.put_u8(255); self.broadcast_packet_except_editioned_sync( &[gameprofile.id], &CSetEntityMetadata::new((runtime_id as i32).into(), java_meta_buf.into()), &actor_data, ); // 2. Spawn existing players for our new Bedrock client let players = self.players.load(); for existing_player in players .iter() .filter(|p| p.gameprofile.id != gameprofile.id) { let ex_profile = &existing_player.gameprofile; let ex_entity = &existing_player.get_entity(); let ex_pos = ex_entity.pos.load(); let ex_vel = ex_entity.velocity.load(); let ex_player_list = CPlayerList { action: CPlayerList::ACTION_ADD, entries: vec![PlayerListEntry { uuid: ex_profile.id, entity_unique_id: VarLong(existing_player.entity_id() as i64), username: ex_profile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), build_platform: 0, skin: (**existing_player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, is_sub_client: false, player_color: [0, 0, 0, 0], }], }; // Send PlayerList FIRST client.send_game_packet(&ex_player_list).await; let ex_add_player = CAddPlayer { uuid: ex_profile.id, username: ex_profile.name.clone(), entity_runtime_id: VarULong(existing_player.entity_id() as u64), platform_chat_id: String::new(), position: Vector3::new(ex_pos.x as f32, ex_pos.y as f32, ex_pos.z as f32), velocity: Vector3::new(ex_vel.x as f32, ex_vel.y as f32, ex_vel.z as f32), pitch: ex_entity.pitch.load(), yaw: ex_entity.yaw.load(), head_yaw: ex_entity.head_yaw.load(), held_item: NetworkItemDescriptor::default(), game_mode: VarInt(match existing_player.gamemode.load() { GameMode::Survival => 0, GameMode::Creative => 1, GameMode::Adventure => 2, GameMode::Spectator => 6, }), metadata: ex_entity.bedrock_metadata(), properties: EntityProperties::default(), ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { entity_unique_id: existing_player.entity_id() as i64, player_permissions: 0, command_permissions: 0, layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { serialized_layer: 0, abilities_set: 0, ability_value: 0, fly_speed: 0.05, vertical_fly_speed: 0.05, walk_speed: 0.1, }], }, links: Vec::new(), device_id: String::new(), build_platform: 0, }; client.send_game_packet(&ex_add_player).await; } // 3. Trigger Join Event and Broadcast Join Message let msg_comp = TextComponent::translate_cross( translation::java::MULTIPLAYER_PLAYER_JOINED, translation::bedrock::MULTIPLAYER_PLAYER_JOINED, [TextComponent::text(player.gameprofile.name.clone())], ) .color_named(NamedColor::Yellow); let mut event = PlayerJoinEvent::new(player.clone(), msg_comp); server.plugin_manager.fire(server, &mut event).await; if !event.cancelled { self.broadcast_system_message(&event.join_message, false) .await; info!("{}", event.join_message.to_pretty_console()); } } #[expect(clippy::too_many_lines)] pub async fn spawn_java_player( &self, base_config: &BasicConfiguration, player: &Arc, server: &Arc, ) { let dimensions: Vec = server .dimensions .iter() .map(|d| ResourceLocation::from(d.minecraft_name)) .collect(); // This code follows the vanilla packet order let entity_id = player.entity_id(); let gamemode = player.gamemode.load(); debug!( "spawning player {}, entity id {}", player.gameprofile.name, entity_id ); let Some(client) = player.client.java() else { return; }; // Send the login packet for our new player client .send_packet_now(&CLogin::new( entity_id, base_config.hardcore, &dimensions, server .advanced_config .networking .java .max_players .try_into() .unwrap(), server .advanced_config .networking .java .view_distance .get() .into(), // TODO: view distance server .advanced_config .networking .java .simulation_distance .get() .into(), // TODO: sim view dinstance false, true, false, PlayerSpawnData::new( self.dimension.clone(), biome::hash_seed(self.level.seed.0), // seed gamemode as u8, player .previous_gamemode .load() .map_or(-1, |gamemode| gamemode as i8), false, false, None, VarInt(player.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32), self.sea_level.into(), ), server.advanced_config.networking.java.online_mode, // This should stay true even when reports are disabled. // It prevents the annoying popup when joining the server. true, )) .await; // Send the current ticking state to the new player so they are in sync. server.tick_rate_manager.update_joining_player(player).await; // Permissions, i.e. the commands a player may use. player.send_permission_lvl_update(); // Difficulty of the world player.send_difficulty_update().await; { let command_dispatcher = server.command_dispatcher.read().await; client_suggestions::send_c_commands_packet(player, server, &command_dispatcher).await; }; let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { let position = player.position(); let yaw = player.get_entity().yaw.load(); //info.spawn_angle; let pitch = player.get_entity().pitch.load(); (position, yaw, pitch) } else { let info = &self.level_info.load(); let spawn_position = Vector2::new(info.spawn_x, info.spawn_z); let chunk_pos = Vector2::new(info.spawn_x >> 4, info.spawn_z >> 4); self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await; let pos_y = self.get_top_block(spawn_position) + 1; // +1 to spawn on top of the block let position = Vector3::new( f64::from(info.spawn_x) + 0.5, f64::from(pos_y), f64::from(info.spawn_z) + 0.5, ); (position, info.spawn_yaw, info.spawn_pitch) }; // Load chunks around the real spawn position before teleporting the client there. player.living_entity.entity.set_pos(position); player.living_entity.entity.set_rotation(yaw, pitch); player.living_entity.entity.last_pos.store(position); chunker::update_position(player).await; let center_chunk = player.living_entity.entity.chunk_pos.load(); let chunk = self .level .get_or_fetch_chunk(center_chunk, std::clone::Clone::clone) .await; if let Some(server) = self.server.upgrade() { let mut event = crate::plugin::world::chunk_send::ChunkSend::new(player.world(), chunk.clone()); server.plugin_manager.fire(&server, &mut event).await; if event.cancelled { return; } } client.send_packet_now(&CChunkBatchStart).await; client.send_packet_now(&CChunkData(&chunk)).await; client.send_packet_now(&CChunkBatchEnd::new(1u16)).await; let velocity = player.living_entity.entity.velocity.load(); debug!("Sending player teleport to {}", player.gameprofile.name); player.request_teleport(position, yaw, pitch).await; let gameprofile = &player.gameprofile; let bedrock_player_list = CPlayerList { action: CPlayerList::ACTION_ADD, entries: vec![PlayerListEntry { uuid: gameprofile.id, entity_unique_id: VarLong(entity_id as i64), username: gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), build_platform: 0, skin: (**player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, is_sub_client: false, player_color: [0, 0, 0, 0], }], }; let player_actions = [ PlayerAction::AddPlayer { name: &gameprofile.name, properties: &gameprofile.properties.load(), }, PlayerAction::UpdateGameMode(VarInt(gamemode as i32)), PlayerAction::UpdateListed(true), PlayerAction::UpdateLatency(VarInt(0)), PlayerAction::UpdateListOrder(VarInt(0)), PlayerAction::UpdateHat(true), ]; let java_player = [pumpkin_protocol::java::client::play::Player { uuid: gameprofile.id, actions: &player_actions, }]; let player_info_update = CPlayerInfoUpdate::new( (PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_GAME_MODE | PlayerInfoFlags::UPDATE_LISTED | PlayerInfoFlags::UPDATE_LATENCY | PlayerInfoFlags::UPDATE_LIST_PRIORITY | PlayerInfoFlags::UPDATE_HAT) .bits(), &java_player, ); self.broadcast_editioned(&player_info_update, &bedrock_player_list) .await; // If the player has a custom tab_list_name, send an update for it if let Some(tab_list_name) = player.get_tab_list_name().await { let actions = [PlayerAction::UpdateDisplayName(Some(&tab_list_name))]; let java_player = [pumpkin_protocol::java::client::play::Player { uuid: gameprofile.id, actions: &actions, }]; self.broadcast_packet_all(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(), &java_player, )); } // Here, we send all the infos of players who already joined. let mut players_tab_list_names = Vec::new(); { let players = self.players.load(); let mut data_to_process = Vec::new(); for p in players .iter() .filter(|p| p.gameprofile.id != player.gameprofile.id) { let props_guard = p.gameprofile.properties.load(); data_to_process.push((props_guard, p)); } let mut current_player_data = Vec::new(); for (properties, player) in &data_to_process { let chat_session = player.chat_session.lock().await; let tab_list_name = player.get_tab_list_name().await; let mut player_actions = vec![PlayerAction::AddPlayer { name: &player.gameprofile.name, properties, }]; if base_config.allow_chat_reports { player_actions.push(PlayerAction::InitializeChat(Some(InitChat { session_id: chat_session.session_id, expires_at: chat_session.expires_at, public_key: chat_session.public_key.clone(), signature: chat_session.signature.clone(), }))); } player_actions.extend([ PlayerAction::UpdateGameMode(VarInt(player.gamemode.load() as i32)), PlayerAction::UpdateListed(player.tab_list_listed.load(Ordering::Relaxed)), PlayerAction::UpdateLatency(VarInt( player.tab_list_latency.load(Ordering::Relaxed), )), PlayerAction::UpdateListOrder(VarInt( player.tab_list_order.load(Ordering::Relaxed), )), PlayerAction::UpdateHat(true), ]); drop(chat_session); current_player_data.push((&player.gameprofile.id, player_actions)); // Collect tab_list_names for sending later if tab_list_name.is_some() { players_tab_list_names.push((player.gameprofile.id, tab_list_name)); } } let mut action_flags = PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_LISTED | PlayerInfoFlags::UPDATE_LATENCY | PlayerInfoFlags::UPDATE_LIST_PRIORITY | PlayerInfoFlags::UPDATE_GAME_MODE | PlayerInfoFlags::UPDATE_HAT; if base_config.allow_chat_reports { action_flags |= PlayerInfoFlags::INITIALIZE_CHAT; } let entries = current_player_data .iter() .map(|(id, actions)| java::client::play::Player { uuid: **id, actions, }) .collect::>(); debug!("Sending player info to {}", player.gameprofile.name); client .enqueue_packet(&CPlayerInfoUpdate::new(action_flags.bits(), &entries)) .await; // Send tab_list_names for existing players with custom names for (player_id, tab_list_name) in &players_tab_list_names { if let Some(name) = tab_list_name { let actions = [PlayerAction::UpdateDisplayName(Some(name))]; let java_player = [pumpkin_protocol::java::client::play::Player { uuid: *player_id, actions: &actions, }]; client .enqueue_packet(&CPlayerInfoUpdate::new( PlayerInfoFlags::UPDATE_DISPLAY_NAME.bits(), &java_player, )) .await; } } }; let gameprofile = &player.gameprofile; let bedrock_add_player = CAddPlayer { uuid: gameprofile.id, username: gameprofile.name.clone(), entity_runtime_id: VarULong(entity_id as u64), platform_chat_id: String::new(), position: Vector3::new(position.x as f32, position.y as f32, position.z as f32), velocity: Vector3::new(velocity.x as f32, velocity.y as f32, velocity.z as f32), pitch, yaw, head_yaw: yaw, held_item: NetworkItemDescriptor::default(), game_mode: VarInt(match player.gamemode.load() { GameMode::Survival => 0, GameMode::Creative => 1, GameMode::Adventure => 2, GameMode::Spectator => 6, }), metadata: player.get_entity().bedrock_metadata(), properties: EntityProperties::default(), ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { entity_unique_id: entity_id as i64, player_permissions: 0, command_permissions: 0, layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { serialized_layer: 0, abilities_set: 0, ability_value: 0, fly_speed: 0.05, vertical_fly_speed: 0.05, walk_speed: 0.1, }], }, links: Vec::new(), device_id: String::new(), build_platform: 0, }; // Spawn the player for every client. let spawn_entity = CSpawnEntity::new( entity_id.into(), gameprofile.id, i32::from(EntityType::PLAYER.id).into(), position, pitch, yaw, yaw, 0.into(), velocity, ); self.broadcast_packet_except_editioned_sync( &[player.gameprofile.id], &spawn_entity, &bedrock_add_player, ); // Broadcast metadata to Java players so they can correctly interact with the new player let config = player.config.load(); let mut java_meta_buf = Vec::new(); { let meta = Metadata::new( TrackedData::PLAYER_MODE_CUSTOMISATION, MetaDataType::BYTE, config.skin_parts, ); meta.write(&mut java_meta_buf, &JavaMinecraftVersion::V_1_21_4) .unwrap(); }; { let meta = Metadata::new( TrackedData::PLAYER_MODE_CUSTOMIZATION_ID, MetaDataType::BYTE, config.skin_parts, ); meta.write(&mut java_meta_buf, &JavaMinecraftVersion::V_1_21_4) .unwrap(); }; java_meta_buf.put_u8(255); self.broadcast_packet_except_editioned_sync( &[gameprofile.id], &CSetEntityMetadata::new((entity_id).into(), java_meta_buf.into()), &CSetActorData { actor_runtime_id: VarULong(entity_id as u64), metadata: player.get_entity().bedrock_metadata(), synced_properties: PropertySyncData { int_properties: HashMap::new(), float_properties: HashMap::new(), }, tick: VarULong(0), }, ); // Spawn players for our client. let id = player.gameprofile.id; for existing_player in self .players .load() .iter() .filter(|c| c.gameprofile.id != id) { let entity = &existing_player.get_entity(); let pos = entity.pos.load(); let gameprofile = &existing_player.gameprofile; let bedrock_add_player = CAddPlayer { uuid: gameprofile.id, username: gameprofile.name.clone(), entity_runtime_id: VarULong(existing_player.entity_id() as u64), platform_chat_id: String::new(), position: Vector3::new(pos.x as f32, pos.y as f32, pos.z as f32), velocity: Vector3::new( entity.velocity.load().x as f32, entity.velocity.load().y as f32, entity.velocity.load().z as f32, ), pitch: entity.pitch.load(), yaw: entity.yaw.load(), head_yaw: entity.head_yaw.load(), held_item: NetworkItemDescriptor::default(), game_mode: VarInt(match existing_player.gamemode.load() { GameMode::Survival => 0, GameMode::Creative => 1, GameMode::Adventure => 2, GameMode::Spectator => 6, }), metadata: entity.bedrock_metadata(), properties: EntityProperties::default(), ability_data: pumpkin_protocol::bedrock::client::add_player::AbilityData { entity_unique_id: existing_player.entity_id() as i64, player_permissions: 0, command_permissions: 0, layers: vec![pumpkin_protocol::bedrock::client::AbilityLayer { serialized_layer: 0, abilities_set: 0, ability_value: 0, fly_speed: 0.05, vertical_fly_speed: 0.05, walk_speed: 0.1, }], }, links: Vec::new(), device_id: String::new(), build_platform: 0, }; let bedrock_player_list = CPlayerList { action: CPlayerList::ACTION_ADD, entries: vec![PlayerListEntry { uuid: gameprofile.id, entity_unique_id: VarLong(existing_player.entity_id() as i64), username: gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), build_platform: 0, skin: (**existing_player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, is_sub_client: false, player_color: [0, 0, 0, 0], }], }; let actions = [ PlayerAction::AddPlayer { name: &gameprofile.name, properties: &gameprofile.properties.load(), }, PlayerAction::UpdateGameMode(VarInt(existing_player.gamemode.load() as i32)), PlayerAction::UpdateListed(existing_player.tab_list_listed.load(Ordering::Relaxed)), PlayerAction::UpdateLatency(VarInt( existing_player.tab_list_latency.load(Ordering::Relaxed), )), PlayerAction::UpdateListOrder(VarInt( existing_player.tab_list_order.load(Ordering::Relaxed), )), PlayerAction::UpdateHat(true), ]; let java_player = [pumpkin_protocol::java::client::play::Player { uuid: gameprofile.id, actions: &actions, }]; player .client .enqueue_packet_editioned( &CPlayerInfoUpdate::new( (PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_LISTED | PlayerInfoFlags::UPDATE_GAME_MODE | PlayerInfoFlags::UPDATE_LATENCY | PlayerInfoFlags::UPDATE_LIST_PRIORITY | PlayerInfoFlags::UPDATE_HAT) .bits(), &java_player, ), &bedrock_player_list, ) .await; player .client .enqueue_packet_editioned( &CSpawnEntity::new( existing_player.entity_id().into(), gameprofile.id, i32::from(EntityType::PLAYER.id).into(), pos, entity.pitch.load(), entity.yaw.load(), entity.head_yaw.load(), 0.into(), entity.velocity.load(), ), &bedrock_add_player, ) .await; { let config = existing_player.config.load(); let mut buf = Vec::new(); { let meta = Metadata::new( TrackedData::PLAYER_MODE_CUSTOMISATION, MetaDataType::BYTE, config.skin_parts, ); meta.write(&mut buf, &client.version.load()).unwrap(); }; { let meta = Metadata::new( TrackedData::PLAYER_MODE_CUSTOMIZATION_ID, MetaDataType::BYTE, config.skin_parts, ); meta.write(&mut buf, &client.version.load()).unwrap(); }; drop(config); // END buf.put_u8(255); client .enqueue_packet(&CSetEntityMetadata::new( existing_player.get_entity().entity_id.into(), buf.into(), )) .await; }; { let mut equipment_list = Vec::new(); equipment_list.push(( EquipmentSlot::MAIN_HAND.discriminant(), existing_player.inventory.held_item().lock().await.clone(), )); for (slot, item_arc_mutex) in &existing_player .inventory .entity_equipment .lock() .await .equipment { let item_stack = item_arc_mutex.lock().await.clone(); equipment_list.push((slot.discriminant(), item_stack)); } let equipment: Vec<(i8, ItemStackSerializer)> = equipment_list .iter() .map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone()))) .collect(); client .enqueue_packet(&CSetEquipment::new( existing_player.entity_id().into(), equipment, )) .await; } } player.send_client_information(); player.send_abilities_update().await; // Sync selected slot player .enqueue_set_held_item_packet(&CSetSelectedSlot::new( player.get_inventory().get_selected_slot() as i8, )) .await; // Start waiting for level chunks. Sets the "Loading Terrain" screen debug!("Sending waiting chunks to {}", player.gameprofile.name); client .send_packet_now(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) .await; self.worldborder.lock().await.init_client(client).await; // Sends initial time player.send_time(self).await; let (spawn_block_pos, yaw, pitch) = { let level_info_lock = self.level_info.load(); ( BlockPos::new( level_info_lock.spawn_x, level_info_lock.spawn_y, level_info_lock.spawn_z, ), level_info_lock.spawn_yaw, level_info_lock.spawn_pitch, ) }; client .send_packet_now(&CPlayerSpawnPosition::new( spawn_block_pos, yaw, pitch, self.dimension.minecraft_name.to_owned(), )) .await; // Send initial weather state let weather = self.weather.lock().await; if weather.raining { client .enqueue_packet(&CGameEvent::new(GameEvent::BeginRaining, 0.0)) .await; // Calculate rain and thunder levels directly from public fields let rain_level = weather.rain_level.clamp(0.0, 1.0); let thunder_level = weather.thunder_level.clamp(0.0, 1.0); drop(weather); client .enqueue_packet(&CGameEvent::new(GameEvent::RainLevelChange, rain_level)) .await; client .enqueue_packet(&CGameEvent::new( GameEvent::ThunderLevelChange, thunder_level, )) .await; } // if let Some(bossbars) = self..lock().get_player_bars(&player.gameprofile.id) { // for bossbar in bossbars { // player.send_bossbar(bossbar); // } // } player.has_played_before.store(true, Ordering::Relaxed); player .on_screen_handler_opened(player.player_screen_handler.clone()) .await; player.send_active_effects().await; player.breath_manager.send_air_supply(player); self.send_player_equipment(player).await; if let crate::net::ClientPlatform::Java(java_client) = player.client.as_ref() && server.advanced_config.recipe.send_recipes { java_client .send_packet_now(&CRecipeBookSettings::default_closed()) .await; let dynamic_recipes = server.recipe_manager.get_dynamic_recipes().await; java_client .send_packet_now(&CRecipeBookAdd::new(true, &dynamic_recipes)) .await; } let msg_comp = TextComponent::translate_cross( translation::java::MULTIPLAYER_PLAYER_JOINED, translation::bedrock::MULTIPLAYER_PLAYER_JOINED, [TextComponent::text(player.gameprofile.name.clone())], ) .color_named(NamedColor::Yellow); let mut event = PlayerJoinEvent::new(player.clone(), msg_comp); server.plugin_manager.fire(server, &mut event).await; if !event.cancelled { self.broadcast_system_message(&event.join_message, false) .await; // TODO: Switch to structured logging, e.g. info!(player = %name, "connected") info!("{}", event.join_message.to_pretty_console()); } } async fn send_player_equipment(&self, from: &Player) { let mut equipment_list = Vec::new(); equipment_list.push(( EquipmentSlot::MAIN_HAND.discriminant(), from.inventory.held_item().lock().await.clone(), )); for (slot, item_arc_mutex) in &from.inventory.entity_equipment.lock().await.equipment { let item_stack = item_arc_mutex.lock().await.clone(); equipment_list.push((slot.discriminant(), item_stack)); } let equipment: Vec<(i8, ItemStackSerializer)> = equipment_list .iter() .map(|(slot, stack)| (*slot, ItemStackSerializer::from(stack.clone()))) .collect(); let chunk_pos = from.get_entity().chunk_pos.load(); self.broadcast_to_chunk_except( chunk_pos, &[from.get_entity().entity_uuid], &CSetEquipment::new(from.entity_id().into(), equipment), ); } pub async fn send_world_info( &self, player: &Arc, position: Vector3, yaw: f32, pitch: f32, ) { if let ClientPlatform::Java(client) = player.client.as_ref() { self.worldborder.lock().await.init_client(client).await; } // TODO: World spawn (compass stuff) player .client .enqueue_packet(&CGameEvent::new(GameEvent::StartWaitingChunks, 0.0)) .await; let entity = &player.get_entity(); self.broadcast_packet_except( &[player.gameprofile.id], // TODO: add velo &CSpawnEntity::new( entity.entity_id.into(), player.gameprofile.id, i32::from(EntityType::PLAYER.id).into(), position, pitch, yaw, yaw, 0.into(), Vector3::new(0.0, 0.0, 0.0), ), ); player.send_client_information(); chunker::update_position(player).await; // Update commands player.set_health(20.0).await; } pub async fn explode(self: &Arc, position: Vector3, power: f32) { let explosion = Explosion::new(power, position); let block_count = explosion.explode(self).await; let particle = if power < 2.0 { Particle::Explosion } else { Particle::ExplosionEmitter }; for player in self.players.load().iter() { let mut sound_id = Sound::EntityGenericExplode as u16; 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); if player.position().squared_distance_to_vec(&position) > 4096.0 { continue; } player .client .enqueue_packet(&CExplosion::new( position, power, block_count as i32, None, VarInt(particle as i32), sound.clone(), )) .await; } } #[allow(clippy::too_many_lines)] pub async fn respawn_player(self: &Arc, player: &Arc, alive: bool) { let last_pos = player.get_entity().last_pos.load(); let death_dimension = ResourceLocation::from(player.world().dimension.minecraft_name); let death_location = BlockPos(Vector3::new( last_pos.x.round() as i32, last_pos.y.round() as i32, last_pos.z.round() as i32, )); let data_kept = u8::from(alive); // Copy spawn info from level_info to avoid holding lock across await let (spawn_x, spawn_z, spawn_yaw, spawn_pitch, keep_inventory) = { let info = self.level_info.load(); ( info.spawn_x, info.spawn_z, info.spawn_yaw, info.spawn_pitch, info.game_rules.keep_inventory, ) }; // Get respawn position and dimension let (position, yaw, pitch, respawn_dimension) = if let Some(respawn) = player.calculate_respawn_point().await { ( respawn.position, respawn.yaw, respawn.pitch, respawn.dimension, ) } else { // No valid respawn point - send notification and use world spawn player .client .send_packet_now(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0)) .await; // FIXME: This spawn position calculation is incorrect. Should use vanilla's // proper spawn position calculation (see #1381). The y-level calculation // needs to account for spawn radius and find a safe spawn position. let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4); self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await; let top = self.get_top_block(Vector2::new(spawn_x, spawn_z)); ( Vector3::new( f64::from(spawn_x) + 0.5, (top + 1).into(), f64::from(spawn_z) + 0.5, ), spawn_yaw, spawn_pitch, self.dimension.clone(), ) }; // Candidate destination world for a cross-dimension respawn. let candidate_world = if respawn_dimension == self.dimension { None } else { self.server.upgrade().map_or_else( || { warn!("Could not get server for cross-dimension respawn"); None }, |server| { let worlds = server.worlds.load(); worlds .iter() .find(|w| w.dimension == respawn_dimension) .cloned() }, ) }; // Fire PlayerChangeWorldEvent (cancellable) before the transfer; it runs before // the non-cancellable PlayerRespawnEvent, which observes the resolved world. let (resolved_world, position, yaw, pitch) = if let Some(new_world) = candidate_world { if let Some(server) = self.server.upgrade() { let mut event = PlayerChangeWorldEvent { player: player.clone(), previous_world: self.clone(), new_world: new_world.clone(), position, yaw, pitch, cancelled: false, }; server.plugin_manager.fire(&server, &mut event).await; if event.cancelled { (None, position, yaw, pitch) } else { let destination = event.new_world; let position = event.position; let yaw = event.yaw; let pitch = event.pitch; // Skip the transfer if redirected back to the current world. if destination.uuid != self.uuid { debug!( "Cross-dimension respawn: {} -> {}", self.dimension.minecraft_name, destination.dimension.minecraft_name ); // Detach from the old world before publishing into the new one, so no // observer sees the player in a world whose chunk manager doesn't match. self.remove_player(player, false).await; player.unload_watched_chunks(self).await; player .chunk_manager .lock() .await .change_world(&self.level, destination.clone()); player.living_entity.entity.set_world(destination.clone()); destination.players.rcu(|current_list| { let mut new_list = (**current_list).clone(); new_list.push(player.clone()); new_list }); } (Some(destination), position, yaw, pitch) } } else { warn!("Server dropped during cross-dimension respawn"); (None, position, yaw, pitch) } } else { if respawn_dimension != self.dimension { warn!( "Target world {:?} not found, using world spawn in {:?}", respawn_dimension, self.dimension ); } (None, position, yaw, pitch) }; // Cancelled or unresolved cross-dimension respawns fall back to the current // world's spawn below; otherwise the resolved values from the event apply. let (target_world, position, yaw, pitch) = if let Some(ref new_world) = resolved_world { (new_world.clone(), position, yaw, pitch) } else if respawn_dimension != self.dimension { // FIXME: This spawn position calculation is incorrect. Should use vanilla's // proper spawn position calculation (see #1381). let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4); self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await; let top = self.get_top_block(Vector2::new(spawn_x, spawn_z)); let fallback_pos = Vector3::new( f64::from(spawn_x) + 0.5, (top + 1).into(), f64::from(spawn_z) + 0.5, ); (self.clone(), fallback_pos, spawn_yaw, spawn_pitch) } else { (self.clone(), position, yaw, pitch) }; // Notify plugins that the player has respawned (non-cancellable). if let Some(server) = self.server.upgrade() { server .plugin_manager .fire( &server, &mut PlayerRespawnEvent::new( player.clone(), self.clone(), target_world.clone(), position, yaw, pitch, alive, ), ) .await; } // Send respawn packet with target dimension (using send_packet_now to ensure proper order) player .client .send_packet_now(&CRespawn::new( PlayerSpawnData::new( target_world.dimension.clone(), biome::hash_seed(target_world.level.seed.0), player.gamemode.load() as u8, player.gamemode.load() as i8, false, false, Some((death_dimension, death_location)), VarInt(player.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32), target_world.sea_level.into(), ), data_kept, )) .await; // Inform the client of the default spawn position so the client doesn't // fall back to (0, 2, 0) while the world reloads (fixes rubberbanding). // This must be sent after the CRespawn packet for proper client positioning. let spawn_block_pos = BlockPos(Vector3::new( position.x.round() as i32, position.y.round() as i32, position.z.round() as i32, )); let bedrock_dimension = match target_world.dimension.minecraft_name { "minecraft:the_nether" => 1, "minecraft:the_end" => 2, _ => 0, }; player .client .send_packet_now_editioned( &CPlayerSpawnPosition::new( spawn_block_pos, yaw, pitch, target_world.dimension.minecraft_name.to_string(), ), &pumpkin_protocol::bedrock::client::CSetSpawnPosition::new( 1, // World spawn spawn_block_pos, bedrock_dimension, spawn_block_pos, ), ) .await; player.living_entity.reset_state().await; player.send_permission_lvl_update(); player.hunger_manager.restart(); if !keep_inventory { player.set_experience(0, 0.0, 0).await; player.inventory.clear().await; } // Set entity position BEFORE loading chunks, so chunks load at the right location // This mirrors the initial spawn flow where update_position is called before teleport player.get_entity().set_pos(position); player.get_entity().set_rotation(yaw, pitch); player.get_entity().last_pos.store(position); // TODO: difficulty, exp bar, status effect // Load chunks and send world info FIRST (before teleport packet) target_world .send_world_info(player, position, yaw, pitch) .await; // Ensure at least the center chunk is sent synchronously before teleport. 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 .get_or_fetch_chunk(center_chunk, std::clone::Clone::clone) .await; java_client.send_packet_now(&CChunkBatchStart).await; java_client.send_packet_now(&CChunkData(&chunk)).await; java_client .send_packet_now(&CChunkBatchEnd::new(1u16)) .await; } // Send teleport packet after at least the center chunk was delivered player.request_teleport(position, yaw, pitch).await; } /// Returns true if enough players are sleeping and we should skip the night. pub fn should_skip_night(&self) -> bool { let players = self.players.load(); let player_count = players.len(); let sleeping_player_count = players .iter() .filter(|player| { player .sleeping_since .load() .is_some_and(|since| since >= 100) }) .count(); drop(players); if player_count == 0 { return false; } let sleep_percentage = self .level_info .load() .game_rules .players_sleeping_percentage .clamp(0, 100); let required_sleeping = ((player_count as f64 * sleep_percentage as f64) / 100.0).ceil() as usize; let required_sleeping = required_sleeping.max(1); sleeping_player_count >= required_sleeping } // NOTE: This function doesn't actually await on anything, it just spawns two tokio tasks /// IMPORTANT: Chunks have to be non-empty fn spawn_world_entity_chunks( self: &Arc, player: Arc, chunks: Vec>, center_chunk: Vector2, ) { #[cfg(debug_assertions)] let inst = std::time::Instant::now(); // Sort such that the first chunks are closest to the center. let mut chunks = chunks; chunks.sort_unstable_by_key(|pos| { let rel_x = pos.x - center_chunk.x; let rel_z = pos.y - center_chunk.y; rel_x * rel_x + rel_z * rel_z }); let mut entity_receiver = self.level.receive_entity_chunks(chunks); let level = self.level.clone(); let world = self.clone(); player.clone().spawn_task(async move { 'main: loop { let recv_result = tokio::select! { () = player.client.await_close_interrupt() => { debug!("Canceling player packet processing"); None }, recv_result = entity_receiver.recv() => { recv_result } }; let Some((chunk_weak, first_load)) = recv_result else { break; }; let Some(chunk) = chunk_weak.upgrade() else { continue; }; let position = Vector2::new(chunk.x, chunk.z); if !level.is_chunk_watched(&position) { // No longer watched: don't make its entities live. Leave the // serialized data untouched so the normal unload path persists // it as-is (nothing went live, so there is nothing to save). trace!( "Received entity chunk {:?}, but it is no longer watched; leaving it for the unload path", &position ); continue 'main; } if first_load { // First watcher: consume the serialized entities and make them // live. The live entity list becomes the single source of // truth, so the chunk's NBT is taken (cleared) to avoid keeping // a duplicate copy that would be re-appended on the next unload // and doubled on every reload. let entity_nbts = std::mem::take(&mut *chunk.data.lock().await); let mut entities_to_add: Vec> = Vec::with_capacity(entity_nbts.len()); for entity_nbt in &entity_nbts { let Some(id) = entity_nbt.get_string("id") else { debug!("Entity has no ID"); continue; }; let Some(entity_type) = EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(id)) else { warn!("Entity has no valid Entity Type {id}"); continue; }; // Keep the persisted UUID so the entity keeps its identity // across reloads (matching vanilla); only fall back to a // fresh one if it is missing/corrupt. let uuid = entity_nbt.get_uuid("UUID").unwrap_or_else(Uuid::new_v4); // Pos is zero since it will be read from nbt. let entity = from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, uuid); entity.read_nbt_non_mut(entity_nbt).await; entity.init_data_tracker().await; let base_entity = entity.get_entity(); // Clear velocity so the client does not replay the drop // animation; residual velocity from the original drop is // stale data. base_entity.velocity.store(Vector3::default()); player .client .enqueue_packet(&base_entity.create_spawn_packet()) .await; entities_to_add.push(entity); } if !entities_to_add.is_empty() { world.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone(); new_entities.extend(entities_to_add.iter().cloned()); new_entities }); } } else { // The chunk's entities are already live (another watcher loaded // them). Just send this player the spawn packets for the live // entities currently in this chunk. for entity in world.entities.load().iter() { let base_entity = entity.get_entity(); if base_entity.chunk_pos.load() == position { player .client .enqueue_packet(&base_entity.create_spawn_packet()) .await; } } } } #[cfg(debug_assertions)] debug!("Chunks queued after {}ms", inst.elapsed().as_millis()); }); } /// Gets a `Player` by an entity id pub fn get_player_by_id(&self, id: i32) -> Option> { for player in self.players.load().iter() { if player.entity_id() == id { return Some(player.clone()); } } None } /// Gets an entity by an entity id pub fn get_entity_by_id(&self, id: i32) -> Option> { for entity in self.entities.load().iter() { if entity.get_entity().entity_id == id { return Some(entity.clone()); } } for player in self.players.load().iter() { if player.get_entity().entity_id == id { return Some(player.clone() as Arc); } } None } /// Gets a `Player` by a username pub fn get_player_by_name(&self, name: &str) -> Option> { for player in self.players.load().iter() { if player.gameprofile.name.eq_ignore_ascii_case(name) { return Some(player.clone()); } } None } // Gets all entities at a Box pub fn get_all_at_box(&self, aabb: &BoundingBox) -> Vec> { let entities_guard = self.entities.load(); let players_guard = self.players.load(); entities_guard .iter() .map(|e| e.clone() as Arc) .chain( players_guard .iter() .map(|p| p.clone() as Arc), ) .filter(|entity| entity.get_entity().bounding_box.load().intersects(aabb)) .collect() } // Gets all non Player entities at a Box pub fn get_entities_at_box(&self, aabb: &BoundingBox) -> Vec> { self.entities .load() .iter() .filter(|entity| entity.get_entity().bounding_box.load().intersects(aabb)) .cloned() .collect() } // Gets all Player entities at a Box pub fn get_players_at_box(&self, aabb: &BoundingBox) -> Vec> { let players_guard = self.players.load(); players_guard .iter() .filter(|player| player.get_entity().bounding_box.load().intersects(aabb)) .cloned() .collect() } /// Retrieves a player by their unique UUID. /// /// This function searches the world's active player list for a player with the specified UUID. /// If found, it returns an `Arc` reference to the player. Otherwise, it returns `None`. /// /// # Arguments /// /// * `id`: The UUID of the player to retrieve. /// /// # Returns /// /// An `Option>` containing the player if found, or `None` if not. pub fn get_player_by_uuid(&self, id: uuid::Uuid) -> Option> { self.players .load() .iter() .find(|p| p.gameprofile.id == id) .cloned() } /// Retrieves an entity by their unique UUID. /// /// This function searches the world's entities for one with the specified UUID. /// If found, it returns an `Arc` reference to that entity. Otherwise, it returns `None`. /// /// # Arguments /// /// * `id`: The UUID of the entity to retrieve. /// /// # Returns /// /// An `Option>` containing the player if found, or `None` if not. pub fn get_entity_by_uuid(&self, id: uuid::Uuid) -> Option> { self.entities .load() .iter() .find(|p| p.get_entity().entity_uuid == id) .cloned() } /// Gets a list of players whose location equals the given position in the world. /// /// It iterates through the players in the world and checks their location. If the player's location matches the /// given position, it will add this to a `Vec` which it later returns. If no /// player was found in that position, it will just return an empty `Vec`. /// /// # Arguments /// /// * `position`: The position the function will check. pub fn get_players_by_pos(&self, position: BlockPos) -> Vec> { self.players .load() .iter() .filter_map(|player| { let player_block_pos = player.get_entity().block_pos.load().0; (position.0.x == player_block_pos.x && position.0.y == player_block_pos.y && position.0.z == player_block_pos.z) .then(|| Arc::clone(player)) }) .collect::<_>() } /// Gets the nearby players around a given world position. /// It "creates" a sphere and checks if whether players are inside /// and returns a `HashMap` where the UUID is the key and the `Player` /// object is the value. /// /// # Arguments /// * `pos`: The center of the sphere. /// * `radius`: The radius of the sphere. The higher the radius, the more area will be checked (in every direction). pub fn get_nearby_players(&self, pos: Vector3, radius: f64) -> Vec> { let radius_squared = radius.powi(2); self.players .load() .iter() .filter_map(|player| { let player_pos = player.get_entity().pos.load(); (player_pos.squared_distance_to_vec(&pos) <= radius_squared).then(|| player.clone()) }) .collect() } pub fn get_nearby_entities( &self, pos: Vector3, radius: f64, ) -> HashMap> { let radius_squared = radius.powi(2); self.entities .load() .iter() .filter_map(|entity| { let entity_pos = entity.get_entity().pos.load(); (entity_pos.squared_distance_to_vec(&pos) <= radius_squared) .then(|| (entity.get_entity().entity_uuid, entity.clone())) }) .collect() } pub fn get_closest_player(&self, pos: Vector3, radius: f64) -> Option> { let players = self.get_nearby_players(pos, radius); players .iter() .min_by(|a, b| { a.get_entity() .pos .load() .squared_distance_to_vec(&pos) .partial_cmp(&b.get_entity().pos.load().squared_distance_to_vec(&pos)) .unwrap() }) .cloned() } /// Gets the closest entity to a position, with optional filtering by entity type. /// /// # Arguments /// /// * `pos` - The position to search around. /// * `radius` - The radius to search within. /// * `entity_types` - Optional array of entity types to filter by. If None, all entity types are included. /// /// # Returns /// /// The closest entity that matches the filter criteria, or None if no entities are found. pub fn get_closest_entity( &self, pos: Vector3, radius: f64, entity_types: Option<&[&'static EntityType]>, ) -> Option> { // Get regular entities let entities = self.get_nearby_entities(pos, radius); // Filter by entity type if specified let filtered_entities = if let Some(types) = entity_types { entities .into_iter() .filter(|(_, entity)| { let entity_type = entity.get_entity().entity_type; types.contains(&entity_type) }) .collect::>() } else { entities }; // Find the closest entity filtered_entities .iter() .min_by(|a, b| { a.1.get_entity() .pos .load() .squared_distance_to_vec(&pos) .partial_cmp(&b.1.get_entity().pos.load().squared_distance_to_vec(&pos)) .unwrap() }) .map(|p| p.1.clone()) } /// Adds entities to the provided [`Vec`] that satisfy a particular condition and are /// present in the provided [`BoundingBox`]. /// /// # Arguments /// /// * `list`: The `Vec` to add to. /// * `max_list_capacity`: The maximum capacity of `list` for adding entities. If this limit is reached, no more /// entities will be added to the list. If `list` already reaches this limit, nothing happens. /// * `bounding_box`: The bounding box to filter any added entities. /// * `predicate`: A predicate function, which has to be `true` for an entity to be added to the list. pub fn extend_entities_in_box_where( &self, list: &mut Vec>, max_list_capacity: usize, bounding_box: BoundingBox, predicate: impl Fn(&dyn EntityBase) -> bool, ) { self.extend_entities_where(list, max_list_capacity, |e| { bounding_box.intersects(&e.get_entity().bounding_box.load()) && predicate(e) }); } /// Adds entities to the provided [`Vec`] that satisfy a particular condition. /// /// # Arguments /// /// * `list`: The `Vec` to add to. /// * `max_list_capacity`: The maximum capacity of `list` for adding entities. If this limit is reached, no more /// entities will be added to the list. If `list` already reaches this limit, nothing happens. /// * `predicate`: A predicate function, which has to be `true` for an entity to be added to the list. pub fn extend_entities_where( &self, list: &mut Vec>, max_list_capacity: usize, predicate: impl Fn(&dyn EntityBase) -> bool, ) { if list.len() >= max_list_capacity { return; } // Loop the players. for player in self.players.load().iter() { if !predicate(player.as_ref()) { continue; } // We add the player to the list. list.push(player.clone()); // Check if the list is too big. if list.len() > max_list_capacity { return; } } // Same with entities. for entity in self.entities.load().iter() { if !predicate(entity.as_ref()) { continue; } list.push(entity.clone()); if list.len() > max_list_capacity { return; } // TODO: Implement ender dragon handling } } /// Adds a player to the world and broadcasts a join message if enabled. /// /// This function takes a player's UUID and an `Arc` reference. /// It inserts the player into the world's `current_players` map using the UUID as the key. /// Additionally, it broadcasts a join message to all connected players in the world. /// /// # Arguments /// /// * `player`: An `Arc` reference to the player object. pub fn add_player(&self, player: &Arc) -> Result<(), String> { self.players.rcu(|current_list| { let mut new_list = (**current_list).clone(); new_list.push(player.clone()); new_list }); Ok(()) } /// Removes a player from the world and broadcasts a disconnect message if enabled. /// /// This function removes a player from the world based on their `Player` reference. /// It performs the following actions: /// /// 1. Removes the player from the `current_players` map using their UUID. /// 2. Broadcasts a `CRemovePlayerInfo` packet to all connected players to inform them about the player leaving. /// 3. Removes the player's entity from the world using its entity ID. /// 4. Optionally sends a disconnect message to all other players notifying them about the player leaving. /// /// # Arguments /// /// * `player`: A reference to the `Player` object to be removed. /// * `fire_event`: A boolean flag indicating whether to fire a `PlayerLeaveEvent` event. /// /// # Notes /// /// - This function assumes `broadcast_packet_expect` and `remove_entity` are defined elsewhere. /// - The disconnect message sending is currently optional. Consider making it a configurable option. pub async fn remove_player( &self, player: &Arc, fire_event: bool, ) -> Option> { let mut removed_player: Option> = None; self.players.rcu(|current_list| { let mut new_list = (**current_list).clone(); // Find the player before we filter them out if let Some(pos) = new_list .iter() .position(|p| p.gameprofile.id == player.gameprofile.id) { removed_player = Some(new_list.remove(pos)); } new_list }); if let Some(ref player) = removed_player { let uuid = player.gameprofile.id; let entity_id = player.entity_id(); let bedrock_remove_player = CPlayerList { action: CPlayerList::ACTION_REMOVE, entries: vec![PlayerListEntry { uuid, entity_unique_id: VarLong(entity_id as i64), username: player.gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), build_platform: 0, skin: Skin::steve(), is_teacher: false, is_host: false, is_sub_client: false, player_color: [0, 0, 0, 0], }], }; self.broadcast_editioned(&CRemovePlayerInfo::new(&[uuid]), &bedrock_remove_player) .await; self.broadcast_editioned( &CRemoveEntities::new(&[entity_id.into()]), &CRemoveActor::new(VarLong(entity_id as i64)), ) .await; if fire_event { let msg_comp = TextComponent::translate_cross( translation::java::MULTIPLAYER_PLAYER_LEFT, translation::bedrock::MULTIPLAYER_PLAYER_LEFT, [TextComponent::text(player.gameprofile.name.clone())], ) .color_named(NamedColor::Yellow); let mut event = PlayerLeaveEvent::new(player.clone(), msg_comp); if let Some(server) = self.server.upgrade() { server.plugin_manager.fire(&server, &mut event).await; if !event.cancelled { for player in self.players.load().iter() { player.send_system_message(&event.leave_message).await; } info!("{}", event.leave_message.to_pretty_console()); } } } } removed_player } pub fn spawn_entity_non_save(&self, entity: &Arc) { let _base_entity = entity.get_entity(); self.broadcast_entity_spawn(entity); self.spawn_state.load().add_entity(self, entity.as_ref()); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone(); new_entities.push(entity.clone()); new_entities }); } pub async fn spawn_entity(&self, entity: Arc) { self.broadcast_entity_spawn(&entity); entity.init_data_tracker().await; self.add_entity_silent(entity).await; } pub fn broadcast_entity_spawn(&self, entity: &Arc) { let base_entity = entity.get_entity(); let chunk_pos = base_entity.chunk_pos.load(); let players = self.players.load(); for player in players.iter() { let center = player.get_entity().chunk_pos.load(); let view_distance = get_view_distance(player).get() as i32; if is_within_view_distance(chunk_pos, center, view_distance) { player.client.try_enqueue_spawn_packet(entity); } } } #[allow(clippy::unused_async)] pub async fn add_entity_silent(&self, entity: Arc) { let base_entity = entity.get_entity(); // Guard against duplicate entities with the same UUID. // This can happen when chunk entity data is loaded while the entity // already exists in the world (e.g. another player is still tracking it). let already_exists = self .entities .load() .iter() .any(|e| e.get_entity().entity_uuid == base_entity.entity_uuid); if already_exists { return; } // The entity stays live-only: it is written to its chunk's saved data on // unload (see `save_entity`), never at spawn, so it can't be both live and // serialized at once (which would double it on the next reload). self.spawn_state.load().add_entity(self, entity.as_ref()); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone(); new_entities.push(entity.clone()); new_entities }); } #[allow(clippy::unused_async)] pub async fn remove_entity(&self, entity: &dyn EntityBase) { let base_entity = entity.get_entity(); self.spawn_state.load().remove_entity(self, entity); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone(); new_entities.retain(|e| e.get_entity().entity_uuid != base_entity.entity_uuid); new_entities }); let chunk_pos = base_entity.chunk_pos.load(); self.broadcast_to_chunk_editioned_sync( chunk_pos, &CRemoveEntities::new(&[base_entity.entity_id.into()]), &CRemoveActor::new(VarLong(base_entity.entity_id as i64)), ); } pub async fn remove_entities_in_chunks( &self, chunks: impl IntoIterator>>, ) { let chunks_set: FxHashSet<_> = chunks.into_iter().map(|c| *c.borrow()).collect(); if chunks_set.is_empty() { return; } let mut entities_to_remove = Vec::new(); self.entities.rcu(|current_entities| { let mut new_entities = (**current_entities).clone(); new_entities.retain(|entity| { let base_entity = entity.get_entity(); let pos = base_entity.chunk_pos.load(); if chunks_set.contains(&pos) { entities_to_remove.push(entity.clone()); false } else { true } }); new_entities }); for entity in entities_to_remove { self.save_entity(&entity).await; self.spawn_state.load().remove_entity(self, entity.as_ref()); } for chunk_pos in &chunks_set { self.block_entities.remove(chunk_pos); } } pub async fn set_block_breaking(&self, from: &Entity, location: BlockPos, progress: i32) { let chunk_pos = location.chunk_position(); // pumpkin's BlockPos already has this method let je_packet = CSetBlockDestroyStage::new(from.entity_id.into(), location, progress as i8); let (event_id, data) = match progress { -1 => (LevelEvent::BlockStopBreak, 0), 0 => (LevelEvent::BlockStartBreak, 0), _ => (LevelEvent::BlockUpdateBreak, progress), }; let be_packet = CLevelEvent { event_id: VarInt(event_id as i32), position: Vector3::new( location.0.x as f32, location.0.y as f32, location.0.z as f32, ), data: VarInt(data), }; self.broadcast_to_chunk_except_editioned( chunk_pos, &[from.entity_uuid], &je_packet, &be_packet, ) .await; } /// Sets a block and returns the old block id #[expect(clippy::too_many_lines)] pub async fn set_block_state( self: &Arc, position: &BlockPos, block_state_id: BlockStateId, flags: BlockFlags, ) -> BlockStateId { let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); let replaced_block_state_id = self .level .read_chunk_sync(&chunk_coordinate, |chunk| { let replaced_block_state_id = chunk.set_block_absolute_y( relative.x as usize, relative.y, relative.z as usize, block_state_id, ); // Mark chunk dirty if it isn't already if replaced_block_state_id != block_state_id && !chunk.is_dirty() { chunk.mark_dirty(true); } replaced_block_state_id }) .unwrap_or(Block::AIR.default_state.id); if replaced_block_state_id == block_state_id { return block_state_id; } self.unsent_block_changes .lock() .await .insert(*position, block_state_id); let old_block = Block::from_state_id(replaced_block_state_id); let new_block = Block::from_state_id(block_state_id); let block_moved = flags.contains(BlockFlags::MOVED); let is_new_block = old_block != new_block; // WorldChunk.java line 305-314 if is_new_block && old_block.default_state.block_entity_type != u16::MAX && let Some(entity) = self.get_block_entity(position) { entity.on_block_replaced(self.clone(), *position).await; self.remove_block_entity(position); } // WorldChunk.java line 317 if is_new_block && (flags.contains(BlockFlags::NOTIFY_NEIGHBORS) || block_moved) { self.block_registry .on_state_replaced( self, old_block, position, replaced_block_state_id, block_moved, ) .await; } // WorldChunk.java line 318 if !flags.contains(BlockFlags::SKIP_BLOCK_ADDED_CALLBACK) && new_block != old_block { self.block_registry .on_placed( self, new_block, block_state_id, position, replaced_block_state_id, block_moved, ) .await; let new_fluid = self.get_fluid(position); self.block_registry .on_placed_fluid( self, new_fluid, block_state_id, position, replaced_block_state_id, block_moved, ) .await; } // Ig they do this cause it could be modified in chunkPos.setBlockState? if self.get_block_state_id(position) == block_state_id { if flags.contains(BlockFlags::NOTIFY_LISTENERS) { // Mob AI update } if flags.contains(BlockFlags::NOTIFY_NEIGHBORS) { self.update_neighbors(position, None).await; // TODO: updateComparators } if !flags.contains(BlockFlags::FORCE_STATE) { let mut new_flags = flags; new_flags.remove(BlockFlags::NOTIFY_NEIGHBORS); new_flags.remove(BlockFlags::NOTIFY_LISTENERS); self.block_registry .prepare( self, position, Block::from_state_id(replaced_block_state_id), replaced_block_state_id, new_flags, ) .await; self.block_registry .update_neighbors( self, position, Block::from_state_id(block_state_id), new_flags, ) .await; self.block_registry .prepare( self, position, Block::from_state_id(block_state_id), block_state_id, new_flags, ) .await; } } let (_chunk_coordinate, _) = position.chunk_and_chunk_relative_position(); self.level .light_engine .update_lighting_at(&self.level, *position); replaced_block_state_id } pub fn get_max_local_raw_brightness(&self, pos: &BlockPos) -> u8 { let sky_light = self.get_sky_light_level(pos); let block_light = self.get_block_light_level(pos).unwrap_or(0); sky_light.max(block_light) // TODO: getSkyDarken } pub fn get_block_light_level(&self, position: &BlockPos) -> Option { self.level .light_engine .get_block_light_level(&self.level, position) } pub fn get_sky_light_level(&self, position: &BlockPos) -> u8 { self.level .light_engine .get_sky_light_level(&self.level, position) } #[must_use] pub fn can_see_sky(&self, position: &BlockPos) -> bool { position.0.y >= self.dimension.min_y && position.0.y < self.dimension.min_y + self.dimension.height && self.get_sky_light_level(position) >= MAX_LIGHT_LEVEL } pub fn set_block_light_level(&self, position: &BlockPos, light_level: u8) { let _ = self .level .light_engine .set_block_light_level(&self.level, position, light_level); } pub fn set_sky_light_level(&self, position: &BlockPos, light_level: u8) { let _ = self .level .light_engine .set_sky_light_level(&self.level, position, light_level); } pub fn get_biome(&self, position: &BlockPos) -> &'static Biome { let chunk_pos = position.chunk_position(); if let Some(chunk) = self.level.loaded_chunks.get(&chunk_pos) { let id = chunk .section .get_rough_biome_absolute_y( (position.0.x & 15) as usize, position.0.y, (position.0.z & 15) as usize, ) .unwrap_or(0); Biome::from_id(id).unwrap_or(&Biome::PLAINS) } else { &Biome::PLAINS } } pub fn schedule_block_tick( &self, block: &Block, block_pos: BlockPos, delay: u8, priority: TickPriority, ) { self.level .schedule_block_tick(block, block_pos, delay, priority); } pub fn schedule_fluid_tick( &self, fluid: &Fluid, block_pos: BlockPos, delay: u8, priority: TickPriority, ) { self.level .schedule_fluid_tick(fluid, block_pos, delay, priority); } pub fn is_block_tick_scheduled(&self, block_pos: &BlockPos, block: &Block) -> bool { self.level.is_block_tick_scheduled(block_pos, block) } pub fn is_fluid_tick_scheduled(&self, block_pos: &BlockPos, fluid: &Fluid) -> bool { self.level.is_fluid_tick_scheduled(block_pos, fluid) } // Return new state #[allow(clippy::too_many_lines)] pub async fn break_block( self: &Arc, position: &BlockPos, cause: Option>, flags: BlockFlags, ) -> Option { let (broken_block, broken_block_state) = self.get_block_and_state_id(position); if is_air(broken_block_state) { return None; } let mut event = BlockBreakEvent::new( cause.clone(), broken_block, *position, 0, !flags.contains(BlockFlags::SKIP_DROPS), ); if let Some(server) = self.server.upgrade() { server .plugin_manager .fire::(&server, &mut event) .await; } if !event.cancelled { let mut flags = flags; if event.drop { flags.remove(BlockFlags::SKIP_DROPS); } else { flags.insert(BlockFlags::SKIP_DROPS); } let new_state_id = if broken_block .properties(broken_block_state) .and_then(|properties| { properties .to_props() .into_iter() .find(|p| p.0 == "waterlogged") .map(|(_, value)| value == "true") }) .unwrap_or(false) { let mut water_props = FlowingFluidProperties::default(&Fluid::FLOWING_WATER); water_props.level = pumpkin_data::fluid::Level::L8; water_props.falling = Falling::False; water_props.to_state_id(&Fluid::FLOWING_WATER) } else { BlockStateId::AIR }; let broken_state_id = self.set_block_state(position, new_state_id, flags).await; // Close container screens for any players viewing this block self.close_container_screens_at(position).await; let luck = cause.as_ref().map_or(0.0, |player| { player.living_entity.get_attribute_value(&Attributes::LUCK) as f32 }); if Block::from_state_id(broken_state_id) != &Block::FIRE { let particles_packet = CWorldEvent::new( WorldEvent::ParticlesDestroyBlock as i32, *position, broken_state_id.as_u16().into(), false, ); let chunk_pos = position.chunk_position(); match &cause { Some(player) => { self.broadcast_to_chunk_except( chunk_pos, &[player.get_entity().entity_uuid], &particles_packet, ); } None => self.broadcast_to_chunk(chunk_pos, &particles_packet), } } if !flags.contains(BlockFlags::SKIP_DROPS) { let tool = if let Some(player) = &cause { let hand_stack = player .inventory .get_stack_in_hand(pumpkin_util::Hand::Right) .await; let stack_guard = hand_stack.lock().await; (stack_guard.item_count > 0).then(|| stack_guard.clone()) } else { None }; let is_raining = self.is_raining().await; let is_thundering = self.is_thundering().await; let params = LootContextParameters { block_state: Some(BlockState::from_id(broken_state_id)), luck, position: Some(pumpkin_util::math::vector3::Vector3::new( position.0.x as f64, position.0.y as f64, position.0.z as f64, )), world_time: self.level_info.load().day_time as u64, tool, is_raining: Some(is_raining), is_thundering: Some(is_thundering), ..Default::default() }; block::drop_loot(self, broken_block, position, true, params).await; } return Some(new_state_id); } None } /// Close container screens for all players who have a container open at the given block position. pub async fn close_container_screens_at(&self, position: &BlockPos) { let players = self.players.load(); for player in players.iter() { if player.open_container_pos.load() == Some(*position) { player.close_handled_screen().await; } } } pub async fn drop_stack(self: &Arc, pos: &BlockPos, stack: ItemStack) { let height = EntityType::ITEM.dimension[1] / 2.0; let spawn_pos = { let mut r = rand::rng(); Vector3::new( f64::from(pos.0.x) + 0.5 + r.random_range(-0.25..0.25), f64::from(pos.0.y) + 0.5 + r.random_range(-0.25..0.25) - f64::from(height), f64::from(pos.0.z) + 0.5 + r.random_range(-0.25..0.25), ) }; let entity = Entity::new(self.clone(), spawn_pos, &EntityType::ITEM); let item_entity = Arc::new(ItemEntity::new(entity, stack)); self.spawn_entity(item_entity).await; } /* ItemScatterer.java */ pub async fn scatter_inventory( self: &Arc, position: &BlockPos, inventory: &Arc, ) { for i in 0..inventory.size() { self.scatter_stack( f64::from(position.0.x), f64::from(position.0.y), f64::from(position.0.z), inventory.remove_stack(i).await, ) .await; } } pub async fn scatter_stack(self: &Arc, x: f64, y: f64, z: f64, mut stack: ItemStack) { const TRIANGULAR_DEVIATION: f64 = 0.114_850_001_711_398_36; const XZ_MODE: f64 = 0.0; const Y_MODE: f64 = 0.2; let width = f64::from(EntityType::ITEM.dimension[0]); let half_width = width / 2.0; let spawn_area = 1.0 - width; let mut rng = Xoroshiro::from_seed(get_seed()); // TODO: Use world random here: world.random.nextDouble() let x = rng.next_f64().mul_add(spawn_area, x.floor()) + half_width; let y = rng.next_f64().mul_add(spawn_area, y.floor()); let z = rng.next_f64().mul_add(spawn_area, z.floor()) + half_width; while !stack.is_empty() { let item = stack.split((rng.next_bounded_i32(21) + 10) as u8); let velocity = Vector3::new( rng.next_triangular(XZ_MODE, TRIANGULAR_DEVIATION), rng.next_triangular(Y_MODE, TRIANGULAR_DEVIATION), rng.next_triangular(XZ_MODE, TRIANGULAR_DEVIATION), ); let entity = Entity::new(self.clone(), Vector3::new(x, y, z), &EntityType::ITEM); let entity = Arc::new(ItemEntity::new_with_velocity(entity, item, velocity, 10)); self.spawn_entity(entity).await; } } /* End ItemScatterer.java */ pub fn sync_world_event(&self, world_event: WorldEvent, position: BlockPos, data: i32) { let chunk_pos = position.chunk_position(); self.broadcast_to_chunk( chunk_pos, &CWorldEvent::new(world_event as i32, position, data, false), ); } #[must_use] pub fn is_valid(dest: BlockPos) -> bool { Self::is_valid_horizontally(dest) && Self::is_valid_vertically(dest.0.y) } #[must_use] pub fn is_valid_horizontally(dest: BlockPos) -> bool { // Note: 30_000_000 is not valid, but -30_000_000 is. (-30_000_000..30_000_000).contains(&dest.0.x) && (-30_000_000..30_000_000).contains(&dest.0.z) } #[must_use] pub fn is_valid_vertically(y: i32) -> bool { // Note: 20_000_000 is not valid, but -20_000_000 is. (-20_000_000..20_000_000).contains(&y) } #[must_use] pub fn is_in_build_limit(&self, dest: BlockPos) -> bool { self.is_in_height_limit(dest.0.y) && Self::is_valid_horizontally(dest) } #[must_use] pub fn is_in_height_limit(&self, y: i32) -> bool { (self.get_bottom_y()..=self.get_top_y()).contains(&y) } pub const fn get_bottom_y(&self) -> i32 { self.dimension.min_y } pub const fn get_top_y(&self) -> i32 { self.dimension.min_y + self.dimension.height - 1 } /// Gets a `Block` from the block registry. Returns `Block::AIR` if the block was not found. pub fn get_block(&self, position: &BlockPos) -> &'static Block { self.get_block_state_id_if_loaded(position) .map_or(&Block::AIR, Block::from_state_id) } #[must_use] pub fn get_block_state_id_if_loaded(&self, position: &BlockPos) -> Option { if !self.is_in_build_limit(*position) { return None; } let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); self.level.read_chunk_sync(&chunk_coordinate, |chunk| { chunk .section .get_block_absolute_y(relative.x as usize, relative.y, relative.z as usize) })? } #[must_use] pub fn get_block_state_if_loaded(&self, position: &BlockPos) -> Option<&'static BlockState> { self.get_block_state_id_if_loaded(position) .map(BlockState::from_id) } #[must_use] pub fn is_loaded(&self, position: &BlockPos) -> bool { self.get_block_state_id_if_loaded(position).is_some() } pub fn get_fluid(&self, position: &BlockPos) -> &'static pumpkin_data::fluid::Fluid { let id = self.get_block_state_id(position); let fluid = Fluid::from_state_id(id).ok_or(&Fluid::EMPTY); if let Ok(fluid) = fluid { return fluid.to_flowing(); } let block = Block::from_state_id(id); block .properties(id) .and_then(|props| { props .to_props() .into_iter() .find(|p| p.0 == "waterlogged") .map(|(_, value)| { if value == "true" { &Fluid::FLOWING_WATER } else { &Fluid::EMPTY } }) }) .unwrap_or(&Fluid::EMPTY) } pub fn get_block_and_fluid( &self, position: &BlockPos, ) -> ( &'static pumpkin_data::Block, &'static pumpkin_data::fluid::Fluid, ) { let id = self.get_block_state_id(position); let block = Block::from_state_id(id); let fluid = Fluid::from_state_id(id) .map(Fluid::to_flowing) .ok_or(&Fluid::EMPTY) .unwrap_or_else(|_| { block .properties(id) .and_then(|props| { props .to_props() .into_iter() .find(|p| p.0 == "waterlogged") .map(|(_, value)| { if value == "true" { &Fluid::FLOWING_WATER } else { &Fluid::EMPTY } }) }) .unwrap_or(&Fluid::EMPTY) }); (block, fluid) } pub fn get_fluid_and_fluid_state( &self, position: &BlockPos, ) -> (&'static Fluid, &'static FluidState) { let id = self.get_block_state_id(position); let Some(raw_fluid) = Fluid::from_state_id(id) else { let block = Block::from_state_id(id); if let Some(properties) = block.properties(id) { for (name, value) in properties.to_props() { if name == "waterlogged" { if value == "true" { let state = &Fluid::FLOWING_WATER.states[0]; return (&Fluid::FLOWING_WATER, state); } break; } } } let state = &Fluid::EMPTY.states[0]; return (&Fluid::EMPTY, state); }; let fluid = raw_fluid.to_flowing(); let state = &fluid.states[0]; (fluid, state) } pub fn get_block_state_id(&self, position: &BlockPos) -> BlockStateId { self.get_block_state_id_if_loaded(position) .unwrap_or(Block::AIR.default_state.id) } /// Gets the `BlockState` from the block registry. Returns Air if the block state was not found. pub fn get_block_state(&self, position: &BlockPos) -> &'static BlockState { let id = self.get_block_state_id(position); BlockState::from_id(id) } /// Gets the Block + Block state from the Block Registry, Returns Air if the Block state has not been found pub fn get_block_and_state( &self, position: &BlockPos, ) -> (&'static Block, &'static BlockState) { let id = self.get_block_state_id(position); BlockState::from_id_with_block(id) } /// Gets the Block + state id from the Block Registry, Returns Air if the Block state has not been found pub fn get_block_and_state_id(&self, position: &BlockPos) -> (&'static Block, BlockStateId) { let id = self.get_block_state_id(position); (Block::from_state_id(id), id) } /// Updates neighboring blocks of a block pub async fn update_neighbors( self: &Arc, block_pos: &BlockPos, except: Option, ) { let source_block = self.get_block(block_pos); for direction in BlockDirection::update_order() { if except.is_some_and(|d| d == direction) { continue; } let neighbor_pos = block_pos.offset(direction.to_offset()); let (neighbor_block, neighbor_fluid) = self.get_block_and_fluid(&neighbor_pos); if let Some(neighbor_pumpkin_block) = self.block_registry.get_pumpkin_block(neighbor_block.id) { neighbor_pumpkin_block .on_neighbor_update(OnNeighborUpdateArgs { world: self, block: neighbor_block, position: &neighbor_pos, source_block, notify: false, }) .await; } if let Some(neighbor_pumpkin_fluid) = self.block_registry.get_pumpkin_fluid(neighbor_fluid.id) { neighbor_pumpkin_fluid .on_neighbor_update(self, neighbor_fluid, &neighbor_pos, false) .await; } } } pub async fn update_neighbor( self: &Arc, neighbor_block_pos: &BlockPos, source_block: &Block, ) { let neighbor_block = self.get_block(neighbor_block_pos); if let Some(neighbor_pumpkin_block) = self.block_registry.get_pumpkin_block(neighbor_block.id) { neighbor_pumpkin_block .on_neighbor_update(OnNeighborUpdateArgs { world: self, block: neighbor_block, position: neighbor_block_pos, source_block, notify: false, }) .await; } } pub async fn update_from_neighbor_shapes( self: &Arc, state_id: BlockStateId, pos: &BlockPos, ) -> BlockStateId { let mut current_state_id = state_id; let block = Block::from_state_id(state_id); for direction in BlockDirection::all() { let neighbor_pos = pos.offset(direction.to_offset()); let neighbor_state_id = self.get_block_state_id(&neighbor_pos); current_state_id = self .block_registry .get_state_for_neighbor_update( self, block, current_state_id, pos, direction, &neighbor_pos, neighbor_state_id, ) .await; } current_state_id } pub async fn replace_with_state_for_neighbor_update( self: &Arc, block_pos: &BlockPos, direction: BlockDirection, flags: BlockFlags, ) { let (block, block_state_id) = self.get_block_and_state_id(block_pos); if flags.contains(BlockFlags::SKIP_REDSTONE_WIRE_STATE_REPLACEMENT) && *block == Block::REDSTONE_WIRE { return; } let neighbor_pos = block_pos.offset(direction.to_offset()); let neighbor_state_id = self.get_block_state_id(&neighbor_pos); let new_state_id = self .block_registry .get_state_for_neighbor_update( self, block, block_state_id, block_pos, direction, &neighbor_pos, neighbor_state_id, ) .await; if new_state_id != block_state_id { if is_air(new_state_id) { self.break_block(block_pos, None, flags).await; } else { self.set_block_state(block_pos, new_state_id, flags).await; } } } /// Returns whether monsters can be spawned in the world pub fn should_spawn_monsters(&self) -> bool { let level_data = self.level_info.load(); level_data.game_rules.spawn_mobs && level_data.game_rules.spawn_monsters && level_data.difficulty != Difficulty::Peaceful } pub fn get_block_entity(&self, block_pos: &BlockPos) -> Option> { let chunk_pos = block_pos.chunk_position(); if let Some(chunk_block_entities) = self.block_entities.get(&chunk_pos) && let Some(entity) = chunk_block_entities.get(block_pos) { return Some(entity.clone()); } let nbt = self .level .read_chunk_sync(&chunk_pos, |chunk| { chunk .pending_block_entities .lock() .unwrap() .remove(block_pos) }) .flatten()?; let entity = block_entity_from_nbt(&nbt)?; self.block_entities .entry(chunk_pos) .or_default() .insert(*block_pos, entity.clone()); Some(entity) } pub fn add_block_entity(&self, block_entity: Arc) { let block_pos = block_entity.get_position(); let chunk_pos = block_pos.chunk_position(); let block_entity_nbt = block_entity.chunk_data_nbt(); let entity_id = block_entity.resource_location().to_string(); if let Some(nbt) = &block_entity_nbt { let mut bytes = Vec::new(); to_bytes_unnamed(nbt, &mut bytes).unwrap(); self.broadcast_to_chunk( chunk_pos, &CBlockEntityData::new( block_entity.get_position(), VarInt(block_entity.get_id() as i32), bytes.into_boxed_slice(), ), ); } self.block_entities .entry(chunk_pos) .or_default() .insert(block_pos, block_entity); if let Some(nbt) = block_entity_nbt { let mut full_nbt = nbt; full_nbt.put_string("id", entity_id); full_nbt.put_int("x", block_pos.0.x); full_nbt.put_int("y", block_pos.0.y); full_nbt.put_int("z", block_pos.0.z); self.add_block_entity_nbt(block_pos, &full_nbt); } self.level.read_chunk_sync(&chunk_pos, |chunk| { chunk.mark_dirty(true); }); } pub(crate) fn add_block_entity_nbt(&self, block_pos: BlockPos, nbt: &NbtCompound) { self.level .read_chunk_sync(&block_pos.chunk_position(), |chunk| { chunk .pending_block_entities .lock() .unwrap() .insert(block_pos, nbt.clone()); chunk.mark_dirty(true); }); } pub fn remove_block_entity(&self, block_pos: &BlockPos) { let chunk_pos = block_pos.chunk_position(); let removed = self.block_entities .get_mut(&chunk_pos) .is_some_and(|mut chunk_block_entities| { chunk_block_entities.remove(block_pos).is_some() }); if removed { // Drop the chunk's map once its last block entity is gone. self.block_entities .remove_if(&chunk_pos, |_, entities| entities.is_empty()); self.level.read_chunk_sync(&chunk_pos, |chunk| { chunk.mark_dirty(true); }); } } fn migrate_pending_block_entities(&self, chunk_pos: Vector2) { let positions: Vec = self .level .read_chunk_sync(&chunk_pos, |chunk| { chunk .pending_block_entities .lock() .unwrap() .keys() .copied() .collect() }) .unwrap_or_default(); for pos in positions { let already_loaded = self .block_entities .get(&chunk_pos) .is_some_and(|m| m.contains_key(&pos)); if !already_loaded && let Some(entity) = self.get_block_entity(&pos) { self.update_block_entity(&entity); } } } pub fn update_block_entity(&self, block_entity: &Arc) { let block_pos = block_entity.get_position(); let chunk_pos = block_pos.chunk_position(); let block_entity_nbt = block_entity.chunk_data_nbt(); if let Some(nbt) = &block_entity_nbt { let mut bytes = Vec::new(); to_bytes_unnamed(nbt, &mut bytes).unwrap(); self.broadcast_to_chunk( chunk_pos, &CBlockEntityData::new( block_entity.get_position(), VarInt(block_entity.get_id() as i32), bytes.into_boxed_slice(), ), ); let mut full_nbt = nbt.clone(); full_nbt.put_string("id", block_entity.resource_location().to_string()); let pos = block_entity.get_position(); full_nbt.put_int("x", pos.0.x); full_nbt.put_int("y", pos.0.y); full_nbt.put_int("z", pos.0.z); self.add_block_entity_nbt(block_pos, &full_nbt); } self.level.read_chunk_sync(&chunk_pos, |chunk| { chunk.mark_dirty(true); }); } fn intersects_aabb_with_direction( from: Vector3, to: Vector3, min: Vector3, max: Vector3, ) -> Option { let dir = to.sub(&from); let mut tmin: f64 = 0.0; let mut tmax: f64 = 1.0; let mut hit_axis = None; let mut hit_is_min = false; macro_rules! check_axis { ($axis:ident, $dir_axis:ident, $min_axis:ident, $max_axis:ident, $direction_min:expr, $direction_max:expr) => {{ if dir.$dir_axis.abs() < 1e-8 { if from.$dir_axis < min.$min_axis || from.$dir_axis > max.$max_axis { return None; } } else { let inv_d = 1.0 / dir.$dir_axis; let t_near = (min.$min_axis - from.$dir_axis) * inv_d; let t_far = (max.$max_axis - from.$dir_axis) * inv_d; // Determine entry and exit points based on ray direction let (t_entry, t_exit, is_min_face) = if inv_d >= 0.0 { (t_near, t_far, true) } else { (t_far, t_near, false) }; if t_entry > tmin { tmin = t_entry; hit_axis = Some(stringify!($axis)); hit_is_min = is_min_face; } tmax = tmax.min(t_exit); if tmax < tmin { return None; } } }}; } check_axis!(x, x, x, x, BlockDirection::West, BlockDirection::East); check_axis!(y, y, y, y, BlockDirection::Down, BlockDirection::Up); check_axis!(z, z, z, z, BlockDirection::North, BlockDirection::South); match (hit_axis, hit_is_min) { (Some("x"), true) => Some(BlockDirection::West), (Some("x"), false) => Some(BlockDirection::East), (Some("y"), true) => Some(BlockDirection::Down), (Some("y"), false) => Some(BlockDirection::Up), (Some("z"), true) => Some(BlockDirection::North), (Some("z"), false) => Some(BlockDirection::South), _ => None, } } fn ray_outline_check( &self, block_pos: &BlockPos, from: Vector3, to: Vector3, ) -> (bool, Option) { let state = self.get_block_state(block_pos); if state.outline_shapes.is_empty() { return (true, None); } let bounding_boxes = state.get_block_outline_shapes(); for shape in bounding_boxes { let world_min = shape.min.add(&block_pos.0.to_f64()); let world_max = shape.max.add(&block_pos.0.to_f64()); let direction = Self::intersects_aabb_with_direction(from, to, world_min, world_max); if direction.is_some() { return (true, direction); } } (false, None) } pub async fn raycast( self: &Arc, start_pos: Vector3, end_pos: Vector3, hit_check: impl AsyncFn(&BlockPos, &Arc) -> bool, ) -> Option<(BlockPos, BlockDirection)> { if start_pos == end_pos { return None; } let adjust = -1.0e-7f64; let to = end_pos.lerp(&start_pos, adjust); let from = start_pos.lerp(&end_pos, adjust); let mut block = BlockPos::floored(from.x, from.y, from.z); let (collision, direction) = self.ray_outline_check(&block, from, to); if let Some(dir) = direction && collision { return Some((block, dir)); } let difference = to.sub(&from); let step = difference.sign(); let delta = Vector3::new( if step.x == 0 { f64::MAX } else { (f64::from(step.x)) / difference.x }, if step.y == 0 { f64::MAX } else { (f64::from(step.y)) / difference.y }, if step.z == 0 { f64::MAX } else { (f64::from(step.z)) / difference.z }, ); let mut next = Vector3::new( delta.x * (if step.x > 0 { 1.0 - (from.x - from.x.floor()) } else { from.x - from.x.floor() }), delta.y * (if step.y > 0 { 1.0 - (from.y - from.y.floor()) } else { from.y - from.y.floor() }), delta.z * (if step.z > 0 { 1.0 - (from.z - from.z.floor()) } else { from.z - from.z.floor() }), ); while next.x <= 1.0 || next.y <= 1.0 || next.z <= 1.0 { let block_direction = match (next.x, next.y, next.z) { (x, y, z) if x < y && x < z => { block.0.x += step.x; next.x += delta.x; if step.x > 0 { BlockDirection::West } else { BlockDirection::East } } (_, y, z) if y < z => { block.0.y += step.y; next.y += delta.y; if step.y > 0 { BlockDirection::Down } else { BlockDirection::Up } } _ => { block.0.z += step.z; next.z += delta.z; if step.z > 0 { BlockDirection::North } else { BlockDirection::South } } }; if hit_check(&block, self).await { let (collision, direction) = self.ray_outline_check(&block, from, to); if collision { if let Some(dir) = direction { return Some((block, dir)); } return Some((block, block_direction)); } } } None } /// Broadcasts a packet to all players who currently have the target chunk loaded. /// This uses highly optimized Chebyshev distance math (Chunk Grid) instead of floating point distance checks. pub fn broadcast_to_chunk(&self, chunk_pos: Vector2, packet: &P) { let players = self.players.load(); let recipients = players.iter().filter(|p| { let center = p.get_entity().chunk_pos.load(); let view_distance = get_view_distance(p).get() as i32; // Chebyshev distance (Minecraft's chunk loading shape) is_within_view_distance(chunk_pos, center, view_distance) }); let recipients_by_version = Self::collect_java_recipients_by_version(recipients); Self::broadcast_java_grouped(packet, recipients_by_version); } pub fn broadcast_to_chunk_editioned_sync( &self, chunk_pos: Vector2, je_packet: &J, be_packet: &B, ) { let players = self.players.load(); let mut java_recipients = Vec::new(); let recipients = players.iter().filter(|p| { let center = p.get_entity().chunk_pos.load(); let view_distance = get_view_distance(p).get() as i32; is_within_view_distance(chunk_pos, center, view_distance) }); for p in recipients { match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Bedrock(be_client) => be_client.try_enqueue_packet(be_packet), } } let recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, recipients_by_version); } /// Broadcasts a packet to chunk watchers, excluding specific players. pub fn broadcast_to_chunk_except( &self, chunk_pos: Vector2, except: &[uuid::Uuid], packet: &P, ) { let players = self.players.load(); let recipients = players.iter().filter(|p| { if except.contains(&p.get_entity().entity_uuid) { return false; } let center = p.get_entity().chunk_pos.load(); let view_distance = get_view_distance(p).get() as i32; is_within_view_distance(chunk_pos, center, view_distance) }); let recipients_by_version = Self::collect_java_recipients_by_version(recipients); Self::broadcast_java_grouped(packet, recipients_by_version); } pub async fn broadcast_to_chunk_except_editioned( &self, chunk_pos: Vector2, except: &[uuid::Uuid], je_packet: &J, be_packet: &B, ) { let players = self.players.load(); let recipients = players.iter().filter(|p| { if except.contains(&p.get_entity().entity_uuid) { return false; } let center = p.get_entity().chunk_pos.load(); let view_distance = get_view_distance(p).get() as i32; is_within_view_distance(chunk_pos, center, view_distance) }); let mut java_recipients = Vec::new(); let mut bedrock_recipients = Vec::new(); for p in recipients { match p.client.as_ref() { ClientPlatform::Java(_) => java_recipients.push(p), ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()), } } let je_recipients_by_version = Self::collect_java_recipients_by_version(java_recipients.into_iter()); Self::broadcast_java_grouped(je_packet, je_recipients_by_version); for recipient in bedrock_recipients { recipient.enqueue_packet(be_packet).await; } } } impl BlockAccessor for World { fn get_block(&self, position: &BlockPos) -> &'static Block { self.get_block_state_id_if_loaded(position) .map_or(&Block::AIR, Block::from_state_id) } fn get_block_state(&self, position: &BlockPos) -> &'static BlockState { self.get_block_state_id_if_loaded(position) .map_or(Block::AIR.default_state, BlockState::from_id) } fn get_block_state_id(&self, position: &BlockPos) -> BlockStateId { self.get_block_state_id_if_loaded(position) .unwrap_or(Block::AIR.default_state.id) } fn get_block_and_state(&self, position: &BlockPos) -> (&'static Block, &'static BlockState) { let id = self .get_block_state_id_if_loaded(position) .unwrap_or(Block::AIR.default_state.id); BlockState::from_id_with_block(id) } } pub struct WorldPortal(pub Arc); // Pure Beauty :cap: impl WorldPortalExt for WorldPortal { fn can_place_at( &self, block: &pumpkin_data::Block, state: &BlockState, block_accessor: &dyn BlockAccessor, block_pos: &BlockPos, ) -> bool { self.0.block_registry.can_place_at( None, None, block_accessor, None, block, state, block_pos, None, None, ) } fn mirror(&self, block: &Block, state_id: BlockStateId, mirror: Mirror) -> &'static BlockState { self.0.block_registry.mirror(block, state_id, mirror) } fn rotate( &self, block: &Block, state_id: BlockStateId, rotation: Rotation, ) -> &'static BlockState { self.0.block_registry.rotate(block, state_id, rotation) } fn spawn_mobs_for_chunk_generation( &self, cache: &mut dyn GenerationCache, biome: &'static Biome, chunk_x: i32, chunk_z: i32, ) { natural_spawner::spawn_mobs_for_chunk_generation(&self.0, cache, biome, chunk_x, chunk_z); } fn spawn_structure_entities(&self, entities: Vec) { let world = self.0.clone(); let Some(server) = world.server.upgrade() else { return; }; server.spawn_task(async move { for nbt in entities { let Some(id) = nbt.get_string("id") else { continue; }; let Some(entity_type) = EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(id)) else { warn!("Unknown structure entity type: {id}"); continue; }; let entity = from_type( entity_type, Vector3::new(0.0, 0.0, 0.0), &world, Uuid::new_v4(), ); entity.get_entity().read_nbt_non_mut(&nbt).await; entity.read_nbt_non_mut(&nbt).await; world.spawn_entity(entity).await; } }); } }