diff --git a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java index 82e16b5eb..78ef61420 100644 --- a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java +++ b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/api/entity/event/v1/ServerPlayerEvents.java @@ -16,6 +16,8 @@ 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; import net.fabricmc.fabric.api.event.Event; @@ -45,6 +47,31 @@ 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> + */ + 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)) { + return false; + } + } + + return true; + }); + @FunctionalInterface public interface CopyFrom { /** @@ -69,6 +96,19 @@ public final class ServerPlayerEvents { void afterRespawn(ServerPlayerEntity oldPlayer, ServerPlayerEntity newPlayer, boolean alive); } + @FunctionalInterface + public interface AllowDeath { + /** + * Called when a player takes fatal damage (before totems of undying can take effect). + * + * @param player the player + * @param damageSource the fatal damage damageSource + * @param damageAmount the damageAmount of damage that has killed the player + * @return true if the death should go ahead, false otherwise. + */ + boolean allowDeath(ServerPlayerEntity player, DamageSource damageSource, float damageAmount); + } + private ServerPlayerEvents() { } } diff --git a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java index 27fc8c68a..ec1acc9c8 100644 --- a/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java +++ b/fabric-entity-events-v1/src/main/java/net/fabricmc/fabric/mixin/entity/event/LivingEntityMixin.java @@ -17,24 +17,40 @@ package net.fabricmc.fabric.mixin.entity.event; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.LocalCapture; import net.minecraft.entity.Entity; 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.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents; +import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents; @Mixin(LivingEntity.class) abstract class LivingEntityMixin extends EntityMixin { + @Shadow + public abstract boolean isDead(); + @Inject(method = "onDeath", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;onKilledOther(Lnet/minecraft/server/world/ServerWorld;Lnet/minecraft/entity/LivingEntity;)V", shift = At.Shift.AFTER), locals = LocalCapture.CAPTURE_FAILEXCEPTION) private void onEntityKilledOther(DamageSource source, CallbackInfo ci, Entity attacker) { // FIXME: Cannot use shadowed fields from supermixins - needs a fix so people can use fabric api in a dev environment even though this is fine in this repo and prod. // A temporary fix is to just cast the mixin to LivingEntity and access the world field with a few ugly casts. 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); + } + + return isDead(); + } } diff --git a/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java b/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java index a7740581e..c2b976231 100644 --- a/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java +++ b/fabric-entity-events-v1/src/testmod/java/net/fabricmc/fabric/test/entity/event/EntityEventTests.java @@ -19,6 +19,9 @@ package net.fabricmc.fabric.test.entity.event; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import net.minecraft.item.Items; +import net.minecraft.util.Hand; + import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents; import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents; @@ -48,5 +51,16 @@ public final class EntityEventTests implements ModInitializer { ServerPlayerEvents.AFTER_RESPAWN.register((oldPlayer, newPlayer, alive) -> { LOGGER.info("Respawned {}, [{}, {}]", oldPlayer.getGameProfile().getName(), oldPlayer.getServerWorld().getRegistryKey().getValue(), newPlayer.getServerWorld().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()); + + if (player.getStackInHand(Hand.MAIN_HAND).getItem() == Items.APPLE) { + player.setHealth(3.0f); + return false; + } + + return true; + }); } }