SPIGOT-7300, #829: Add new DamageSource API providing enhanced information about entity damage

By: Doc <nachito94@msn.com>
Also-by: 2008Choco <hawkeboyz2@hotmail.com>
This commit is contained in:
Bukkit/Spigot 2024-02-11 09:54:21 +11:00
parent e46e33f5e2
commit f9381f1dc4
13 changed files with 467 additions and 27 deletions

View file

@ -14,8 +14,8 @@
<properties>
<skipTests>true</skipTests>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

View file

@ -14,6 +14,7 @@ import org.bukkit.attribute.Attribute;
import org.bukkit.block.Biome;
import org.bukkit.block.banner.PatternType;
import org.bukkit.boss.KeyedBossBar;
import org.bukkit.damage.DamageType;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Cat;
import org.bukkit.entity.EntityType;
@ -207,6 +208,13 @@ public interface Registry<T extends Keyed> extends Iterable<T> {
*/
@ApiStatus.Experimental
Registry<TrimPattern> TRIM_PATTERN = Bukkit.getRegistry(TrimPattern.class);
/**
* Damage types.
*
* @see DamageType
*/
@ApiStatus.Experimental
Registry<DamageType> DAMAGE_TYPE = Objects.requireNonNull(Bukkit.getRegistry(DamageType.class), "No registry present for DamageType. This is a bug.");
/**
* Villager profession.
*

View file

@ -5,6 +5,9 @@ import org.bukkit.advancement.Advancement;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.block.data.BlockData;
import org.bukkit.damage.DamageEffect;
import org.bukkit.damage.DamageSource;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.EntityType;
import org.bukkit.inventory.CreativeCategory;
import org.bukkit.inventory.EquipmentSlot;
@ -13,6 +16,7 @@ import org.bukkit.material.MaterialData;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.potion.PotionType;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -100,4 +104,18 @@ public interface UnsafeValues {
* @return an internal potion data
*/
PotionType.InternalPotionData getInternalPotionData(NamespacedKey key);
@ApiStatus.Internal
@Nullable
DamageEffect getDamageEffect(@NotNull String key);
/**
* Create a new {@link DamageSource.Builder}.
*
* @param damageType the {@link DamageType} to use
* @return a {@link DamageSource.Builder}
*/
@ApiStatus.Internal
@NotNull
DamageSource.Builder createDamageSourceBuilder(@NotNull DamageType damageType);
}

View file

@ -0,0 +1,53 @@
package org.bukkit.damage;
import com.google.common.base.Preconditions;
import org.bukkit.Bukkit;
import org.bukkit.Sound;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Represents a type of effect that occurs when damage is inflicted. Currently,
* effects only determine the sound that plays.
*/
@ApiStatus.Experimental
public interface DamageEffect {
/**
* The default damage effect.
*/
public static final DamageEffect HURT = getDamageEffect("hurt");
/**
* Thorns.
*/
public static final DamageEffect THORNS = getDamageEffect("thorns");
/**
* Drowning.
*/
public static final DamageEffect DROWNING = getDamageEffect("drowning");
/**
* A single burn tick (fire, lava, etc.).
*/
public static final DamageEffect BURNING = getDamageEffect("burning");
/**
* Poked by a berry bush.
*/
public static final DamageEffect POKING = getDamageEffect("poking");
/**
* Freeze tick (powder snow).
*/
public static final DamageEffect FREEZING = getDamageEffect("freezing");
@NotNull
private static DamageEffect getDamageEffect(@NotNull String key) {
return Preconditions.checkNotNull(Bukkit.getUnsafe().getDamageEffect(key), "No DamageEffect found for %s. This is a bug.", key);
}
/**
* Get the {@link Sound} played for this {@link DamageEffect}.
*
* @return the sound
*/
@NotNull
public Sound getSound();
}

View file

@ -0,0 +1,26 @@
package org.bukkit.damage;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
/**
* A means of damage scaling with respect to the server's difficulty.
*/
@ApiStatus.Experimental
public enum DamageScaling {
/**
* Damage is not scaled.
*/
NEVER,
/**
* Damage is scaled only when the
* {@link DamageSource#getCausingEntity() causing entity} is not a
* {@link Player}.
*/
WHEN_CAUSED_BY_LIVING_NON_PLAYER,
/**
* Damage is always scaled.
*/
ALWAYS;
}

View file

