diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index fd828d9..879d9cb 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -3,10 +3,11 @@ @NullMarked module net.thenextlvl.portals { exports net.thenextlvl.portals.action; + exports net.thenextlvl.portals.bounds; exports net.thenextlvl.portals.event; - exports net.thenextlvl.portals.model; exports net.thenextlvl.portals.selection; exports net.thenextlvl.portals.shape; + exports net.thenextlvl.portals.view; exports net.thenextlvl.portals; requires core.paper; diff --git a/api/src/main/java/net/thenextlvl/portals/action/ActionTypes.java b/api/src/main/java/net/thenextlvl/portals/action/ActionTypes.java index 08af2b6..a92a7e7 100644 --- a/api/src/main/java/net/thenextlvl/portals/action/ActionTypes.java +++ b/api/src/main/java/net/thenextlvl/portals/action/ActionTypes.java @@ -2,7 +2,7 @@ import net.thenextlvl.binder.StaticBinder; import net.thenextlvl.portals.PortalLike; -import net.thenextlvl.portals.model.Bounds; +import net.thenextlvl.portals.bounds.Bounds; import org.bukkit.Location; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; diff --git a/api/src/main/java/net/thenextlvl/portals/bounds/Bounds.java b/api/src/main/java/net/thenextlvl/portals/bounds/Bounds.java new file mode 100644 index 0000000..a251c72 --- /dev/null +++ b/api/src/main/java/net/thenextlvl/portals/bounds/Bounds.java @@ -0,0 +1,136 @@ +package net.thenextlvl.portals.bounds; + +import io.papermc.paper.math.BlockPosition; +import net.kyori.adventure.key.Key; +import net.thenextlvl.binder.StaticBinder; +import org.bukkit.Location; +import org.bukkit.World; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CompletableFuture; + +/** + * Represents a bounding box for teleportation. + * + * @since 0.2.0 + */ +@ApiStatus.NonExtendable +public interface Bounds { + /** + * Gets the factory for creating Bounds instances. + * + * @return The bounds factory. + * @since 0.2.0 + */ + static @CheckReturnValue BoundsFactory factory() { + return StaticBinder.getInstance(BoundsFactory.class.getClassLoader()).find(BoundsFactory.class); + } + + /** + * Gets the world the bounds are in. + *

+ * May be empty if the world is not loaded. + * + * @return The world. + * @since 0.2.0 + */ + Optional world(); + + /** + * Gets the world key. + * + * @return The world key. + * @since 0.2.0 + */ + @Contract(pure = true) + Key worldKey(); + + /** + * Gets the minimum position of the bounds. + * + * @return the minimum position of the bounds + * @since 0.2.0 + */ + @Contract(value = " -> new", pure = true) + BlockPosition minPosition(); + + /** + * Gets the maximum position of the bounds. + * + * @return the maximum position of the bounds + * @since 0.2.0 + */ + @Contract(value = " -> new", pure = true) + BlockPosition maxPosition(); + + /** + * Gets the minimum X coordinate of the bounds. + * + * @return the minimum X coordinate of the bounds + * @since 0.2.0 + */ + int minX(); + + /** + * Gets the minimum Y coordinate of the bounds. + * + * @return the minimum Y coordinate of the bounds + * @since 0.2.0 + */ + int minY(); + + /** + * Gets the minimum Z coordinate of the bounds. + * + * @return the minimum Z coordinate of the bounds + * @since 0.2.0 + */ + int minZ(); + + /** + * Gets the maximum X coordinate of the bounds. + * + * @return the maximum X coordinate of the bounds + * @since 0.2.0 + */ + int maxX(); + + /** + * Gets the maximum Y coordinate of the bounds. + * + * @return the maximum Y coordinate of the bounds + * @since 0.2.0 + */ + int maxY(); + + /** + * Gets the maximum Z coordinate of the bounds. + * + * @return the maximum Z coordinate of the bounds + * @since 0.2.0 + */ + int maxZ(); + + /** + * Searches for a safe location within the bounds using a smart algorithm. + *

+ * The algorithm works as follows: + *

    + *
  1. Pick a random X and Z coordinate within the bounds
  2. + *
  3. Pick a random Y coordinate and search up and down for a safe location
  4. + *
  5. If no safe location is found within height bounds, try a new X coordinate
  6. + *
  7. If still no safe location is found, try a new Z coordinate
  8. + *
  9. If still no safe location is found, try both new X and Z coordinates
  10. + *
+ * + * @param random The random number generator. + * @return A CompletableFuture that completes with a safe location, or {@code null} if none is found. + * @since 0.2.0 + */ + CompletableFuture<@Nullable Location> searchSafeLocation(Random random); +} diff --git a/api/src/main/java/net/thenextlvl/portals/bounds/BoundsFactory.java b/api/src/main/java/net/thenextlvl/portals/bounds/BoundsFactory.java new file mode 100644 index 0000000..866712b --- /dev/null +++ b/api/src/main/java/net/thenextlvl/portals/bounds/BoundsFactory.java @@ -0,0 +1,91 @@ +package net.thenextlvl.portals.bounds; + +import io.papermc.paper.math.Position; +import net.kyori.adventure.key.Key; +import org.bukkit.World; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; + +/** + * Factory for creating Bounds instances. + * + * @since 0.2.0 + */ +@ApiStatus.NonExtendable +public interface BoundsFactory { + /** + * Creates new Bounds for teleportation to happen within. + * + * @param world The key of the world the teleport will happen in. + * @param minX The minimum X coordinate. + * @param minY The minimum Y coordinate. + * @param minZ The minimum Z coordinate. + * @param maxX The maximum X coordinate. + * @param maxY The maximum Y coordinate. + * @param maxZ The maximum Z coordinate. + * @since 0.2.0 + */ + @Contract(value = "_, _, _, _, _, _, _ -> new", pure = true) + Bounds of(Key world, int minX, int minY, int minZ, int maxX, int maxY, int maxZ); + + /** + * Creates new Bounds for teleportation to happen within. + * + * @param world The world the teleport will happen in. + * @param minX The minimum X coordinate. + * @param minY The minimum Y coordinate. + * @param minZ The minimum Z coordinate. + * @param maxX The maximum X coordinate. + * @param maxY The maximum Y coordinate. + * @param maxZ The maximum Z coordinate. + * @since 0.2.0 + */ + @Contract(value = "_, _, _, _, _, _, _ -> new", pure = true) + Bounds of(World world, int minX, int minY, int minZ, int maxX, int maxY, int maxZ); + + /** + * Creates new Bounds for teleportation to happen within. + * + * @param key The world key. + * @param min The minimum position. + * @param max The maximum position. + * @since 0.2.0 + */ + @Contract(value = "_, _, _ -> new", pure = true) + Bounds of(Key key, Position min, Position max); + + /** + * Creates new Bounds for teleportation to happen within. + * + * @param world The world. + * @param min The minimum position. + * @param max The maximum position. + * @since 0.2.0 + */ + @Contract(value = "_, _, _ -> new", pure = true) + Bounds of(World world, Position min, Position max); + + /** + * Creates new Bounds based on a center position, radius, and height. + * + * @param world The key of the world. + * @param center The center position. + * @param radius The radius. + * @param height The height. + * @since 0.2.0 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + Bounds radius(Key world, Position center, int radius, int height); + + /** + * Creates new Bounds based on a center position, radius, and height. + * + * @param world The world. + * @param center The center position. + * @param radius The radius. + * @param height The height. + * @since 0.2.0 + */ + @Contract(value = "_, _, _, _ -> new", pure = true) + Bounds radius(World world, Position center, int radius, int height); +} diff --git a/api/src/main/java/net/thenextlvl/portals/model/Bounds.java b/api/src/main/java/net/thenextlvl/portals/model/Bounds.java deleted file mode 100644 index f678b11..0000000 --- a/api/src/main/java/net/thenextlvl/portals/model/Bounds.java +++ /dev/null @@ -1,120 +0,0 @@ -package net.thenextlvl.portals.model; - -import io.papermc.paper.math.FinePosition; -import io.papermc.paper.math.Position; -import org.bukkit.Location; -import org.bukkit.World; -import org.jetbrains.annotations.Contract; - -import java.util.Random; - -/** - * Represents a bounding box for teleportation. - * - * @param world The world the teleport will happen in. - * @param minX The minimum X coordinate. - * @param minY The minimum Y coordinate. - * @param minZ The minimum Z coordinate. - * @param maxX The maximum X coordinate. - * @param maxY The maximum Y coordinate. - * @param maxZ The maximum Z coordinate. - * @since 0.1.0 - */ -public record Bounds( - World world, - double minX, double minY, double minZ, - double maxX, double maxY, double maxZ -) { - /** - * Creates new Bounds for teleportation to happen within. - * - * @param world The world the teleport will happen in. - * @param minX The minimum X coordinate. - * @param minY The minimum Y coordinate. - * @param minZ The minimum Z coordinate. - * @param maxX The maximum X coordinate. - * @param maxY The maximum Y coordinate. - * @param maxZ The maximum Z coordinate. - * @since 0.1.0 - */ - public Bounds(World world, double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { - this.world = world; - this.minX = Math.min(minX, maxX); - this.minY = Math.min(minY, maxY); - this.minZ = Math.min(minZ, maxZ); - this.maxX = Math.max(minX, maxX); - this.maxY = Math.max(minY, maxY); - this.maxZ = Math.max(minZ, maxZ); - } - - /** - * Creates new Bounds for teleportation to happen within. - * - * @param min The minimum position. - * @param max The maximum position. - * @since 0.1.0 - */ - public Bounds(World world, Position min, Position max) { - this(world, min.x(), min.y(), min.z(), max.x(), max.y(), max.z()); - } - - /** - * Gets the minimum position of the bounds. - * - * @return the minimum position of the bounds - * @since 0.1.0 - */ - @Contract(value = " -> new", pure = true) - public FinePosition minPosition() { - return Position.fine(minX, minY, minZ); - } - - /** - * Gets the maximum position of the bounds. - * - * @return the maximum position of the bounds - * @since 0.1.0 - */ - @Contract(value = " -> new", pure = true) - public FinePosition maxPosition() { - return Position.fine(maxX, maxY, maxZ); - } - - /** - * Creates new Teleportation Bounds based on a center position, radius, and height. - * - * @param center The center position. - * @param radius The radius. - * @param height The height. - * @since 0.1.0 - */ - @Contract(value = "_, _, _, _ -> new", pure = true) - public static Bounds radius(World world, Position center, double radius, double height) { - return new Bounds( - world, - center.x() - radius, - center.y() - height / 2, - center.z() - radius, - center.x() + radius, - center.y() + height / 2, - center.z() + radius - ); - } - - /** - * Gets a random location within the bounds. - * - * @param random The random number generator. - * @return A random location within the bounds. - * @since 0.1.0 - */ - public Location getRandomLocation(Random random) { - return new Location( - world, - minX == maxX ? maxX : random.nextDouble(minX, maxX), - minY == maxY ? maxY : random.nextDouble(minY, maxY), - minZ == maxZ ? maxZ : random.nextDouble(minZ, maxZ) - ); - } - // todo: safe teleportation – searchSafeLocation -} diff --git a/api/src/main/java/net/thenextlvl/portals/model/PortalConfig.java b/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java similarity index 77% rename from api/src/main/java/net/thenextlvl/portals/model/PortalConfig.java rename to api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java index c981292..8c75141 100644 --- a/api/src/main/java/net/thenextlvl/portals/model/PortalConfig.java +++ b/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java @@ -1,4 +1,4 @@ -package net.thenextlvl.portals.model; +package net.thenextlvl.portals.view; import net.thenextlvl.binder.StaticBinder; import org.jetbrains.annotations.ApiStatus; @@ -7,7 +7,7 @@ /** * Configuration for the portal plugin. * - * @since 0.1.0 + * @since 0.2.0 */ @ApiStatus.NonExtendable public interface PortalConfig { @@ -15,17 +15,25 @@ public interface PortalConfig { * Gets the portal configuration. * * @return the portal configuration - * @since 0.1.0 + * @since 0.2.0 */ static @CheckReturnValue PortalConfig config() { return StaticBinder.getInstance(PortalConfig.class.getClassLoader()).find(PortalConfig.class); } + /** + * Whether to allow random teleports to caves. + * + * @return {@code true} if cave and spawns are allowed, {@code false} otherwise + * @since 0.2.0 + */ + boolean allowCaveSpawns(); + /** * Whether to use economy for entry costs. * * @return {@code true} if economy is used for entry costs, {@code false} otherwise - * @since 0.1.0 + * @since 0.2.0 */ boolean entryCosts(); @@ -33,7 +41,7 @@ public interface PortalConfig { * Whether to ignore entity movement. * * @return {@code true} if entity movement is ignored, {@code false} otherwise - * @since 0.1.0 + * @since 0.2.0 */ boolean ignoreEntityMovement(); @@ -41,7 +49,7 @@ public interface PortalConfig { * Whether to push back entities that are denied entry. * * @return {@code true} if entities are pushed back, {@code false} otherwise - * @since 0.1.0 + * @since 0.2.0 */ boolean pushBackOnEntryDenied(); @@ -49,7 +57,7 @@ public interface PortalConfig { * Speed at which entities are pushed back. * * @return the pushback speed - * @since 0.1.0 + * @since 0.2.0 */ double pushbackSpeed(); } diff --git a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java index fe17291..3f67e05 100644 --- a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java +++ b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java @@ -16,9 +16,10 @@ import net.thenextlvl.portals.action.SimpleActionTypes; import net.thenextlvl.portals.adapter.BoundingBoxAdapter; import net.thenextlvl.portals.adapter.EntryActionAdapter; +import net.thenextlvl.portals.adapter.FinePositionAdapter; import net.thenextlvl.portals.adapter.KeyAdapter; import net.thenextlvl.portals.adapter.PortalAdapter; -import net.thenextlvl.portals.adapter.PositionAdapter; +import net.thenextlvl.portals.bounds.SimpleBoundsFactory; import net.thenextlvl.portals.command.PortalCommand; import net.thenextlvl.portals.economy.EconomyProvider; import net.thenextlvl.portals.economy.EmptyEconomyProvider; @@ -26,7 +27,8 @@ import net.thenextlvl.portals.economy.VaultEconomyProvider; import net.thenextlvl.portals.listener.PortalListener; import net.thenextlvl.portals.listener.WorldListener; -import net.thenextlvl.portals.model.PortalConfig; +import net.thenextlvl.portals.bounds.BoundsFactory; +import net.thenextlvl.portals.view.PortalConfig; import net.thenextlvl.portals.model.SimplePortalConfig; import net.thenextlvl.portals.portal.PaperPortalProvider; import net.thenextlvl.portals.selection.SelectionProvider; @@ -52,9 +54,9 @@ public final class PortalsPlugin extends JavaPlugin { private final FileIO portalConfig = new GsonFile<>( IO.of(getDataPath().resolve("config.json")), - new SimplePortalConfig(true, false, true, 0.3), + new SimplePortalConfig(false, true, false, true, 0.3), SimplePortalConfig.class - ).validate().saveIfAbsent(); + ).validate().save(); private final ComponentBundle bundle = ComponentBundle.builder( Key.key("portals", "translations"), @@ -66,6 +68,7 @@ public final class PortalsPlugin extends JavaPlugin { public PortalsPlugin() { StaticBinder.getInstance(ActionTypes.class.getClassLoader()).bind(ActionTypes.class, SimpleActionTypes.INSTANCE); + StaticBinder.getInstance(BoundsFactory.class.getClassLoader()).bind(BoundsFactory.class, SimpleBoundsFactory.INSTANCE); StaticBinder.getInstance(ActionTypeRegistry.class.getClassLoader()).bind(ActionTypeRegistry.class, SimpleActionTypeRegistry.INSTANCE); StaticBinder.getInstance(PortalConfig.class.getClassLoader()).bind(PortalConfig.class, portalConfig.getRoot()); StaticBinder.getInstance(PortalProvider.class.getClassLoader()).bind(PortalProvider.class, portalProvider); @@ -125,7 +128,7 @@ public NBT nbt(World world) { return NBT.builder() .registerTypeHierarchyAdapter(BoundingBox.class, new BoundingBoxAdapter(world)) .registerTypeHierarchyAdapter(EntryAction.class, new EntryActionAdapter(this)) - .registerTypeHierarchyAdapter(Position.class, new PositionAdapter()) + .registerTypeHierarchyAdapter(Position.class, new FinePositionAdapter()) .registerTypeHierarchyAdapter(Key.class, new KeyAdapter()) .registerTypeHierarchyAdapter(Portal.class, new PortalAdapter(this)) .build(); diff --git a/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java b/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java index d2a3835..c482a3e 100644 --- a/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java +++ b/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java @@ -3,8 +3,9 @@ import core.paper.messenger.PluginMessenger; import io.papermc.paper.entity.TeleportFlag; import net.thenextlvl.portals.PortalLike; +import net.thenextlvl.portals.PortalsPlugin; import net.thenextlvl.portals.listener.PortalListener; -import net.thenextlvl.portals.model.Bounds; +import net.thenextlvl.portals.bounds.Bounds; import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent; @@ -17,8 +18,9 @@ @NullMarked public final class SimpleActionTypes implements ActionTypes { public static final SimpleActionTypes INSTANCE = new SimpleActionTypes(); - - private final PluginMessenger messenger = new PluginMessenger(JavaPlugin.getProvidingPlugin(SimpleActionTypes.class)); + + private final PortalsPlugin plugin = JavaPlugin.getPlugin(PortalsPlugin.class); + private final PluginMessenger messenger = new PluginMessenger(plugin); private final ActionType connect = ActionType.create("connect", String.class, (entity, portal, server) -> { if (!(entity instanceof Player player)) return false; @@ -115,9 +117,17 @@ public final class SimpleActionTypes implements ActionTypes { }); private final ActionType teleportRandom = ActionType.create("teleport_random", Bounds.class, (entity, portal, bounds) -> { - var random = ThreadLocalRandom.current(); - var location = bounds.getRandomLocation(random).setRotation(entity.getLocation().getRotation()); - entity.teleportAsync(location, PlayerTeleportEvent.TeleportCause.PLUGIN); + bounds.searchSafeLocation(ThreadLocalRandom.current()).thenAccept(location -> { + if (location == null) { + System.out.println("Failed to find a safe location within bounds: " + bounds); + // todo: send message to player + // plugin.bundle().sendMessage(entity, "portal.action.teleport-random.failed"); + return; + } + location.setRotation(entity.getLocation().getRotation()); + entity.teleportAsync(location, PlayerTeleportEvent.TeleportCause.PLUGIN); + System.out.println("random teleported to " + location); + }); return true; }); diff --git a/src/main/java/net/thenextlvl/portals/adapter/BlockPositionAdapter.java b/src/main/java/net/thenextlvl/portals/adapter/BlockPositionAdapter.java new file mode 100644 index 0000000..5679936 --- /dev/null +++ b/src/main/java/net/thenextlvl/portals/adapter/BlockPositionAdapter.java @@ -0,0 +1,32 @@ +package net.thenextlvl.portals.adapter; + +import io.papermc.paper.math.BlockPosition; +import io.papermc.paper.math.Position; +import net.thenextlvl.nbt.serialization.ParserException; +import net.thenextlvl.nbt.serialization.TagAdapter; +import net.thenextlvl.nbt.serialization.TagDeserializationContext; +import net.thenextlvl.nbt.serialization.TagSerializationContext; +import net.thenextlvl.nbt.tag.CompoundTag; +import net.thenextlvl.nbt.tag.Tag; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class BlockPositionAdapter implements TagAdapter { + @Override + public BlockPosition deserialize(Tag tag, TagDeserializationContext context) throws ParserException { + var root = tag.getAsCompound(); + var x = root.get("x").getAsInt(); + var y = root.get("y").getAsInt(); + var z = root.get("z").getAsInt(); + return Position.block(x, y, z); + } + + @Override + public Tag serialize(BlockPosition position, TagSerializationContext context) throws ParserException { + var tag = CompoundTag.empty(); + tag.add("x", position.blockX()); + tag.add("y", position.blockY()); + tag.add("z", position.blockZ()); + return tag; + } +} diff --git a/src/main/java/net/thenextlvl/portals/adapter/BoundsAdapter.java b/src/main/java/net/thenextlvl/portals/adapter/BoundsAdapter.java index dbdd581..cccea66 100644 --- a/src/main/java/net/thenextlvl/portals/adapter/BoundsAdapter.java +++ b/src/main/java/net/thenextlvl/portals/adapter/BoundsAdapter.java @@ -1,14 +1,14 @@ package net.thenextlvl.portals.adapter; -import io.papermc.paper.math.FinePosition; +import io.papermc.paper.math.BlockPosition; +import net.kyori.adventure.key.Key; import net.thenextlvl.nbt.serialization.ParserException; import net.thenextlvl.nbt.serialization.TagAdapter; import net.thenextlvl.nbt.serialization.TagDeserializationContext; import net.thenextlvl.nbt.serialization.TagSerializationContext; import net.thenextlvl.nbt.tag.CompoundTag; import net.thenextlvl.nbt.tag.Tag; -import net.thenextlvl.portals.model.Bounds; -import org.bukkit.World; +import net.thenextlvl.portals.bounds.Bounds; import org.jspecify.annotations.NullMarked; @NullMarked @@ -16,18 +16,18 @@ public final class BoundsAdapter implements TagAdapter { @Override public Bounds deserialize(Tag tag, TagDeserializationContext context) throws ParserException { var root = tag.getAsCompound(); - var world = context.deserialize(root.get("world"), World.class); - var min = context.deserialize(root.get("min"), FinePosition.class); - var max = context.deserialize(root.get("max"), FinePosition.class); - return new Bounds(world, min, max); + var world = context.deserialize(root.get("world"), Key.class); + var min = context.deserialize(root.get("min"), BlockPosition.class); + var max = context.deserialize(root.get("max"), BlockPosition.class); + return Bounds.factory().of(world, min, max); } @Override public Tag serialize(Bounds bounds, TagSerializationContext context) throws ParserException { var tag = CompoundTag.empty(); - tag.add("world", context.serialize(bounds.world(), World.class)); - tag.add("min", context.serialize(bounds.minPosition(), FinePosition.class)); - tag.add("max", context.serialize(bounds.maxPosition(), FinePosition.class)); + tag.add("world", context.serialize(bounds.worldKey(), Key.class)); + tag.add("min", context.serialize(bounds.minPosition())); + tag.add("max", context.serialize(bounds.maxPosition())); return tag; } } diff --git a/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java b/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java index 88b4c65..c40bf30 100644 --- a/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java +++ b/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java @@ -1,5 +1,6 @@ package net.thenextlvl.portals.adapter; +import io.papermc.paper.math.BlockPosition; import net.kyori.adventure.key.Key; import net.thenextlvl.nbt.serialization.NBT; import net.thenextlvl.nbt.serialization.ParserException; @@ -12,7 +13,7 @@ import net.thenextlvl.portals.PortalsPlugin; import net.thenextlvl.portals.action.ActionTypeRegistry; import net.thenextlvl.portals.action.EntryAction; -import net.thenextlvl.portals.model.Bounds; +import net.thenextlvl.portals.bounds.Bounds; import org.bukkit.Location; import org.jspecify.annotations.NullMarked; @@ -22,6 +23,7 @@ public final class EntryActionAdapter implements TagAdapter> { public EntryActionAdapter(PortalsPlugin plugin) { this.nbt = NBT.builder() + .registerTypeHierarchyAdapter(BlockPosition.class, new BlockPositionAdapter()) .registerTypeHierarchyAdapter(Bounds.class, new BoundsAdapter()) .registerTypeHierarchyAdapter(Key.class, new KeyAdapter()) .registerTypeHierarchyAdapter(Location.class, new LazyLocationAdapter()) diff --git a/src/main/java/net/thenextlvl/portals/adapter/PositionAdapter.java b/src/main/java/net/thenextlvl/portals/adapter/FinePositionAdapter.java similarity index 93% rename from src/main/java/net/thenextlvl/portals/adapter/PositionAdapter.java rename to src/main/java/net/thenextlvl/portals/adapter/FinePositionAdapter.java index d29c895..71661c6 100644 --- a/src/main/java/net/thenextlvl/portals/adapter/PositionAdapter.java +++ b/src/main/java/net/thenextlvl/portals/adapter/FinePositionAdapter.java @@ -10,7 +10,7 @@ import org.jspecify.annotations.NullMarked; @NullMarked -public final class PositionAdapter implements TagAdapter { +public final class FinePositionAdapter implements TagAdapter { @Override public Position deserialize(Tag tag, TagDeserializationContext context) throws ParserException { var root = tag.getAsCompound(); diff --git a/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java new file mode 100644 index 0000000..0c238d4 --- /dev/null +++ b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java @@ -0,0 +1,143 @@ +package net.thenextlvl.portals.bounds; + +import io.papermc.paper.math.BlockPosition; +import io.papermc.paper.math.Position; +import net.kyori.adventure.key.Key; +import net.thenextlvl.portals.PortalsPlugin; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.plugin.java.JavaPlugin; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; +import java.util.Random; +import java.util.concurrent.CompletableFuture; + +@NullMarked +record SimpleBounds( + Key worldKey, + int minX, int minY, int minZ, + int maxX, int maxY, int maxZ +) implements Bounds { + private static final PortalsPlugin plugin = JavaPlugin.getPlugin(PortalsPlugin.class); + + public SimpleBounds(Key worldKey, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + this.minX = Math.min(minX, maxX); + this.minY = Math.min(minY, maxY); + this.minZ = Math.min(minZ, maxZ); + this.maxX = Math.max(minX, maxX); + this.maxY = Math.max(minY, maxY); + this.maxZ = Math.max(minZ, maxZ); + this.worldKey = worldKey; + } + + @Override + public Optional world() { + return Optional.ofNullable(plugin.getServer().getWorld(worldKey)); + } + + @Override + public BlockPosition minPosition() { + return Position.block(minX, minY, minZ); + } + + @Override + public BlockPosition maxPosition() { + return Position.block(maxX, maxY, maxZ); + } + + @Override + public CompletableFuture<@Nullable Location> searchSafeLocation(Random random) { + var world = world().orElse(null); + if (world == null) return CompletableFuture.completedFuture(null); + + // Clamp bounds to world border + var border = world.getWorldBorder(); + var borderSize = Math.min((int) border.getSize() / 2, plugin.getServer().getMaxWorldSize()); + var centerX = border.getCenter().getBlockX(); + var centerZ = border.getCenter().getBlockZ(); + var borderMinX = centerX - borderSize; + var borderMaxX = centerX + borderSize; + var borderMinZ = centerZ - borderSize; + var borderMaxZ = centerZ + borderSize; + + var clampedMinX = Math.clamp(minX, borderMinX, borderMaxX); + var clampedMaxX = Math.clamp(maxX, borderMinX, borderMaxX); + var clampedMinZ = Math.clamp(minZ, borderMinZ, borderMaxZ); + var clampedMaxZ = Math.clamp(maxZ, borderMinZ, borderMaxZ); + + var initialX = clampedMinX == clampedMaxX ? clampedMaxX : random.nextInt(clampedMinX, clampedMaxX); + var initialZ = clampedMinZ == clampedMaxZ ? clampedMaxZ : random.nextInt(clampedMinZ, clampedMaxZ); + + // Try initial X and Z position + return searchSafeLocationAtXZ(random, world, initialX, initialZ).thenCompose(location -> { + if (location != null) return CompletableFuture.completedFuture(location); + + // Try different X position + var newX = getAlternativeCoordinate(random, initialX, clampedMinX, clampedMaxX); + return searchSafeLocationAtXZ(random, world, newX, initialZ); + }).thenCompose(location -> { + if (location != null) return CompletableFuture.completedFuture(location); + + // Try different Z position + var newZ = getAlternativeCoordinate(random, initialZ, clampedMinZ, clampedMaxZ); + return searchSafeLocationAtXZ(random, world, initialX, newZ); + }).thenCompose(location -> { + if (location != null) return CompletableFuture.completedFuture(location); + + // Try both new X and Z + var newX = getAlternativeCoordinate(random, initialX, clampedMinX, clampedMaxX); + var newZ = getAlternativeCoordinate(random, initialZ, clampedMinZ, clampedMaxZ); + return searchSafeLocationAtXZ(random, world, newX, newZ); + }); + } + + private CompletableFuture<@Nullable Location> searchSafeLocationAtXZ(Random random, World world, int x, int z) { + // Clamp to world height limits + var minY = Math.max(this.minY, world.getMinHeight()); + var maxY = Math.min(this.maxY, world.getMaxHeight()); + + var startY = minY == maxY ? maxY : random.nextInt(minY, maxY + 1); + + // Load chunk asynchronously before accessing blocks + return world.getChunkAtAsync(x >> 4, z >> 4).thenApply(chunk -> { + // Search upward from startY + for (int y = startY; y <= maxY; y++) { + if (isSafeLocation(world, x, y, z)) { + return new Location(world, x + 0.5, y, z + 0.5); + } + } + + // Search downward from startY + for (int y = startY - 1; y >= minY; y--) { + if (isSafeLocation(world, x, y, z)) { + return new Location(world, x + 0.5, y, z + 0.5); + } + } + + return null; + }); + } + + private int getAlternativeCoordinate(Random random, int current, int min, int max) { + if (min == max) return max; + return random.nextInt(min, max); + } + + private boolean isSafeLocation(World world, int x, int y, int z) { + if (!isValidSpawn(world.getBlockAt(x, y - 1, z))) return false; + if (isInvalidSpawnInside(world.getBlockAt(x, y, z))) return false; + return !isInvalidSpawnInside(world.getBlockAt(x, y + 1, z)); + } + + private boolean isValidSpawn(Block block) { + return block.isCollidable() && block.getType().isOccluding(); + } + + private boolean isInvalidSpawnInside(Block block) { + if (block.getLightFromSky() == 0 && !plugin.config().allowCaveSpawns()) return true; + return !block.isPassable() || block.isLiquid(); + } +} diff --git a/src/main/java/net/thenextlvl/portals/bounds/SimpleBoundsFactory.java b/src/main/java/net/thenextlvl/portals/bounds/SimpleBoundsFactory.java new file mode 100644 index 0000000..833a96e --- /dev/null +++ b/src/main/java/net/thenextlvl/portals/bounds/SimpleBoundsFactory.java @@ -0,0 +1,48 @@ +package net.thenextlvl.portals.bounds; + +import io.papermc.paper.math.Position; +import net.kyori.adventure.key.Key; +import org.bukkit.World; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class SimpleBoundsFactory implements BoundsFactory { + public static final SimpleBoundsFactory INSTANCE = new SimpleBoundsFactory(); + + @Override + public Bounds of(Key world, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + return new SimpleBounds(world, minX, minY, minZ, maxX, maxY, maxZ); + } + + @Override + public Bounds of(World world, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + return of(world.key(), minX, minY, minZ, maxX, maxY, maxZ); + } + + @Override + public Bounds of(Key key, Position min, Position max) { + return of(key, min.blockX(), min.blockY(), min.blockZ(), max.blockX(), max.blockY(), max.blockZ()); + } + + @Override + public Bounds of(World world, Position min, Position max) { + return of(world.key(), min, max); + } + + @Override + public Bounds radius(Key world, Position center, int radius, int height) { + return of(world, + center.blockX() - radius, + center.blockY() - height / 2, + center.blockZ() - radius, + center.blockX() + radius, + center.blockY() + height / 2, + center.blockZ() + radius + ); + } + + @Override + public Bounds radius(World world, Position center, int radius, int height) { + return radius(world.key(), center, radius, height); + } +} diff --git a/src/main/java/net/thenextlvl/portals/command/action/TeleportRandomCommand.java b/src/main/java/net/thenextlvl/portals/command/action/TeleportRandomCommand.java index 9b5d6fa..2903217 100644 --- a/src/main/java/net/thenextlvl/portals/command/action/TeleportRandomCommand.java +++ b/src/main/java/net/thenextlvl/portals/command/action/TeleportRandomCommand.java @@ -1,6 +1,6 @@ package net.thenextlvl.portals.command.action; -import com.mojang.brigadier.arguments.DoubleArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; @@ -8,13 +8,13 @@ import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; import io.papermc.paper.command.brigadier.argument.ArgumentTypes; -import io.papermc.paper.command.brigadier.argument.resolvers.FinePositionResolver; +import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver; import net.kyori.adventure.text.minimessage.tag.resolver.Formatter; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.thenextlvl.portals.Portal; import net.thenextlvl.portals.PortalsPlugin; import net.thenextlvl.portals.action.ActionTypes; -import net.thenextlvl.portals.model.Bounds; +import net.thenextlvl.portals.bounds.Bounds; import org.bukkit.World; import org.jspecify.annotations.NullMarked; @@ -28,19 +28,20 @@ public static LiteralArgumentBuilder create(PortalsPlugin pl var command = new TeleportRandomCommand(plugin); return command.create().then(Commands.argument("world", ArgumentTypes.world()) .then(command.boundsArgument()) - .then(command.radiusArgument())); + .then(command.radiusArgument()) + .executes(command)); } private ArgumentBuilder radiusArgument() { - var center = Commands.argument("center", ArgumentTypes.finePosition()); - var radius = Commands.argument("radius", DoubleArgumentType.doubleArg()); - var height = Commands.argument("height", DoubleArgumentType.doubleArg()); + var center = Commands.argument("center", ArgumentTypes.blockPosition()); + var radius = Commands.argument("radius", IntegerArgumentType.integer(1)); + var height = Commands.argument("height", IntegerArgumentType.integer(1)); return center.then(radius.then(height.executes(this))); } private ArgumentBuilder boundsArgument() { - var from = Commands.argument("from", ArgumentTypes.finePosition()); - var to = Commands.argument("to", ArgumentTypes.finePosition()); + var from = Commands.argument("from", ArgumentTypes.blockPosition()); + var to = Commands.argument("to", ArgumentTypes.blockPosition()); return from.then(to.executes(this)); } @@ -48,18 +49,23 @@ public static LiteralArgumentBuilder create(PortalsPlugin pl public int run(CommandContext context) throws CommandSyntaxException { var world = context.getArgument("world", World.class); - var bounds = resolveArgument(context, "center", FinePositionResolver.class).map(center -> { - var radius = context.getArgument("radius", Double.class); - var height = context.getArgument("height", Double.class); - return Bounds.radius(world, center, radius, height); + var bounds = resolveArgument(context, "center", BlockPositionResolver.class).map(center -> { + var radius = context.getArgument("radius", int.class); + var height = context.getArgument("height", int.class); + return Bounds.factory().radius(world, center, radius, height); }).orElse(null); if (bounds == null) { - var from = resolveArgument(context, "from", FinePositionResolver.class).orElseThrow(); - var to = resolveArgument(context, "to", FinePositionResolver.class).orElseThrow(); - bounds = new Bounds(world, from, to); + var from = resolveArgument(context, "from", BlockPositionResolver.class).orElse(null); + var to = resolveArgument(context, "to", BlockPositionResolver.class).orElse(null); + if (from != null && to != null) bounds = Bounds.factory().of(world, from, to); } + if (bounds == null) bounds = Bounds.factory().of(world, + -30_000_000, world.getMinHeight(), -30_000_000, + 30_000_000, world.getMaxHeight(), 30_000_000 + ); + return addAction(context, bounds); } @@ -67,7 +73,7 @@ public int run(CommandContext context) throws CommandSyntaxE protected void onSuccess(CommandContext context, Portal portal, Bounds input) { plugin.bundle().sendMessage(context.getSource().getSender(), "portal.action.teleport-random", Placeholder.parsed("portal", portal.getName()), - Placeholder.parsed("world", input.world().getName()), + Placeholder.parsed("world", input.world().map(World::getName).orElse(input.worldKey().asString())), Formatter.number("min_x", input.minX()), Formatter.number("min_y", input.minY()), Formatter.number("min_z", input.minZ()), diff --git a/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java b/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java index 75b2127..a168897 100644 --- a/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java +++ b/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java @@ -1,6 +1,9 @@ package net.thenextlvl.portals.model; +import net.thenextlvl.portals.view.PortalConfig; + public record SimplePortalConfig( + boolean allowCaveSpawns, boolean entryCosts, boolean ignoreEntityMovement, boolean pushBackOnEntryDenied,