mirror of
https://github.com/FabricMC/fabric.git
synced 2025-04-15 00:14:28 -04:00
Add two more sleep events (#1755)
* 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:
parent
bb09662468
commit
86675b35dc
5 changed files with 162 additions and 13 deletions
fabric-entity-events-v1
|
@ -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'
|
||||
|
|
|
@ -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() {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Reference in a new issue