@ -0,0 +1,155 @@
package org.bukkit.damage;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents a source of damage.
*/
@ApiStatus.Experimental
public interface DamageSource {
/**
* Get the {@link DamageType}.
*
* @return the damage type
*/
@NotNull
public DamageType getDamageType();
/**
* Get the {@link Entity} that caused the damage to occur.
* <p>
* Not to be confused with {@link #getDirectEntity()}, the causing entity is
* the entity to which the damage is ultimately attributed if the receiver
* is killed. If, for example, the receiver was damaged by a projectile, the
* shooter/thrower would be returned.
*
* @return an Entity or null
*/
@Nullable
public Entity getCausingEntity();
/**
* Get the {@link Entity} that directly caused the damage.
* <p>
* Not to be confused with {@link #getCausingEntity()}, the direct entity is
* the entity that actually inflicted the damage. If, for example, the
* receiver was damaged by a projectile, the projectile would be returned.
*
* @return an Entity or null
*/
@Nullable
public Entity getDirectEntity();
/**
* Get the {@link Location} from where the damage originated. This will only
* be present if an entity did not cause the damage.
*
* @return the location, or null if none
*/
@Nullable
public Location getDamageLocation();
/**
* Get the {@link Location} from where the damage originated.
* <p>
* This is a convenience method to get the final location of the damage.
* This method will attempt to return
* {@link #getDamageLocation() the damage location}. If this is null, the
* {@link #getCausingEntity() causing entity location} will be returned.
* Finally if there is no damage location nor a causing entity, null will be
* returned.
*
* @return the source of the location or null.
*/
@Nullable
public Location getSourceLocation();
/**
* Get if this damage is indirect.
* <p>
* Damage is considered indirect if {@link #getCausingEntity()} is not equal
* to {@link #getDirectEntity()}. This will be the case, for example, if a
* skeleton shot an arrow or a player threw a potion.
*
* @return {@code true} if is indirect, {@code false} otherwise.
*/
public boolean isIndirect();
/**
* Get the amount of hunger exhaustion caused by this damage.
*
* @return the amount of hunger exhaustion caused.
*/
public float getFoodExhaustion();
/**
* Gets if this source of damage scales with difficulty.
*
* @return {@code True} if scales.
*/
public boolean scalesWithDifficulty();
/**
* Create a new {@link DamageSource.Builder}.
*
* @param damageType the {@link DamageType} to use
* @return a {@link DamageSource.Builder}
*/
@NotNull
@SuppressWarnings("deprecation")
public static Builder builder(@NotNull DamageType damageType) {
return Bukkit.getUnsafe().createDamageSourceBuilder(damageType);
}
/**
* Utility class to make building a {@link DamageSource} easier. Only a
* {@link DamageType} is required.
*/
public static interface Builder {
/**
* Set the {@link Entity} that caused the damage.
*
* @param entity the entity
* @return this instance. Allows for chained method calls
* @see DamageSource#getCausingEntity()
*/
@NotNull
public Builder withCausingEntity(@NotNull Entity entity);
/**
* Set the {@link Entity} that directly inflicted the damage.
*
* @param entity the entity
* @return this instance. Allows for chained method calls
* @see DamageSource#getDirectEntity()
*/
@NotNull
public Builder withDirectEntity(@NotNull Entity entity);
/**
* Set the {@link Location} of the source of damage.
*
* @param location the location where the damage occurred
* @return this instance. Allows for chained method calls
* @see DamageSource#getSourceLocation()
*/
@NotNull
public Builder withDamageLocation(@NotNull Location location);
/**
* Create a new {@link DamageSource} instance using the supplied
* parameters.
*
* @return the damage source instance
*/
@NotNull
public DamageSource build();
}
}

View file

