Skip to content

Commit

Permalink
Add Particle command
Browse files Browse the repository at this point in the history
  • Loading branch information
Snowiiii committed Feb 11, 2025
1 parent af0573d commit 06334ec
Show file tree
Hide file tree
Showing 13 changed files with 296 additions and 51 deletions.
18 changes: 6 additions & 12 deletions pumpkin-data/build/damage_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
}
Expand Down
39 changes: 38 additions & 1 deletion pumpkin-data/build/particle.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use heck::ToPascalCase;
use proc_macro2::TokenStream;
use quote::quote;
use quote::{format_ident, quote};

use crate::array_to_tokenstream;

Expand All @@ -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
}
}
}
}
}
14 changes: 9 additions & 5 deletions pumpkin-protocol/src/client/play/particle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,32 @@ 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,
data: &'a [u8],
}

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,
Expand Down
2 changes: 1 addition & 1 deletion pumpkin/src/command/args/damage_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl ArgumentConsumer for DamageTypeArgumentConsumer {

impl DefaultNameArgConsumer for DamageTypeArgumentConsumer {
fn default_name(&self) -> &'static str {
"damageType"
"damage_type"
}
}

Expand Down
3 changes: 3 additions & 0 deletions pumpkin/src/command/args/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -93,6 +95,7 @@ pub enum Arg<'a> {
Block(&'a str),
BossbarColor(BossbarColor),
BossbarStyle(BossbarDivisions),
Particle(Particle),
Msg(String),
TextComponent(TextComponent),
Time(i32),
Expand Down
68 changes: 68 additions & 0 deletions pumpkin/src/command/args/particle.rs
Original file line number Diff line number Diff line change
@@ -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()))),
}
}
}
1 change: 1 addition & 0 deletions pumpkin/src/command/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
92 changes: 92 additions & 0 deletions pumpkin/src/command/commands/particle.rs
Original file line number Diff line number Diff line change
@@ -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
)
}
Loading

0 comments on commit 06334ec

Please sign in to comment.