diff --git a/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java b/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java index 19a48dce7c2f..265968dcb27b 100644 --- a/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java +++ b/paper-api/src/main/java/org/bukkit/entity/LivingEntity.java @@ -841,6 +841,24 @@ default boolean addPotionEffect(@NotNull PotionEffect effect) { */ public boolean isSleeping(); + /** + * Attempts to make the entity sleep at the given location. + *
+ * The location must be in the current world and have a bed placed at the + * location. + * + * @param location the location of the bed + * @return whether the sleep was successful + */ + public boolean sleep(@NotNull Location location); + + /** + * Causes this entity to wake up if it is currently sleeping. + * + * @throws IllegalStateException if not sleeping + */ + public void wakeup(); + /** * Gets if the entity is climbing. * diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java index 5a4e98531031..9db67e3acd4d 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java @@ -210,6 +210,11 @@ public boolean sleep(Location location, boolean force) { return true; } + @Override + public boolean sleep(Location location) { + return this.sleep(location, false); + } + @Override public void wakeup(boolean setSpawnLocation) { Preconditions.checkState(this.isSleeping(), "Cannot wakeup if not sleeping"); @@ -217,6 +222,11 @@ public void wakeup(boolean setSpawnLocation) { this.getHandle().stopSleepInBed(true, setSpawnLocation); } + @Override + public void wakeup() { + this.wakeup(false); + } + @Override public void startRiptideAttack(int duration, float damage, ItemStack attackItem) { Preconditions.checkArgument(duration > 0, "Duration must be greater than 0"); diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java index b0987314d263..89451bca56f0 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftLivingEntity.java @@ -13,6 +13,7 @@ import io.papermc.paper.adventure.PaperAdventure; import net.kyori.adventure.key.Key; import net.minecraft.Optionull; +import net.minecraft.core.BlockPos; import io.papermc.paper.world.damagesource.CombatTracker; import net.minecraft.core.component.DataComponents; import net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket; @@ -40,6 +41,8 @@ import net.minecraft.world.entity.projectile.arrow.ThrownTrident; import net.minecraft.world.item.Items; import net.minecraft.world.item.component.Consumable; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; import net.minecraft.world.waypoints.WaypointStyleAsset; import net.minecraft.world.waypoints.WaypointStyleAssets; @@ -61,6 +64,7 @@ import org.bukkit.craftbukkit.inventory.CraftEntityEquipment; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.craftbukkit.potion.CraftPotionEffectType; +import org.bukkit.craftbukkit.util.CraftLocation; import org.bukkit.entity.AbstractArrow; import org.bukkit.entity.AbstractWindCharge; import org.bukkit.entity.Arrow; @@ -769,6 +773,31 @@ public boolean isSleeping() { return this.getHandle().isSleeping(); } + @Override + public boolean sleep(Location location) { + Preconditions.checkArgument(location != null, "Location cannot be null"); + Preconditions.checkArgument(location.getWorld() != null, "Location needs to be in a world"); + Preconditions.checkArgument(location.getWorld().equals(this.getWorld()), "Cannot sleep across worlds"); + Preconditions.checkState(!this.getHandle().generation, "Cannot sleep during world generation"); + + BlockPos position = CraftLocation.toBlockPosition(location); + BlockState state = this.getHandle().level().getBlockState(position); + if (!(state.getBlock() instanceof BedBlock)) { + return false; + } + + this.getHandle().startSleeping(position); + return true; + } + + @Override + public void wakeup() { + Preconditions.checkState(this.isSleeping(), "Cannot wakeup if not sleeping"); + Preconditions.checkState(!this.getHandle().generation, "Cannot wakeup during world generation"); + + this.getHandle().stopSleeping(); + } + @Override public boolean isClimbing() { Preconditions.checkState(!this.getHandle().generation, "Cannot check if climbing during world generation"); diff --git a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java index fd891f5b1fad..6b5f2913281e 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/TestPlugin.java @@ -1,14 +1,79 @@ package io.papermc.testplugin; +import java.util.Comparator; +import java.util.List; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.defaults.BukkitCommand; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; public final class TestPlugin extends JavaPlugin implements Listener { @Override public void onEnable() { this.getServer().getPluginManager().registerEvents(this, this); + this.registerSleepTestCommands(); // io.papermc.testplugin.brigtests.Registration.registerViaOnEnable(this); } + + private void registerSleepTestCommands() { + this.getServer().getCommandMap().register("test-plugin", new BukkitCommand("testsleep", "Sleep the nearest living entity at its current location", "/testsleep", List.of()) { + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command."); + return true; + } + + LivingEntity target = findNearestLivingEntity(player); + if (target == null) { + player.sendMessage("No living entity found nearby."); + return true; + } + + boolean slept = target.sleep(target.getLocation()); + player.sendMessage("testsleep: " + target.getType() + " -> " + slept + " (sleeping=" + target.isSleeping() + ")"); + return true; + } + }); + + this.getServer().getCommandMap().register("test-plugin", new BukkitCommand("testwakeup", "Wake the nearest sleeping living entity", "/testwakeup", List.of()) { + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage("Only players can use this command."); + return true; + } + + LivingEntity target = findNearestSleepingLivingEntity(player); + if (target == null) { + player.sendMessage("No sleeping living entity found nearby."); + return true; + } + + target.wakeup(); + player.sendMessage("testwakeup: " + target.getType() + " -> sleeping=" + target.isSleeping()); + return true; + } + }); + } + + private static LivingEntity findNearestLivingEntity(Player player) { + return player.getLocation().getNearbyEntitiesByType(LivingEntity.class, 8.0, entity -> !(entity instanceof Player)) + .stream() + .min(Comparator.comparingDouble(entity -> entity.getLocation().distanceSquared(player.getLocation()))) + .orElse(null); + } + + private static LivingEntity findNearestSleepingLivingEntity(Player player) { + return player.getLocation().getNearbyEntitiesByType(LivingEntity.class, 8.0, entity -> !(entity instanceof Player) && entity.isSleeping()) + .stream() + .min(Comparator.comparingDouble(entity -> entity.getLocation().distanceSquared(player.getLocation()))) + .orElse(null); + } }