Add Particle command

This commit is contained in:
Alexander Medvedev 2025-02-11 20:43:16 +01:00
parent af0573d9b8
commit 06334ec658
13 changed files with 296 additions and 51 deletions

View File

@ -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<Self> {
match name {
#(#type_name_pairs,)*
#type_from_name
_ => None
}
}

View File

@ -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<String> = 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::<TokenStream>();
let type_to_name = &particle
.iter()
.map(|particle| {
let id = &particle;
let name = format_ident!("{}", particle.to_pascal_case());
quote! {
Self::#name => #id,
}
})
.collect::<TokenStream>();
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<Self> {
match name {
#type_from_name
_ => None
}
}
pub const fn to_name(&self) -> &'static str {
match self {
#type_to_name
}
}
}
}
}

View File

@ -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<f64>,
offset: Vector3<f64>,
offset: Vector3<f32>,
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<f64>,
offset: Vector3<f64>,
offset: Vector3<f32>,
max_speed: f32,
particle_count: i32,
pariticle_id: VarInt,
data: &'a [u8],
) -> Self {
Self {
long_distance,
force_spawn,
important,
position,
offset,
max_speed,

View File

@ -52,7 +52,7 @@ impl ArgumentConsumer for DamageTypeArgumentConsumer {
impl DefaultNameArgConsumer for DamageTypeArgumentConsumer {
fn default_name(&self) -> &'static str {
"damageType"
"damage_type"
}
}

View File

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

View File

@ -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<SuggestionProviders> {
None
}
}
#[async_trait]
impl ArgumentConsumer for ParticleArgumentConsumer {
async fn consume<'a>(
&'a self,
_sender: &CommandSender<'a>,
_server: &'a Server,
args: &mut RawArgs<'a>,
) -> Option<Arg<'a>> {
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<Option<Vec<CommandSuggestion>>, 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<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::Particle(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

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

View File

@ -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::<f32>::find_arg(args, ARG_SPEED);
let count = BoundedNumArgumentConsumer::<i32>::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<f32> = 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::<f32>::new().min(0.0),
)
.execute(ParticleExecutor)
.then(
argument(
ARG_COUNT,
BoundedNumArgumentConsumer::<i32>::new().min(0),
)
.execute(ParticleExecutor),
),
),
),
),
// TODO: Add NBT
)
}

View File

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

View File

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

View File

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

View File

@ -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<f64>,
offset: Vector3<f32>,
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,

View File

@ -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<f64>,
offset: Vector3<f32>,
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<f64>) {
self.play_sound_raw(sound as u16, category, position, 1.0, 1.0)
.await;