Add ServerPlayerEvents.ALLOW_DEATH event ()

Co-authored-by: NinjaPhenix <5214513+NinjaPhenix@users.noreply.github.com>
Co-authored-by: liach <7806504+liach@users.noreply.github.com>
This commit is contained in:
William Bradford Larcombe 2021-05-01 22:07:44 +01:00 committed by Player
parent 4b24d382c1
commit 6e4bae3683
3 changed files with 70 additions and 0 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

View file

@ -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() {
}
}

View file

@ -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();
}
}

View file

@ -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;
});
}
}