diff --git a/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java b/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java
new file mode 100644
index 000000000..e63644ac2
--- /dev/null
+++ b/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/BukkitFuture.java
@@ -0,0 +1,111 @@
+package mineplex.core.common.util;
+
+import org.bukkit.Bukkit;
+import org.bukkit.plugin.Plugin;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Utilities for interleaving Bukkit scheduler operations as
+ * intermediate and terminal operations in a {@link CompletionStage}
+ * pipeline.
+ *
+ * Any {@link Function}s returned by methods are suitable for use
+ * in {@link CompletionStage#thenCompose(Function)}
+ *
+ * @see CompletableFuture#thenCompose(Function)
+ */
+public class BukkitFuture
+{
+ private static final Plugin LOADING_PLUGIN = JavaPlugin.getProvidingPlugin(BukkitFuture.class);
+
+ private static void runBlocking(Runnable action)
+ {
+ Bukkit.getScheduler().runTask(LOADING_PLUGIN, action);
+ }
+
+ /**
+ * Finalize a {@link CompletionStage} by consuming its value
+ * on the main thread.
+ *
+ * @param action the {@link Consumer} to call on the main thread
+ * @return a {@link Function} to be passed as an argument to
+ * {@link CompletionStage#thenCompose(Function)}
+ * @see CompletableFuture#thenCompose(Function)
+ */
+ public static Function> accept(Consumer super T> action)
+ {
+ return val ->
+ {
+ CompletableFuture future = new CompletableFuture<>();
+ runBlocking(() ->
+ {
+ action.accept(val);
+ future.complete(null);
+ });
+ return future;
+ };
+ }
+
+ /**
+ * Finalize a {@link CompletionStage} by executing code on the
+ * main thread after its completion.
+ *
+ * @param action the {@link Runnable} that will execute
+ * @return a {@link Function} to be passed as an argument to
+ * {@link CompletionStage#thenCompose(Function)}
+ * @see CompletableFuture#thenCompose(Function)
+ */
+ public static Function> run(Runnable action)
+ {
+ return val ->
+ {
+ CompletableFuture future = new CompletableFuture<>();
+ runBlocking(() ->
+ {
+ action.run();
+ future.complete(null);
+ });
+ return future;
+ };
+ }
+
+ /**
+ * Transform a value contained within a {@link CompletionStage}
+ * by executing a mapping {@link Function} on the main thread.
+ *
+ * @param fn the {@link Function} used to transform the value
+ * @return a {@link Function} to be passed as an argument to
+ * {@link CompletionStage#thenCompose(Function)}
+ * @see CompletableFuture#thenCompose(Function)
+ */
+ public static Function> map(Function super T,? extends U> fn)
+ {
+ return val ->
+ {
+ CompletableFuture future = new CompletableFuture<>();
+ runBlocking(() -> future.complete(fn.apply(val)));
+ return future;
+ };
+ }
+
+ /**
+ * Create a {@link CompletionStage} from a supplier executed on the
+ * main thread.
+ *
+ * @param supplier the supplier to run on the main thread
+ * @return a {@link CompletionStage} whose value will be supplied
+ * during the next Minecraft tick
+ */
+ public static CompletionStage supply(Supplier supplier)
+ {
+ CompletableFuture future = new CompletableFuture<>();
+ runBlocking(() -> future.complete(supplier.get()));
+ return future;
+ }
+}
diff --git a/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/UtilWorld.java b/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/UtilWorld.java
index cef329774..fe44de9c8 100644
--- a/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/UtilWorld.java
+++ b/Plugins/Mineplex.Core.Common/src/mineplex/core/common/util/UtilWorld.java
@@ -10,6 +10,8 @@ import org.bukkit.World.Environment;
import org.bukkit.WorldBorder;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
+import org.bukkit.craftbukkit.v1_8_R3.entity.CraftEntity;
+import org.bukkit.entity.Entity;
import org.bukkit.util.Vector;
import com.google.common.collect.Lists;
@@ -28,7 +30,12 @@ public class UtilWorld
if (chunk == null)
return "";
- return chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ();
+ return chunkToStr(chunk.getWorld().getName(), chunk.getX(), chunk.getZ());
+ }
+
+ public static String chunkToStr(String world, int x, int z)
+ {
+ return world + "," + x + "," + z;
}
public static String chunkToStrClean(Chunk chunk)
@@ -289,4 +296,15 @@ public class UtilWorld
return startX >= minX && startZ <= maxX && endX >= minZ && endZ <= maxZ;
}
+ public static double distanceSquared(Entity a, Entity b)
+ {
+ if (a.getWorld() != b.getWorld())
+ throw new IllegalArgumentException("Different worlds: " + a.getWorld().getName() + " and " + b.getWorld().getName());
+ net.minecraft.server.v1_8_R3.Entity entityA = ((CraftEntity) a).getHandle();
+ net.minecraft.server.v1_8_R3.Entity entityB = ((CraftEntity) b).getHandle();
+ double dx = entityA.locX - entityB.locX;
+ double dy = entityA.locY - entityB.locY;
+ double dz = entityA.locZ - entityB.locZ;
+ return (dx * dx) + (dy * dy) + (dz * dz);
+ }
}
diff --git a/Plugins/Mineplex.Core/src/mineplex/core/account/CoreClientManager.java b/Plugins/Mineplex.Core/src/mineplex/core/account/CoreClientManager.java
index 69a745818..f2c320b74 100644
--- a/Plugins/Mineplex.Core/src/mineplex/core/account/CoreClientManager.java
+++ b/Plugins/Mineplex.Core/src/mineplex/core/account/CoreClientManager.java
@@ -491,7 +491,7 @@ public class CoreClientManager extends MiniPlugin
}
}
- @EventHandler(priority = EventPriority.HIGHEST)
+ @EventHandler(priority = EventPriority.MONITOR)
public void Quit(PlayerQuitEvent event)
{
// When an account is logged in to this server and the same account name logs in
diff --git a/Plugins/Mineplex.Core/src/mineplex/core/creature/Creature.java b/Plugins/Mineplex.Core/src/mineplex/core/creature/Creature.java
index f5bc05d1b..e892c9f4d 100644
--- a/Plugins/Mineplex.Core/src/mineplex/core/creature/Creature.java
+++ b/Plugins/Mineplex.Core/src/mineplex/core/creature/Creature.java
@@ -103,6 +103,11 @@ public class Creature extends MiniPlugin
event.setDroppedExp(0);
List drops = event.getDrops();
+ if (event.getEntity().hasMetadata("Creature.DoNotDrop"))
+ {
+ drops.clear();
+ return;
+ }
if (event.getEntityType() == EntityType.PLAYER)
drops.add(ItemStackFactory.Instance.CreateStack(Material.BONE, 1));
diff --git a/Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java b/Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java
new file mode 100644
index 000000000..e0148bd0a
--- /dev/null
+++ b/Plugins/Mineplex.Core/src/mineplex/core/database/PlayerKeyValueRepository.java
@@ -0,0 +1,291 @@
+package mineplex.core.database;
+
+import com.google.common.collect.ImmutableMap;
+import mineplex.serverdata.database.DBPool;
+
+import java.sql.*;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A SQL-backed repository supporting {@link String} keys and
+ * values of type {@link V}
+ *
+ * Each java primitive (sans char) and String are supported by default.
+ * Serializing functions for any additional types can be supplied
+ * to {@link PlayerKeyValueRepository(String, Serializer, Deserializer)}.
+ * For example, if {@link String} was not supported, one could use:
+ *
+ * {@code new PlayerKeyValueRepository("tableName", PreparedStatement::setString, ResultSet::getString, "VARCHAR(255)")}
+ *
+ * NOTE: EACH CONSTRUCTOR IS BLOCKING, and initializes a backing table
+ * if one does not yet exist
+ *
+ * @param The value type to use for this repository
+ */
+public class PlayerKeyValueRepository
+{
+ private static final ImmutableMap, ValueMapper>> PRIM_MAPPERS = ImmutableMap., ValueMapper>>builder()
+ .put(String.class, new ValueMapper<>(PreparedStatement::setString, ResultSet::getString, "VARCHAR(255)"))
+ .put(Boolean.class, new ValueMapper<>(PreparedStatement::setBoolean, ResultSet::getBoolean, "BOOL"))
+ .put(Byte.class, new ValueMapper<>(PreparedStatement::setByte, ResultSet::getByte, "TINYINT"))
+ .put(Short.class, new ValueMapper<>(PreparedStatement::setShort, ResultSet::getShort, "SMALLINT"))
+ .put(Integer.class, new ValueMapper<>(PreparedStatement::setInt, ResultSet::getInt, "INTEGER"))
+ .put(Long.class, new ValueMapper<>(PreparedStatement::setLong, ResultSet::getLong, "BIGINT"))
+ .put(Float.class, new ValueMapper<>(PreparedStatement::setFloat, ResultSet::getFloat, "REAL"))
+ .put(Double.class, new ValueMapper<>(PreparedStatement::setDouble, ResultSet::getDouble, "DOUBLE"))
+ .build();
+ private final String _tableName;
+ private final ValueMapper _mapper;
+
+ /**
+ * Build a PlayerKeyValueRepository with the given class'
+ * built-in deserializer.
+ *
+ * @param tableName the underlying table's name
+ * @param clazz the type of values to used
+ * @throws IllegalArgumentException if the provided class isn't a supported type
+ */
+ @SuppressWarnings("unchecked") // java's generics are garbage.
+ public PlayerKeyValueRepository(String tableName, Class clazz) // we could infer the type parameter at runtime, but it's super ugly
+ {
+ this(tableName, (ValueMapper) PRIM_MAPPERS.get(clazz));
+ }
+
+ /**
+ * Build a PlayerKeyValueRepository with an explicit deserializer.
+ * This is the constructor to use if the type you're deserializing
+ * isn't supported by default.
+ *
+ * @param tableName the underlying table's name
+ * @param serializer the serializing function used to insert values
+ * @param deserializer the deserializing function used to retrieve
+ * values
+ * @param columnDef the value type's SQL datatype declaration, e.g., {@code "VARCHAR(255)"} for Strings.
+ */
+ public PlayerKeyValueRepository(String tableName, Serializer serializer, Deserializer deserializer, String columnDef)
+ {
+ this(tableName, new ValueMapper(serializer, deserializer, columnDef));
+ }
+
+ private PlayerKeyValueRepository(String tableName, ValueMapper mapper)
+ {
+ this._tableName = tableName;
+ this._mapper = mapper;
+
+ // Create a table to back this repository
+ try (Connection conn = DBPool.getAccount().getConnection())
+ {
+ Statement stmt = conn.createStatement();
+ stmt.executeUpdate("CREATE TABLE IF NOT EXISTS " + _tableName + "("
+ + "accountId INT NOT NULL,"
+ + "kvKey VARCHAR(255) NOT NULL,"
+ + "kvValue " + _mapper._columnDef + ","
+ + "PRIMARY KEY (accountId,kvKey),"
+ + "INDEX acc_ind (accountId),"
+ + "FOREIGN KEY (accountId) REFERENCES accounts(id) ON DELETE NO ACTION ON UPDATE NO ACTION"
+ + ")");
+ }
+ catch (SQLException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Get all value for a player's key
+ *
+ * @param uuid the {@link UUID} of the player
+ * @return a CompletableFuture containing all key/value pairs
+ * associated with the player
+ */
+ public CompletableFuture get(UUID uuid, String key)
+ {
+ return CompletableFuture.supplyAsync(() ->
+ {
+ try (Connection conn = DBPool.getAccount().getConnection())
+ {
+ PreparedStatement stmt = conn.prepareStatement("SELECT kvValue FROM " + _tableName + " WHERE accountId = (SELECT id FROM accounts WHERE uuid=?) AND kvKey=?");
+ stmt.setString(1, uuid.toString());
+ stmt.setString(2, key);
+
+ ResultSet set = stmt.executeQuery();
+ if (set.next())
+ {
+ return _mapper._deserializer.read(set, 1);
+ }
+ return null;
+ } catch (SQLException ignored) {}
+
+ return null; // yuck
+ });
+ }
+
+ /**
+ * Get all key/value pairs for a player
+ *
+ * @param uuid the {@link UUID} of the player
+ * @return a CompletableFuture containing all key/value pairs
+ * associated with the player
+ */
+ public CompletableFuture