Add damage types & damage command (#526)

This commit is contained in:
drakeerv
2025-02-02 05:22:49 -05:00
committed by GitHub
parent 1729f58a6f
commit 09d6eb6da2
13 changed files with 467 additions and 12 deletions

1
assets/damage_type.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,7 @@ use syn::Ident;
mod biome;
mod chunk_status;
mod damage_type;
mod entity_pose;
mod entity_type;
mod game_event;
@@ -34,6 +35,7 @@ pub fn main() {
write_generated_file(entity_type::build(), "entity_type.rs");
write_generated_file(noise_parameter::build(), "noise_parameter.rs");
write_generated_file(biome::build(), "biome.rs");
write_generated_file(damage_type::build(), "damage_type.rs");
write_generated_file(message_type::build(), "message_type.rs");
}

View File

@@ -0,0 +1,141 @@
use heck::{ToPascalCase, ToShoutySnakeCase};
use proc_macro2::TokenStream;
use quote::quote;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Deserialize)]
struct DamageTypeEntry {
id: u32,
components: DamageTypeData,
}
#[derive(Deserialize)]
pub struct DamageTypeData {
death_message_type: Option<String>,
exhaustion: f32,
message_id: String,
scaling: String,
}
pub(crate) fn build() -> TokenStream {
println!("cargo:rerun-if-changed=../assets/damage_type.json");
let damage_types: HashMap<String, DamageTypeEntry> =
serde_json::from_str(include_str!("../../assets/damage_type.json"))
.expect("Failed to parse damage_type.json");
let mut constants = Vec::new();
let mut enum_variants = Vec::new();
for (name, entry) in damage_types {
let const_ident = crate::ident(name.to_shouty_snake_case());
let enum_ident = crate::ident(name.to_pascal_case());
enum_variants.push(enum_ident.clone());
let data = &entry.components;
let death_message_type = match &data.death_message_type {
Some(msg) => quote! { Some(#msg) },
None => quote! { None },
};
let exhaustion = data.exhaustion;
let message_id = &data.message_id;
let scaling = &data.scaling;
let id = entry.id;
constants.push(quote! {
pub const #const_ident: DamageTypeData = DamageTypeData {
death_message_type: #death_message_type,
exhaustion: #exhaustion,
message_id: #message_id,
scaling: #scaling,
id: #id,
};
});
}
let enum_arms = enum_variants.iter().map(|variant| {
let const_name = variant.to_string().to_shouty_snake_case();
let const_ident = crate::ident(&const_name);
quote! {
DamageType::#variant => &#const_ident,
}
});
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)
}
});
let type_to_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! {
Self::#variant => #resource_name
}
});
// Create array of all variants for values() method
let variant_array = enum_variants.iter().map(|variant| {
quote! {
DamageType::#variant
}
});
quote! {
#[derive(Clone, Debug)]
pub struct DamageTypeData {
pub death_message_type: Option<&'static str>,
pub exhaustion: f32,
pub message_id: &'static str,
pub scaling: &'static str,
pub id: u32,
}
#(#constants)*
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum DamageType {
#(#enum_variants,)*
}
impl DamageType {
pub const fn data(&self) -> &'static DamageTypeData {
match self {
#(#enum_arms)*
}
}
#[doc = r" Get all possible damage types"]
pub fn values() -> &'static [DamageType] {
static VALUES: &[DamageType] = &[
#(#variant_array,)*
];
VALUES
}
#[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,)*
_ => None
}
}
#[doc = r" Get the resource location string for this damage type"]
pub const fn to_name(&self) -> &'static str {
match self {
#(#type_to_name_pairs,)*
}
}
}
}
}

View File

