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:
+ *
+ * - Pick a random X and Z coordinate within the bounds
+ * - Pick a random Y coordinate and search up and down for a safe location
+ * - If no safe location is found within height bounds, try a new X coordinate
+ * - If still no safe location is found, try a new Z coordinate
+ * - If still no safe location is found, try both new X and Z coordinates
+ *
+ *
+ * @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,