use std::pin::Pin; use std::sync::Weak; use std::sync::atomic::Ordering::Relaxed; use std::time::Duration; use std::{ collections::HashMap, sync::{Arc, atomic::Ordering}, }; pub mod chunker; pub mod explosion; pub mod loot; pub mod portal; pub mod time; use crate::block::RandomTickArgs; use crate::world::loot::LootContextParameters; use crate::{ PLUGIN_MANAGER, block::{ self, registry::BlockRegistry, {OnNeighborUpdateArgs, OnScheduledTickArgs}, }, command::client_suggestions, entity::{Entity, EntityBase, player::Player, r#type::from_type}, error::PumpkinError, net::ClientPlatform, plugin::{ block::block_break::BlockBreakEvent, player::{player_join::PlayerJoinEvent, player_leave::PlayerLeaveEvent}, }, server::Server, }; use crate::{block::BlockEvent, entity::item::ItemEntity}; use border::Worldborder; use bytes::BufMut; use crossbeam::queue::SegQueue; use explosion::Explosion; use pumpkin_config::BasicConfiguration; use pumpkin_data::block_properties::is_air; 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, entity::{EntityStatus, EntityType}, fluid::Fluid, particle::Particle, sound::{Sound, SoundCategory}, world::{RAW, WorldEvent}, }; use pumpkin_data::{BlockDirection, BlockState}; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed}; use pumpkin_protocol::bedrock::client::set_actor_data::{ CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag, entity_data_key, }; use pumpkin_protocol::bedrock::client::start_game::CStartGame; use pumpkin_protocol::bedrock::frame_set::FrameSet; use pumpkin_protocol::java::client::play::CPlayerSpawnPosition; use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata}; use pumpkin_protocol::ser::serializer::Serializer; use pumpkin_protocol::{ BClientPacket, ClientPacket, IdOr, SoundEvent, bedrock::{ client::{ creative_content::{CreativeContent, Group}, gamerules_changed::GameRules, play_status::CPlayStatus, start_game::{Experiments, GamePublishSetting, LevelSettings}, update_artributes::{Attribute, CUpdateAttributes}, }, network_item::NetworkItemDescriptor, server::text::SText, }, codec::{ bedrock_block_pos::NetworkPos, 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}, }; use pumpkin_protocol::{ codec::var_int::VarInt, java::client::play::{ CBlockUpdate, CDisguisedChatMessage, CExplosion, CRespawn, CSetBlockDestroyStage, CWorldEvent, }, }; use pumpkin_util::resource_location::ResourceLocation; use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::{ Difficulty, math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}, }; use pumpkin_util::{ math::{position::chunk_section_from_pos, vector2::Vector2}, random::{RandomImpl, get_seed, xoroshiro128::Xoroshiro}, }; use pumpkin_world::chunk::palette::BlockPalette; use pumpkin_world::inventory::Clearable; use pumpkin_world::world::{GetBlockError, WorldFuture}; use pumpkin_world::{ BlockStateId, CURRENT_BEDROCK_MC_VERSION, biome, block::entities::BlockEntity, chunk::io::Dirtiable, inventory::Inventory, item::ItemStack, world::SimpleWorld, }; use pumpkin_world::{chunk::ChunkData, world::BlockAccessor}; use pumpkin_world::{level::Level, tick::TickPriority}; use pumpkin_world::{world::BlockFlags, world_info::LevelData}; use rand::seq::SliceRandom; use rand::{Rng, rng}; use scoreboard::Scoreboard; use serde::Serialize; use time::LevelTime; use tokio::sync::Mutex; use tokio::sync::RwLock; pub mod border; pub mod bossbar; pub mod custom_bossbar; pub mod natural_spawner; pub mod scoreboard; pub mod weather; use crate::world::natural_spawner::{SpawnState, spawn_for_chunk}; use pumpkin_data::effect::StatusEffect; use pumpkin_world::chunk::ChunkHeightmapType::MotionBlocking; use pumpkin_world::generation::settings::gen_settings_from_dimension; use uuid::Uuid; use weather::Weather; type FlowingFluidProperties = pumpkin_data::fluid::FlowingWaterLikeFluidProperties; impl PumpkinError for GetBlockError { fn is_kick(&self) -> bool { false } fn severity(&self) -> log::Level { log::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 { /// 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: Arc>>>, /// A map of active entities within the world, keyed by their unique UUID. /// This does not include players. pub entities: Arc>>>, /// 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, decrease_block_light_queue: SegQueue<(BlockPos, u8)>, increase_block_light_queue: SegQueue<(BlockPos, u8)>, synced_block_event_queue: Mutex>, /// A map of unsent block changes, keyed by block position. unsent_block_changes: Mutex>, } impl World { #[must_use] pub fn load( level: Arc, level_info: Arc>, dimension: Dimension, block_registry: Arc, server: Weak, ) -> Self { // TODO let generation_settings = gen_settings_from_dimension(&dimension); Self { level, level_info, players: Arc::new(RwLock::new(HashMap::new())), entities: Arc::new(RwLock::new(HashMap::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()), decrease_block_light_queue: SegQueue::new(), increase_block_light_queue: SegQueue::new(), server, } } pub async fn shutdown(&self) { for (uuid, entity) in self.entities.read().await.iter() { self.save_entity(uuid, entity).await; } self.level.shutdown().await; } async fn save_entity(&self, uuid: &uuid::Uuid, entity: &Arc) { // First lets see if the entity was saved on an other chunk, and if the current chunk does not match we remove it // Otherwise we just update the nbt data let base_entity = entity.get_entity(); let current_chunk_coordinate = base_entity.block_pos.load().chunk_position(); let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt).await; if let Some(old_chunk) = base_entity.first_loaded_chunk_position.load() { let old_chunk = old_chunk.to_vec2_i32(); let chunk = self.level.get_entity_chunk(old_chunk).await; let mut chunk = chunk.write().await; chunk.mark_dirty(true); if old_chunk == current_chunk_coordinate { chunk.data.insert(*uuid, nbt); return; } // The chunk has changed, lets remove the entity from the old chunk chunk.data.remove(uuid); } // We did not continue, so lets save data in a new chunk let chunk = self.level.get_entity_chunk(current_chunk_coordinate).await; let mut chunk = chunk.write().await; chunk.data.insert(*uuid, nbt); chunk.mark_dirty(true); } async fn remove_entity_data(&self, entity: &Entity) { let current_chunk_coordinate = entity.block_pos.load().chunk_position(); if let Some(old_chunk) = entity.first_loaded_chunk_position.load() { let old_chunk = old_chunk.to_vec2_i32(); let chunk = self.level.get_entity_chunk(old_chunk).await; let mut chunk = chunk.write().await; chunk.mark_dirty(true); if old_chunk == current_chunk_coordinate { chunk.data.remove(&entity.entity_uuid); drop(chunk); } else { let chunk = self.level.get_entity_chunk(current_chunk_coordinate).await; let mut chunk = chunk.write().await; // The chunk has changed, lets remove the entity from the old chunk chunk.data.remove(&entity.entity_uuid); chunk.mark_dirty(true); } } } pub async fn send_entity_status(&self, entity: &Entity, status: EntityStatus) { // TODO: only nearby self.broadcast_packet_all(&CEntityStatus::new(entity.entity_id, status as i8)) .await; } pub async fn send_remove_mob_effect( &self, entity: &Entity, effect_type: &'static StatusEffect, ) { // TODO: only nearby self.broadcast_packet_all(&CRemoveMobEffect::new( entity.entity_id.into(), VarInt(i32::from(effect_type.id)), )) .await; } pub async fn set_difficulty(&self, difficulty: Difficulty) { let mut level_info = self.level_info.write().await; level_info.difficulty = difficulty; } 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) { let events; // THIS IS IMPORTANT // it prevents deadlocks and also removes the need to wait for a lock when adding a new synced block { let mut queue = self.synced_block_event_queue.lock().await; events = queue.clone(); queue.clear(); }; for event in events { let block = self.get_block(&event.pos).await; // TODO if !self .block_registry .on_synced_block_event(block, self, &event.pos, event.r#type, event.data) .await { continue; } self.broadcast_packet_all(&CBlockEvent::new( event.pos, event.r#type, event.data, VarInt(i32::from(block.id)), )) .await; } } /// 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 async fn broadcast_packet_all(&self, packet: &P) { let current_players = self.players.read().await; for player in current_players.values() { player.client.enqueue_packet(packet).await; } } 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 current_players = self.players.read().await; for player in current_players.values() { match &player.client { ClientPlatform::Java(client) => client.enqueue_packet(je_packet).await, ClientPlatform::Bedrock(client) => client.send_game_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.read().await.values() { 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.clone(), chat_message.message.clone(), 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::text(""), // Not needed since we're injecting the name in the message for custom formatting None, ); recipient.client.enqueue_packet(packet).await; recipient .signature_cache .lock() .await .add_seen_signature(&chat_message.signature.clone().unwrap()); // Unwrap is safe because we check for None in validate_chat_message 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; } /// 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 async fn broadcast_packet_except( &self, except: &[uuid::Uuid], packet: &P, ) { let current_players = self.players.read().await; for (_, player) in current_players.iter().filter(|c| !except.contains(c.0)) { player.client.enqueue_packet(packet).await; } } pub async fn spawn_particle( &self, position: Vector3, offset: Vector3, max_speed: f32, particle_count: i32, particle: Particle, ) { let players = self.players.read().await; for player in players.values() { player .spawn_particle(position, offset, max_speed, particle_count, particle) .await; } } pub async fn play_sound(&self, sound: Sound, category: SoundCategory, position: &Vector3) { self.play_sound_raw(sound as u16, category, position, 1.0, 1.0) .await; } pub async 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) .await; } pub async 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) .await; } pub async fn play_sound_raw( &self, sound_id: u16, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, ) { let seed = rng().random::(); let packet = CSoundEffect::new(IdOr::Id(sound_id), category, position, volume, pitch, seed); self.broadcast_packet_all(&packet).await; } pub async fn play_sound_raw_expect( &self, player: &Player, sound_id: u16, category: SoundCategory, position: &Vector3, volume: f32, pitch: f32, ) { let seed = rng().random::(); let packet = CSoundEffect::new(IdOr::Id(sound_id), category, position, volume, pitch, seed); self.broadcast_packet_except(&[player.gameprofile.id], &packet) .await; } pub async 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).await; } pub async 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) .await; } pub async fn tick(self: &Arc, server: &Server) { let start = tokio::time::Instant::now(); // 1. Block & Environment self.flush_block_updates().await; self.flush_synced_block_events().await; self.tick_environment().await; // 2. Chunks let chunk_start = tokio::time::Instant::now(); self.tick_chunks().await; let chunk_elapsed = chunk_start.elapsed(); // 3. Players let player_start = tokio::time::Instant::now(); let players_to_tick: Vec<_> = self.players.read().await.values().cloned().collect(); let player_count = players_to_tick.len(); for player in &players_to_tick { player.tick(server).await; } let player_elapsed = player_start.elapsed(); // 4. Entities & Collision let entity_start = tokio::time::Instant::now(); let entities_to_tick: Vec<_> = self.entities.read().await.values().cloned().collect(); let entity_count = entities_to_tick.len(); for entity in entities_to_tick { entity.get_entity().age.fetch_add(1, Relaxed); entity.tick(entity.clone(), server).await; for player in &players_to_tick { if player .living_entity .entity .bounding_box .load() .expand(1.0, 0.5, 1.0) .intersects(&entity.get_entity().bounding_box.load()) { entity.on_player_collision(player).await; break; } } } let entity_elapsed = entity_start.elapsed(); self.level.chunk_loading.lock().unwrap().send_change(); // 5. Detailed Slow Tick Logging let total_elapsed = start.elapsed(); if total_elapsed.as_millis() > 50 { log::debug!( "Slow Tick [{}ms]: Chunks: {:?} | Players({}): {:?} | Entities({}): {:?}", total_elapsed.as_millis(), chunk_elapsed, player_count, player_elapsed, entity_count, entity_elapsed, ); } } pub async fn flush_block_updates(&self) { let mut block_state_updates_by_chunk_section = HashMap::new(); for (position, block_state_id) in self.unsent_block_changes.lock().await.drain() { let chunk_section = chunk_section_from_pos(&position); block_state_updates_by_chunk_section .entry(chunk_section) .or_insert(Vec::new()) .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 in block_state_updates_by_chunk_section.values() { if chunk_section.is_empty() { continue; } if chunk_section.len() == 1 { let (block_pos, block_state_id) = chunk_section[0]; self.broadcast_packet_all(&CBlockUpdate::new( block_pos, i32::from(block_state_id).into(), )) .await; } else { self.broadcast_packet_all(&CMultiBlockUpdate::new(chunk_section.clone())) .await; } } } async fn tick_environment(&self) { let mut level_time = self.level_time.lock().await; let (advance_time, advance_weather) = { let lock = self.level_info.read().await; ( 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); if level_time.world_age % 300 != 0 { self.level.level_channel.notify(); } } if level_time.world_age % 300 == 0 { self.level.should_save.store(true, Relaxed); self.level.level_channel.notify(); } let mut weather = self.weather.lock().await; weather.tick_weather(self).await; if self.should_skip_night().await && level_time.is_night() { let time = level_time.time_of_day + 24000; level_time.set_time(time - time % 24000); level_time.send_time(self).await; for player in self.players.read().await.values() { player.wake_up().await; } if weather.weather_cycle_enabled && (weather.raining || weather.thundering) { weather.reset_weather_cycle(self).await; } } else if level_time.world_age % 20 == 0 { level_time.send_time(self).await; } } pub async fn tick_chunks(self: &Arc) { let tick_data = self.level.get_tick_data().await; for scheduled_tick in tick_data.block_ticks { let block = self.get_block(&scheduled_tick.position).await; if let Some(pumpkin_block) = self.block_registry.get_pumpkin_block(block.id) { pumpkin_block .on_scheduled_tick(OnScheduledTickArgs { world: self, block, position: &scheduled_tick.position, }) .await; } } for scheduled_tick in tick_data.fluid_ticks { let fluid = self.get_fluid(&scheduled_tick.position).await; if let Some(pumpkin_fluid) = self.block_registry.get_pumpkin_fluid(fluid.id) { pumpkin_fluid .on_scheduled_tick(self, fluid, &scheduled_tick.position) .await; } } // TODO: Fix this deadlock // TODO: ^ find this deadlock ^ for scheduled_tick in tick_data.random_ticks { let block = self.get_block(&scheduled_tick.position).await; if let Some(pumpkin_block) = self.block_registry.get_pumpkin_block(block.id) { pumpkin_block .random_tick(RandomTickArgs { world: self, block, position: &scheduled_tick.position, }) .await; } } let spawn_entity_clock_start = tokio::time::Instant::now(); let mut spawning_chunks_map = HashMap::new(); // TODO use FixedPlayerDistanceChunkTracker for i in self.players.read().await.values() { let center = i.living_entity.entity.chunk_pos.load(); for dx in -8..=8 { for dy in -8..=8 { // if dx.abs() <= 2 || dy.abs() <= 2 || dx.abs() >= 6 || dy.abs() >= 6 { // this is only for debug, spawning runs too slow // continue; // } let chunk_pos = center.add_raw(dx, dy); if !spawning_chunks_map.contains_key(&chunk_pos) && let Some(chunk) = self.level.try_get_chunk(&chunk_pos) { spawning_chunks_map.entry(chunk_pos).or_insert(chunk); } } } } let mut spawning_chunks: Vec<(Vector2, Arc>)> = spawning_chunks_map.into_iter().collect(); let get_chunks_clock = spawn_entity_clock_start.elapsed(); // log::debug!("spawning chunks size {}", spawning_chunks.len()); let mut spawn_state = SpawnState::new(spawning_chunks.len() as i32, &self.entities, self).await; // TODO store it // TODO gamerule this.spawnEnemies || this.spawnFriendlies let spawn_passives = self.level_time.lock().await.time_of_day % 400 == 0; let spawn_list: Vec<&'static MobCategory> = natural_spawner::get_filtered_spawning_categories( &spawn_state, true, true, spawn_passives, ); // log::debug!("spawning list size {}", spawn_list.len()); spawning_chunks.shuffle(&mut rng()); // TODO i think it can be multithread for (pos, chunk) in &spawning_chunks { self.tick_spawning_chunk(pos, chunk, &spawn_list, &mut spawn_state) .await; } log::trace!( "Spawning entity took {:?}, getting chunks {:?}, spawning chunks: {}, avg {:?} per chunk", spawn_entity_clock_start.elapsed(), get_chunks_clock, spawning_chunks.len(), spawn_entity_clock_start .elapsed() .checked_div(spawning_chunks.len() as u32) .unwrap_or(Duration::new(0, 0)) ); for block_entity in tick_data.block_entities { let world: Arc = self.clone(); block_entity.tick(world).await; } } pub async fn get_fluid_collisions(self: &Arc, bounding_box: BoundingBox) -> Vec { 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).await; if fluid.id != Fluid::EMPTY.id { let height = f64::from(state.height); if height >= bounding_box.min.y { collisions.push(fluid); } } } } } collisions } pub async 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).await; if fluid.id != Fluid::EMPTY.id { let height = f64::from(state.height); if height >= bounding_box.min.y { return true; } } } } } false } // FlowableFluid.getVelocity() pub async fn get_fluid_velocity( &self, pos0: BlockPos, fluid0: &Fluid, state0: &FluidState, ) -> Vector3 { let mut velo = Vector3::default(); for dir in BlockDirection::horizontal() { let mut amplitude = 0.0; let offset = dir.to_offset(); let pos = pos0.offset(offset); let block_state_id = self.get_block_state_id(&pos).await; let fluid = Fluid::from_state_id(block_state_id).unwrap_or(&Fluid::EMPTY); if fluid.id == Fluid::EMPTY.id { let block = Block::from_state_id(block_state_id); let block_state = BlockState::from_id(block_state_id); let blocks_movement = block_state.is_solid() && block != &Block::COBWEB && block != &Block::BAMBOO_SAPLING; if !blocks_movement { let down_pos = pos.down(); let (down_fluid, down_state) = self.get_fluid_and_fluid_state(&down_pos).await; if down_fluid.id == fluid0.id { amplitude = f64::from(state0.height - down_state.height) + 0.888_888_9; } } } else { if fluid.id != fluid0.id { continue; } //let state = fluid.get_state(block_state_id); let state = fluid.states[0].clone(); amplitude = f64::from(state0.height - state.height); } if amplitude == 0.0 { continue; } velo.x += f64::from(offset.x) * amplitude; velo.z += f64::from(offset.z) * amplitude; } // TODO: FALLING if state0.falling { for dir in BlockDirection::horizontal() { let pos = pos0.offset(dir.to_offset()); if self.is_flow_blocked(fluid0.id, pos, dir).await || self.is_flow_blocked(fluid0.id, pos.up(), dir).await { if velo.length_squared() != 0.0 { velo = velo.normalize(); } velo.y -= 6.0; break; } } } if velo.length_squared() == 0.0 { velo } else { velo.normalize() } } // FlowableFluid.isFlowBlocked() async fn is_flow_blocked( &self, fluid0_id: u16, pos: BlockPos, direction: BlockDirection, ) -> bool { let id = self.get_block_state_id(&pos).await; let fluid = Fluid::from_state_id(id).unwrap_or(&Fluid::EMPTY); if 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().unwrap() { 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.to_bounding_box()); } } inside } pub fn check_collision( bounding_box: &BoundingBox, pos: BlockPos, state: &BlockState, use_collision_shape: bool, mut using_collision_shape: F, ) -> bool where F: FnMut(&BoundingBox), { let mut collided = false; if !state.is_air() && state.is_solid() && !state.collision_shapes.is_empty() { for shape in state.get_block_collision_shapes() { let collision_shape = shape.at_pos(pos); if collision_shape.intersects(bounding_box) { collided = true; if !use_collision_shape { break; } using_collision_shape(&collision_shape.to_bounding_box()); } } } collided } // For adjusting movement pub async fn get_block_collisions( self: &Arc, bounding_box: BoundingBox, ) -> (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).await; if state.is_air() { continue; } let collided = Self::check_collision( &bounding_box, pos, state, true, |collision_shape: &BoundingBox| { collisions.push(*collision_shape); }, ); if collided { positions.push((collisions.len(), pos)); } } (collisions, positions) } pub async 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).await; let collided = Self::check_collision(&bounding_box, pos, state, false, |_| ()); if collided { return false; } } true } pub async fn tick_spawning_chunk( self: &Arc, chunk_pos: &Vector2, chunk: &Arc>, spawn_list: &Vec<&'static MobCategory>, spawn_state: &mut SpawnState, ) { // this.level.tickThunder(chunk); //TODO check in simulation distance let weather = self.weather.lock().await; if weather.raining && weather.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.read().await.heightmap.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()).await != &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) spawn_for_chunk(self, chunk_pos, chunk, spawn_state, spawn_list).await; } /// Gets the y position of the first non air block from the top down pub async fn get_top_block(&self, position: Vector2) -> i32 { for y in (self.dimension.min_y..self.dimension.height).rev() { let pos = BlockPos::new(position.x, y, position.y); let block = self.get_block_state(&pos).await; if block.is_air() { continue; } return y; } self.dimension.min_y } #[allow(clippy::too_many_lines)] pub async fn spawn_bedrock_player( &self, base_config: &BasicConfiguration, player: Arc, server: &Server, ) { let level_info = server.level_info.read().await; 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.living_entity.entity.yaw.load(); //info.spawn_angle; let pitch = player.living_entity.entity.pitch.load(); (position, yaw, pitch) } else { let info = &self.level_info.read().await; let spawn_position = Vector2::new(info.spawn_x, info.spawn_z); let pos_y = self.get_top_block(spawn_position).await + 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) }; // 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: NetworkPos(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: base_config.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_id: String::new(), world_id: String::new(), scenario_id: String::new(), owner_id: String::new(), }; drop(level_info); drop(weather); let client = player.client.bedrock(); 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, // TODO Make this description better! // This gets send from the client to mojang for telemetry 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(), // TODO The client needs extra biome data for this enable_clientside_generation: false, blocknetwork_ids_are_hashed: false, server_auth_sounds: false, }) .await; chunker::update_position(&player).await; client .send_game_packet(&CreativeContent { groups: &[Group { creative_category: 1, name: String::new(), icon_item: NetworkItemDescriptor::default(), }], entries: &[], }) .await; { let mut abilities = player.abilities.lock().await; abilities.set_for_gamemode(player.gamemode.load()); }; let mut metadata = EntityMetadata::default(); metadata.set(entity_data_key::WIDTH, MetadataValue::Float(0.6)); metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8)); // This is super important, oterwise the client will float by default metadata.set_flag(entity_data_flag::HAS_GRAVITY); // Prevents the client from showing air buddles on hud even when not in water metadata.set_flag(entity_data_flag::BREATHING); 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 mut frame_set = FrameSet::default(); // https://github.com/pmmp/PocketMine-MP/blob/0b6d8f8cb2aaa05ffad0b6386bd88d73ef54b395/src/entity/AttributeFactory.php#L34 client .write_game_packet_to_set( &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), }, &mut frame_set, ) .await; client .write_game_packet_to_set(&CPlayStatus::PlayerSpawn, &mut frame_set) .await; client.send_frame_set(frame_set, 0x84).await; } #[expect(clippy::too_many_lines)] pub async fn spawn_java_player( &self, base_config: &BasicConfiguration, player: Arc, server: &Server, ) { 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(); log::debug!( "spawning player {}, entity id {}", player.gameprofile.name, entity_id ); let client = player.client.java(); // Send the login packet for our new player client .send_packet_now(&CLogin::new( entity_id, base_config.hardcore, &dimensions, base_config.max_players.try_into().unwrap(), base_config.view_distance.get().into(), // TODO: view distance base_config.simulation_distance.get().into(), // TODO: sim view dinstance false, true, false, (self.dimension.id).into(), ResourceLocation::from(self.dimension.minecraft_name), 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(), // 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().await; // Difficulty of the world player.send_difficulty_update().await; { let command_dispatcher = server.command_dispatcher.read().await; client_suggestions::send_c_commands_packet(&player, &command_dispatcher).await; }; // Spawn in initial chunks // This is made before the player teleport so that the player doesn't glitch out when spawning chunker::update_position(&player).await; // Teleport let (position, yaw, pitch) = if player.has_played_before.load(Ordering::Relaxed) { let position = player.position(); let yaw = player.living_entity.entity.yaw.load(); //info.spawn_angle; let pitch = player.living_entity.entity.pitch.load(); (position, yaw, pitch) } else { let info = &self.level_info.read().await; let spawn_position = Vector2::new(info.spawn_x, info.spawn_z); let pos_y = self.get_top_block(spawn_position).await + 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) }; let velocity = player.living_entity.entity.velocity.load(); log::debug!("Sending player teleport to {}", player.gameprofile.name); player.request_teleport(position, yaw, pitch).await; player.living_entity.entity.last_pos.store(position); let gameprofile = &player.gameprofile; // Firstly, send an info update to our new player, so they can see their skin // and also send their info to everyone else. log::debug!("Broadcasting player info for {}", player.gameprofile.name); self.broadcast_packet_all(&CPlayerInfoUpdate::new( (PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_GAME_MODE | PlayerInfoFlags::UPDATE_LISTED) .bits(), &[pumpkin_protocol::java::client::play::Player { uuid: gameprofile.id, actions: &[ PlayerAction::AddPlayer { name: &gameprofile.name, properties: &gameprofile.properties, }, PlayerAction::UpdateGameMode(VarInt(gamemode as i32)), PlayerAction::UpdateListed(true), ], }], )) .await; let current_players = self.players.read().await; // Here, we send all the infos of players who already joined. { let mut current_player_data = Vec::new(); for (_, player) in current_players .iter() .filter(|(c, _)| **c != player.gameprofile.id) { let chat_session = player.chat_session.lock().await; let mut player_actions = vec![ PlayerAction::AddPlayer { name: &player.gameprofile.name, properties: &player.gameprofile.properties, }, PlayerAction::UpdateListed(true), ]; 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(), }))); } drop(chat_session); current_player_data.push((&player.gameprofile.id, player_actions)); } let mut action_flags = PlayerInfoFlags::ADD_PLAYER | PlayerInfoFlags::UPDATE_LISTED; 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::>(); log::debug!("Sending player info to {}", player.gameprofile.name); client .enqueue_packet(&CPlayerInfoUpdate::new(action_flags.bits(), &entries)) .await; }; let gameprofile = &player.gameprofile; log::debug!("Broadcasting player spawn for {}", player.gameprofile.name); // Spawn the player for every client. self.broadcast_packet_except( &[player.gameprofile.id], &CSpawnEntity::new( entity_id.into(), gameprofile.id, i32::from(EntityType::PLAYER.id).into(), position, pitch, yaw, yaw, 0.into(), velocity, ), ) .await; // Spawn players for our client. let id = player.gameprofile.id; for (_, existing_player) in current_players.iter().filter(|c| c.0 != &id) { let entity = &existing_player.living_entity.entity; let pos = entity.pos.load(); let gameprofile = &existing_player.gameprofile; log::debug!("Sending player entities to {}", player.gameprofile.name); client .enqueue_packet(&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(), )) .await; { let config = existing_player.config.read().await; let mut buf = Vec::new(); { let meta = Metadata::new( TrackedData::DATA_PLAYER_MODE_CUSTOMIZATION_ID, MetaDataType::Byte, config.skin_parts, ); let mut serializer_buf = Vec::new(); let mut serializer = Serializer::new(&mut serializer_buf); meta.serialize(&mut serializer).unwrap(); buf.extend(serializer_buf); }; 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().await; 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 log::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.read().await; ( 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().await.get_player_bars(&player.gameprofile.id) { // for bossbar in bossbars { // player.send_bossbar(bossbar).await; // } // } player.has_played_before.store(true, Ordering::Relaxed); player .on_screen_handler_opened(player.player_screen_handler.clone()) .await; player.send_active_effects().await; self.send_player_equipment(&player).await; } 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(); self.broadcast_packet_except( &[from.get_entity().entity_uuid], &CSetEquipment::new(from.entity_id().into(), equipment), ) .await; } pub async fn send_world_info( &self, player: &Arc, position: Vector3, yaw: f32, pitch: f32, ) { if let ClientPlatform::Java(client) = &player.client { 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.living_entity.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), ), ) .await; player.send_client_information().await; 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 }; let sound = IdOr::::Id(Sound::EntityGenericExplode as u16); for player in self.players.read().await.values() { 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; } } pub async fn respawn_player(&self, player: &Arc, alive: bool) { let last_pos = player.living_entity.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); // TODO: switch world in player entity to new world player .client .enqueue_packet(&CRespawn::new( (self.dimension.id).into(), ResourceLocation::from(self.dimension.minecraft_name), biome::hash_seed(self.level.seed.0), // seed 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), self.sea_level.into(), data_kept, )) .await; player.living_entity.reset_state().await; log::debug!("Sending player abilities to {}", player.gameprofile.name); player.send_abilities_update().await; player.send_permission_lvl_update().await; player.hunger_manager.restart(); let info = self.level_info.read().await; if !info.game_rules.keep_inventory { player.set_experience(0, 0.0, 0).await; player.inventory.clear().await; } // Teleport let (position, yaw, pitch) = if let Some(respawn) = player.get_respawn_point().await { respawn } else { let top = self .get_top_block(Vector2::new(info.spawn_x, info.spawn_z)) .await; ( Vector3::new( f64::from(info.spawn_x) + 0.5, (top + 1).into(), f64::from(info.spawn_z) + 0.5, ), info.spawn_yaw, info.spawn_pitch, ) }; log::debug!("Sending player teleport to {}", player.gameprofile.name); player.request_teleport(position, yaw, pitch).await; player.living_entity.entity.last_pos.store(position); // TODO: difficulty, exp bar, status effect self.send_world_info(player, position, yaw, pitch).await; } /// Returns true if enough players are sleeping and we should skip the night. pub async fn should_skip_night(&self) -> bool { let players = self.players.read().await; let player_count = players.len(); let sleeping_player_count = players .values() .filter(|player| { player .sleeping_since .load() .is_some_and(|since| since >= 100) }) .count(); drop(players); // TODO: sleep ratio sleeping_player_count == player_count && player_count != 0 } // NOTE: This function doesn't actually await on anything, it just spawns two tokio tasks /// IMPORTANT: Chunks have to be non-empty #[expect(clippy::too_many_lines)] 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() => { log::debug!("Canceling player packet processing"); None }, recv_result = entity_receiver.recv() => { recv_result } }; let Some((chunk, _first_load)) = recv_result else { break; }; let tmp_chunk = chunk.read().await; let position = Vector2::new(tmp_chunk.x, tmp_chunk.z); drop(tmp_chunk); let chunk = if level.is_chunk_watched(&position) { chunk } else { log::trace!( "Received chunk {:?}, but it is no longer watched... cleaning", &position ); let mut ids = Vec::new(); // Remove all the entities from the world let mut entities = world.entities.write().await; for (uuid, entity_nbt) in &chunk.read().await.data { let Some(id) = entity_nbt.get_string("id") else { log::warn!("Entity has no ID"); continue; }; let Some(entity_type) = EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(id)) else { log::warn!("Entity has no valid Entity Type {id}"); continue; }; // Pos is zero since it will read from nbt let entity = from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, *uuid) .await; entity.read_nbt_non_mut(entity_nbt).await; let base_entity = entity.get_entity(); entities.remove(&base_entity.entity_uuid); ids.push(VarInt(base_entity.entity_id)); let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt).await; if let Some(old_chunk) = base_entity.first_loaded_chunk_position.load() { let old_chunk = old_chunk.to_vec2_i32(); let chunk = world.level.get_entity_chunk(old_chunk).await; let mut chunk = chunk.write().await; chunk.mark_dirty(true); let base_entity = entity.get_entity(); let current_chunk_coordinate = base_entity.block_pos.load().chunk_position(); if old_chunk == current_chunk_coordinate { chunk.data.insert(*uuid, nbt); return; } // The chunk has changed, lets remove the entity from the old chunk chunk.data.remove(uuid); } let mut chunk = chunk.write().await; chunk.data.insert(*uuid, nbt); chunk.mark_dirty(true); } if !ids.is_empty() { player .client .enqueue_packet(&CRemoveEntities::new(&ids)) .await; } level.clean_entity_chunk(&position).await; continue 'main; }; let entity_chunk = chunk.read().await; // Add all new Entities to the world let mut current_entities = world.entities.write().await; for (uuid, entity_nbt) in &entity_chunk.data { let Some(id) = entity_nbt.get_string("id") else { log::warn!("Entity has no ID"); continue; }; let Some(entity_type) = EntityType::from_name(id.strip_prefix("minecraft:").unwrap_or(id)) else { log::warn!("Entity has no valid Entity Type {id}"); continue; }; // Pos is zero since it will read from nbt let entity = from_type(entity_type, Vector3::new(0.0, 0.0, 0.0), &world, *uuid).await; entity.read_nbt_non_mut(entity_nbt).await; let base_entity = entity.get_entity(); player .client .enqueue_packet(&base_entity.create_spawn_packet()) .await; entity.init_data_tracker().await; current_entities.insert(base_entity.entity_uuid, entity); } } #[cfg(debug_assertions)] log::debug!("Chunks queued after {}ms", inst.elapsed().as_millis()); }); } /// Gets a `Player` by an entity id pub async fn get_player_by_id(&self, id: i32) -> Option> { for player in self.players.read().await.values() { if player.entity_id() == id { return Some(player.clone()); } } None } /// Gets an entity by an entity id pub async fn get_entity_by_id(&self, id: i32) -> Option> { for entity in self.entities.read().await.values() { if entity.get_entity().entity_id == id { return Some(entity.clone()); } } for player in self.players.read().await.values() { if player.get_entity().entity_id == id { return Some(player.clone() as Arc); } } None } /// Gets a `Player` by a username pub async fn get_player_by_name(&self, name: &str) -> Option> { for player in self.players.read().await.values() { if player.gameprofile.name.eq_ignore_ascii_case(name) { return Some(player.clone()); } } None } pub async fn get_entities_at_box(&self, aabb: &BoundingBox) -> Vec> { let entities_guard = self.entities.read().await; entities_guard .values() .filter(|entity| entity.get_entity().bounding_box.load().intersects(aabb)) .cloned() .collect() } pub async fn get_players_at_box(&self, aabb: &BoundingBox) -> Vec> { let players_guard = self.players.read().await; players_guard .values() .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 async fn get_player_by_uuid(&self, id: uuid::Uuid) -> Option> { self.players.read().await.get(&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 async fn get_players_by_pos(&self, position: BlockPos) -> HashMap> { self.players .read() .await .iter() .filter_map(|(uuid, player)| { let player_block_pos = player.living_entity.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(|| (*uuid, 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 async fn get_nearby_players( &self, pos: Vector3, radius: f64, ) -> HashMap> { let radius_squared = radius.powi(2); self.players .read() .await .iter() .filter_map(|(id, player)| { let player_pos = player.living_entity.entity.pos.load(); (player_pos.squared_distance_to_vec(pos) <= radius_squared) .then(|| (*id, player.clone())) }) .collect() } pub async fn get_nearby_entities( &self, pos: Vector3, radius: f64, ) -> HashMap> { let radius_squared = radius.powi(2); self.entities .read() .await .iter() .filter_map(|(id, entity)| { let entity_pos = entity.get_entity().pos.load(); (entity_pos.squared_distance_to_vec(pos) <= radius_squared) .then(|| (*id, entity.clone())) }) .collect() } pub async fn get_closest_player(&self, pos: Vector3, radius: f64) -> Option> { let players = self.get_nearby_players(pos, radius).await; players .iter() .min_by(|a, b| { a.1.living_entity .entity .pos .load() .squared_distance_to_vec(pos) .partial_cmp( &b.1.living_entity .entity .pos .load() .squared_distance_to_vec(pos), ) .unwrap() }) .map(|p| p.1.clone()) } /// 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 async 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).await; // 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 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 /// /// * `uuid`: The unique UUID of the player to add. /// * `player`: An `Arc` reference to the player object. pub async fn add_player(&self, uuid: uuid::Uuid, player: Arc) -> Result<(), String> { self.players.write().await.insert(uuid, player.clone()); let current_players = self.players.clone(); player.clone().spawn_task(async move { let msg_comp = TextComponent::translate( "multiplayer.player.joined", [TextComponent::text(player.gameprofile.name.clone())], ) .color_named(NamedColor::Yellow); let event = PlayerJoinEvent::new(player.clone(), msg_comp); let event = PLUGIN_MANAGER.fire(event).await; if !event.cancelled { for player in current_players.read().await.values() { player.send_system_message(&event.join_message).await; } log::info!("{}", event.join_message.to_pretty_console()); } }); 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) { if self .players .write() .await .remove(&player.gameprofile.id) .is_none() { return; } let uuid = player.gameprofile.id; self.broadcast_packet_all(&CRemovePlayerInfo::new(&[uuid])) .await; self.broadcast_packet_all(&CRemoveEntities::new(&[player.entity_id().into()])) .await; if fire_event { let msg_comp = TextComponent::translate( "multiplayer.player.left", [TextComponent::text(player.gameprofile.name.clone())], ) .color_named(NamedColor::Yellow); let event = PlayerLeaveEvent::new(player.clone(), msg_comp); let event = PLUGIN_MANAGER.fire(event).await; if !event.cancelled { let players = self.players.read().await; for player in players.values() { player.send_system_message(&event.leave_message).await; } drop(players); log::info!("{}", event.leave_message.to_pretty_console()); } } } /// Adds an entity to the world. pub async fn spawn_entity(&self, entity: Arc) { let base_entity = entity.get_entity(); self.broadcast_packet_all(&base_entity.create_spawn_packet()) .await; entity.init_data_tracker().await; let chunk_coordinate = base_entity.block_pos.load().chunk_position(); let chunk = self.level.get_entity_chunk(chunk_coordinate).await; { let mut chunk = chunk.write().await; let mut nbt = NbtCompound::new(); entity.write_nbt(&mut nbt).await; chunk.data.insert(base_entity.entity_uuid, nbt); chunk.mark_dirty(true); }; let mut current_entities = self.entities.write().await; current_entities.insert(base_entity.entity_uuid, entity); } pub async fn remove_entity(&self, entity: &Entity) { self.entities.write().await.remove(&entity.entity_uuid); self.broadcast_packet_all(&CRemoveEntities::new(&[entity.entity_id.into()])) .await; self.remove_entity_data(entity).await; } pub async fn set_block_breaking(&self, from: &Entity, location: BlockPos, progress: i32) { self.broadcast_packet_except( &[from.entity_uuid], &CSetBlockDestroyStage::new(from.entity_id.into(), location, progress as i8), ) .await; } pub fn queue_block_light_decrease(self: &Arc, pos: BlockPos, level: u8) { self.decrease_block_light_queue.push((pos, level)); } pub fn queue_block_light_increase(self: &Arc, pos: BlockPos, level: u8) { self.increase_block_light_queue.push((pos, level)); } pub async fn perform_block_light_updates(self: &Arc) -> i32 { let mut updates = 0; updates += self.perform_block_light_decrease_updates().await; updates += self.perform_block_light_increase_updates().await; updates } async fn perform_block_light_decrease_updates(self: &Arc) -> i32 { let mut updates = 0; while let Some((pos, expected_light)) = self.decrease_block_light_queue.pop() { self.propagate_block_light_decrease(&pos, expected_light) .await; updates += 1; } updates } async fn perform_block_light_increase_updates(self: &Arc) -> i32 { let mut updates = 0; while let Some((pos, expected_light)) = self.increase_block_light_queue.pop() { self.propagate_block_light_increase(&pos, expected_light) .await; updates += 1; } updates } async fn propagate_block_light_increase(self: &Arc, pos: &BlockPos, light_level: u8) { for dir in BlockDirection::all() { let neighbor_pos = pos.offset(dir.to_offset()); if let Some(neighbor_light) = self.get_block_light_level(&neighbor_pos).await { let neighbor_state = self.get_block_state(&neighbor_pos).await; let new_light = light_level.saturating_sub(neighbor_state.opacity.max(1)); if new_light > neighbor_light { // TODO: Add shape checking for non-trivial blocks self.set_block_light_level(&neighbor_pos, new_light) .await .unwrap(); if new_light > 1 { self.queue_block_light_increase(neighbor_pos, new_light); } } } } } async fn propagate_block_light_decrease( self: &Arc, pos: &BlockPos, removed_light_level: u8, ) { for dir in BlockDirection::all() { let neighbor_pos = pos.offset(dir.to_offset()); if let Some(neighbor_light) = self.get_block_light_level(&neighbor_pos).await { if neighbor_light == 0 { continue; // Skip if already 0 } let neighbor_state = self.get_block_state(&neighbor_pos).await; let opacity = neighbor_state.opacity.max(1); let expected_from_removed_source = removed_light_level.saturating_sub(opacity); if neighbor_light <= expected_from_removed_source { let neighbor_luminance = neighbor_state.luminance; self.set_block_light_level(&neighbor_pos, 0).await.unwrap(); if neighbor_luminance == 0 { self.queue_block_light_decrease(neighbor_pos, neighbor_light); } else { self.set_block_light_level(&neighbor_pos, neighbor_luminance) .await .unwrap(); self.queue_block_light_increase(neighbor_pos, neighbor_luminance); } } else { self.queue_block_light_increase(neighbor_pos, neighbor_light); } } } } pub async fn check_block_light_updates(self: &Arc, pos: BlockPos) { let current_light = self.get_block_light_level(&pos).await.unwrap_or(0); let block_state = self.get_block_state(&pos).await; let expected_light = block_state.luminance; if expected_light < current_light { self.set_block_light_level(&pos, 0).await.unwrap(); self.queue_block_light_decrease(pos, current_light); } if expected_light > 0 { self.set_block_light_level(&pos, expected_light) .await .unwrap(); self.queue_block_light_increase(pos, expected_light); } //TODO check sky light updates self.check_neighbors_light_updates(pos, current_light).await; } pub async fn check_neighbors_light_updates(self: &Arc, pos: BlockPos, current_light: u8) { for dir in BlockDirection::all() { let neighbor_pos = pos.offset(dir.to_offset()); if let Some(neighbor_light) = self.get_block_light_level(&neighbor_pos).await && neighbor_light > current_light + 1 { self.queue_block_light_increase(neighbor_pos, neighbor_light); } } // TODO check sky light updates } pub async fn get_block_light_level(&self, position: &BlockPos) -> Option { let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); let chunk = self.level.get_chunk(chunk_coordinate).await; let Ok(chunk) = tokio::time::timeout(std::time::Duration::from_secs(1), chunk.read()).await else { panic!("Timed out while waiting to acquire chunk read lock") }; let section_index = (relative.y - chunk.section.min_y) as usize / BlockPalette::SIZE; // +1 since block light has 1 section padding on both top and bottom if section_index + 1 >= chunk.light_engine.block_light.len() { return None; } Some(chunk.light_engine.block_light[section_index + 1].get( relative.x as usize, (relative.y - chunk.section.min_y) as usize % BlockPalette::SIZE, relative.z as usize, )) } pub async fn set_block_light_level( &self, position: &BlockPos, light_level: u8, ) -> Result<(), String> { let (chunk_coordinate, relative) = position.chunk_and_chunk_relative_position(); let chunk = self.level.get_chunk(chunk_coordinate).await; let Ok(mut chunk) = tokio::time::timeout(std::time::Duration::from_secs(1), chunk.write()).await else { panic!("Timed out while waiting to acquire chunk write lock") }; let section_index = (relative.y - chunk.section.min_y) as usize / BlockPalette::SIZE; if section_index >= chunk.light_engine.block_light.len() { return Err("Invalid section index".to_string()); } let relative_y = (relative.y - chunk.section.min_y) as usize % BlockPalette::SIZE; chunk.light_engine.block_light[section_index + 1].set( relative.x as usize, relative_y, relative.z as usize, light_level, ); chunk.mark_dirty(true); Ok(()) } /// 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 chunk = self.level.get_chunk(chunk_coordinate).await; let Ok(mut chunk) = tokio::time::timeout(std::time::Duration::from_secs(1), chunk.write()).await else { panic!("Timed out while waiting to acquire chunk write lock") }; let replaced_block_state_id = chunk.section.set_block_absolute_y( relative.x as usize, relative.y, relative.z as usize, block_state_id, ); if replaced_block_state_id == block_state_id { return block_state_id; } chunk.mark_dirty(true); self.unsent_block_changes .lock() .await .insert(*position, block_state_id); drop(chunk); 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).await { let world: Arc = self.clone(); entity.on_block_replaced(world, *position).await; self.remove_block_entity(position).await; } // 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).await; 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).await == 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; } } self.check_block_light_updates(*position).await; self.perform_block_light_updates().await; replaced_block_state_id } pub async fn schedule_block_tick( &self, block: &Block, block_pos: BlockPos, delay: u8, priority: TickPriority, ) { self.level .schedule_block_tick(block, block_pos, delay, priority) .await; } pub async fn schedule_fluid_tick( &self, fluid: &Fluid, block_pos: BlockPos, delay: u8, priority: TickPriority, ) { self.level .schedule_fluid_tick(fluid, block_pos, delay, priority) .await; } pub async fn is_block_tick_scheduled(&self, block_pos: &BlockPos, block: &Block) -> bool { self.level.is_block_tick_scheduled(block_pos, block).await } pub async fn is_fluid_tick_scheduled(&self, block_pos: &BlockPos, fluid: &Fluid) -> bool { self.level.is_fluid_tick_scheduled(block_pos, fluid).await } pub async fn break_block( self: &Arc, position: &BlockPos, cause: Option>, flags: BlockFlags, ) { let (broken_block, broken_block_state) = self.get_block_and_state_id(position).await; let event = BlockBreakEvent::new(cause.clone(), broken_block, *position, 0, false); let event = PLUGIN_MANAGER.fire::(event).await; if !event.cancelled { 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 { 0 }; let broken_state_id = self.set_block_state(position, new_state_id, flags).await; if Block::from_state_id(broken_state_id) != &Block::FIRE { let particles_packet = CWorldEvent::new( WorldEvent::BlockBroken as i32, *position, broken_state_id.into(), false, ); match cause { Some(player) => { self.broadcast_packet_except(&[player.gameprofile.id], &particles_packet) .await; } None => self.broadcast_packet_all(&particles_packet).await, } } if !flags.contains(BlockFlags::SKIP_DROPS) { let params = LootContextParameters { block_state: Some(BlockState::from_id(broken_state_id)), ..Default::default() }; block::drop_loot(self, broken_block, position, true, params).await; } } } pub async fn drop_stack(self: &Arc, pos: &BlockPos, stack: ItemStack) { let height = EntityType::ITEM.dimension[1] / 2.0; let pos = Vector3::new( f64::from(pos.0.x) + 0.5 + rand::rng().random_range(-0.25..0.25), f64::from(pos.0.y) + 0.5 + rand::rng().random_range(-0.25..0.25) - f64::from(height), f64::from(pos.0.z) + 0.5 + rand::rng().random_range(-0.25..0.25), ); let entity = Entity::new(self.clone(), pos, &EntityType::ITEM); let item_entity = Arc::new(ItemEntity::new(entity, stack).await); 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 = x.floor() + rng.next_f64() * spawn_area + half_width; let y = y.floor() + rng.next_f64() * spawn_area; let z = z.floor() + rng.next_f64() * spawn_area + 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).await); self.spawn_entity(entity).await; } } /* End ItemScatterer.java */ pub async fn sync_world_event(&self, world_event: WorldEvent, position: BlockPos, data: i32) { self.broadcast_packet_all(&CWorldEvent::new(world_event as i32, position, data, false)) .await; } #[must_use] pub fn is_valid(dest: Vector3) -> bool { Self::is_valid_horizontally(dest) && Self::is_valid_vertically(dest.y) } #[must_use] pub fn is_valid_horizontally(dest: Vector3) -> bool { (-30_000_000.0..=30_000_000.0).contains(&dest.x) && (-30_000_000.0..=30_000_000.0).contains(&dest.z) } #[must_use] pub fn is_valid_vertically(y: f64) -> bool { (-20_000_000.0..=20_000_000.0).contains(&y) } /// Gets a `Block` from the block registry. Returns `Block::AIR` if the block was not found. pub async fn get_block(&self, position: &BlockPos) -> &'static Block { let id = self.get_block_state_id(position).await; Block::from_state_id(id) } pub async fn get_fluid(&self, position: &BlockPos) -> &'static pumpkin_data::fluid::Fluid { let id = self.get_block_state_id(position).await; let fluid = Fluid::from_state_id(id).ok_or(&Fluid::EMPTY); if let Ok(fluid) = fluid { return fluid; } 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 async 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).await; let block = Block::from_state_id(id); let fluid = Fluid::from_state_id(id) .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 async fn get_fluid_and_fluid_state(&self, position: &BlockPos) -> (Fluid, FluidState) { let id = self.get_block_state_id(position).await; let Some(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 fluid = Fluid::FLOWING_WATER; let state = fluid.states[0].clone(); return (fluid, state); } break; } } } let fluid = Fluid::EMPTY; let state = fluid.states[0].clone(); return (fluid, state); }; //let state = fluid.get_state(id); let state = fluid.states[0].clone(); (fluid.clone(), state) } pub async fn get_block_state_id(&self, position: &BlockPos) -> BlockStateId { self.level.get_block_state(position).await.0 } /// Gets the `BlockState` from the block registry. Returns Air if the block state was not found. pub async fn get_block_state(&self, position: &BlockPos) -> &'static BlockState { let id = self.get_block_state_id(position).await; BlockState::from_id(id) } /// Gets the Block + Block state from the Block Registry, Returns Air if the Block state has not been found pub async fn get_block_and_state( &self, position: &BlockPos, ) -> (&'static Block, &'static BlockState) { let id = self.get_block_state_id(position).await; 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 async fn get_block_and_state_id(&self, position: &BlockPos) -> (&'static Block, u16) { let id = self.get_block_state_id(position).await; (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).await; 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).await; 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).await; 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 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).await; 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).await; 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; } } } pub async fn get_block_entity(&self, block_pos: &BlockPos) -> Option> { let chunk = self.level.get_chunk(block_pos.chunk_position()).await; let chunk: tokio::sync::RwLockReadGuard = chunk.read().await; chunk.block_entities.get(block_pos).cloned() } pub async fn add_block_entity(&self, block_entity: Arc) { let block_pos = block_entity.get_position(); let chunk = self.level.get_chunk(block_pos.chunk_position()).await; let mut chunk: tokio::sync::RwLockWriteGuard = chunk.write().await; 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_packet_all(&CBlockEntityData::new( block_entity.get_position(), VarInt(block_entity.get_id() as i32), bytes.into_boxed_slice(), )) .await; } chunk.block_entities.insert(block_pos, block_entity); chunk.mark_dirty(true); } pub async fn remove_block_entity(&self, block_pos: &BlockPos) { let chunk = self.level.get_chunk(block_pos.chunk_position()).await; let mut chunk: tokio::sync::RwLockWriteGuard = chunk.write().await; chunk.block_entities.remove(block_pos); chunk.mark_dirty(true); } pub async fn update_block_entity(&self, block_entity: &Arc) { let block_pos = block_entity.get_position(); let chunk = self.level.get_chunk(block_pos.chunk_position()).await; let mut chunk: tokio::sync::RwLockWriteGuard = chunk.write().await; 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_packet_all(&CBlockEntityData::new( block_entity.get_position(), VarInt(block_entity.get_id() as i32), bytes.into_boxed_slice(), )) .await; } 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, } } async fn ray_outline_check( self: &Arc, block_pos: &BlockPos, from: Vector3, to: Vector3, ) -> (bool, Option) { let state = self.get_block_state(block_pos).await; let Some(bounding_boxes) = state.get_block_outline_shapes() else { return (false, None); }; if bounding_boxes.is_empty() { return (true, None); } 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).await; 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).await; if collision { if let Some(dir) = direction { return Some((block, dir)); } return Some((block, block_direction)); } } } None } } impl pumpkin_world::world::SimpleWorld for World { fn set_block_state( self: Arc, position: &BlockPos, block_state_id: BlockStateId, flags: BlockFlags, ) -> WorldFuture<'_, BlockStateId> { Box::pin(async move { Self::set_block_state(&self, position, block_state_id, flags).await }) } fn update_neighbor<'a>( self: Arc, neighbor_block_pos: &'a BlockPos, source_block: &'a pumpkin_data::Block, ) -> WorldFuture<'a, ()> { Box::pin(async move { Self::update_neighbor(&self, neighbor_block_pos, source_block).await; }) } fn update_neighbors( self: Arc, block_pos: &BlockPos, except: Option, ) -> WorldFuture<'_, ()> { Box::pin(async move { Self::update_neighbors(&self, block_pos, except).await; }) } fn is_space_empty(&self, bounding_box: BoundingBox) -> WorldFuture<'_, bool> { Box::pin(async move { self.is_space_empty(bounding_box).await }) } fn add_synced_block_event(&self, pos: BlockPos, r#type: u8, data: u8) -> WorldFuture<'_, ()> { Box::pin(async move { self.add_synced_block_event(pos, r#type, data).await; }) } fn sync_world_event( &self, world_event: WorldEvent, position: BlockPos, data: i32, ) -> WorldFuture<'_, ()> { Box::pin(async move { self.sync_world_event(world_event, position, data).await; }) } fn spawn_from_type( self: Arc, entity_type: &'static EntityType, position: Vector3, ) -> WorldFuture<'static, ()> { Box::pin(async move { let mob = from_type(entity_type, position, &self, Uuid::new_v4()).await; self.spawn_entity(mob).await; }) } fn remove_block_entity<'a>(&'a self, block_pos: &'a BlockPos) -> WorldFuture<'a, ()> { Box::pin(async move { self.remove_block_entity(block_pos).await; }) } fn get_block_entity<'a>( &'a self, block_pos: &'a BlockPos, ) -> WorldFuture<'a, Option>> { Box::pin(async move { self.get_block_entity(block_pos).await }) } fn get_world_age(&self) -> WorldFuture<'_, i64> { Box::pin(async move { // Note: MutexGuard must be released before returning the future's result. let level_time_guard = self.level_time.lock().await; level_time_guard.world_age }) } fn play_sound<'a>( &'a self, sound: Sound, category: SoundCategory, position: &'a Vector3, ) -> WorldFuture<'a, ()> { Box::pin(async move { self.play_sound(sound, category, position).await; }) } fn play_sound_fine<'a>( &'a self, sound: Sound, category: SoundCategory, position: &'a Vector3, volume: f32, pitch: f32, ) -> WorldFuture<'a, ()> { Box::pin(async move { self.play_sound_fine(sound, category, position, volume, pitch) .await; }) } fn scatter_inventory<'a>( self: Arc, position: &'a BlockPos, inventory: &'a Arc, ) -> WorldFuture<'a, ()> { Box::pin(async move { Self::scatter_inventory(&self, position, inventory).await; }) } } impl BlockAccessor for World { fn get_block<'a>( &'a self, position: &'a BlockPos, ) -> Pin + Send + 'a>> { Box::pin(async move { Self::get_block(self, position).await }) } fn get_block_state<'a>( &'a self, position: &'a BlockPos, ) -> Pin + Send + 'a>> { Box::pin(async move { Self::get_block_state(self, position).await }) } fn get_block_state_id<'a>( &'a self, position: &'a BlockPos, ) -> Pin + Send + 'a>> { Box::pin(async move { Self::get_block_state_id(self, position).await }) } fn get_block_and_state<'a>( &'a self, position: &'a BlockPos, ) -> Pin + Send + 'a>> { Box::pin(async move { self.get_block_and_state(position).await }) } }