@@ -38,3 +38,7 @@ pub mod world {
pub mod scoreboard {
include!(concat!(env!("OUT_DIR"), "/scoreboard_slot.rs"));
}
pub mod damage {
include!(concat!(env!("OUT_DIR"), "/damage_type.rs"));
}

View File

@@ -0,0 +1,74 @@
use async_trait::async_trait;
use pumpkin_data::damage::DamageType;
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 DamageTypeArgumentConsumer;
impl GetClientSideArgParser for DamageTypeArgumentConsumer {
fn get_client_side_parser(&self) -> ArgumentType {
ArgumentType::ResourceLocation
}
fn get_client_side_suggestion_type_override(&self) -> Option<SuggestionProviders> {
Some(SuggestionProviders::AskServer)
}
}
#[async_trait]
impl ArgumentConsumer for DamageTypeArgumentConsumer {
async fn consume<'a>(
&'a self,
_sender: &CommandSender<'a>,
_server: &'a Server,
args: &mut RawArgs<'a>,
) -> Option<Arg<'a>> {
let s = args.pop()?;
// Create a static damage type first
let damage_type = DamageType::from_name(s)?;
// Find matching static damage type from values array
DamageType::values()
.iter()
.find(|&&dt| std::mem::discriminant(&dt) == std::mem::discriminant(&damage_type))
.map(Arg::DamageType)
}
async fn suggest<'a>(
&'a self,
_sender: &CommandSender<'a>,
_server: &'a Server,
_input: &'a str,
) -> Result<Option<Vec<CommandSuggestion>>, CommandError> {
// Get all available damage types
let suggestions = DamageType::values()
.iter()
.map(|dt| CommandSuggestion::new(dt.to_name().to_string(), None))
.collect();
Ok(Some(suggestions))
}
}
impl DefaultNameArgConsumer for DamageTypeArgumentConsumer {
fn default_name(&self) -> &'static str {
"damageType"
}
}
impl<'a> FindArg<'a> for DamageTypeArgumentConsumer {
type Data = &'a DamageType;
fn find_arg(args: &'a super::ConsumedArgs, name: &str) -> Result<Self::Data, CommandError> {
match args.get(name) {
Some(Arg::DamageType(data)) => Ok(data),
_ => Err(CommandError::InvalidConsumption(Some(name.to_string()))),
}
}
}

View File

@@ -2,6 +2,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::sound::SoundCategory;
use pumpkin_protocol::client::play::{ArgumentType, CommandSuggestion, SuggestionProviders};
use pumpkin_util::text::TextComponent;
@@ -25,6 +26,7 @@ pub mod bossbar_style;
pub mod bounded_num;
pub mod command;
mod coordinate;
pub mod damage_type;
pub mod entities;
pub mod entity;
pub mod gamemode;
@@ -97,6 +99,7 @@ pub enum Arg<'a> {
#[allow(unused)]
Simple(&'a str),
SoundCategory(SoundCategory),
DamageType(&'a DamageType),
}
/// see [`crate::commands::tree::builder::argument`] and [`CommandTree::execute`]/[`crate::commands::tree::builder::NonLeafNodeBuilder::execute`]

View File

@@ -0,0 +1,188 @@
use async_trait::async_trait;
use pumpkin_data::damage::DamageType;
use pumpkin_util::text::{
color::{Color, NamedColor},
TextComponent,
};
use crate::command::{
args::{
bounded_num::BoundedNumArgumentConsumer, damage_type::DamageTypeArgumentConsumer,
entity::EntityArgumentConsumer, position_3d::Position3DArgumentConsumer, Arg, ConsumedArgs,
FindArg,
},
tree::builder::{argument, literal},
tree::CommandTree,
CommandError, CommandExecutor, CommandSender,
};
const NAMES: [&str; 1] = ["damage"];
const DESCRIPTION: &str = "Deals damage to entities";
const ARG_TARGET: &str = "target";
const ARG_AMOUNT: &str = "amount";
const ARG_DAMAGE_TYPE: &str = "damageType";
const ARG_LOCATION: &str = "location";
const ARG_ENTITY: &str = "entity";
const ARG_CAUSE: &str = "cause";
fn amount_consumer() -> BoundedNumArgumentConsumer<f32> {
BoundedNumArgumentConsumer::new().name(ARG_AMOUNT).min(0.0)
}
struct DamageLocationExecutor;
struct DamageEntityExecutor(bool);
async fn send_damage_result(
sender: &mut CommandSender<'_>,
success: bool,
amount: f32,
target_name: String,
) {
if !success {
sender
.send_message(
TextComponent::translate("commands.damage.invulnerable", [].into())
.color(Color::Named(NamedColor::Red)),
)
.await;
return;
}
sender
.send_message(TextComponent::translate(
"commands.damage.success",
[
TextComponent::text(amount.to_string()),
TextComponent::text(target_name),
]
.into(),
))
.await;
}
#[async_trait]
impl CommandExecutor for DamageLocationExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
_server: &crate::server::Server,
args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?;
let Ok(Ok(amount)) = BoundedNumArgumentConsumer::<f32>::find_arg(args, ARG_AMOUNT) else {
sender
.send_message(
TextComponent::text("Invalid damage amount")
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
};
let damage_type = args
.get(ARG_DAMAGE_TYPE)
.map_or(DamageType::Generic, |arg| match arg {
Arg::DamageType(dt) => **dt,
_ => DamageType::Generic,
});
let location = Position3DArgumentConsumer::find_arg(args, ARG_LOCATION)?;
let success = target
.living_entity
.damage_with_context(amount, damage_type, Some(location), None, None)
.await;
send_damage_result(sender, success, amount, target.gameprofile.name.clone()).await;
Ok(())
}
}
#[async_trait]
impl CommandExecutor for DamageEntityExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
_server: &crate::server::Server,
args: &ConsumedArgs<'a>,
) -> Result<(), CommandError> {
let target = EntityArgumentConsumer::find_arg(args, ARG_TARGET)?;
let Ok(Ok(amount)) = BoundedNumArgumentConsumer::<f32>::find_arg(args, ARG_AMOUNT) else {
sender
.send_message(
TextComponent::text("Invalid damage amount")
.color(Color::Named(NamedColor::Red)),
)
.await;
return Ok(());
};
let damage_type = args
.get(ARG_DAMAGE_TYPE)
.map_or(DamageType::Generic, |arg| match arg {
Arg::DamageType(dt) => **dt,
_ => DamageType::Generic,
});
let source = EntityArgumentConsumer::find_arg(args, ARG_ENTITY).ok();
let cause = if self.0 {
EntityArgumentConsumer::find_arg(args, ARG_CAUSE).ok()
} else {
None
};
let success = target
.living_entity
.damage_with_context(
amount,
damage_type,
None,
source.as_ref().map(|e| &e.living_entity.entity),
cause.as_ref().map(|e| &e.living_entity.entity),
)
.await;
send_damage_result(sender, success, amount, target.gameprofile.name.clone()).await;
Ok(())
}
}
pub fn init_command_tree() -> CommandTree {
CommandTree::new(NAMES, DESCRIPTION).then(
argument(ARG_TARGET, EntityArgumentConsumer).then(
argument(ARG_AMOUNT, amount_consumer())
// Basic damage
.execute(DamageEntityExecutor(false))
// With damage type
.then(
argument(ARG_DAMAGE_TYPE, DamageTypeArgumentConsumer)
.execute(DamageEntityExecutor(false))
// At location
.then(
literal("at").then(
argument(ARG_LOCATION, Position3DArgumentConsumer)
.execute(DamageLocationExecutor),
),
)
// By entity
.then(
literal("by").then(
argument(ARG_ENTITY, EntityArgumentConsumer)
.execute(DamageEntityExecutor(false))
// From cause
.then(
literal("from").then(
argument(ARG_CAUSE, EntityArgumentConsumer)
.execute(DamageEntityExecutor(true)),
),
),
),
),
),
),
)
}

