From 55219f870f0da3921283270e425411a3da4f6665 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 16 Jul 2026 19:56:53 +0200 Subject: [PATCH] fix: https://github.com/Pumpkin-MC/Pumpkin/issues/2233 --- pumpkin-protocol/src/bedrock/client/mod.rs | 4 + .../src/bedrock/client/set_difficulty.rs | 18 ++ .../src/bedrock/client/set_spawn_position.rs | 42 ++++ .../generation/structure/structures/jigsaw.rs | 5 +- .../structure/template/structure_template.rs | 14 +- pumpkin/src/command/args/entities.rs | 12 +- .../argument_types/entity_selector/mod.rs | 6 + .../argument_types/entity_selector/option.rs | 33 ++++ pumpkin/src/command/commands/difficulty.rs | 2 +- pumpkin/src/entity/player.rs | 180 ++++++++++-------- pumpkin/src/net/mod.rs | 11 ++ pumpkin/src/server/mod.rs | 10 +- pumpkin/src/world/mod.rs | 25 ++- 13 files changed, 264 insertions(+), 98 deletions(-) create mode 100644 pumpkin-protocol/src/bedrock/client/set_difficulty.rs create mode 100644 pumpkin-protocol/src/bedrock/client/set_spawn_position.rs diff --git a/pumpkin-protocol/src/bedrock/client/mod.rs b/pumpkin-protocol/src/bedrock/client/mod.rs index 9a11b84b1..f96f287b2 100644 --- a/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/pumpkin-protocol/src/bedrock/client/mod.rs @@ -35,8 +35,10 @@ pub mod resource_packs_info; pub mod scoreboard; pub mod set_actor_data; pub mod set_actor_motion; +pub mod set_difficulty; pub mod set_health; pub mod set_player_gamemode; +pub mod set_spawn_position; pub mod set_time; pub mod set_title; pub mod show_credits; @@ -84,8 +86,10 @@ pub use resource_packs_info::*; pub use scoreboard::*; pub use set_actor_data::*; pub use set_actor_motion::*; +pub use set_difficulty::*; pub use set_health::*; pub use set_player_gamemode::*; +pub use set_spawn_position::*; pub use set_time::*; pub use set_title::*; pub use show_credits::*; diff --git a/pumpkin-protocol/src/bedrock/client/set_difficulty.rs b/pumpkin-protocol/src/bedrock/client/set_difficulty.rs new file mode 100644 index 000000000..708ec95c8 --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/set_difficulty.rs @@ -0,0 +1,18 @@ +use pumpkin_macros::packet; + +use crate::{codec::var_uint::VarUInt, serial::PacketWrite}; + +#[derive(PacketWrite)] +#[packet(60)] +pub struct CSetDifficulty { + pub difficulty: VarUInt, +} + +impl CSetDifficulty { + #[must_use] + pub const fn new(difficulty: u32) -> Self { + Self { + difficulty: VarUInt(difficulty), + } + } +} diff --git a/pumpkin-protocol/src/bedrock/client/set_spawn_position.rs b/pumpkin-protocol/src/bedrock/client/set_spawn_position.rs new file mode 100644 index 000000000..c5a05a38b --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/set_spawn_position.rs @@ -0,0 +1,42 @@ +use std::io::{Error, Write}; + +use pumpkin_macros::packet; +use pumpkin_util::math::position::BlockPos; + +use crate::{codec::var_int::VarInt, serial::PacketWrite}; + +#[derive(Clone, Copy)] +#[packet(43)] +pub struct CSetSpawnPosition { + pub spawn_type: VarInt, + pub position: BlockPos, + pub dimension: VarInt, + pub spawn_position: BlockPos, +} + +impl CSetSpawnPosition { + #[must_use] + pub const fn new( + spawn_type: i32, + position: BlockPos, + dimension: i32, + spawn_position: BlockPos, + ) -> Self { + Self { + spawn_type: VarInt(spawn_type), + position, + dimension: VarInt(dimension), + spawn_position, + } + } +} + +impl PacketWrite for CSetSpawnPosition { + fn write(&self, writer: &mut W) -> Result<(), Error> { + self.spawn_type.write(writer)?; + self.position.write(writer)?; + self.dimension.write(writer)?; + self.spawn_position.write(writer)?; + Ok(()) + } +} diff --git a/pumpkin-world/src/generation/structure/structures/jigsaw.rs b/pumpkin-world/src/generation/structure/structures/jigsaw.rs index ebf777df3..0a9b9b6d9 100644 --- a/pumpkin-world/src/generation/structure/structures/jigsaw.rs +++ b/pumpkin-world/src/generation/structure/structures/jigsaw.rs @@ -67,7 +67,10 @@ struct RawWeightedPoolElement { enum RawPoolElement { #[serde(rename = "minecraft:empty_pool_element")] Empty, - #[serde(rename = "minecraft:single_pool_element")] + #[serde( + rename = "minecraft:single_pool_element", + alias = "minecraft:legacy_single_pool_element" + )] Single { location: String, processors: RawProcessorList, diff --git a/pumpkin-world/src/generation/structure/template/structure_template.rs b/pumpkin-world/src/generation/structure/template/structure_template.rs index f0bd21b9e..e954c9ce7 100644 --- a/pumpkin-world/src/generation/structure/template/structure_template.rs +++ b/pumpkin-world/src/generation/structure/template/structure_template.rs @@ -177,9 +177,17 @@ impl StructureTemplate { } fn parse_palette(compound: &NbtCompound) -> Result, TemplateError> { - let palette_list = compound - .get_list("palette") - .ok_or(TemplateError::MissingField("palette"))?; + let palette_list = if let Some(list) = compound.get_list("palette") { + list + } else if let Some(palettes) = compound.get_list("palettes") { + if let Some(NbtTag::List(list)) = palettes.first() { + list + } else { + return Err(TemplateError::MissingField("palette")); + } + } else { + return Err(TemplateError::MissingField("palette")); + }; let mut palette = Vec::with_capacity(palette_list.len()); diff --git a/pumpkin/src/command/args/entities.rs b/pumpkin/src/command/args/entities.rs index 2a6c087c4..6683ae524 100644 --- a/pumpkin/src/command/args/entities.rs +++ b/pumpkin/src/command/args/entities.rs @@ -90,8 +90,9 @@ impl FromStr for EntityFilter { match key { "type" => { + let name = value.strip_prefix("minecraft:").unwrap_or(value); let entity_type = - EntityType::from_name(value).ok_or(format!("Invalid entity type {value}"))?; + EntityType::from_name(name).ok_or(format!("Invalid entity type {value}"))?; Ok(Self::Type(if negate { ValueCondition::NotEquals(entity_type) } else { @@ -496,4 +497,13 @@ mod test { assert_eq!(translate_key, translation::java::ARGUMENT_PLAYER_ENTITIES); assert_eq!(error.context.expect("Error should have context").cursor, 4); } + + #[test] + fn parse_type_and_inverted_type() { + let s = "@e[type=!player,type=minecraft:zombie]" + .parse::() + .expect("should parse"); + assert!(s.includes_entities()); + assert_eq!(s.conditions.len(), 2); + } } diff --git a/pumpkin/src/command/argument_types/entity_selector/mod.rs b/pumpkin/src/command/argument_types/entity_selector/mod.rs index d7fe7c159..d26b9ae47 100644 --- a/pumpkin/src/command/argument_types/entity_selector/mod.rs +++ b/pumpkin/src/command/argument_types/entity_selector/mod.rs @@ -382,6 +382,8 @@ pub enum EntitySelectorPredicate { BoundingBox(BoundingBox), /// A predicate to check whether an entity is within a specified range from some position. Distance(DoubleBounds, Vector3), + /// A predicate to check the entity type. This check can also be inverted. + EntityType(&'static EntityType, bool), /// Used to combine sub-predicates. AllOf(Vec), @@ -437,6 +439,10 @@ impl EntitySelectorPredicate { Self::Distance(bounds, pos) => { bounds.matches_square(entity.get_entity().pos.load().squared_distance_to_vec(pos)) } + Self::EntityType(expected_type, invert) => { + let actual_type = entity.get_entity().entity_type; + (actual_type.id == expected_type.id) ^ invert + } Self::AllOf(predicates) => predicates.iter().all(|predicate| predicate.test(entity)), } } diff --git a/pumpkin/src/command/argument_types/entity_selector/option.rs b/pumpkin/src/command/argument_types/entity_selector/option.rs index aa57c1ff5..a6ce01800 100644 --- a/pumpkin/src/command/argument_types/entity_selector/option.rs +++ b/pumpkin/src/command/argument_types/entity_selector/option.rs @@ -7,6 +7,7 @@ use crate::command::errors::command_syntax_error::CommandSyntaxError; use crate::command::errors::error_types::CommandErrorType; use crate::command::string_reader::StringReader; use crate::command::suggestion::suggestions::SuggestionsBuilder; +use pumpkin_data::entity::EntityType; use pumpkin_data::translation; use pumpkin_util::GameMode; use pumpkin_util::math::bounds::{DoubleBounds, FloatDegreeBounds, IntBounds}; @@ -41,6 +42,10 @@ pub const GAMEMODE_INVALID_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::n translation::java::ARGUMENT_ENTITY_OPTIONS_MODE_INVALID, translation::java::ARGUMENT_ENTITY_OPTIONS_MODE_INVALID, ); +pub const TYPE_INVALID_ERROR_TYPE: CommandErrorType<1> = CommandErrorType::new( + translation::java::ARGUMENT_ENTITY_OPTIONS_TYPE_INVALID, + translation::java::ARGUMENT_ENTITY_OPTIONS_TYPE_INVALID, +); /// Options to customize an [`EntitySelectorParser`]. /// @@ -187,6 +192,7 @@ impl EntitySelectorOption { /// Any required fields will be parsed by this method using [`StringReader`] /// methods, and any required predicates will be added. /// Any found errors will be returned. + #[allow(clippy::too_many_lines)] pub fn modify_parser( self, parser: &mut EntitySelectorParser, @@ -278,6 +284,33 @@ impl EntitySelectorOption { .create(parser.reader, TextComponent::text(string))) } } + Self::Type => { + let start = parser.reader.cursor(); + let invert = parser.consume_inverted_start(); + if parser.has_flag(Flags::ENTITY_TYPE_INVERTED) && !invert { + parser.reader.set_cursor(start); + return Err(self.inapplicable_error(parser.reader)); + } + let mut string = parser.reader.read_unquoted_string(); + if let Some(stripped) = string.strip_prefix("minecraft:") { + string = stripped.to_string(); + } + if let Some(entity_type) = EntityType::from_name(&string) { + if entity_type.id == EntityType::PLAYER.id && !invert { + parser.limit_to_players(); + } + parser.add_predicate(EntitySelectorPredicate::EntityType(entity_type, invert)); + if invert { + parser.set_flag(Flags::ENTITY_TYPE_INVERTED, true); + } else { + parser.entity_type = Some(entity_type); + } + Ok(()) + } else { + parser.reader.set_cursor(start); + Err(TYPE_INVALID_ERROR_TYPE.create(parser.reader, TextComponent::text(string))) + } + } _ => { tracing::warn!("Unimplemented entity selector option: {:?}", self); Err(UNKNOWN_OPTION_ERROR_TYPE.create_without_context(self.name_component())) diff --git a/pumpkin/src/command/commands/difficulty.rs b/pumpkin/src/command/commands/difficulty.rs index 5e250020a..d7fd30834 100644 --- a/pumpkin/src/command/commands/difficulty.rs +++ b/pumpkin/src/command/commands/difficulty.rs @@ -59,7 +59,7 @@ impl CommandExecutor for DifficultySetExecutor { } } - server.set_difficulty(difficulty, true); + server.set_difficulty(difficulty, true).await; context .source diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 478624865..484a4bc34 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -1298,13 +1298,26 @@ impl Player { return false; } + let bedrock_dimension = match dimension.minecraft_name { + "minecraft:the_nether" => 1, + "minecraft:the_end" => 2, + _ => 0, + }; self.client - .send_packet_now(&CPlayerSpawnPosition::new( - block_pos, - yaw, - pitch, - dimension.minecraft_name.to_owned(), - )) + .send_packet_now_editioned( + &CPlayerSpawnPosition::new( + block_pos, + yaw, + pitch, + dimension.minecraft_name.to_owned(), + ), + &pumpkin_protocol::bedrock::client::CSetSpawnPosition::new( + 0, // Player spawn + block_pos, + bedrock_dimension, + block_pos, + ), + ) .await; *self.respawn_point.lock().await = Some(RespawnPoint { @@ -1739,22 +1752,41 @@ impl Player { } pub async fn show_title(&self, text: &TextComponent, mode: &TitleMode) { - match self.client.as_ref() { - ClientPlatform::Java(client) => match mode { - TitleMode::Title => client.enqueue_packet(&CTitleText::new(text)).await, - TitleMode::SubTitle => client.enqueue_packet(&CSubtitle::new(text)).await, - TitleMode::ActionBar => client.enqueue_packet(&CActionBar::new(text)).await, - }, - ClientPlatform::Bedrock(client) => { - let action_type = match mode { - TitleMode::Title => 2, - TitleMode::SubTitle => 3, - TitleMode::ActionBar => 4, - }; - client - .send_game_packet( + match mode { + TitleMode::Title => { + self.client + .enqueue_packet_editioned( + &CTitleText::new(text), &pumpkin_protocol::bedrock::client::set_title::CSetTitle::new( - action_type, + 2, + text.clone().get_text(), + 0, + 0, + 0, + ), + ) + .await; + } + TitleMode::SubTitle => { + self.client + .enqueue_packet_editioned( + &CSubtitle::new(text), + &pumpkin_protocol::bedrock::client::set_title::CSetTitle::new( + 3, + text.clone().get_text(), + 0, + 0, + 0, + ), + ) + .await; + } + TitleMode::ActionBar => { + self.client + .enqueue_packet_editioned( + &CActionBar::new(text), + &pumpkin_protocol::bedrock::client::set_title::CSetTitle::new( + 4, text.clone().get_text(), 0, 0, @@ -2384,10 +2416,12 @@ impl Player { let world = self.world(); let level_info = world.level_info.load(); self.client - .enqueue_packet(&CChangeDifficulty::new( - level_info.difficulty as u8, - level_info.difficulty_locked, - )) + .enqueue_packet_editioned( + &CChangeDifficulty::new(level_info.difficulty as u8, level_info.difficulty_locked), + &pumpkin_protocol::bedrock::client::CSetDifficulty::new( + level_info.difficulty as u32, + ), + ) .await; } @@ -2412,24 +2446,12 @@ impl Player { /// Sends the world time to only this player. pub async fn send_time(&self, world: &World) { let l_world = world.level_time.lock().await; - match self.client.as_ref() { - ClientPlatform::Java(java_client) => { - java_client - .enqueue_packet(&CUpdateTime::new( - l_world.world_age, - l_world.time_of_day, - true, - )) - .await; - } - ClientPlatform::Bedrock(bedrock_client) => { - bedrock_client - .send_game_packet(&CSetTime { - time: VarInt(l_world.query_daytime() as _), - }) - .await; - } - } + self.client + .enqueue_packet_editioned( + &CUpdateTime::new(l_world.world_age, l_world.time_of_day, true), + &CSetTime::new(l_world.query_daytime() as _), + ) + .await; } pub async fn unload_watched_chunks(&self, world: &World) { @@ -2652,26 +2674,18 @@ impl Player { return; } - match self.client.as_ref() { - ClientPlatform::Java(client) => { - client - .enqueue_packet(&CSetHealth::new( - self.living_entity.health.load(), - self.hunger_manager.level.load().into(), - self.hunger_manager.saturation.load(), - )) - .await; - } - ClientPlatform::Bedrock(client) => { - client - .send_game_packet( - &pumpkin_protocol::bedrock::client::set_health::CSetHealth::new( - self.living_entity.health.load() as i32, - ), - ) - .await; - } - } + self.client + .enqueue_packet_editioned( + &CSetHealth::new( + self.living_entity.health.load(), + self.hunger_manager.level.load().into(), + self.hunger_manager.saturation.load(), + ), + &pumpkin_protocol::bedrock::client::set_health::CSetHealth::new( + self.living_entity.health.load() as i32, + ), + ) + .await; } pub async fn tick_health(&self) { @@ -2926,25 +2940,14 @@ impl Player { }], )); - match self.client.as_ref() { - crate::net::ClientPlatform::Java(client) => { - client - .enqueue_packet(&CGameEvent::new( - GameEvent::ChangeGameMode, - gamemode as i32 as f32, - )) - .await; - } - crate::net::ClientPlatform::Bedrock(client) => { - client - .send_game_packet( - &pumpkin_protocol::bedrock::client::set_player_gamemode::CSetPlayerGamemode { - gamemode, - }, - ) - .await; - } - } + self.client + .enqueue_packet_editioned( + &CGameEvent::new(GameEvent::ChangeGameMode, gamemode as i32 as f32), + &pumpkin_protocol::bedrock::client::set_player_gamemode::CSetPlayerGamemode { + gamemode, + }, + ) + .await; true } @@ -5134,7 +5137,18 @@ impl InventoryPlayer for Player { packet: &'a CSetSelectedSlot, ) -> PlayerFuture<'a, ()> { Box::pin(async move { - self.client.enqueue_packet(packet).await; + self.client + .enqueue_packet_editioned( + packet, + &pumpkin_protocol::bedrock::client::CPlayerHotbar { + selected_slot: pumpkin_protocol::codec::var_uint::VarUInt( + packet.slot as u32, + ), + container_id: 0, + should_select_block: true, + }, + ) + .await; }) } diff --git a/pumpkin/src/net/mod.rs b/pumpkin/src/net/mod.rs index b2b1dbf1c..82949d46e 100644 --- a/pumpkin/src/net/mod.rs +++ b/pumpkin/src/net/mod.rs @@ -268,6 +268,17 @@ impl ClientPlatform { } } + pub async fn send_packet_now_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + match self { + Self::Java(java) => java.send_packet_now(je_packet).await, + Self::Bedrock(bedrock) => bedrock.send_game_packet(be_packet).await, + } + } + pub async fn send_packet_now_data(&self, data: Bytes) { match self { Self::Java(java) => java.send_packet_now_data(data).await, diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index e2cab8778..cbc7c42a5 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -681,7 +681,7 @@ impl Server { /// # Note /// /// This function does not handle the actual mob spawn options update, which is a TODO item for future implementation. - pub fn set_difficulty(&self, difficulty: Difficulty, force_update: bool) { + pub async fn set_difficulty(&self, difficulty: Difficulty, force_update: bool) { let current_info = self.level_info.load(); if current_info.difficulty_locked && !force_update { return; @@ -701,9 +701,13 @@ impl Server { for world in self.worlds.load().iter() { world.set_difficulty(difficulty); + world + .broadcast_editioned( + &CChangeDifficulty::new(difficulty as u8, locked), + &pumpkin_protocol::bedrock::client::CSetDifficulty::new(difficulty as u32), + ) + .await; } - - self.broadcast_packet_all(&CChangeDifficulty::new(difficulty as u8, locked)); } /// Searches for a player by their username across all worlds. diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 98d7289ef..ecd4f12ea 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -3473,14 +3473,27 @@ impl World { position.y.round() as i32, position.z.round() as i32, )); + let bedrock_dimension = match target_world.dimension.minecraft_name { + "minecraft:the_nether" => 1, + "minecraft:the_end" => 2, + _ => 0, + }; player .client - .send_packet_now(&CPlayerSpawnPosition::new( - spawn_block_pos, - yaw, - pitch, - target_world.dimension.minecraft_name.to_string(), - )) + .send_packet_now_editioned( + &CPlayerSpawnPosition::new( + spawn_block_pos, + yaw, + pitch, + target_world.dimension.minecraft_name.to_string(), + ), + &pumpkin_protocol::bedrock::client::CSetSpawnPosition::new( + 1, // World spawn + spawn_block_pos, + bedrock_dimension, + spawn_block_pos, + ), + ) .await; player.living_entity.reset_state().await;