diff --git a/Cargo.lock b/Cargo.lock index f81176702..d880aecab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3475,6 +3475,7 @@ dependencies = [ "hyper", "image", "indexmap", + "libc", "libloading", "notify", "num-bigint 0.5.1", diff --git a/Cargo.toml b/Cargo.toml index b3ad90e7e..c45d19a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,7 @@ heck = { version = "0.5", default-features = false } hmac = { version = "=0.13.0", default-features = false } indexmap = { version = "2.14", default-features = false, features = ["std"] } itertools = { version = "0.15.0", default-features = false, features = ["use_std"] } +libc = { version = "0.2", default-features = false } libloading = { version = "0.9", default-features = false, features = ["std"] } lru = { version = "0.18.3", default-features = false } lz4-java-wrc = { version = "0.2.0", default-features = false, features = ["lz4_flex"] } diff --git a/crates/pumpkin-config/src/networking/query.rs b/crates/pumpkin-config/src/networking/query.rs index cc8ea9bd4..6089c35df 100644 --- a/crates/pumpkin-config/src/networking/query.rs +++ b/crates/pumpkin-config/src/networking/query.rs @@ -17,7 +17,7 @@ pub struct QueryConfig { impl Default for QueryConfig { fn default() -> Self { Self { - enabled: true, + enabled: false, address: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 25565), } } diff --git a/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs b/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs index 62763d92a..399a87e2b 100644 --- a/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs +++ b/crates/pumpkin-protocol/src/java/client/play/spawn_living_entity.rs @@ -379,4 +379,3 @@ impl ClientPacket for CSpawnLivingEntity { Ok(()) } } - diff --git a/crates/pumpkin/Cargo.toml b/crates/pumpkin/Cargo.toml index b41f6da5e..a91a9b692 100644 --- a/crates/pumpkin/Cargo.toml +++ b/crates/pumpkin/Cargo.toml @@ -100,6 +100,7 @@ rustc-hash.workspace = true tokio-util = { workspace = true, features = ["rt"] } # System details & Rustc version +libc = { workspace = true } sysinfo.workspace = true rustc_version_runtime.workspace = true diff --git a/crates/pumpkin/src/block/blocks/farmland.rs b/crates/pumpkin/src/block/blocks/farmland.rs index c271327c2..237d92413 100644 --- a/crates/pumpkin/src/block/blocks/farmland.rs +++ b/crates/pumpkin/src/block/blocks/farmland.rs @@ -59,11 +59,12 @@ impl BlockBehaviour for FarmlandBlock { let mut props = FarmlandProperties::default(args.block); let mut new_moisture = 7; if let Some(server) = args.world.server.upgrade() { - let mut event = crate::plugin::api::events::block::moisture_change::MoistureChangeEvent::new( - *args.position, - args.world.clone(), - new_moisture, - ); + let mut event = + crate::plugin::api::events::block::moisture_change::MoistureChangeEvent::new( + *args.position, + args.world.clone(), + new_moisture, + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/block/blocks/leaves.rs b/crates/pumpkin/src/block/blocks/leaves.rs index 13e463778..13d13d95e 100644 --- a/crates/pumpkin/src/block/blocks/leaves.rs +++ b/crates/pumpkin/src/block/blocks/leaves.rs @@ -102,10 +102,11 @@ impl BlockBehaviour for LeavesBlock { let props = OakLeavesLikeProperties::from_state_id(state_id); if !props.persistent && props.distance == DECAY_DISTANCE { if let Some(server) = args.world.server.upgrade() { - let mut event = crate::plugin::api::events::block::leaves_decay::LeavesDecayEvent::new( - *args.position, - args.world.clone(), - ); + let mut event = + crate::plugin::api::events::block::leaves_decay::LeavesDecayEvent::new( + *args.position, + args.world.clone(), + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/block/blocks/plant/crop/mod.rs b/crates/pumpkin/src/block/blocks/plant/crop/mod.rs index 2155c846b..6fe26b3d6 100644 --- a/crates/pumpkin/src/block/blocks/plant/crop/mod.rs +++ b/crates/pumpkin/src/block/blocks/plant/crop/mod.rs @@ -74,14 +74,15 @@ trait CropBlockBase: PlantBlockBase { if rand::rng().random_range(0..=(25.0 / f).floor() as i64) == 0 { let new_state_id = self.state_with_age(block, state, age + 1); if let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::block::block_grow::BlockGrowEvent::new( - world.clone(), - block, - state, - block, - new_state_id, - *pos, - ); + let mut event = + crate::plugin::api::events::block::block_grow::BlockGrowEvent::new( + world.clone(), + block, + state, + block, + new_state_id, + *pos, + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs b/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs index cf4841b42..93894de2c 100644 --- a/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs +++ b/crates/pumpkin/src/block/blocks/plant/crop/sweet_berry_bush.rs @@ -56,10 +56,7 @@ impl BlockBehaviour for SweetBerryBushBlock { } props.age = 1; for stack in drops { - args.world.drop_stack( - args.position, - stack, - ); + args.world.drop_stack(args.position, stack); } args.world.set_block_state( args.position, diff --git a/crates/pumpkin/src/block/blocks/redstone/crafter.rs b/crates/pumpkin/src/block/blocks/redstone/crafter.rs index 4a8f68efc..49ecf3e86 100644 --- a/crates/pumpkin/src/block/blocks/redstone/crafter.rs +++ b/crates/pumpkin/src/block/blocks/redstone/crafter.rs @@ -134,11 +134,12 @@ impl CrafterBlock { let mut result_stack = ItemStack::new(recipe_result.count, item); if let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::block::crafter_craft::CrafterCraftEvent::new( - *pos, - world.clone(), - result_stack.clone(), - ); + let mut event = + crate::plugin::api::events::block::crafter_craft::CrafterCraftEvent::new( + *pos, + world.clone(), + result_stack.clone(), + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/block/entities/brewing_stand.rs b/crates/pumpkin/src/block/entities/brewing_stand.rs index a28832d31..fe605ca84 100644 --- a/crates/pumpkin/src/block/entities/brewing_stand.rs +++ b/crates/pumpkin/src/block/entities/brewing_stand.rs @@ -465,7 +465,8 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity { if fuel_event.cancelled { false } else { - self.fuel.store(fuel_event.fuel_power as i32, Ordering::Relaxed); + self.fuel + .store(fuel_event.fuel_power as i32, Ordering::Relaxed); items[4].decrement(1); true } @@ -506,12 +507,15 @@ impl crate::block::entities::BlockEntity for BrewingStandBlockEntity { } } else if brewable && self.fuel.load(Ordering::Relaxed) > 0 { let brew_time = if let Some(server) = world.server.upgrade() { - let mut start_event = crate::plugin::api::events::block::brewing_start::BrewingStartEvent::new( - self.position, - world.clone(), - 400, - ); - server.plugin_manager.fire_blocking(&server, &mut start_event); + let mut start_event = + crate::plugin::api::events::block::brewing_start::BrewingStartEvent::new( + self.position, + world.clone(), + 400, + ); + server + .plugin_manager + .fire_blocking(&server, &mut start_event); if start_event.cancelled { return; } diff --git a/crates/pumpkin/src/block/registry.rs b/crates/pumpkin/src/block/registry.rs index 16c2ad667..aa4f10182 100644 --- a/crates/pumpkin/src/block/registry.rs +++ b/crates/pumpkin/src/block/registry.rs @@ -527,12 +527,13 @@ impl BlockRegistry { } if behaviour.is_bonemeal_success(args) { if let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::block::block_fertilize::BlockFertilizeEvent::new( - *position, - world.clone(), - None, - vec![], - ); + let mut event = + crate::plugin::api::events::block::block_fertilize::BlockFertilizeEvent::new( + *position, + world.clone(), + None, + vec![], + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return false; diff --git a/crates/pumpkin/src/command/argument_types/resource.rs b/crates/pumpkin/src/command/argument_types/resource.rs index 31b002591..67b2f8ee7 100644 --- a/crates/pumpkin/src/command/argument_types/resource.rs +++ b/crates/pumpkin/src/command/argument_types/resource.rs @@ -8,6 +8,7 @@ use crate::command::string_reader::StringReader; use crate::command::suggestion::suggestions::{Suggestions, SuggestionsBuilder}; use pumpkin_data::entity::EntityType; use pumpkin_data::translation; +use pumpkin_protocol::java::client::play::SuggestionProviders; use pumpkin_util::identifier::Identifier; use pumpkin_util::text::TextComponent; use std::any::Any; @@ -144,6 +145,7 @@ impl ArgumentType for ResourceArgument { if self.0 == ENTITY_TYPE_REGISTRY { let entity_types = EntityType::ALL .iter() + .filter(|entity_type| entity_type.summonable) .map(|entity_type| format!("minecraft:{}", entity_type.resource_name)); suggestions_builder .filter_and_suggest_iter(entity_types) @@ -216,6 +218,10 @@ impl ArgumentType for ResourceArgument { identifier: self.0.clone(), } } + + fn override_suggestion_providers(&self) -> Option { + (self.0 == ENTITY_TYPE_REGISTRY).then_some(SuggestionProviders::SummonableEntities) + } } impl ResourceArgument { @@ -228,10 +234,11 @@ impl ResourceArgument { TextComponent::text(format!("Could not find argument with name '{name}'")), ); let node = context - .tree + .nodes .iter() - .find_map(|node| { - if let AttachedNode::Argument(cur) = node + .rev() + .find_map(|parsed| { + if let AttachedNode::Argument(cur) = &context.tree[parsed.node] && cur.meta.name == name { Some(cur) @@ -239,6 +246,17 @@ impl ResourceArgument { None } }) + .or_else(|| { + context.tree.iter().find_map(|node| { + if let AttachedNode::Argument(cur) = node + && cur.meta.name == name + { + Some(cur) + } else { + None + } + }) + }) .ok_or(missing_argument.clone())?; let invalid_argument = DISPATCHER_PARSE_EXCEPTION.create_without_context(TextComponent::text(format!( diff --git a/crates/pumpkin/src/command/commands/summon.rs b/crates/pumpkin/src/command/commands/summon.rs index 93eef25b6..b49f6c2d0 100644 --- a/crates/pumpkin/src/command/commands/summon.rs +++ b/crates/pumpkin/src/command/commands/summon.rs @@ -1,22 +1,41 @@ use pumpkin_data::translation; use pumpkin_util::PermissionLvl; +use pumpkin_util::difficulty::Difficulty; +use pumpkin_util::math::position::BlockPos; use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry}; use pumpkin_util::text::TextComponent; use uuid::Uuid; use crate::command::argument_builder::{ArgumentBuilder, argument, command}; use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType; +use crate::command::argument_types::nbt::NbtCompoundArgumentType; use crate::command::argument_types::resource::{ENTITY_TYPE_ARGUMENT, ResourceArgument}; use crate::command::context::command_context::CommandContext; +use crate::command::errors::error_types::CommandErrorType; use crate::command::node::dispatcher::CommandDispatcher; use crate::command::node::{CommandExecutor, CommandExecutorResult}; use crate::entity::r#type::from_type; +use crate::world::World; const DESCRIPTION: &str = "Spawns an entity at position."; const PERMISSION: &str = "minecraft:command.summon"; +const ERROR_FAILED_PEACEFUL: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SUMMON_FAILED_PEACEFUL, + translation::bedrock::COMMANDS_SUMMON_FAILED, +); +const ERROR_DUPLICATE_UUID: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SUMMON_FAILED_UUID, + translation::bedrock::COMMANDS_SUMMON_FAILED, +); +const INVALID_POSITION: CommandErrorType<0> = CommandErrorType::new( + translation::java::COMMANDS_SUMMON_INVALIDPOSITION, + translation::bedrock::COMMANDS_SUMMON_OUTOFWORLD, +); + struct SummonExecutor { has_pos: bool, + has_nbt: bool, } impl CommandExecutor for SummonExecutor { @@ -28,8 +47,42 @@ impl CommandExecutor for SummonExecutor { context.source.position }; + let block_pos = BlockPos::floored_v(pos); + if !World::is_valid(block_pos) { + return Err(INVALID_POSITION.create_without_context()); + } + let world = context.source.world(); - let entity = from_type(entity_type, pos, world, Uuid::new_v4()); + let difficulty = world.level_info.load().difficulty; + if difficulty == Difficulty::Peaceful && !entity_type.category.is_friendly { + return Err(ERROR_FAILED_PEACEFUL.create_without_context()); + } + + let nbt = if self.has_nbt { + Some(NbtCompoundArgumentType::get(context, "nbt")?) + } else { + None + }; + + let uuid = nbt + .and_then(|nbt| nbt.get_uuid("UUID")) + .unwrap_or_else(Uuid::new_v4); + + let entity = from_type(entity_type, pos, world, uuid); + if let Some(nbt) = nbt { + entity.read_nbt_non_mut(nbt); + entity.get_entity().set_pos(pos); + } + + if world + .entities + .load() + .iter() + .any(|e| e.get_entity().entity_uuid == entity.get_entity().entity_uuid) + { + return Err(ERROR_DUPLICATE_UUID.create_without_context()); + } + let name = entity.get_display_name(); world.spawn_entity(entity); @@ -56,11 +109,69 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr dispatcher.register( command("summon", DESCRIPTION).requires(PERMISSION).then( argument("entity", ENTITY_TYPE_ARGUMENT.clone()) - .executes(SummonExecutor { has_pos: false }) + .executes(SummonExecutor { + has_pos: false, + has_nbt: false, + }) .then( argument("pos", Vec3ArgumentType::Default) - .executes(SummonExecutor { has_pos: true }), + .executes(SummonExecutor { + has_pos: true, + has_nbt: false, + }) + .then( + argument("nbt", NbtCompoundArgumentType).executes(SummonExecutor { + has_pos: true, + has_nbt: true, + }), + ), ), ), ); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::command::commands::fetchprofile; + use crate::command::context::command_source::CommandSource; + use std::sync::Arc; + + #[test] + fn parse_summon_cat() { + let mut dispatcher = CommandDispatcher::new(); + let registry = PermissionRegistry::default(); + + // Register fetchprofile first to ensure its "entity" argument doesn't collide + fetchprofile::register(&mut dispatcher, ®istry); + register(&mut dispatcher, ®istry); + + let source = Arc::new(CommandSource::dummy()); + let result = dispatcher.parse_input("summon cat", &source); + assert!(result.errors.is_empty()); + let context = result.context.build("summon cat"); + let entity_type = ResourceArgument::get_summonable_entity_type(&context, "entity") + .expect("Should resolve summonable cat entity type"); + assert_eq!(entity_type.resource_name, "cat"); + } + + #[test] + fn parse_summon_cat_with_pos_and_nbt() { + let mut dispatcher = CommandDispatcher::new(); + let registry = PermissionRegistry::default(); + + fetchprofile::register(&mut dispatcher, ®istry); + register(&mut dispatcher, ®istry); + + let source = Arc::new(CommandSource::dummy()); + let result = dispatcher.parse_input("summon cat ~ ~ ~ {NoAI:1b}", &source); + assert!(result.errors.is_empty()); + let context = result.context.build("summon cat ~ ~ ~ {NoAI:1b}"); + let entity_type = ResourceArgument::get_summonable_entity_type(&context, "entity") + .expect("Should resolve summonable cat entity type"); + assert_eq!(entity_type.resource_name, "cat"); + let nbt = + NbtCompoundArgumentType::get(&context, "nbt").expect("Should resolve NBT argument"); + assert_eq!(nbt.get_byte("NoAI"), Some(1)); + } +} diff --git a/crates/pumpkin/src/command/context/command_source.rs b/crates/pumpkin/src/command/context/command_source.rs index fa4e6bac6..76cd2b332 100644 --- a/crates/pumpkin/src/command/context/command_source.rs +++ b/crates/pumpkin/src/command/context/command_source.rs @@ -480,7 +480,15 @@ impl CommandSource { /// server (i.e. this is a dummy [`CommandSource`].) #[must_use] pub fn has_permission(&self, permission: &str) -> bool { - self.output.has_permission(self.server(), permission) + self.server.as_ref().map_or( + matches!( + self.output, + crate::command::CommandSender::Console + | crate::command::CommandSender::Rcon(_) + | crate::command::CommandSender::Dummy + ), + |server| self.output.has_permission(server, permission), + ) } /// Returns whether this source has the permission provided. diff --git a/crates/pumpkin/src/entity/combat.rs b/crates/pumpkin/src/entity/combat.rs index 9581e139f..48b96a48b 100644 --- a/crates/pumpkin/src/entity/combat.rs +++ b/crates/pumpkin/src/entity/combat.rs @@ -606,7 +606,8 @@ impl CombatTracker { ) } } else if let Some(killer_name) = kill_credit_name.or_else(|| { - self.get_killer_entry().and_then(|k| k.attacker_name.clone()) + self.get_killer_entry() + .and_then(|k| k.attacker_name.clone()) }) { pumpkin_util::text::TextComponent::translate_cross( format!("death.attack.{msg_id}.player"), diff --git a/crates/pumpkin/src/entity/living.rs b/crates/pumpkin/src/entity/living.rs index 01e27da25..b6f02a755 100644 --- a/crates/pumpkin/src/entity/living.rs +++ b/crates/pumpkin/src/entity/living.rs @@ -1666,9 +1666,7 @@ impl LivingEntity { source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) -> TextComponent { - let kill_credit = dyn_self - .get_living_entity() - .and_then(Self::get_kill_credit); + let kill_credit = dyn_self.get_living_entity().and_then(Self::get_kill_credit); let kill_credit_name = kill_credit.as_ref().map(|c| c.get_display_name()); if let Some(living) = dyn_self.get_living_entity() { @@ -1687,7 +1685,11 @@ impl LivingEntity { format!("death.attack.{}", damage_type.message_id), [dyn_self.get_display_name(), cause.get_display_name()], ) - } else if let Some(killer) = cause.or(source).map(|c| c.get_display_name()).or(kill_credit_name) { + } else if let Some(killer) = cause + .or(source) + .map(|c| c.get_display_name()) + .or(kill_credit_name) + { TextComponent::translate_cross( format!("death.attack.{}.player", damage_type.message_id), format!("death.attack.{}.player", damage_type.message_id), @@ -1887,11 +1889,12 @@ impl LivingEntity { if let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id) && let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::entity::entity_death::PlayerDeathEvent::new( - player_arc, - final_death_message.clone(), - 0, - ); + let mut event = + crate::plugin::api::events::entity::entity_death::PlayerDeathEvent::new( + player_arc, + final_death_message.clone(), + 0, + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; @@ -1902,9 +1905,7 @@ impl LivingEntity { player.handle_killed(&final_death_message); } - if show_death_messages - && let Some(server) = world.server.upgrade() - { + if show_death_messages && let Some(server) = world.server.upgrade() { for player in server.get_all_players() { player.send_system_message(&final_death_message); } @@ -2639,7 +2640,13 @@ impl LivingEntity { | DamageType::STALAGMITE ) { - let damager_pos = position.map(|p| BlockPos(Vector3::new(p.x.floor() as i32, p.y.floor() as i32, p.z.floor() as i32))); + let damager_pos = position.map(|p| { + BlockPos(Vector3::new( + p.x.floor() as i32, + p.y.floor() as i32, + p.z.floor() as i32, + )) + }); let mut by_block_event = crate::plugin::api::events::entity::entity_damage_by_block::EntityDamageByBlockEvent { entity_id: self.entity.entity_id, @@ -2897,8 +2904,7 @@ impl LivingEntity { if let Some(attacker) = cause.or(source) { let attacker_id = attacker.get_entity().entity_id; - self.last_attacker_id - .store(attacker_id, Relaxed); + self.last_attacker_id.store(attacker_id, Relaxed); self.last_attacked_time .store(self.entity.age.load(Relaxed), Relaxed); diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 1bdfabdc3..7d64f9b1c 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -3371,12 +3371,13 @@ impl Entity { && let Some(player) = holder.get_player() && let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id) { - let mut event = crate::plugin::api::events::player::player_leash_entity::PlayerLeashEntityEvent { - player: player_arc, - entity_id: self.entity_id, - holder_id: holder_entity_id, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_leash_entity::PlayerLeashEntityEvent { + player: player_arc, + entity_id: self.entity_id, + holder_id: holder_entity_id, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; @@ -3416,10 +3417,11 @@ impl Entity { pub fn unleash(&self) { let world = self.world.load(); if let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::entity::entity_unleash::EntityUnleashEvent::new( - self.entity_id, - "unleashed".to_string(), - ); + let mut event = + crate::plugin::api::events::entity::entity_unleash::EntityUnleashEvent::new( + self.entity_id, + "unleashed".to_string(), + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 7f71e563f..f64382f9d 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -1607,12 +1607,13 @@ impl Player { if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) && let Some(server) = self.world().server.upgrade() { - let mut event = crate::plugin::api::events::player::player_spawn_change::PlayerSpawnChangeEvent { - player: player_arc, - new_spawn: Some(block_pos), - forced, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_spawn_change::PlayerSpawnChangeEvent { + player: player_arc, + new_spawn: Some(block_pos), + forced, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return false; @@ -1628,7 +1629,12 @@ impl Player { _ => 0, }; self.client.try_enqueue_packet_editioned( - &CPlayerSpawnPosition::new(final_block_pos, yaw, pitch, dimension.minecraft_name.to_owned()), + &CPlayerSpawnPosition::new( + final_block_pos, + yaw, + pitch, + dimension.minecraft_name.to_owned(), + ), &pumpkin_protocol::bedrock::client::CSetSpawnPosition { spawn_position_type: pumpkin_protocol::bedrock::client::SpawnPositionType::PlayerRespawn, @@ -2315,12 +2321,13 @@ impl Player { if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) && let Some(server) = self.world().server.upgrade() { - let mut event = crate::plugin::api::events::player::player_open_sign::PlayerOpenSignEvent { - player: player_arc, - block_pos: location, - is_front: is_front_text, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_open_sign::PlayerOpenSignEvent { + player: player_arc, + block_pos: location, + is_front: is_front_text, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; @@ -2334,11 +2341,12 @@ impl Player { if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) && let Some(server) = self.world().server.upgrade() { - let mut event = crate::plugin::api::events::player::player_velocity::PlayerVelocityEvent { - player: player_arc, - velocity, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_velocity::PlayerVelocityEvent { + player: player_arc, + velocity, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; @@ -2391,10 +2399,11 @@ impl Player { && let Some(server) = self.world().server.upgrade() { let link_strings = links.iter().map(|l| l.url.clone()).collect(); - let mut event = crate::plugin::api::events::player::player_links_send::PlayerLinksSendEvent::new( - player_arc, - link_strings, - ); + let mut event = + crate::plugin::api::events::player::player_links_send::PlayerLinksSendEvent::new( + player_arc, + link_strings, + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; @@ -3089,7 +3098,8 @@ impl Player { } pub fn increment_stat(&self, category: statistics::StatisticCategory, stat: i32, amount: i32) { - let final_amount = if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) + let final_amount = if let Some(player_arc) = + self.world().get_player_by_uuid(self.gameprofile.id) && let Some(server) = self.world().server.upgrade() { let mut event = crate::plugin::api::events::player::player_statistic_increment::PlayerStatisticIncrementEvent { @@ -5228,13 +5238,14 @@ impl Player { if let Some(player_arc) = self.world().get_player_by_uuid(self.gameprofile.id) && let Some(server) = self.world().server.upgrade() { - let mut event = crate::plugin::api::events::player::player_item_mend::PlayerItemMendEvent { - player: player_arc, - item_name: stack.item.registry_key.to_string(), - repair_amount: repaired, - exp_consumed: xp_used, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_item_mend::PlayerItemMendEvent { + player: player_arc, + item_name: stack.item.registry_key.to_string(), + repair_amount: repaired, + exp_consumed: xp_used, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return xp; diff --git a/crates/pumpkin/src/entity/projectile/firework_rocket.rs b/crates/pumpkin/src/entity/projectile/firework_rocket.rs index b22b8f119..076baffaf 100644 --- a/crates/pumpkin/src/entity/projectile/firework_rocket.rs +++ b/crates/pumpkin/src/entity/projectile/firework_rocket.rs @@ -71,10 +71,11 @@ impl FireworkRocketEntity { pub fn explode_and_remove(&self, world: &World) { let entity = self.get_entity(); if let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::entity::firework_explode::FireworkExplodeEvent { - entity_id: entity.entity_id, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::entity::firework_explode::FireworkExplodeEvent { + entity_id: entity.entity_id, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/entity/projectile/splash_potion.rs b/crates/pumpkin/src/entity/projectile/splash_potion.rs index f0cd0d627..24cfcf93e 100644 --- a/crates/pumpkin/src/entity/projectile/splash_potion.rs +++ b/crates/pumpkin/src/entity/projectile/splash_potion.rs @@ -244,14 +244,18 @@ impl EntityBase for SplashPotionEntity { } } - let affected_ids: Vec = affected.iter().map(|(c, _)| c.get_entity().entity_id).collect(); + let affected_ids: Vec = affected + .iter() + .map(|(c, _)| c.get_entity().entity_id) + .collect(); if let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::entity::potion_splash::PotionSplashEvent::new( - self.get_entity().entity_id, - block_pos, - stack.item.registry_key.to_string(), - affected_ids, - ); + let mut event = + crate::plugin::api::events::entity::potion_splash::PotionSplashEvent::new( + self.get_entity().entity_id, + block_pos, + stack.item.registry_key.to_string(), + affected_ids, + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/item/items/brush.rs b/crates/pumpkin/src/item/items/brush.rs index d78025365..529d06a78 100644 --- a/crates/pumpkin/src/item/items/brush.rs +++ b/crates/pumpkin/src/item/items/brush.rs @@ -120,12 +120,13 @@ impl ItemBehaviour for BrushItem { if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) && let Some(server) = player.world().server.upgrade() { - let mut event = crate::plugin::api::events::block::block_brush::BlockBrushEvent::new( - location, - world.clone(), - player_arc, - player.inventory().held_item(), - ); + let mut event = + crate::plugin::api::events::block::block_brush::BlockBrushEvent::new( + location, + world.clone(), + player_arc, + player.inventory().held_item(), + ); server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/item/items/shears.rs b/crates/pumpkin/src/item/items/shears.rs index fbcfcc14e..9cd1ad0f1 100644 --- a/crates/pumpkin/src/item/items/shears.rs +++ b/crates/pumpkin/src/item/items/shears.rs @@ -194,12 +194,13 @@ fn handle_beehive( if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) && let Some(server) = player.world().server.upgrade() { - let mut event = crate::plugin::api::events::player::player_harvest_block::PlayerHarvestBlockEvent { - player: player_arc, - block_pos: *location, - harvested_items: drops.clone(), - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_harvest_block::PlayerHarvestBlockEvent { + player: player_arc, + block_pos: *location, + harvested_items: drops.clone(), + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return false; diff --git a/crates/pumpkin/src/item/items/trident.rs b/crates/pumpkin/src/item/items/trident.rs index ebcf5d550..f8ce0d81d 100644 --- a/crates/pumpkin/src/item/items/trident.rs +++ b/crates/pumpkin/src/item/items/trident.rs @@ -68,11 +68,12 @@ impl ItemBehaviour for TridentItem { if let Some(player_arc) = world.get_player_by_uuid(player.gameprofile.id) && let Some(server) = world.server.upgrade() { - let mut event = crate::plugin::api::events::player::player_riptide::PlayerRiptideEvent { - player: player_arc, - item_name: "trident".to_string(), - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_riptide::PlayerRiptideEvent { + player: player_arc, + item_name: "trident".to_string(), + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { player.living_entity.clear_active_hand(); diff --git a/crates/pumpkin/src/lib.rs b/crates/pumpkin/src/lib.rs index 3d4dfe312..7110bc9e4 100644 --- a/crates/pumpkin/src/lib.rs +++ b/crates/pumpkin/src/lib.rs @@ -251,6 +251,9 @@ impl PumpkinServer { ) -> Self { let server = Server::new(basic_config, advanced_config, vanilla_data).await; + #[cfg(target_family = "unix")] + adjust_file_descriptor_limit(); + let rcon = server.advanced_config.networking.rcon.clone(); if rcon.enabled { @@ -615,6 +618,15 @@ impl PumpkinServer { }); } Err(e) => { + #[cfg(target_family = "unix")] + if e.raw_os_error() == Some(libc::EMFILE) { + error!( + "Too many open files! Server reached file descriptor limit. \ + New connections cannot be accepted until existing connections close or `ulimit -n` is increased." + ); + sleep(Duration::from_millis(500)).await; + return true; + } error!("Failed to accept Java client connection: {e}"); sleep(Duration::from_millis(50)).await; } @@ -826,3 +838,58 @@ fn scrub_address(ip: &str) -> String { .map(|ch| if ch == '.' || ch == ':' { ch } else { 'x' }) .collect() } + +#[cfg(target_family = "unix")] +fn adjust_file_descriptor_limit() { + let mut rlim = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + // SAFETY: Passing a valid mutable pointer to a stack-allocated `rlimit` struct. + if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut rlim) } != 0 { + return; + } + + let max_target = if rlim.rlim_max == libc::RLIM_INFINITY { + 1_048_576 + } else { + rlim.rlim_max + }; + + if rlim.rlim_cur < max_target { + let old_limit = rlim.rlim_cur; + rlim.rlim_cur = max_target; + + // SAFETY: Calling `setrlimit` with a valid resource and valid pointer to initialized `rlimit`. + let res = unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const rlim) }; + if res == 0 { + debug!("Increased open file descriptor limit from {old_limit} to {max_target}"); + } else { + // Fallback: try setting to a reasonable high value (65,536) if max_target was rejected by the OS. + let fallback = 65_536.min(max_target); + if fallback > old_limit { + rlim.rlim_cur = fallback; + // SAFETY: Calling `setrlimit` with a valid resource and valid pointer to initialized `rlimit`. + if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const rlim) } == 0 { + debug!("Increased open file descriptor limit from {old_limit} to {fallback}"); + } + } + } + } + + let mut current_rlim = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + // SAFETY: Passing a valid mutable pointer to a stack-allocated `rlimit` struct. + if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut current_rlim) } == 0 + && current_rlim.rlim_cur < 4096 + { + warn!( + "Open file descriptor limit is low ({}). Supporting >1000 concurrent players may fail with 'Too many open files'. \ + Consider increasing the limit with `ulimit -n 65535` or setting `LimitNOFILE=65535` in systemd.", + current_rlim.rlim_cur + ); + } +} diff --git a/crates/pumpkin/src/net/chunk_sender.rs b/crates/pumpkin/src/net/chunk_sender.rs index 0f023f00f..c4336ed67 100644 --- a/crates/pumpkin/src/net/chunk_sender.rs +++ b/crates/pumpkin/src/net/chunk_sender.rs @@ -160,8 +160,7 @@ impl ChunkSender { epoch: u32, version: JavaMinecraftVersion, ) -> Option { - if version >= JavaMinecraftVersion::V_1_20_2 - && self.in_flight_batches >= self.max_in_flight + if version >= JavaMinecraftVersion::V_1_20_2 && self.in_flight_batches >= self.max_in_flight { return None; } diff --git a/crates/pumpkin/src/net/java/login/encryption_response.rs b/crates/pumpkin/src/net/java/login/encryption_response.rs index d7525ec72..f8de40938 100644 --- a/crates/pumpkin/src/net/java/login/encryption_response.rs +++ b/crates/pumpkin/src/net/java/login/encryption_response.rs @@ -149,14 +149,18 @@ impl PendingConnection { server: &Arc, profile: &GameProfile, ) -> Option { - let mut pre_login_event = crate::plugin::api::events::player::async_player_pre_login::AsyncPlayerPreLoginEvent { - player_name: profile.name.clone(), - player_uuid: profile.id, - ip_address: self.address, - kick_message: TextComponent::text("Disconnected"), - cancelled: false, - }; - server.plugin_manager.fire(server, &mut pre_login_event).await; + let mut pre_login_event = + crate::plugin::api::events::player::async_player_pre_login::AsyncPlayerPreLoginEvent { + player_name: profile.name.clone(), + player_uuid: profile.id, + ip_address: self.address, + kick_message: TextComponent::text("Disconnected"), + cancelled: false, + }; + server + .plugin_manager + .fire(server, &mut pre_login_event) + .await; if pre_login_event.cancelled { self.kick(pre_login_event.kick_message).await; return Some(PacketHandlerResult::Stop); diff --git a/crates/pumpkin/src/net/java/mod.rs b/crates/pumpkin/src/net/java/mod.rs index ab2db05b0..fb0e9c901 100644 --- a/crates/pumpkin/src/net/java/mod.rs +++ b/crates/pumpkin/src/net/java/mod.rs @@ -1183,7 +1183,9 @@ impl JavaClient { player.clone(), ch.to_string(), ); - server.plugin_manager.fire_blocking(server, &mut unreg_event); + server + .plugin_manager + .fire_blocking(server, &mut unreg_event); } } } diff --git a/crates/pumpkin/src/net/java/play/edit_book.rs b/crates/pumpkin/src/net/java/play/edit_book.rs index d9f67916f..bff6902fb 100644 --- a/crates/pumpkin/src/net/java/play/edit_book.rs +++ b/crates/pumpkin/src/net/java/play/edit_book.rs @@ -16,14 +16,15 @@ impl JavaClient { if let Some(player_arc) = player.world().get_player_by_uuid(player.gameprofile.id) && let Some(server) = player.world().server.upgrade() { - let mut event = crate::plugin::api::events::player::player_edit_book::PlayerEditBookEvent { - player: player_arc, - slot, - pages: pages.clone(), - title: title.clone(), - signing, - cancelled: false, - }; + let mut event = + crate::plugin::api::events::player::player_edit_book::PlayerEditBookEvent { + player: player_arc, + slot, + pages: pages.clone(), + title: title.clone(), + signing, + cancelled: false, + }; server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; diff --git a/crates/pumpkin/src/net/java/play/player_action.rs b/crates/pumpkin/src/net/java/play/player_action.rs index 4e53ffa47..7a3a8c407 100644 --- a/crates/pumpkin/src/net/java/play/player_action.rs +++ b/crates/pumpkin/src/net/java/play/player_action.rs @@ -169,7 +169,9 @@ impl JavaClient { world.clone(), player.inventory().held_item(), ); - server_arc.plugin_manager.fire_blocking(&server_arc, &mut abort_event); + server_arc + .plugin_manager + .fire_blocking(&server_arc, &mut abort_event); } player.mining.store(false, Ordering::Relaxed); diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs index f3d79da3f..cd3e15dc2 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/entity.rs @@ -930,4 +930,3 @@ pub(crate) fn from_wit_entity_type( .copied() .ok_or_else(|| wasmtime::Error::msg(format!("Invalid entity type index: {index}"))) } - diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs index 02f31dca5..3e5fc9486 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs @@ -1024,8 +1024,7 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { let world_ref = self.get_world_res(&world)?; let world_provider = world_ref.provider.clone(); - let internal_type = - super::entity::from_wit_entity_type(entity_type)?; + let internal_type = super::entity::from_wit_entity_type(entity_type)?; let internal_pos = pumpkin_util::math::vector3::Vector3::new(pos.0, pos.1, pos.2); let entity = crate::entity::r#type::from_type(