Alexander Medvedev 2026-07-16 19:56:53 +02:00
parent 21e7f1418a
commit 55219f870f
13 changed files with 264 additions and 98 deletions

View File

@ -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::*;

View File

@ -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),
}
}
}

View File

@ -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<W: 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(())
}
}

View File

@ -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,

View File

@ -177,9 +177,17 @@ impl StructureTemplate {
}
fn parse_palette(compound: &NbtCompound) -> Result<Vec<PaletteEntry>, 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());

View File

@ -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::<TargetSelector>()
.expect("should parse");
assert!(s.includes_entities());
assert_eq!(s.conditions.len(), 2);
}
}

View File

@ -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<f64>),
/// A predicate to check the entity type. This check can also be inverted.
EntityType(&'static EntityType, bool),
/// Used to combine sub-predicates.
AllOf(Vec<Self>),
@ -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)),
}
}

View File

@ -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()))

View File

@ -59,7 +59,7 @@ impl CommandExecutor for DifficultySetExecutor {
}
}
server.set_difficulty(difficulty, true);
server.set_difficulty(difficulty, true).await;
context
.source

View File

@ -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;
})
}

View File

@ -268,6 +268,17 @@ impl ClientPlatform {
}
}
pub async fn send_packet_now_editioned<J: ClientPacket, B: BClientPacket>(
&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,

View File

@ -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.

View File

@ -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;