From fc1b4683fa007e55d63501af6d57c79c627e757e Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 14:36:49 +0100 Subject: [PATCH 1/9] Refactored Bounds API --- api/src/main/java/module-info.java | 1 + .../portals/action/ActionTypes.java | 2 +- .../net/thenextlvl/portals/bounds/Bounds.java | 136 +++++++++++++++ .../portals/bounds/BoundsFactory.java | 91 ++++++++++ .../net/thenextlvl/portals/model/Bounds.java | 120 ------------- .../net/thenextlvl/portals/PortalsPlugin.java | 4 +- .../portals/action/SimpleActionTypes.java | 16 +- .../portals/adapter/BoundsAdapter.java | 20 +-- .../portals/adapter/EntryActionAdapter.java | 2 +- .../portals/bounds/SimpleBounds.java | 157 ++++++++++++++++++ .../portals/bounds/SimpleBoundsFactory.java | 48 ++++++ .../command/action/TeleportRandomCommand.java | 17 +- 12 files changed, 471 insertions(+), 143 deletions(-) create mode 100644 api/src/main/java/net/thenextlvl/portals/bounds/Bounds.java create mode 100644 api/src/main/java/net/thenextlvl/portals/bounds/BoundsFactory.java delete mode 100644 api/src/main/java/net/thenextlvl/portals/model/Bounds.java create mode 100644 src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java create mode 100644 src/main/java/net/thenextlvl/portals/bounds/SimpleBoundsFactory.java diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index fd828d9..e21d1ea 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -3,6 +3,7 @@ @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; 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/src/main/java/net/thenextlvl/portals/PortalsPlugin.java b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java index fe17291..516d558 100644 --- a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java +++ b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java @@ -26,7 +26,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; @@ -66,6 +67,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); diff --git a/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java b/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java index d2a3835..4d8fd68 100644 --- a/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java +++ b/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java @@ -4,7 +4,7 @@ import io.papermc.paper.entity.TeleportFlag; import net.thenextlvl.portals.PortalLike; 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; @@ -115,9 +115,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/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..5fca8f8 100644 --- a/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java +++ b/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java @@ -12,7 +12,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; 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..a9e84c1 --- /dev/null +++ b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java @@ -0,0 +1,157 @@ +package net.thenextlvl.portals.bounds; + +import io.papermc.paper.math.BlockPosition; +import io.papermc.paper.math.Position; +import io.papermc.paper.registry.RegistryAccess; +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; +import io.papermc.paper.registry.tag.TagKey; +import net.kyori.adventure.key.Key; +import net.thenextlvl.portals.view.PortalConfig; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.BlockType; +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 { + 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(Bukkit.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, Bukkit.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); + System.out.println(startY); + + // 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 (PortalConfig.config().customSafeSearchAlgorithm()) { + if (!isValidSpawn(world.getBlockAt(x, y - 1, z))) return false; + if (isInvalidSpawn(world.getBlockAt(x, y, z))) return false; + return !isInvalidSpawn(world.getBlockAt(x, y + 1, z)); + } else { + if (!world.getBlockAt(x, y, z).isPassable()) return false; + if (!world.getBlockAt(x, y + 1, z).isPassable()) return false; + return world.getBlockAt(x, y - 1, z).isCollidable(); + } + } + + private boolean isValidSpawn(Block block) { + return isTagged(BlockTypeTagKeys.VALID_SPAWN, block); + } + + private boolean isInvalidSpawn(Block block) { + return isTagged(BlockTypeTagKeys.INVALID_SPAWN_INSIDE, block); + } + + private boolean isTagged(TagKey tagKey, Block block) { + return RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK) + .getTagValues(tagKey).contains(block.getType().asBlockType()); + } +} 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..06ecad0 100644 --- a/src/main/java/net/thenextlvl/portals/command/action/TeleportRandomCommand.java +++ b/src/main/java/net/thenextlvl/portals/command/action/TeleportRandomCommand.java @@ -14,7 +14,7 @@ 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; @@ -51,15 +51,20 @@ public int run(CommandContext context) throws CommandSyntaxE 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); + 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 +72,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()), From b550b893bed997fa0441392306d4064af772a9a4 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 14:38:02 +0100 Subject: [PATCH 2/9] Updated argument types in TeleportRandomCommand - Made all arguments optional apart from world --- .../command/action/TeleportRandomCommand.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) 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 06ecad0..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,7 +8,7 @@ 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; @@ -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,9 +49,9 @@ 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); + 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); From 7585e491cecda527081eb28f5ab0626e45e20f05 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 14:38:33 +0100 Subject: [PATCH 3/9] Moved PortalConfig to view package and added safe search flag --- api/src/main/java/module-info.java | 2 +- .../portals/{model => view}/PortalConfig.java | 27 ++++++++++++++----- .../net/thenextlvl/portals/PortalsPlugin.java | 2 +- .../portals/model/SimplePortalConfig.java | 3 +++ 4 files changed, 25 insertions(+), 9 deletions(-) rename api/src/main/java/net/thenextlvl/portals/{model => view}/PortalConfig.java (66%) diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index e21d1ea..879d9cb 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -5,9 +5,9 @@ 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/model/PortalConfig.java b/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java similarity index 66% 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..bad9f14 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,30 @@ 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 use the custom safe search algorithm. + *

+ * If {@code false}, the vanilla safe search algorithm is used. + *

+ * The custom safe search algorithm is less strict and allows cave and structure spawns + * that might potentially be unsafe. + * + * @return {@code true} if the custom safe search algorithm is used, {@code false} otherwise + * @since 0.2.0 + */ + boolean customSafeSearchAlgorithm(); + /** * 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 +46,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 +54,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 +62,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 516d558..5ba73d1 100644 --- a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java +++ b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java @@ -53,7 +53,7 @@ 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(true, true, false, true, 0.3), SimplePortalConfig.class ).validate().saveIfAbsent(); diff --git a/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java b/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java index 75b2127..4a6319c 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 customSafeSearchAlgorithm, boolean entryCosts, boolean ignoreEntityMovement, boolean pushBackOnEntryDenied, From 3dd40fdfa1c887a263f7feffb631558c41ec6d4c Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 14:38:40 +0100 Subject: [PATCH 4/9] Initialized plugin and messenger fields --- .../net/thenextlvl/portals/action/SimpleActionTypes.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java b/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java index 4d8fd68..c482a3e 100644 --- a/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java +++ b/src/main/java/net/thenextlvl/portals/action/SimpleActionTypes.java @@ -3,6 +3,7 @@ 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.bounds.Bounds; import org.bukkit.Location; @@ -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; From 4d3272ec4725ed439bf667901af1cbbfc34a58d1 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 14:38:49 +0100 Subject: [PATCH 5/9] Added BlockPositionAdapter for NBT serialization --- .../net/thenextlvl/portals/PortalsPlugin.java | 5 +-- .../portals/adapter/BlockPositionAdapter.java | 32 +++++++++++++++++++ .../portals/adapter/EntryActionAdapter.java | 2 ++ ...nAdapter.java => FinePositionAdapter.java} | 2 +- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 src/main/java/net/thenextlvl/portals/adapter/BlockPositionAdapter.java rename src/main/java/net/thenextlvl/portals/adapter/{PositionAdapter.java => FinePositionAdapter.java} (93%) diff --git a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java index 5ba73d1..19859b7 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; @@ -127,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/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/EntryActionAdapter.java b/src/main/java/net/thenextlvl/portals/adapter/EntryActionAdapter.java index 5fca8f8..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; @@ -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(); From cf9fbeab570f7074a9e47262cf70b9463f9b835d Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 15:36:01 +0100 Subject: [PATCH 6/9] Renamed safe search flag to allowCaveSpawns - Updated method name for clarity - Adjusted documentation to match new name --- .../net/thenextlvl/portals/view/PortalConfig.java | 11 +++-------- .../java/net/thenextlvl/portals/PortalsPlugin.java | 2 +- .../thenextlvl/portals/model/SimplePortalConfig.java | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java b/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java index bad9f14..8c75141 100644 --- a/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java +++ b/api/src/main/java/net/thenextlvl/portals/view/PortalConfig.java @@ -22,17 +22,12 @@ public interface PortalConfig { } /** - * Whether to use the custom safe search algorithm. - *

- * If {@code false}, the vanilla safe search algorithm is used. - *

- * The custom safe search algorithm is less strict and allows cave and structure spawns - * that might potentially be unsafe. + * Whether to allow random teleports to caves. * - * @return {@code true} if the custom safe search algorithm is used, {@code false} otherwise + * @return {@code true} if cave and spawns are allowed, {@code false} otherwise * @since 0.2.0 */ - boolean customSafeSearchAlgorithm(); + boolean allowCaveSpawns(); /** * Whether to use economy for entry costs. diff --git a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java index 19859b7..25ce579 100644 --- a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java +++ b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java @@ -54,7 +54,7 @@ public final class PortalsPlugin extends JavaPlugin { private final FileIO portalConfig = new GsonFile<>( IO.of(getDataPath().resolve("config.json")), - new SimplePortalConfig(true, true, false, true, 0.3), + new SimplePortalConfig(false, true, false, true, 0.3), SimplePortalConfig.class ).validate().saveIfAbsent(); diff --git a/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java b/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java index 4a6319c..a168897 100644 --- a/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java +++ b/src/main/java/net/thenextlvl/portals/model/SimplePortalConfig.java @@ -3,7 +3,7 @@ import net.thenextlvl.portals.view.PortalConfig; public record SimplePortalConfig( - boolean customSafeSearchAlgorithm, + boolean allowCaveSpawns, boolean entryCosts, boolean ignoreEntityMovement, boolean pushBackOnEntryDenied, From b051200b3be1ad73ed28aa4a1cd15274fc959434 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 15:36:15 +0100 Subject: [PATCH 7/9] Always save config --- src/main/java/net/thenextlvl/portals/PortalsPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java index 25ce579..3f67e05 100644 --- a/src/main/java/net/thenextlvl/portals/PortalsPlugin.java +++ b/src/main/java/net/thenextlvl/portals/PortalsPlugin.java @@ -56,7 +56,7 @@ public final class PortalsPlugin extends JavaPlugin { IO.of(getDataPath().resolve("config.json")), new SimplePortalConfig(false, true, false, true, 0.3), SimplePortalConfig.class - ).validate().saveIfAbsent(); + ).validate().save(); private final ComponentBundle bundle = ComponentBundle.builder( Key.key("portals", "translations"), From 4d8374f616910fdcf48c54b3fcdc851e04b5be9b Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 15:36:51 +0100 Subject: [PATCH 8/9] Refactored bounds to use plugin instance --- .../net/thenextlvl/portals/bounds/SimpleBounds.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java index a9e84c1..667c88b 100644 --- a/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java +++ b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java @@ -7,12 +7,11 @@ import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; import io.papermc.paper.registry.tag.TagKey; import net.kyori.adventure.key.Key; -import net.thenextlvl.portals.view.PortalConfig; -import org.bukkit.Bukkit; +import net.thenextlvl.portals.PortalsPlugin; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.block.Block; -import org.bukkit.block.BlockType; +import org.bukkit.plugin.java.JavaPlugin; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -26,6 +25,8 @@ record SimpleBounds( 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); @@ -38,7 +39,7 @@ public SimpleBounds(Key worldKey, int minX, int minY, int minZ, int maxX, int ma @Override public Optional world() { - return Optional.ofNullable(Bukkit.getWorld(worldKey)); + return Optional.ofNullable(plugin.getServer().getWorld(worldKey)); } @Override @@ -58,7 +59,7 @@ public BlockPosition maxPosition() { // Clamp bounds to world border var border = world.getWorldBorder(); - var borderSize = Math.min((int) border.getSize() / 2, Bukkit.getMaxWorldSize()); + 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; @@ -103,7 +104,6 @@ public BlockPosition maxPosition() { var maxY = Math.min(this.maxY, world.getMaxHeight()); var startY = minY == maxY ? maxY : random.nextInt(minY, maxY + 1); - System.out.println(startY); // Load chunk asynchronously before accessing blocks return world.getChunkAtAsync(x >> 4, z >> 4).thenApply(chunk -> { From 8440fe3ce093fb39e1bf685c26347fb42b392ec4 Mon Sep 17 00:00:00 2001 From: david Date: Sun, 9 Nov 2025 15:37:03 +0100 Subject: [PATCH 9/9] Refined spawn safety checks in SimpleBounds - Simplified isSafeLocation logic - Updated spawn validation to use occluding and passable checks - Removed registry tag dependencies for spawn validation --- .../portals/bounds/SimpleBounds.java | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java index 667c88b..0c238d4 100644 --- a/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java +++ b/src/main/java/net/thenextlvl/portals/bounds/SimpleBounds.java @@ -2,10 +2,6 @@ import io.papermc.paper.math.BlockPosition; import io.papermc.paper.math.Position; -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; -import io.papermc.paper.registry.tag.TagKey; import net.kyori.adventure.key.Key; import net.thenextlvl.portals.PortalsPlugin; import org.bukkit.Location; @@ -131,27 +127,17 @@ private int getAlternativeCoordinate(Random random, int current, int min, int ma } private boolean isSafeLocation(World world, int x, int y, int z) { - if (PortalConfig.config().customSafeSearchAlgorithm()) { - if (!isValidSpawn(world.getBlockAt(x, y - 1, z))) return false; - if (isInvalidSpawn(world.getBlockAt(x, y, z))) return false; - return !isInvalidSpawn(world.getBlockAt(x, y + 1, z)); - } else { - if (!world.getBlockAt(x, y, z).isPassable()) return false; - if (!world.getBlockAt(x, y + 1, z).isPassable()) return false; - return world.getBlockAt(x, y - 1, z).isCollidable(); - } + 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 isTagged(BlockTypeTagKeys.VALID_SPAWN, block); + return block.isCollidable() && block.getType().isOccluding(); } - private boolean isInvalidSpawn(Block block) { - return isTagged(BlockTypeTagKeys.INVALID_SPAWN_INSIDE, block); - } - - private boolean isTagged(TagKey tagKey, Block block) { - return RegistryAccess.registryAccess().getRegistry(RegistryKey.BLOCK) - .getTagValues(tagKey).contains(block.getType().asBlockType()); + private boolean isInvalidSpawnInside(Block block) { + if (block.getLightFromSky() == 0 && !plugin.config().allowCaveSpawns()) return true; + return !block.isPassable() || block.isLiquid(); } }