@ -0,0 +1,117 @@
package org.bukkit.damage;
import com.google.common.base.Preconditions;
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.Translatable;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Represent a type of damage that an entity can receive.
* <p>
* Constants in this class include the base types provided by the vanilla
* server. Data packs are capable of registering more types of damage which may
* be obtained through the {@link Registry#DAMAGE_TYPE}.
*
* @see <a href="https://minecraft.wiki/w/Damage_type">Minecraft Wiki</a>
*/
@ApiStatus.Experimental
public interface DamageType extends Keyed, Translatable {
public static final DamageType IN_FIRE = getDamageType("in_fire");
public static final DamageType LIGHTNING_BOLT = getDamageType("lightning_bolt");
public static final DamageType ON_FIRE = getDamageType("on_fire");
public static final DamageType LAVA = getDamageType("lava");
public static final DamageType HOT_FLOOR = getDamageType("hot_floor");
public static final DamageType IN_WALL = getDamageType("in_wall");
public static final DamageType CRAMMING = getDamageType("cramming");
public static final DamageType DROWN = getDamageType("drown");
public static final DamageType STARVE = getDamageType("starve");
public static final DamageType CACTUS = getDamageType("cactus");
public static final DamageType FALL = getDamageType("fall");
public static final DamageType FLY_INTO_WALL = getDamageType("fly_into_wall");
public static final DamageType OUT_OF_WORLD = getDamageType("out_of_world");
public static final DamageType GENERIC = getDamageType("generic");
public static final DamageType MAGIC = getDamageType("magic");
public static final DamageType WITHER = getDamageType("wither");
public static final DamageType DRAGON_BREATH = getDamageType("dragon_breath");
public static final DamageType DRY_OUT = getDamageType("dry_out");
public static final DamageType SWEET_BERRY_BUSH = getDamageType("sweet_berry_bush");
public static final DamageType FREEZE = getDamageType("freeze");
public static final DamageType STALAGMITE = getDamageType("stalagmite");
public static final DamageType FALLING_BLOCK = getDamageType("falling_block");
public static final DamageType FALLING_ANVIL = getDamageType("falling_anvil");
public static final DamageType FALLING_STALACTITE = getDamageType("falling_stalactite");
public static final DamageType STING = getDamageType("sting");
public static final DamageType MOB_ATTACK = getDamageType("mob_attack");
public static final DamageType MOB_ATTACK_NO_AGGRO = getDamageType("mob_attack_no_aggro");
public static final DamageType PLAYER_ATTACK = getDamageType("player_attack");
public static final DamageType ARROW = getDamageType("arrow");
public static final DamageType TRIDENT = getDamageType("trident");
public static final DamageType MOB_PROJECTILE = getDamageType("mob_projectile");
public static final DamageType FIREWORKS = getDamageType("fireworks");
public static final DamageType FIREBALL = getDamageType("fireball");
public static final DamageType UNATTRIBUTED_FIREBALL = getDamageType("unattributed_fireball");
public static final DamageType WITHER_SKULL = getDamageType("wither_skull");
public static final DamageType THROWN = getDamageType("thrown");
public static final DamageType INDIRECT_MAGIC = getDamageType("indirect_magic");
public static final DamageType THORNS = getDamageType("thorns");
public static final DamageType EXPLOSION = getDamageType("explosion");
public static final DamageType PLAYER_EXPLOSION = getDamageType("player_explosion");
public static final DamageType SONIC_BOOM = getDamageType("sonic_boom");
public static final DamageType BAD_RESPAWN_POINT = getDamageType("bad_respawn_point");
public static final DamageType OUTSIDE_BORDER = getDamageType("outside_border");
public static final DamageType GENERIC_KILL = getDamageType("generic_kill");
@NotNull
private static DamageType getDamageType(@NotNull String key) {
NamespacedKey namespacedKey = NamespacedKey.minecraft(key);
return Preconditions.checkNotNull(Registry.DAMAGE_TYPE.get(namespacedKey), "No DamageType found for %s. This is a bug.", namespacedKey);
}
/**
* {@inheritDoc}
* <p>
* The returned key is that of the death message sent when this damage type
* is responsible for the death of an entity.
* <p>
* <strong>Note</strong> This translation key is only used if
* {@link #getDeathMessageType()} is {@link DeathMessageType#DEFAULT}
*/
@NotNull
@Override
public String getTranslationKey();
/**
* Get the {@link DamageScaling} for this damage type.
*
* @return the damage scaling
*/
@NotNull
public DamageScaling getDamageScaling();
/**
* Get the {@link DamageEffect} for this damage type.
*
* @return the damage effect
*/
@NotNull
public DamageEffect getDamageEffect();
/**
* Get the {@link DeathMessageType} for this damage type.
*
* @return the death message type
*/
@NotNull
public DeathMessageType getDeathMessageType();
/**
* Get the amount of hunger exhaustion caused by this damage type.
*
* @return the exhaustion
*/
public float getExhaustion();
}

View file

@ -0,0 +1,26 @@
package org.bukkit.damage;
import org.jetbrains.annotations.ApiStatus;
/**
* Represents a type of death message used by a {@link DamageSource}.
*/
@ApiStatus.Experimental
public enum DeathMessageType {
/**
* No special death message logic is applied.
*/
DEFAULT,
/**
* Shows a variant of fall damage death instead of a regular death message.
* <br>
* <b>Example:</b> death.fell.assist.item
*/
FALL_VARIANTS,
/**
* Shows the intentional game design death message instead of a regular
* death message.
*/
INTENTIONAL_GAME_DESIGN;
}

View file

@ -0,0 +1,5 @@
/**
* Classes concerning damage types and sources applicable to living entities.
*/
@org.jetbrains.annotations.ApiStatus.Experimental
package org.bukkit.damage;

View file

@ -1,6 +1,9 @@
package org.bukkit.entity;
import org.bukkit.attribute.Attribute;
import org.bukkit.damage.DamageSource;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
@ -15,14 +18,24 @@ public interface Damageable extends Entity {
void damage(double amount);
/**
* Deals the given amount of damage to this entity, from a specified
* entity.
* Deals the given amount of damage to this entity from a specified
* {@link Entity}.
*
* @param amount Amount of damage to deal
* @param source Entity which to attribute this damage from
* @param amount amount of damage to deal
* @param source entity to which the damage should be attributed
*/
void damage(double amount, @Nullable Entity source);
/**
* Deals the given amount of damage to this entity from a specified
* {@link DamageSource}.
*
* @param amount amount of damage to deal
* @param damageSource source to which the damage should be attributed
*/
@ApiStatus.Experimental
void damage(double amount, @NotNull DamageSource damageSource);
/**
* Gets the entity's health from 0 to {@link #getMaxHealth()}, where 0 is dead.
*

View file

@ -3,6 +3,7 @@ package org.bukkit.event.entity;
import com.google.common.base.Function;
import java.util.Map;
import org.bukkit.block.Block;
import org.bukkit.damage.DamageSource;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -13,13 +14,13 @@ import org.jetbrains.annotations.Nullable;
public class EntityDamageByBlockEvent extends EntityDamageEvent {
private final Block damager;
public EntityDamageByBlockEvent(@Nullable final Block damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, final double damage) {
super(damagee, cause, damage);
public EntityDamageByBlockEvent(@Nullable final Block damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final DamageSource damageSource, final double damage) {
super(damagee, cause, damageSource, damage);
this.damager = damager;
}
public EntityDamageByBlockEvent(@Nullable final Block damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final Map<DamageModifier, Double> modifiers, @NotNull final Map<DamageModifier, ? extends Function<? super Double, Double>> modifierFunctions) {
super(damagee, cause, modifiers, modifierFunctions);
public EntityDamageByBlockEvent(@Nullable final Block damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final DamageSource damageSource, @NotNull final Map<DamageModifier, Double> modifiers, @NotNull final Map<DamageModifier, ? extends Function<? super Double, Double>> modifierFunctions) {
super(damagee, cause, damageSource, modifiers, modifierFunctions);
this.damager = damager;
}

View file

@ -2,6 +2,7 @@ package org.bukkit.event.entity;
import com.google.common.base.Function;
import java.util.Map;
import org.bukkit.damage.DamageSource;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
@ -11,13 +12,13 @@ import org.jetbrains.annotations.NotNull;
public class EntityDamageByEntityEvent extends EntityDamageEvent {
private final Entity damager;
public EntityDamageByEntityEvent(@NotNull final Entity damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, final double damage) {
super(damagee, cause, damage);
public EntityDamageByEntityEvent(@NotNull final Entity damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final DamageSource damageSource, final double damage) {
super(damagee, cause, damageSource, damage);
this.damager = damager;
}
public EntityDamageByEntityEvent(@NotNull final Entity damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final Map<DamageModifier, Double> modifiers, @NotNull final Map<DamageModifier, ? extends Function<? super Double, Double>> modifierFunctions) {
super(damagee, cause, modifiers, modifierFunctions);
public EntityDamageByEntityEvent(@NotNull final Entity damager, @NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final DamageSource damageSource, @NotNull final Map<DamageModifier, Double> modifiers, @NotNull final Map<DamageModifier, ? extends Function<? super Double, Double>> modifierFunctions) {
super(damagee, cause, damageSource, modifiers, modifierFunctions);
this.damager = damager;
}

View file

@ -9,6 +9,7 @@ import java.util.Map;
import java.util.Objects;
import org.bukkit.Material;
import org.bukkit.WorldBorder;
import org.bukkit.damage.DamageSource;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
@ -27,12 +28,13 @@ public class EntityDamageEvent extends EntityEvent implements Cancellable {
private final Map<DamageModifier, Double> originals;
private boolean cancelled;
private final DamageCause cause;
private final DamageSource damageSource;
public EntityDamageEvent(@NotNull final Entity damagee, @NotNull final DamageCause cause, final double damage) {
this(damagee, cause, new EnumMap<DamageModifier, Double>(ImmutableMap.of(DamageModifier.BASE, damage)), new EnumMap<DamageModifier, Function<? super Double, Double>>(ImmutableMap.of(DamageModifier.BASE, ZERO)));
public EntityDamageEvent(@NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final DamageSource damageSource, final double damage) {
this(damagee, cause, damageSource, new EnumMap<DamageModifier, Double>(ImmutableMap.of(DamageModifier.BASE, damage)), new EnumMap<DamageModifier, Function<? super Double, Double>>(ImmutableMap.of(DamageModifier.BASE, ZERO)));
}
public EntityDamageEvent(@NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final Map<DamageModifier, Double> modifiers, @NotNull final Map<DamageModifier, ? extends Function<? super Double, Double>> modifierFunctions) {
public EntityDamageEvent(@NotNull final Entity damagee, @NotNull final DamageCause cause, @NotNull final DamageSource damageSource, @NotNull final Map<DamageModifier, Double> modifiers, @NotNull final Map<DamageModifier, ? extends Function<? super Double, Double>> modifierFunctions) {
super(damagee);
Preconditions.checkArgument(modifiers.containsKey(DamageModifier.BASE), "BASE DamageModifier missing");
Preconditions.checkArgument(!modifiers.containsKey(null), "Cannot have null DamageModifier");
@ -43,6 +45,7 @@ public class EntityDamageEvent extends EntityEvent implements Cancellable {
this.cause = cause;
this.modifiers = modifiers;
this.modifierFunctions = modifierFunctions;
this.damageSource = damageSource;
}
@Override
@ -64,14 +67,9 @@ public class EntityDamageEvent extends EntityEvent implements Cancellable {
* @throws IllegalArgumentException if type is null
*/
public double getOriginalDamage(@NotNull DamageModifier type) throws IllegalArgumentException {
Preconditions.checkArgument(type != null, "Cannot have null DamageModifier");
final Double damage = originals.get(type);
if (damage != null) {
return damage;
}
if (type == null) {
throw new IllegalArgumentException("Cannot have null DamageModifier");
}
return 0;
return (damage != null) ? damage : 0;
}
/**
@ -86,8 +84,9 @@ public class EntityDamageEvent extends EntityEvent implements Cancellable {
* @see #getFinalDamage()
*/
public void setDamage(@NotNull DamageModifier type, double damage) throws IllegalArgumentException, UnsupportedOperationException {
Preconditions.checkArgument(type != null, "Cannot have null DamageModifier");
if (!modifiers.containsKey(type)) {
throw type == null ? new IllegalArgumentException("Cannot have null DamageModifier") : new UnsupportedOperationException(type + " is not applicable to " + getEntity());
throw new UnsupportedOperationException(type + " is not applicable to " + getEntity());
}
modifiers.put(type, damage);
}
@ -185,14 +184,32 @@ public class EntityDamageEvent extends EntityEvent implements Cancellable {
/**
* Gets the cause of the damage.
* <p>
* While a DamageCause may indicate a specific Bukkit-assigned cause of damage,
* {@link #getDamageSource()} may expose additional types of damage such as custom
* damage types provided by data packs, as well as any direct or indirect entities,
* locations, or other contributing factors to the damage being inflicted. The
* alternative is generally preferred, but DamageCauses provided to this event
* should largely encompass most common use cases for developers if a simple cause
* is required.
*
* @return A DamageCause value detailing the cause of the damage.
* @return a DamageCause value detailing the cause of the damage.
*/
@NotNull
public DamageCause getCause() {
return cause;
}
/**
* Get the source of damage.
*
* @return a DamageSource detailing the source of the damage.
*/
@NotNull
public DamageSource getDamageSource() {
return damageSource;
}
@NotNull
@Override
public HandlerList getHandlers() {
@ -210,7 +227,7 @@ public class EntityDamageEvent extends EntityEvent implements Cancellable {
* @deprecated This API is responsible for a large number of implementation
* problems and is in general unsustainable to maintain. It is likely to be
* removed very soon in a subsequent release. Please see
* https://www.spigotmc.org/threads/194446/ for more information.
* <a href="https://www.spigotmc.org/threads/194446/">this thread</a> for more information.
*/
@Deprecated
public enum DamageModifier {