Add two more sleep events ()

* Add event for setting bed occupation state

* Add wake up pos event and update tests

* Bump version

Just in case. I have a bad feeling that
this could become a patch version otherwise.

* Add code tags for true/false/null in EntitySleepEvents jd

* Add Dynamic annotations for lambda body mixins

* Update class javadoc to contain the new events
This commit is contained in:
Juuxel 2021-10-17 19:14:37 +01:00 committed by modmuss50
parent bb09662468
commit 86675b35dc
5 changed files with 162 additions and 13 deletions
fabric-entity-events-v1
build.gradle
src
main/java/net/fabricmc/fabric
api/entity/event/v1
mixin/entity/event
testmod/java/net/fabricmc/fabric/test/entity/event

View file

@ -1,5 +1,5 @@
archivesBaseName = "fabric-entity-events-v1"
version = getSubprojectVersion(project, "1.2.4")
version = getSubprojectVersion(project, "1.3.0")
moduleDependencies(project, [
'fabric-api-base'

View file

@ -24,6 +24,7 @@ import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
@ -38,7 +39,8 @@ import net.fabricmc.fabric.api.event.EventFactory;
* {@link #ALLOW_NEARBY_MONSTERS}, {@link #ALLOW_SETTING_SPAWN} and {@link #ALLOW_SLEEPING}
*
* <p><b>Note:</b> Only the {@link #ALLOW_BED} event applies to non-player entities.</li>
* <li>Modifiers: {@link #MODIFY_SLEEPING_DIRECTION}</li>
* <li>Modifiers: {@link #MODIFY_SLEEPING_DIRECTION}, {@link #SET_BED_OCCUPATION_STATE}
* and {@link #MODIFY_WAKE_UP_POSITION}</li>
* </ol>
*
* <p>Sleep events are useful for making custom bed blocks that do not extend {@link net.minecraft.block.BedBlock}.
@ -54,7 +56,7 @@ public final class EntitySleepEvents {
* if they match your use case! This helps with mod compatibility.
*
* <p>If this event returns a {@link net.minecraft.entity.player.PlayerEntity.SleepFailureReason}, it is used
* as the return value of {@link PlayerEntity#trySleep(BlockPos)} and sleeping fails. A null return value
* as the return value of {@link PlayerEntity#trySleep(BlockPos)} and sleeping fails. A {@code null} return value
* means that the player will start sleeping.
*
* <p>When this event is called, all vanilla sleeping checks have already succeeded, i.e. this event
@ -95,7 +97,7 @@ public final class EntitySleepEvents {
* An event that is called to check whether a block is valid for sleeping.
*
* <p>Used for checking whether the block at the current sleeping position is a valid bed block.
* If false, the player wakes up.
* If {@code false}, the player wakes up.
*
* <p>This event is only checked <i>during</i> sleeping, so an entity can
* {@linkplain LivingEntity#sleep(BlockPos) start sleeping} on any block, but will immediately
@ -196,6 +198,37 @@ public final class EntitySleepEvents {
return true;
});
/**
* An event that sets the occupation state of a bed.
*
* <p>Note that this is <b>not</b> needed for blocks using {@link net.minecraft.block.BedBlock},
* which are handled automatically.
*/
public static final Event<SetBedOccupationState> SET_BED_OCCUPATION_STATE = EventFactory.createArrayBacked(SetBedOccupationState.class, callbacks -> (entity, sleepingPos, bedState, occupied) -> {
for (SetBedOccupationState callback : callbacks) {
if (callback.setBedOccupationState(entity, sleepingPos, bedState, occupied)) {
return true;
}
}
return false;
});
/**
* An event that can be used to provide the entity's wake-up position if missing.
*
* <p>This is useful for custom bed blocks that need to determine the wake-up position themselves.
* If the block is not a {@link net.minecraft.block.BedBlock}, you need to provide the wake-up position manually
* with this event.
*/
public static final Event<ModifyWakeUpPosition> MODIFY_WAKE_UP_POSITION = EventFactory.createArrayBacked(ModifyWakeUpPosition.class, callbacks -> (entity, sleepingPos, bedState, wakeUpPos) -> {
for (ModifyWakeUpPosition callback : callbacks) {
wakeUpPos = callback.modifyWakeUpPosition(entity, sleepingPos, bedState, wakeUpPos);
}
return wakeUpPos;
});
@FunctionalInterface
public interface AllowSleeping {
/**
@ -203,7 +236,7 @@ public final class EntitySleepEvents {
*
* @param player the sleeping player
* @param sleepingPos the future {@linkplain LivingEntity#getSleepingPosition() sleeping position} of the entity
* @return null if the player can sleep, or a failure reason if they cannot
* @return {@code null} if the player can sleep, or a failure reason if they cannot
* @see PlayerEntity#trySleep(BlockPos)
*/
@Nullable
@ -242,7 +275,7 @@ public final class EntitySleepEvents {
* @param entity the sleeping entity
* @param sleepingPos the position of the block
* @param state the block state to check
* @param vanillaResult true if vanilla allows the block, false otherwise
* @param vanillaResult {@code true} if vanilla allows the block, {@code false} otherwise
* @return {@link ActionResult#SUCCESS} if the bed is valid, {@link ActionResult#FAIL} if it's not,
* {@link ActionResult#PASS} to fall back to other callbacks
*/
@ -258,7 +291,7 @@ public final class EntitySleepEvents {
*
* @param player the sleeping player
* @param sleepingPos the (possibly still unset) {@linkplain LivingEntity#getSleepingPosition() sleeping position} of the player
* @param vanillaResult true if vanilla allows the time, false otherwise
* @param vanillaResult {@code true} if vanilla allows the time, {@code false} otherwise
* @return {@link ActionResult#SUCCESS} if the time is valid, {@link ActionResult#FAIL} if it's not,
* {@link ActionResult#PASS} to fall back to other callbacks
*/
@ -274,7 +307,7 @@ public final class EntitySleepEvents {
*
* @param player the sleeping player
* @param sleepingPos the (possibly still unset) {@linkplain LivingEntity#getSleepingPosition() sleeping position} of the player
* @param vanillaResult true if vanilla's monster check succeeded, false otherwise
* @param vanillaResult {@code true} if vanilla's monster check succeeded (there were no monsters), {@code false} otherwise
* @return {@link ActionResult#SUCCESS} to allow sleeping, {@link ActionResult#FAIL} to prevent sleeping,
* {@link ActionResult#PASS} to fall back to other callbacks
*/
@ -287,7 +320,7 @@ public final class EntitySleepEvents {
* Checks whether a sleeping player counts into skipping the current day and resetting the time to 0.
*
* @param player the sleeping player
* @return true if allowed, false otherwise
* @return {@code true} if allowed, {@code false} otherwise
*/
boolean allowResettingTime(PlayerEntity player);
}
@ -300,7 +333,7 @@ public final class EntitySleepEvents {
*
* @param entity the sleeping entity
* @param sleepingPos the position of the block slept on
* @param sleepingDirection the old sleeping direction, or null if not determined by vanilla logic
* @param sleepingDirection the old sleeping direction, or {@code null} if not determined by vanilla or previous callbacks
* @return the new sleeping direction
*/
@Nullable
@ -314,11 +347,40 @@ public final class EntitySleepEvents {
*
* @param player the sleeping player
* @param sleepingPos the sleeping position
* @return true if allowed, false otherwise
* @return {@code true} if allowed, {@code false} otherwise
*/
boolean allowSettingSpawn(PlayerEntity player, BlockPos sleepingPos);
}
@FunctionalInterface
public interface SetBedOccupationState {
/**
* Sets the occupation state of a bed block.
*
* @param entity the sleeping entity
* @param sleepingPos the sleeping position
* @param bedState the block state of the bed
* @param occupied {@code true} if occupied, {@code false} if free
* @return {@code true} if the occupation state was successfully modified, {@code false} to fall back to other callbacks
*/
boolean setBedOccupationState(LivingEntity entity, BlockPos sleepingPos, BlockState bedState, boolean occupied);
}
@FunctionalInterface
public interface ModifyWakeUpPosition {
/**
* Modifies or provides a wake-up position for an entity waking up.
*
* @param entity the sleeping entity
* @param sleepingPos the position of the block slept on
* @param bedState the block slept on
* @param wakeUpPos the old wake-up position, or {@code null} if not determined by vanilla or previous callbacks
* @return the new wake-up position
*/
@Nullable
Vec3d modifyWakeUpPosition(LivingEntity entity, BlockPos sleepingPos, BlockState bedState, @Nullable Vec3d wakeUpPos);
}
private EntitySleepEvents() {
}
}

View file

@ -19,17 +19,22 @@ package net.fabricmc.fabric.mixin.entity.event;
import java.util.Optional;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Dynamic;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import net.minecraft.block.BedBlock;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
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;
@ -37,6 +42,9 @@ import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.CollisionView;
import net.minecraft.world.World;
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
@ -81,7 +89,7 @@ abstract class LivingEntityMixin {
}
}
// Synthetic lambda body for Optional.map in isSleepingInBed
@Dynamic("method_18405: Synthetic lambda body for Optional.map in isSleepingInBed")
@Inject(method = "method_18405", at = @At("RETURN"), cancellable = true)
private void onIsSleepingInBed(BlockPos sleepingPos, CallbackInfoReturnable<Boolean> info) {
BlockState bedState = ((LivingEntity) (Object) this).world.getBlockState(sleepingPos);
@ -98,4 +106,50 @@ abstract class LivingEntityMixin {
info.setReturnValue(EntitySleepEvents.MODIFY_SLEEPING_DIRECTION.invoker().modifySleepDirection((LivingEntity) (Object) this, sleepingPos, info.getReturnValue()));
}
}
// This is needed 1) so that the vanilla logic in wakeUp runs for modded beds and 2) for the injector below.
// The injector is shared because method_18404 and sleep share much of the structure here.
@Dynamic("method_18404: Synthetic lambda body for Optional.ifPresent in wakeUp")
@ModifyVariable(method = {"method_18404", "sleep"}, at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/world/World;getBlockState(Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/block/BlockState;"))
private BlockState modifyBedForOccupiedState(BlockState state, BlockPos sleepingPos) {
ActionResult result = EntitySleepEvents.ALLOW_BED.invoker().allowBed((LivingEntity) (Object) this, sleepingPos, state, state.getBlock() instanceof BedBlock);
// If a valid bed, replace with vanilla red bed so that the vanilla instanceof check succeeds.
return result.isAccepted() ? Blocks.RED_BED.getDefaultState() : state;
}
// The injector is shared because method_18404 and sleep share much of the structure here.
@Dynamic("method_18404: Synthetic lambda body for Optional.ifPresent in wakeUp")
@Redirect(method = {"method_18404", "sleep"}, at = @At(value = "INVOKE", target = "Lnet/minecraft/world/World;setBlockState(Lnet/minecraft/util/math/BlockPos;Lnet/minecraft/block/BlockState;I)Z"))
private boolean setOccupiedState(World world, BlockPos pos, BlockState state, int flags) {
// This might have been replaced by a red bed above, so we get it again.
// Note that we *need* to replace it so the state.with(OCCUPIED, ...) call doesn't crash
// when the bed doesn't have the property.
BlockState originalState = world.getBlockState(pos);
boolean occupied = state.get(BedBlock.OCCUPIED);
if (EntitySleepEvents.SET_BED_OCCUPATION_STATE.invoker().setBedOccupationState((LivingEntity) (Object) this, pos, originalState, occupied)) {
return true;
} else if (originalState.contains(BedBlock.OCCUPIED)) {
// This check is widened from (instanceof BedBlock) to a property check to allow modded blocks
// that don't use the event.
return world.setBlockState(pos, originalState.with(BedBlock.OCCUPIED, occupied), flags);
} else {
return false;
}
}
@Dynamic("method_18404: Synthetic lambda body for Optional.ifPresent in wakeUp")
@Redirect(method = "method_18404", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BedBlock;findWakeUpPosition(Lnet/minecraft/entity/EntityType;Lnet/minecraft/world/CollisionView;Lnet/minecraft/util/math/BlockPos;F)Ljava/util/Optional;"))
private Optional<Vec3d> modifyWakeUpPosition(EntityType<?> type, CollisionView world, BlockPos pos, float yaw) {
Optional<Vec3d> original = Optional.empty();
BlockState bedState = world.getBlockState(pos);
if (bedState.getBlock() instanceof BedBlock) {
original = BedBlock.findWakeUpPosition(type, world, pos, yaw);
}
Vec3d newPos = EntitySleepEvents.MODIFY_WAKE_UP_POSITION.invoker().modifyWakeUpPosition((LivingEntity) (Object) this, pos, bedState, original.orElse(null));
return Optional.ofNullable(newPos);
}
}

View file

@ -21,6 +21,7 @@ import org.apache.logging.log4j.Logger;
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.block.Material;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
@ -35,6 +36,7 @@ import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.registry.Registry;
import net.fabricmc.api.ModInitializer;
@ -95,6 +97,15 @@ public final class EntityEventTests implements ModInitializer {
EntitySleepEvents.START_SLEEPING.register((entity, sleepingPos) -> {
LOGGER.info("Entity {} sleeping at {}", entity, sleepingPos);
BlockState bedState = entity.world.getBlockState(sleepingPos);
if (bedState.isOf(TEST_BED)) {
boolean shouldBeOccupied = !entity.getStackInHand(Hand.MAIN_HAND).isOf(Items.ORANGE_WOOL);
if (bedState.get(TestBedBlock.OCCUPIED) != shouldBeOccupied) {
throw new AssertionError("Test bed should " + (!shouldBeOccupied ? "not " : "") + "be occupied");
}
}
});
EntitySleepEvents.STOP_SLEEPING.register((entity, sleepingPos) -> {
@ -141,6 +152,20 @@ public final class EntityEventTests implements ModInitializer {
return !player.getStackInHand(Hand.MAIN_HAND).isOf(Items.BLACK_WOOL);
});
EntitySleepEvents.SET_BED_OCCUPATION_STATE.register((entity, sleepingPos, bedState, occupied) -> {
// Don't set occupied state if holding orange wool
return entity.getStackInHand(Hand.MAIN_HAND).isOf(Items.ORANGE_WOOL);
});
EntitySleepEvents.MODIFY_WAKE_UP_POSITION.register((entity, sleepingPos, bedState, wakeUpPos) -> {
// If holding cyan wool, wake up 10 blocks above the bed
if (entity.getStackInHand(Hand.MAIN_HAND).isOf(Items.CYAN_WOOL)) {
return Vec3d.ofCenter(sleepingPos).add(0, 10, 0);
}
return wakeUpPos;
});
CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
dispatcher.register(CommandManager.literal("addsleeptestwools").executes(context -> {
addSleepWools(context.getSource().getPlayer());
@ -157,6 +182,8 @@ public final class EntityEventTests implements ModInitializer {
inventory.offerOrDrop(createNamedItem(Items.RED_WOOL, "Detect nearby monsters"));
inventory.offerOrDrop(createNamedItem(Items.WHITE_WOOL, "Don't set spawn"));
inventory.offerOrDrop(createNamedItem(Items.BLACK_WOOL, "Don't reset time"));
inventory.offerOrDrop(createNamedItem(Items.ORANGE_WOOL, "Don't set occupied state"));
inventory.offerOrDrop(createNamedItem(Items.CYAN_WOOL, "Wake up high above"));
}
private static ItemStack createNamedItem(Item item, String name) {

View file

@ -24,6 +24,7 @@ import net.minecraft.state.StateManager;
import net.minecraft.state.property.BooleanProperty;
import net.minecraft.state.property.Properties;
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableText;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.hit.BlockHitResult;
@ -48,7 +49,12 @@ public class TestBedBlock extends Block {
@Override
public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
if (!state.get(OCCUPIED) && world.getDimension().isBedWorking()) {
if (state.get(OCCUPIED)) {
player.sendMessage(new TranslatableText("block.minecraft.bed.occupied"), true);
return ActionResult.CONSUME;
}
if (world.getDimension().isBedWorking()) {
if (!world.isClient) {
player.trySleep(pos).ifLeft(sleepFailureReason -> {
Text message = sleepFailureReason.toText();