From 06334ec658f5fe2dcde7d082cf5a6515b65a4e50 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 11 Feb 2025 20:43:16 +0100 Subject: [PATCH] Add Particle command --- pumpkin-data/build/damage_type.rs | 18 ++-- pumpkin-data/build/particle.rs | 39 ++++++++- pumpkin-protocol/src/client/play/particle.rs | 14 +-- pumpkin/src/command/args/damage_type.rs | 2 +- pumpkin/src/command/args/mod.rs | 3 + pumpkin/src/command/args/particle.rs | 68 +++++++++++++++ pumpkin/src/command/commands/mod.rs | 1 + pumpkin/src/command/commands/particle.rs | 92 ++++++++++++++++++++ pumpkin/src/command/mod.rs | 41 +++++---- pumpkin/src/entity/combat.rs | 16 ++-- pumpkin/src/entity/mod.rs | 7 ++ pumpkin/src/entity/player.rs | 29 +++++- pumpkin/src/world/mod.rs | 17 ++++ 13 files changed, 296 insertions(+), 51 deletions(-) create mode 100644 pumpkin/src/command/args/particle.rs create mode 100644 pumpkin/src/command/commands/particle.rs diff --git a/pumpkin-data/build/damage_type.rs b/pumpkin-data/build/damage_type.rs index f0b6aa570..c42c4394f 100644 --- a/pumpkin-data/build/damage_type.rs +++ b/pumpkin-data/build/damage_type.rs @@ -55,12 +55,15 @@ pub(crate) fn build() -> TokenStream { .expect("Failed to parse damage_type.json"); let mut constants = Vec::new(); - let mut enum_variants = Vec::new(); + let mut type_from_name = TokenStream::new(); for (name, entry) in damage_types { let const_ident = format_ident!("{}", name.to_shouty_snake_case()); + let resource_name = name.to_lowercase(); - enum_variants.push(const_ident.clone()); + type_from_name.extend(quote! { + #resource_name => Some(Self::#const_ident), + }); let data = &entry.components; let death_message_type = match &data.death_message_type { @@ -100,15 +103,6 @@ pub(crate) fn build() -> TokenStream { }); } - let type_name_pairs = enum_variants.iter().map(|variant| { - let name = variant.to_string(); - let name_lowercase = name.to_lowercase(); - let resource_name = format!("minecraft:{}", name_lowercase); - quote! { - #resource_name => Some(Self::#variant) - } - }); - quote! { #[derive(Clone, Copy, Debug, PartialEq)] pub struct DamageType { @@ -150,7 +144,7 @@ pub(crate) fn build() -> TokenStream { #[doc = r" Try to parse a damage type from a resource location string"] pub fn from_name(name: &str) -> Option { match name { - #(#type_name_pairs,)* + #type_from_name _ => None } } diff --git a/pumpkin-data/build/particle.rs b/pumpkin-data/build/particle.rs index d94475b60..15905c0e7 100644 --- a/pumpkin-data/build/particle.rs +++ b/pumpkin-data/build/particle.rs @@ -1,5 +1,6 @@ +use heck::ToPascalCase; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use crate::array_to_tokenstream; @@ -9,11 +10,47 @@ pub(crate) fn build() -> TokenStream { let particle: Vec = serde_json::from_str(include_str!("../../assets/particles.json")) .expect("Failed to parse particles.json"); let variants = array_to_tokenstream(&particle); + let type_from_name = &particle + .iter() + .map(|particle| { + let id = &particle; + let name = format_ident!("{}", particle.to_pascal_case()); + quote! { + #id => Some(Self::#name), + } + }) + .collect::(); + let type_to_name = &particle + .iter() + .map(|particle| { + let id = &particle; + let name = format_ident!("{}", particle.to_pascal_case()); + + quote! { + Self::#name => #id, + } + }) + .collect::(); quote! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Particle { #variants } + + impl Particle { + #[doc = r" Try to parse Particle from a resource location string"] + pub fn from_name(name: &str) -> Option { + match name { + #type_from_name + _ => None + } + } + pub const fn to_name(&self) -> &'static str { + match self { + #type_to_name + } + } + } } } diff --git a/pumpkin-protocol/src/client/play/particle.rs b/pumpkin-protocol/src/client/play/particle.rs index 57a06309e..0b35aca01 100644 --- a/pumpkin-protocol/src/client/play/particle.rs +++ b/pumpkin-protocol/src/client/play/particle.rs @@ -8,10 +8,11 @@ use crate::VarInt; #[derive(Serialize)] #[client_packet(PLAY_LEVEL_PARTICLES)] pub struct CParticle<'a> { + force_spawn: bool, /// If true, particle distance increases from 256 to 65536. - long_distance: bool, + important: bool, position: Vector3, - offset: Vector3, + offset: Vector3, max_speed: f32, particle_count: i32, pariticle_id: VarInt, @@ -19,17 +20,20 @@ pub struct CParticle<'a> { } impl<'a> CParticle<'a> { + #[expect(clippy::too_many_arguments)] pub fn new( - long_distance: bool, + force_spawn: bool, + important: bool, position: Vector3, - offset: Vector3, + offset: Vector3, max_speed: f32, particle_count: i32, pariticle_id: VarInt, data: &'a [u8], ) -> Self { Self { - long_distance, + force_spawn, + important, position, offset, max_speed, diff --git a/pumpkin/src/command/args/damage_type.rs b/pumpkin/src/command/args/damage_type.rs index 5c9df7255..e40db91a5 100644 --- a/pumpkin/src/command/args/damage_type.rs +++ b/pumpkin/src/command/args/damage_type.rs @@ -52,7 +52,7 @@ impl ArgumentConsumer for DamageTypeArgumentConsumer { impl DefaultNameArgConsumer for DamageTypeArgumentConsumer { fn default_name(&self) -> &'static str { - "damageType" + "damage_type" } } diff --git a/pumpkin/src/command/args/mod.rs b/pumpkin/src/command/args/mod.rs index 2a693c7ca..7bc02729c 100644 --- a/pumpkin/src/command/args/mod.rs +++ b/pumpkin/src/command/args/mod.rs @@ -3,6 +3,7 @@ use std::{collections::HashMap, hash::Hash, sync::Arc}; use async_trait::async_trait; use bounded_num::{NotInBounds, Number}; use pumpkin_data::damage::DamageType; +use pumpkin_data::particle::Particle; use pumpkin_data::sound::SoundCategory; use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; use pumpkin_util::text::TextComponent; @@ -32,6 +33,7 @@ pub mod entity; pub mod gamemode; pub mod item; pub mod message; +pub mod particle; pub mod players; pub mod position_2d; pub mod position_3d; @@ -93,6 +95,7 @@ pub enum Arg<'a> { Block(&'a str), BossbarColor(BossbarColor), BossbarStyle(BossbarDivisions), + Particle(Particle), Msg(String), TextComponent(TextComponent), Time(i32), diff --git a/pumpkin/src/command/args/particle.rs b/pumpkin/src/command/args/particle.rs new file mode 100644 index 000000000..317375fef --- /dev/null +++ b/pumpkin/src/command/args/particle.rs @@ -0,0 +1,68 @@ +use async_trait::async_trait; +use pumpkin_data::particle::Particle; +use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders}; + +use crate::command::{ + args::{Arg, ArgumentConsumer, DefaultNameArgConsumer, FindArg, GetClientSideArgParser}, + dispatcher::CommandError, + tree::RawArgs, + CommandSender, +}; +use crate::server::Server; + +pub struct ParticleArgumentConsumer; + +impl GetClientSideArgParser for ParticleArgumentConsumer { + fn get_client_side_parser(&self) -> ArgumentType { + ArgumentType::Resource { + identifier: "particle_type", + } + } + + fn get_client_side_suggestion_type_override(&self) -> Option { + None + } +} + +#[async_trait] +impl ArgumentConsumer for ParticleArgumentConsumer { + async fn consume<'a>( + &'a self, + _sender: &CommandSender<'a>, + _server: &'a Server, + args: &mut RawArgs<'a>, + ) -> Option> { + let name = args.pop()?; + + // Create a static damage type first + let particle = Particle::from_name(&name.replace("minecraft:", ""))?; + // Find matching static damage type from values array + Some(Arg::Particle(particle)) + } + + async fn suggest<'a>( + &'a self, + _sender: &CommandSender<'a>, + _server: &'a Server, + _input: &'a str, + ) -> Result>, CommandError> { + Ok(None) + } +} + +impl DefaultNameArgConsumer for ParticleArgumentConsumer { + fn default_name(&self) -> &'static str { + "particle_type" + } +} + +impl<'a> FindArg<'a> for ParticleArgumentConsumer { + type Data = &'a Particle; + + fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result { + match args.get(name) { + Some(Arg::Particle(data)) => Ok(data), + _ => Err(CommandError::InvalidConsumption(Some(name.to_string()))), + } + } +} diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index 803e9b485..0459f26ac 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -18,6 +18,7 @@ pub mod msg; pub mod op; pub mod pardon; pub mod pardonip; +pub mod particle; pub mod playsound; pub mod plugin; pub mod plugins; diff --git a/pumpkin/src/command/commands/particle.rs b/pumpkin/src/command/commands/particle.rs new file mode 100644 index 000000000..c695dbf6d --- /dev/null +++ b/pumpkin/src/command/commands/particle.rs @@ -0,0 +1,92 @@ +use async_trait::async_trait; +use pumpkin_util::{math::vector3::Vector3, text::TextComponent}; + +use crate::command::{ + args::{ + bounded_num::BoundedNumArgumentConsumer, particle::ParticleArgumentConsumer, + position_3d::Position3DArgumentConsumer, ConsumedArgs, FindArg, + }, + tree::{builder::argument, CommandTree}, + CommandError, CommandExecutor, CommandSender, +}; +const NAMES: [&str; 1] = ["particle"]; + +const DESCRIPTION: &str = "Spawns a Particle at position."; + +const ARG_NAME: &str = "name"; + +const ARG_POS: &str = "pos"; +const ARG_DELTA: &str = "delta"; +const ARG_SPEED: &str = "speed"; +const ARG_COUNT: &str = "count"; + +struct ParticleExecutor; + +#[async_trait] +impl CommandExecutor for ParticleExecutor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let particle = ParticleArgumentConsumer::find_arg(args, ARG_NAME)?; + let pos = Position3DArgumentConsumer::find_arg(args, ARG_POS); + let delta = Position3DArgumentConsumer::find_arg(args, ARG_DELTA); + let speed = BoundedNumArgumentConsumer::::find_arg(args, ARG_SPEED); + let count = BoundedNumArgumentConsumer::::find_arg(args, ARG_COUNT); + + // TODO: Make this work in console + if let Some(player) = sender.as_player() { + let pos = pos.unwrap_or(player.living_entity.entity.pos.load()); + let delta = delta.unwrap_or(Vector3::new(0.0, 0.0, 0.0)); + let delta: Vector3 = Vector3::new(delta.x as f32, delta.y as f32, delta.z as f32); + let speed = speed.unwrap_or(Ok(0.0)).unwrap_or(0.0); + let count = count.unwrap_or(Ok(0)).unwrap_or(0); + + player + .world() + .await + .spawn_particle(pos, delta, speed, count, *particle) + .await; + sender + .send_message(TextComponent::translate( + "commands.particle.success", + [TextComponent::text(format!("{particle:?}"))].into(), + )) + .await; + } + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).then( + argument(ARG_NAME, ParticleArgumentConsumer) + .execute(ParticleExecutor) + .then( + argument(ARG_POS, Position3DArgumentConsumer) + .execute(ParticleExecutor) + .then( + argument(ARG_DELTA, Position3DArgumentConsumer) + .execute(ParticleExecutor) + .then( + argument( + ARG_SPEED, + BoundedNumArgumentConsumer::::new().min(0.0), + ) + .execute(ParticleExecutor) + .then( + argument( + ARG_COUNT, + BoundedNumArgumentConsumer::::new().min(0), + ) + .execute(ParticleExecutor), + ), + ), + ), + ), + // TODO: Add NBT + ) +} diff --git a/pumpkin/src/command/mod.rs b/pumpkin/src/command/mod.rs index 3bcf975fe..175cd1a66 100644 --- a/pumpkin/src/command/mod.rs +++ b/pumpkin/src/command/mod.rs @@ -11,8 +11,8 @@ use args::ConsumedArgs; use async_trait::async_trait; use commands::{ ban, banip, banlist, clear, damage, deop, experience, fill, gamemode, give, help, kick, kill, - list, me, msg, op, pardon, pardonip, playsound, plugin, plugins, pumpkin, say, setblock, stop, - summon, teleport, time, title, weather, worldborder, + list, me, msg, op, pardon, pardonip, particle, playsound, plugin, plugins, pumpkin, say, + setblock, stop, summon, teleport, time, title, weather, worldborder, }; use dispatcher::CommandError; use pumpkin_util::math::vector3::Vector3; @@ -110,41 +110,46 @@ impl CommandSender<'_> { pub fn default_dispatcher() -> CommandDispatcher { let mut dispatcher = CommandDispatcher::default(); + // Zerp dispatcher.register(pumpkin::init_command_tree(), PermissionLvl::Zero); - dispatcher.register(bossbar::init_command_tree(), PermissionLvl::Two); - dispatcher.register(say::init_command_tree(), PermissionLvl::Two); - dispatcher.register(gamemode::init_command_tree(), PermissionLvl::Two); - dispatcher.register(stop::init_command_tree(), PermissionLvl::Four); dispatcher.register(help::init_command_tree(), PermissionLvl::Zero); + dispatcher.register(list::init_command_tree(), PermissionLvl::Zero); + dispatcher.register(transfer::init_command_tree(), PermissionLvl::Zero); + dispatcher.register(me::init_command_tree(), PermissionLvl::Zero); + dispatcher.register(msg::init_command_tree(), PermissionLvl::Zero); + // Two dispatcher.register(kill::init_command_tree(), PermissionLvl::Two); - dispatcher.register(kick::init_command_tree(), PermissionLvl::Three); - dispatcher.register(plugin::init_command_tree(), PermissionLvl::Three); - dispatcher.register(plugins::init_command_tree(), PermissionLvl::Three); dispatcher.register(worldborder::init_command_tree(), PermissionLvl::Two); dispatcher.register(teleport::init_command_tree(), PermissionLvl::Two); dispatcher.register(time::init_command_tree(), PermissionLvl::Two); dispatcher.register(give::init_command_tree(), PermissionLvl::Two); - dispatcher.register(list::init_command_tree(), PermissionLvl::Zero); dispatcher.register(clear::init_command_tree(), PermissionLvl::Two); dispatcher.register(setblock::init_command_tree(), PermissionLvl::Two); dispatcher.register(seed::init_command_tree(), PermissionLvl::Two); - dispatcher.register(transfer::init_command_tree(), PermissionLvl::Zero); dispatcher.register(fill::init_command_tree(), PermissionLvl::Two); - dispatcher.register(op::init_command_tree(), PermissionLvl::Three); - dispatcher.register(deop::init_command_tree(), PermissionLvl::Three); - dispatcher.register(me::init_command_tree(), PermissionLvl::Zero); dispatcher.register(playsound::init_command_tree(), PermissionLvl::Two); dispatcher.register(title::init_command_tree(), PermissionLvl::Two); dispatcher.register(summon::init_command_tree(), PermissionLvl::Two); - dispatcher.register(msg::init_command_tree(), PermissionLvl::Zero); + dispatcher.register(experience::init_command_tree(), PermissionLvl::Two); + dispatcher.register(weather::init_command_tree(), PermissionLvl::Two); + dispatcher.register(particle::init_command_tree(), PermissionLvl::Two); + dispatcher.register(damage::init_command_tree(), PermissionLvl::Two); + dispatcher.register(bossbar::init_command_tree(), PermissionLvl::Two); + dispatcher.register(say::init_command_tree(), PermissionLvl::Two); + dispatcher.register(gamemode::init_command_tree(), PermissionLvl::Two); + // Three + dispatcher.register(op::init_command_tree(), PermissionLvl::Three); + dispatcher.register(deop::init_command_tree(), PermissionLvl::Three); + dispatcher.register(kick::init_command_tree(), PermissionLvl::Three); + dispatcher.register(plugin::init_command_tree(), PermissionLvl::Three); + dispatcher.register(plugins::init_command_tree(), PermissionLvl::Three); dispatcher.register(ban::init_command_tree(), PermissionLvl::Three); dispatcher.register(banip::init_command_tree(), PermissionLvl::Three); dispatcher.register(banlist::init_command_tree(), PermissionLvl::Three); dispatcher.register(pardon::init_command_tree(), PermissionLvl::Three); dispatcher.register(pardonip::init_command_tree(), PermissionLvl::Three); - dispatcher.register(experience::init_command_tree(), PermissionLvl::Two); - dispatcher.register(weather::init_command_tree(), PermissionLvl::Two); - dispatcher.register(damage::init_command_tree(), PermissionLvl::Two); + // Four + dispatcher.register(stop::init_command_tree(), PermissionLvl::Four); dispatcher } diff --git a/pumpkin/src/entity/combat.rs b/pumpkin/src/entity/combat.rs index a9f57d98b..a11401124 100644 --- a/pumpkin/src/entity/combat.rs +++ b/pumpkin/src/entity/combat.rs @@ -2,10 +2,7 @@ use pumpkin_data::{ particle::Particle, sound::{Sound, SoundCategory}, }; -use pumpkin_protocol::{ - client::play::{CEntityVelocity, CParticle}, - codec::var_int::VarInt, -}; +use pumpkin_protocol::{client::play::CEntityVelocity, codec::var_int::VarInt}; use pumpkin_util::math::vector3::Vector3; use pumpkin_world::item::ItemStack; @@ -93,19 +90,16 @@ pub async fn spawn_sweep_particle(attacker_entity: &Entity, world: &World, pos: let e = f64::from((yaw.to_radians()).cos()); let scale = 0.5; - // TODO: use entity height - let body_y = pos.y * 2.0 * scale; + let body_y = pos.y + f64::from(attacker_entity.height()) * scale; world - .broadcast_packet_all(&CParticle::new( - false, + .spawn_particle( Vector3::new(pos.x + d, body_y, pos.z + e), Vector3::new(0.0, 0.0, 0.0), 0.0, 0, - VarInt(Particle::SweepAttack as i32), // sweep - &[], - )) + Particle::SweepAttack, + ) .await; } diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index 02175a306..0386e8e5e 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -273,6 +273,13 @@ impl Entity { entity_vel, ) } + pub fn width(&self) -> f32 { + self.bounding_box_size.load().width + } + + pub fn height(&self) -> f32 { + self.bounding_box_size.load().height + } /// Applies knockback to the entity, following vanilla Minecraft's mechanics. /// diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 5a105e124..430b1bf3c 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -14,6 +14,7 @@ use pumpkin_data::{ damage::DamageType, entity::EntityType, item::Operation, + particle::Particle, sound::{Sound, SoundCategory}, }; use pumpkin_inventory::player::PlayerInventory; @@ -22,9 +23,9 @@ use pumpkin_protocol::{ bytebuf::packet::Packet, client::play::{ CActionBar, CCombatDeath, CDisguisedChatMessage, CEntityStatus, CGameEvent, CHurtAnimation, - CKeepAlive, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, CPlayerPosition, - CRespawn, CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, CTitleText, - CUnloadChunk, GameEvent, MetaDataType, PlayerAction, + CKeepAlive, CParticle, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, + CPlayerPosition, CRespawn, CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, + CTitleText, CUnloadChunk, GameEvent, MetaDataType, PlayerAction, }, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, @@ -385,6 +386,28 @@ impl Player { } } + pub async fn spawn_particle( + &self, + position: Vector3, + offset: Vector3, + max_speed: f32, + particle_count: i32, + pariticle: Particle, + ) { + self.client + .send_packet(&CParticle::new( + false, + false, + position, + offset, + max_speed, + particle_count, + VarInt(pariticle as i32), + &[], + )) + .await; + } + pub async fn play_sound( &self, sound_id: u16, diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index 2c8f34870..904bfbc11 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -23,6 +23,7 @@ use border::Worldborder; use pumpkin_config::BasicConfiguration; use pumpkin_data::{ entity::EntityType, + particle::Particle, sound::{Sound, SoundCategory}, world::WorldEvent, }; @@ -187,6 +188,22 @@ impl World { } } + pub async fn spawn_particle( + &self, + position: Vector3, + offset: Vector3, + max_speed: f32, + particle_count: i32, + pariticle: Particle, + ) { + let players = self.players.lock().await; + for (_, player) in players.iter() { + player + .spawn_particle(position, offset, max_speed, particle_count, pariticle) + .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;