mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-21 03:10:54 -04:00
Add AFTER_DEATH
and ALLOW_DAMAGE
events; generalise ALLOW_DEATH
to living entities (#2573)
* Add AFTER_DEATH and ALLOW_DAMAGE events; generalise ALLOW_DEATH to living entities * Make class final and constructor private * player -> entity; clarify where ALLOW_DAMAGE is fired * Also deprecate the old AllowDeath funcint
This commit is contained in:
parent
c9f64f5a6e
commit
a1d87cb885
5 changed files with 183 additions and 22 deletions
fabric-entity-events-v1/src
main/java/net/fabricmc/fabric
api/entity/event/v1
mixin/entity/event
testmod/java/net/fabricmc/fabric/test/entity/event
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.fabricmc.fabric.api.entity.event.v1;
|
||||
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
|
||||
import net.fabricmc.fabric.api.event.Event;
|
||||
import net.fabricmc.fabric.api.event.EventFactory;
|
||||
|
||||
/**
|
||||
* Various server-side only events related to living entities.
|
||||
*/
|
||||
public final class ServerLivingEntityEvents {
|
||||
/**
|
||||
* An event that is called when a living entity is going to take damage.
|
||||
* This is fired from {@link LivingEntity#damage}, before armor or any other mitigation are applied.
|
||||
* Mods can cancel this to prevent the damage entirely.
|
||||
*/
|
||||
public static final Event<AllowDamage> ALLOW_DAMAGE = EventFactory.createArrayBacked(AllowDamage.class, callbacks -> (entity, source, amount) -> {
|
||||
for (AllowDamage callback : callbacks) {
|
||||
if (!callback.allowDamage(entity, source, amount)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* An event that is called when an entity takes fatal damage.
|
||||
*
|
||||
* <p>Mods can cancel this to keep the entity alive.
|
||||
*
|
||||
* <p>Vanilla checks for entity health {@code <= 0} each tick (with {@link LivingEntity#isDead()}), and kills if true -
|
||||
* so the entity will still die next tick if this event is cancelled.
|
||||
* It's assumed that the listener will do something to prevent this, for example, if the entity is a player:
|
||||
* <ul>
|
||||
* <li>a minigame mod teleporting the player into a 'respawn room' and setting their health to 20.0</li>
|
||||
* <li>a mod that changes death mechanics switching the player over to the mod's play-mode, where death doesn't apply</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static final Event<AllowDeath> ALLOW_DEATH = EventFactory.createArrayBacked(AllowDeath.class, callbacks -> (entity, damageSource, damageAmount) -> {
|
||||
for (AllowDeath callback : callbacks) {
|
||||
if (!callback.allowDeath(entity, damageSource, damageAmount)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
/**
|
||||
* An event that is called when a living entity dies.
|
||||
*/
|
||||
public static final Event<AfterDeath> AFTER_DEATH = EventFactory.createArrayBacked(AfterDeath.class, callbacks -> (entity, damageSource) -> {
|
||||
for (AfterDeath callback : callbacks) {
|
||||
callback.afterDeath(entity, damageSource);
|
||||
}
|
||||
});
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AllowDamage {
|
||||
/**
|
||||
* Called when a living entity is going to take damage. Can be used to cancel the damage entirely.
|
||||
*
|
||||
* <p>The amount corresponds to the "incoming" damage amount, before armor and other mitigations have been applied.
|
||||
*
|
||||
* @param entity the entity
|
||||
* @param source the source of the damage
|
||||
* @param amount the amount of damage that the entity will take (before mitigations)
|
||||
* @return true if the damage should go ahead, false to cancel the damage.
|
||||
*/
|
||||
boolean allowDamage(LivingEntity entity, DamageSource source, float amount);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AllowDeath {
|
||||
/**
|
||||
* Called when a living entity takes fatal damage (before totems of undying can take effect).
|
||||
*
|
||||
* @param entity the entity
|
||||
* @param damageSource the source of the fatal damage
|
||||
* @param damageAmount the amount of damage that has killed the entity
|
||||
* @return true if the death should go ahead, false to cancel the death.
|
||||
*/
|
||||
boolean allowDeath(LivingEntity entity, DamageSource damageSource, float damageAmount);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface AfterDeath {
|
||||
/**
|
||||
* Called when a living entity dies. The death cannot be canceled at this point.
|
||||
*
|
||||
* @param entity the entity
|
||||
* @param damageSource the source of the fatal damage
|
||||
*/
|
||||
void afterDeath(LivingEntity entity, DamageSource damageSource);
|
||||
}
|
||||
|
||||
private ServerLivingEntityEvents() {
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package net.fabricmc.fabric.api.entity.event.v1;
|
||||
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
|
@ -50,18 +49,9 @@ public final class ServerPlayerEvents {
|
|||
/**
|
||||
* An event that is called when a player takes fatal damage.
|
||||
*
|
||||
* <p>Mods can cancel this to keep the player alive.
|
||||
*
|
||||
* <p>Vanilla checks for player health {@code <= 0} each tick (with {@link LivingEntity#isDead()}), and kills if true -
|
||||
* so the player will still die next tick if this event is cancelled. It's assumed that the listener will do
|
||||
* something to prevent this, for example:
|
||||
*
|
||||
* <ul>
|
||||
* <li>a minigame mod teleporting the player into a 'respawn room' and setting their health to 20.0</li>
|
||||
* <li>a mod that changes death mechanics switching the player over to the mod's play-mode, where death doesn't
|
||||
* apply</li>
|
||||
* </ul>
|
||||
* @deprecated Use the more general {@link ServerLivingEntityEvents#ALLOW_DEATH} event instead and check for {@code instanceof ServerPlayerEntity}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final Event<AllowDeath> ALLOW_DEATH = EventFactory.createArrayBacked(AllowDeath.class, callbacks -> (player, damageSource, damageAmount) -> {
|
||||
for (AllowDeath callback : callbacks) {
|
||||
if (!callback.allowDeath(player, damageSource, damageAmount)) {
|
||||
|
@ -96,6 +86,10 @@ public final class ServerPlayerEvents {
|
|||
void afterRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer, boolean alive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the more general {@link ServerLivingEntityEvents#ALLOW_DEATH} event instead and check for {@code instanceof ServerPlayerEntity}.
|
||||
*/
|
||||
@Deprecated
|
||||
@FunctionalInterface
|
||||
public interface AllowDeath {
|
||||
/**
|
||||
|
@ -111,4 +105,15 @@ public final class ServerPlayerEvents {
|
|||
|
||||
private ServerPlayerEvents() {
|
||||
}
|
||||
|
||||
static {
|
||||
// Forward general living entity event to (older) player-specific event.
|
||||
ServerLivingEntityEvents.ALLOW_DEATH.register((entity, damageSource, damageAmount) -> {
|
||||
if (entity instanceof ServerPlayerEntity player) {
|
||||
return ServerPlayerEvents.ALLOW_DEATH.invoker().allowDeath(player, damageSource, damageAmount);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,6 @@ import net.minecraft.entity.Entity;
|
|||
import net.minecraft.entity.EntityType;
|
||||
import net.minecraft.entity.LivingEntity;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.util.ActionResult;
|
||||
import net.minecraft.util.math.BlockPos;
|
||||
|
@ -48,7 +47,7 @@ import net.minecraft.world.World;
|
|||
|
||||
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
|
||||
@Mixin(LivingEntity.class)
|
||||
abstract class LivingEntityMixin {
|
||||
|
@ -65,13 +64,21 @@ abstract class LivingEntityMixin {
|
|||
ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.invoker().afterKilledOtherEntity((ServerWorld) ((LivingEntity) (Object) this).world, attacker, (LivingEntity) (Object) this);
|
||||
}
|
||||
|
||||
@Redirect(method = "damage", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;isDead()Z", ordinal = 1))
|
||||
boolean beforePlayerKilled(LivingEntity livingEntity, DamageSource source, float amount) {
|
||||
if (livingEntity instanceof ServerPlayerEntity) {
|
||||
return isDead() && ServerPlayerEvents.ALLOW_DEATH.invoker().allowDeath((ServerPlayerEntity) livingEntity, source, amount);
|
||||
}
|
||||
@Inject(method = "onDeath", at = @At(value = "INVOKE", target = "net/minecraft/world/World.sendEntityStatus(Lnet/minecraft/entity/Entity;B)V"))
|
||||
private void notifyDeath(DamageSource source, CallbackInfo ci) {
|
||||
ServerLivingEntityEvents.AFTER_DEATH.invoker().afterDeath((LivingEntity) (Object) this, source);
|
||||
}
|
||||
|
||||
return isDead();
|
||||
@Redirect(method = "damage", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/LivingEntity;isDead()Z", ordinal = 1))
|
||||
boolean beforeEntityKilled(LivingEntity livingEntity, DamageSource source, float amount) {
|
||||
return isDead() && ServerLivingEntityEvents.ALLOW_DEATH.invoker().allowDeath(livingEntity, source, amount);
|
||||
}
|
||||
|
||||
@Inject(method = "damage", at = @At(value = "INVOKE", target = "net/minecraft/entity/LivingEntity.isSleeping()Z"), cancellable = true)
|
||||
private void beforeDamage(DamageSource source, float amount, CallbackInfoReturnable<Boolean> cir) {
|
||||
if (!ServerLivingEntityEvents.ALLOW_DAMAGE.invoker().allowDamage((LivingEntity) (Object) this, source, amount)) {
|
||||
cir.setReturnValue(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "sleep", at = @At("RETURN"))
|
||||
|
|
|
@ -48,6 +48,7 @@ import net.minecraft.world.World;
|
|||
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
|
||||
@Mixin(ServerPlayerEntity.class)
|
||||
|
@ -71,6 +72,11 @@ abstract class ServerPlayerEntityMixin extends LivingEntityMixin {
|
|||
}
|
||||
}
|
||||
|
||||
@Inject(method = "onDeath", at = @At("TAIL"))
|
||||
private void notifyDeath(DamageSource source, CallbackInfo ci) {
|
||||
ServerLivingEntityEvents.AFTER_DEATH.invoker().afterDeath((ServerPlayerEntity) (Object) this, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called by both "moveToWorld" and "teleport".
|
||||
* So this is suitable to handle the after event from both call sites.
|
||||
|
|
|
@ -23,6 +23,7 @@ import net.minecraft.block.AbstractBlock;
|
|||
import net.minecraft.block.Block;
|
||||
import net.minecraft.block.BlockState;
|
||||
import net.minecraft.block.Material;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
import net.minecraft.entity.player.PlayerEntity;
|
||||
import net.minecraft.entity.player.PlayerInventory;
|
||||
import net.minecraft.item.BlockItem;
|
||||
|
@ -45,6 +46,7 @@ import net.fabricmc.fabric.api.entity.event.v1.EntityElytraEvents;
|
|||
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
|
||||
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
|
||||
|
||||
public final class EntityEventTests implements ModInitializer {
|
||||
|
@ -78,9 +80,29 @@ public final class EntityEventTests implements ModInitializer {
|
|||
LOGGER.info("Respawned {}, [{}, {}]", oldPlayer.getGameProfile().getName(), oldPlayer.getWorld().getRegistryKey().getValue(), newPlayer.getWorld().getRegistryKey().getValue());
|
||||
});
|
||||
|
||||
ServerPlayerEvents.ALLOW_DEATH.register((player, source, amount) -> {
|
||||
LOGGER.info("{} is going to die to {} damage from {} damage source", player.getGameProfile().getName(), amount, source.getName());
|
||||
// No fall damage if holding a feather in the main hand
|
||||
ServerLivingEntityEvents.ALLOW_DAMAGE.register((entity, source, amount) -> {
|
||||
if (source == DamageSource.FALL && entity.getStackInHand(Hand.MAIN_HAND).isOf(Items.FEATHER)) {
|
||||
LOGGER.info("Avoided {} of fall damage by holding a feather", amount);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
ServerLivingEntityEvents.ALLOW_DEATH.register((entity, source, amount) -> {
|
||||
LOGGER.info("{} is going to die to {} damage from {} damage source", entity.getName().getString(), amount, source.getName());
|
||||
|
||||
if (entity.getStackInHand(Hand.MAIN_HAND).getItem() == Items.CARROT) {
|
||||
entity.setHealth(3.0f);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Test that the legacy event still works
|
||||
ServerPlayerEvents.ALLOW_DEATH.register((player, source, amount) -> {
|
||||
if (player.getStackInHand(Hand.MAIN_HAND).getItem() == Items.APPLE) {
|
||||
player.setHealth(3.0f);
|
||||
return false;
|
||||
|
@ -89,6 +111,10 @@ public final class EntityEventTests implements ModInitializer {
|
|||
return true;
|
||||
});
|
||||
|
||||
ServerLivingEntityEvents.AFTER_DEATH.register((entity, source) -> {
|
||||
LOGGER.info("{} died due to {} damage source", entity.getName().getString(), source.getName());
|
||||
});
|
||||
|
||||
EntitySleepEvents.ALLOW_SLEEPING.register((player, sleepingPos) -> {
|
||||
// Can't sleep if holds blue wool
|
||||
if (player.getStackInHand(Hand.MAIN_HAND).isOf(Items.BLUE_WOOL)) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue