package com.supaham.commons.bukkit.utils;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.supaham.commons.utils.NumberUtils.roundExact;
import static com.supaham.commons.utils.StringUtils.checkNotNullOrEmpty;
import static java.lang.Double.parseDouble;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.supaham.commons.utils.RandomUtils;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.util.Vector;
import java.util.Random;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.supaham.commons.minecraft.world.space.Position;
/**
* Utility methods for working with {@link Location} instances. This class contains methods such as
* {@link #deserialize(String)}, {@link #serialize(Location)}, and more.
*
* @since 0.1
*/
public class LocationUtils {
/**
* Deserializes a {@link String} to represent a {@link Location}. <p>
* LocationUtils.deserialize("world 123.0 64.0 124.5") = {@link Location}(world, 123.0D, 64.0D,
* 124.5D) <br />
*
* LocationUtils.deserialize("world 123.0 64.0 124.5 1.2") = {@link Location}(world, 123.0D,
* 64.0D, 124.5D, 1.2F)<br />
*
* LocationUtils.deserialize("world 123.0 64.0 124.5 1.2 20") = {@link Location}(world, 123.0D,
* 64.0D, 124.5D, 1.2F, 20F)<br />
*
* LocationUtils.deserialize("world 123.0 64.0") = {@link IllegalArgumentException} too few args
* <br />
*
* LocationUtils.deserialize("world 123.0 64.0 124.5 1.2 20 66") = {@link
* IllegalArgumentException} too many args <br /> </p>
*
* @param string string representing to deserialize
*
* @return returns the deserialized {@link Location}
*
* @throws NullPointerException thrown if the world in the {@code string} is null
* @throws IllegalArgumentException thrown if the {@code string} is in the incorrect format
*/
@Nonnull
public static Location deserialize(@Nonnull String string) throws NullPointerException {
checkNotNullOrEmpty(string);
String[] split = string.split("\\s+|,");
checkArgument(split.length >= 4 && split.length <= 6, "string is in an invalid format.");
World world = Bukkit.getWorld(split[0]);
checkNotNull(world, "world '" + split[0] + "' doesn't exist.");
Location loc = new Location(world, parseDouble(split[1]), parseDouble(split[2]),
parseDouble(split[3]));
if (split.length > 4) {
loc.setYaw(Float.parseFloat(split[4]));
}
if (split.length > 5) {
loc.setPitch(Float.parseFloat(split[5]));
}
return loc;
}
/**
* Serializes a {@link Location} in the form of 'world x y z yaw pitch'. The x, y, and z
* coordinates are rounded to <em>two</em> decimal places. The yaw and the pitch are rounded to
* <em>three</em> decimal places. If the yaw and/or pitch are true but their values are ZERO,
* they
* will be set to false, removing it from the serialized string that is returned. <p/>
* <b>NOTE</b>: This is equivalent to calling {@code serialize(Location, true)}
*
* @param location location to serialize
*
* @return serialized {@code location}
*
* @see #serialize(Location, boolean)
*/
public static String serialize(Location location) {
return serialize(location, true);
}
/**
* Serializes a {@link Location} in the form of 'world x y z yaw pitch'. The x, y, and z
* coordinates are rounded to <em>two</em> decimal places. The yaw and the pitch are rounded to
* <em>three</em> decimal places. If the yaw and/or pitch are true but their values are ZERO,
* they
* will be set to false, removing it from the serialized string that is returned. <p/>
* <b>NOTE</b>: This is equivalent to calling {@code serialize(Location, direction, direction)}
*
* @param location location to serialize
* @param direction whether to store the direction (yaw and pitch)
*
* @return serialized {@code location}
*
* @see #serialize(Location, boolean, boolean)
*/
public static String serialize(Location location, boolean direction) {
return serialize(location, direction, direction);
}
/**
* Serializes a {@link Location} in the form of 'world x y z yaw pitch'. The x, y, and z
* coordinates are rounded to <em>two</em> decimal places. The yaw and the pitch are rounded to
* <em>three</em> decimal places. If the yaw and/or pitch are true but their values are ZERO,
* they
* will be set to false, removing it from the serialized string that is returned.
*
* @param location location to serialize
* @param yaw whether to store the yaw, keep in mind this will be forcefully set to true if the
* {@code pitch} is true
* @param pitch whether to store the pitch
*
* @return serialized {@code location}
*/
public static String serialize(Location location, boolean yaw, boolean pitch) {
pitch = pitch && location.getPitch() > 0;
yaw = pitch || (yaw && location.getYaw() > 0);
return location.getWorld().getName() + ","
+ VectorUtils.serialize(location.toVector()) // x y z
+ (yaw ? "," + roundExact(3, location.getYaw()) : "")
+ (pitch ? "," + roundExact(3, location.getPitch()) : "");
}
/**
* Returns a new random {@link Location} that is within two given Locations. This is equivalent
* to
* calling #getRandomLocationWithin(Location, Location, boolean) with the boolean as false.
*
* @param min minimum location of a cuboid region
* @param max maximum location of a cuboid region
*
* @return a pseudorandom location
*
* @see #getRandomLocationWithin(Location, Location, boolean)
*/
public static Location getRandomLocationWithin(Location min, Location max) {
return getRandomLocationWithin(min, max, false);
}
/**
* Returns a new random {@link Location} that is within two given Locations. This is equivalent
* to
* calling #getRandomLocationWithin(Random, Location, Location, boolean) using {@link
* RandomUtils#getRandom()} and the boolean as false.
*
* @param min minimum location of a cuboid region
* @param max maximum location of a cuboid region
* @param highestBlock whether to immediately call {@link World#getHighestBlockAt(Location)} to
* attach the pseudorandom location to ground
*
* @return a pseudorandom location
*
* @see #getRandomLocationWithin(Random, Location, Location, boolean)
*/
public static Location getRandomLocationWithin(@Nonnull Location min, @Nonnull Location max,
boolean highestBlock) {
return getRandomLocationWithin(RandomUtils.getRandom(), min, max, highestBlock);
}
/**
* Returns a new random {@link Location} that is within two given Locations. This is equivalent
* to
* calling #getRandomLocationWithin(Random, Location, Location, boolean) with the boolean as
* false.
*
* @param random random instance to use
* @param min minimum location of a cuboid region
* @param max maximum location of a cuboid region
*
* @return a pseudorandom location
*
* @see #getRandomLocationWithin(Random, Location, Location, boolean)
*/
public static Location getRandomLocationWithin(@Nonnull Random random, @Nonnull Location min,
@Nonnull Location max) {
return getRandomLocationWithin(random, min, max, false);
}
/**
* Returns a new random {@link Location} that is within two given Locations.
*
* @param random random instance to use
* @param min minimum location of a cuboid region
* @param max maximum location of a cuboid region
* @param highestBlock whether to immediately call {@link World#getHighestBlockAt(Location)} to
* attach the pseudorandom location to ground
*
* @return a pseudorandom location
*/
public static Location getRandomLocationWithin(@Nonnull Random random, @Nonnull Location min,
@Nonnull Location max, boolean highestBlock) {
checkNotNull(random, "random cannot be null.");
checkNotNull(min, "min cannot be null.");
checkNotNull(max, "max cannot be null.");
checkArgument(min.getWorld().equals(max.getWorld()), "min and max worlds don't match.");
Location loc = new Location(min.getWorld(),
RandomUtils.nextInt(min.getBlockX(), max.getBlockX()),
RandomUtils.nextInt(min.getBlockY(), max.getBlockY()),
RandomUtils.nextInt(min.getBlockZ(), max.getBlockZ()));
return !highestBlock ? loc : loc.getWorld().getHighestBlockAt(loc).getLocation();
}
/**
* Returns a new {@link Location} where at least the specified required amount of space is
* available above it. This method works its way from bottom to top, starting from the given
* location's y coordinate and all the way to the location's world's max height value.
*
* @param location location to start from
* @param required required amount of space
*
* @return a location with the required free space
*/
public static Location getFreeLocation(@Nonnull Location location, double required) {
checkNotNull(location, "location cannot be null.");
checkArgument(required > 0, "required space must be larger than 0.");
location = location.clone();
if (location.getBlockY() >= location.getWorld().getMaxHeight()) {
return location;
}
World world = location.getWorld();
int y = location.getBlockY();
int free = 0;
while (y < world.getMaxHeight()) {
if (!world.getBlockAt(location.getBlockX(), y, location.getBlockZ()).getType().isSolid()) {
free++;
} else {
free = 0;
}
if (free >= required) {
break;
}
y++;
}
location.setY((double) y - required);
return location;
}
/**
* Checks if two {@link Location} instances are within the same block. If both of them are null,
* true is returned. If either one is null, false is returned.
*
* @param l1 first location to match with {@code l2}
* @param l2 second location to match with {@code l1}
*
* @return whether both locations are within the same block
*
* @see VectorUtils#isSameBlock(Vector, Vector)
*/
public static boolean isSameBlock(@Nullable Location l1, @Nullable Location l2) {
return VectorUtils.isSameBlock(l1 != null ? l1.toVector() : null,
l2 != null ? l2.toVector() : null);
}
/**
* Checks if two {@link Location} instances are the same coordinates. If both of them are null,
* true is returned. If either one is null, false is returned.
*
* @param l1 first location to match with {@code l2}, nullable
* @param l2 second location to match with {@code l1}, nullable
*
* @return whether both locations are the same coordinates
*/
public static boolean isSameCoordinates(@Nullable Location l1, @Nullable Location l2) {
if (l1 == null && l2 == null) {
return true;
} else if (l1 == null || l2 == null) {
return false;
}
return l1.getX() == l2.getX() && l1.getY() == l2.getY() && l1.getZ() == l2.getZ();
}
/**
* Gets a {@link Function} which converts a {@link Location} into {@link Vector} by calling
* {@link Location#toVector()}.
*
* @return function
*/
public static Function<Location, Vector> toVectorFunction() {
return ToVectorFunction.INSTANCE;
}
/**
* Convert {@link Vector} to a {@link Location}.
*
* @param vector vector to use
* @param world world to use
*
* @return Location
*/
public static Location toLocation(@Nonnull com.supaham.commons.minecraft.world.space.Vector vector, World world) {
Preconditions.checkNotNull(vector, "vector cannot be null.");
return new Location(world, vector.getX(), vector.getY(), vector.getZ());
}
/**
* Convert {@link Position} to a {@link Location}.
*
* @param position position to use
* @param world world to use
*
* @return Location
*/
public static Location toLocation(@Nonnull Position position, World world) {
Preconditions.checkNotNull(position, "position cannot be null.");
return new Location(world, position.getX(), position.getY(), position.getZ(), position.getYaw(),
position.getPitch());
}
private static final class ToVectorFunction implements Function<Location, Vector> {
public static final ToVectorFunction INSTANCE = new ToVectorFunction();
@Nullable
@Override
public Vector apply(@Nullable Location input) {
return input == null ? null : input.toVector();
}
}
}