diff --git a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java index 7dfd49f30..e08c6699c 100644 --- a/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/core/command/MVCommandContexts.java @@ -116,7 +116,8 @@ private ContentFilter parseContentFilter(BukkitCommandExecutionContext context) } return destinationsProvider.parseDestination(context.getSender(), destination) - .getOrThrow(failure -> MVInvalidCommandArgument.of(failure.getFailureMessage())); + .getOrThrow(failure -> + new InvalidCommandArgument(failure.getFailureMessage().formatted(context.getIssuer()))); } private GameRule parseGameRule(BukkitCommandExecutionContext context) { diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java index e1b0b4337..c37969d05 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestination.java @@ -19,8 +19,12 @@ import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.destination.Destination; import org.mvplugins.multiverse.core.destination.DestinationSuggestionPacket; +import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException; import org.mvplugins.multiverse.core.locale.MVCorei18n; import org.mvplugins.multiverse.core.utils.REPatterns; +import org.mvplugins.multiverse.core.utils.position.EntityPosition; +import org.mvplugins.multiverse.core.utils.position.PositionNumber; +import org.mvplugins.multiverse.core.utils.position.VectorPosition; import org.mvplugins.multiverse.core.utils.result.Attempt; import org.mvplugins.multiverse.core.utils.result.FailureReason; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; @@ -62,7 +66,11 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry * @return A new {@link ExactDestinationInstance} */ public @NotNull ExactDestinationInstance fromLocation(@NotNull Location location) { - return new ExactDestinationInstance(this, new UnloadedWorldLocation(location)); + return new ExactDestinationInstance( + this, + location.getWorld().getName(), + EntityPosition.ofLocation(location) + ); } /** @@ -73,12 +81,16 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry @NotNull CommandSender sender, @NotNull String destinationParams ) { - String[] items = REPatterns.COLON.split(destinationParams); + String[] items = REPatterns.COLON.split(destinationParams, 2); if (items.length < 2) { if (items[0].equals("@here")) { return getLocationFromSender(sender) .map(location -> Attempt.success( - new ExactDestinationInstance(this, new UnloadedWorldLocation(location)) + new ExactDestinationInstance( + this, + location.getWorld().getName(), + EntityPosition.ofLocation(location) + ) )) .getOrElse(() -> Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT)); // todo: specific failure reason for this case } @@ -86,41 +98,21 @@ public ExactDestination(CoreConfig config, WorldManager worldManager, WorldEntry } String worldName = items[0]; - String coordinates = items[1]; - String[] coordinatesParams = REPatterns.COMMA.split(coordinates); - if (coordinatesParams.length != 3) { - return Attempt.failure(InstanceFailureReason.INVALID_COORDINATES_FORMAT); - } + String positionStr = items[1]; World world = getLoadedMultiverseWorld(worldName).flatMap(LoadedMultiverseWorld::getBukkitWorld).getOrNull(); if (world == null) { return Attempt.failure(InstanceFailureReason.WORLD_NOT_FOUND, Replace.WORLD.with(worldName)); } - UnloadedWorldLocation location; + EntityPosition position; try { - location = new UnloadedWorldLocation( - world, - Double.parseDouble(coordinatesParams[0]), - Double.parseDouble(coordinatesParams[1]), - Double.parseDouble(coordinatesParams[2]) - ); - } catch (NumberFormatException e) { + position = EntityPosition.fromString(positionStr); + } catch (PositionParseException e) { return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e)); } - if (items.length == 4) { - String pitch = items[2]; - String yaw = items[3]; - try { - location.setPitch(Float.parseFloat(pitch)); - location.setYaw(Float.parseFloat(yaw)); - } catch (NumberFormatException e) { - return Attempt.failure(InstanceFailureReason.INVALID_NUMBER_FORMAT, Replace.ERROR.with(e)); - } - } - - return Attempt.success(new ExactDestinationInstance(this, location)); + return Attempt.success(new ExactDestinationInstance(this, worldName, position)); } //TODO: Extract to a world finder class diff --git a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java index aa31df75e..3eebadfd8 100644 --- a/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java +++ b/src/main/java/org/mvplugins/multiverse/core/destination/core/ExactDestinationInstance.java @@ -1,6 +1,7 @@ package org.mvplugins.multiverse.core.destination.core; import io.vavr.control.Option; +import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.entity.Entity; @@ -8,22 +9,30 @@ import org.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.core.destination.DestinationInstance; +import org.mvplugins.multiverse.core.utils.position.EntityPosition; +import org.mvplugins.multiverse.core.utils.position.VectorPosition; import org.mvplugins.multiverse.core.world.location.UnloadedWorldLocation; /** * Destination instance implementation for the {@link ExactDestination}. */ public final class ExactDestinationInstance extends DestinationInstance { - private final UnloadedWorldLocation location; + private final String worldName; + private final EntityPosition position; /** * Constructor. * - * @param location The location to teleport to. + * @param destination The parent destination. + * @param worldName The name of the world. + * @param position The position in the world. */ - ExactDestinationInstance(@NotNull ExactDestination destination, @NotNull UnloadedWorldLocation location) { + ExactDestinationInstance(@NotNull ExactDestination destination, + @NotNull String worldName, + @NotNull EntityPosition position) { super(destination); - this.location = location; + this.worldName = worldName; + this.position = position; } /** @@ -31,10 +40,13 @@ public final class ExactDestinationInstance extends DestinationInstance getLocation(@NotNull Entity teleportee) { - if (location.getWorld() == null) { + World world = Bukkit.getWorld(worldName); + if (world == null) { return Option.none(); } - return Option.of(location.toBukkitLocation()); + Location destinationLocation = position.toBukkitLocation(teleportee.getLocation()); + destinationLocation.setWorld(world); + return Option.of(destinationLocation); } /** @@ -58,7 +70,7 @@ public boolean checkTeleportSafety() { */ @Override public @NotNull Option getFinerPermissionSuffix() { - return Option.of(location.getWorld()).map(World::getName); + return Option.of(worldName); } /** @@ -66,7 +78,6 @@ public boolean checkTeleportSafety() { */ @Override public @NotNull String serialise() { - return location.getWorldName() + ":" + location.getX() + "," + location.getY() - + "," + location.getZ() + ":" + location.getPitch() + ":" + location.getYaw(); + return worldName + ":" + position.toString(); } } diff --git a/src/main/java/org/mvplugins/multiverse/core/exceptions/utils/position/PositionParseException.java b/src/main/java/org/mvplugins/multiverse/core/exceptions/utils/position/PositionParseException.java new file mode 100644 index 000000000..6dd96f4f0 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/exceptions/utils/position/PositionParseException.java @@ -0,0 +1,23 @@ +package org.mvplugins.multiverse.core.exceptions.utils.position; + +import org.jetbrains.annotations.Nullable; +import org.mvplugins.multiverse.core.exceptions.MultiverseException; +import org.mvplugins.multiverse.core.locale.message.Message; + +public class PositionParseException extends MultiverseException { + public PositionParseException(String message) { + super(message); + } + + public PositionParseException(@Nullable Message message) { + super(message); + } + + public PositionParseException(@Nullable String message, @Nullable Throwable cause) { + super(message, cause); + } + + public PositionParseException(@Nullable Message message, @Nullable Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java index 7ab179660..4c6a80327 100644 --- a/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java +++ b/src/main/java/org/mvplugins/multiverse/core/locale/MVCorei18n.java @@ -346,6 +346,11 @@ public enum MVCorei18n implements MessageKeyProvider { EXCEPTION_MULTIVERSEWORLD_UNLOADPLAYERSINWORLD, EXCEPTION_MULTIVERSEWORLD_UNLOADERROR, + // multiverse position parse exception + EXCEPTION_POSITIONPARSE_INVALIDDIRECTION, + EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES, + EXCEPTION_POSITIONPARSE_INVALIDNUMBER, + // generic GENERIC_SUCCESS, GENERIC_FAILURE, diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/position/EntityPosition.java b/src/main/java/org/mvplugins/multiverse/core/utils/position/EntityPosition.java new file mode 100644 index 000000000..3a7c28f28 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/position/EntityPosition.java @@ -0,0 +1,150 @@ +package org.mvplugins.multiverse.core.utils.position; + +import org.bukkit.Location; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException; +import org.mvplugins.multiverse.core.utils.REPatterns; + +/** + * Represents a position for an entity in 3D space, including both coordinates and facing direction. + * + * @since 5.3 + */ +@ApiStatus.AvailableSince("5.3") +public class EntityPosition { + + /** + * Creates an EntityPosition with absolute coordinates and direction. + * + * @param x The absolute X coordinate. + * @param y The absolute Y coordinate. + * @param z The absolute Z coordinate. + * @param pitch The absolute pitch (vertical angle). + * @param yaw The absolute yaw (horizontal angle). + * @return A new EntityPosition instance with the specified absolute coordinates and direction. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static EntityPosition ofAbsolute(double x, double y, double z, double pitch, double yaw) { + return new EntityPosition( + VectorPosition.ofAbsolute(x, y, z), + FaceDirection.ofAbsolute(pitch, yaw) + ); + } + + /** + * Creates an EntityPosition from a Bukkit Location with absolute coordinates and direction. + * + * @param location The Bukkit Location to convert. + * @return A new EntityPosition instance representing the given location. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static EntityPosition ofLocation(Location location) { + return new EntityPosition(VectorPosition.ofLocation(location), FaceDirection.ofLocation(location)); + } + + /** + * Parses an EntityPosition from a string representation. + * The expected format is "<x>,<y>,<z>:<pitch>:<yaw>" for absolute coordinates and direction, + * or "<x>,<y>,<z>" for absolute coordinates with default direction (0 pitch, 0 yaw). + *
+ * Relative coordinates and direction can be specified using the '~' prefix, e.g., "~10,~,~-10:0:90". + * + * @param positionStr The string representation of the position. + * @return A new EntityPosition instance parsed from the string. + * @throws PositionParseException If the string format is invalid. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static EntityPosition fromString(String positionStr) throws PositionParseException { + String[] parts = REPatterns.COLON.split(positionStr, 2); + return parts.length == 2 + ? new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.fromString(parts[1])) + : new EntityPosition(VectorPosition.fromString(parts[0]), FaceDirection.ofAbsolute(0, 0)); + } + + private final VectorPosition vector; + private final FaceDirection direction; + + /** + * Creates a new EntityPosition with the specified vector and direction. + * + * @param vector The vector position (coordinates). + * @param direction The facing direction (pitch and yaw). + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public EntityPosition(VectorPosition vector, FaceDirection direction) { + this.vector = vector; + this.direction = direction; + } + + /** + * Gets the vector position (coordinates). + * + * @return The vector position. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public VectorPosition getVector() { + return vector; + } + + /** + * Gets the facing direction (pitch and yaw). + * + * @return The facing direction. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public FaceDirection getDirection() { + return direction; + } + + /** + * Augments a given Bukkit Location by applying the relative components of this EntityPosition. + * This modifies the provided Location in place. + * + * @param base The base Bukkit Location to augment and offset for relative positioning as required. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public void augmentBukkitLocation(Location base) { + vector.augmentBukkitLocation(base); + direction.augmentBukkitLocation(base); + } + + /** + * Converts this EntityPosition to a new Bukkit Location based on a given base Location. + * This does not modify the base Location, but returns a new Location instance. + * + * @param base The base Bukkit Location to use as a reference for relative positioning as required. + * @return A new Bukkit Location representing this EntityPosition. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public Location toBukkitLocation(Location base) { + return new Location( + base.getWorld(), + vector.getX().getValue(base.getX()), + vector.getY().getValue(base.getY()), + vector.getZ().getValue(base.getZ()), + (float) direction.getYaw().getValue(base.getYaw()), + (float) direction.getPitch().getValue(base.getPitch()) + ); + } + + @Override + public String toString() { + return vector + ":" + direction; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/position/FaceDirection.java b/src/main/java/org/mvplugins/multiverse/core/utils/position/FaceDirection.java new file mode 100644 index 000000000..7d2f40f6f --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/position/FaceDirection.java @@ -0,0 +1,138 @@ +package org.mvplugins.multiverse.core.utils.position; + +import org.bukkit.Location; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException; +import org.mvplugins.multiverse.core.locale.MVCorei18n; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.REPatterns; + +import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace; + +/** + * Represents a direction to face, defined by pitch and yaw. + * + * @since 5.3 + */ +@ApiStatus.AvailableSince("5.3") +public class FaceDirection { + + /** + * Creates a FaceDirection with absolute pitch and yaw. + * + * @param pitch The absolute pitch (vertical angle). + * @param yaw The absolute yaw (horizontal angle). + * @return A new FaceDirection instance with the specified absolute pitch and yaw. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static FaceDirection ofAbsolute(double pitch, double yaw) { + return new FaceDirection( + PositionNumber.ofAbsolute(pitch), + PositionNumber.ofAbsolute(yaw) + ); + } + + /** + * Gets the pitch and yaw from a Bukkit Location to create an FaceDirection with absolute values. + * + * @param location The Bukkit Location to convert. + * @return A new FaceDirection instance representing the direction of the given location. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static FaceDirection ofLocation(org.bukkit.Location location) { + return new FaceDirection( + PositionNumber.ofAbsolute(location.getPitch()), + PositionNumber.ofAbsolute(location.getYaw()) + ); + } + + /** + * Parses a FaceDirection from a string representation. + * The expected format is "<pitch>:<yaw>" for absolute pitch and yaw. + *
+ * Relative pitch and yaw can be specified using the '~' prefix, e.g., "~:~5". + * + * @param directionStr The string representation of the direction. + * @return A new FaceDirection instance parsed from the string. + * @throws PositionParseException If the string format is invalid. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static FaceDirection fromString(String directionStr) throws PositionParseException { + String[] parts = REPatterns.COLON.split(directionStr, 2); + if (parts.length != 2) { + throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDDIRECTION, + replace("{format}").with(directionStr))); + } + //TODO: Add support for compass directions (N, S, E, W, NE, NW, SE, SW) for yaw + PositionNumber pitch = PositionNumber.fromString(parts[0]); + PositionNumber yaw = PositionNumber.fromString(parts[1]); + return new FaceDirection(pitch, yaw); + } + + private final PositionNumber pitch; + private final PositionNumber yaw; + + /** + * Creates a new FaceDirection with the specified pitch and yaw. + * + * @param pitch The pitch (vertical angle). + * @param yaw The yaw (horizontal angle). + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public FaceDirection(PositionNumber pitch, PositionNumber yaw) { + this.pitch = pitch; + this.yaw = yaw; + } + + /** + * Gets the pitch component of this FaceDirection. + * + * @return The pitch number representation. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public PositionNumber getPitch() { + return pitch; + } + + /** + * Gets the yaw component of this FaceDirection. + * + * @return The yaw number representation. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public PositionNumber getYaw() { + return yaw; + } + + /** + * Applies this FaceDirection to a given Bukkit Location, modifying its pitch and yaw. + *
+ * Relative pitch and yaw will be offset based on the current values in the Location. + * + * @param base The Bukkit Location to modify. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public void augmentBukkitLocation(Location base) { + base.setPitch((float) pitch.getValue(base.getPitch())); + base.setYaw((float) yaw.getValue(base.getYaw())); + } + + @Override + public String toString() { + return pitch + ":" + yaw; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/position/PositionNumber.java b/src/main/java/org/mvplugins/multiverse/core/utils/position/PositionNumber.java new file mode 100644 index 000000000..c00cf21c9 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/position/PositionNumber.java @@ -0,0 +1,187 @@ +package org.mvplugins.multiverse.core.utils.position; + +import io.vavr.control.Try; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException; +import org.mvplugins.multiverse.core.locale.MVCorei18n; +import org.mvplugins.multiverse.core.locale.message.Message; + +import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace; + +/** + * Represents a number that can be either absolute or relative (prefixed with '~'). + * + * @since 5.3 + */ +@ApiStatus.AvailableSince("5.3") +public sealed interface PositionNumber permits PositionNumber.Relative, PositionNumber.Absolute { + + /** + * Creates an absolute PositionNumber. Absolute numbers represent fixed coordinates that will not be adjusted + * based on any base value. + * + * @param value the absolute value + * @return a PositionNumber representing the absolute value + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + static PositionNumber ofAbsolute(double value) { + return new Absolute(value); + } + + /** + * Creates a relative PositionNumber. Relative numbers are prefixed with '~' and represent an offset + * from a base value. For example, a relative PositionNumber of '~5' means 5 units more than the base value, + * while '~ -3' means 3 units less than the base value. + * + * @param value the relative offset value + * @return a PositionNumber representing the relative offset + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + static PositionNumber ofRelative(double value) { + return new Relative(value); + } + + /** + * Parses a PositionNumber from a string. The string can represent either an absolute number (e.g., "10.5") + * or a relative number prefixed with '~' (e.g., "~5" or "~ -3"). If the string is "~" with no number, + * it is treated as a relative value of 0. + * + * @param string the string to parse + * @return a PositionNumber representing the parsed value. + * @throws PositionParseException If the string format is invalid. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + static PositionNumber fromString(String string) throws PositionParseException { + if (string.startsWith("~")) { + if (string.length() == 1) { + return new Relative(0); + } + return new Relative(tryParseDouble(string.substring(1))); + } + return new Absolute(tryParseDouble(string)); + } + + private static double tryParseDouble(String str) throws PositionParseException { + return Try.of(() -> Double.parseDouble(str)) + .getOrElseThrow(throwable -> new PositionParseException( + Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDNUMBER, + replace("{number}").with(str)))); + } + + /** + * Calculates the effective value based on the base value. + * For absolute PositionNumbers, this returns the stored value. + * For relative PositionNumbers, this returns base + stored value. + * + * @param base the base value to use for relative calculations + * @return the calculated effective value + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + double getValue(double base); + + /** + * Checks if this PositionNumber is relative. + * + * @return true if this is a relative PositionNumber, false if absolute + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + boolean isRelative(); + + /** + * Checks if this PositionNumber is absolute. + * + * @return true if this is an absolute PositionNumber, false if relative + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + boolean isAbsolute(); + + /** + * Gets the raw stored value without any base adjustment. + * + * @return the raw value (for relative, this is the offset; for absolute, this is the fixed value) + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + double getRawValue(); + + final class Relative implements PositionNumber { + + private final double value; + + Relative(double value) { + this.value = value; + } + + @Override + public double getValue(double base) { + return base + value; + } + + @Override + public boolean isRelative() { + return true; + } + + @Override + public boolean isAbsolute() { + return false; + } + + @Override + public double getRawValue() { + return value; + } + + @Override + public String toString() { + return "~" + value; + } + } + + final class Absolute implements PositionNumber { + + private final double value; + + Absolute(double value) { + this.value = value; + } + + @Override + public double getValue(double base) { + return value; + } + + @Override + public boolean isRelative() { + return false; + } + + @Override + public boolean isAbsolute() { + return true; + } + + @Override + public double getRawValue() { + return value; + } + + @Override + public String toString() { + return Double.toString(value); + } + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/position/VectorPosition.java b/src/main/java/org/mvplugins/multiverse/core/utils/position/VectorPosition.java new file mode 100644 index 000000000..db2a8e45b --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/position/VectorPosition.java @@ -0,0 +1,227 @@ +package org.mvplugins.multiverse.core.utils.position; + +import org.bukkit.Location; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.ApiStatus; +import org.mvplugins.multiverse.core.exceptions.utils.position.PositionParseException; +import org.mvplugins.multiverse.core.locale.MVCorei18n; +import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.core.utils.REPatterns; + +import static org.mvplugins.multiverse.core.locale.message.MessageReplacement.replace; + +/** + * Represents an x, y, z position in 3D space, with support for absolute and relative coordinates. + * + * @since 5.3 + */ +@ApiStatus.AvailableSince("5.3") +public class VectorPosition { + + /** + * Creates a VectorPosition with absolute coordinates. + * @param x The absolute x coordinate. + * @param y The absolute y coordinate. + * @param z The absolute z coordinate. + * @return A new VectorPosition instance with the specified absolute coordinates. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static VectorPosition ofAbsolute(double x, double y, double z) { + return new VectorPosition( + PositionNumber.ofAbsolute(x), + PositionNumber.ofAbsolute(y), + PositionNumber.ofAbsolute(z) + ); + } + + /** + * Creates a VectorPosition from a Bukkit Vector, using absolute coordinates. + * + * @param vector The Bukkit Vector to convert. + * @return A new VectorPosition instance representing the given vector. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static VectorPosition ofVector(Vector vector) { + return new VectorPosition( + PositionNumber.ofAbsolute(vector.getX()), + PositionNumber.ofAbsolute(vector.getY()), + PositionNumber.ofAbsolute(vector.getZ()) + ); + } + + /** + * Creates a VectorPosition from a Bukkit Location, using absolute coordinates. + * + * @param location The Bukkit Location to convert. + * @return A new VectorPosition instance representing the given location. + * + * @since 5.3 + */ + public static VectorPosition ofLocation(Location location) { + return new VectorPosition( + PositionNumber.ofAbsolute(location.getX()), + PositionNumber.ofAbsolute(location.getY()), + PositionNumber.ofAbsolute(location.getZ()) + ); + } + + /** + * Parses a VectorPosition from a string representation. + * The expected format is "<x>,<y>,<z>" for absolute or relative coordinates. + *
+ * Relative coordinates can be specified using the '~' prefix, e.g., "~10,~,~-10". + * + * @param coordStr The string representation of the coordinates. + * @return A new VectorPosition instance parsed from the string. + * @throws PositionParseException If the string format is invalid. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public static VectorPosition fromString(String coordStr) throws PositionParseException { + String[] parts = REPatterns.COMMA.split(coordStr); + if (parts.length != 3) { + throw new PositionParseException(Message.of(MVCorei18n.EXCEPTION_POSITIONPARSE_INVALIDCOORDINATES, + replace("{format}").with(coordStr))); + } + return new VectorPosition( + PositionNumber.fromString(parts[0]), + PositionNumber.fromString(parts[1]), + PositionNumber.fromString(parts[2]) + ); + } + + private final PositionNumber x; + private final PositionNumber y; + private final PositionNumber z; + + /** + * Creates a new VectorPosition with the specified PositionNumbers for x, y, and z. + * + * @param x The PositionNumber for the x coordinate. + * @param y The PositionNumber for the y coordinate. + * @param z The PositionNumber for the z coordinate. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public VectorPosition(PositionNumber x, PositionNumber y, PositionNumber z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Gets the PositionNumber for the x coordinate. + * + * @return The PositionNumber representing the x coordinate. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public PositionNumber getX() { + return x; + } + + /** + * Gets the PositionNumber for the y coordinate. + * + * @return The PositionNumber representing the y coordinate. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public PositionNumber getY() { + return y; + } + + /** + * Gets the PositionNumber for the z coordinate. + * + * @return The PositionNumber representing the z coordinate. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public PositionNumber getZ() { + return z; + } + + /** + * Augments the given Bukkit Vector in place by applying this VectorPosition's coordinates. + * Relative coordinates will adjust the existing values, while absolute coordinates will set them directly. + * + * @param base The Bukkit Vector to augment. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public void augmentBukkitVector(Vector base) { + base.setX(x.getValue(base.getX())); + base.setY(y.getValue(base.getY())); + base.setZ(z.getValue(base.getZ())); + } + + /** + * Augments the given Bukkit Location in place by applying this VectorPosition's coordinates. + * Relative coordinates will adjust the existing values, while absolute coordinates will set them directly. + * + * @param location The Bukkit Location to augment. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public void augmentBukkitLocation(Location location) { + location.setX(x.getValue(location.getX())); + location.setY(y.getValue(location.getY())); + location.setZ(z.getValue(location.getZ())); + } + + /** + * Converts this VectorPosition to a new Bukkit Vector based on a given base Vector. + * This does not modify the base Vector, but returns a new Vector instance. + * + * @param base The base Bukkit Vector to use as a reference for relative positioning as required. + * @return A new Bukkit Vector representing this VectorPosition. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public Vector toBukkitVector(Vector base) { + return new Vector( + x.getValue(base.getX()), + y.getValue(base.getY()), + z.getValue(base.getZ()) + ); + } + + /** + * Converts this VectorPosition to a new Bukkit Location based on a given base Location. + * This does not modify the base Location, but returns a new Location instance. + * + * @param base The base Bukkit Location to use as a reference for relative positioning as required. + * @return A new Bukkit Location representing this VectorPosition. + * + * @since 5.3 + */ + @ApiStatus.AvailableSince("5.3") + public Location toBukkitLocation(Location base) { + return new Location( + base.getWorld(), + x.getValue(base.getX()), + y.getValue(base.getY()), + z.getValue(base.getZ()), + base.getYaw(), + base.getPitch() + ); + } + + @Override + public String toString() { + return x + "," + y + "," + z; + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/location/UnloadedWorldLocation.java b/src/main/java/org/mvplugins/multiverse/core/world/location/UnloadedWorldLocation.java index 838e8c8c4..449ac5d0f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/location/UnloadedWorldLocation.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/location/UnloadedWorldLocation.java @@ -22,13 +22,13 @@ * This is useful to store location with world that may not be loaded yet or have been unloaded at some point. */ @SerializableAs("UnloadedWorldLocation") -public final class UnloadedWorldLocation extends Location { +public class UnloadedWorldLocation extends Location { public static UnloadedWorldLocation fromLocation(@NotNull Location location) { return new UnloadedWorldLocation(location.getWorld(), location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch()); } - private String worldName; + private @Nullable String worldName; public UnloadedWorldLocation(@Nullable String worldName, double x, double y, double z) { super(null, x, y, z); @@ -67,7 +67,7 @@ public void setWorldName(@Nullable String worldName) { this.worldName = worldName; } - public String getWorldName() { + public @Nullable String getWorldName() { return worldName; } @@ -160,10 +160,10 @@ public boolean equals(Object obj) { @Override public int hashCode() { int hash = 3; - hash = 19 * hash + worldName.hashCode(); - hash = 19 * hash + (int) (Double.doubleToLongBits(this.getX()) ^ (Double.doubleToLongBits(this.getX()) >>> 32)); - hash = 19 * hash + (int) (Double.doubleToLongBits(this.getY()) ^ (Double.doubleToLongBits(this.getY()) >>> 32)); - hash = 19 * hash + (int) (Double.doubleToLongBits(this.getZ()) ^ (Double.doubleToLongBits(this.getZ()) >>> 32)); + hash = 19 * hash + String.valueOf(worldName).hashCode(); + hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getX())); + hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getY())); + hash = 19 * hash + Long.hashCode(Double.doubleToLongBits(this.getZ())); hash = 19 * hash + Float.floatToIntBits(this.getPitch()); hash = 19 * hash + Float.floatToIntBits(this.getYaw()); return hash; @@ -171,7 +171,7 @@ public int hashCode() { @Override public String toString() { - return "Location{" + + return "UnloadedWorldLocation{" + "world=" + worldName + ",x=" + getX() + ",y=" + getY() + diff --git a/src/main/resources/multiverse-core_en.properties b/src/main/resources/multiverse-core_en.properties index 18c72f9c5..7dbe70cb8 100644 --- a/src/main/resources/multiverse-core_en.properties +++ b/src/main/resources/multiverse-core_en.properties @@ -255,7 +255,7 @@ mv-core.destination.cannon.failurereason.invalidformat=&cCannon destination form mv-core.destination.exact.failurereason.invalidformat=&cExact destination format is: &6e:worldname:x,y,z:pitch:yaw mv-core.destination.player.failurereason.playernotfound=&cPlayer '&6{player}&c' does not exist or is not online! mv-core.destination.shared.failurereason.invalidcoordinatesformat=You must specify a valid coordinate in this format: &6{x},{y},{z} -mv-core.destination.shared.failurereason.invalidnumberformat=Value is not a valid number. {error}. +mv-core.destination.shared.failurereason.invalidnumberformat=&cInvalid destination input. {error} mv-core.destination.shared.failurereason.worldnotfound=&cWorld '&6{world}&c' does not exist or is not loaded! mv-core.destination.parse.failurereason.invaliddestinationid=Invalid destination id: {id}. Valid destination ids are: {ids} @@ -327,6 +327,11 @@ mv-core.exception.multiverseworld.unloaddefaultworld=&cYou can't unload the defa mv-core.exception.multiverseworld.unloadplayersinworld=&cThere are still &6{count}&c player(s) in the world! Use '&6--remove-players&c' flag to your command to teleport all players out of the world. mv-core.exception.multiverseworld.unloaderror=&cAn unknown error occurred while unloading world: &6{world}&c.\n&cSee console for more details. +# multiverse position parse exception +mv-core.exception.positionparse.invaliddirection=&cInvalid direction string format: {format}. Expected format: : +mv-core.exception.positionparse.invalidcoordinates=&cInvalid coordinates format: {format}. Expected format: ,, +mv-core.exception.positionparse.invalidnumber=&cInvalid number format: {number}. Expects a numeric value. + # generic mv-core.generic.success=Success! mv-core.generic.failure=Failed! diff --git a/src/test/java/org/mvplugins/multiverse/core/destination/DestinationTest.kt b/src/test/java/org/mvplugins/multiverse/core/destination/DestinationTest.kt index d079d9ab5..ab0fb47ac 100644 --- a/src/test/java/org/mvplugins/multiverse/core/destination/DestinationTest.kt +++ b/src/test/java/org/mvplugins/multiverse/core/destination/DestinationTest.kt @@ -69,6 +69,22 @@ class DestinationTest : TestWithMockBukkit() { assertEquals("e:world:1.2,2.0,3.0:10.5:9.5", destination.toString()) } + @Test + fun `Exact destination instance with relative position`() { + val destination = destinationsProvider.parseDestination("e:world:~1.5,2,~-3:10.5:~9.5").orNull + assertTrue(destination is ExactDestinationInstance) + val expectedLocation = Location( + world.bukkitWorld.orNull, + player.location.x + 1.5, + 2.0, + player.location.z - 3.0, + player.location.yaw + 9.5F, + 10.5F + ) + assertLocationEquals(expectedLocation, destination.getLocation(player).orNull) + assertEquals("e:world:~1.5,2.0,~-3.0:10.5:~9.5", destination.toString()) + } + @Test fun `Exact destination instance from location`() { val exactDestination = serviceLocator.getActiveService(ExactDestination::class.java).takeIf { it != null } ?: run { @@ -123,6 +139,7 @@ class DestinationTest : TestWithMockBukkit() { assertTrue(destinationsProvider.parseDestination("b:invalid-bed").isFailure) assertTrue(destinationsProvider.parseDestination("ca:invalid-cannon").isFailure) assertTrue(destinationsProvider.parseDestination("e:world:1,2,x").isFailure) + assertTrue(destinationsProvider.parseDestination("e:world:1,2,3:").isFailure) assertTrue(destinationsProvider.parseDestination("pl:invalid-player").isFailure) assertTrue(destinationsProvider.parseDestination("w:invalid-world").isFailure) // todo: should we make invalid yaw for WorldDestination fail?