View File

@@ -3,6 +3,7 @@ pub mod banip;
pub mod banlist;
pub mod bossbar;
pub mod clear;
pub mod damage;
pub mod deop;
pub mod experience;
pub mod fill;

View File

@@ -10,9 +10,9 @@ use crate::world::World;
use args::ConsumedArgs;
use async_trait::async_trait;
use commands::{
ban, banip, banlist, clear, 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, worldborder,
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, worldborder,
};
use dispatcher::CommandError;
use pumpkin_util::math::vector3::Vector3;
@@ -143,6 +143,7 @@ pub fn default_dispatcher() -> CommandDispatcher {
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(damage::init_command_tree(), PermissionLvl::Two);
dispatcher
}

View File

@@ -2,7 +2,7 @@ use std::sync::atomic::AtomicI32;
use async_trait::async_trait;
use crossbeam::atomic::AtomicCell;
use pumpkin_data::sound::Sound;
use pumpkin_data::{damage::DamageType, sound::Sound};
use pumpkin_nbt::tag::NbtTag;
use pumpkin_protocol::client::play::{CDamageEvent, CEntityStatus, MetaDataType, Metadata};
use pumpkin_util::math::vector3::Vector3;
@@ -66,16 +66,27 @@ impl LivingEntity {
self.entity.entity_id
}
// TODO add damage_type enum
pub async fn damage(&self, amount: f32, damage_type: u8) {
pub async fn damage_with_context(
&self,
amount: f32,
damage_type: DamageType,
position: Option<Vector3<f64>>,
source: Option<&Entity>,
cause: Option<&Entity>,
) -> bool {
// Check invulnerability before applying damage
if self.entity.is_invulnerable_to(damage_type) {
return false;
}
self.entity
.world
.broadcast_packet_all(&CDamageEvent::new(
self.entity.entity_id.into(),
damage_type.into(),
None,
None,
None,
damage_type.data().id.into(),
source.map(|e| e.entity_id.into()),
cause.map(|e| e.entity_id.into()),
position,
))
.await;
@@ -86,6 +97,13 @@ impl LivingEntity {
} else {
self.set_health(new_health).await;
}
true
}
pub async fn damage(&self, amount: f32, damage_type: DamageType) -> bool {
self.damage_with_context(amount, damage_type, None, None, None)
.await
}
/// Returns if the entity was damaged or not
@@ -132,7 +150,7 @@ impl LivingEntity {
.play_sound(Self::get_fall_sound(fall_distance as i32))
.await;
// TODO: Play block fall sound
self.damage(damage, 10).await; // Fall
self.damage(damage, DamageType::Fall).await; // Fall
} else if height_difference < 0.0 {
let distance = self.fall_distance.load();
self.fall_distance

View File

@@ -9,6 +9,7 @@ use crossbeam::atomic::AtomicCell;
use living::LivingEntity;
use player::Player;
use pumpkin_data::{
damage::DamageType,
entity::{EntityPose, EntityType},
sound::{Sound, SoundCategory},
};
@@ -91,6 +92,10 @@ pub struct Entity {
pub bounding_box: AtomicCell<BoundingBox>,
///The size (width and height) of the bounding box
pub bounding_box_size: AtomicCell<BoundingBoxSize>,
/// Whether this entity is invulnerable to all damage
pub invulnerable: AtomicBool,
/// List of damage types this entity is immune to
pub damage_immunities: Vec<DamageType>,
}
impl Entity {
@@ -104,6 +109,7 @@ impl Entity {
standing_eye_height: f32,
bounding_box: AtomicCell<BoundingBox>,
bounding_box_size: AtomicCell<BoundingBoxSize>,
invulnerable: bool,
) -> Self {
let floor_x = position.x.floor() as i32;
let floor_y = position.y.floor() as i32;
@@ -130,6 +136,8 @@ impl Entity {
pose: AtomicCell::new(EntityPose::Standing),
bounding_box,
bounding_box_size,
invulnerable: AtomicBool::new(invulnerable),
damage_immunities: Vec::new(),
}
}
@@ -352,6 +360,11 @@ impl Entity {
self.send_meta_data(Metadata::new(6, MetaDataType::EntityPose, VarInt(pose)))
.await;
}
pub fn is_invulnerable_to(&self, damage_type: DamageType) -> bool {
self.invulnerable.load(std::sync::atomic::Ordering::Relaxed)
|| self.damage_immunities.contains(&damage_type)
}
}
#[async_trait]

View File

@@ -11,6 +11,7 @@ use async_trait::async_trait;
use crossbeam::atomic::AtomicCell;
use pumpkin_config::{ADVANCED_CONFIG, BASIC_CONFIG};
use pumpkin_data::{
damage::DamageType,
entity::EntityType,
sound::{Sound, SoundCategory},
};
@@ -183,6 +184,7 @@ impl Player {
1.62,
AtomicCell::new(BoundingBox::new_default(&bounding_box_size)),
AtomicCell::new(bounding_box_size),
matches!(gamemode, GameMode::Creative | GameMode::Spectator),
)),
config: Mutex::new(config),
gameprofile,
@@ -354,7 +356,7 @@ impl Player {
victim
.living_entity
.damage(damage as f32, 34) // PlayerAttack
.damage(damage as f32, DamageType::PlayerAttack) // PlayerAttack
.await;
let mut knockback_strength = 1.0;
@@ -675,6 +677,11 @@ impl Player {
abilities.set_for_gamemode(gamemode);
};
self.send_abilities_update().await;
self.living_entity.entity.invulnerable.store(
matches!(gamemode, GameMode::Creative | GameMode::Spectator),
std::sync::atomic::Ordering::Relaxed,
);
self.living_entity
.entity
.world
@@ -686,6 +693,7 @@ impl Player {
}],
))
.await;
#[allow(clippy::cast_precision_loss)]
self.client
.send_packet(&CGameEvent::new(

View File

@@ -252,6 +252,7 @@ impl Server {
&bounding_box_size,
)),
AtomicCell::new(bounding_box_size),
false,
)
}